Skip to content

Commit ca48023

Browse files
authored
feat: ink! v5 (#5791)
* adds definitions and types according to ink v5 changes * adds toV5 boilerplate code draft * adds v5 flipper test contract code * fix license dates * adds test v5 toLatest test * implements new scheme to determine event * apply linter changes * adds test result outputs * change `EventRecord['topics'][0]` type to plain `Hash` * adds testcases for decoding payload data of a ink!v4 and ink!v5 event * changes `Abi.decodeEvent(data:Bytes)` method interface to `Abi.decodeEvent(record:EventRecord)` which includes the event and the topic for decoding. * draft implementation with version metadata * cleaner implementation of versioned Metadata by actually leveraging the `version` field included since v2 contract metadata * trying to make linter happy * makes `ContractMetadataSupported` in internal to `Abi` type and not exposing it externally. * properly types unused parameter for tsc 🤷 * adds `@polkadot/types-support` dev dependency * Update yarn.lock * references `types-support` in `api-contract * resolving change requests * resolves linter warnings * changes ContractMetadataV5 field to `u64` from `Text` * adds contracts and contract metadata compiled with the most recent ink version * implements decoding of anonymous events if possible * removes done todo comments
1 parent f9b2d26 commit ca48023

39 files changed

+6245
-93
lines changed

packages/api-contract/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
},
3434
"devDependencies": {
3535
"@polkadot/api-augment": "10.11.3",
36-
"@polkadot/keyring": "^12.6.2"
36+
"@polkadot/keyring": "^12.6.2",
37+
"@polkadot/types-support": "10.11.3"
3738
}
3839
}

packages/api-contract/src/Abi/Abi.spec.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ import fs from 'node:fs';
99
import process from 'node:process';
1010

1111
import { TypeDefInfo } from '@polkadot/types/types';
12+
import rpcMetadata from '@polkadot/types-support/metadata/static-substrate-contracts-node';
1213
import { blake2AsHex } from '@polkadot/util-crypto';
1314

15+
import { Metadata, TypeRegistry } from '../../../types/src/bundle.js';
1416
import abis from '../test/contracts/index.js';
1517
import { Abi } from './index.js';
1618

@@ -122,4 +124,112 @@ describe('Abi', (): void => {
122124
// the hash as per the actual Abi
123125
expect(bundle.source.hash).toEqual(abi.info.source.wasmHash.toHex());
124126
});
127+
128+
describe('Events', (): void => {
129+
const registry = new TypeRegistry();
130+
131+
beforeAll((): void => {
132+
const metadata = new Metadata(registry, rpcMetadata);
133+
134+
registry.setMetadata(metadata);
135+
});
136+
137+
it('decoding <=ink!v4 event', (): void => {
138+
const abiJson = abis['ink_v4_erc20Metadata'];
139+
140+
expect(abiJson).toBeDefined();
141+
const abi = new Abi(abiJson);
142+
143+
const eventRecordHex =
144+
'0x0001000000080360951b8baf569bca905a279c12d6ce17db7cdce23a42563870ef585129ce5dc64d010001d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4800505a4f7e9f4eb106000000000000000c0045726332303a3a5472616e7366657200000000000000000000000000000000da2d695d3b5a304e0039e7fc4419c34fa0c1f239189c99bb72a6484f1634782b2b00c7d40fe6d84d660f3e6bed90f218e022a0909f7e1a7ea35ada8b6e003564';
145+
const record = registry.createType('EventRecord', eventRecordHex);
146+
147+
const decodedEvent = abi.decodeEvent(record);
148+
149+
expect(decodedEvent.event.args.length).toEqual(3);
150+
expect(decodedEvent.args.length).toEqual(3);
151+
expect(decodedEvent.event.identifier).toEqual('Transfer');
152+
153+
const decodedEventHuman = decodedEvent.event.args.reduce((prev, cur, index) => {
154+
return {
155+
...prev,
156+
[cur.name]: decodedEvent.args[index].toHuman()
157+
};
158+
}, {});
159+
160+
const expectedEvent = {
161+
from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
162+
to: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
163+
value: '123.4567 MUnit'
164+
};
165+
166+
expect(decodedEventHuman).toEqual(expectedEvent);
167+
});
168+
169+
it('decoding >=ink!v5 event', (): void => {
170+
const abiJson = abis['ink_v5_erc20Metadata'];
171+
172+
expect(abiJson).toBeDefined();
173+
const abi = new Abi(abiJson);
174+
175+
const eventRecordHex =
176+
'0x00010000000803da17150e96b3955a4db6ad35ddeb495f722f9c1d84683113bfb096bf3faa30f2490101d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4800505a4f7e9f4eb106000000000000000cb5b61a3e6a21a16be4f044b517c28ac692492f73c5bfd3f60178ad98c767f4cbd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48';
177+
const record = registry.createType('EventRecord', eventRecordHex);
178+
179+
const decodedEvent = abi.decodeEvent(record);
180+
181+
expect(decodedEvent.event.args.length).toEqual(3);
182+
expect(decodedEvent.args.length).toEqual(3);
183+
expect(decodedEvent.event.identifier).toEqual('erc20::erc20::Transfer');
184+
185+
const decodedEventHuman = decodedEvent.event.args.reduce((prev, cur, index) => {
186+
return {
187+
...prev,
188+
[cur.name]: decodedEvent.args[index].toHuman()
189+
};
190+
}, {});
191+
192+
const expectedEvent = {
193+
from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
194+
to: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
195+
value: '123.4567 MUnit'
196+
};
197+
198+
expect(decodedEventHuman).toEqual(expectedEvent);
199+
});
200+
201+
it('decoding >=ink!v5 anonymous event', (): void => {
202+
const abiJson = abis['ink_v5_erc20AnonymousTransferMetadata'];
203+
204+
expect(abiJson).toBeDefined();
205+
const abi = new Abi(abiJson);
206+
207+
expect(abi.events[0].identifier).toEqual('erc20::erc20::Transfer');
208+
expect(abi.events[0].signatureTopic).toEqual(null);
209+
210+
const eventRecordWithAnonymousEventHex = '0x00010000000803538e726248a9c155911e7d99f4f474c3408630a2f6275dd501d4471c7067ad2c490101d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4800505a4f7e9f4eb1060000000000000008d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48';
211+
const record = registry.createType('EventRecord', eventRecordWithAnonymousEventHex);
212+
213+
const decodedEvent = abi.decodeEvent(record);
214+
215+
expect(decodedEvent.event.args.length).toEqual(3);
216+
expect(decodedEvent.args.length).toEqual(3);
217+
expect(decodedEvent.event.identifier).toEqual('erc20::erc20::Transfer');
218+
219+
const decodedEventHuman = decodedEvent.event.args.reduce((prev, cur, index) => {
220+
return {
221+
...prev,
222+
[cur.name]: decodedEvent.args[index].toHuman()
223+
};
224+
}, {});
225+
226+
const expectedEvent = {
227+
from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
228+
to: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
229+
value: '123.4567 MUnit'
230+
};
231+
232+
expect(decodedEventHuman).toEqual(expectedEvent);
233+
});
234+
});
125235
});

packages/api-contract/src/Abi/index.ts

Lines changed: 120 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,27 @@
11
// Copyright 2017-2024 @polkadot/api-contract authors & contributors
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import type { Bytes } from '@polkadot/types';
5-
import type { ChainProperties, ContractConstructorSpecLatest, ContractEventSpecLatest, ContractMessageParamSpecLatest, ContractMessageSpecLatest, ContractMetadata, ContractMetadataLatest, ContractProjectInfo, ContractTypeSpec } from '@polkadot/types/interfaces';
4+
import type { Bytes, Vec } from '@polkadot/types';
5+
import type { ChainProperties, ContractConstructorSpecLatest, ContractEventParamSpecLatest, ContractMessageParamSpecLatest, ContractMessageSpecLatest, ContractMetadata, ContractMetadataV4, ContractMetadataV5, ContractProjectInfo, ContractTypeSpec, EventRecord } from '@polkadot/types/interfaces';
66
import type { Codec, Registry, TypeDef } from '@polkadot/types/types';
7-
import type { AbiConstructor, AbiEvent, AbiMessage, AbiParam, DecodedEvent, DecodedMessage } from '../types.js';
7+
import type { AbiConstructor, AbiEvent, AbiEventParam, AbiMessage, AbiMessageParam, AbiParam, DecodedEvent, DecodedMessage } from '../types.js';
88

99
import { Option, TypeRegistry } from '@polkadot/types';
1010
import { TypeDefInfo } from '@polkadot/types-create';
1111
import { assertReturn, compactAddLength, compactStripLength, isBn, isNumber, isObject, isString, isUndefined, logger, stringCamelCase, stringify, u8aConcat, u8aToHex } from '@polkadot/util';
1212

13-
import { convertVersions, enumVersions } from './toLatest.js';
13+
import { convertVersions, enumVersions } from './toLatestCompatible.js';
1414

1515
interface AbiJson {
1616
version?: string;
1717

1818
[key: string]: unknown;
1919
}
2020

21+
type EventOf<M> = M extends {spec: { events: Vec<infer E>}} ? E : never
22+
export type ContractMetadataSupported = ContractMetadataV4 | ContractMetadataV5;
23+
type ContractEventSupported = EventOf<ContractMetadataSupported>;
24+
2125
const l = logger('Abi');
2226

2327
const PRIMITIVE_ALWAYS = ['AccountId', 'AccountIndex', 'Address', 'Balance'];
@@ -32,7 +36,7 @@ function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string
3236
return assertReturn(message, () => `Attempted to call an invalid contract interface, ${stringify(messageOrId)}`);
3337
}
3438

35-
function getLatestMeta (registry: Registry, json: AbiJson): ContractMetadataLatest {
39+
function getMetadata (registry: Registry, json: AbiJson): ContractMetadataSupported {
3640
// this is for V1, V2, V3
3741
const vx = enumVersions.find((v) => isObject(json[v]));
3842

@@ -50,20 +54,23 @@ function getLatestMeta (registry: Registry, json: AbiJson): ContractMetadataLate
5054
? { [`V${jsonVersion}`]: json }
5155
: { V0: json }
5256
);
57+
5358
const converter = convertVersions.find(([v]) => metadata[`is${v}`]);
5459

5560
if (!converter) {
56-
throw new Error(`Unable to convert ABI with version ${metadata.type} to latest`);
61+
throw new Error(`Unable to convert ABI with version ${metadata.type} to a supported version`);
5762
}
5863

59-
return converter[1](registry, metadata[`as${converter[0]}`]);
64+
const upgradedMetadata = converter[1](registry, metadata[`as${converter[0]}`]);
65+
66+
return upgradedMetadata;
6067
}
6168

62-
function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataLatest, ContractProjectInfo] {
69+
function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataSupported, ContractProjectInfo] {
6370
const registry = new TypeRegistry();
6471
const info = registry.createType('ContractProjectInfo', json) as unknown as ContractProjectInfo;
65-
const latest = getLatestMeta(registry, json as unknown as AbiJson);
66-
const lookup = registry.createType('PortableRegistry', { types: latest.types }, true);
72+
const metadata = getMetadata(registry, json as unknown as AbiJson);
73+
const lookup = registry.createType('PortableRegistry', { types: metadata.types }, true);
6774

6875
// attach the lookup to the registry - now the types are known
6976
registry.setLookup(lookup);
@@ -77,7 +84,7 @@ function parseJson (json: Record<string, unknown>, chainProperties?: ChainProper
7784
lookup.getTypeDef(id)
7885
);
7986

80-
return [json, registry, latest, info];
87+
return [json, registry, metadata, info];
8188
}
8289

8390
/**
@@ -102,7 +109,7 @@ export class Abi {
102109
readonly info: ContractProjectInfo;
103110
readonly json: Record<string, unknown>;
104111
readonly messages: AbiMessage[];
105-
readonly metadata: ContractMetadataLatest;
112+
readonly metadata: ContractMetadataSupported;
106113
readonly registry: Registry;
107114
readonly environment = new Map<string, TypeDef | Codec>();
108115

@@ -123,8 +130,8 @@ export class Abi {
123130
: null
124131
})
125132
);
126-
this.events = this.metadata.spec.events.map((spec: ContractEventSpecLatest, index) =>
127-
this.#createEvent(spec, index)
133+
this.events = this.metadata.spec.events.map((_: ContractEventSupported, index: number) =>
134+
this.#createEvent(index)
128135
);
129136
this.messages = this.metadata.spec.messages.map((spec: ContractMessageSpecLatest, index): AbiMessage =>
130137
this.#createMessage(spec, index, {
@@ -162,7 +169,59 @@ export class Abi {
162169
/**
163170
* Warning: Unstable API, bound to change
164171
*/
165-
public decodeEvent (data: Bytes | Uint8Array): DecodedEvent {
172+
public decodeEvent (record: EventRecord): DecodedEvent {
173+
switch (this.metadata.version.toString()) {
174+
// earlier version are hoisted to v4
175+
case '4':
176+
return this.#decodeEventV4(record);
177+
// Latest
178+
default:
179+
return this.#decodeEventV5(record);
180+
}
181+
}
182+
183+
#decodeEventV5 = (record: EventRecord): DecodedEvent => {
184+
// Find event by first topic, which potentially is the signature_topic
185+
const signatureTopic = record.topics[0];
186+
const data = record.event.data[1] as Bytes;
187+
188+
if (signatureTopic) {
189+
const event = this.events.find((e) => e.signatureTopic !== undefined && e.signatureTopic !== null && e.signatureTopic === signatureTopic.toHex());
190+
191+
// Early return if event found by signature topic
192+
if (event) {
193+
return event.fromU8a(data);
194+
}
195+
}
196+
197+
// If no event returned yet, it might be anonymous
198+
const amountOfTopics = record.topics.length;
199+
const potentialEvents = this.events.filter((e) => {
200+
// event can't have a signature topic
201+
if (e.signatureTopic !== null && e.signatureTopic !== undefined) {
202+
return false;
203+
}
204+
205+
// event should have same amount of indexed fields as emitted topics
206+
const amountIndexed = e.args.filter((a) => a.indexed).length;
207+
208+
if (amountIndexed !== amountOfTopics) {
209+
return false;
210+
}
211+
212+
// If all conditions met, it's a potential event
213+
return true;
214+
});
215+
216+
if (potentialEvents.length === 1) {
217+
return potentialEvents[0].fromU8a(data);
218+
}
219+
220+
throw new Error('Unable to determine event');
221+
};
222+
223+
#decodeEventV4 = (record: EventRecord): DecodedEvent => {
224+
const data = record.event.data[1] as Bytes;
166225
const index = data[0];
167226
const event = this.events[index];
168227

@@ -171,7 +230,7 @@ export class Abi {
171230
}
172231

173232
return event.fromU8a(data.subarray(1));
174-
}
233+
};
175234

176235
/**
177236
* Warning: Unstable API, bound to change
@@ -195,7 +254,7 @@ export class Abi {
195254
return findMessage(this.messages, messageOrId);
196255
}
197256

198-
#createArgs = (args: ContractMessageParamSpecLatest[], spec: unknown): AbiParam[] => {
257+
#createArgs = (args: ContractMessageParamSpecLatest[] | ContractEventParamSpecLatest[], spec: unknown): AbiParam[] => {
199258
return args.map(({ label, type }, index): AbiParam => {
200259
try {
201260
if (!isObject(type)) {
@@ -233,8 +292,47 @@ export class Abi {
233292
});
234293
};
235294

236-
#createEvent = (spec: ContractEventSpecLatest, index: number): AbiEvent => {
237-
const args = this.#createArgs(spec.args, spec);
295+
#createMessageParams = (args: ContractMessageParamSpecLatest[], spec: unknown): AbiMessageParam[] => {
296+
return this.#createArgs(args, spec);
297+
};
298+
299+
#createEventParams = (args: ContractEventParamSpecLatest[], spec: unknown): AbiEventParam[] => {
300+
const params = this.#createArgs(args, spec);
301+
302+
return params.map((p, index): AbiEventParam => ({ ...p, indexed: args[index].indexed.toPrimitive() }));
303+
};
304+
305+
#createEvent = (index: number): AbiEvent => {
306+
// TODO TypeScript would narrow this type to the correct version,
307+
// but version is `Text` so I need to call `toString()` here,
308+
// which breaks the type inference.
309+
switch (this.metadata.version.toString()) {
310+
case '4':
311+
return this.#createEventV4((this.metadata as ContractMetadataV4).spec.events[index], index);
312+
default:
313+
return this.#createEventV5((this.metadata as ContractMetadataV5).spec.events[index], index);
314+
}
315+
};
316+
317+
#createEventV5 = (spec: EventOf<ContractMetadataV5>, index: number): AbiEvent => {
318+
const args = this.#createEventParams(spec.args, spec);
319+
const event = {
320+
args,
321+
docs: spec.docs.map((d) => d.toString()),
322+
fromU8a: (data: Uint8Array): DecodedEvent => ({
323+
args: this.#decodeArgs(args, data),
324+
event
325+
}),
326+
identifier: [spec.module_path, spec.label].join('::'),
327+
index,
328+
signatureTopic: spec.signature_topic.isSome ? spec.signature_topic.unwrap().toHex() : null
329+
};
330+
331+
return event;
332+
};
333+
334+
#createEventV4 = (spec: EventOf<ContractMetadataV4>, index: number): AbiEvent => {
335+
const args = this.#createEventParams(spec.args, spec);
238336
const event = {
239337
args,
240338
docs: spec.docs.map((d) => d.toString()),
@@ -250,7 +348,7 @@ export class Abi {
250348
};
251349

252350
#createMessage = (spec: ContractMessageSpecLatest | ContractConstructorSpecLatest, index: number, add: Partial<AbiMessage> = {}): AbiMessage => {
253-
const args = this.#createArgs(spec.args, spec);
351+
const args = this.#createMessageParams(spec.args, spec);
254352
const identifier = spec.label.toString();
255353
const message = {
256354
...add,
@@ -267,7 +365,7 @@ export class Abi {
267365
path: identifier.split('::').map((s) => stringCamelCase(s)),
268366
selector: spec.selector,
269367
toU8a: (params: unknown[]) =>
270-
this.#encodeArgs(spec, args, params)
368+
this.#encodeMessageArgs(spec, args, params)
271369
};
272370

273371
return message;
@@ -299,7 +397,7 @@ export class Abi {
299397
return message.fromU8a(trimmed.subarray(4));
300398
};
301399

302-
#encodeArgs = ({ label, selector }: ContractMessageSpecLatest | ContractConstructorSpecLatest, args: AbiParam[], data: unknown[]): Uint8Array => {
400+
#encodeMessageArgs = ({ label, selector }: ContractMessageSpecLatest | ContractConstructorSpecLatest, args: AbiMessageParam[], data: unknown[]): Uint8Array => {
303401
if (data.length !== args.length) {
304402
throw new Error(`Expected ${args.length} arguments to contract message '${label.toString()}', found ${data.length}`);
305403
}

0 commit comments

Comments
 (0)