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

feat: ink! v5 #5791

Merged
merged 28 commits into from
Mar 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
db3822d
adds definitions and types according to ink v5 changes
peetzweg Feb 1, 2024
9f93a19
adds toV5 boilerplate code draft
peetzweg Feb 1, 2024
0cf749e
adds v5 flipper test contract code
peetzweg Feb 1, 2024
cfba268
fix license dates
peetzweg Feb 1, 2024
7d404f4
adds test v5 toLatest test
peetzweg Feb 1, 2024
1f9e6f5
implements new scheme to determine event
peetzweg Feb 1, 2024
985365f
apply linter changes
peetzweg Feb 1, 2024
5db72af
adds test result outputs
peetzweg Feb 1, 2024
cc946da
change `EventRecord['topics'][0]` type to plain `Hash`
peetzweg Feb 16, 2024
54fd609
adds testcases for decoding payload data of a ink!v4 and ink!v5 event
peetzweg Feb 23, 2024
013ea59
changes `Abi.decodeEvent(data:Bytes)` method interface to `Abi.decode…
peetzweg Feb 26, 2024
a028c55
draft implementation with version metadata
peetzweg Feb 26, 2024
3e0d8d1
cleaner implementation of versioned Metadata by actually leveraging t…
peetzweg Feb 27, 2024
ada84b3
Merge branch 'polkadot-js:master' into pz/ink-v5
peetzweg Feb 27, 2024
0029e18
trying to make linter happy
peetzweg Feb 27, 2024
f000f24
Merge branch 'pz/ink-v5' of github.com:peetzweg/pjs-api into pz/ink-v5
peetzweg Feb 27, 2024
0124e14
makes `ContractMetadataSupported` in internal to `Abi` type and not e…
peetzweg Feb 27, 2024
fcb684d
properly types unused parameter for tsc :shrug:
peetzweg Feb 27, 2024
64feb63
adds `@polkadot/types-support` dev dependency
peetzweg Feb 27, 2024
df9956c
merge master
peetzweg Feb 27, 2024
f1a1b9d
Update yarn.lock
peetzweg Feb 27, 2024
1cda4b9
references `types-support` in `api-contract
peetzweg Feb 27, 2024
246570d
resolving change requests
peetzweg Feb 28, 2024
f88fcf7
resolves linter warnings
peetzweg Feb 28, 2024
f67a88a
changes ContractMetadataV5 field to `u64` from `Text`
peetzweg Feb 28, 2024
27d6b46
adds contracts and contract metadata compiled with the most recent in…
peetzweg Feb 28, 2024
0e039e7
implements decoding of anonymous events if possible
peetzweg Mar 1, 2024
3b48940
removes done todo comments
peetzweg Mar 1, 2024
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
38 changes: 36 additions & 2 deletions packages/api-contract/src/Abi/Abi.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ describe('Abi', (): void => {
registry.setMetadata(metadata);
});

it('decoding <=ink!v4 events', (): void => {
it('decoding <=ink!v4 event', (): void => {
const abiJson = abis['ink_v4_erc20Metadata'];

expect(abiJson).toBeDefined();
Expand Down Expand Up @@ -166,7 +166,7 @@ describe('Abi', (): void => {
expect(decodedEventHuman).toEqual(expectedEvent);
});

it('decoding >=ink!v5 events', (): void => {
it('decoding >=ink!v5 event', (): void => {
const abiJson = abis['ink_v5_erc20Metadata'];

expect(abiJson).toBeDefined();
Expand Down Expand Up @@ -197,5 +197,39 @@ describe('Abi', (): void => {

expect(decodedEventHuman).toEqual(expectedEvent);
});
peetzweg marked this conversation as resolved.
Show resolved Hide resolved

it('decoding >=ink!v5 anonymous event', (): void => {
const abiJson = abis['ink_v5_erc20AnonymousTransferMetadata'];

expect(abiJson).toBeDefined();
const abi = new Abi(abiJson);

expect(abi.events[0].identifier).toEqual('erc20::erc20::Transfer');
expect(abi.events[0].signatureTopic).toEqual(null);

const eventRecordWithAnonymousEventHex = '0x00010000000803538e726248a9c155911e7d99f4f474c3408630a2f6275dd501d4471c7067ad2c490101d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d018eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a4800505a4f7e9f4eb1060000000000000008d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48';
const record = registry.createType('EventRecord', eventRecordWithAnonymousEventHex);

const decodedEvent = abi.decodeEvent(record);

expect(decodedEvent.event.args.length).toEqual(3);
expect(decodedEvent.args.length).toEqual(3);
expect(decodedEvent.event.identifier).toEqual('erc20::erc20::Transfer');

const decodedEventHuman = decodedEvent.event.args.reduce((prev, cur, index) => {
return {
...prev,
[cur.name]: decodedEvent.args[index].toHuman()
};
}, {});

const expectedEvent = {
from: '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY',
to: '5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty',
value: '123.4567 MUnit'
};

expect(decodedEventHuman).toEqual(expectedEvent);
});
});
});
66 changes: 52 additions & 14 deletions packages/api-contract/src/Abi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
// SPDX-License-Identifier: Apache-2.0

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

import { Option, TypeRegistry } from '@polkadot/types';
import { TypeDefInfo } from '@polkadot/types-create';
Expand Down Expand Up @@ -181,15 +181,43 @@ export class Abi {
}

#decodeEventV5 = (record: EventRecord): DecodedEvent => {
peetzweg marked this conversation as resolved.
Show resolved Hide resolved
const data = record.event.data[1] as Bytes;
// Find event by first topic, which potentially is the signature_topic
const signatureTopic = record.topics[0];
const event = this.events.find((e) => e.signatureTopic !== undefined && e.signatureTopic === signatureTopic.toHex());
const data = record.event.data[1] as Bytes;

if (!event) {
throw new Error(`Unable to find event with signature_topic ${signatureTopic.toHex()}`);
if (signatureTopic) {
const event = this.events.find((e) => e.signatureTopic !== undefined && e.signatureTopic !== null && e.signatureTopic === signatureTopic.toHex());

// Early return if event found by signature topic
if (event) {
return event.fromU8a(data);
}
}

return event.fromU8a(data.subarray(0));
// If no event returned yet, it might be anonymous
const amountOfTopics = record.topics.length;
const potentialEvents = this.events.filter((e) => {
// event can't have a signature topic
if (e.signatureTopic !== null && e.signatureTopic !== undefined) {
return false;
}

// event should have same amount of indexed fields as emitted topics
const amountIndexed = e.args.filter((a) => a.indexed).length;

if (amountIndexed !== amountOfTopics) {
return false;
}

// If all conditions met, it's a potential event
return true;
});

if (potentialEvents.length === 1) {
return potentialEvents[0].fromU8a(data);
}

throw new Error('Unable to determine event');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other case to consider is if a cross contract call occurs, which in turn raises an event. Since the event is raised from another contract we don't have the metadata here...so we might not want to raise an error and just give back the raw bytes instead of attempting to decode?

This scenario might also lead to a false positive in the heuristic for above for determining anon events, if the foreign event has the same number of topics 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay, so if contract A calls a function on B which emits an event, the event will be emitted by A and not B?

Feels like it makes sense as users might not know anything about B so can only listen to A.

As of now I have to little knowledge on what's happening above this little 'Abi.decodeEvent world' to have an idea how to handle this the best, yet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh okay, so if contract A calls a function on B which emits an event, the event will be emitted by A and not B?

It depends, using forward_call (contract A calling contract B), the account id topic will be for contract B so would be easy to distinguish

The other case is using delegate_call where the code of contract B is invoked from contract A, in this case the account id topic would be of contract A.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah okay! Will tackle this in a follow up feature pr! 🚀

};

#decodeEventV4 = (record: EventRecord): DecodedEvent => {
Expand Down Expand Up @@ -226,7 +254,7 @@ export class Abi {
return findMessage(this.messages, messageOrId);
}

#createArgs = (args: ContractMessageParamSpecLatest[], spec: unknown): AbiParam[] => {
#createArgs = (args: ContractMessageParamSpecLatest[] | ContractEventParamSpecLatest[], spec: unknown): AbiParam[] => {
return args.map(({ label, type }, index): AbiParam => {
try {
if (!isObject(type)) {
Expand Down Expand Up @@ -264,6 +292,16 @@ export class Abi {
});
};

#createMessageParams = (args: ContractMessageParamSpecLatest[], spec: unknown): AbiMessageParam[] => {
return this.#createArgs(args, spec);
};

#createEventParams = (args: ContractEventParamSpecLatest[], spec: unknown): AbiEventParam[] => {
const params = this.#createArgs(args, spec);

return params.map((p, index): AbiEventParam => ({ ...p, indexed: args[index].indexed.toPrimitive() }));
};

#createEvent = (index: number): AbiEvent => {
// TODO TypeScript would narrow this type to the correct version,
// but version is `Text` so I need to call `toString()` here,
Expand All @@ -277,7 +315,7 @@ export class Abi {
};

#createEventV5 = (spec: EventOf<ContractMetadataV5>, index: number): AbiEvent => {
const args = this.#createArgs(spec.args, spec);
const args = this.#createEventParams(spec.args, spec);
const event = {
args,
docs: spec.docs.map((d) => d.toString()),
Expand All @@ -287,14 +325,14 @@ export class Abi {
}),
identifier: [spec.module_path, spec.label].join('::'),
index,
signatureTopic: spec.signature_topic.toHex()
signatureTopic: spec.signature_topic.isSome ? spec.signature_topic.unwrap().toHex() : null
};

return event;
};

#createEventV4 = (spec: EventOf<ContractMetadataV4>, index: number): AbiEvent => {
const args = this.#createArgs(spec.args, spec);
const args = this.#createEventParams(spec.args, spec);
const event = {
args,
docs: spec.docs.map((d) => d.toString()),
Expand All @@ -310,7 +348,7 @@ export class Abi {
};

#createMessage = (spec: ContractMessageSpecLatest | ContractConstructorSpecLatest, index: number, add: Partial<AbiMessage> = {}): AbiMessage => {
const args = this.#createArgs(spec.args, spec);
const args = this.#createMessageParams(spec.args, spec);
const identifier = spec.label.toString();
const message = {
...add,
Expand All @@ -327,7 +365,7 @@ export class Abi {
path: identifier.split('::').map((s) => stringCamelCase(s)),
selector: spec.selector,
toU8a: (params: unknown[]) =>
this.#encodeArgs(spec, args, params)
this.#encodeMessageArgs(spec, args, params)
};

return message;
Expand Down Expand Up @@ -359,7 +397,7 @@ export class Abi {
return message.fromU8a(trimmed.subarray(4));
};

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