diff --git a/.github/workflows/erdjs-publish.yml b/.github/workflows/erdjs-publish.yml index e4f4beda..c1814694 100644 --- a/.github/workflows/erdjs-publish.yml +++ b/.github/workflows/erdjs-publish.yml @@ -22,7 +22,7 @@ jobs: echo "" >> notes.txt RELEASE_TAG=v$(node -p "require('./package.json').version") - gh release create $RELEASE_TAG --target=$GITHUB_SHA --title="$RELEASE_TAG" --generate-notes --notes-file=notes.txt + gh release create --prerelease $RELEASE_TAG --target=$GITHUB_SHA --title="$RELEASE_TAG" --generate-notes --notes-file=notes.txt - run: npm ci - run: npm test @@ -30,4 +30,4 @@ jobs: - name: Publish to npmjs env: NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} - run: npm publish --access=public + run: npm publish --access=public --tag=older diff --git a/package-lock.json b/package-lock.json index b638585e..787e3481 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@elrondnetwork/erdjs", - "version": "9.2.5", + "version": "9.2.6", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 14cce4e4..7373d12e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@elrondnetwork/erdjs", - "version": "9.2.5", + "version": "9.2.6", "description": "Smart Contracts interaction framework", "main": "out/index.js", "types": "out/index.d.js", diff --git a/src/smartcontracts/typesystem/abiRegistry.spec.ts b/src/smartcontracts/typesystem/abiRegistry.spec.ts index e798814d..a2aadceb 100644 --- a/src/smartcontracts/typesystem/abiRegistry.spec.ts +++ b/src/smartcontracts/typesystem/abiRegistry.spec.ts @@ -92,4 +92,19 @@ describe("test abi registry", () => { ); assert.equal(result.valueOf().name, "SendTransferExecute"); }); + + it("should load ABI containing arrayN and nested structs", async () => { + let registry = await loadAbiRegistry(["src/testdata/array-in-nested-structs.abi.json"]); + let dummyType = registry.getStruct("Dummy"); + let fooType = registry.getStruct("Foo"); + let barType = registry.getStruct("Bar"); + let fooTypeFromBarType = barType.getFieldDefinition("foo")!.type; + let dummyTypeFromFooTypeFromBarType = fooTypeFromBarType.getFieldDefinition("dummy")!.type; + + assert.equal(dummyType.getClassName(), StructType.ClassName); + assert.equal(fooType.getClassName(), StructType.ClassName); + assert.equal(barType.getClassName(), StructType.ClassName); + assert.isTrue(fooType == fooTypeFromBarType); + assert.isTrue(dummyType == dummyTypeFromFooTypeFromBarType); + }); }); diff --git a/src/smartcontracts/typesystem/abiRegistry.ts b/src/smartcontracts/typesystem/abiRegistry.ts index aae72569..8a22d004 100644 --- a/src/smartcontracts/typesystem/abiRegistry.ts +++ b/src/smartcontracts/typesystem/abiRegistry.ts @@ -102,14 +102,13 @@ export class AbiRegistry { * The result is an equivalent, more explicit ABI registry. */ remapToKnownTypes(): AbiRegistry { - let mapper = new TypeMapper(this.customTypes); + let mapper = new TypeMapper([]); let newCustomTypes: CustomType[] = []; let newInterfaces: ContractInterface[] = []; // First, remap custom types (actually, under the hood, this will remap types of struct fields) for (const type of this.customTypes) { const mappedTyped = mapper.mapType(type); newCustomTypes.push(mappedTyped); - mapper.feedCustomType(mappedTyped); } // Then, remap types of all endpoint parameters. // But we'll use an enhanced mapper, that takes into account the results from the previous step. diff --git a/src/smartcontracts/typesystem/enum.ts b/src/smartcontracts/typesystem/enum.ts index 4cb76cff..b8fa116c 100644 --- a/src/smartcontracts/typesystem/enum.ts +++ b/src/smartcontracts/typesystem/enum.ts @@ -56,9 +56,13 @@ export class EnumVariantDefinition { return new EnumVariantDefinition(json.name, json.discriminant, definitions); } - getFieldsDefinitions() { + getFieldsDefinitions(): FieldDefinition[] { return this.fieldsDefinitions; } + + getFieldDefinition(name: string): FieldDefinition | undefined { + return this.fieldsDefinitions.find(item => item.name == name); + } } export class EnumValue extends TypedValue { diff --git a/src/smartcontracts/typesystem/struct.ts b/src/smartcontracts/typesystem/struct.ts index 965a2b06..9924ec5d 100644 --- a/src/smartcontracts/typesystem/struct.ts +++ b/src/smartcontracts/typesystem/struct.ts @@ -19,9 +19,13 @@ export class StructType extends CustomType { return new StructType(json.name, definitions); } - getFieldsDefinitions() { + getFieldsDefinitions(): FieldDefinition[] { return this.fieldsDefinitions; } + + getFieldDefinition(name: string): FieldDefinition | undefined { + return this.fieldsDefinitions.find(item => item.name == name); + } } // TODO: implement setField(), convenience method. diff --git a/src/smartcontracts/typesystem/typeMapper.ts b/src/smartcontracts/typesystem/typeMapper.ts index e6d2c9a6..b951b61f 100644 --- a/src/smartcontracts/typesystem/typeMapper.ts +++ b/src/smartcontracts/typesystem/typeMapper.ts @@ -35,8 +35,9 @@ type TypeFactory = (...typeParameters: Type[]) => Type; export class TypeMapper { private readonly openTypesFactories: Map; private readonly closedTypesMap: Map; + private readonly learnedTypesMap: Map; - constructor(customTypes: CustomType[] = []) { + constructor(learnedTypes: CustomType[] = []) { this.openTypesFactories = new Map([ ["Option", (...typeParameters: Type[]) => new OptionType(typeParameters[0])], ["List", (...typeParameters: Type[]) => new ListType(typeParameters[0])], @@ -61,7 +62,7 @@ export class TypeMapper { ["tuple7", (...typeParameters: Type[]) => new TupleType(...typeParameters)], ["tuple8", (...typeParameters: Type[]) => new TupleType(...typeParameters)], // Known-length arrays. - // TODO: Handle these in typeExpressionParser, perhaps? + // TODO: Handle these in typeExpressionParser! ["array20", (...typeParameters: Type[]) => new ArrayVecType(20, typeParameters[0])], ["array32", (...typeParameters: Type[]) => new ArrayVecType(32, typeParameters[0])], ["array46", (...typeParameters: Type[]) => new ArrayVecType(46, typeParameters[0])], @@ -93,14 +94,41 @@ export class TypeMapper { ["AsyncCall", new NothingType()] ]); - for (const customType of customTypes) { - this.closedTypesMap.set(customType.getName(), customType); + this.learnedTypesMap = new Map(); + + // Boostrap from previously learned types, if any. + for (const type of learnedTypes) { + this.learnedTypesMap.set(type.getName(), type); + } + } + + mapType(type: Type): Type { + let mappedType = this.mapRecursiveType(type); + if (mappedType) { + // We do not learn generic types (that also have type parameters) + if (!mappedType.isGenericType()) { + this.learnType(mappedType); + } + + return mappedType; } + + throw new errors.ErrTypingSystem(`Cannot map the type "${type.getName()}" to a known type`); } mapRecursiveType(type: Type): Type | null { let isGeneric = type.isGenericType(); + let previouslyLearnedType = this.learnedTypesMap.get(type.getName()); + if (previouslyLearnedType) { + return previouslyLearnedType; + } + + let knownClosedType = this.closedTypesMap.get(type.getName()); + if (knownClosedType) { + return knownClosedType; + } + if (type.hasExactClass(EnumType.ClassName)) { // This will call mapType() recursively, for all the enum variant fields. return this.mapEnumType(type); @@ -115,27 +143,13 @@ export class TypeMapper { // This will call mapType() recursively, for all the type parameters. return this.mapGenericType(type); } - - return null; - } - - mapType(type: Type): Type { - let mappedType = this.mapRecursiveType(type); - if (mappedType !== null) { - return mappedType; - } - - let knownClosedType = this.closedTypesMap.get(type.getName()); - if (!knownClosedType) { - throw new errors.ErrTypingSystem(`Cannot map the type "${type.getName()}" to a known type`); - } - return this.mapRecursiveType(knownClosedType) ?? knownClosedType; + return null; } - feedCustomType(type: Type): void { - this.closedTypesMap.delete(type.getName()); - this.closedTypesMap.set(type.getName(), type); + private learnType(type: Type): void { + this.learnedTypesMap.delete(type.getName()); + this.learnedTypesMap.set(type.getName(), type); } private mapStructType(type: StructType): StructType { diff --git a/src/testdata/array-in-nested-structs.abi.json b/src/testdata/array-in-nested-structs.abi.json new file mode 100644 index 00000000..80e58fd1 --- /dev/null +++ b/src/testdata/array-in-nested-structs.abi.json @@ -0,0 +1,33 @@ +{ + "name": "ArraysSample", + "endpoints": [], + "types": { + "Dummy": { + "type": "struct", + "fields": [ + { + "name": "raw", + "type": "array20" + } + ] + }, + "Foo": { + "type": "struct", + "fields": [ + { + "name": "dummy", + "type": "Dummy" + } + ] + }, + "Bar": { + "type": "struct", + "fields": [ + { + "name": "foo", + "type": "Foo" + } + ] + } + } +} diff --git a/src/testdata/esdt-nft-marketplace.abi.json b/src/testdata/esdt-nft-marketplace.abi.json index 9d667b12..f7ebedb3 100644 --- a/src/testdata/esdt-nft-marketplace.abi.json +++ b/src/testdata/esdt-nft-marketplace.abi.json @@ -403,6 +403,40 @@ ], "hasCallback": false, "types": { + "EsdtToken": { + "type": "struct", + "fields": [ + { + "name": "token_type", + "type": "TokenIdentifier" + }, + { + "name": "nonce", + "type": "u64" + } + ] + }, + "AuctionType": { + "type": "enum", + "variants": [ + { + "name": "None", + "discriminant": 0 + }, + { + "name": "Nft", + "discriminant": 1 + }, + { + "name": "SftAll", + "discriminant": 2 + }, + { + "name": "SftOnePerPayment", + "discriminant": 3 + } + ] + }, "Auction": { "type": "struct", "fields": [ @@ -459,40 +493,6 @@ "type": "BigUint" } ] - }, - "AuctionType": { - "type": "enum", - "variants": [ - { - "name": "None", - "discriminant": 0 - }, - { - "name": "Nft", - "discriminant": 1 - }, - { - "name": "SftAll", - "discriminant": 2 - }, - { - "name": "SftOnePerPayment", - "discriminant": 3 - } - ] - }, - "EsdtToken": { - "type": "struct", - "fields": [ - { - "name": "token_type", - "type": "TokenIdentifier" - }, - { - "name": "nonce", - "type": "u64" - } - ] } } }