Skip to content
20 changes: 12 additions & 8 deletions packages/api-contract/src/Abi/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

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

Expand All @@ -19,12 +19,12 @@
}

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

const l = logger('Abi');

const PRIMITIVE_ALWAYS = ['AccountId', 'AccountIndex', 'Address', 'Balance'];
const PRIMITIVE_ALWAYS = ['AccountId', 'AccountId20', 'AccountIndex', 'Address', 'Balance'];

function findMessage <T extends AbiMessage> (list: T[], messageOrId: T | string | number): T {
const message = isNumber(messageOrId)
Expand Down Expand Up @@ -62,13 +62,16 @@
}

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

return upgradedMetadata;

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

View workflow job for this annotation

GitHub Actions / pr (lint)

Expected blank line before this statement
}

function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataSupported, ContractProjectInfo] {
function parseJson (json: Record<string, unknown>, chainProperties?: ChainProperties): [Record<string, unknown>, Registry, ContractMetadataSupported, ContractProjectInfo, boolean] {
const registry = new TypeRegistry();
const info = registry.createType('ContractProjectInfo', json) as unknown as ContractProjectInfo;

const isRevive = Boolean((json as any)?.source?.contract_binary) || (json as any)?.version >= 6;

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

View workflow job for this annotation

GitHub Actions / pr (lint)

Unsafe member access .version on an `any` value

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

View workflow job for this annotation

GitHub Actions / pr (lint)

Unsafe member access .source on an `any` value
const typeName = isRevive ? 'ContractReviveProjectInfo' : 'ContractProjectInfo';

const info = registry.createType(typeName, json) as unknown as ContractProjectInfo;
const metadata = getMetadata(registry, json as unknown as AbiJson);
const lookup = registry.createType('PortableRegistry', { types: metadata.types }, true);

Expand All @@ -84,7 +87,7 @@
lookup.getTypeDef(id)
);

return [json, registry, metadata, info];
return [json, registry, metadata, info, isRevive];
}

/**
Expand Down Expand Up @@ -112,9 +115,10 @@
readonly metadata: ContractMetadataSupported;
readonly registry: Registry;
readonly environment = new Map<string, TypeDef | Codec>();
readonly isRevive: boolean;

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.isRevive] = parseJson(
isString(abiJson)
? JSON.parse(abiJson) as Record<string, unknown>
: abiJson,
Expand Down
28 changes: 27 additions & 1 deletion packages/api-contract/src/Abi/toLatestCompatible.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { TypeRegistry } from '@polkadot/types';

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

describe('v0ToLatestCompatible', (): void => {
const registry = new TypeRegistry();
Expand Down Expand Up @@ -191,3 +191,29 @@
expect(latest.version.toString()).toEqual('5');
});
});

describe('v6ToLatestCompatible', (): void => {
const registry = new TypeRegistry();
const contract = registry.createType('ContractMetadata', { V6: abis['ink_v6_erc20Metadata'] });
const latest = v6ToLatestCompatible(registry, contract.asV6);

it('has the correct messages', (): void => {
expect(
latest.spec.messages.map(({ label }) => label.toString())
).toEqual(['total_supply', 'balance_of', 'allowance', 'transfer', 'approve', 'transfer_from']);
});

it('has H160 as the type of balance_of argument', (): void => {
const arg = latest.spec.messages.find(
(m) => m.label.toString() === 'balance_of'
)?.args[0];

const name = arg?.type.displayName?.[0]?.toString();

expect(name).toBe('H160');
});

it('has the latest compatible version number', (): void => {
expect(latest.version.toString()).toEqual('6');
});
});

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

View workflow job for this annotation

GitHub Actions / pr (lint)

Newline required at end of file but not found
9 changes: 7 additions & 2 deletions packages/api-contract/src/Abi/toLatestCompatible.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2017-2025 @polkadot/api-contract authors & contributors
// SPDX-License-Identifier: Apache-2.0

import type { ContractMetadataV4, ContractMetadataV5 } from '@polkadot/types/interfaces';
import type { ContractMetadataV4, ContractMetadataV5, ContractMetadataV6 } from '@polkadot/types/interfaces';
import type { Registry } from '@polkadot/types/types';
import type { ContractMetadataSupported } from './index.js';

Expand All @@ -12,7 +12,7 @@ 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;
export const enumVersions = ['V6', 'V5', 'V4', 'V3', 'V2', 'V1'] as const;

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

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

export function v6ToLatestCompatible (_registry: Registry, v6: ContractMetadataV6): ContractMetadataV6 {
return v6;
}

export function v5ToLatestCompatible (_registry: Registry, v5: ContractMetadataV5): ContractMetadataV5 {
return v5;
}
Expand All @@ -38,6 +42,7 @@ export const v1ToLatestCompatible = /*#__PURE__*/ createConverter(v2ToLatestComp
export const v0ToLatestCompatible = /*#__PURE__*/ createConverter(v1ToLatestCompatible, v0ToV1);

export const convertVersions: [Versions, Converter][] = [
['V6', v6ToLatestCompatible],
['V5', v5ToLatestCompatible],
['V4', v4ToLatestCompatible],
['V3', v3ToLatestCompatible],
Expand Down
22 changes: 18 additions & 4 deletions packages/api-contract/src/base/Base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,11 @@

protected readonly _decorateMethod: DecorateMethod<ApiType>;
protected readonly _isWeightV1: boolean;
protected readonly _isRevive: boolean;

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

this.abi = abi instanceof Abi
Expand All @@ -32,6 +29,23 @@
this.api = api;
this._decorateMethod = decorateMethod;
this._isWeightV1 = !api.registry.createType<WeightV2>('Weight').proofSize;
this._isRevive = this.abi.isRevive;


Check failure on line 34 in packages/api-contract/src/base/Base.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

More than 1 blank line not allowed
if (this._isRevive) {
if (!api.tx.revive || !isFunction(api.tx.revive.instantiateWithCode) || api.tx.revive.instantiateWithCode.meta.args.length !== 6) {
throw new Error('The runtime does not expose api.tx.revive.instantiateWithCode with storageDepositLimit');
} else if (!api.call.reviveApi || !isFunction(api.call.reviveApi.call)) {
throw new Error('Your runtime does not expose the api.call.reviveApi.call runtime interfaces');
}
}

Check failure on line 41 in packages/api-contract/src/base/Base.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Closing curly brace does not appear on the same line as the subsequent block
else {
if (!api.tx.contracts || !isFunction(api.tx.contracts.instantiateWithCode) || api.tx.contracts.instantiateWithCode.meta.args.length !== 6) {
throw new Error('The runtime does not expose api.tx.contracts.instantiateWithCode with storageDepositLimit');
} else if (!api.call.contractsApi || !isFunction(api.call.contractsApi.call)) {
throw new Error('Your runtime does not expose the api.call.contractsApi.call runtime interfaces');
}
}
}

public get registry (): Registry {
Expand Down
25 changes: 21 additions & 4 deletions packages/api-contract/src/base/Blueprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@
}

#deploy = (constructorOrId: AbiConstructor | string | number, { gasLimit = BN_ZERO, salt, storageDepositLimit = null, value = BN_ZERO }: BlueprintOptions, params: unknown[]): SubmittableExtrinsic<ApiType, BlueprintSubmittableResult<ApiType>> => {
return this.api.tx.contracts.instantiate(
const palletTx = this._isRevive
? this.api.tx.revive
: this.api.tx.contracts;
return palletTx.instantiate(

Check failure on line 61 in packages/api-contract/src/base/Blueprint.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Expected blank line before this statement
value,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore jiggle v1 weights, metadata points to latest
Expand All @@ -67,9 +70,23 @@
this.abi.findConstructor(constructorOrId).toU8a(params),
encodeSalt(salt)
).withResultTransform((result: ISubmittableResult) =>
new BlueprintSubmittableResult(result, applyOnEvent(result, ['Instantiated'], ([record]: EventRecord[]) =>
new Contract<ApiType>(this.api, this.abi, record.event.data[1] as AccountId, this._decorateMethod)
))
new BlueprintSubmittableResult(result,

Check failure on line 73 in packages/api-contract/src/base/Blueprint.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Trailing spaces not allowed
this._isRevive

Check failure on line 74 in packages/api-contract/src/base/Blueprint.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Trailing spaces not allowed
? (
(result.status.isInBlock || result.status.isFinalized)

Check failure on line 76 in packages/api-contract/src/base/Blueprint.ts

View workflow job for this annotation

GitHub Actions / pr (lint)

Expected indentation of 12 spaces but found 14
? new Contract<ApiType>(
this.api,
this.abi,
// your fixed address for revive deployments
this.registry.createType('AccountId', '0x'),
this._decorateMethod
)
: undefined
)
: applyOnEvent(result, ['Instantiated'], ([record]: EventRecord[]) =>
new Contract<ApiType>(this.api, this.abi, record.event.data[1] as AccountId, this._decorateMethod)
)
)
);
};
}
Expand Down
13 changes: 12 additions & 1 deletion packages/api-contract/src/base/Code.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { toPromiseMethod } from '@polkadot/api';
import v0contractFlipper from '../test/contracts/ink/v0/flipper.contract.json' assert { type: 'json' };
import v0abiFlipper from '../test/contracts/ink/v0/flipper.json' assert { type: 'json' };
import v1contractFlipper from '../test/contracts/ink/v1/flipper.contract.json' assert { type: 'json' };
import v6contractErc20 from '../test/contracts/ink/v6/erc20.contract.json' assert { type: 'json' };
import { Code } from './Code.js';
import { mockApi } from './mock.js';
import { mockApi, mockReviveApi } from './mock.js';

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

Expand All @@ -33,4 +34,14 @@ describe('Code', (): void => {
() => new Code(mockApi, v1contractFlipper as Record<string, unknown>, null, toPromiseMethod)
).not.toThrow();
});

it('can construct a revive compatible contract (v6)', (): void => {
expect(
() => new Code(mockApi, v6contractErc20 as Record<string, unknown>, null, toPromiseMethod)
).toThrow('The runtime does not expose api.tx.revive.instantiateWithCode with storageDepositLimit');

expect(
() => new Code(mockReviveApi, v6contractErc20 as Record<string, unknown>, null, toPromiseMethod)
).not.toThrow();
});
});
25 changes: 23 additions & 2 deletions packages/api-contract/src/base/Code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,28 @@ export class Code<ApiType extends ApiTypes> extends Base<ApiType> {
}

#instantiate = (constructorOrId: AbiConstructor | string | number, { gasLimit = BN_ZERO, salt, storageDepositLimit = null, value = BN_ZERO }: BlueprintOptions, params: unknown[]): SubmittableExtrinsic<ApiType, CodeSubmittableResult<ApiType>> => {
return this.api.tx.contracts.instantiateWithCode(
const palletTx = this._isRevive ? this.api.tx.revive : this.api.tx.contracts;
if (this._isRevive) {
return palletTx.instantiateWithCode(
value,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore jiggle v1 weights, metadata points to latest
this._isWeightV1
? convertWeight(gasLimit).v1Weight
: convertWeight(gasLimit).v2Weight,
storageDepositLimit,
compactAddLength(this.code),
this.abi.findConstructor(constructorOrId).toU8a(params),
encodeSalt(salt)
).withResultTransform((result: ISubmittableResult) =>
new CodeSubmittableResult(
result,
new Blueprint<ApiType>(this.api, this.abi, this.abi.info.source.hash, this._decorateMethod),
new Contract<ApiType>(this.api, this.abi, "0x", this._decorateMethod)
)
);
}
return palletTx.instantiateWithCode(
value,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore jiggle v1 weights, metadata points to latest
Expand All @@ -79,7 +100,7 @@ export class Code<ApiType extends ApiTypes> extends Base<ApiType> {
compactAddLength(this.code),
this.abi.findConstructor(constructorOrId).toU8a(params),
encodeSalt(salt)
).withResultTransform((result: ISubmittableResult) =>
).withResultTransform((result: ISubmittableResult) =>
new CodeSubmittableResult(result, ...(applyOnEvent(result, ['CodeStored', 'Instantiated'], (records: EventRecord[]) =>
records.reduce<[Blueprint<ApiType> | undefined, Contract<ApiType> | undefined]>(([blueprint, contract], { event }) =>
this.api.events.contracts.Instantiated.is(event)
Expand Down
35 changes: 19 additions & 16 deletions packages/api-contract/src/base/Contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type { ApiBase } from '@polkadot/api/base';
import type { SubmittableExtrinsic } from '@polkadot/api/submittable/types';
import type { ApiTypes, DecorateMethod } from '@polkadot/api/types';
import type { AccountId, ContractExecResult, EventRecord, Weight, WeightV2 } from '@polkadot/types/interfaces';
import type { AccountId, AccountId20, ContractExecResult, EventRecord, Weight, WeightV2 } from '@polkadot/types/interfaces';
import type { ISubmittableResult } from '@polkadot/types/types';
import type { Abi } from '../Abi/index.js';
import type { AbiMessage, ContractCallOutcome, ContractOptions, DecodedEvent, WeightAll } from '../types.js';
Expand Down Expand Up @@ -52,15 +52,15 @@ export class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
/**
* @description The on-chain address for this contract
*/
readonly address: AccountId;
readonly address: AccountId | AccountId20;

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

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

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

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

#exec = (messageOrId: AbiMessage | string | number, { gasLimit = BN_ZERO, storageDepositLimit = null, value = BN_ZERO }: ContractOptions, params: unknown[]): SubmittableExtrinsic<ApiType> => {
return this.api.tx.contracts.call(
const palletTx = this._isRevive ? this.api.tx.revive : this.api.tx.contracts;
return palletTx.call(
this.address,
value,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
Expand Down Expand Up @@ -134,17 +135,19 @@ export class Contract<ApiType extends ApiTypes> extends Base<ApiType> {
return {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
send: this._decorateMethod((origin: string | AccountId | Uint8Array) =>
this.api.rx.call.contractsApi.call<ContractExecResult>(
origin,
this.address,
value,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore jiggle v1 weights, metadata points to latest
this._isWeightV1
? this.#getGas(gasLimit, true).v1Weight
: this.#getGas(gasLimit, true).v2Weight,
storageDepositLimit,
message.toU8a(params)
(this._isRevive
? this.api.rx.call.reviveApi.call
: this.api.rx.call.contractsApi.call)<ContractExecResult>(
origin,
this.address,
value,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore jiggle v1 weights, metadata points to latest
this._isWeightV1
? this.#getGas(gasLimit, true).v1Weight
: this.#getGas(gasLimit, true).v2Weight,
storageDepositLimit,
message.toU8a(params)
).pipe(
map(({ debugMessage, gasConsumed, gasRequired, result, storageDeposit }): ContractCallOutcome => ({
debugMessage,
Expand Down
17 changes: 17 additions & 0 deletions packages/api-contract/src/base/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,20 @@ export const mockApi = {
}
}
} as unknown as ApiBase<'promise'>;

export const mockReviveApi = {
call: {
reviveApi: {
call: (): never => {
throw new Error('mock');
}
}
},
isConnected: true,
registry,
tx: {
revive: {
instantiateWithCode
}
}
} as unknown as ApiBase<'promise'>;
4 changes: 2 additions & 2 deletions packages/api-contract/src/promise/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0

import type { ApiPromise } from '@polkadot/api';
import type { AccountId, Hash } from '@polkadot/types/interfaces';
import type { AccountId, AccountId20, Hash } from '@polkadot/types/interfaces';
import type { Abi } from '../Abi/index.js';

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

export class ContractPromise extends Contract<'promise'> {
constructor (api: ApiPromise, abi: string | Record<string, unknown> | Abi, address: string | AccountId) {
constructor (api: ApiPromise, abi: string | Record<string, unknown> | Abi, address: string | AccountId | AccountId20) {
super(api, abi, address, toPromiseMethod);
}
}
Loading