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 diff --git a/packages/abi-coder/src/Interface.ts b/packages/abi-coder/src/Interface.ts index d2172d569fd..2d45add94e1 100644 --- a/packages/abi-coder/src/Interface.ts +++ b/packages/abi-coder/src/Interface.ts @@ -5,7 +5,7 @@ import { arrayify } from '@fuel-ts/utils'; import { AbiCoder } from './AbiCoder'; import { FunctionFragment } from './FunctionFragment'; -import type { DecodedValue, InputValue } from './encoding/coders/AbstractCoder'; +import type { Coder, DecodedValue, InputValue } from './encoding/coders/AbstractCoder'; import type { JsonAbiArgument, JsonAbiOld } from './types/JsonAbi'; import type { Configurable, JsonAbi } from './types/JsonAbiNew'; import { type EncodingVersion } from './utils/constants'; @@ -88,26 +88,23 @@ export class Interface { }); } - encodeType(concreteTypeId: string, value: InputValue): Uint8Array { + getCoder(concreteTypeId: string): Coder { const typeArg = parseConcreteType( this.jsonAbi, this.jsonAbiOld.types, concreteTypeId, '' ) as JsonAbiArgument; - return AbiCoder.encode(this.jsonAbiOld, typeArg, value, { - encoding: this.encoding, - }); + return AbiCoder.getCoder(this.jsonAbiOld, typeArg, { encoding: this.encoding }); } - decodeType(concreteTypeId: string, data: Uint8Array): [DecodedValue | undefined, number] { - const typeArg = parseConcreteType( - this.jsonAbi, - this.jsonAbiOld.types, - concreteTypeId, - '' - ) as JsonAbiArgument; + encodeType(concreteTypeId: string, value: InputValue): Uint8Array { + const coder = this.getCoder(concreteTypeId); + return coder.encode(value); + } - return AbiCoder.decode(this.jsonAbiOld, typeArg, data, 0, { encoding: this.encoding }); + decodeType(concreteTypeId: string, data: Uint8Array): [DecodedValue | undefined, number] { + const coder = this.getCoder(concreteTypeId); + return coder.decode(data, 0) as [DecodedValue | undefined, number]; } } diff --git a/packages/abi-coder/src/types/JsonAbi.ts b/packages/abi-coder/src/types/JsonAbi.ts index eed9fee4d46..80b586cde9e 100644 --- a/packages/abi-coder/src/types/JsonAbi.ts +++ b/packages/abi-coder/src/types/JsonAbi.ts @@ -54,4 +54,5 @@ export interface JsonAbiConfigurable { name: string; configurableType: JsonAbiArgument; offset: number; + indirect?: boolean; } diff --git a/packages/abi-coder/src/types/JsonAbiNew.ts b/packages/abi-coder/src/types/JsonAbiNew.ts index 6aff2ad11c1..ec2d90937b3 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..e22e9c6f0b2 100644 --- a/packages/abi-coder/src/utils/transpile-abi.ts +++ b/packages/abi-coder/src/utils/transpile-abi.ts @@ -123,6 +123,7 @@ export function transpileAbi(abi) { name: conf.name, configurableType: parseConcreteType(abi, types, conf.concreteTypeId), offset: conf.offset, + indirect: conf.indirect ?? false, })); // 5. loggedTypes diff --git a/packages/abi-typegen/src/types/interfaces/JsonAbi.ts b/packages/abi-typegen/src/types/interfaces/JsonAbi.ts index eed9fee4d46..80b586cde9e 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..ec2d90937b3 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..e22e9c6f0b2 100644 --- a/packages/abi-typegen/src/utils/transpile-abi.ts +++ b/packages/abi-typegen/src/utils/transpile-abi.ts @@ -123,6 +123,7 @@ export function transpileAbi(abi) { name: conf.name, configurableType: parseConcreteType(abi, types, conf.concreteTypeId), offset: conf.offset, + indirect: conf.indirect ?? false, })); // 5. loggedTypes diff --git a/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs b/packages/abi-typegen/test/fixtures/templates/contract-with-configurable/main.hbs index 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 } ] }; 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, }, ]; 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 e7bb9eec1b6..9a0892202d2 100644 --- a/packages/account/src/predicate/predicate.ts +++ b/packages/account/src/predicate/predicate.ts @@ -23,6 +23,7 @@ import type { TransactionRequestLike, TransactionResponse, } from '../providers'; +import { createConfigurables } from '../utils/createConfigurables'; import { deployScriptOrPredicate } from '../utils/deployScriptOrPredicate'; import { getPredicateRoot } from './utils'; @@ -191,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 { @@ -241,53 +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( - bytes: Uint8Array, - configurableConstants: { [name: string]: unknown }, - abiInterface: Interface - ) { - const mutatedBytes = bytes; - - try { - if (Object.keys(abiInterface.configurables).length === 0) { - throw new FuelError( - ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, - 'Predicate has no configurable constants to be set' - ); - } - - Object.entries(configurableConstants).forEach(([key, value]) => { - if (!abiInterface?.configurables[key]) { - throw new FuelError( - ErrorCode.CONFIGURABLE_NOT_FOUND, - `No configurable constant named '${key}' found in the Predicate` - ); - } - - const { offset } = abiInterface.configurables[key]; - - const encoded = abiInterface.encodeConfigurable(key, value as InputValue); - - mutatedBytes.set(encoded, offset); - }); - } catch (err) { - throw new FuelError( - ErrorCode.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: ${(err).message}.` - ); - } - - return mutatedBytes; - } - /** * Returns the index of the witness placeholder that was added to this predicate. * If no witness placeholder was added, it returns -1. @@ -347,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.test.ts b/packages/account/src/utils/createConfigurables.test.ts new file mode 100644 index 00000000000..3d88567e817 --- /dev/null +++ b/packages/account/src/utils/createConfigurables.test.ts @@ -0,0 +1,275 @@ +import { Interface } from '@fuel-ts/abi-coder'; +import { ZeroBytes32 } from '@fuel-ts/address/configs'; +import { arrayify, decompressBytecode } from '@fuel-ts/utils'; + +import { createConfigurables } from './createConfigurables'; +import { + extractBlobIdAndDataOffset, + getPredicateScriptLoaderInstructions, + isBytecodeLoader, +} from './predicate-script-loader-instructions'; + +const abi = { + programType: 'predicate', + specVersion: '1', + encodingVersion: '1', + concreteTypes: [ + { + type: 'bool', + concreteTypeId: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + }, + { + type: 'str', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + type: 'u8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + ], + metadataTypes: [], + functions: [ + { + inputs: [ + { + name: 'some_bool', + concreteTypeId: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + }, + { + name: 'some_u8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + { + name: 'some_str', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + name: 'some_str_2', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + name: 'some_str_3', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + }, + { + name: 'some_last_u8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + }, + ], + name: 'main', + output: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + attributes: null, + }, + ], + loggedTypes: [], + messagesTypes: [], + configurables: [ + { + name: 'BOOL', + concreteTypeId: 'b760f44fa5965c2474a3b471467a22c43185152129295af588b022ae50b50903', + offset: 2048, + indirect: false, + }, + { + name: 'U8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + offset: 2088, + indirect: false, + }, + { + name: 'STR', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + offset: 2064, + indirect: true, + }, + { + name: 'STR_2', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + offset: 2072, + indirect: true, + }, + { + name: 'STR_3', + concreteTypeId: '8c25cb3686462e9a86d2883c5688a22fe738b0bbc85f458d2d2b5f3f667c6d5a', + offset: 2080, + indirect: true, + }, + { + name: 'LAST_U8', + concreteTypeId: 'c89951a24c6ca28c13fd1cfdc646b2b656d69e61a92b91023be7eb58eb914b6b', + offset: 2056, + indirect: false, + }, + ], +}; + +// Predicate with dynamic configurables +const bytecode = decompressBytecode( + 'H4sIAAAAAAAAA5VWTWgTWxQ+k8bXwOvjXUwXffctOkjBCi4G/7a9QzrEmIbcEEXFTHuL6EpR6/9Kl24EFfxZunTnLHXXlbiSbgRFhIh2ITXgwoDFRfzOzYwdJ6loIJxhznfu/c53v3MT+dmjC0R5sp9CPyCGvWVH9Hp0m2i3XvtM+gM5uq3I/bqbjnxr5/S3dh51N5ErxDknk3uDnAjXHpJYe7wMzBgwuQymC8xEBrM1g/kEjJvBbMtg3gMzHfPYlck9lZ8099E+p2hkMchRsSLo0n76a2k/5YrB+PVLs+TsQ9eLfu4A8A7i2EKH/tel6JX8SNmer8nyCg15f0tWV0jXoyuck6teNv9O1lfIdDwBzI1NMK8Yo5uRMA2Le7AJ7oXFHY5cc8gTQ7icliXLxZuvCdKBV5gPXI7ClCOB6JpAcfRMoDmq+YAI/XotH7EceToQhVaA2iqeK65qVVBfjx6ZOuqPRU/MUdQ3o+emoe3+clWQfOeRfGtIvlYkX7pZTs+Y05Qiukv07z1YLT6XCVl6SLJ8i2T1Osl622pr13sfZdcYD7EGzsYB18KQvif4bJAT8z64I8pZQdOVvJJfiO5wft2jB9j7Pjgc+Gq5jCZc+jzAodqmBeghu+Cw7ib40Rj/g3sf3+eb4fEP91qsADdH7LV80Scl5+A71fea7EKrdZHlMrB2iDVY+7BGJGqeWvBR13V/Wfsnml4kmvmVpshPWS/BN7FHfkfTyRQXleXC/t7wzM93CvY7HvPZgr2mse9Egh2msemQYN8a8OK4NEsixU2luE3G3GZS3MwGN2gNjcOaR2GDaOkgFUQjgN6K+XqmQYWFjjdqOtb32qAHxonG7LLFlCLFHEKen2bkgn8eeO5BGF8JE5BgP4GTSXlqJubUG86J9TpLsomZOgxPHIN2LUWt8CwV/S3L7CXoMKKV2sVzW/THrb/w7m+8y5mOK/jc7N5r0TANp1nDuGYMNVuBx92Auwdzbyqo+4gzWh2YZZmp24Y6Ze+UKrTZvG4sVce8nT7vg/C0PS9oYxJtekO8pNPnxWfPfWX22LlxhwrwGPjNmpJNzGZtL76YJY35nKP/+K40NeY9cNduj9dTm6w3ae8cnDk8Mixf4J53NPZo2VXco06df+LJkVSPIpl9218pInjKifURqbkf+c3aRNuB2jLuISf+xRdxjD874rg3jn7yzyB//vLi1eT55Jml4z+eL5449R232/RNTQgAAA==' +); + +// 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); + }); +}); + +// 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({ + bytecode, + abi: new Interface(abi), + }); + + const entries = configurables.all(); + + expect(entries).toEqual([ + { name: 'BOOL', value: true }, + { name: 'U8', value: 8 }, + { name: 'STR', value: 'sway' }, + { name: 'STR_2', value: 'forc' }, + { name: 'STR_3', value: 'fuel' }, + { name: 'LAST_U8', value: 16 }, + ]); + }); + + it('should write direct configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + configurables.set({ + BOOL: false, + U8: 16, + LAST_U8: 32, + }); + + const entries = configurables.all(); + expect(entries).toEqual( + expect.arrayContaining([ + { name: 'BOOL', value: false }, // Changed + { name: 'U8', value: 16 }, // Changed + { name: 'STR', value: 'sway' }, + { name: 'STR_2', value: 'forc' }, + { name: 'STR_3', value: 'fuel' }, + { name: 'LAST_U8', value: 32 }, // Changed + ]) + ); + }); + + it('should write indirect configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + configurables.set({ + STR: 'sway-sway-sway', + STR_2: 'forc-forc', + STR_3: '', + }); + + const entries = configurables.all(); + expect(entries).toMatchObject( + expect.arrayContaining([ + { name: 'BOOL', value: true }, + { name: 'U8', value: 8 }, + { name: 'STR', value: 'sway-sway-sway' }, // Changed + { name: 'STR_2', value: 'forc-forc' }, // Changed + { name: 'STR_3', value: '' }, // Changed + { name: 'LAST_U8', value: 16 }, + ]) + ); + }); + + it('should write both direct and indirect configurables', () => { + const configurables = createConfigurables({ + bytecode, + abi: new Interface(abi), + }); + + configurables.set({ + STR: 'sway-sway-sway', + STR_2: 'forc-forc', + STR_3: '', + BOOL: false, + U8: 16, + LAST_U8: 32, + }); + + const entries = configurables.all(); + expect(entries).toMatchObject( + expect.arrayContaining([ + { name: 'BOOL', value: false }, // Changed + { name: 'U8', value: 16 }, // Changed + { name: 'STR', value: 'sway-sway-sway' }, // Changed + { name: 'STR_2', value: 'forc-forc' }, // Changed + { name: 'STR_3', value: '' }, // Changed + { name: 'LAST_U8', value: 32 }, // Changed + ]) + ); + }); +}); diff --git a/packages/account/src/utils/createConfigurables.ts b/packages/account/src/utils/createConfigurables.ts new file mode 100644 index 00000000000..45e6a40d9c5 --- /dev/null +++ b/packages/account/src/utils/createConfigurables.ts @@ -0,0 +1,156 @@ +import { BigNumberCoder } from '@fuel-ts/abi-coder'; +import type { InputValue, Interface } from '@fuel-ts/abi-coder'; +import type { Configurable } from '@fuel-ts/abi-coder/dist/types/JsonAbiNew'; +import { ErrorCode, FuelError } from '@fuel-ts/errors'; + +import { extractBlobIdAndDataOffset } from './predicate-script-loader-instructions'; + +export const createConfigurables = (opts: { bytecode: Uint8Array; abi: Interface }) => { + const { abi } = opts; + let bytecode = new Uint8Array(opts.bytecode); + const configurables = Object.values(abi.configurables); + const { dataOffset } = extractBlobIdAndDataOffset(bytecode); + const dynamicOffsetCoder = new BigNumberCoder('u64'); + + const getConfigurable = (name: string) => { + const configurable = configurables.find((conf) => conf.name === name); + if (!configurable) { + throw new FuelError( + ErrorCode.CONFIGURABLE_NOT_FOUND, + `A configurable with the '${name}' was not found in the ABI.` + ); + } + return configurable; + }; + + const readIndirectOffset = ({ offset }: Pick) => { + const [dynamicOffsetBn] = dynamicOffsetCoder.decode(bytecode, offset); + const dynamicOffset = dataOffset + dynamicOffsetBn.toNumber(); + return dynamicOffset; + }; + + /** + * Readers + */ + const readDirect = ({ name, concreteTypeId, offset }: Configurable) => { + const coder = abi.getCoder(concreteTypeId); + const [value] = coder.decode(bytecode, offset); + return { name, value }; + }; + + const readIndirect = ({ name, concreteTypeId, offset }: Configurable) => { + const dynamicOffset = readIndirectOffset({ offset }); + const coder = abi.getCoder(concreteTypeId); + const [value] = coder.decode(bytecode, dynamicOffset); + return { name, value }; + }; + + const read = (configurable: Configurable) => { + const reader = configurable.indirect ? readIndirect : readDirect; + return reader(configurable); + }; + + /** + * Writers + */ + const writeDirect = ({ name, offset }: Configurable, value: InputValue) => { + const encodedValue = abi.encodeConfigurable(name, value); + bytecode.set(encodedValue, offset); + }; + + const writeIndirect = ({ concreteTypeId, offset }: Configurable, value: InputValue) => { + const dynamicOffset = readIndirectOffset({ offset }); + + // Read the original value + const coder = abi.getCoder(concreteTypeId); + const [, originalOffset] = coder.decode(bytecode, dynamicOffset); + const originalLength = originalOffset - dynamicOffset; + + // Encode the new value + const encodedValue = coder.encode(value); + const newLength = encodedValue.length; + + // Update the bytecode + bytecode = new Uint8Array([ + ...bytecode.slice(0, dynamicOffset), + ...encodedValue, + ...bytecode.slice(dynamicOffset + originalLength), + ]); + + const additionalOffset = newLength - originalLength; + + // Update the other dynamic configurable offsets + configurables + .filter((configurable) => configurable.indirect && configurable.offset > offset) + .forEach((configurable) => { + const newDynamicOffset = readIndirectOffset({ offset: configurable.offset }); + const newOffset = newDynamicOffset + additionalOffset - dataOffset; + + const encodedOffset = dynamicOffsetCoder.encode(newOffset); + bytecode.set(encodedOffset, configurable.offset); + }); + }; + + const write = (configurable: Configurable, value: InputValue) => { + const writer = configurable.indirect ? writeIndirect : writeDirect; + return writer(configurable, value); + }; + + return { + /** + * Reads the value of a configurable. + * + * @param name - The name of the configurable to read. + * @returns The value of the configurable. + */ + read: (name: string) => read(getConfigurable(name)), + /** + * Reads all the configurables. + * + * @returns An 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 }) => { + try { + const configurableKeys = Object.keys(abi.configurables); + const providedKeys = Object.keys(configurableValues); + + if (!configurableKeys.length) { + throw new FuelError( + FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, + `the program does not have configurable constants to be set.` + ); + } + + const unknownKeys = providedKeys.filter((key) => !configurableKeys.includes(key)); + if (unknownKeys.length) { + throw new FuelError( + FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, + `unknown keys supplied:\n${unknownKeys.map((key) => `- '${key}'`).join('\n')}` + ); + } + + configurables + .sort((a, b) => b.offset - a.offset) + .filter((configurable) => Object.hasOwn(configurableValues, configurable.name)) + .forEach((configurable) => { + const value = configurableValues[configurable.name]; + write(configurable, value as InputValue); + }); + } catch (err) { + throw new FuelError( + FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, + `Error setting configurable constants, ${(err).message}` + ); + } + + return bytecode; + }, + }; +}; diff --git a/packages/account/src/utils/deployScriptOrPredicate.ts b/packages/account/src/utils/deployScriptOrPredicate.ts index 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); diff --git a/packages/account/src/utils/predicate-script-loader-instructions.ts b/packages/account/src/utils/predicate-script-loader-instructions.ts index 4b021d61af4..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; @@ -8,6 +8,8 @@ const REG_ADDRESS_OF_DATA_AFTER_CODE = 0x10; const REG_START_OF_LOADED_CODE = 0x11; const REG_GENERAL_USE = 0x12; const WORD_SIZE = 8; // size in bytes +// https://github.com/FuelLabs/fuel-vm/blob/a340921a00050bd1734e7dcf278a1d13edf2786b/fuel-asm/src/lib.rs#L185-L186 +const LDC_INSTRUCTION_PREAMPLE = 0x32; export const DATA_OFFSET_INDEX = 8; export const CONFIGURABLE_OFFSET_INDEX = 16; @@ -66,23 +68,30 @@ export function getLegacyBlobId(bytecode: Uint8Array): string { return sha256(byteCodeWithoutDataSection); } -export function getPredicateScriptLoaderInstructions( - originalBinary: Uint8Array, - blobId: Uint8Array -) { - // The final code is going to have this structure: - // 1. loader instructions - // 2. blob id - // 3. length_of_data_section - // 4. the data_section (updated with configurables as needed) +/** + * TODO: verify this is correct + * + * Check if the bytecode is a loader + * + * @param bytecode - The bytecode to check + * @returns True if the bytecode is a loader, false otherwise + */ +export function isBytecodeLoader(bytecode: Uint8Array): boolean { + const dataView = new DataView(bytecode.buffer); + const preample = dataView.getUint8(REG_ADDRESS_OF_DATA_AFTER_CODE); + return preample === LDC_INSTRUCTION_PREAMPLE; +} +function getInstructionsWithDataSection(): Uint8Array { const { RegId, Instruction } = asm; const REG_PC = RegId.pc().to_u8(); const REG_SP = RegId.sp().to_u8(); const REG_IS = RegId.is().to_u8(); - const getInstructions = (numOfInstructions: number) => [ + const NUM_OF_INSTRUCTIONS = 12; + + const instructions = [ // 1. Load the blob content into memory // Find the start of the hardcoded blob ID, which is located after the loader code ends. asm.move_(REG_ADDRESS_OF_DATA_AFTER_CODE, REG_PC), @@ -90,7 +99,7 @@ export function getPredicateScriptLoaderInstructions( asm.addi( REG_ADDRESS_OF_DATA_AFTER_CODE, REG_ADDRESS_OF_DATA_AFTER_CODE, - numOfInstructions * Instruction.size() + NUM_OF_INSTRUCTIONS * Instruction.size() ), // The code is going to be loaded from the current value of SP onwards, save // the location into REG_START_OF_LOADED_CODE so we can jump into it at the end. @@ -118,7 +127,24 @@ export function getPredicateScriptLoaderInstructions( asm.jmp(REG_START_OF_LOADED_CODE), ]; - const getInstructionsNoDataSection = (numOfInstructions: number) => [ + // Ensures that the number of instructions is always correct. + if (instructions.length !== NUM_OF_INSTRUCTIONS) { + throw new Error('Invalid number of instructions, check the NUM_OF_INSTRUCTIONS is correct.'); + } + + return new Uint8Array(instructions.flatMap((instruction) => Array.from(instruction.to_bytes()))); +} + +function getInstructionsWithoutDataSection(): Uint8Array { + const { RegId, Instruction } = asm; + + const REG_PC = RegId.pc().to_u8(); + const REG_SP = RegId.sp().to_u8(); + const REG_IS = RegId.is().to_u8(); + + const NUM_OF_INSTRUCTIONS = 8; + + const instructions = [ // 1. Load the blob content into memory // Find the start of the hardcoded blob ID, which is located after the loader code ends. // 1. Load the blob content into memory @@ -128,7 +154,7 @@ export function getPredicateScriptLoaderInstructions( asm.addi( REG_ADDRESS_OF_DATA_AFTER_CODE, REG_ADDRESS_OF_DATA_AFTER_CODE, - numOfInstructions * Instruction.size() + NUM_OF_INSTRUCTIONS * Instruction.size() ), // The code is going to be loaded from the current value of SP onwards, save // the location into REG_START_OF_LOADED_CODE so we can jump into it at the end. @@ -147,6 +173,23 @@ export function getPredicateScriptLoaderInstructions( asm.jmp(REG_START_OF_LOADED_CODE), ]; + // Ensures that the number of instructions is always correct. + if (instructions.length !== NUM_OF_INSTRUCTIONS) { + throw new Error('Invalid number of instructions, check the NUM_OF_INSTRUCTIONS is correct.'); + } + + return new Uint8Array(instructions.flatMap((instruction) => Array.from(instruction.to_bytes()))); +} + +export function getPredicateScriptLoaderInstructions( + originalBinary: Uint8Array, + blobId: Uint8Array +) { + // The final code is going to have this structure: + // 1. loader instructions + // 2. blob id + // 3. length_of_data_section + // 4. the data_section (updated with configurables as needed) const offset = getBytecodeConfigurableOffset(originalBinary); // if the binary length is smaller than the offset @@ -161,18 +204,8 @@ export function getPredicateScriptLoaderInstructions( // Check if the configurable section is non-empty if (configurableSection.length > 0) { - // Get the number of instructions (assuming it won't exceed u16::MAX) - const numOfInstructions = getInstructions(0).length; - if (numOfInstructions > 65535) { - throw new Error('Too many instructions, exceeding u16::MAX.'); - } - // Convert instructions to bytes - const instructionBytes = new Uint8Array( - getInstructions(numOfInstructions).flatMap((instruction) => - Array.from(instruction.to_bytes()) - ) - ); + const instructionBytes = getInstructionsWithDataSection(); // Convert blobId to bytes const blobBytes = new Uint8Array(blobId); @@ -194,18 +227,9 @@ export function getPredicateScriptLoaderInstructions( blobOffset: loaderBytecode.length, }; } - // Handle case where there is no configurable section - const numOfInstructions = getInstructionsNoDataSection(0).length; - if (numOfInstructions > 65535) { - throw new Error('Too many instructions, exceeding u16::MAX.'); - } // Convert instructions to bytes - const instructionBytes = new Uint8Array( - getInstructionsNoDataSection(numOfInstructions).flatMap((instruction) => - Array.from(instruction.to_bytes()) - ) - ); + const instructionBytes = getInstructionsWithoutDataSection(); // Convert blobId to bytes const blobBytes = new Uint8Array(blobId); @@ -215,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, + }; +} 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/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/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..c258bfc9c5b --- /dev/null +++ b/packages/fuel-gauge/src/predicate/predicate-with-dynamic-configurables.test.ts @@ -0,0 +1,222 @@ +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 predicate = new PredicateWithDynamicConfigurable({ + provider, + // data: [true, 8, 'sway', 'forc', 'fuel', 16], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + // Fund predicate + await funder.transfer(loader.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await loader.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForTransfer(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + it('should allow initializing of dynamic configurables', async () => { + using launched = await launchTestNode(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + configurableConstants: { + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }, + data: [false, 0, 'STR', 'STR_2', 'STR_3', 0], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + // Fund predicate + await funder.transfer(loader.address, 1000); + + // Transfer from predicate -> receiver + const { waitForResult: waitForTransfer } = await loader.transfer(receiver.address, 100); + const { isStatusSuccess } = await waitForTransfer(); + expect(isStatusSuccess).toBe(true); + + // Check balance + const balance = await receiver.getBalance(); + expect(balance).toEqual(expect.toEqualBn(100)); + }); + + 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(); + + const { + provider, + wallets: [deployer, funder], + } = launched; + const receiver = WalletUnlocked.generate({ provider }); + + const predicate = new PredicateWithDynamicConfigurable({ + provider, + data: [true, 8, 'sway', 'forc', 'fuel-incorrect', 16], + }); + const { waitForResult: waitForDeploy } = await predicate.deploy(deployer); + const loader = await waitForDeploy(); + + // Fund predicate + await funder.transfer(loader.address, 1000); + + // Transfer from predicate -> receiver + await expect(() => loader.transfer(receiver.address, 100)).rejects.toThrow( + /PredicateVerificationFailed/ + ); + }); + }); +}); 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 new file mode 100644 index 00000000000..329d3ad6be3 --- /dev/null +++ b/packages/fuel-gauge/src/script-with-dynamic-configurables.test.ts @@ -0,0 +1,137 @@ +import { launchTestNode } from 'fuels/test-utils'; + +import { ScriptWithDynamicConfigurables } from '../test/typegen'; + +/** + * @group node + * @group browser + */ +describe('Script with dynamic configurables', () => { + describe('Script', () => { + it('should accept existing dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + + const { waitForResult } = await script.functions + .main(true, 8, 'sway', 'forc', 'fuel', 16) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should allow setting of dynamic configurables', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + + script.setConfigurableConstants({ + BOOL: false, + U8: 0, + STR: 'STR', + STR_2: 'STR_2', + STR_3: 'STR_3', + LAST_U8: 0, + }); + + const { waitForResult } = await script.functions + .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) + .call(); + const { value } = await waitForResult(); + + expect(value).toBe(true); + }); + + it('should return false for script with incorrect data', async () => { + using launched = await launchTestNode(); + const { + wallets: [deployer], + } = launched; + + const script = new ScriptWithDynamicConfigurables(deployer); + + const { waitForResult } = await script.functions + .main(true, 8, 'sway', 'forc', 'fuel-incorrect', 16) + .call(); + + const { value } = await waitForResult(); + + expect(value).toBe(false); + }); + }); + + describe('ScriptLoader', () => { + 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, logs } = await waitForResult(); + + console.log({ + logs, + }); + 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(); + + console.log({ + configurableConstants: loader.getConfigurableConstants(), + }); + + const { waitForResult } = await loader.functions + .main(false, 0, 'STR', 'STR_2', 'STR_3', 0) + .call(); + const { value, logs } = await waitForResult(); + + console.log({ + logs, + }); + + 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 acff8f4e760..762daa3d831 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", @@ -41,6 +42,7 @@ members = [ "predicate-validate-transfer", "predicate-vector-types", "predicate-with-configurable", + "predicate-with-dynamic-configurables", "predicate-with-more-configurables", "predicate-false-configurable", "proxy-contract", @@ -60,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", @@ -78,3 +81,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/contract-with-dynamic-configurables/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/Forc.toml new file mode 100644 index 00000000000..85fd0724065 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "contract-with-dynamic-configurables" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw new file mode 100644 index 00000000000..a0645430d3c --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/contract-with-dynamic-configurables/src/main.sw @@ -0,0 +1,34 @@ +contract; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + STR: str = "sway", + STR_2: str = "forc", + STR_3: str = "fuel", + LAST_U8: u8 = 16, +} + +abi ContractWithDynamicConfigurables { + fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, + ) -> bool; +} + +impl ContractWithDynamicConfigurables for Contract { + fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, + ) -> bool { + some_bool == BOOL && some_u8 == U8 && some_str == STR && some_str_2 == STR_2 && some_str_3 == STR_3 && some_last_u8 == LAST_U8 + } +} diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml new file mode 100644 index 00000000000..52b148cc8c1 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "predicate-with-dynamic-configurable" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw new file mode 100644 index 00000000000..2db407c9b0f --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/predicate-with-dynamic-configurables/src/main.sw @@ -0,0 +1,26 @@ +predicate; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + STR: str = "sway", + STR_2: str = "forc", + STR_3: str = "fuel", + LAST_U8: u8 = 16, +} + +fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, +) -> bool { + some_bool == BOOL && + some_u8 == U8 && + some_str == STR && + some_str_2 == STR_2 && + some_str_3 == STR_3 && + some_last_u8 == LAST_U8 +} \ No newline at end of file diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml new file mode 100644 index 00000000000..2b4017da87d --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/Forc.toml @@ -0,0 +1,6 @@ +[project] +authors = ["Fuel Labs "] +license = "Apache-2.0" +name = "script-with-dynamic-configurables" + +[dependencies] diff --git a/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw new file mode 100644 index 00000000000..75f11805314 --- /dev/null +++ b/packages/fuel-gauge/test/fixtures/forc-projects/script-with-dynamic-configurables/src/main.sw @@ -0,0 +1,39 @@ +script; + +configurable { + BOOL: bool = true, + U8: u8 = 8, + STR: str = "sway", + STR_2: str = "forc", + STR_3: str = "fuel", + LAST_U8: u8 = 16, +} + +fn main( + some_bool: bool, + some_u8: u8, + some_str: str, + some_str_2: str, + some_str_3: str, + some_last_u8: u8, +) -> bool { + log(BOOL); + log(some_bool); + + log(U8); + log(some_u8); + + log(STR); + log(some_str); + + log(STR_2); + log(some_str_2); + + log(STR_3); + log(some_str_3); + + log(LAST_U8); + log(some_last_u8); + + some_bool == BOOL && some_u8 == U8 && some_str == STR && some_str_2 == STR_2 && some_str_3 == STR_3 && some_last_u8 == LAST_U8 +} diff --git a/packages/script/src/script.test.ts b/packages/script/src/script.test.ts index b00ebe04f37..9bea7affb0f 100644 --- a/packages/script/src/script.test.ts +++ b/packages/script/src/script.test.ts @@ -127,7 +127,7 @@ describe('Script', () => { () => newScript.setConfigurableConstants({ FEE: 8 }), new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - 'Error setting configurable constants: The script does not have configurable constants to be set.' + 'Error setting configurable constants, the program does not have configurable constants to be set.' ) ); }); @@ -156,7 +156,7 @@ describe('Script', () => { () => script.setConfigurableConstants({ NOT_DEFINED: 8 }), new FuelError( FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: The script does not have a configurable constant named: 'NOT_DEFINED'.` + `Error setting configurable constants, unknown keys supplied:\n- 'NOT_DEFINED'` ) ); }); diff --git a/packages/script/src/script.ts b/packages/script/src/script.ts index fd86461b8c9..7a0ad108263 100644 --- a/packages/script/src/script.ts +++ b/packages/script/src/script.ts @@ -1,8 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { Interface } from '@fuel-ts/abi-coder'; import type { InputValue, JsonAbi } from '@fuel-ts/abi-coder'; -import { deployScriptOrPredicate, type Account, type Provider } from '@fuel-ts/account'; -import { FuelError } from '@fuel-ts/errors'; +import { + createConfigurables, + deployScriptOrPredicate, + type Account, + type Provider, +} from '@fuel-ts/account'; import type { BN } from '@fuel-ts/math'; import type { ScriptRequest } from '@fuel-ts/program'; import type { BytesLike } from '@fuel-ts/utils'; @@ -88,35 +92,13 @@ export class Script, TOutput> extends AbstractScript { * @throws Will throw an error if the script has no configurable constants to be set or if an invalid constant is provided. * @returns This instance of the `Script`. */ - setConfigurableConstants(configurables: { [name: string]: unknown }) { - try { - if (!Object.keys(this.interface.configurables).length) { - throw new FuelError( - FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `The script does not have configurable constants to be set` - ); - } - - Object.entries(configurables).forEach(([key, value]) => { - if (!this.interface.configurables[key]) { - throw new FuelError( - FuelError.CODES.CONFIGURABLE_NOT_FOUND, - `The script does not have a configurable constant named: '${key}'` - ); - } - - const { offset } = this.interface.configurables[key]; - - const encoded = this.interface.encodeConfigurable(key, value as InputValue); - - this.bytes.set(encoded, offset); - }); - } catch (err) { - throw new FuelError( - FuelError.CODES.INVALID_CONFIGURABLE_CONSTANTS, - `Error setting configurable constants: ${(err).message}.` - ); - } + setConfigurableConstants(configurableValues: { [name: string]: unknown }) { + const configurables = createConfigurables({ + bytecode: this.bytes, + abi: this.interface, + }); + + this.bytes = configurables.set(configurableValues); return this; }