Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for contracts metadata V2 #4289

Merged
merged 33 commits into from
Dec 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
66904af
Support for contracts metadata V2
jacogr Dec 7, 2021
bfcbcd6
Add & use latest
jacogr Dec 7, 2021
3568d64
Build interfaces
jacogr Dec 7, 2021
9138d45
conversion of V1 -> Latest
jacogr Dec 7, 2021
4be3787
spreads
jacogr Dec 7, 2021
b834759
Use V2 in creation, aka s/name/label/
jacogr Dec 7, 2021
7bbf402
Extract -> latest conversion
jacogr Dec 7, 2021
a039dcd
Flatten
jacogr Dec 7, 2021
f166d59
Add V2 test contracts (tests are failing)
jacogr Dec 7, 2021
418e90f
V2, V1 & V1 catered for in Abi.spec.ts
jacogr Dec 7, 2021
8e94b81
Explicit v0/v1 -> Latest conversions
jacogr Dec 7, 2021
3173db1
Adjust V2/V1/V0 check order
jacogr Dec 7, 2021
d5e9ad5
Cleanup type assertions
jacogr Dec 7, 2021
dd25b76
Save
jacogr Dec 7, 2021
1246406
Remove eslint unsafe
jacogr Dec 7, 2021
2e1e582
Flatten test exports
jacogr Dec 7, 2021
755a862
Entries... last non-critical, OCD-driven change...
jacogr Dec 7, 2021
4bd4b36
tsc check fixes
jacogr Dec 7, 2021
940d544
Conversion tests
jacogr Dec 7, 2021
f5052a9
v1ToLatest renames
jacogr Dec 7, 2021
6126519
Text[]
jacogr Dec 7, 2021
2c6cbfb
Split conversion (ease of future change)
jacogr Dec 7, 2021
0af5015
V2 in V2 conversion
jacogr Dec 7, 2021
8058081
Dedupe?
jacogr Dec 8, 2021
56c309e
Additional cleanups
jacogr Dec 8, 2021
7555bbb
Merge branch 'master' into jg-contracts-V2
jacogr Dec 8, 2021
fc82687
Adjust
jacogr Dec 8, 2021
cc5b717
Flatten types
jacogr Dec 8, 2021
7754884
Adjust
jacogr Dec 8, 2021
5cf6105
Merge branch 'master' into jg-contracts-V2
jacogr Dec 8, 2021
b0e6e70
filter versions adjustments
jacogr Dec 8, 2021
4cea92d
Merge branch 'jg-contracts-V2' of github.com:polkadot-js/api into jg-…
jacogr Dec 8, 2021
4625a79
additional, get it done.
jacogr Dec 8, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 19 additions & 7 deletions packages/api-contract/src/Abi/Abi.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2017-2021 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

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

import fs from 'fs';
import path from 'path';
Expand All @@ -14,6 +14,7 @@ import { Abi } from '.';

interface SpecDef {
messages: {
label: string;
name: string[] | string
}[]
}
Expand All @@ -28,6 +29,9 @@ interface JSONAbi {
spec: SpecDef;
V1: {
spec: SpecDef;
},
V2: {
spec: SpecDef;
}
}

Expand All @@ -47,11 +51,19 @@ function stringifyJson (registry: Registry): string {

describe('Abi', (): void => {
describe('ABI', (): void => {
Object.entries(abis).forEach(([abiName, abi]: [string, JSONAbi]) => {
Object.entries(abis).forEach(([abiName, _abi]) => {
const abi = _abi as unknown as 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] as AnyJson);
const messageIds = (abi.V2 || abi.V1 || abi).spec.messages.map(({ label, name }) =>
label || (
Array.isArray(name)
? name[0]
: name
)
);
const inkAbi = new Abi(abis[abiName]);

expect(inkAbi.messages.map(({ identifier }) => identifier)).toEqual(messageIds);
} catch (error) {
Expand All @@ -66,7 +78,7 @@ describe('Abi', (): void => {
describe('TypeDef', (): void => {
Object.keys(abis).forEach((abiName) => {
it(`initializes from a contract ABI (${abiName})`, (): void => {
const abi = new Abi(abis[abiName] as AnyJson);
const abi = new Abi(abis[abiName]);
const json = stringifyJson(abi.registry);
const cmpPath = path.join(__dirname, `../../test/compare/${abiName}.test.json`);

Expand All @@ -87,8 +99,8 @@ describe('Abi', (): void => {
});

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

// manual
expect(bundle.source.hash).toEqual(blake2AsHex(bundle.source.wasm));
Expand Down
75 changes: 39 additions & 36 deletions packages/api-contract/src/Abi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,14 @@
// SPDX-License-Identifier: Apache-2.0

import type { Bytes, PortableRegistry } from '@polkadot/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 { ChainProperties, ContractConstructorSpecLatest, ContractEventSpecLatest, ContractMessageParamSpecLatest, ContractMessageSpecLatest, ContractMetadataLatest, ContractProjectInfo } from '@polkadot/types/interfaces';
import type { 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 { toLatest } from './toLatest';

interface V0AbiJson {
metadataVersion: string;
spec: {
constructors: unknown[];
events: unknown[];
messages: unknown[];
};
types: unknown[];
}
import { v0ToLatest, v1ToLatest } from './toLatest';

const l = logger('Abi');

Expand All @@ -35,16 +25,29 @@ 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] {
// FIXME: This is still workable with V0, V1 & V2, but certainly is not a scalable
// approach (right at this point don't quite have better ideas that is not as complex
// as the conversion tactics in the runtime Metadata)
function getLatestMeta (registry: Registry, json: Record<string, unknown>): ContractMetadataLatest {
const metadata = registry.createType('ContractMetadata',
isObject(json.V2)
? { V2: json.V2 }
: isObject(json.V1)
? { V1: json.V1 }
: { V0: json }
);

return metadata.isV2
? metadata.asV2
: metadata.isV1
? v1ToLatest(registry, metadata.asV1)
: v0ToLatest(registry, metadata.asV0);
}

function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, 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 latest = getLatestMeta(registry, json);
const lookup = registry.createType<PortableRegistry>('PortableRegistry', { types: latest.types });

// attach the lookup to the registry - now the types are known
Expand All @@ -69,30 +72,30 @@ export class Abi {

public readonly info: ContractProjectInfo;

public readonly json: AnyJson;
public readonly json: Record<string, unknown>;

public readonly messages: AbiMessage[];

public readonly metadata: ContractMetadataLatest;

public readonly registry: Registry;

constructor (abiJson: AnyJson, chainProperties?: ChainProperties) {
constructor (abiJson: Record<string, unknown> | string, chainProperties?: ChainProperties) {
[this.json, this.registry, this.metadata, this.info] = parseJson(
isString(abiJson)
? JSON.parse(abiJson) as AnyJson
? JSON.parse(abiJson) as Record<string, unknown>
: abiJson,
chainProperties
);
this.constructors = this.metadata.spec.constructors.map((spec: ContractConstructorSpec, index) =>
this.constructors = this.metadata.spec.constructors.map((spec: ContractConstructorSpecLatest, index) =>
this.#createMessage(spec, index, {
isConstructor: true
})
);
this.events = this.metadata.spec.events.map((spec: ContractEventSpec, index) =>
this.events = this.metadata.spec.events.map((spec: ContractEventSpecLatest, index) =>
this.#createEvent(spec, index)
);
this.messages = this.metadata.spec.messages.map((spec: ContractMessageSpec, index): AbiMessage => {
this.messages = this.metadata.spec.messages.map((spec: ContractMessageSpecLatest, index): AbiMessage => {
const typeSpec = spec.returnType.unwrapOr(null);

return this.#createMessage(spec, index, {
Expand Down Expand Up @@ -139,15 +142,15 @@ export class Abi {
return findMessage(this.messages, messageOrId);
}

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

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

if (displayName && PRIMITIVE_ALWAYS.includes(displayName)) {
return {
Expand Down Expand Up @@ -175,7 +178,7 @@ export class Abi {
});
};

#createEvent = (spec: ContractEventSpec, index: number): AbiEvent => {
#createEvent = (spec: ContractEventSpecLatest, index: number): AbiEvent => {
const args = this.#createArgs(spec.args, spec);
const event = {
args,
Expand All @@ -184,16 +187,16 @@ export class Abi {
args: this.#decodeArgs(args, data),
event
}),
identifier: spec.name.toString(),
identifier: spec.label.toString(),
index
};

return event;
};

#createMessage = (spec: ContractMessageSpec | ContractConstructorSpec, index: number, add: Partial<AbiMessage> = {}): AbiMessage => {
#createMessage = (spec: ContractMessageSpecLatest | ContractConstructorSpecLatest, index: number, add: Partial<AbiMessage> = {}): AbiMessage => {
const args = this.#createArgs(spec.args, spec);
const identifier = spec.name.toString();
const identifier = spec.label.toString();
const message = {
...add,
args,
Expand Down Expand Up @@ -237,8 +240,8 @@ export class Abi {
return message.fromU8a(trimmed.subarray(4));
};

#encodeArgs = ({ name, selector }: ContractMessageSpec | ContractConstructorSpec, args: AbiParam[], data: unknown[]): Uint8Array => {
assert(data.length === args.length, () => `Expected ${args.length} arguments to contract message '${name.toString()}', found ${data.length}`);
#encodeArgs = ({ label, selector }: ContractMessageSpecLatest | ContractConstructorSpecLatest, args: AbiParam[], data: unknown[]): Uint8Array => {
assert(data.length === args.length, () => `Expected ${args.length} arguments to contract message '${label.toString()}', found ${data.length}`);

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

import { TypeRegistry } from '@polkadot/types';

import abis from '../../test/contracts';
import { v0ToLatest, v1ToLatest } from './toLatest';

describe('v0ToLatest', (): void => {
const registry = new TypeRegistry();
const contract = registry.createType('ContractMetadata', { V0: abis.ink_v0_erc20 });
const latest = v0ToLatest(registry, contract.asV0);

it('has the correct constructors', (): void => {
expect(
latest.spec.constructors.map(({ label }) => label.toString())
).toEqual(['new']);
});

it('has the correct messages', (): void => {
expect(
latest.spec.messages.map(({ label }) => label.toString())
).toEqual(['total_supply', 'balance_of', 'allowance', 'transfer', 'approve', 'transfer_from']);
});

it('has the correct events', (): void => {
expect(
latest.spec.events.map(({ label }) => label.toString())
).toEqual(['Transfer', 'Approval']);
});

it('has the correct constructor arguments', (): void => {
expect(
latest.spec.constructors[0].args.map(({ label }) => label.toString())
).toEqual(['initial_supply']);
});

it('has the correct message arguments', (): void => {
expect(
latest.spec.messages[1].args.map(({ label }) => label.toString())
).toEqual(['owner']);
});

it('has the correct event arguments', (): void => {
expect(
latest.spec.events[0].args.map(({ label }) => label.toString())
).toEqual(['from', 'to', 'value']);
});
});

describe('v1ToLatest', (): void => {
const registry = new TypeRegistry();
const contract = registry.createType('ContractMetadata', { V1: abis.ink_v1_flipper.V1 });
const latest = v1ToLatest(registry, contract.asV1);

it('has the correct constructors', (): void => {
expect(
latest.spec.constructors.map(({ label }) => label.toString())
).toEqual(['new', 'default']);
});

it('has the correct messages', (): void => {
expect(
latest.spec.messages.map(({ label }) => label.toString())
).toEqual(['flip', 'get']);
});

it('has the correct constructor arguments', (): void => {
expect(
latest.spec.constructors[0].args.map(({ label }) => label.toString())
).toEqual(['init_value']);
});
});
16 changes: 9 additions & 7 deletions packages/api-contract/src/Abi/toLatest.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
// Copyright 2017-2021 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

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

import { convertSiV0toV1 } from '@polkadot/types';
import { v0ToV1 } from './toV1';
import { v1ToV2 } from './toV2';

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

export function v0ToLatest (registry: Registry, v0: ContractMetadataV0): ContractMetadataLatest {
return v1ToLatest(registry, v0ToV1(registry, v0));
}
14 changes: 14 additions & 0 deletions packages/api-contract/src/Abi/toV1.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 { ContractMetadataV0, ContractMetadataV1 } from '@polkadot/types/interfaces';
import type { Registry } from '@polkadot/types/types';

import { convertSiV0toV1 } from '@polkadot/types';
import { objectSpread } from '@polkadot/util';

export function v0ToV1 (registry: Registry, v0: ContractMetadataV0): ContractMetadataV1 {
return registry.createType('ContractMetadataV1', objectSpread({}, v0, {
types: convertSiV0toV1(registry, v0.types)
}));
}
52 changes: 52 additions & 0 deletions packages/api-contract/src/Abi/toV2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2017-2021 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { Text } from '@polkadot/types';
import type { ContractMetadataV1, ContractMetadataV2 } from '@polkadot/types/interfaces';
import type { InterfaceTypes, Registry } from '@polkadot/types/types';

import { objectSpread } from '@polkadot/util';

type WithArgs = keyof typeof ARG_TYPES;

interface NamedEntry {
name: Text | Text[];
}

interface ArgsEntry <T extends WithArgs> extends NamedEntry {
args: InterfaceTypes[`${T}V0`]['args'][0][];
}

const ARG_TYPES = {
ContractConstructorSpec: 'ContractMessageParamSpecV2',
ContractEventSpec: 'ContractEventParamSpecV2',
ContractMessageSpec: 'ContractMessageParamSpecV2'
};

function v1ToV2Label (entry: NamedEntry): { label: Text } {
return objectSpread({}, entry, {
label: Array.isArray(entry.name)
? entry.name[0]
: entry.name
});
}

function v1ToV2Labels <T extends WithArgs> (registry: Registry, outType: T, all: ArgsEntry<T>[]): unknown[] {
return all.map((e) =>
registry.createType(`${outType}V2`, objectSpread(v1ToV2Label(e), {
args: e.args.map((a) =>
registry.createType(ARG_TYPES[outType], v1ToV2Label(a))
)
}))
);
}

export function v1ToV2 (registry: Registry, v1: ContractMetadataV1): ContractMetadataV2 {
return registry.createType('ContractMetadataV2', objectSpread({}, v1, {
spec: objectSpread({}, v1.spec, {
constructors: v1ToV2Labels(registry, 'ContractConstructorSpec', v1.spec.constructors),
events: v1ToV2Labels(registry, 'ContractEventSpec', v1.spec.events),
messages: v1ToV2Labels(registry, 'ContractMessageSpec', v1.spec.messages)
})
}));
}
Loading