diff --git a/CHANGELOG.md b/CHANGELOG.md index b3ec2d110..99f80979a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## Unreleased - TBD +## 10.2.1 + - [When loading the ABI, sort custom types by their type dependencies](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/208) + ## 10.2.0 - [Define Transaction Factory and Gas Estimator](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/203) - [Fix type mapper / ABI registry (scenario: nested structs with ArrayN)](https://github.com/ElrondNetwork/elrond-sdk-erdjs/pull/198) diff --git a/package-lock.json b/package-lock.json index e93e7dbaa..ea8419096 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@elrondnetwork/erdjs", - "version": "10.2.0", + "version": "10.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@elrondnetwork/erdjs", - "version": "10.2.0", + "version": "10.2.1", "license": "GPL-3.0-or-later", "dependencies": { "@elrondnetwork/transaction-decoder": "0.1.0", @@ -551,9 +551,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.12.tgz", + "integrity": "sha512-az/NhpIwP3K33ILr0T2bso+k2E/SLf8Yidd8mHl0n6sCQ4YdyC8qDhZA6kOPDNDBA56ZnIjngVl0U3jREA0BUA==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -2092,9 +2092,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.129", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.129.tgz", - "integrity": "sha512-GgtN6bsDtHdtXJtlMYZWGB/uOyjZWjmRDumXTas7dGBaB9zUyCjzHet1DY2KhyHN8R0GLbzZWqm4efeddqqyRQ==", + "version": "1.4.131", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.131.tgz", + "integrity": "sha512-oi3YPmaP87hiHn0c4ePB67tXaF+ldGhxvZnT19tW9zX6/Ej+pLN0Afja5rQ6S+TND7I9EuwQTT8JYn1k7R7rrw==", "dev": true }, "node_modules/elliptic": { @@ -4844,26 +4844,28 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -6125,9 +6127,9 @@ "dev": true }, "@jridgewell/sourcemap-codec": { - "version": "1.4.11", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.11.tgz", - "integrity": "sha512-Fg32GrJo61m+VqYSdRSjRXMjQ06j8YIYfcTqndLYVAaHmroZHLJZCydsWBOTDqXS2v+mjxohBWEMfg97GXmYQg==", + "version": "1.4.12", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.12.tgz", + "integrity": "sha512-az/NhpIwP3K33ILr0T2bso+k2E/SLf8Yidd8mHl0n6sCQ4YdyC8qDhZA6kOPDNDBA56ZnIjngVl0U3jREA0BUA==", "dev": true }, "@jridgewell/trace-mapping": { @@ -7462,9 +7464,9 @@ } }, "electron-to-chromium": { - "version": "1.4.129", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.129.tgz", - "integrity": "sha512-GgtN6bsDtHdtXJtlMYZWGB/uOyjZWjmRDumXTas7dGBaB9zUyCjzHet1DY2KhyHN8R0GLbzZWqm4efeddqqyRQ==", + "version": "1.4.131", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.131.tgz", + "integrity": "sha512-oi3YPmaP87hiHn0c4ePB67tXaF+ldGhxvZnT19tW9zX6/Ej+pLN0Afja5rQ6S+TND7I9EuwQTT8JYn1k7R7rrw==", "dev": true }, "elliptic": { @@ -9637,23 +9639,25 @@ } }, "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" } }, "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", "dev": true, "requires": { "call-bind": "^1.0.2", - "define-properties": "^1.1.3" + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" } }, "strip-ansi": { diff --git a/package.json b/package.json index 44cb4f9ae..bd3347cee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@elrondnetwork/erdjs", - "version": "10.2.0", + "version": "10.2.1", "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 807b9255e..db6a1efa3 100644 --- a/src/smartcontracts/typesystem/abiRegistry.spec.ts +++ b/src/smartcontracts/typesystem/abiRegistry.spec.ts @@ -97,4 +97,12 @@ describe("test abi registry", () => { assert.isTrue(dummyType == dummyTypeFromFooTypeFromBarType); assert.equal(dummyType.getFieldDefinition("raw")!.type.getClassName(), ArrayVecType.ClassName); }); + + it("should load ABI when custom types are out of order", async () => { + const registry = await loadAbiRegistry("src/testdata/custom-types-out-of-order.abi.json"); + + assert.deepEqual(registry.getStruct("TypeA").getNamesOfDependencies(), ["TypeB", "TypeC", "u64"]); + assert.deepEqual(registry.getStruct("TypeB").getNamesOfDependencies(), ["TypeC", "u64"]); + assert.deepEqual(registry.getStruct("TypeC").getNamesOfDependencies(), ["u64"]); + }); }); diff --git a/src/smartcontracts/typesystem/abiRegistry.ts b/src/smartcontracts/typesystem/abiRegistry.ts index 180fd11ea..56a73f600 100644 --- a/src/smartcontracts/typesystem/abiRegistry.ts +++ b/src/smartcontracts/typesystem/abiRegistry.ts @@ -19,9 +19,11 @@ export class AbiRegistry { private extend(json: { name: string; endpoints: any[]; types: any[] }): AbiRegistry { json.types = json.types || {}; + // The "endpoints" collection is interpreted by "ContractInterface". let iface = ContractInterface.fromJSON(json); this.interfaces.push(iface); + for (const customTypeName in json.types) { let itemJson = json.types[customTypeName]; let typeDiscriminant = itemJson.type; @@ -30,8 +32,12 @@ export class AbiRegistry { let customType = this.createCustomType(typeDiscriminant, itemJson); this.customTypes.push(customType); } + + this.sortCustomTypesByDependencies(); + return this; } + private createCustomType(typeDiscriminant: string, json: any): CustomType { if (typeDiscriminant == "struct") { return StructType.fromJSON(json); @@ -41,30 +47,50 @@ export class AbiRegistry { } throw new errors.ErrTypingSystem(`Unknown type discriminant: ${typeDiscriminant}`); } + + private sortCustomTypesByDependencies() { + this.customTypes.sort((a: CustomType, b: CustomType) => { + const bDependsonA = b.getNamesOfDependencies().indexOf(a.getName()) > -1; + if (bDependsonA) { + // Sort "a" before "b". + return -1; + } + + // Keep original order. + return 0; + }); + } + getInterface(name: string): ContractInterface { let result = this.interfaces.find((e) => e.name == name); guardValueIsSetWithMessage(`interface [${name}] not found`, result); return result!; } + getInterfaces(names: string[]): ContractInterface[] { return names.map((name) => this.getInterface(name)); } + getStruct(name: string): StructType { let result = this.customTypes.find((e) => e.getName() == name && e.hasExactClass(StructType.ClassName)); guardValueIsSetWithMessage(`struct [${name}] not found`, result); return result!; } + getStructs(names: string[]): StructType[] { return names.map((name) => this.getStruct(name)); } + getEnum(name: string): EnumType { let result = this.customTypes.find((e) => e.getName() == name && e.hasExactClass(EnumType.ClassName)); guardValueIsSetWithMessage(`enum [${name}] not found`, result); return result!; } + getEnums(names: string[]): EnumType[] { return names.map((name) => this.getEnum(name)); } + /** * Right after loading ABI definitions into a registry, the endpoints and the custom types (structs, enums) * use raw types for their I/O parameters (in the case of endpoints), or for their fields (in the case of structs). @@ -79,11 +105,13 @@ export class AbiRegistry { 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); } + // Then, remap types of all endpoint parameters. // But we'll use an enhanced mapper, that takes into account the results from the previous step. mapper = new TypeMapper(newCustomTypes); @@ -95,6 +123,7 @@ export class AbiRegistry { let newConstructor = iface.constructorDefinition ? mapEndpoint(iface.constructorDefinition, mapper) : null; newInterfaces.push(new ContractInterface(iface.name, newConstructor, newEndpoints)); } + // Now return the new registry, with all types remapped to known types let newRegistry = new AbiRegistry(); newRegistry.customTypes.push(...newCustomTypes); diff --git a/src/smartcontracts/typesystem/enum.ts b/src/smartcontracts/typesystem/enum.ts index 4128ad090..2b8571c67 100644 --- a/src/smartcontracts/typesystem/enum.ts +++ b/src/smartcontracts/typesystem/enum.ts @@ -34,6 +34,16 @@ export class EnumType extends CustomType { guardValueIsSet(`variant by name (${name})`, result); return result!; } + + getNamesOfDependencies(): string[] { + const dependencies: string[] = []; + + for (const variant of this.variants) { + dependencies.push(...variant.getNamesOfDependencies()); + } + + return [...new Set(dependencies)]; + } } export class EnumVariantDefinition { @@ -64,6 +74,10 @@ export class EnumVariantDefinition { getFieldDefinition(name: string): FieldDefinition | undefined { return this.fieldsDefinitions.find(item => item.name == name); } + + getNamesOfDependencies(): string[] { + return Fields.getNamesOfTypeDependencies(this.fieldsDefinitions); + } } export class EnumValue extends TypedValue { diff --git a/src/smartcontracts/typesystem/fields.ts b/src/smartcontracts/typesystem/fields.ts index 1d7668346..c281df94e 100644 --- a/src/smartcontracts/typesystem/fields.ts +++ b/src/smartcontracts/typesystem/fields.ts @@ -74,4 +74,15 @@ export class Fields { return true; } + + static getNamesOfTypeDependencies(definitions: FieldDefinition[]): string[] { + const dependencies: string[] = []; + + for (const definition of definitions) { + dependencies.push(definition.type.getName()); + dependencies.push(...definition.type.getNamesOfDependencies()); + } + + return [...new Set(dependencies)]; + } } diff --git a/src/smartcontracts/typesystem/struct.ts b/src/smartcontracts/typesystem/struct.ts index c1a74b8ff..e0adaac06 100644 --- a/src/smartcontracts/typesystem/struct.ts +++ b/src/smartcontracts/typesystem/struct.ts @@ -27,6 +27,10 @@ export class StructType extends CustomType { getFieldDefinition(name: string): FieldDefinition | undefined { return this.fieldsDefinitions.find(item => item.name == name); } + + getNamesOfDependencies(): string[] { + return Fields.getNamesOfTypeDependencies(this.fieldsDefinitions); + } } export class Struct extends TypedValue { diff --git a/src/smartcontracts/typesystem/types.spec.ts b/src/smartcontracts/typesystem/types.spec.ts index bd96e803c..b140b658c 100644 --- a/src/smartcontracts/typesystem/types.spec.ts +++ b/src/smartcontracts/typesystem/types.spec.ts @@ -66,4 +66,10 @@ describe("test types", () => { assert.deepEqual(new BytesType().getClassHierarchy(), ["Type", "PrimitiveType", "BytesType"]); assert.deepEqual(new BytesValue(Buffer.from("foobar")).getClassHierarchy(), ["TypedValue", "PrimitiveValue", "BytesValue"]); }); + + it("should report type dependencies", () => { + assert.deepEqual(parser.parse("MultiResultVec").getNamesOfDependencies(), ["u32"]); + assert.deepEqual(parser.parse("tuple2").getNamesOfDependencies(), ["Address", "BigUint"]); + assert.deepEqual(parser.parse("Option").getNamesOfDependencies(), ["FooBar"]); + }); }); diff --git a/src/smartcontracts/typesystem/types.ts b/src/smartcontracts/typesystem/types.ts index 2a7e1483a..1d2b6c3e1 100644 --- a/src/smartcontracts/typesystem/types.ts +++ b/src/smartcontracts/typesystem/types.ts @@ -133,6 +133,17 @@ export class Type { return fullyQualifiedNames; } + getNamesOfDependencies(): string[] { + const dependencies: string[] = []; + + for (const type of this.typeParameters) { + dependencies.push(type.getName()); + dependencies.push(...type.getNamesOfDependencies()); + } + + return [...new Set(dependencies)]; + } + /** * Converts the account to a pretty, plain JavaScript object. */ diff --git a/src/testdata/custom-types-out-of-order.abi.json b/src/testdata/custom-types-out-of-order.abi.json new file mode 100644 index 000000000..aa407c327 --- /dev/null +++ b/src/testdata/custom-types-out-of-order.abi.json @@ -0,0 +1,66 @@ +{ + "name": "Sample", + "types": { + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_type", + "type": "EsdtTokenType" + }, + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "EsdtTokenType": { + "type": "enum", + "variants": [ + { + "name": "Fungible", + "discriminant": 0 + }, + { + "name": "NonFungible", + "discriminant": 1 + } + ] + }, + "TypeA": { + "type": "struct", + "fields": [ + { + "name": "b", + "type": "TypeB" + } + ] + }, + "TypeB": { + "type": "struct", + "fields": [ + { + "name": "c", + "type": "TypeC" + } + ] + }, + "TypeC": { + "type": "struct", + "fields": [ + { + "name": "foobar", + "type": "u64" + } + ] + } + } +}