Skip to content

Commit

Permalink
draft implementation with version metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
peetzweg committed Feb 26, 2024
1 parent 013ea59 commit a028c55
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 50 deletions.
122 changes: 73 additions & 49 deletions packages/api-contract/src/Abi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
// SPDX-License-Identifier: Apache-2.0

import type { Bytes } from '@polkadot/types';
import type { ChainProperties, ContractConstructorSpecLatest, ContractEventSpecLatest, ContractMessageParamSpecLatest, ContractMessageSpecLatest, ContractMetadata, ContractMetadataLatest, 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 { Option, TypeRegistry } from '@polkadot/types';
import { type ChainProperties, type ContractConstructorSpecLatest, type ContractMessageParamSpecLatest, type ContractMessageSpecLatest, type ContractMetadata, type ContractMetadataVersion, type ContractProjectInfo, type ContractTypeSpec, type EventRecord, isV5Metadata, type SupportedContractMetadata } from '@polkadot/types/interfaces';

Check failure on line 9 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

'"@polkadot/types/interfaces"' has no exported member named 'ContractMetadataVersion'. Did you mean 'ContractMetadata'?

Check failure on line 9 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Module '"@polkadot/types/interfaces"' has no exported member 'isV5Metadata'.

Check failure on line 9 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Module '"@polkadot/types/interfaces"' has no exported member 'SupportedContractMetadata'.

Check failure on line 9 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

'"@polkadot/types/interfaces"' has no exported member named 'ContractMetadataVersion'. Did you mean 'ContractMetadata'?

Check failure on line 9 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Module '"@polkadot/types/interfaces"' has no exported member 'isV5Metadata'.

Check failure on line 9 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Module '"@polkadot/types/interfaces"' has no exported member 'SupportedContractMetadata'.
import { TypeDefInfo } from '@polkadot/types-create';
import { assertReturn, compactAddLength, compactStripLength, isBn, isNumber, isObject, isString, isUndefined, logger, stringCamelCase, stringify, u8aConcat, u8aToHex } from '@polkadot/util';

import { convertVersions, enumVersions } from './toLatest.js';
import { convertVersions as convertVersionsCompatible, enumVersions } from './toLatestCompatible.js';

interface AbiJson {
version?: string;
Expand All @@ -32,7 +32,7 @@ function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string
return assertReturn(message, () => `Attempted to call an invalid contract interface, ${stringify(messageOrId)}`);
}

function getLatestMeta (registry: Registry, json: AbiJson): ContractMetadataLatest {
function getMetadata (registry: Registry, json: AbiJson): [SupportedContractMetadata, ContractMetadataVersion] {
// this is for V1, V2, V3
const vx = enumVersions.find((v) => isObject(json[v]));

Expand All @@ -50,20 +50,23 @@ function getLatestMeta (registry: Registry, json: AbiJson): ContractMetadataLate
? { [`V${jsonVersion}`]: json }
: { V0: json }
);
const converter = convertVersions.find(([v]) => metadata[`is${v}`]);
const converter = convertVersionsCompatible.find(([v]) => metadata[`is${v}`]);

if (!converter) {
throw new Error(`Unable to convert ABI with version ${metadata.type} to latest`);
}

return converter[1](registry, metadata[`as${converter[0]}`]);
const upgradedMetadata = converter[1](registry, metadata[`as${converter[0]}`]);

return [upgradedMetadata, metadata.type];
}

function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataLatest, ContractProjectInfo] {
function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, SupportedContractMetadata, ContractProjectInfo, ContractMetadataVersion] {
const registry = new TypeRegistry();
const info = registry.createType('ContractProjectInfo', json) as unknown as ContractProjectInfo;
const latest = getLatestMeta(registry, json as unknown as AbiJson);
const lookup = registry.createType('PortableRegistry', { types: latest.types }, true);
// const latest = getLatestMeta(registry, json as unknown as AbiJson);
const [metadata, version] = getMetadata(registry, json as unknown as AbiJson);
const lookup = registry.createType('PortableRegistry', { types: metadata.types }, true);

// attach the lookup to the registry - now the types are known
registry.setLookup(lookup);
Expand All @@ -77,7 +80,7 @@ function parseJson (json: Record<string, unknown>, chainProperties?: ChainProper
lookup.getTypeDef(id)
);

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

/**
Expand All @@ -102,12 +105,13 @@ export class Abi {
readonly info: ContractProjectInfo;
readonly json: Record<string, unknown>;
readonly messages: AbiMessage[];
readonly metadata: ContractMetadataLatest;
readonly metadata: SupportedContractMetadata;
readonly version: ContractMetadataVersion;
readonly registry: Registry;
readonly environment = new Map<string, TypeDef | Codec>();

constructor (abiJson: Record<string, unknown> | string, chainProperties?: ChainProperties) {
[this.json, this.registry, this.metadata, this.info] = parseJson(
[this.json, this.registry, this.metadata, this.info, this.version] = parseJson(
isString(abiJson)
? JSON.parse(abiJson) as Record<string, unknown>
: abiJson,
Expand All @@ -123,8 +127,8 @@ export class Abi {
: null
})
);
this.events = this.metadata.spec.events.map((spec: ContractEventSpecLatest, index) =>
this.#createEvent(spec, index)
this.events = this.metadata.spec.events.map((_, index) =>

Check failure on line 130 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Parameter '_' implicitly has an 'any' type.

Check failure on line 130 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Parameter 'index' implicitly has an 'any' type.

Check failure on line 130 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Parameter '_' implicitly has an 'any' type.

Check failure on line 130 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Parameter 'index' implicitly has an 'any' type.
this.#createEvent(index)
);
this.messages = this.metadata.spec.messages.map((spec: ContractMessageSpecLatest, index): AbiMessage =>

Check failure on line 133 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Parameter 'index' implicitly has an 'any' type.

Check failure on line 133 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Parameter 'index' implicitly has an 'any' type.
this.#createMessage(spec, index, {
Expand Down Expand Up @@ -163,34 +167,37 @@ export class Abi {
* Warning: Unstable API, bound to change
*/
public decodeEvent (record: EventRecord): DecodedEvent {
// TODO we could double check here if record is actually section `contract` and type `ContractExecution` or `ContractEmitted`
const data = record.event.data[1] as Bytes;

const signatureTopic = record.topics[0];

if (signatureTopic !== undefined) {
const event = this.events.find((e) => e.signatureTopic !== undefined && e.signatureTopic === signatureTopic.toHex());
switch (this.version) {
case 'V4':
case 'V3':
case 'V2':
case 'V1':

if (event) {
return event.fromU8a(data.subarray(0));
} else {
throw new Error(`Unable to find event with signature_topic ${signatureTopic.toHex()}`);
}
} else {
if (!data) {
throw new Error('Unable to find event data');
}
case 'V0':{

Check failure on line 178 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Expected a 'break' statement before 'case'
const index = data[0];

// otherwise fallback to using the index to determine event - ink! v4 downwards
const index = data[0];
const event = this.events[index];

const event = this.events[index];
if (!event) {
throw new Error(`Unable to find event with index ${index}`);
}

if (!event) {
throw new Error(`Unable to find event with index ${index}`);
return event.fromU8a(data.subarray(1));
}

return event.fromU8a(data.subarray(1));
// Latest
default: {
const signatureTopic = record.topics[0];
const event = this.events.find((e) => e.signatureTopic !== undefined && e.signatureTopic === signatureTopic.toHex());

if (event) {
return event.fromU8a(data.subarray(0));
} else {
throw new Error(`Unable to find event with signature_topic ${signatureTopic.toHex()}`);
}
}
}
}

Expand Down Expand Up @@ -254,22 +261,39 @@ export class Abi {
});
};

#createEvent = (spec: ContractEventSpecLatest, index: number): AbiEvent => {
const args = this.#createArgs(spec.args, spec);

const event = {
args,
docs: spec.docs.map((d) => d.toString()),
fromU8a: (data: Uint8Array): DecodedEvent => ({
args: this.#decodeArgs(args, data),
event
}),
identifier: [spec.module_path, spec.label].filter((t) => !t.isEmpty).join('::'),
index,
signatureTopic: spec.signature_topic.toHex() || undefined
};

return event;
#createEvent = (index: number): AbiEvent => {
if (isV5Metadata(this.metadata, this.version)) {
const spec = this.metadata.spec.events[index];
const args = this.#createArgs(spec.args, spec);
const event = {
args,
docs: spec.docs.map((d) => d.toString()),

Check failure on line 270 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Parameter 'd' implicitly has an 'any' type.

Check failure on line 270 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Parameter 'd' implicitly has an 'any' type.
fromU8a: (data: Uint8Array): DecodedEvent => ({
args: this.#decodeArgs(args, data),
event
}),
identifier: [spec.module_path, spec.label].filter((t) => !t.isEmpty).join('::'),
index,
signatureTopic: spec.signature_topic.toHex() || undefined
};

return event;
} else {
const spec = this.metadata.spec.events[index];
const args = this.#createArgs(spec.args, spec);
const event = {
args,
docs: spec.docs.map((d) => d.toString()),

Check failure on line 286 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Parameter 'd' implicitly has an 'any' type.

Check failure on line 286 in packages/api-contract/src/Abi/index.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Parameter 'd' implicitly has an 'any' type.
fromU8a: (data: Uint8Array): DecodedEvent => ({
args: this.#decodeArgs(args, data),
event
}),
identifier: spec.label.toString(),
index
};

return event;
}
};

#createMessage = (spec: ContractMessageSpecLatest | ContractConstructorSpecLatest, index: number, add: Partial<AbiMessage> = {}): AbiMessage => {
Expand Down
46 changes: 46 additions & 0 deletions packages/api-contract/src/Abi/toLatestCompatible.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Copyright 2017-2024 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

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

import { v0ToV1 } from './toV1.js';
import { v1ToV2 } from './toV2.js';
import { v2ToV3 } from './toV3.js';
import { v3ToV4 } from './toV4.js';

// The versions where an enum is used, aka V0 is missing
// (Order from newest, i.e. we expect more on newest vs oldest)
export const enumVersions = ['V5', 'V4', 'V3', 'V2', 'V1'] as const;

type Versions = typeof enumVersions[number] | 'V0';

type Converter = (registry: Registry, vx: any) => ContractMetadataV4 | ContractMetadataV5;

// Helper to convert metadata from one step to the next
function createConverter <I, O> (next: (registry: Registry, input: O) => ContractMetadataLatest, step: (registry: Registry, input: I) => O): (registry: Registry, input: I) => ContractMetadataLatest {
return (registry: Registry, input: I): ContractMetadataLatest =>
next(registry, step(registry, input));
}

export function v5ToLatestCompatible (_registry: Registry, v5: ContractMetadataV5): ContractMetadataV5 {
return v5;
}

export function v4ToLatestCompatible (_registry: Registry, v4: ContractMetadataV4): ContractMetadataV4 {
return v4;
}

export const v3ToLatestCompatible = /*#__PURE__*/ createConverter(v4ToLatestCompatible, v3ToV4);

Check failure on line 34 in packages/api-contract/src/Abi/toLatestCompatible.ts

View workflow job for this annotation

GitHub Actions / pr (build)

Argument of type '(_registry: Registry, v4: ContractMetadataV4) => ContractMetadataV4' is not assignable to parameter of type '(registry: Registry, input: ContractMetadataV4) => ContractMetadataLatest'.

Check failure on line 34 in packages/api-contract/src/Abi/toLatestCompatible.ts

View workflow job for this annotation

GitHub Actions / pr (deno)

Argument of type '(_registry: Registry, v4: ContractMetadataV4) => ContractMetadataV4' is not assignable to parameter of type '(registry: Registry, input: ContractMetadataV4) => ContractMetadataLatest'.
export const v2ToLatestCompatible = /*#__PURE__*/ createConverter(v3ToLatestCompatible, v2ToV3);
export const v1ToLatestCompatible = /*#__PURE__*/ createConverter(v2ToLatestCompatible, v1ToV2);
export const v0ToLatestCompatible = /*#__PURE__*/ createConverter(v1ToLatestCompatible, v0ToV1);

export const convertVersions: [Versions, Converter][] = [
['V5', v5ToLatestCompatible],
['V4', v4ToLatestCompatible],
['V3', v3ToLatestCompatible],
['V2', v2ToLatestCompatible],
['V1', v1ToLatestCompatible],
['V0', v0ToLatestCompatible]
];
22 changes: 21 additions & 1 deletion packages/types/src/interfaces/contractsAbi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export interface ContractEventParamSpecV2 extends Struct {
/** @name ContractEventSpecLatest */
export interface ContractEventSpecLatest extends ContractEventSpecV3 {}

/** @name SupportedContractEventSpec */
export type SupportedContractEventSpec = SupportedContractMetadata['spec']['events'][number]

/** @name ContractEventSpecV0 */
export interface ContractEventSpecV0 extends Struct {
readonly name: Text;
Expand Down Expand Up @@ -290,6 +293,11 @@ export interface ContractMessageSpecV3 extends Struct {
readonly default: bool;
}



/** @name ContractMetadataVersion */
export type ContractMetadataVersion ='V0' | 'V1' | 'V2' | 'V3' | 'V4' | 'V5';

/** @name ContractMetadata */
export interface ContractMetadata extends Enum {
readonly isV0: boolean;
Expand All @@ -304,12 +312,24 @@ export interface ContractMetadata extends Enum {
readonly asV4: ContractMetadataV4;
readonly isV5: boolean;
readonly asV5: ContractMetadataV5;
readonly type: 'V0' | 'V1' | 'V2' | 'V3' | 'V4' | 'V5';
readonly type: ContractMetadataVersion;
}


/** @name ContractMetadataLatest */
export interface ContractMetadataLatest extends ContractMetadataV5 {}

/** @name SupportedContractMetadata */
export type SupportedContractMetadata = ContractMetadataV5 | ContractMetadataV4

export const isV4Metadata = (metadata: SupportedContractMetadata, version: ContractMetadataVersion): metadata is ContractMetadataV4 =>{
return version === 'V4'
}

export const isV5Metadata = (metadata: SupportedContractMetadata, version: ContractMetadataVersion): metadata is ContractMetadataV5 =>{
return version === 'V5'
}

/** @name ContractMetadataV0 */
export interface ContractMetadataV0 extends Struct {
readonly metadataVersion: Text;
Expand Down

0 comments on commit a028c55

Please sign in to comment.