diff --git a/internal/forc/VERSION b/internal/forc/VERSION index 1e3540c7961..3b5664f6782 100644 --- a/internal/forc/VERSION +++ b/internal/forc/VERSION @@ -1 +1 @@ -0.68.7 +git:xunilrj/dynamic-types-configurables diff --git a/packages/abi-coder/src/Interface.ts b/packages/abi-coder/src/Interface.ts index d2172d569fd..2d45add94e1 100644 --- a/packages/abi-coder/src/Interface.ts +++ b/packages/abi-coder/src/Interface.ts @@ -5,7 +5,7 @@ import { arrayify } from '@fuel-ts/utils'; import { AbiCoder } from './AbiCoder'; import { FunctionFragment } from './FunctionFragment'; -import type { DecodedValue, InputValue } from './encoding/coders/AbstractCoder'; +import type { Coder, DecodedValue, InputValue } from './encoding/coders/AbstractCoder'; import type { JsonAbiArgument, JsonAbiOld } from './types/JsonAbi'; import type { Configurable, JsonAbi } from './types/JsonAbiNew'; import { type EncodingVersion } from './utils/constants'; @@ -88,26 +88,23 @@ export class Interface { }); } - encodeType(concreteTypeId: string, value: InputValue): Uint8Array { + getCoder(concreteTypeId: string): Coder { const typeArg = parseConcreteType( this.jsonAbi, this.jsonAbiOld.types, concreteTypeId, '' ) as JsonAbiArgument; - return AbiCoder.encode(this.jsonAbiOld, typeArg, value, { - encoding: this.encoding, - }); + return AbiCoder.getCoder(this.jsonAbiOld, typeArg, { encoding: this.encoding }); } - decodeType(concreteTypeId: string, data: Uint8Array): [DecodedValue | undefined, number] { - const typeArg = parseConcreteType( - this.jsonAbi, - this.jsonAbiOld.types, - concreteTypeId, - '' - ) as JsonAbiArgument; + encodeType(concreteTypeId: string, value: InputValue): Uint8Array { + const coder = this.getCoder(concreteTypeId); + return coder.encode(value); + } - return AbiCoder.decode(this.jsonAbiOld, typeArg, data, 0, { encoding: this.encoding }); + decodeType(concreteTypeId: string, data: Uint8Array): [DecodedValue | undefined, number] { + const coder = this.getCoder(concreteTypeId); + return coder.decode(data, 0) as [DecodedValue | undefined, number]; } } diff --git a/packages/abi-coder/src/types/JsonAbi.ts b/packages/abi-coder/src/types/JsonAbi.ts index eed9fee4d46..80b586cde9e 100644 --- a/packages/abi-coder/src/types/JsonAbi.ts +++ b/packages/abi-coder/src/types/JsonAbi.ts @@ -54,4 +54,5 @@ export interface JsonAbiConfigurable { name: string; configurableType: JsonAbiArgument; offset: number; + indirect?: boolean; } diff --git a/packages/abi-coder/src/types/JsonAbiNew.ts b/packages/abi-coder/src/types/JsonAbiNew.ts index 036c13eaf92..bffa4e2ee75 100644 --- a/packages/abi-coder/src/types/JsonAbiNew.ts +++ b/packages/abi-coder/src/types/JsonAbiNew.ts @@ -99,6 +99,7 @@ export interface Configurable { readonly name: string; readonly concreteTypeId: string; readonly offset: number; + readonly indirect?: boolean; } export interface ErrorPosition { diff --git a/packages/abi-coder/src/utils/transpile-abi.ts b/packages/abi-coder/src/utils/transpile-abi.ts index 9f9c526053d..f2c96e5cc9b 100644 --- a/packages/abi-coder/src/utils/transpile-abi.ts +++ b/packages/abi-coder/src/utils/transpile-abi.ts @@ -123,6 +123,7 @@ export function transpileAbi(abi) { name: conf.name, configurableType: parseConcreteType(abi, types, conf.concreteTypeId), offset: conf.offset, + indirect: conf.indirect ?? false, })); // 5. loggedTypes diff --git a/packages/abi-typegen/src/types/interfaces/JsonAbi.ts b/packages/abi-typegen/src/types/interfaces/JsonAbi.ts index 2aaa51598ae..6ad97c28eed 100644 --- a/packages/abi-typegen/src/types/interfaces/JsonAbi.ts +++ b/packages/abi-typegen/src/types/interfaces/JsonAbi.ts @@ -55,6 +55,7 @@ export interface JsonAbiConfigurable { name: string; configurableType: JsonAbiArgument; offset: number; + indirect?: boolean; } export interface JsonAbiErrorCode { diff --git a/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts b/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts index 036c13eaf92..bffa4e2ee75 100644 --- a/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts +++ b/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts @@ -99,6 +99,7 @@ export interface Configurable { readonly name: string; readonly concreteTypeId: string; readonly offset: number; + readonly indirect?: boolean; } export interface ErrorPosition { diff --git a/packages/abi-typegen/src/utils/transpile-abi.ts b/packages/abi-typegen/src/utils/transpile-abi.ts index 9f9c526053d..f2c96e5cc9b 100644 --- a/packages/abi-typegen/src/utils/transpile-abi.ts +++ b/packages/abi-typegen/src/utils/transpile-abi.ts @@ -123,6 +123,7 @@ export function transpileAbi(abi) { name: conf.name, configurableType: parseConcreteType(abi, types, conf.concreteTypeId), offset: conf.offset, + indirect: conf.indirect ?? false, })); // 5. loggedTypes diff --git a/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs index 3fb509316be..58992d42b92 100644 --- a/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs +++ b/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs @@ -139,7 +139,6 @@ const abi = { ], "functions": [ { - "name": "main", "inputs": [ { "name": "x", @@ -150,6 +149,7 @@ const abi = { "concreteTypeId": "338a25cb65b9251663dcce6362b744fe10aa849758299590f4efed5dd299bf50" } ], + "name": "main", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null } @@ -175,8 +175,7 @@ const abi = { "offset": 2384, "indirect": false } - ], - "errorCodes": {} + ] }; const storageSlots: StorageSlot[] = []; diff --git a/packages/abi-typegen/test/fixtures/templates/contract/main.hbs b/packages/abi-typegen/test/fixtures/templates/contract/main.hbs index db2380781c2..40f5f5aa793 100644 --- a/packages/abi-typegen/test/fixtures/templates/contract/main.hbs +++ b/packages/abi-typegen/test/fixtures/templates/contract/main.hbs @@ -675,150 +675,149 @@ const abi = { ], "functions": [ { - "name": "alias_types_tuple_with_native_types", "inputs": [ { "name": "x", "concreteTypeId": "a95e1fcceb1451b8a76471f593f66c4a52ca04bde3c227c746ad7aaf988de5c6" } ], + "name": "alias_types_tuple_with_native_types", "output": "a95e1fcceb1451b8a76471f593f66c4a52ca04bde3c227c746ad7aaf988de5c6", "attributes": null }, { - "name": "type_address", "inputs": [ { "name": "x", "concreteTypeId": "f597b637c3b0f588fb8d7086c6f4735caa3122b85f0423b82e489f9bb58e2308" } ], + "name": "type_address", "output": "f597b637c3b0f588fb8d7086c6f4735caa3122b85f0423b82e489f9bb58e2308", "attributes": null }, { - "name": "type_contract_id", "inputs": [ { "name": "x", "concreteTypeId": "29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54" } ], + "name": "type_contract_id", "output": "29c10735d33b5159f0c71ee1dbd17b36a3e69e41f00fab0d42e1bd9f428d8a54", "attributes": null }, { - "name": "type_external_enum", "inputs": [ { "name": "x", "concreteTypeId": "d255d7621a80835aee6d3d0643eaec25fbb2c3215ed140fcf98ceffa69d895bc" } ], + "name": "type_external_enum", "output": "d255d7621a80835aee6d3d0643eaec25fbb2c3215ed140fcf98ceffa69d895bc", "attributes": null }, { - "name": "type_external_struct", "inputs": [ { "name": "x", "concreteTypeId": "d8b341b0e34229cd4e78f2683daadb4e8f32487b5a8d06420889e23530a0b3a5" } ], + "name": "type_external_struct", "output": "d8b341b0e34229cd4e78f2683daadb4e8f32487b5a8d06420889e23530a0b3a5", "attributes": null }, { - "name": "type_identity", "inputs": [ { "name": "x", "concreteTypeId": "ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335" } ], + "name": "type_identity", "output": "ab7cd04e05be58e3fc15d424c2c4a57f824a2a2d97d67252440a3925ebdc1335", "attributes": null }, { - "name": "types_array", "inputs": [ { "name": "x", "concreteTypeId": "2dc21094c0e9d81b843d1c1c308e2d60755d727d0f9b8981389845dd6d8686b2" } ], + "name": "types_array", "output": "2dc21094c0e9d81b843d1c1c308e2d60755d727d0f9b8981389845dd6d8686b2", "attributes": null }, { - "name": "types_asset_id", "inputs": [ { "name": "x", "concreteTypeId": "c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974" } ], + "name": "types_asset_id", "output": "c0710b6731b1dd59799cf6bef33eee3b3b04a2e40e80a0724090215bbf2ca974", "attributes": null }, { - "name": "types_b256", "inputs": [ { "name": "x", "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" } ], + "name": "types_b256", "output": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b", "attributes": null }, { - "name": "types_b512", "inputs": [ { "name": "x", "concreteTypeId": "745e252e80bec590efc3999ae943f07ccea4d5b45b00bb6575499b64abdd3322" } ], + "name": "types_b512", "output": "745e252e80bec590efc3999ae943f07ccea4d5b45b00bb6575499b64abdd3322", "attributes": null }, { - "name": "types_bool", "inputs": [ { "name": "x", "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903" } ], + "name": "types_bool", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null }, { - "name": "types_bytes", "inputs": [ { "name": "x", "concreteTypeId": "cdd87b7d12fe505416570c294c884bca819364863efe3bf539245fa18515fbbb" } ], + "name": "types_bytes", "output": "cdd87b7d12fe505416570c294c884bca819364863efe3bf539245fa18515fbbb", "attributes": null }, { - "name": "types_empty", "inputs": [ { "name": "x", "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" } ], + "name": "types_empty", "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", "attributes": null }, { - "name": "types_empty_then_value", "inputs": [ { "name": "x", @@ -829,231 +828,231 @@ const abi = { "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" } ], + "name": "types_empty_then_value", "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", "attributes": null }, { - "name": "types_enum", "inputs": [ { "name": "x", "concreteTypeId": "f097a9c7cabaa22d324d564b43210e927784b6f73609fe9e55900268b15910f5" } ], + "name": "types_enum", "output": "f097a9c7cabaa22d324d564b43210e927784b6f73609fe9e55900268b15910f5", "attributes": null }, { - "name": "types_enum_with_vector", "inputs": [ { "name": "x", "concreteTypeId": "11fe8db04751c1f2572544df9ac6b0659a5c159fecce1236148ed67ccb63f0a4" } ], + "name": "types_enum_with_vector", "output": "11fe8db04751c1f2572544df9ac6b0659a5c159fecce1236148ed67ccb63f0a4", "attributes": null }, { - "name": "types_evm_address", "inputs": [ { "name": "x", "concreteTypeId": "05a44d8c3e00faf7ed545823b7a2b32723545d8715d87a0ab3cf65904948e8d2" } ], + "name": "types_evm_address", "output": "05a44d8c3e00faf7ed545823b7a2b32723545d8715d87a0ab3cf65904948e8d2", "attributes": null }, { - "name": "types_generic_enum", "inputs": [ { "name": "x", "concreteTypeId": "ecbb748d034980313898a72ea8a896a31506cfef0f41883bffe50d0b73f0d693" } ], + "name": "types_generic_enum", "output": "ecbb748d034980313898a72ea8a896a31506cfef0f41883bffe50d0b73f0d693", "attributes": null }, { - "name": "types_generic_struct", "inputs": [ { "name": "x", "concreteTypeId": "1fcf420f386194ee1b5bd6d86346b554e2602516dee0b9f14d3a6a5e8b59ee10" } ], + "name": "types_generic_struct", "output": "1fcf420f386194ee1b5bd6d86346b554e2602516dee0b9f14d3a6a5e8b59ee10", "attributes": null }, { - "name": "types_option", "inputs": [ { "name": "x", "concreteTypeId": "2da102c46c7263beeed95818cd7bee801716ba8303dddafdcd0f6c9efda4a0f1" } ], + "name": "types_option", "output": "2da102c46c7263beeed95818cd7bee801716ba8303dddafdcd0f6c9efda4a0f1", "attributes": null }, { - "name": "types_option_geo", "inputs": [ { "name": "x", "concreteTypeId": "3597e0782bd4dbaf5c8025b40ff3a325845ee34caa713a6d664bda034a31d02a" } ], + "name": "types_option_geo", "output": "3597e0782bd4dbaf5c8025b40ff3a325845ee34caa713a6d664bda034a31d02a", "attributes": null }, { - "name": "types_raw_slice", "inputs": [ { "name": "x", "concreteTypeId": "1e1c7c52c1c7a9901681337f8669555f62aac58911332c9ff6b4ea8e73786570" } ], + "name": "types_raw_slice", "output": "1e1c7c52c1c7a9901681337f8669555f62aac58911332c9ff6b4ea8e73786570", "attributes": null }, { - "name": "types_result", "inputs": [ { "name": "x", "concreteTypeId": "b3131b4c08c16cfa55b3150d587c3afa3e4cdebe0399f3f599fa160baaa64e0c" } ], + "name": "types_result", "output": "9891b1ee451eed790368ea3969e3c8f550efa87de489b5d7b933e2290800791b", "attributes": null }, { - "name": "types_std_string", "inputs": [ { "name": "x", "concreteTypeId": "9a7f1d3e963c10e0a4ea70a8e20a4813d1dc5682e28f74cb102ae50d32f7f98c" } ], + "name": "types_std_string", "output": "9a7f1d3e963c10e0a4ea70a8e20a4813d1dc5682e28f74cb102ae50d32f7f98c", "attributes": null }, { - "name": "types_str", "inputs": [ { "name": "x", "concreteTypeId": "84877f6e98274b9e4721db68b4c0bdb9e52b8e9572c5bd7811c07a41ced882c7" } ], + "name": "types_str", "output": "84877f6e98274b9e4721db68b4c0bdb9e52b8e9572c5bd7811c07a41ced882c7", "attributes": null }, { - "name": "types_str_slice", "inputs": [ { "name": "x", "concreteTypeId": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a" } ], + "name": "types_str_slice", "output": "8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a", "attributes": null }, { - "name": "types_struct", "inputs": [ { "name": "x", "concreteTypeId": "392d58c694d2d91f3025f2bccfadacf2a105936f5da881b0899185d49f264522" } ], + "name": "types_struct", "output": "392d58c694d2d91f3025f2bccfadacf2a105936f5da881b0899185d49f264522", "attributes": null }, { - "name": "types_tuple", "inputs": [ { "name": "x", "concreteTypeId": "79239b6d6f2383e2cfbaf4da7fdf7ee7fb59b7bf517acfff2d9433e9e76e8fc4" } ], + "name": "types_tuple", "output": "79239b6d6f2383e2cfbaf4da7fdf7ee7fb59b7bf517acfff2d9433e9e76e8fc4", "attributes": null }, { - "name": "types_tuple_with_native_types", "inputs": [ { "name": "x", "concreteTypeId": "a95e1fcceb1451b8a76471f593f66c4a52ca04bde3c227c746ad7aaf988de5c6" } ], + "name": "types_tuple_with_native_types", "output": "a95e1fcceb1451b8a76471f593f66c4a52ca04bde3c227c746ad7aaf988de5c6", "attributes": null }, { - "name": "types_u16", "inputs": [ { "name": "x", "concreteTypeId": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef" } ], + "name": "types_u16", "output": "29881aad8730c5ab11d275376323d8e4ff4179aae8ccb6c13fe4902137e162ef", "attributes": null }, { - "name": "types_u256", "inputs": [ { "name": "x", "concreteTypeId": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e" } ], + "name": "types_u256", "output": "1b5759d94094368cfd443019e7ca5ec4074300e544e5ea993a979f5da627261e", "attributes": null }, { - "name": "types_u32", "inputs": [ { "name": "x", "concreteTypeId": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc" } ], + "name": "types_u32", "output": "d7649d428b9ff33d188ecbf38a7e4d8fd167fa01b2e10fe9a8f9308e52f1d7cc", "attributes": null }, { - "name": "types_u64", "inputs": [ { "name": "x", "concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0" } ], + "name": "types_u64", "output": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0", "attributes": null }, { - "name": "types_u8", "inputs": [ { "name": "x", "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" } ], + "name": "types_u8", "output": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b", "attributes": null }, { - "name": "types_value_then_empty", "inputs": [ { "name": "x", @@ -1064,11 +1063,11 @@ const abi = { "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" } ], + "name": "types_value_then_empty", "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", "attributes": null }, { - "name": "types_value_then_empty_then_value", "inputs": [ { "name": "x", @@ -1083,11 +1082,11 @@ const abi = { "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b" } ], + "name": "types_value_then_empty_then_value", "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", "attributes": null }, { - "name": "types_value_then_value_then_empty_then_empty", "inputs": [ { "name": "x", @@ -1106,47 +1105,47 @@ const abi = { "concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d" } ], + "name": "types_value_then_value_then_empty_then_empty", "output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d", "attributes": null }, { - "name": "types_vector_geo", "inputs": [ { "name": "x", "concreteTypeId": "82a718cca7f57f954a06e7f4016e97f8f5fe2e3544196604d7470c99c9627027" } ], + "name": "types_vector_geo", "output": "82a718cca7f57f954a06e7f4016e97f8f5fe2e3544196604d7470c99c9627027", "attributes": null }, { - "name": "types_vector_option", "inputs": [ { "name": "x", "concreteTypeId": "7433529a12c0ea55ba489b39a099c831a225e86a5d7319279912df84093dbdba" } ], + "name": "types_vector_option", "output": "7433529a12c0ea55ba489b39a099c831a225e86a5d7319279912df84093dbdba", "attributes": null }, { - "name": "types_vector_u8", "inputs": [ { "name": "x", "concreteTypeId": "27a0fb3d3a821e04e7a3f17ab6a617f0eb10f11e6eeb0f2c0ff9e6237207319e" } ], + "name": "types_vector_u8", "output": "27a0fb3d3a821e04e7a3f17ab6a617f0eb10f11e6eeb0f2c0ff9e6237207319e", "attributes": null } ], "loggedTypes": [], "messagesTypes": [], - "configurables": [], - "errorCodes": {} + "configurables": [] }; const storageSlots: StorageSlot[] = []; diff --git a/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs index ed6412e5b25..88bef1b6e99 100644 --- a/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs +++ b/packages/abi-typegen/test/fixtures/templates/predicate-with-configurable/main.hbs @@ -52,7 +52,6 @@ const abi = { "metadataTypes": [], "functions": [ { - "name": "main", "inputs": [ { "name": "fee", @@ -63,6 +62,7 @@ const abi = { "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b" } ], + "name": "main", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null } @@ -82,8 +82,7 @@ const abi = { "offset": 792, "indirect": false } - ], - "errorCodes": {} + ] }; const bytecode = decompressBytecode('0x-bytecode-here'); diff --git a/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs b/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs index d1422561286..db72da7067d 100644 --- a/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs +++ b/packages/abi-typegen/test/fixtures/templates/predicate/main.hbs @@ -246,7 +246,6 @@ const abi = { ], "functions": [ { - "name": "main", "inputs": [ { "name": "vec", @@ -265,14 +264,14 @@ const abi = { "concreteTypeId": "85dace7aaa469c8bb476be79ddec34883ef101b3cde470636f47e299bfcdc3da" } ], + "name": "main", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null } ], "loggedTypes": [], "messagesTypes": [], - "configurables": [], - "errorCodes": {} + "configurables": [] }; const bytecode = decompressBytecode('0x-bytecode-here'); diff --git a/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs index 27558d2355b..b6130420e49 100644 --- a/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs +++ b/packages/abi-typegen/test/fixtures/templates/script-with-configurable/main.hbs @@ -64,13 +64,13 @@ const abi = { ], "functions": [ { - "name": "main", "inputs": [ { "name": "score", "concreteTypeId": "bd5dbfd643b808e6ef22f73778cc8277cc5f01a4f974359b3169caa06e661d7e" } ], + "name": "main", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null } @@ -84,8 +84,7 @@ const abi = { "offset": 664, "indirect": false } - ], - "errorCodes": {} + ] }; const bytecode = decompressBytecode('0x-bytecode-here'); diff --git a/packages/abi-typegen/test/fixtures/templates/script/main.hbs b/packages/abi-typegen/test/fixtures/templates/script/main.hbs index b22dd2c9546..a3e0446c47b 100644 --- a/packages/abi-typegen/test/fixtures/templates/script/main.hbs +++ b/packages/abi-typegen/test/fixtures/templates/script/main.hbs @@ -238,7 +238,6 @@ const abi = { ], "functions": [ { - "name": "main", "inputs": [ { "name": "vec", @@ -257,14 +256,14 @@ const abi = { "concreteTypeId": "85dace7aaa469c8bb476be79ddec34883ef101b3cde470636f47e299bfcdc3da" } ], + "name": "main", "output": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", "attributes": null } ], "loggedTypes": [], "messagesTypes": [], - "configurables": [], - "errorCodes": {} + "configurables": [] }; const bytecode = decompressBytecode('0x-bytecode-here'); diff --git a/packages/abi-typegen/test/utils/getNewAbiTypegen.ts b/packages/abi-typegen/test/utils/getNewAbiTypegen.ts index 4df50b4050b..27be5b52493 100644 --- a/packages/abi-typegen/test/utils/getNewAbiTypegen.ts +++ b/packages/abi-typegen/test/utils/getNewAbiTypegen.ts @@ -83,6 +83,7 @@ export function getNewAbiTypegen( typeArguments: null, }, offset: 120, + indirect: false, }, ]; diff --git a/packages/account/src/index.ts b/packages/account/src/index.ts index d9e7fdd1755..13255b38b4a 100644 --- a/packages/account/src/index.ts +++ b/packages/account/src/index.ts @@ -10,6 +10,7 @@ export * from './wallet-manager'; export * from './predicate'; export * from './providers'; export * from './connectors'; +export { createConfigurables } from './utils/createConfigurables'; export { deployScriptOrPredicate } from './utils/deployScriptOrPredicate'; export { getBytecodeId, diff --git a/packages/account/src/predicate/predicate.ts b/packages/account/src/predicate/predicate.ts index a2a8f3b537b..eb7c4f76f42 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -23,6 +23,7 @@ import type { TransactionRequestLike, TransactionResponse, } from '../providers'; +import { createConfigurables } from '../utils/createConfigurables'; import { deployScriptOrPredicate } from '../utils/deployScriptOrPredicate'; import { getPredicateRoot } from './utils'; @@ -219,11 +220,11 @@ export class Predicate< } if (configurableConstants && Object.keys(configurableConstants).length) { - predicateBytes = Predicate.setConfigurableConstants( - predicateBytes, - configurableConstants, - abiInterface - ); + const configurables = createConfigurables({ + bytecode: predicateBytes, + abi: abiInterface, + }); + predicateBytes = configurables.set(configurableConstants); } return { @@ -232,6 +233,15 @@ export class Predicate< }; } + getConfigurables(): Record { + const configurables = createConfigurables({ + bytecode: this.bytes, + abi: this.interface, + }); + + return configurables.all(); + } + /** * Retrieves resources satisfying the spend query for the account. * @@ -269,53 +279,6 @@ export class Predicate< })); } - /** - * Sets the configurable constants for the predicate. - * - * @param bytes - The bytes of the predicate. - * @param configurableConstants - Configurable constants to be set. - * @param abiInterface - The ABI interface of the predicate. - * @returns The mutated bytes with the configurable constants set. - */ - private static setConfigurableConstants( - bytes: Uint8Array, - configurableConstants: { [name: string]: unknown }, - abiInterface: Interface - ) { - const mutatedBytes = bytes; - - try { - if (Object.keys(abiInterface.configurables).length === 0) { - throw new FuelError( - ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, - 'Predicate has no configurable constants to be set' - ); - } - - Object.entries(configurableConstants).forEach(([key, value]) => { - if (!abiInterface?.configurables[key]) { - throw new FuelError( - ErrorCode.CONFIGURABLE_NOT_FOUND, - `No configurable constant named '${key}' found in the Predicate` - ); - } - - const { offset } = abiInterface.configurables[key]; - - const encoded = abiInterface.encodeConfigurable(key, value as InputValue); - - mutatedBytes.set(encoded, offset); - }); - } catch (err) { - throw new FuelError( - ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: ${(err).message}.` - ); - } - - return mutatedBytes; - } - /** * Returns the index of the witness placeholder that was added to this predicate. * If no witness placeholder was added, it returns -1. @@ -375,6 +338,7 @@ export class Predicate< abi: newAbi, provider: this.provider, data: this.predicateData, + configurableConstants: this.configurableConstants, }) as T, }); } diff --git a/packages/account/src/utils/createConfigurables.test.ts b/packages/account/src/utils/createConfigurables.test.ts new file mode 100644 index 00000000000..080045a87e1 --- /dev/null +++ b/packages/account/src/utils/createConfigurables.test.ts @@ -0,0 +1,201 @@ +import { Interface } from '@fuel-ts/abi-coder'; +import { decompressBytecode } from '@fuel-ts/utils'; + +import { createConfigurables } from './createConfigurables'; + +const abi = { + programType: 'predicate', + specVersion: '1', + encodingVersion: '1', + concreteTypes: [ + { + type: 'bool', + concreteTypeId: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + }, + { + type: 'str', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + type: 'u8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + ], + metadataTypes: [], + functions: [ + { + inputs: [ + { + name: 'some_bool', + concreteTypeId: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + }, + { + name: 'some_u8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + { + name: 'some_str', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + name: 'some_str_2', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + name: 'some_str_3', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + name: 'some_last_u8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + ], + name: 'main', + output: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + attributes: null, + }, + ], + loggedTypes: [], + messagesTypes: [], + configurables: [ + { + name: 'BOOL', + concreteTypeId: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + offset: 2048, + indirect: false, + }, + { + name: 'U8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + offset: 2088, + indirect: false, + }, + { + name: 'STR', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + offset: 2064, + indirect: true, + }, + { + name: 'STR_2', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + offset: 2072, + indirect: true, + }, + { + name: 'STR_3', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + offset: 2080, + indirect: true, + }, + { + name: 'LAST_U8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + offset: 2056, + indirect: false, + }, + ], +}; + +// Predicate with dynamic configurables +const bytecode = decompressBytecode( + 'H4sIAAAAAAAAA5VWTWgTWxQ+k8bXwOvjXUwXffctOkjBCi4G/7a9QzrEmIbcEEXFTHuL6EpR6/9Kl24EFfxZunTnLHXXlbiSbgRFhIh2ITXgwoDFRfzOzYwdJ6loIJxhznfu/c53v3MT+dmjC0R5sp9CPyCGvWVH9Hp0m2i3XvtM+gM5uq3I/bqbjnxr5/S3dh51N5ErxDknk3uDnAjXHpJYe7wMzBgwuQymC8xEBrM1g/kEjJvBbMtg3gMzHfPYlck9lZ8099E+p2hkMchRsSLo0n76a2k/5YrB+PVLs+TsQ9eLfu4A8A7i2EKH/tel6JX8SNmer8nyCg15f0tWV0jXoyuck6teNv9O1lfIdDwBzI1NMK8Yo5uRMA2Le7AJ7oXFHY5cc8gTQ7icliXLxZuvCdKBV5gPXI7ClCOB6JpAcfRMoDmq+YAI/XotH7EceToQhVaA2iqeK65qVVBfjx6ZOuqPRU/MUdQ3o+emoe3+clWQfOeRfGtIvlYkX7pZTs+Y05Qiukv07z1YLT6XCVl6SLJ8i2T1Osl622pr13sfZdcYD7EGzsYB18KQvif4bJAT8z64I8pZQdOVvJJfiO5wft2jB9j7Pjgc+Gq5jCZc+jzAodqmBeghu+Cw7ib40Rj/g3sf3+eb4fEP91qsADdH7LV80Scl5+A71fea7EKrdZHlMrB2iDVY+7BGJGqeWvBR13V/Wfsnml4kmvmVpshPWS/BN7FHfkfTyRQXleXC/t7wzM93CvY7HvPZgr2mse9Egh2msemQYN8a8OK4NEsixU2luE3G3GZS3MwGN2gNjcOaR2GDaOkgFUQjgN6K+XqmQYWFjjdqOtb32qAHxonG7LLFlCLFHEKen2bkgn8eeO5BGF8JE5BgP4GTSXlqJubUG86J9TpLsomZOgxPHIN2LUWt8CwV/S3L7CXoMKKV2sVzW/THrb/w7m+8y5mOK/jc7N5r0TANp1nDuGYMNVuBx92Auwdzbyqo+4gzWh2YZZmp24Y6Ze+UKrTZvG4sVce8nT7vg/C0PS9oYxJtekO8pNPnxWfPfWX22LlxhwrwGPjNmpJNzGZtL76YJY35nKP/+K40NeY9cNduj9dTm6w3ae8cnDk8Mixf4J53NPZo2VXco06df+LJkVSPIpl9218pInjKifURqbkf+c3aRNuB2jLuISf+xRdxjD874rg3jn7yzyB//vLi1eT55Jml4z+eL5449R232/RNTQgAAA==' +); + +describe('configurables', () => { + it('should return the configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + const entries = configurables.all(); + + expect(entries).toEqual([ + { name: 'BOOL', value: true }, + { name: 'U8', value: 8 }, + { name: 'STR', value: 'sway' }, + { name: 'STR_2', value: 'forc' }, + { name: 'STR_3', value: 'fuel' }, + { name: 'LAST_U8', value: 16 }, + ]); + }); + + it('should write direct configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + configurables.set({ + BOOL: false, + U8: 16, + LAST_U8: 32, + }); + + const entries = configurables.all(); + expect(entries).toEqual( + expect.arrayContaining([ + { name: 'BOOL', value: false }, // Changed + { name: 'U8', value: 16 }, // Changed + { name: 'STR', value: 'sway' }, + { name: 'STR_2', value: 'forc' }, + { name: 'STR_3', value: 'fuel' }, + { name: 'LAST_U8', value: 32 }, // Changed + ]) + ); + }); + + it('should write indirect configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + configurables.set({ + STR: 'sway-sway-sway', + STR_2: 'forc-forc', + STR_3: '', + }); + + const entries = configurables.all(); + expect(entries).toMatchObject( + expect.arrayContaining([ + { name: 'BOOL', value: true }, + { name: 'U8', value: 8 }, + { name: 'STR', value: 'sway-sway-sway' }, // Changed + { name: 'STR_2', value: 'forc-forc' }, // Changed + { name: 'STR_3', value: '' }, // Changed + { name: 'LAST_U8', value: 16 }, + ]) + ); + }); + + it('should write both direct and indirect configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + configurables.set({ + STR: 'sway-sway-sway', + STR_2: 'forc-forc', + STR_3: '', + BOOL: false, + U8: 16, + LAST_U8: 32, + }); + + const entries = configurables.all(); + expect(entries).toMatchObject( + expect.arrayContaining([ + { name: 'BOOL', value: false }, // Changed + { name: 'U8', value: 16 }, // Changed + { name: 'STR', value: 'sway-sway-sway' }, // Changed + { name: 'STR_2', value: 'forc-forc' }, // Changed + { name: 'STR_3', value: '' }, // Changed + { name: 'LAST_U8', value: 32 }, // Changed + ]) + ); + }); +}); diff --git a/packages/account/src/utils/createConfigurables.ts b/packages/account/src/utils/createConfigurables.ts new file mode 100644 index 00000000000..db309c93525 --- /dev/null +++ b/packages/account/src/utils/createConfigurables.ts @@ -0,0 +1,157 @@ +import { BigNumberCoder } from '@fuel-ts/abi-coder'; +import type { InputValue, Interface } from '@fuel-ts/abi-coder'; +import type { Configurable } from '@fuel-ts/abi-coder/dist/types/JsonAbiNew'; +import { ErrorCode, FuelError } from '@fuel-ts/errors'; + +import { extractBlobIdAndDataOffset } from './predicate-script-loader-instructions'; + +export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }) => { + const { abi } = opts; + let bytecode = new Uint8Array(opts.bytecode); + const configurables = Object.values(abi.configurables); + const { dataOffset } = extractBlobIdAndDataOffset(bytecode); + const dynamicOffsetCoder = new BigNumberCoder('u64'); + + const getConfigurable = (name: string) => { + const configurable = configurables.find((conf) => conf.name === name); + if (!configurable) { + throw new FuelError( + ErrorCode.CONFIGURABLE_NOT_FOUND, + `A configurable with the '${name}' was not found in the ABI.` + ); + } + return configurable; + }; + + const readIndirectOffset = ({ offset }: Pick) => { + const [dynamicOffsetBn] = dynamicOffsetCoder.decode(bytecode, offset); + const dynamicOffset = dataOffset + dynamicOffsetBn.toNumber(); + return dynamicOffset; + }; + + /** + * Readers + */ + const readDirect = ({ name, concreteTypeId, offset }: Configurable) => { + const coder = abi.getCoder(concreteTypeId); + const [value] = coder.decode(bytecode, offset); + return { name, value }; + }; + + const readIndirect = ({ name, concreteTypeId, offset }: Configurable) => { + const dynamicOffset = readIndirectOffset({ offset }); + const coder = abi.getCoder(concreteTypeId); + const [value] = coder.decode(bytecode, dynamicOffset); + return { name, value }; + }; + + const read = (configurable: Configurable) => { + const reader = configurable.indirect ? readIndirect : readDirect; + return reader(configurable); + }; + + /** + * Writers + */ + const writeDirect = ({ name, offset }: Configurable, value: InputValue) => { + const encodedValue = abi.encodeConfigurable(name, value); + bytecode.set(encodedValue, offset); + }; + + const writeIndirect = ({ concreteTypeId, offset }: Configurable, value: InputValue) => { + const dynamicOffset = readIndirectOffset({ offset }); + + // Read the original value + const coder = abi.getCoder(concreteTypeId); + const [, originalOffset] = coder.decode(bytecode, dynamicOffset); + const originalLength = originalOffset - dynamicOffset; + + // Encode the new value + const encodedValue = coder.encode(value); + const newLength = encodedValue.length; + + // Update the bytecode + bytecode = new Uint8Array([ + ...bytecode.slice(0, dynamicOffset), + ...encodedValue, + ...bytecode.slice(dynamicOffset + originalLength), + ]); + + const additionalOffset = newLength - originalLength; + + // Update the other dynamic configurable offsets + configurables + .filter((configurable) => configurable.indirect && configurable.offset > offset) + .forEach((configurable) => { + const newDynamicOffset = readIndirectOffset({ offset: configurable.offset }); + const newOffset = newDynamicOffset + additionalOffset - dataOffset; + + const encodedOffset = dynamicOffsetCoder.encode(newOffset); + bytecode.set(encodedOffset, configurable.offset); + }); + }; + + const write = (configurable: Configurable, value: InputValue) => { + const writer = configurable.indirect ? writeIndirect : writeDirect; + return writer(configurable, value); + }; + + return { + /** + * Reads the value of a configurable. + * + * @param name - The name of the configurable to read. + * @returns The value of the configurable. + */ + read: (name: string) => read(getConfigurable(name)), + /** + * Reads all the configurables. + * + * @returns An object of all configurables + */ + all: (): Record => + Object.fromEntries(configurables.map(read).map(({ name, value }) => [name, value])), + /** + * Updates the bytecode with the new configurable values. + * + * @param configurableValues - The new configurable values to set. + * @returns The mutated bytecode. + */ + set: (configurableValues: { [name: string]: unknown }) => { + try { + const configurableKeys = Object.keys(abi.configurables); + const providedKeys = Object.keys(configurableValues); + + if (!configurableKeys.length) { + throw new FuelError( + FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, + `the program does not have configurable constants to be set.` + ); + } + + const unknownKeys = providedKeys.filter((key) => !configurableKeys.includes(key)); + if (unknownKeys.length) { + throw new FuelError( + FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, + `unknown keys supplied:\n${unknownKeys.map((key) => `- '${key}'`).join('\n')}` + ); + } + + configurables + .sort((a, b) => b.offset - a.offset) + .filter((configurable) => Object.hasOwn(configurableValues, configurable.name)) + .forEach((configurable) => { + const value = configurableValues[configurable.name]; + write(configurable, value as InputValue); + }); + } catch (err) { + throw new FuelError( + FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, + `Error setting configurable constants, ${(err).message}` + ); + } + + return bytecode; + }, + }; +}; diff --git a/packages/account/src/utils/deployScriptOrPredicate.ts b/packages/account/src/utils/deployScriptOrPredicate.ts index 804937e0d51..af8efd65ad7 100644 --- a/packages/account/src/utils/deployScriptOrPredicate.ts +++ b/packages/account/src/utils/deployScriptOrPredicate.ts @@ -1,4 +1,4 @@ -import type { JsonAbi } from '@fuel-ts/abi-coder'; +import { type JsonAbi } from '@fuel-ts/abi-coder'; import { FuelError, ErrorCode } from '@fuel-ts/errors'; import { arrayify } from '@fuel-ts/utils'; @@ -49,6 +49,24 @@ export async function deployScriptOrPredicate({ const configurableOffset = getBytecodeConfigurableOffset(arrayify(bytecode)); const byteCodeWithoutConfigurableSection = bytecode.slice(0, configurableOffset); + /** + * Expected to have to shift the indirect "dynamic offsets" to point to new values. + * However, the Sway code compiles and runs with the pre-existing values. + * Check that this is the case once everything is merged and ready + */ + // // Adjust the indirect configurable offsets to point to the new data offsets for the loader + // const dataOffset = getBytecodeDataOffset(arrayify(bytecode)); + // const newIndirectConfigurableOffsetDiff = configurableOffset - dataOffset; + // const dynamicOffsetCoder = new BigNumberCoder('u64'); + // abi.configurables + // .filter((configurable) => configurable.indirect ?? false) + // .forEach((configurable) => { + // const [existingOffset] = dynamicOffsetCoder.decode(bytecode, configurable.offset); + // const newOffset = existingOffset.sub(newIndirectConfigurableOffsetDiff); + // const newOffsetBytes = dynamicOffsetCoder.encode(newOffset); + // bytecode.set(newOffsetBytes, configurable.offset); + // }); + const blobTxRequest = new BlobTransactionRequest({ blobId, witnessIndex: 0, @@ -60,6 +78,7 @@ export async function deployScriptOrPredicate({ arrayify(blobId) ); + // Adjust the ABI configurable offset const newConfigurableOffsetDiff = byteCodeWithoutConfigurableSection.length - (blobOffset || 0); const newAbi = adjustConfigurableOffsets(abi, newConfigurableOffsetDiff); diff --git a/packages/account/src/utils/predicate-script-loader-instructions.test.ts b/packages/account/src/utils/predicate-script-loader-instructions.test.ts index 36111775ae7..3ccc51031f2 100644 --- a/packages/account/src/utils/predicate-script-loader-instructions.test.ts +++ b/packages/account/src/utils/predicate-script-loader-instructions.test.ts @@ -1,10 +1,14 @@ -import { concat } from '@fuel-ts/utils'; +import { ZeroBytes32 } from '@fuel-ts/address/configs'; +import { arrayify, concat, decompressBytecode } from '@fuel-ts/utils'; import { + extractBlobIdAndDataOffset, getBytecodeConfigurableOffset, getBytecodeDataOffset, getBytecodeId, getLegacyBlobId, + getPredicateScriptLoaderInstructions, + isBytecodeLoader, } from './predicate-script-loader-instructions'; /** @@ -36,4 +40,70 @@ describe('Predicate Script Loader Instructions', () => { const id = getLegacyBlobId(bytecode); expect(id).toBe('0x709e80c88487a2411e1ee4dfb9f22a861492d20c4765150c0c794abd70f8147c'); }); + + describe('extractBlobIdAndDataOffset', () => { + it('should return the correct blobId and dataOffset [regular]', () => { + const PredicateWithDynamicConfigurableConfigurables = { + bytecode: decompressBytecode( + 'H4sIAAAAAAAAA5VWTWgTWxQ+k8bXwOvjXUwXffctOkjBCi4G/7a9QzrEmIbcEEXFTHuL6EpR6/9Kl24EFfxZunTnLHXXlbiSbgRFhIh2ITXgwoDFRfzOzYwdJ6loIJxhznfu/c53v3MT+dmjC0R5sp9CPyCGvWVH9Hp0m2i3XvtM+gM5uq3I/bqbjnxr5/S3dh51N5ErxDknk3uDnAjXHpJYe7wMzBgwuQymC8xEBrM1g/kEjJvBbMtg3gMzHfPYlck9lZ8099E+p2hkMchRsSLo0n76a2k/5YrB+PVLs+TsQ9eLfu4A8A7i2EKH/tel6JX8SNmer8nyCg15f0tWV0jXoyuck6teNv9O1lfIdDwBzI1NMK8Yo5uRMA2Le7AJ7oXFHY5cc8gTQ7icliXLxZuvCdKBV5gPXI7ClCOB6JpAcfRMoDmq+YAI/XotH7EceToQhVaA2iqeK65qVVBfjx6ZOuqPRU/MUdQ3o+emoe3+clWQfOeRfGtIvlYkX7pZTs+Y05Qiukv07z1YLT6XCVl6SLJ8i2T1Osl622pr13sfZdcYD7EGzsYB18KQvif4bJAT8z64I8pZQdOVvJJfiO5wft2jB9j7Pjgc+Gq5jCZc+jzAodqmBeghu+Cw7ib40Rj/g3sf3+eb4fEP91qsADdH7LV80Scl5+A71fea7EKrdZHlMrB2iDVY+7BGJGqeWvBR13V/Wfsnml4kmvmVpshPWS/BN7FHfkfTyRQXleXC/t7wzM93CvY7HvPZgr2mse9Egh2msemQYN8a8OK4NEsixU2luE3G3GZS3MwGN2gNjcOaR2GDaOkgFUQjgN6K+XqmQYWFjjdqOtb32qAHxonG7LLFlCLFHEKen2bkgn8eeO5BGF8JE5BgP4GTSXlqJubUG86J9TpLsomZOgxPHIN2LUWt8CwV/S3L7CXoMKKV2sVzW/THrb/w7m+8y5mOK/jc7N5r0TANp1nDuGYMNVuBx92Auwdzbyqo+4gzWh2YZZmp24Y6Ze+UKrTZvG4sVce8nT7vg/C0PS9oYxJtekO8pNPnxWfPfWX22LlxhwrwGPjNmpJNzGZtL76YJY35nKP/+K40NeY9cNduj9dTm6w3ae8cnDk8Mixf4J53NPZo2VXco06df+LJkVSPIpl9218pInjKifURqbkf+c3aRNuB2jLuISf+xRdxjD874rg3jn7yzyB//vLi1eT55Jml4z+eL5449R232/RNTQgAAA==' + ), + }; + const expectedBlobId = '0x63eb718285528ee78dfc07d6935616deaa8a69285571716d8b52782a5d43840d'; + const expectedDataOffset = 2048; + + const { blobId, dataOffset } = extractBlobIdAndDataOffset( + PredicateWithDynamicConfigurableConfigurables.bytecode + ); + + expect(blobId).toEqual(arrayify(expectedBlobId)); + expect(dataOffset).toBe(expectedDataOffset); + }); + + it('should return the correct blobId and dataOffset [loader w/ data]', () => { + const PredicateWithDynamicConfigurableConfigurablesLoader = { + bytecode: decompressBytecode( + 'H4sIAAAAAAAAA5NyMGAIcGQwkHIJYNjlycBg5MDSCOQrxALZQJoDyG9ScBVmCHIVYPFyYWBIfl3Y1BrU97z3D/u1yWFi91Z1ZWqEFhbmdgdVaMU6t/AyQIAvI5QhAKWhQBNKm0JpRw4og6W4PLESxk7LL0qGs0tTcwAvXB4ppQAAAA==' + ), + }; + const expectedBlobId = '0x63eb718285528ee78dfc07d6935616deaa8a69285571716d8b52782a5d43840d'; + const expectedDataOffset = 88; + + const { blobId, dataOffset } = extractBlobIdAndDataOffset( + PredicateWithDynamicConfigurableConfigurablesLoader.bytecode + ); + + expect(blobId).toEqual(arrayify(expectedBlobId)); + expect(dataOffset).toBe(expectedDataOffset); + }); + + it('should return the correct blobId and dataOffset [loader w/o data]', () => { + const PredicateTrueLoader = { + bytecode: decompressBytecode( + 'H4sIAAAAAAAAA5NyMGAIcGRQkHIJYNjlycBg5MDSqOAqzBDkKsDi5cLAcCCvQX0T42l2s/N5nkbxS2Z9e5Rt/OdVRE/Mwzsmha+e7wAAU6+fgEAAAAA=' + ), + }; + const expectedBlobId = '0xc06e8027b201cb0736cf6e49325fa49af6e26b33fcea588c5ce1dc3471eae7b8'; + const expectedDataOffset = 64; + + const { blobId, dataOffset } = extractBlobIdAndDataOffset(PredicateTrueLoader.bytecode); + + expect(blobId).toEqual(arrayify(expectedBlobId)); + expect(dataOffset).toBe(expectedDataOffset); + expect(dataOffset).toEqual(PredicateTrueLoader.bytecode.length); + }); + }); + + describe('isBytecodeLoader', () => { + it('should return false for non-loader bytecode', () => { + const isLoader = isBytecodeLoader(bytecode); + expect(isLoader).toBe(false); + }); + + it('should return true for loader bytecode', () => { + const blobId = arrayify(ZeroBytes32); + const { loaderBytecode } = getPredicateScriptLoaderInstructions(bytecode, blobId); + const isLoader = isBytecodeLoader(loaderBytecode); + expect(isLoader).toBe(true); + }); + }); }); diff --git a/packages/account/src/utils/predicate-script-loader-instructions.ts b/packages/account/src/utils/predicate-script-loader-instructions.ts index 4b021d61af4..f48fcdb7770 100644 --- a/packages/account/src/utils/predicate-script-loader-instructions.ts +++ b/packages/account/src/utils/predicate-script-loader-instructions.ts @@ -1,6 +1,6 @@ import { BigNumberCoder } from '@fuel-ts/abi-coder'; import { sha256 } from '@fuel-ts/hasher'; -import { concat } from '@fuel-ts/utils'; +import { concat, arrayify, hexlify } from '@fuel-ts/utils'; import * as asm from '@fuels/vm-asm'; const BLOB_ID_SIZE = 32; @@ -8,6 +8,8 @@ const REG_ADDRESS_OF_DATA_AFTER_CODE = 0x10; const REG_START_OF_LOADED_CODE = 0x11; const REG_GENERAL_USE = 0x12; const WORD_SIZE = 8; // size in bytes +// https://github.com/FuelLabs/fuel-vm/blob/a340921a00050bd1734e7dcf278a1d13edf2786b/fuel-asm/src/lib.rs#L185-L186 +const LDC_INSTRUCTION_PREAMPLE = 0x32; export const DATA_OFFSET_INDEX = 8; export const CONFIGURABLE_OFFSET_INDEX = 16; @@ -66,23 +68,30 @@ export function getLegacyBlobId(bytecode: Uint8Array): string { return sha256(byteCodeWithoutDataSection); } -export function getPredicateScriptLoaderInstructions( - originalBinary: Uint8Array, - blobId: Uint8Array -) { - // The final code is going to have this structure: - // 1. loader instructions - // 2. blob id - // 3. length_of_data_section - // 4. the data_section (updated with configurables as needed) +/** + * TODO: verify this is correct + * + * Check if the bytecode is a loader + * + * @param bytecode - The bytecode to check + * @returns True if the bytecode is a loader, false otherwise + */ +export function isBytecodeLoader(bytecode: Uint8Array): boolean { + const dataView = new DataView(bytecode.buffer); + const preample = dataView.getUint8(REG_ADDRESS_OF_DATA_AFTER_CODE); + return preample === LDC_INSTRUCTION_PREAMPLE; +} +function getInstructionsWithDataSection(): Uint8Array { const { RegId, Instruction } = asm; const REG_PC = RegId.pc().to_u8(); const REG_SP = RegId.sp().to_u8(); const REG_IS = RegId.is().to_u8(); - const getInstructions = (numOfInstructions: number) => [ + const NUM_OF_INSTRUCTIONS = 12; + + const instructions = [ // 1. Load the blob content into memory // Find the start of the hardcoded blob ID, which is located after the loader code ends. asm.move_(REG_ADDRESS_OF_DATA_AFTER_CODE, REG_PC), @@ -90,7 +99,7 @@ export function getPredicateScriptLoaderInstructions( asm.addi( REG_ADDRESS_OF_DATA_AFTER_CODE, REG_ADDRESS_OF_DATA_AFTER_CODE, - numOfInstructions * Instruction.size() + NUM_OF_INSTRUCTIONS * Instruction.size() ), // The code is going to be loaded from the current value of SP onwards, save // the location into REG_START_OF_LOADED_CODE so we can jump into it at the end. @@ -118,7 +127,24 @@ export function getPredicateScriptLoaderInstructions( asm.jmp(REG_START_OF_LOADED_CODE), ]; - const getInstructionsNoDataSection = (numOfInstructions: number) => [ + // Ensures that the number of instructions is always correct. + if (instructions.length !== NUM_OF_INSTRUCTIONS) { + throw new Error('Invalid number of instructions, check the NUM_OF_INSTRUCTIONS is correct.'); + } + + return new Uint8Array(instructions.flatMap((instruction) => Array.from(instruction.to_bytes()))); +} + +function getInstructionsWithoutDataSection(): Uint8Array { + const { RegId, Instruction } = asm; + + const REG_PC = RegId.pc().to_u8(); + const REG_SP = RegId.sp().to_u8(); + const REG_IS = RegId.is().to_u8(); + + const NUM_OF_INSTRUCTIONS = 8; + + const instructions = [ // 1. Load the blob content into memory // Find the start of the hardcoded blob ID, which is located after the loader code ends. // 1. Load the blob content into memory @@ -128,7 +154,7 @@ export function getPredicateScriptLoaderInstructions( asm.addi( REG_ADDRESS_OF_DATA_AFTER_CODE, REG_ADDRESS_OF_DATA_AFTER_CODE, - numOfInstructions * Instruction.size() + NUM_OF_INSTRUCTIONS * Instruction.size() ), // The code is going to be loaded from the current value of SP onwards, save // the location into REG_START_OF_LOADED_CODE so we can jump into it at the end. @@ -147,6 +173,23 @@ export function getPredicateScriptLoaderInstructions( asm.jmp(REG_START_OF_LOADED_CODE), ]; + // Ensures that the number of instructions is always correct. + if (instructions.length !== NUM_OF_INSTRUCTIONS) { + throw new Error('Invalid number of instructions, check the NUM_OF_INSTRUCTIONS is correct.'); + } + + return new Uint8Array(instructions.flatMap((instruction) => Array.from(instruction.to_bytes()))); +} + +export function getPredicateScriptLoaderInstructions( + originalBinary: Uint8Array, + blobId: Uint8Array +) { + // The final code is going to have this structure: + // 1. loader instructions + // 2. blob id + // 3. length_of_data_section + // 4. the data_section (updated with configurables as needed) const offset = getBytecodeConfigurableOffset(originalBinary); // if the binary length is smaller than the offset @@ -161,18 +204,8 @@ export function getPredicateScriptLoaderInstructions( // Check if the configurable section is non-empty if (configurableSection.length > 0) { - // Get the number of instructions (assuming it won't exceed u16::MAX) - const numOfInstructions = getInstructions(0).length; - if (numOfInstructions > 65535) { - throw new Error('Too many instructions, exceeding u16::MAX.'); - } - // Convert instructions to bytes - const instructionBytes = new Uint8Array( - getInstructions(numOfInstructions).flatMap((instruction) => - Array.from(instruction.to_bytes()) - ) - ); + const instructionBytes = getInstructionsWithDataSection(); // Convert blobId to bytes const blobBytes = new Uint8Array(blobId); @@ -194,18 +227,9 @@ export function getPredicateScriptLoaderInstructions( blobOffset: loaderBytecode.length, }; } - // Handle case where there is no configurable section - const numOfInstructions = getInstructionsNoDataSection(0).length; - if (numOfInstructions > 65535) { - throw new Error('Too many instructions, exceeding u16::MAX.'); - } // Convert instructions to bytes - const instructionBytes = new Uint8Array( - getInstructionsNoDataSection(numOfInstructions).flatMap((instruction) => - Array.from(instruction.to_bytes()) - ) - ); + const instructionBytes = getInstructionsWithoutDataSection(); // Convert blobId to bytes const blobBytes = new Uint8Array(blobId); @@ -213,5 +237,54 @@ export function getPredicateScriptLoaderInstructions( // Combine the instruction bytes and blob bytes const loaderBytecode = new Uint8Array([...instructionBytes, ...blobBytes]); - return { loaderBytecode }; + return { loaderBytecode, blobOffset: 0 }; +} + +/** + * Extract the blob ID and data offset from the bytecode + * + * @param bytecode - The bytecode to extract the blob ID and data offset from + * @returns The blob ID and data offset + */ +export function extractBlobIdAndDataOffset(bytecode: Uint8Array): { + blobId: Uint8Array; + dataOffset: number; +} { + if (!isBytecodeLoader(bytecode)) { + return { + blobId: arrayify(getLegacyBlobId(bytecode)), + dataOffset: getBytecodeDataOffset(bytecode), + }; + } + + const instructionsWithData = getInstructionsWithDataSection(); + + const hexlifiedBytecode = hexlify(bytecode); + const hexlifiedInstructionsWithData = hexlify(instructionsWithData); + + // Check if the bytecode starts with the instructions with data section + if (hexlifiedBytecode.startsWith(hexlifiedInstructionsWithData)) { + let offset = instructionsWithData.length; + + // Read off the blob ID + const blobId = bytecode.slice(offset, (offset += BLOB_ID_SIZE)); + // We skip over `WORD_SIZE` bytes as this stores the data length. + // After this, the offset of the data section is found. + const dataOffset = offset + WORD_SIZE; + return { + blobId, + dataOffset, + }; + } + + const instructionsWithoutData = getInstructionsWithoutDataSection(); + let offset = instructionsWithoutData.length; + + // Read off the blob ID + const blobId = bytecode.slice(offset, (offset += BLOB_ID_SIZE)); + + return { + blobId, + dataOffset: offset, + }; } diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 4823477fa8c..1efec9e7460 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -1,5 +1,5 @@ import { Interface, WORD_SIZE } from '@fuel-ts/abi-coder'; -import type { JsonAbi, InputValue } from '@fuel-ts/abi-coder'; +import type { JsonAbi } from '@fuel-ts/abi-coder'; import type { Account, CreateTransactionRequestLike, @@ -14,6 +14,7 @@ import { BlobTransactionRequest, TransactionStatus, calculateGasFee, + createConfigurables, setAndValidateGasAndFeeForAssembledTx, } from '@fuel-ts/account'; import { randomBytes } from '@fuel-ts/crypto'; @@ -419,33 +420,12 @@ export default class ContractFactory { */ setConfigurableConstants(configurableConstants: { [name: string]: unknown }) { try { - const hasConfigurable = Object.keys(this.interface.configurables).length; - - if (!hasConfigurable) { - throw new FuelError( - ErrorCode.CONFIGURABLE_NOT_FOUND, - 'Contract does not have configurables to be set' - ); - } - - Object.entries(configurableConstants).forEach(([key, value]) => { - if (!this.interface.configurables[key]) { - throw new FuelError( - ErrorCode.CONFIGURABLE_NOT_FOUND, - `Contract does not have a configurable named: '${key}'` - ); - } - - const { offset } = this.interface.configurables[key]; - - const encoded = this.interface.encodeConfigurable(key, value as InputValue); - - const bytes = arrayify(this.bytecode); - - bytes.set(encoded, offset); - - this.bytecode = bytes; + const configurables = createConfigurables({ + bytecode: arrayify(this.bytecode), + abi: this.interface, }); + + this.bytecode = configurables.set(configurableConstants); } catch (err) { throw new FuelError( ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, diff --git a/packages/fuel-gauge/src/contract-with-dynamic-configurables.test.ts b/packages/fuel-gauge/src/contract-with-dynamic-configurables.test.ts new file mode 100644 index 00000000000..6f4abfb69b3 --- /dev/null +++ b/packages/fuel-gauge/src/contract-with-dynamic-configurables.test.ts @@ -0,0 +1,100 @@ +import { launchTestNode } from 'fuels/test-utils'; + +import { ContractWithDynamicConfigurablesFactory } from '../test/typegen'; + +/** + * @group node + * @group browser + */ +describe('Contract with dynamic configurables', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const contractFactory = new ContractWithDynamicConfigurablesFactory(deployer); + const { waitForResult: waitForDeploy } = await contractFactory.deploy(); + const { contract } = await waitForDeploy(); + + const { waitForResult } = await contract.functions + .main(true, 8, 'sway', 'forc', 'fuel', 16) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should allow setting of dynamic configurables [create tx]', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const contractFactory = new ContractWithDynamicConfigurablesFactory(deployer); + const { waitForResult: waitForDeploy } = await contractFactory.deployAsCreateTx({ + configurableConstants: { + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }, + }); + const { contract } = await waitForDeploy(); + + const { waitForResult } = await contract.functions + .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should allow setting of dynamic configurables [blob tx]', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const contractFactory = new ContractWithDynamicConfigurablesFactory(deployer); + const { waitForResult: waitForDeploy } = await contractFactory.deployAsBlobTx({ + configurableConstants: { + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }, + }); + const { contract } = await waitForDeploy(); + + const { waitForResult } = await contract.functions + .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should return false for contract with incorrect data', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const contractFactory = new ContractWithDynamicConfigurablesFactory(deployer); + const { waitForResult: waitForDeploy } = await contractFactory.deploy(); + const { contract } = await waitForDeploy(); + + const { waitForResult } = await contract.functions + .main(true, 8, 'sway', 'forc', 'fuel-incorrect', 16) + .call(); + + const { value } = await waitForResult(); + + expect(value).toBe(false); + }); +}); diff --git a/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts b/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts index aed6f72c789..f6088491a57 100644 --- a/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts @@ -271,7 +271,7 @@ describe('Predicate', () => { }), new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - 'Error setting configurable constants: Predicate has no configurable constants to be set.' + 'Error setting configurable constants, the program does not have configurable constants to be set.' ) ); }); @@ -292,7 +292,7 @@ describe('Predicate', () => { }), new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: No configurable constant named 'NOPE' found in the Predicate.` + `Error setting configurable constants, unknown keys supplied:\n- 'NOPE'` ) ); }); diff --git a/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts b/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts new file mode 100644 index 00000000000..8ff41b1d157 --- /dev/null +++ b/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts @@ -0,0 +1,321 @@ +import { WalletUnlocked } from 'fuels'; +import { launchTestNode } from 'fuels/test-utils'; + +import { PredicateWithDynamicConfigurable } from '../../test/typegen'; + +/** + * @group node + * @group browser + */ +describe('Predicate with dynamic configurables', () => { + describe('Predicate', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel', 16], + }); + + // Fund predicate + await funder.transfer(predicate.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult } = await predicate.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForResult(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + it('should allow setting of dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + configurableConstants: { + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }, + data: [false, 0, 'STR', 'STR_2', 'STR_3', 0], + }); + + // Fund predicate + await funder.transfer(predicate.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult } = await predicate.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForResult(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + it('should fail predicate with incorrect data', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + + // Fund predicate + await funder.transfer(predicate.address, 1000); + + // Transfer from predicate -> receiver + await expect(() => predicate.transfer(receiver.address, 100)).rejects.toThrow( + /PredicateVerificationFailed/ + ); + }); + + it('should allow getting of dynamic configurables [initial configurables]', async () => { + const expectedConfigurables = { + BOOL: true, + U8: 8, + STR: 'sway', + STR_2: 'forc', + STR_3: 'fuel', + LAST_U8: 16, + }; + using launched = await launchTestNode(); + const { provider } = launched; + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + + const actualConfigurables = predicate.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + + it('should allow getting of dynamic configurables [updated configurables]', async () => { + const expectedConfigurables = { + BOOL: false, + U8: 0, + STR: 'STR3123123123123123123123', + STR_2: 'STR_2123123123123123123', + STR_3: 'STR_3123123123123123123123', + LAST_U8: 0, + }; + using launched = await launchTestNode(); + const { provider } = launched; + const predicate = new PredicateWithDynamicConfigurable({ + provider, + configurableConstants: expectedConfigurables, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + + const actualConfigurables = predicate.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + }); + + describe('PredicateLoader', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel', 16], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + // Fund predicate + await funder.transfer(loader.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await loader.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForTransfer(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + it('should allow initializing of dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + configurableConstants: { + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }, + data: [false, 0, 'STR', 'STR_2', 'STR_3', 0], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + // Fund predicate + await funder.transfer(loader.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await loader.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForTransfer(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + // TODO: this requires changes on the compiler to allow. + it.todo('should allow setting of dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel', 16], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + const newLoader = await loader + .toNewInstance({ + data: [false, 0, 'STR', 'STR_2', 'STR_3', 0], + }) + .deploy(deployer) + .then(({ waitForResult: waitForNewLoaderDeploy }) => waitForNewLoaderDeploy()); + + // Fund predicate + await funder.transfer(newLoader.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await newLoader.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForTransfer(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + it('should fail predicate with incorrect data', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + // Fund predicate + await funder.transfer(loader.address, 1000); + + // Transfer from predicate -> receiver + await expect(() => loader.transfer(receiver.address, 100)).rejects.toThrow( + /PredicateVerificationFailed/ + ); + }); + + it('should allow getting of dynamic configurables [initial configurables]', async () => { + const expectedConfigurables = { + BOOL: true, + U8: 8, + STR: 'sway', + STR_2: 'forc', + STR_3: 'fuel', + LAST_U8: 16, + }; + using launched = await launchTestNode(); + const { + provider, + wallets: [deployer], + } = launched; + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + const loader = await predicate.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + const actualConfigurables = loader.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + + it('should allow getting of dynamic configurables [updated configurables]', async () => { + const expectedConfigurables = { + BOOL: false, + U8: 0, + STR: 'STR3123123123123123123123', + STR_2: 'STR_2123123123123123123', + STR_3: 'STR_3123123123123123123123', + LAST_U8: 0, + }; + using launched = await launchTestNode(); + const { + provider, + wallets: [deployer], + } = launched; + const predicate = new PredicateWithDynamicConfigurable({ + provider, + configurableConstants: expectedConfigurables, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + const loader = await predicate.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + const actualConfigurables = loader.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + }); +}); diff --git a/packages/fuel-gauge/src/revert-error.test.ts b/packages/fuel-gauge/src/revert-error.test.ts index 2e32f00afc1..c8d4eeff761 100644 --- a/packages/fuel-gauge/src/revert-error.test.ts +++ b/packages/fuel-gauge/src/revert-error.test.ts @@ -24,8 +24,10 @@ function launchContract() { /** * @group node * @group browser + * + * TODO: remove skip once sway PR up to date */ -describe('Revert Error Testing', () => { +describe.skip('Revert Error Testing', () => { it('can pass require checks [valid]', async () => { using contractInstance = await launchContract(); diff --git a/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts b/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts new file mode 100644 index 00000000000..be093cda394 --- /dev/null +++ b/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts @@ -0,0 +1,217 @@ +import { launchTestNode } from 'fuels/test-utils'; + +import { ScriptWithDynamicConfigurables } from '../test/typegen'; + +/** + * @group node + * @group browser + */ +describe('Script with dynamic configurables', () => { + describe('Script', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + + const { waitForResult } = await script.functions + .main(true, 8, 'sway', 'forc', 'fuel', 16) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should allow setting of dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + + script.setConfigurableConstants({ + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }); + + const { waitForResult } = await script.functions + .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should return false for script with incorrect data', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + + const { waitForResult } = await script.functions + .main(true, 8, 'sway', 'forc', 'fuel-incorrect', 16) + .call(); + + const { value } = await waitForResult(); + + expect(value).toBe(false); + }); + + it('should allow getting of dynamic configurables [initial configurables]', async () => { + const expectedConfigurables = { + BOOL: true, + U8: 8, + STR: 'sway', + STR_2: 'forc', + STR_3: 'fuel', + LAST_U8: 16, + }; + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + const script = new ScriptWithDynamicConfigurables(deployer); + + const actualConfigurables = script.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + + it('should allow getting of dynamic configurables [updated configurables]', async () => { + const expectedConfigurables = { + BOOL: false, + U8: 0, + STR: 'STR3123123123123123123123', + STR_2: 'STR_2123123123123123123', + STR_3: 'STR_3123123123123123123123', + LAST_U8: 0, + }; + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + const script = new ScriptWithDynamicConfigurables(deployer); + script.setConfigurableConstants(expectedConfigurables); + + const actualConfigurables = script.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + }); + + describe('ScriptLoader', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + const loader = await script.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + const { waitForResult } = await loader.functions + .main(true, 8, 'sway', 'forc', 'fuel', 16) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + // TODO: this requires changes on the compiler to allow. + it.todo('should allow setting of dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + const loader = await script.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + loader.setConfigurableConstants({ + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }); + + const { waitForResult } = await loader.functions + .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should return false for script with incorrect data', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + const loader = await script.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + const { waitForResult } = await loader.functions + .main(true, 8, 'sway', 'forc', 'fuel-incorrect', 16) + .call(); + + const { value } = await waitForResult(); + + expect(value).toBe(false); + }); + + it('should allow getting of dynamic configurables [initial configurables]', async () => { + const expectedInitialConfigurables = { + BOOL: true, + U8: 8, + STR: 'sway', + STR_2: 'forc', + STR_3: 'fuel', + LAST_U8: 16, + }; + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + const script = new ScriptWithDynamicConfigurables(deployer); + const loader = await script.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + const actualInitialConfigurables = loader.getConfigurables(); + + expect(actualInitialConfigurables).toEqual(expectedInitialConfigurables); + }); + + it('should allow getting of dynamic configurables [updated configurables]', async () => { + const expectedConfigurables = { + BOOL: false, + U8: 0, + STR: 'STR3123123123123123123123', + STR_2: 'STR_2123123123123123123', + STR_3: 'STR_3123123123123123123123', + LAST_U8: 0, + }; + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + const script = new ScriptWithDynamicConfigurables(deployer); + script.setConfigurableConstants(expectedConfigurables); + const loader = await script.deploy(deployer).then(({ waitForResult }) => waitForResult()); + + const actualConfigurables = loader.getConfigurables(); + + expect(actualConfigurables).toEqual(expectedConfigurables); + }); + }); +}); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index 23d1a4f8fef..2613326b716 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml @@ -14,6 +14,7 @@ members = [ "complex-script", "configurable-contract", "coverage-contract", + "contract-with-dynamic-configurables", "data-structure-library", "generic-types-contract", "large-contract", @@ -43,6 +44,7 @@ members = [ "predicate-validate-transfer", "predicate-vector-types", "predicate-with-configurable", + "predicate-with-dynamic-configurables", "predicate-with-more-configurables", "predicate-false-configurable", "proxy-contract", @@ -63,6 +65,7 @@ members = [ "script-str-slice", "script-with-array", "script-with-configurable", + "script-with-dynamic-configurables", "script-with-more-configurable", "script-with-vector", "script-with-options", diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/Forc.toml new file mode 100644 index 00000000000..85fd0724065 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "contract-with-dynamic-configurables" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw new file mode 100644 index 00000000000..a0645430d3c --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw @@ -0,0 +1,34 @@ +contract; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + STR: str = "sway", + STR_2: str = "forc", + STR_3: str = "fuel", + LAST_U8: u8 = 16, +} + +abi ContractWithDynamicConfigurables { + fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, + ) -> bool; +} + +impl ContractWithDynamicConfigurables for Contract { + fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, + ) -> bool { + some_bool == BOOL && some_u8 == U8 && some_str == STR && some_str_2 == STR_2 && some_str_3 == STR_3 && some_last_u8 == LAST_U8 + } +} diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml new file mode 100644 index 00000000000..52b148cc8c1 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "predicate-with-dynamic-configurable" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw new file mode 100644 index 00000000000..2db407c9b0f --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw @@ -0,0 +1,26 @@ +predicate; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + STR: str = "sway", + STR_2: str = "forc", + STR_3: str = "fuel", + LAST_U8: u8 = 16, +} + +fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, +) -> bool { + some_bool == BOOL && + some_u8 == U8 && + some_str == STR && + some_str_2 == STR_2 && + some_str_3 == STR_3 && + some_last_u8 == LAST_U8 +} \ No newline at end of file diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml new file mode 100644 index 00000000000..2b4017da87d --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "script-with-dynamic-configurables" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw new file mode 100644 index 00000000000..75f11805314 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw @@ -0,0 +1,39 @@ +script; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + STR: str = "sway", + STR_2: str = "forc", + STR_3: str = "fuel", + LAST_U8: u8 = 16, +} + +fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, +) -> bool { + log(BOOL); + log(some_bool); + + log(U8); + log(some_u8); + + log(STR); + log(some_str); + + log(STR_2); + log(some_str_2); + + log(STR_3); + log(some_str_3); + + log(LAST_U8); + log(some_last_u8); + + some_bool == BOOL && some_u8 == U8 && some_str == STR && some_str_2 == STR_2 && some_str_3 == STR_3 && some_last_u8 == LAST_U8 +} diff --git a/packages/script/src/script.test.ts b/packages/script/src/script.test.ts index b00ebe04f37..9bea7affb0f 100644 --- a/packages/script/src/script.test.ts +++ b/packages/script/src/script.test.ts @@ -127,7 +127,7 @@ describe('Script', () => { () => newScript.setConfigurableConstants({ FEE: 8 }), new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - 'Error setting configurable constants: The script does not have configurable constants to be set.' + 'Error setting configurable constants, the program does not have configurable constants to be set.' ) ); }); @@ -156,7 +156,7 @@ describe('Script', () => { () => script.setConfigurableConstants({ NOT_DEFINED: 8 }), new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: The script does not have a configurable constant named: 'NOT_DEFINED'.` + `Error setting configurable constants, unknown keys supplied:\n- 'NOT_DEFINED'` ) ); }); diff --git a/packages/script/src/script.ts b/packages/script/src/script.ts index fd86461b8c9..831e0044933 100644 --- a/packages/script/src/script.ts +++ b/packages/script/src/script.ts @@ -1,8 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Interface } from '@fuel-ts/abi-coder'; import type { InputValue, JsonAbi } from '@fuel-ts/abi-coder'; -import { deployScriptOrPredicate, type Account, type Provider } from '@fuel-ts/account'; -import { FuelError } from '@fuel-ts/errors'; +import { + createConfigurables, + deployScriptOrPredicate, + type Account, + type Provider, +} from '@fuel-ts/account'; import type { BN } from '@fuel-ts/math'; import type { ScriptRequest } from '@fuel-ts/program'; import type { BytesLike } from '@fuel-ts/utils'; @@ -88,39 +92,26 @@ export class Script, TOutput> extends AbstractScript { * @throws Will throw an error if the script has no configurable constants to be set or if an invalid constant is provided. * @returns This instance of the `Script`. */ - setConfigurableConstants(configurables: { [name: string]: unknown }) { - try { - if (!Object.keys(this.interface.configurables).length) { - throw new FuelError( - FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `The script does not have configurable constants to be set` - ); - } - - Object.entries(configurables).forEach(([key, value]) => { - if (!this.interface.configurables[key]) { - throw new FuelError( - FuelError.CODES.CONFIGURABLE_NOT_FOUND, - `The script does not have a configurable constant named: '${key}'` - ); - } - - const { offset } = this.interface.configurables[key]; - - const encoded = this.interface.encodeConfigurable(key, value as InputValue); - - this.bytes.set(encoded, offset); - }); - } catch (err) { - throw new FuelError( - FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: ${(err).message}.` - ); - } + setConfigurableConstants(configurableValues: { [name: string]: unknown }) { + const configurables = createConfigurables({ + bytecode: this.bytes, + abi: this.interface, + }); + + this.bytes = configurables.set(configurableValues); return this; } + getConfigurables(): Record { + const configurables = createConfigurables({ + bytecode: this.bytes, + abi: this.interface, + }); + + return configurables.all(); + } + /** * * @param account - The account used to pay the deployment costs.