Skip to content

Commit 3ffc196

Browse files
AlexD10Speterwht
andauthored
feat(api-contract): add support for ink! v6 and pallet-revive compatibility (#6158)
* wip: support revive for ink! v6 * revive components * fix: support v6 metadata * fix: remove debug messages * test: extend tests for v6ToLatestCompatible * refactor: remove Revive specific components * test: construct revive compatible contract * feat: use ContractReviveProjectInfo new type * fix: parse version to decide what kind of contract * refactor: remove hardcoded address * fix: format --------- Co-authored-by: Peter White <[email protected]>
1 parent 252de3b commit 3ffc196

File tree

20 files changed

+2157
-34
lines changed

20 files changed

+2157
-34
lines changed

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

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
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';
5+
import type { ChainProperties, ContractConstructorSpecLatest, ContractEventParamSpecLatest, ContractMessageParamSpecLatest, ContractMessageSpecLatest, ContractMetadata, ContractMetadataV4, ContractMetadataV5, ContractMetadataV6, ContractProjectInfo, ContractTypeSpec, EventRecord } from '@polkadot/types/interfaces';
66
import type { Codec, Registry, TypeDef } from '@polkadot/types/types';
77
import type { AbiConstructor, AbiEvent, AbiEventParam, AbiMessage, AbiMessageParam, AbiParam, DecodedEvent, DecodedMessage } from '../types.js';
88

@@ -19,12 +19,12 @@ interface AbiJson {
1919
}
2020

2121
type EventOf<M> = M extends {spec: { events: Vec<infer E>}} ? E : never
22-
export type ContractMetadataSupported = ContractMetadataV4 | ContractMetadataV5;
22+
export type ContractMetadataSupported = ContractMetadataV4 | ContractMetadataV5 | ContractMetadataV6;
2323
type ContractEventSupported = EventOf<ContractMetadataSupported>;
2424

2525
const l = logger('Abi');
2626

27-
const PRIMITIVE_ALWAYS = ['AccountId', 'AccountIndex', 'Address', 'Balance'];
27+
const PRIMITIVE_ALWAYS = ['AccountId', 'AccountId20', 'AccountIndex', 'Address', 'Balance'];
2828

2929
function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string | number): T {
3030
const message = isNumber(messageOrId)
@@ -66,9 +66,28 @@ function getMetadata (registry: Registry, json: AbiJson): ContractMetadataSuppor
6666
return upgradedMetadata;
6767
}
6868

69-
function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataSupported, ContractProjectInfo] {
69+
function isRevive (json: Record<string, unknown>): boolean {
70+
const source = json['source'];
71+
const version = json['version'];
72+
73+
const hasContractBinary =
74+
typeof source === 'object' &&
75+
source !== null &&
76+
'contract_binary' in source;
77+
78+
const hasVersion =
79+
typeof version === 'number' && version >= 6;
80+
81+
return hasContractBinary || hasVersion;
82+
}
83+
84+
function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataSupported, ContractProjectInfo, boolean] {
7085
const registry = new TypeRegistry();
71-
const info = registry.createType('ContractProjectInfo', json) as unknown as ContractProjectInfo;
86+
87+
const revive = isRevive(json);
88+
const typeName = revive ? 'ContractReviveProjectInfo' : 'ContractProjectInfo';
89+
90+
const info = registry.createType(typeName, json) as unknown as ContractProjectInfo;
7291
const metadata = getMetadata(registry, json as unknown as AbiJson);
7392
const lookup = registry.createType('PortableRegistry', { types: metadata.types }, true);
7493

@@ -84,7 +103,7 @@ function parseJson (json: Record<string, unknown>, chainProperties?: ChainProper
84103
lookup.getTypeDef(id)
85104
);
86105

87-
return [json, registry, metadata, info];
106+
return [json, registry, metadata, info, revive];
88107
}
89108

90109
/**
@@ -112,9 +131,10 @@ export class Abi {
112131
readonly metadata: ContractMetadataSupported;
113132
readonly registry: Registry;
114133
readonly environment = new Map<string, TypeDef | Codec>();
134+
readonly isRevive: boolean;
115135

116136
constructor (abiJson: Record<string, unknown> | string, chainProperties?: ChainProperties) {
117-
[this.json, this.registry, this.metadata, this.info] = parseJson(
137+
[this.json, this.registry, this.metadata, this.info, this.isRevive] = parseJson(
118138
isString(abiJson)
119139
? JSON.parse(abiJson) as Record<string, unknown>
120140
: abiJson,

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

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import { TypeRegistry } from '@polkadot/types';
77

88
import abis from '../test/contracts/index.js';
9-
import { v0ToLatestCompatible, v1ToLatestCompatible, v2ToLatestCompatible, v3ToLatestCompatible, v4ToLatestCompatible, v5ToLatestCompatible } from './toLatestCompatible.js';
9+
import { v0ToLatestCompatible, v1ToLatestCompatible, v2ToLatestCompatible, v3ToLatestCompatible, v4ToLatestCompatible, v5ToLatestCompatible, v6ToLatestCompatible } from './toLatestCompatible.js';
1010

1111
describe('v0ToLatestCompatible', (): void => {
1212
const registry = new TypeRegistry();
@@ -191,3 +191,29 @@ describe('v5ToLatestCompatible', (): void => {
191191
expect(latest.version.toString()).toEqual('5');
192192
});
193193
});
194+
195+
describe('v6ToLatestCompatible', (): void => {
196+
const registry = new TypeRegistry();
197+
const contract = registry.createType('ContractMetadata', { V6: abis['ink_v6_erc20Metadata'] });
198+
const latest = v6ToLatestCompatible(registry, contract.asV6);
199+
200+
it('has the correct messages', (): void => {
201+
expect(
202+
latest.spec.messages.map(({ label }) => label.toString())
203+
).toEqual(['total_supply', 'balance_of', 'allowance', 'transfer', 'approve', 'transfer_from']);
204+
});
205+
206+
it('has H160 as the type of balance_of argument', (): void => {
207+
const arg = latest.spec.messages.find(
208+
(m) => m.label.toString() === 'balance_of'
209+
)?.args[0];
210+
211+
const name = arg?.type.displayName?.[0]?.toString();
212+
213+
expect(name).toBe('H160');
214+
});
215+
216+
it('has the latest compatible version number', (): void => {
217+
expect(latest.version.toString()).toEqual('6');
218+
});
219+
});

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

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

4-
import type { ContractMetadataV4, ContractMetadataV5 } from '@polkadot/types/interfaces';
4+
import type { ContractMetadataV4, ContractMetadataV5, ContractMetadataV6 } from '@polkadot/types/interfaces';
55
import type { Registry } from '@polkadot/types/types';
66
import type { ContractMetadataSupported } from './index.js';
77

@@ -12,7 +12,7 @@ import { v3ToV4 } from './toV4.js';
1212

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

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

@@ -24,6 +24,10 @@ function createConverter <I, O> (next: (registry: Registry, input: O) => Contrac
2424
next(registry, step(registry, input));
2525
}
2626

27+
export function v6ToLatestCompatible (_registry: Registry, v6: ContractMetadataV6): ContractMetadataV6 {
28+
return v6;
29+
}
30+
2731
export function v5ToLatestCompatible (_registry: Registry, v5: ContractMetadataV5): ContractMetadataV5 {
2832
return v5;
2933
}
@@ -38,6 +42,7 @@ export const v1ToLatestCompatible = /*#__PURE__*/ createConverter(v2ToLatestComp
3842
export const v0ToLatestCompatible = /*#__PURE__*/ createConverter(v1ToLatestCompatible, v0ToV1);
3943

4044
export const convertVersions: [Versions, Converter][] = [
45+
['V6', v6ToLatestCompatible],
4146
['V5', v5ToLatestCompatible],
4247
['V4', v4ToLatestCompatible],
4348
['V3', v3ToLatestCompatible],

packages/api-contract/src/base/Base.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,11 @@ export abstract class Base<ApiType extends ApiTypes> {
1616

1717
protected readonly _decorateMethod: DecorateMethod<ApiType>;
1818
protected readonly _isWeightV1: boolean;
19+
protected readonly _isRevive: boolean;
1920

2021
constructor (api: ApiBase<ApiType>, abi: string | Record<string, unknown> | Abi, decorateMethod: DecorateMethod<ApiType>) {
2122
if (!api || !api.isConnected || !api.tx) {
2223
throw new Error('Your API has not been initialized correctly and is not connected to a chain');
23-
} else if (!api.tx.contracts || !isFunction(api.tx.contracts.instantiateWithCode) || api.tx.contracts.instantiateWithCode.meta.args.length !== 6) {
24-
throw new Error('The runtime does not expose api.tx.contracts.instantiateWithCode with storageDepositLimit');
25-
} else if (!api.call.contractsApi || !isFunction(api.call.contractsApi.call)) {
26-
throw new Error('Your runtime does not expose the api.call.contractsApi.call runtime interfaces');
2724
}
2825

2926
this.abi = abi instanceof Abi
@@ -32,6 +29,21 @@ export abstract class Base<ApiType extends ApiTypes> {
3229
this.api = api;
3330
this._decorateMethod = decorateMethod;
3431
this._isWeightV1 = !api.registry.createType<WeightV2>('Weight').proofSize;
32+
this._isRevive = this.abi.isRevive;
33+
34+
if (this._isRevive) {
35+
if (!api.tx.revive || !isFunction(api.tx.revive.instantiateWithCode) || api.tx.revive.instantiateWithCode.meta.args.length !== 6) {
36+
throw new Error('The runtime does not expose api.tx.revive.instantiateWithCode with storageDepositLimit');
37+
} else if (!api.call.reviveApi || !isFunction(api.call.reviveApi.call)) {
38+
throw new Error('Your runtime does not expose the api.call.reviveApi.call runtime interfaces');
39+
}
40+
} else {
41+
if (!api.tx.contracts || !isFunction(api.tx.contracts.instantiateWithCode) || api.tx.contracts.instantiateWithCode.meta.args.length !== 6) {
42+
throw new Error('The runtime does not expose api.tx.contracts.instantiateWithCode with storageDepositLimit');
43+
} else if (!api.call.contractsApi || !isFunction(api.call.contractsApi.call)) {
44+
throw new Error('Your runtime does not expose the api.call.contractsApi.call runtime interfaces');
45+
}
46+
}
3547
}
3648

3749
public get registry (): Registry {

packages/api-contract/src/base/Blueprint.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,11 @@ export class Blueprint<ApiType extends ApiTypes> extends Base<ApiType> {
5555
}
5656

5757
#deploy = (constructorOrId: AbiConstructor | string | number, { gasLimit = BN_ZERO, salt, storageDepositLimit = null, value = BN_ZERO }: BlueprintOptions, params: unknown[]): SubmittableExtrinsic<ApiType, BlueprintSubmittableResult<ApiType>> => {
58-
return this.api.tx.contracts.instantiate(
58+
const palletTx = this._isRevive
59+
? this.api.tx.revive
60+
: this.api.tx.contracts;
61+
62+
return palletTx.instantiate(
5963
value,
6064
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
6165
// @ts-ignore jiggle v1 weights, metadata points to latest
@@ -67,9 +71,23 @@ export class Blueprint<ApiType extends ApiTypes> extends Base<ApiType> {
6771
this.abi.findConstructor(constructorOrId).toU8a(params),
6872
encodeSalt(salt)
6973
).withResultTransform((result: ISubmittableResult) =>
70-
new BlueprintSubmittableResult(result, applyOnEvent(result, ['Instantiated'], ([record]: EventRecord[]) =>
71-
new Contract<ApiType>(this.api, this.abi, record.event.data[1] as AccountId, this._decorateMethod)
72-
))
74+
new BlueprintSubmittableResult(result,
75+
this._isRevive
76+
? (
77+
(result.status.isInBlock || result.status.isFinalized)
78+
? new Contract<ApiType>(
79+
this.api,
80+
this.abi,
81+
// your fixed address for revive deployments
82+
this.registry.createType('AccountId', '0x'),
83+
this._decorateMethod
84+
)
85+
: undefined
86+
)
87+
: applyOnEvent(result, ['Instantiated'], ([record]: EventRecord[]) =>
88+
new Contract<ApiType>(this.api, this.abi, record.event.data[1] as AccountId, this._decorateMethod)
89+
)
90+
)
7391
);
7492
};
7593
}

packages/api-contract/src/base/Code.spec.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ import { toPromiseMethod } from '@polkadot/api';
1010
import v0contractFlipper from '../test/contracts/ink/v0/flipper.contract.json' assert { type: 'json' };
1111
import v0abiFlipper from '../test/contracts/ink/v0/flipper.json' assert { type: 'json' };
1212
import v1contractFlipper from '../test/contracts/ink/v1/flipper.contract.json' assert { type: 'json' };
13+
import v6contractErc20 from '../test/contracts/ink/v6/erc20.contract.json' assert { type: 'json' };
1314
import { Code } from './Code.js';
14-
import { mockApi } from './mock.js';
15+
import { mockApi, mockReviveApi } from './mock.js';
1516

1617
const v0wasmFlipper = fs.readFileSync(new URL('../test/contracts/ink/v0/flipper.wasm', import.meta.url), 'utf-8');
1718

@@ -33,4 +34,14 @@ describe('Code', (): void => {
3334
() => new Code(mockApi, v1contractFlipper as Record<string, unknown>, null, toPromiseMethod)
3435
).not.toThrow();
3536
});
37+
38+
it('can construct a revive compatible contract (v6)', (): void => {
39+
expect(
40+
() => new Code(mockApi, v6contractErc20 as Record<string, unknown>, null, toPromiseMethod)
41+
).toThrow('The runtime does not expose api.tx.revive.instantiateWithCode with storageDepositLimit');
42+
43+
expect(
44+
() => new Code(mockReviveApi, v6contractErc20 as Record<string, unknown>, null, toPromiseMethod)
45+
).not.toThrow();
46+
});
3647
});

packages/api-contract/src/base/Code.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,30 @@ export class Code<ApiType extends ApiTypes> extends Base<ApiType> {
6868
}
6969

7070
#instantiate = (constructorOrId: AbiConstructor | string | number, { gasLimit = BN_ZERO, salt, storageDepositLimit = null, value = BN_ZERO }: BlueprintOptions, params: unknown[]): SubmittableExtrinsic<ApiType, CodeSubmittableResult<ApiType>> => {
71-
return this.api.tx.contracts.instantiateWithCode(
71+
const palletTx = this._isRevive ? this.api.tx.revive : this.api.tx.contracts;
72+
73+
if (this._isRevive) {
74+
return palletTx.instantiateWithCode(
75+
value,
76+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
77+
// @ts-ignore jiggle v1 weights, metadata points to latest
78+
this._isWeightV1
79+
? convertWeight(gasLimit).v1Weight
80+
: convertWeight(gasLimit).v2Weight,
81+
storageDepositLimit,
82+
compactAddLength(this.code),
83+
this.abi.findConstructor(constructorOrId).toU8a(params),
84+
encodeSalt(salt)
85+
).withResultTransform((result: ISubmittableResult) =>
86+
new CodeSubmittableResult(
87+
result,
88+
new Blueprint<ApiType>(this.api, this.abi, this.abi.info.source.hash, this._decorateMethod),
89+
new Contract<ApiType>(this.api, this.abi, '0x', this._decorateMethod)
90+
)
91+
);
92+
}
93+
94+
return palletTx.instantiateWithCode(
7295
value,
7396
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
7497
// @ts-ignore jiggle v1 weights, metadata points to latest

packages/api-contract/src/base/Contract.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import type { ApiBase } from '@polkadot/api/base';
55
import type { SubmittableExtrinsic } from '@polkadot/api/submittable/types';
66
import type { ApiTypes, DecorateMethod } from '@polkadot/api/types';
7-
import type { AccountId, ContractExecResult, EventRecord, Weight, WeightV2 } from '@polkadot/types/interfaces';
7+
import type { AccountId, AccountId20, ContractExecResult, EventRecord, Weight, WeightV2 } from '@polkadot/types/interfaces';
88
import type { ISubmittableResult } from '@polkadot/types/types';
99
import type { Abi } from '../Abi/index.js';
1010
import type { AbiMessage, ContractCallOutcome, ContractOptions, DecodedEvent, WeightAll } from '../types.js';
@@ -52,15 +52,15 @@ export class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
5252
/**
5353
* @description The on-chain address for this contract
5454
*/
55-
readonly address: AccountId;
55+
readonly address: AccountId | AccountId20;
5656

5757
readonly #query: MapMessageQuery<ApiType> = {};
5858
readonly #tx: MapMessageTx<ApiType> = {};
5959

60-
constructor (api: ApiBase<ApiType>, abi: string | Record<string, unknown> | Abi, address: string | AccountId, decorateMethod: DecorateMethod<ApiType>) {
60+
constructor (api: ApiBase<ApiType>, abi: string | Record<string, unknown> | Abi, address: string | AccountId | AccountId20, decorateMethod: DecorateMethod<ApiType>) {
6161
super(api, abi, decorateMethod);
6262

63-
this.address = this.registry.createType('AccountId', address);
63+
this.address = this.registry.createType(this._isRevive ? 'AccountId20' : 'AccountId', address);
6464

6565
this.abi.messages.forEach((m): void => {
6666
if (isUndefined(this.#tx[m.method])) {
@@ -100,7 +100,9 @@ export class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
100100
};
101101

102102
#exec = (messageOrId: AbiMessage | string | number, { gasLimit = BN_ZERO, storageDepositLimit = null, value = BN_ZERO }: ContractOptions, params: unknown[]): SubmittableExtrinsic<ApiType> => {
103-
return this.api.tx.contracts.call(
103+
const palletTx = this._isRevive ? this.api.tx.revive : this.api.tx.contracts;
104+
105+
return palletTx.call(
104106
this.address,
105107
value,
106108
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
@@ -134,7 +136,9 @@ export class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
134136
return {
135137
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
136138
send: this._decorateMethod((origin: string | AccountId | Uint8Array) =>
137-
this.api.rx.call.contractsApi.call<ContractExecResult>(
139+
(this._isRevive
140+
? this.api.rx.call.reviveApi.call
141+
: this.api.rx.call.contractsApi.call)<ContractExecResult>(
138142
origin,
139143
this.address,
140144
value,

packages/api-contract/src/base/mock.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,20 @@ export const mockApi = {
2929
}
3030
}
3131
} as unknown as ApiBase<'promise'>;
32+
33+
export const mockReviveApi = {
34+
call: {
35+
reviveApi: {
36+
call: (): never => {
37+
throw new Error('mock');
38+
}
39+
}
40+
},
41+
isConnected: true,
42+
registry,
43+
tx: {
44+
revive: {
45+
instantiateWithCode
46+
}
47+
}
48+
} as unknown as ApiBase<'promise'>;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// SPDX-License-Identifier: Apache-2.0
33

44
import type { ApiPromise } from '@polkadot/api';
5-
import type { AccountId, Hash } from '@polkadot/types/interfaces';
5+
import type { AccountId, AccountId20, Hash } from '@polkadot/types/interfaces';
66
import type { Abi } from '../Abi/index.js';
77

88
import { toPromiseMethod } from '@polkadot/api';
@@ -22,7 +22,7 @@ export class CodePromise extends Code<'promise'> {
2222
}
2323

2424
export class ContractPromise extends Contract<'promise'> {
25-
constructor (api: ApiPromise, abi: string | Record<string, unknown> | Abi, address: string | AccountId) {
25+
constructor (api: ApiPromise, abi: string | Record<string, unknown> | Abi, address: string | AccountId | AccountId20) {
2626
super(api, abi, address, toPromiseMethod);
2727
}
2828
}

0 commit comments

Comments
 (0)