Skip to content

Commit

Permalink
implements decoding of anonymous events if possible
Browse files Browse the repository at this point in the history
  • Loading branch information
peetzweg committed Mar 1, 2024
1 parent 27d6b46 commit 0e039e7
Show file tree
Hide file tree
Showing 8 changed files with 1,496 additions and 21 deletions.
39 changes: 37 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,40 @@ describe('Abi', (): void => {

expect(decodedEventHuman).toEqual(expectedEvent);
});

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

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);
// TODO what should even happen?

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 => {
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');
};

#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

0 comments on commit 0e039e7

Please sign in to comment.