Skip to content

Commit

Permalink
Use Si V1 in contracts (w/ PortableRegistry usage) (#3988)
Browse files Browse the repository at this point in the history
* Use Si V1 in contracts (w/ PortableRegistry usage)

* Type id lookups

* Fix build & linting

* Remove extra .toNumber() (VSCode has gone crazy)

* v1 tests

* Cleanup parsing

* Explicit type cast for v1 types

* Unneeded toNumber

* Align events exposure with messages/constructors

* dedupe

* Re-add typedef extraction tests

* with typedefs

* Override for always primitives

* displayName improvements

* Versioned project

* New structure

* Fixes

* Cleanups

* Ensure API registry is the default used

* Adjust arg & return encoding/decoding

* Recursive contract

* DoNotConstruct to Si type
  • Loading branch information
jacogr authored Oct 11, 2021
1 parent 6a12ace commit d053b86
Show file tree
Hide file tree
Showing 83 changed files with 2,695 additions and 2,496 deletions.
52 changes: 0 additions & 52 deletions packages/api-contract/src/Abi.spec.ts

This file was deleted.

102 changes: 102 additions & 0 deletions packages/api-contract/src/Abi/Abi.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2017-2021 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Registry } from '@polkadot/types/types';

import fs from 'fs';
import path from 'path';

import { TypeDefInfo } from '@polkadot/types/types';
import { blake2AsHex } from '@polkadot/util-crypto';

import abis from '../../test/contracts';
import { Abi } from '.';

interface SpecDef {
messages: {
name: string[] | string
}[]
}

interface JSONAbi {
source: {
compiler: string,
hash: string,
language: string,
wasm: string
},
spec: SpecDef;
V1: {
spec: SpecDef;
}
}

function stringifyInfo (key: string, value: unknown): unknown {
return key === 'info'
? TypeDefInfo[value as number]
: value;
}

function stringifyJson (registry: Registry): string {
const defs = registry.lookup.types.map(({ id }) =>
registry.lookup.getTypeDef(id)
);

return JSON.stringify(defs, stringifyInfo, 2);
}

describe('Abi', (): void => {
describe('ABI', (): void => {
Object.entries(abis).forEach(([abiName, abi]: [string, JSONAbi]) => {
it(`initializes from a contract ABI (${abiName})`, (): void => {
try {
const messageIds = (abi.V1 ? abi.V1 : abi).spec.messages.map(({ name }) => Array.isArray(name) ? name[0] : name);
const inkAbi = new Abi(abis[abiName]);

expect(inkAbi.messages.map(({ identifier }) => identifier)).toEqual(messageIds);
} catch (error) {
console.error(error);

throw error;
}
});
});
});

describe('TypeDef', (): void => {
Object.keys(abis).forEach((abiName) => {
it(`initializes from a contract ABI (${abiName})`, (): void => {
const abi = new Abi(abis[abiName]);
const json = stringifyJson(abi.registry);
const cmpPath = path.join(__dirname, `../../test/compare/${abiName}.test.json`);

try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
expect(JSON.parse(json)).toEqual(require(cmpPath));
} catch (error) {
if (process.env.GITHUB_REPOSITORY) {
console.error(json);

throw error;
}

fs.writeFileSync(cmpPath, json, { flag: 'w' });
}
});
});
});

it('has the correct hash for the source', (): void => {
const bundle = abis.ink_v0_flipperBundle as JSONAbi;
const abi = new Abi(bundle as any);

// manual
expect(bundle.source.hash).toEqual(blake2AsHex(bundle.source.wasm));

// the Codec hash
expect(bundle.source.hash).toEqual(abi.info.source.wasm.hash.toHex());

// the hash as per the actual Abi
expect(bundle.source.hash).toEqual(abi.info.source.wasmHash.toHex());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,29 @@
// SPDX-License-Identifier: Apache-2.0

import type { Bytes } from '@polkadot/types';
import type { ChainProperties, ContractConstructorSpec, ContractEventSpec, ContractMessageParamSpec, ContractMessageSpec, ContractProject } from '@polkadot/types/interfaces';
import type { AnyJson, Codec } from '@polkadot/types/types';
import type { AbiConstructor, AbiEvent, AbiMessage, AbiParam, DecodedEvent, DecodedMessage } from './types';
import type { ChainProperties, ContractConstructorSpec, ContractEventSpec, ContractMessageParamSpec, ContractMessageSpec, ContractMetadataLatest, ContractProjectInfo } from '@polkadot/types/interfaces';
import type { AnyJson, Codec, Registry } from '@polkadot/types/types';
import type { AbiConstructor, AbiEvent, AbiMessage, AbiParam, DecodedEvent, DecodedMessage } from '../types';

import { TypeDefInfo, TypeRegistry } from '@polkadot/types';
import { assert, assertReturn, compactAddLength, compactStripLength, isNumber, isObject, isString, logger, stringCamelCase, stringify, u8aConcat, u8aToHex } from '@polkadot/util';

import { MetaRegistry } from './MetaRegistry';
import { toLatest } from './toLatest';

interface V0AbiJson {
metadataVersion: string;
spec: {
constructors: unknown[];
events: unknown[];
messages: unknown[];
};
types: unknown[];
}

const l = logger('Abi');

const PRIMITIVE_ALWAYS = ['AccountId', 'AccountIndex', 'Address', 'Balance'];

function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string | number): T {
const message = isNumber(messageOrId)
? list[messageOrId]
Expand All @@ -22,51 +35,71 @@ function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string
return assertReturn(message, () => `Attempted to call an invalid contract interface, ${stringify(messageOrId)}`);
}

function parseJson (json: AnyJson, chainProperties?: ChainProperties): [AnyJson, Registry, ContractMetadataLatest, ContractProjectInfo] {
const registry = new TypeRegistry();
const info = registry.createType('ContractProjectInfo', json);
const metadata = registry.createType('ContractMetadata', isString((json as unknown as V0AbiJson).metadataVersion)
? { V0: json }
: { V1: (json as Record<string, AnyJson>).V1 }
);
const latest = metadata.isV0
? toLatest(registry, metadata.asV0)
: metadata.asV1;
const lookup = registry.createType('PortableRegistry', { types: latest.types });

// attach the lookup to the registry - now the types are known
registry.setLookup(lookup);

if (chainProperties) {
registry.setChainProperties(chainProperties);
}

// warm-up the actual type, pre-use
lookup.types.forEach(({ id }) =>
lookup.getTypeDef(id)
);

return [json, registry, latest, info];
}

export class Abi {
readonly #events: AbiEvent[];
public readonly events: AbiEvent[];

public readonly constructors: AbiConstructor[];

public readonly info: ContractProjectInfo;

public readonly json: AnyJson;

public readonly messages: AbiMessage[];

public readonly project: ContractProject;
public readonly metadata: ContractMetadataLatest;

public readonly registry: MetaRegistry;
public readonly registry: Registry;

constructor (abiJson: AnyJson, chainProperties?: ChainProperties) {
const json = isString(abiJson)
? JSON.parse(abiJson) as AnyJson
: abiJson;

assert(isObject(json) && !Array.isArray(json) && json.metadataVersion && isObject(json.spec) && !Array.isArray(json.spec) && Array.isArray(json.spec.constructors) && Array.isArray(json.spec.messages), 'Invalid JSON ABI structure supplied, expected a recent metadata version');

this.json = json;
this.registry = new MetaRegistry(json.metadataVersion as string, chainProperties);
this.project = this.registry.createType('ContractProject', json);

this.registry.setMetaTypes(this.project.types);

this.project.types.forEach((_, index) =>
this.registry.getMetaTypeDef({ type: this.registry.createType('Si0LookupTypeId', index + this.registry.typeOffset) })
[this.json, this.registry, this.metadata, this.info] = parseJson(
isString(abiJson)
? JSON.parse(abiJson) as AnyJson
: abiJson,
chainProperties
);
this.constructors = this.project.spec.constructors.map((spec: ContractConstructorSpec, index) =>
this.constructors = this.metadata.spec.constructors.map((spec: ContractConstructorSpec, index) =>
this.#createMessage(spec, index, {
isConstructor: true
})
);
this.#events = this.project.spec.events.map((spec: ContractEventSpec, index) =>
this.events = this.metadata.spec.events.map((spec: ContractEventSpec, index) =>
this.#createEvent(spec, index)
);
this.messages = this.project.spec.messages.map((spec: ContractMessageSpec, index): AbiMessage => {
this.messages = this.metadata.spec.messages.map((spec: ContractMessageSpec, index): AbiMessage => {
const typeSpec = spec.returnType.unwrapOr(null);

return this.#createMessage(spec, index, {
isMutating: spec.mutates.isTrue,
isPayable: spec.payable.isTrue,
returnType: typeSpec
? this.registry.getMetaTypeDef(typeSpec)
? this.registry.lookup.getTypeDef(typeSpec.type)
: null
});
});
Expand All @@ -77,7 +110,7 @@ export class Abi {
*/
public decodeEvent (data: Bytes | Uint8Array): DecodedEvent {
const index = data[0];
const event = this.#events[index];
const event = this.events[index];

assert(event, () => `Unable to find event with index ${index}`);

Expand Down Expand Up @@ -107,13 +140,32 @@ export class Abi {
}

#createArgs = (args: ContractMessageParamSpec[], spec: unknown): AbiParam[] => {
return args.map((arg, index): AbiParam => {
return args.map(({ name, type }, index): AbiParam => {
try {
assert(isObject(arg.type), 'Invalid type definition found');
assert(isObject(type), 'Invalid type definition found');

const displayName = type.displayName.length
? type.displayName[type.displayName.length - 1].toString()
: undefined;
const camelName = stringCamelCase(name);

if (displayName && PRIMITIVE_ALWAYS.includes(displayName)) {
return {
name: camelName,
type: {
info: TypeDefInfo.Plain,
type: displayName
}
};
}

const typeDef = this.registry.lookup.getTypeDef(type.type);

return {
name: stringCamelCase(arg.name),
type: this.registry.getMetaTypeDef(arg.type)
name: camelName,
type: displayName && !typeDef.type.startsWith(displayName)
? { displayName, ...typeDef }
: typeDef
};
} catch (error) {
l.error(`Error expanding argument ${index} in ${stringify(spec)}`);
Expand Down Expand Up @@ -166,8 +218,8 @@ export class Abi {
// no length added (this allows use with events as well)
let offset = 0;

return args.map(({ type }): Codec => {
const value = this.registry.createType(type.type as 'Text', data.subarray(offset));
return args.map(({ type: { lookupName, type } }): Codec => {
const value = this.registry.createType(lookupName || type, data.subarray(offset));

offset += value.encodedLength;

Expand All @@ -191,7 +243,9 @@ export class Abi {
return compactAddLength(
u8aConcat(
this.registry.createType('ContractSelector', selector).toU8a(),
...args.map(({ type }, index) => this.registry.createType(type.type as 'Text', data[index]).toU8a())
...args.map(({ type: { lookupName, type } }, index) =>
this.registry.createType(lookupName || type, data[index]).toU8a()
)
)
);
}
Expand Down
14 changes: 14 additions & 0 deletions packages/api-contract/src/Abi/toLatest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2017-2021 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { ContractMetadataLatest, ContractMetadataV0 } from '@polkadot/types/interfaces';
import type { Registry } from '@polkadot/types/types';

import { convertSiV0toV1 } from '@polkadot/types/generic/PortableRegistry';

export function toLatest (registry: Registry, v0: ContractMetadataV0): ContractMetadataLatest {
return registry.createType('ContractMetadataLatest', {
...v0,
types: convertSiV0toV1(registry, v0.types)
});
}
Loading

0 comments on commit d053b86

Please sign in to comment.