From 6a0c413d1be8bee333005def05df7a83ed9c545a Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 5 Feb 2025 17:09:49 +0000 Subject: [PATCH 01/19] chore: update version --- internal/forc/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/forc/VERSION b/internal/forc/VERSION index 0b8bc4d6f99..1e662f805b1 100644 --- a/internal/forc/VERSION +++ b/internal/forc/VERSION @@ -1 +1 @@ -0.66.6 +git:xunilrj/dynamic-types-configurables \ No newline at end of file From cc2afe97090c73bb4a3fb0cd9826591dc0db8aa3 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 5 Feb 2025 17:14:39 +0000 Subject: [PATCH 02/19] chore: updated templates --- .../templates/contract-with-configurable/main.hbs | 9 ++++++--- .../templates/predicate-with-configurable/main.hbs | 6 ++++-- .../fixtures/templates/script-with-configurable/main.hbs | 3 ++- 3 files changed, 12 insertions(+), 6 deletions(-) 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 d6840c81bd4..1d9ce2a97c8 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 @@ -160,17 +160,20 @@ const abi = { { "name": "SHOULD_RETURN", "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", - "offset": 2584 + "offset": 2584, + "indirect": false }, { "name": "AN_OPTION", "concreteTypeId": "2da102c46c7263beeed95818cd7bee801716ba8303dddafdcd0f6c9efda4a0f1", - "offset": 2560 + "offset": 2560, + "indirect": false }, { "name": "A_GENERIC_STRUCT", "concreteTypeId": "71df88006611ffff852cf617defb70f77adaf507305088cedd41d276c783aab0", - "offset": 2576 + "offset": 2576, + "indirect": false } ] }; 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 6760a9439c9..49c4a84cb0e 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 @@ -73,12 +73,14 @@ const abi = { { "name": "FEE", "concreteTypeId": "c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b", - "offset": 888 + "offset": 888, + "indirect": false }, { "name": "ADDRESS", "concreteTypeId": "7c5ee1cecf5f8eacd1284feb5f0bf2bdea533a51e2f0c9aabe9236d335989f3b", - "offset": 856 + "offset": 856, + "indirect": false } ] }; 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 a574e27c8a1..b687cd750d0 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 @@ -81,7 +81,8 @@ const abi = { { "name": "SHOULD_RETURN", "concreteTypeId": "b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903", - "offset": 696 + "offset": 696, + "indirect": false } ] }; From 30d9b6eb1202378ca7d6a5e083a0f9838b074df9 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Thu, 6 Feb 2025 12:10:36 +0000 Subject: [PATCH 03/19] chore: update types --- packages/abi-coder/src/types/JsonAbi.ts | 1 + packages/abi-coder/src/types/JsonAbiNew.ts | 1 + packages/abi-coder/src/utils/transpile-abi.ts | 1 + packages/abi-typegen/src/types/interfaces/JsonAbi.ts | 1 + packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts | 1 + packages/abi-typegen/src/utils/transpile-abi.ts | 1 + 6 files changed, 6 insertions(+) diff --git a/packages/abi-coder/src/types/JsonAbi.ts b/packages/abi-coder/src/types/JsonAbi.ts index eed9fee4d46..8e639d13b91 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 6aff2ad11c1..a35ed84fbde 100644 --- a/packages/abi-coder/src/types/JsonAbiNew.ts +++ b/packages/abi-coder/src/types/JsonAbiNew.ts @@ -98,4 +98,5 @@ export interface Configurable { readonly name: string; readonly concreteTypeId: string; readonly offset: number; + readonly indirect: boolean; } diff --git a/packages/abi-coder/src/utils/transpile-abi.ts b/packages/abi-coder/src/utils/transpile-abi.ts index 2d1d061d7b1..e1929a5b111 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, })); // 5. loggedTypes diff --git a/packages/abi-typegen/src/types/interfaces/JsonAbi.ts b/packages/abi-typegen/src/types/interfaces/JsonAbi.ts index eed9fee4d46..8e639d13b91 100644 --- a/packages/abi-typegen/src/types/interfaces/JsonAbi.ts +++ b/packages/abi-typegen/src/types/interfaces/JsonAbi.ts @@ -54,4 +54,5 @@ export interface JsonAbiConfigurable { name: string; configurableType: JsonAbiArgument; offset: number; + indirect: boolean; } diff --git a/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts b/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts index 6aff2ad11c1..a35ed84fbde 100644 --- a/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts +++ b/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts @@ -98,4 +98,5 @@ export interface Configurable { readonly name: string; readonly concreteTypeId: string; readonly offset: number; + readonly indirect: boolean; } diff --git a/packages/abi-typegen/src/utils/transpile-abi.ts b/packages/abi-typegen/src/utils/transpile-abi.ts index 2d1d061d7b1..e1929a5b111 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, })); // 5. loggedTypes From b31a523e264ef36d4bb55b55819b51137db78134 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Fri, 7 Feb 2025 11:54:45 +0000 Subject: [PATCH 04/19] chore: added dynamic configurable test for predicates --- .../test/fixtures/forc-projects/Forc.toml | 4 +++ .../Forc.toml | 6 +++++ .../src/main.sw | 26 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index acff8f4e760..3114af10683 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml @@ -41,6 +41,7 @@ members = [ "predicate-validate-transfer", "predicate-vector-types", "predicate-with-configurable", + "predicate-with-dynamic-configurables", "predicate-with-more-configurables", "predicate-false-configurable", "proxy-contract", @@ -78,3 +79,6 @@ members = [ "vectors", "void", ] + +[patch.'https://github.com/fuellabs/sway'] +std = { git = "https://github.com/fuellabs/sway", branch = "xunilrj/dynamic-types-configurables" } \ No newline at end of file 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 From 401795978d4da42c408dcf228efbdeeebe74ffa8 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Fri, 7 Feb 2025 12:07:20 +0000 Subject: [PATCH 05/19] chore: enable backwards compatible ABI --- packages/abi-coder/src/types/JsonAbi.ts | 2 +- packages/abi-coder/src/types/JsonAbiNew.ts | 2 +- packages/abi-coder/src/utils/transpile-abi.ts | 2 +- packages/abi-typegen/src/types/interfaces/JsonAbi.ts | 2 +- packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts | 2 +- packages/abi-typegen/src/utils/transpile-abi.ts | 2 +- packages/abi-typegen/test/utils/getNewAbiTypegen.ts | 1 + 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/packages/abi-coder/src/types/JsonAbi.ts b/packages/abi-coder/src/types/JsonAbi.ts index 8e639d13b91..80b586cde9e 100644 --- a/packages/abi-coder/src/types/JsonAbi.ts +++ b/packages/abi-coder/src/types/JsonAbi.ts @@ -54,5 +54,5 @@ export interface JsonAbiConfigurable { name: string; configurableType: JsonAbiArgument; offset: number; - indirect: boolean; + indirect?: boolean; } diff --git a/packages/abi-coder/src/types/JsonAbiNew.ts b/packages/abi-coder/src/types/JsonAbiNew.ts index a35ed84fbde..ec2d90937b3 100644 --- a/packages/abi-coder/src/types/JsonAbiNew.ts +++ b/packages/abi-coder/src/types/JsonAbiNew.ts @@ -98,5 +98,5 @@ export interface Configurable { readonly name: string; readonly concreteTypeId: string; readonly offset: number; - readonly indirect: boolean; + readonly indirect?: boolean; } diff --git a/packages/abi-coder/src/utils/transpile-abi.ts b/packages/abi-coder/src/utils/transpile-abi.ts index e1929a5b111..e22e9c6f0b2 100644 --- a/packages/abi-coder/src/utils/transpile-abi.ts +++ b/packages/abi-coder/src/utils/transpile-abi.ts @@ -123,7 +123,7 @@ export function transpileAbi(abi) { name: conf.name, configurableType: parseConcreteType(abi, types, conf.concreteTypeId), offset: conf.offset, - indirect: conf.indirect, + 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 8e639d13b91..80b586cde9e 100644 --- a/packages/abi-typegen/src/types/interfaces/JsonAbi.ts +++ b/packages/abi-typegen/src/types/interfaces/JsonAbi.ts @@ -54,5 +54,5 @@ export interface JsonAbiConfigurable { name: string; configurableType: JsonAbiArgument; offset: number; - indirect: boolean; + indirect?: boolean; } diff --git a/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts b/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts index a35ed84fbde..ec2d90937b3 100644 --- a/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts +++ b/packages/abi-typegen/src/types/interfaces/JsonAbiNew.ts @@ -98,5 +98,5 @@ export interface Configurable { readonly name: string; readonly concreteTypeId: string; readonly offset: number; - readonly indirect: boolean; + readonly indirect?: boolean; } diff --git a/packages/abi-typegen/src/utils/transpile-abi.ts b/packages/abi-typegen/src/utils/transpile-abi.ts index e1929a5b111..e22e9c6f0b2 100644 --- a/packages/abi-typegen/src/utils/transpile-abi.ts +++ b/packages/abi-typegen/src/utils/transpile-abi.ts @@ -123,7 +123,7 @@ export function transpileAbi(abi) { name: conf.name, configurableType: parseConcreteType(abi, types, conf.concreteTypeId), offset: conf.offset, - indirect: conf.indirect, + indirect: conf.indirect ?? false, })); // 5. loggedTypes diff --git a/packages/abi-typegen/test/utils/getNewAbiTypegen.ts b/packages/abi-typegen/test/utils/getNewAbiTypegen.ts index 360cc6bcb2b..c55a74a0570 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, }, ]; From 7d8c7a6a126f10d2d9fa7377c4bb86e80fb39e07 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Fri, 7 Feb 2025 12:26:31 +0000 Subject: [PATCH 06/19] chore: added method to Interface to getCoder by concreteTypeId --- packages/abi-coder/src/Interface.ts | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) 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]; } } From e00ef7e9cd3e8e2c358df488bc472f4b905e85dd Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Fri, 7 Feb 2025 12:27:03 +0000 Subject: [PATCH 07/19] feat: added function for reading configurables --- .../account/src/predicate/configurable.ts | 49 +++++++ .../src/predicate/configurables.test.ts | 122 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 packages/account/src/predicate/configurable.ts create mode 100644 packages/account/src/predicate/configurables.test.ts diff --git a/packages/account/src/predicate/configurable.ts b/packages/account/src/predicate/configurable.ts new file mode 100644 index 00000000000..cde699e0568 --- /dev/null +++ b/packages/account/src/predicate/configurable.ts @@ -0,0 +1,49 @@ +import { BigNumberCoder, type 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 { getBytecodeDataOffset } from '../utils/predicate-script-loader-instructions'; + +export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }) => { + const { bytecode, abi } = opts; + const bytecodeDataOffset = getBytecodeDataOffset(bytecode); + const dynamicOffsetCoder = new BigNumberCoder('u64'); + + const getConfigurable = (name: string) => { + const configurable = abi.configurables[name]; + if (!configurable) { + throw new FuelError( + ErrorCode.CONFIGURABLE_NOT_FOUND, + `A configurable with the '${name}' was not found in the ABI.` + ); + } + return configurable; + }; + + 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) => { + // Find the actual offset of the dynamic value + const [dynamicOffsetBn] = dynamicOffsetCoder.decode(bytecode, offset); + const dynamicOffset = bytecodeDataOffset + dynamicOffsetBn.toNumber(); + + // Read the dynamic value + const coder = abi.getCoder(concreteTypeId); + const [value] = coder.decode(bytecode, dynamicOffset); + return { name, value }; + }; + + const read = (name: string) => { + const configurable = getConfigurable(name); + const reader = configurable.indirect ? readIndirect : readDirect; + return reader(configurable); + }; + + return { + all: () => Object.keys(abi.configurables).map(read), + }; +}; diff --git a/packages/account/src/predicate/configurables.test.ts b/packages/account/src/predicate/configurables.test.ts new file mode 100644 index 00000000000..ac246fccc14 --- /dev/null +++ b/packages/account/src/predicate/configurables.test.ts @@ -0,0 +1,122 @@ +import { Interface } from '@fuel-ts/abi-coder'; +import { decompressBytecode } from '@fuel-ts/utils'; + +import { createConfigurables } from './configurable'; + +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, + }, + ], +}; + +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 }, + ]); + }); +}); From babab587ed2a42939fd987fea7f4964c61170511 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Fri, 7 Feb 2025 14:03:15 +0000 Subject: [PATCH 08/19] feat: added writer for direct configurables --- .../account/src/predicate/configurable.ts | 43 ++++++++++++++++++- .../src/predicate/configurables.test.ts | 23 ++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/packages/account/src/predicate/configurable.ts b/packages/account/src/predicate/configurable.ts index cde699e0568..db5b1a4e998 100644 --- a/packages/account/src/predicate/configurable.ts +++ b/packages/account/src/predicate/configurable.ts @@ -1,11 +1,13 @@ -import { BigNumberCoder, type Interface } from '@fuel-ts/abi-coder'; +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 { getBytecodeDataOffset } from '../utils/predicate-script-loader-instructions'; export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }) => { - const { bytecode, abi } = opts; + const { abi } = opts; + const bytecode = new Uint8Array(opts.bytecode); const bytecodeDataOffset = getBytecodeDataOffset(bytecode); const dynamicOffsetCoder = new BigNumberCoder('u64'); @@ -20,6 +22,9 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface return configurable; }; + /** + * Readers + */ const readDirect = ({ name, concreteTypeId, offset }: Configurable) => { const coder = abi.getCoder(concreteTypeId); const [value] = coder.decode(bytecode, offset); @@ -43,7 +48,41 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface return reader(configurable); }; + /** + * Writers + */ + const writeDirect = ( + bytes: Uint8Array, + { name, offset }: Configurable, + value: InputValue + ): Uint8Array => { + const encodedValue = abi.encodeConfigurable(name, value); + bytes.set(encodedValue, offset); + return bytes; + }; + + const writeIndirect = ( + bytes: Uint8Array, + { name, concreteTypeId, offset }: Configurable, + value: InputValue + ): Uint8Array => { + const test = true; + return bytes; + }; + + const write = (bytes: Uint8Array, name: string, value: InputValue) => { + const configurable = getConfigurable(name); + const writer = configurable.indirect ? writeIndirect : writeDirect; + return writer(bytes, configurable, value); + }; + return { all: () => Object.keys(abi.configurables).map(read), + set: (configurableValues: Record) => { + Object.entries(configurableValues).forEach(([name, value]) => { + write(bytecode, name, value); + }); + return bytecode; + }, }; }; diff --git a/packages/account/src/predicate/configurables.test.ts b/packages/account/src/predicate/configurables.test.ts index ac246fccc14..f6f9c1d2347 100644 --- a/packages/account/src/predicate/configurables.test.ts +++ b/packages/account/src/predicate/configurables.test.ts @@ -119,4 +119,27 @@ describe('configurables', () => { { 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([ + { name: 'BOOL', value: false }, + { name: 'U8', value: 16 }, + { name: 'STR', value: 'sway' }, + { name: 'STR_2', value: 'forc' }, + { name: 'STR_3', value: 'fuel' }, + { name: 'LAST_U8', value: 32 }, + ]); + }); }); From 749e5e1fa70cf2c0128e77c1af2080ab15f81f0b Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 12 Feb 2025 09:37:21 +0000 Subject: [PATCH 09/19] feat: added configurables encapulation --- .../account/src/predicate/configurable.ts | 107 +++++++++++++----- .../src/predicate/configurables.test.ts | 71 ++++++++++-- 2 files changed, 139 insertions(+), 39 deletions(-) diff --git a/packages/account/src/predicate/configurable.ts b/packages/account/src/predicate/configurable.ts index db5b1a4e998..70f37410659 100644 --- a/packages/account/src/predicate/configurable.ts +++ b/packages/account/src/predicate/configurable.ts @@ -7,12 +7,13 @@ import { getBytecodeDataOffset } from '../utils/predicate-script-loader-instruct export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }) => { const { abi } = opts; - const bytecode = new Uint8Array(opts.bytecode); + let bytecode = new Uint8Array(opts.bytecode); + const configurables = Object.values(abi.configurables); const bytecodeDataOffset = getBytecodeDataOffset(bytecode); const dynamicOffsetCoder = new BigNumberCoder('u64'); const getConfigurable = (name: string) => { - const configurable = abi.configurables[name]; + const configurable = configurables.find((conf) => conf.name === name); if (!configurable) { throw new FuelError( ErrorCode.CONFIGURABLE_NOT_FOUND, @@ -22,6 +23,12 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface return configurable; }; + const readIndirectOffset = ({ offset }: Pick) => { + const [dynamicOffsetBn] = dynamicOffsetCoder.decode(bytecode, offset); + const dynamicOffset = bytecodeDataOffset + dynamicOffsetBn.toNumber(); + return dynamicOffset; + }; + /** * Readers */ @@ -32,18 +39,13 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }; const readIndirect = ({ name, concreteTypeId, offset }: Configurable) => { - // Find the actual offset of the dynamic value - const [dynamicOffsetBn] = dynamicOffsetCoder.decode(bytecode, offset); - const dynamicOffset = bytecodeDataOffset + dynamicOffsetBn.toNumber(); - - // Read the dynamic value + const dynamicOffset = readIndirectOffset({ offset }); const coder = abi.getCoder(concreteTypeId); const [value] = coder.decode(bytecode, dynamicOffset); return { name, value }; }; - const read = (name: string) => { - const configurable = getConfigurable(name); + const read = (configurable: Configurable) => { const reader = configurable.indirect ? readIndirect : readDirect; return reader(configurable); }; @@ -51,37 +53,80 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface /** * Writers */ - const writeDirect = ( - bytes: Uint8Array, - { name, offset }: Configurable, - value: InputValue - ): Uint8Array => { + const writeDirect = ({ name, offset }: Configurable, value: InputValue) => { const encodedValue = abi.encodeConfigurable(name, value); - bytes.set(encodedValue, offset); - return bytes; + bytecode.set(encodedValue, offset); }; - const writeIndirect = ( - bytes: Uint8Array, - { name, concreteTypeId, offset }: Configurable, - value: InputValue - ): Uint8Array => { - const test = true; - return bytes; + 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 - bytecodeDataOffset; + + const encodedOffset = dynamicOffsetCoder.encode(newOffset); + bytecode.set(encodedOffset, configurable.offset); + }); }; - const write = (bytes: Uint8Array, name: string, value: InputValue) => { - const configurable = getConfigurable(name); + const write = (configurable: Configurable, value: InputValue) => { const writer = configurable.indirect ? writeIndirect : writeDirect; - return writer(bytes, configurable, value); + return writer(configurable, value); }; return { - all: () => Object.keys(abi.configurables).map(read), - set: (configurableValues: Record) => { - Object.entries(configurableValues).forEach(([name, value]) => { - write(bytecode, name, value); - }); + /** + * 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 array of all the configurables. + */ + all: () => configurables.map(read), + /** + * 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 }) => { + // TODO: add assertions for no configurables + + 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); + }); + return bytecode; }, }; diff --git a/packages/account/src/predicate/configurables.test.ts b/packages/account/src/predicate/configurables.test.ts index f6f9c1d2347..b62872445de 100644 --- a/packages/account/src/predicate/configurables.test.ts +++ b/packages/account/src/predicate/configurables.test.ts @@ -133,13 +133,68 @@ describe('configurables', () => { }); const entries = configurables.all(); - expect(entries).toEqual([ - { name: 'BOOL', value: false }, - { name: 'U8', value: 16 }, - { name: 'STR', value: 'sway' }, - { name: 'STR_2', value: 'forc' }, - { name: 'STR_3', value: 'fuel' }, - { name: 'LAST_U8', value: 32 }, - ]); + 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 + ]) + ); }); }); From 97e1f6726d60f185fe48a8eac109510df85bfa54 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 12 Feb 2025 09:37:43 +0000 Subject: [PATCH 10/19] feat: use configurables with predicate --- packages/account/src/predicate/predicate.ts | 33 +-- ...edicate-with-dynamic-configurables.test.ts | 190 ++++++++++++++++++ 2 files changed, 198 insertions(+), 25 deletions(-) create mode 100644 packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts diff --git a/packages/account/src/predicate/predicate.ts b/packages/account/src/predicate/predicate.ts index e7bb9eec1b6..3f607e9ef34 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -25,6 +25,7 @@ import type { } from '../providers'; import { deployScriptOrPredicate } from '../utils/deployScriptOrPredicate'; +import { createConfigurables } from './configurable'; import { getPredicateRoot } from './utils'; export type PredicateParams< @@ -250,42 +251,24 @@ export class Predicate< * @returns The mutated bytes with the configurable constants set. */ private static setConfigurableConstants( - bytes: Uint8Array, + bytecode: 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); + const configurables = createConfigurables({ + bytecode, + abi: abiInterface, }); + + const mutatedBytecode = configurables.set(configurableConstants); + return mutatedBytecode; } catch (err) { throw new FuelError( ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, `Error setting configurable constants: ${(err).message}.` ); } - - return mutatedBytes; } /** 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..f9ee662ae21 --- /dev/null +++ b/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts @@ -0,0 +1,190 @@ +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/ + ); + }); + }); + + 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 loader = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel', 16], + }); + const { waitForResult: waitForDeploy } = await loader.deploy(deployer); + const predicate = await waitForDeploy(); + + // Fund predicate + await funder.transfer(predicate.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await predicate.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 setting of dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const loader = 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 loader.deploy(deployer); + const predicate = await waitForDeploy(); + + // Fund predicate + await funder.transfer(predicate.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await predicate.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 loader = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + const { waitForResult: waitForDeploy } = await loader.deploy(deployer); + const predicate = await waitForDeploy(); + + // Fund predicate + await funder.transfer(predicate.address, 1000); + + // Transfer from predicate -> receiver + await expect(() => predicate.transfer(receiver.address, 100)).rejects.toThrow( + /PredicateVerificationFailed/ + ); + }); + }); +}); From 71dc8300f7516cbcdc99d8d52db4233dafa56f04 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 12 Feb 2025 10:57:32 +0000 Subject: [PATCH 11/19] chore: shuffle around configurable wrapper --- packages/account/src/index.ts | 1 + packages/account/src/predicate/predicate.ts | 2 +- .../configurables.test.ts => utils/createConfigurables.test.ts} | 2 +- .../{predicate/configurable.ts => utils/createConfigurables.ts} | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) rename packages/account/src/{predicate/configurables.test.ts => utils/createConfigurables.test.ts} (99%) rename packages/account/src/{predicate/configurable.ts => utils/createConfigurables.ts} (98%) 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 3f607e9ef34..93181f3ac1b 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -23,9 +23,9 @@ import type { TransactionRequestLike, TransactionResponse, } from '../providers'; +import { createConfigurables } from '../utils/createConfigurables'; import { deployScriptOrPredicate } from '../utils/deployScriptOrPredicate'; -import { createConfigurables } from './configurable'; import { getPredicateRoot } from './utils'; export type PredicateParams< diff --git a/packages/account/src/predicate/configurables.test.ts b/packages/account/src/utils/createConfigurables.test.ts similarity index 99% rename from packages/account/src/predicate/configurables.test.ts rename to packages/account/src/utils/createConfigurables.test.ts index b62872445de..645142a762c 100644 --- a/packages/account/src/predicate/configurables.test.ts +++ b/packages/account/src/utils/createConfigurables.test.ts @@ -1,7 +1,7 @@ import { Interface } from '@fuel-ts/abi-coder'; import { decompressBytecode } from '@fuel-ts/utils'; -import { createConfigurables } from './configurable'; +import { createConfigurables } from './createConfigurables'; const abi = { programType: 'predicate', diff --git a/packages/account/src/predicate/configurable.ts b/packages/account/src/utils/createConfigurables.ts similarity index 98% rename from packages/account/src/predicate/configurable.ts rename to packages/account/src/utils/createConfigurables.ts index 70f37410659..5339308dd59 100644 --- a/packages/account/src/predicate/configurable.ts +++ b/packages/account/src/utils/createConfigurables.ts @@ -3,7 +3,7 @@ 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 { getBytecodeDataOffset } from '../utils/predicate-script-loader-instructions'; +import { getBytecodeDataOffset } from './predicate-script-loader-instructions'; export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }) => { const { abi } = opts; From f1c7c9e19bdf93a9cafa4d345981f4bd24d2d570 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 12 Feb 2025 10:58:11 +0000 Subject: [PATCH 12/19] feat: implement contract configurables --- packages/contract/src/contract-factory.ts | 32 ++---- ...ontract-with-dynamic-configurables.test.ts | 100 ++++++++++++++++++ .../test/fixtures/forc-projects/Forc.toml | 1 + .../Forc.toml | 6 ++ .../src/main.sw | 34 ++++++ 5 files changed, 147 insertions(+), 26 deletions(-) create mode 100644 packages/fuel-gauge/src/contract-with-dynamic-configurables.test.ts create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw diff --git a/packages/contract/src/contract-factory.ts b/packages/contract/src/contract-factory.ts index 0ad2c00e955..cf007fa7702 100644 --- a/packages/contract/src/contract-factory.ts +++ b/packages/contract/src/contract-factory.ts @@ -13,6 +13,7 @@ import { BlobTransactionRequest, TransactionStatus, calculateGasFee, + createConfigurables, } from '@fuel-ts/account'; import { randomBytes } from '@fuel-ts/crypto'; import { ErrorCode, FuelError } from '@fuel-ts/errors'; @@ -385,33 +386,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/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index 3114af10683..e878e630fb2 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", 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 + } +} From 89b2c89e8d4c93c6ef578b76dd78ac0adde6acf8 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Wed, 12 Feb 2025 14:42:24 +0000 Subject: [PATCH 13/19] feat: implement script configurables --- .../script-with-dynamic-configurables.test.ts | 143 ++++++++++++++++++ .../test/fixtures/forc-projects/Forc.toml | 1 + .../Forc.toml | 6 + .../src/main.sw | 21 +++ packages/script/src/script.ts | 34 ++--- 5 files changed, 183 insertions(+), 22 deletions(-) create mode 100644 packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml create mode 100644 packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw 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..5a278afb003 --- /dev/null +++ b/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts @@ -0,0 +1,143 @@ +import { getBytecodeConfigurableOffset, getBytecodeDataOffset } from '@fuel-ts/account'; +import { arrayify, hexlify } from '@fuel-ts/utils'; +import { log } from 'console'; +import { writeFileSync } from 'fs'; +import { BigNumberCoder } from 'fuels'; +import { launchTestNode } from 'fuels/test-utils'; + +import { + ScriptWithDynamicConfigurables, + ScriptWithDynamicConfigurablesLoader, +} 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); + }); + }); + + describe('PredicateLoader', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const scriptLoader = new ScriptWithDynamicConfigurables(deployer); + const { waitForResult: waitForDeploy } = await scriptLoader.deploy(deployer); + const script = await waitForDeploy(); + + 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); + const { waitForResult: waitForDeploy } = await script.deploy(deployer); + const loader = await waitForDeploy(); + + loader.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 scriptLoader = new ScriptWithDynamicConfigurables(deployer); + const { waitForResult: waitForDeploy } = await scriptLoader.deploy(deployer); + const script = await waitForDeploy(); + + const { waitForResult } = await script.functions + .main(true, 8, 'sway', 'forc', 'fuel-incorrect', 16) + .call(); + + const { value } = await waitForResult(); + + expect(value).toBe(false); + }); + }); +}); diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml index e878e630fb2..762daa3d831 100644 --- a/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml +++ b/packages/fuel-gauge/test/fixtures/forc-projects/Forc.toml @@ -62,6 +62,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/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..214f5f31c24 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw @@ -0,0 +1,21 @@ +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 { + 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.ts b/packages/script/src/script.ts index fd86461b8c9..6168c8f3e5b 100644 --- a/packages/script/src/script.ts +++ b/packages/script/src/script.ts @@ -1,7 +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 { + createConfigurables, + deployScriptOrPredicate, + type Account, + type Provider, +} from '@fuel-ts/account'; import { FuelError } from '@fuel-ts/errors'; import type { BN } from '@fuel-ts/math'; import type { ScriptRequest } from '@fuel-ts/program'; @@ -88,29 +93,14 @@ 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 }) { + setConfigurableConstants(configurableValues: { [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); + const configurables = createConfigurables({ + bytecode: this.bytes, + abi: this.interface, }); + + this.bytes = configurables.set(configurableValues); } catch (err) { throw new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, From 38c6ce6da29a85ecfd1529c0a96e06dc9281bdff Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Thu, 13 Feb 2025 13:13:29 +0000 Subject: [PATCH 14/19] chore: added `isBytecodeLoader` --- .../src/utils/createConfigurables.test.ts | 22 ++++++++++++++++++- .../predicate-script-loader-instructions.ts | 16 ++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/packages/account/src/utils/createConfigurables.test.ts b/packages/account/src/utils/createConfigurables.test.ts index 645142a762c..95a767f6193 100644 --- a/packages/account/src/utils/createConfigurables.test.ts +++ b/packages/account/src/utils/createConfigurables.test.ts @@ -1,7 +1,12 @@ import { Interface } from '@fuel-ts/abi-coder'; -import { decompressBytecode } from '@fuel-ts/utils'; +import { ZeroBytes32 } from '@fuel-ts/address/configs'; +import { arrayify, decompressBytecode } from '@fuel-ts/utils'; import { createConfigurables } from './createConfigurables'; +import { + getPredicateScriptLoaderInstructions, + isBytecodeLoader, +} from './predicate-script-loader-instructions'; const abi = { programType: 'predicate', @@ -101,6 +106,21 @@ 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==' ); +// TODO: move to more appropriate place +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); + }); +}); + describe('configurables', () => { it('should return the configurables', () => { const configurables = createConfigurables({ diff --git a/packages/account/src/utils/predicate-script-loader-instructions.ts b/packages/account/src/utils/predicate-script-loader-instructions.ts index 4b021d61af4..8142f288efe 100644 --- a/packages/account/src/utils/predicate-script-loader-instructions.ts +++ b/packages/account/src/utils/predicate-script-loader-instructions.ts @@ -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,6 +68,20 @@ export function getLegacyBlobId(bytecode: Uint8Array): string { return sha256(byteCodeWithoutDataSection); } +/** + * 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; +} + export function getPredicateScriptLoaderInstructions( originalBinary: Uint8Array, blobId: Uint8Array From f7d0798c19cd0e714a17f8f94f99b42af1d9aa0e Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Thu, 13 Feb 2025 19:58:21 +0000 Subject: [PATCH 15/19] chore: refactor the loader instruction set --- .../predicate-script-loader-instructions.ts | 78 ++++++++++--------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/packages/account/src/utils/predicate-script-loader-instructions.ts b/packages/account/src/utils/predicate-script-loader-instructions.ts index 8142f288efe..5a23c7b250a 100644 --- a/packages/account/src/utils/predicate-script-loader-instructions.ts +++ b/packages/account/src/utils/predicate-script-loader-instructions.ts @@ -82,23 +82,16 @@ export function isBytecodeLoader(bytecode: Uint8Array): boolean { return preample === LDC_INSTRUCTION_PREAMPLE; } -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) - +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), @@ -106,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. @@ -134,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 @@ -144,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. @@ -163,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 @@ -177,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); @@ -210,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); From ea690e82d9497332cb57e5f7a646d8461384d521 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Tue, 18 Feb 2025 09:06:44 +0000 Subject: [PATCH 16/19] feat: added method to extract the correct data offset and blob ID --- .../src/utils/createConfigurables.test.ts | 55 +++++++++++++++++++ .../predicate-script-loader-instructions.ts | 51 ++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/packages/account/src/utils/createConfigurables.test.ts b/packages/account/src/utils/createConfigurables.test.ts index 95a767f6193..3d88567e817 100644 --- a/packages/account/src/utils/createConfigurables.test.ts +++ b/packages/account/src/utils/createConfigurables.test.ts @@ -4,6 +4,7 @@ import { arrayify, decompressBytecode } from '@fuel-ts/utils'; import { createConfigurables } from './createConfigurables'; import { + extractBlobIdAndDataOffset, getPredicateScriptLoaderInstructions, isBytecodeLoader, } from './predicate-script-loader-instructions'; @@ -102,6 +103,7 @@ const abi = { ], }; +// 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==' ); @@ -121,6 +123,59 @@ describe('isBytecodeLoader', () => { }); }); +// TODO: move to more appropriate place +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('configurables', () => { it('should return the configurables', () => { const configurables = createConfigurables({ diff --git a/packages/account/src/utils/predicate-script-loader-instructions.ts b/packages/account/src/utils/predicate-script-loader-instructions.ts index 5a23c7b250a..dfd50300f8a 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; @@ -239,3 +239,52 @@ export function getPredicateScriptLoaderInstructions( return { loaderBytecode }; } + +/** + * 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, + }; +} From 63e50acaf194318808149c64d065bdfbde152524 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Tue, 18 Feb 2025 09:11:47 +0000 Subject: [PATCH 17/19] feat: use the correct data offset for configurables --- .../account/src/utils/createConfigurables.ts | 8 ++++---- .../src/utils/deployScriptOrPredicate.ts | 17 ++++++++++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/packages/account/src/utils/createConfigurables.ts b/packages/account/src/utils/createConfigurables.ts index 5339308dd59..facaa89b090 100644 --- a/packages/account/src/utils/createConfigurables.ts +++ b/packages/account/src/utils/createConfigurables.ts @@ -3,13 +3,13 @@ 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 { getBytecodeDataOffset } from './predicate-script-loader-instructions'; +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 bytecodeDataOffset = getBytecodeDataOffset(bytecode); + const { dataOffset } = extractBlobIdAndDataOffset(bytecode); const dynamicOffsetCoder = new BigNumberCoder('u64'); const getConfigurable = (name: string) => { @@ -25,7 +25,7 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface const readIndirectOffset = ({ offset }: Pick) => { const [dynamicOffsetBn] = dynamicOffsetCoder.decode(bytecode, offset); - const dynamicOffset = bytecodeDataOffset + dynamicOffsetBn.toNumber(); + const dynamicOffset = dataOffset + dynamicOffsetBn.toNumber(); return dynamicOffset; }; @@ -84,7 +84,7 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface .filter((configurable) => configurable.indirect && configurable.offset > offset) .forEach((configurable) => { const newDynamicOffset = readIndirectOffset({ offset: configurable.offset }); - const newOffset = newDynamicOffset + additionalOffset - bytecodeDataOffset; + const newOffset = newDynamicOffset + additionalOffset - dataOffset; const encodedOffset = dynamicOffsetCoder.encode(newOffset); bytecode.set(encodedOffset, configurable.offset); diff --git a/packages/account/src/utils/deployScriptOrPredicate.ts b/packages/account/src/utils/deployScriptOrPredicate.ts index 97d12bff45f..714427ffd88 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 { BigNumberCoder, type JsonAbi } from '@fuel-ts/abi-coder'; import { FuelError, ErrorCode } from '@fuel-ts/errors'; import { bn } from '@fuel-ts/math'; import { arrayify } from '@fuel-ts/utils'; @@ -8,6 +8,7 @@ import { BlobTransactionRequest, calculateGasFee, TransactionStatus } from '../p import { getBytecodeConfigurableOffset, + getBytecodeDataOffset, getBytecodeId, getPredicateScriptLoaderInstructions, } from './predicate-script-loader-instructions'; @@ -66,8 +67,21 @@ export async function deployScriptOrPredicate({ const blobId = getBytecodeId(arrayify(bytecode)); const configurableOffset = getBytecodeConfigurableOffset(arrayify(bytecode)); + const dataOffset = getBytecodeDataOffset(arrayify(bytecode)); const byteCodeWithoutConfigurableSection = bytecode.slice(0, configurableOffset); + // Adjust the indirect configurable offsets to point to the new data offsets for the loader + 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, @@ -79,6 +93,7 @@ export async function deployScriptOrPredicate({ arrayify(blobId) ); + // Adjust the ABI configurable offset const newConfigurableOffsetDiff = byteCodeWithoutConfigurableSection.length - (blobOffset || 0); const newAbi = adjustConfigurableOffsets(abi, newConfigurableOffsetDiff); From 105b0c765362ec1fc057a5ba44c9c06cd836a75e Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Tue, 18 Feb 2025 10:56:41 +0000 Subject: [PATCH 18/19] chore: reintroduce configurable constant error handling --- packages/account/src/predicate/predicate.ts | 40 +++--------------- .../account/src/utils/createConfigurables.ts | 41 +++++++++++++++---- .../predicate/predicate-configurables.test.ts | 4 +- packages/script/src/script.test.ts | 4 +- packages/script/src/script.ts | 20 +++------ 5 files changed, 48 insertions(+), 61 deletions(-) diff --git a/packages/account/src/predicate/predicate.ts b/packages/account/src/predicate/predicate.ts index 93181f3ac1b..9a0892202d2 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -192,11 +192,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 { @@ -242,35 +242,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( - bytecode: Uint8Array, - configurableConstants: { [name: string]: unknown }, - abiInterface: Interface - ) { - try { - const configurables = createConfigurables({ - bytecode, - abi: abiInterface, - }); - - const mutatedBytecode = configurables.set(configurableConstants); - return mutatedBytecode; - } catch (err) { - throw new FuelError( - ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: ${(err).message}.` - ); - } - } - /** * Returns the index of the witness placeholder that was added to this predicate. * If no witness placeholder was added, it returns -1. @@ -330,6 +301,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.ts b/packages/account/src/utils/createConfigurables.ts index facaa89b090..45e6a40d9c5 100644 --- a/packages/account/src/utils/createConfigurables.ts +++ b/packages/account/src/utils/createConfigurables.ts @@ -117,15 +117,38 @@ export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface * @returns The mutated bytecode. */ set: (configurableValues: { [name: string]: unknown }) => { - // TODO: add assertions for no configurables - - 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); - }); + 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/fuel-gauge/src/predicate/predicate-configurables.test.ts b/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts index 6e08cf1b6c7..321318f44ce 100644 --- a/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-configurables.test.ts @@ -270,7 +270,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.' ) ); }); @@ -291,7 +291,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/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 6168c8f3e5b..7a0ad108263 100644 --- a/packages/script/src/script.ts +++ b/packages/script/src/script.ts @@ -7,7 +7,6 @@ import { type Account, type Provider, } from '@fuel-ts/account'; -import { FuelError } from '@fuel-ts/errors'; import type { BN } from '@fuel-ts/math'; import type { ScriptRequest } from '@fuel-ts/program'; import type { BytesLike } from '@fuel-ts/utils'; @@ -94,19 +93,12 @@ export class Script, TOutput> extends AbstractScript { * @returns This instance of the `Script`. */ setConfigurableConstants(configurableValues: { [name: string]: unknown }) { - try { - const configurables = createConfigurables({ - bytecode: this.bytes, - abi: this.interface, - }); - - this.bytes = configurables.set(configurableValues); - } catch (err) { - throw new FuelError( - FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: ${(err).message}.` - ); - } + const configurables = createConfigurables({ + bytecode: this.bytes, + abi: this.interface, + }); + + this.bytes = configurables.set(configurableValues); return this; } From 712e61d52532047525a258a79bcad6e7718a2b80 Mon Sep 17 00:00:00 2001 From: Peter Smith Date: Tue, 18 Feb 2025 18:54:29 +0000 Subject: [PATCH 19/19] WIP changes --- ...edicate-with-dynamic-configurables.test.ts | 68 ++++++++++++++----- packages/fuel-gauge/src/script-deploy.test.ts | 0 .../script-with-dynamic-configurables.test.ts | 34 ++++------ .../src/main.sw | 18 +++++ 4 files changed, 82 insertions(+), 38 deletions(-) create mode 100644 packages/fuel-gauge/src/script-deploy.test.ts 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 index f9ee662ae21..c258bfc9c5b 100644 --- a/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts +++ b/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts @@ -20,7 +20,7 @@ describe('Predicate with dynamic configurables', () => { const predicate = new PredicateWithDynamicConfigurable({ provider, - data: [true, 8, 'sway', 'forc', 'fuel', 16], + // data: [true, 8, 'sway', 'forc', 'fuel', 16], }); // Fund predicate @@ -105,18 +105,18 @@ describe('Predicate with dynamic configurables', () => { } = launched; const receiver = WalletUnlocked.generate({ provider }); - const loader = new PredicateWithDynamicConfigurable({ + const predicate = new PredicateWithDynamicConfigurable({ provider, - data: [true, 8, 'sway', 'forc', 'fuel', 16], + // data: [true, 8, 'sway', 'forc', 'fuel', 16], }); - const { waitForResult: waitForDeploy } = await loader.deploy(deployer); - const predicate = await waitForDeploy(); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); // Fund predicate - await funder.transfer(predicate.address, 1000); + await funder.transfer(loader.address, 1000); // Transfer from predicate -> receiver - const { waitForResult: waitForTransfer } = await predicate.transfer(receiver.address, 100); + const { waitForResult: waitForTransfer } = await loader.transfer(receiver.address, 100); const { isStatusSuccess } = await waitForTransfer(); expect(isStatusSuccess).toBe(true); @@ -125,7 +125,7 @@ describe('Predicate with dynamic configurables', () => { expect(balance).toEqual(expect.toEqualBn(100)); }); - it('should allow setting of dynamic configurables', async () => { + it('should allow initializing of dynamic configurables', async () => { using launched = await launchTestNode(); const { @@ -134,7 +134,7 @@ describe('Predicate with dynamic configurables', () => { } = launched; const receiver = WalletUnlocked.generate({ provider }); - const loader = new PredicateWithDynamicConfigurable({ + const predicate = new PredicateWithDynamicConfigurable({ provider, configurableConstants: { BOOL: false, @@ -146,14 +146,14 @@ describe('Predicate with dynamic configurables', () => { }, data: [false, 0, 'STR', 'STR_2', 'STR_3', 0], }); - const { waitForResult: waitForDeploy } = await loader.deploy(deployer); - const predicate = await waitForDeploy(); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); // Fund predicate - await funder.transfer(predicate.address, 1000); + await funder.transfer(loader.address, 1000); // Transfer from predicate -> receiver - const { waitForResult: waitForTransfer } = await predicate.transfer(receiver.address, 100); + const { waitForResult: waitForTransfer } = await loader.transfer(receiver.address, 100); const { isStatusSuccess } = await waitForTransfer(); expect(isStatusSuccess).toBe(true); @@ -162,6 +162,38 @@ describe('Predicate with dynamic configurables', () => { expect(balance).toEqual(expect.toEqualBn(100)); }); + it('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, + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + const newLoader = await loader.toNewInstance({ + data: [true, 8, 'sway', 'forc', 'fuel', 16], + }); + + // 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(); @@ -171,18 +203,18 @@ describe('Predicate with dynamic configurables', () => { } = launched; const receiver = WalletUnlocked.generate({ provider }); - const loader = new PredicateWithDynamicConfigurable({ + const predicate = new PredicateWithDynamicConfigurable({ provider, data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], }); - const { waitForResult: waitForDeploy } = await loader.deploy(deployer); - const predicate = await waitForDeploy(); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); // Fund predicate - await funder.transfer(predicate.address, 1000); + await funder.transfer(loader.address, 1000); // Transfer from predicate -> receiver - await expect(() => predicate.transfer(receiver.address, 100)).rejects.toThrow( + await expect(() => loader.transfer(receiver.address, 100)).rejects.toThrow( /PredicateVerificationFailed/ ); }); diff --git a/packages/fuel-gauge/src/script-deploy.test.ts b/packages/fuel-gauge/src/script-deploy.test.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts b/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts index 5a278afb003..329d3ad6be3 100644 --- a/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts +++ b/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts @@ -1,14 +1,6 @@ -import { getBytecodeConfigurableOffset, getBytecodeDataOffset } from '@fuel-ts/account'; -import { arrayify, hexlify } from '@fuel-ts/utils'; -import { log } from 'console'; -import { writeFileSync } from 'fs'; -import { BigNumberCoder } from 'fuels'; import { launchTestNode } from 'fuels/test-utils'; -import { - ScriptWithDynamicConfigurables, - ScriptWithDynamicConfigurablesLoader, -} from '../test/typegen'; +import { ScriptWithDynamicConfigurables } from '../test/typegen'; /** * @group node @@ -75,7 +67,7 @@ describe('Script with dynamic configurables', () => { }); }); - describe('PredicateLoader', () => { + describe('ScriptLoader', () => { it('should accept existing dynamic configurables', async () => { using launched = await launchTestNode(); const { @@ -89,8 +81,11 @@ describe('Script with dynamic configurables', () => { const { waitForResult } = await script.functions .main(true, 8, 'sway', 'forc', 'fuel', 16) .call(); - const { value } = await waitForResult(); + const { value, logs } = await waitForResult(); + console.log({ + logs, + }); expect(value).toBe(true); }); @@ -104,19 +99,18 @@ describe('Script with dynamic configurables', () => { const { waitForResult: waitForDeploy } = await script.deploy(deployer); const loader = await waitForDeploy(); - loader.setConfigurableConstants({ - BOOL: false, - U8: 0, - STR: 'STR', - STR_2: 'STR_2', - STR_3: 'STR_3', - LAST_U8: 0, + console.log({ + configurableConstants: loader.getConfigurableConstants(), }); - const { waitForResult } = await script.functions + const { waitForResult } = await loader.functions .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) .call(); - const { value } = await waitForResult(); + const { value, logs } = await waitForResult(); + + console.log({ + logs, + }); expect(value).toBe(true); }); 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 index 214f5f31c24..75f11805314 100644 --- 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 @@ -17,5 +17,23 @@ fn main( 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 }