Skip to content

feat: include support for ink v6 contracts #571

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

Open
wants to merge 28 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8e7f600
feat(instantiate): spike, instantiate on revive. Doesn't import deplo…
peterwht Apr 9, 2025
2a26924
wip: add contract and calling contracts works. Modifies @polkadot/api…
peterwht Apr 9, 2025
b49629e
feat: contract address calculation
peterwht Apr 15, 2025
426b6fd
feat: create1 address calc works and autorecognize contract
peterwht Apr 16, 2025
0a210bf
fix: build errors and add warning banner
peterwht Apr 16, 2025
a82cf94
build: script to install forked polkadot-js
peterwht Apr 17, 2025
93857f7
build: add prebuilt api-contract
peterwht Apr 17, 2025
ea11eec
build: update yarn.lock
peterwht Apr 17, 2025
89573a8
westend asset hub only
peterwht Apr 21, 2025
34b1986
chore: include pop network
AlexD10S May 23, 2025
f4d47d9
fix: update yarn
AlexD10S May 22, 2025
7f96476
feat: version context
AlexD10S May 22, 2025
0cff94e
chore: api-revive and api-contract in the same repo
AlexD10S May 22, 2025
66473ed
chore: ink-node or substrate-contracts-node depending on version
AlexD10S May 22, 2025
8ae5daa
chore: update api-contracts
AlexD10S May 26, 2025
1224753
chore: display endpoints depending on the version
AlexD10S May 26, 2025
72fdb63
chore: update api-contract
AlexD10S May 27, 2025
7a5b5c9
chore: getVersion
AlexD10S May 27, 2025
e69736f
feat: isValidAddress for inkv6
AlexD10S May 28, 2025
8196feb
chore: remove polkadot-js packages
AlexD10S Jun 1, 2025
5a71edf
refactor: remove dotAddress
AlexD10S Jun 1, 2025
24c0b77
chore: alpha version only for v6
AlexD10S Jun 1, 2025
7e171db
chore: interact and steps
AlexD10S Jun 1, 2025
208340f
refactor: useNewContract
AlexD10S Jun 2, 2025
dce7d7b
test: address tests
AlexD10S Jun 2, 2025
f1f0fa9
fix: checkOnChainCode for v6
AlexD10S Jun 2, 2025
980abef
fix: calculate account when salt is provided
AlexD10S Jun 2, 2025
0cb7d75
refactor: clean up
AlexD10S Jun 2, 2025
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
768 changes: 0 additions & 768 deletions .yarn/releases/yarn-3.1.1.cjs

This file was deleted.

934 changes: 934 additions & 0 deletions .yarn/releases/yarn-4.6.0.cjs

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,2 +1,7 @@
compressionLevel: mixed

enableGlobalCache: false

nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-3.1.1.cjs

yarnPath: .yarn/releases/yarn-4.6.0.cjs
11 changes: 7 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@
"dependencies": {
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^1.0.6",
"@polkadot/api": "15.8.1",
"@polkadot/api-contract": "15.8.1",
"@polkadot/api": "file:../polkadot-js-api/packages/api/build",
"@polkadot/api-contract": "file:../polkadot-js-api/packages/api-contract/build",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To update once this polkadot-js/api#6158 gets merged and released

"@polkadot/extension-dapp": "^0.58.6",
"@polkadot/types": "file:../polkadot-js-api/packages/types/build",
"@polkadot/ui-keyring": "^3.12.2",
"@polkadot/ui-shared": "^3.12.2",
"big.js": "^6.2.1",
Expand All @@ -44,6 +45,7 @@
"date-fns": "^2.30.0",
"dexie": "^3.2.4",
"dexie-react-hooks": "1.1.7",
"ethers": "^6.13.5",
"json5": "^2.2.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -66,7 +68,8 @@
"@types/bcryptjs": "^2.4.6",
"@types/big.js": "^6.2.2",
"@types/node": "^22.5.0",
"@types/react-dom": "^18.3.0",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@typescript-eslint/eslint-plugin": "^8.2.0",
"@typescript-eslint/parser": "^8.2.0",
"@vitejs/plugin-react": "^4.3.1",
Expand Down Expand Up @@ -109,5 +112,5 @@
"minimist": "npm:minimist@^1.2.6",
"node-forge": "npm:node-forge@^1.3.0"
},
"packageManager": "yarn@3.1.1"
"packageManager": "yarn@4.6.0"
}
33 changes: 15 additions & 18 deletions src/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,36 +8,30 @@ export const LOCAL_STORAGE_KEY = {
CUSTOM_ENDPOINT: 'contractsUiCustomEndpoint',
PREFERRED_ENDPOINT: 'contractsUiPreferredEndpoint',
THEME: 'theme',
VERSION: 'inkVersion',
} as const;

export type LocalStorageKey = (typeof LOCAL_STORAGE_KEY)[keyof typeof LOCAL_STORAGE_KEY];

export const ROCOCO_CONTRACTS = {
relay: 'Rococo',
name: 'Contracts (Rococo)',
rpc: 'wss://rococo-contracts-rpc.polkadot.io',
};

const CUSTOM_ENDPOINT = localStorage.getItem(LOCAL_STORAGE_KEY.CUSTOM_ENDPOINT);
export const LOCAL = {
relay: undefined,
name: 'Local Node',
rpc: CUSTOM_ENDPOINT ? (JSON.parse(CUSTOM_ENDPOINT) as string) : 'ws://127.0.0.1:9944',
};

// https://docs.peaq.network/networks-overview
// const PEAQ_AGUNG = {
// relay: 'Rococo',
// name: 'Peaq Agung',
// rpc: 'wss://wss.agung.peaq.network',
// };

const POP_NETWORK_TESTNET = {
export const POP_NETWORK_TESTNET = {
relay: 'Paseo',
name: 'Pop Network Testnet',
rpc: 'wss://rpc2.paseo.popnetwork.xyz',
};

export const ASSET_HUB_WESTEND = {
relay: 'Westend',
name: 'Westend Asset Hub',
rpc: 'wss://westend-asset-hub-rpc.polkadot.io',
};

const PHALA_TESTNET = {
relay: undefined,
name: 'Phala PoC-6',
Expand Down Expand Up @@ -102,10 +96,13 @@ const ZEITGEIST_BATTERY_STATION = {
rpc: 'wss://bsr.zeitgeist.pm',
};

export const TESTNETS = [
export const TESTNETS_V6 = [
...[ASSET_HUB_WESTEND, POP_NETWORK_TESTNET].sort((a, b) => a.name.localeCompare(b.name)),
LOCAL,
];

export const TESTNETS_V5 = [
...[
ROCOCO_CONTRACTS,
// PEAQ_AGUNG,
PHALA_TESTNET,
ASTAR_SHIBUYA,
ALEPH_ZERO_TESTNET,
Expand All @@ -118,7 +115,7 @@ export const TESTNETS = [
LOCAL,
];

export const MAINNETS = [ASTAR, SHIDEN, ALEPH_ZERO].sort((a, b) => a.name.localeCompare(b.name));
export const MAINNETS_V5 = [ASTAR, SHIDEN, ALEPH_ZERO].sort((a, b) => a.name.localeCompare(b.name));

export const DEFAULT_DECIMALS = 12;

Expand Down
33 changes: 33 additions & 0 deletions src/lib/address.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright 2022-2024 use-ink/contracts-ui authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { describe, expect, it } from 'vitest';
import { getAddress } from 'ethers';
import { decodeAddress } from '@polkadot/keyring';
import { create1, create2, toEthAddress } from './address';

// Similar to pallet_revive tests: https://github.com/paritytech/polkadot-sdk/blob/65ade498b63bf2216d1c444f28c1b48085417f13/substrate/frame/revive/src/address.rs#L257
describe('address utilities', () => {
const deployer = '0x' + '01'.repeat(20);
const code = Uint8Array.from([0x60, 0x00, 0x60, 0x00, 0x55, 0x60, 0x01, 0x60, 0x00]);
const inputData = Uint8Array.from([0x55]);
const salt = '0x1234567890123456789012345678901234567890123456789012345678901234';

it('should compute correct address with create1', () => {
const address = create1(deployer, 1);
expect(getAddress(address)).toBe(getAddress('0xc851da37e4e8d3a20d8d56be2963934b4ad71c3b'));
});

it('should compute correct address with create2', () => {
const address = create2(deployer, code, inputData, salt);
expect(getAddress(address)).toBe(getAddress('0x7f31e795e5836a19a8f919ab5a9de9a197ecd2b6'));
});

it('should convert Substrate account ID to Ethereum address', () => {
const accountId = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const ethAddress = toEthAddress(decodeAddress(accountId));

expect(ethAddress.startsWith('0x')).toBe(true);
expect(getAddress(ethAddress)).toBe(getAddress('0x9621dde636de098b43efb0fa9b61facfe328f99d'));
});
});
121 changes: 121 additions & 0 deletions src/lib/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright 2022-2024 use-ink/contracts-ui authors & contributors
// SPDX-License-Identifier: GPL-3.0-only

import { BigNumberish, ethers } from 'ethers';
import { hexToU8a, stringToU8a, u8aToHex } from '@polkadot/util';
import { keccak256 } from 'ethers';

/**
* TypeScript equivalent of H160 (20-byte Ethereum address)
*/
type Address = string;

/**
* Determine the address of a contract using CREATE semantics.
* @param deployer The address of the deployer
* @param nonce The nonce value
* @returns The contract address
*/
export function create1(deployer: string, nonce: number): Address {
// Convert deployer to bytes (remove 0x prefix if present)
const deployerBytes = ethers.hexlify(deployer);
ethers.toBeHex(nonce as BigNumberish);
// Convert nonce to hex (minimal encoding)
const nonceBytes = ethers.toBeHex(nonce as BigNumberish);

// RLP encode [deployer, nonce]
const encodedData = ethers.encodeRlp([deployerBytes, nonceBytes]);

// Calculate keccak256 hash of the RLP encoded data
const hash = ethers.keccak256(encodedData);

// Take the last 20 bytes (40 hex chars + 0x prefix)
return ethers.getAddress('0x' + hash.substring(26));
}

/**
* Determine the address of a contract using CREATE2 semantics.
* @param deployer The address of the deployer
* @param code The contract code (WASM or EVM bytecode)
* @param inputData The constructor arguments or init input
* @param salt A 32-byte salt value (as hex string)
* @returns The deterministic contract address
*/
export function create2(
deployer: string,
code: Uint8Array,
inputData: Uint8Array,
salt: string,
): Address {
const initCode = new Uint8Array([...code, ...inputData]);
const initCodeHash = hexToU8a(keccak256(initCode));

const parts = new Uint8Array(1 + 20 + 32 + 32); // 0xff + deployer + salt + initCodeHash
parts[0] = 0xff;
parts.set(hexToU8a(deployer), 1);
parts.set(hexToU8a(salt), 21);
parts.set(initCodeHash, 53);

const hash = keccak256(parts);

// Return last 20 bytes as 0x-prefixed hex string
return ethers.getAddress('0x' + hash.substring(26));
}

/**
* Converts an account ID to an Ethereum address (H160)
* @param accountId The account ID bytes
* @returns The Ethereum address
*/
export function toEthAddress(accountId: Uint8Array | string): string {
// Convert string input to Uint8Array if needed
const accountBytes = typeof accountId === 'string' ? stringToU8a(accountId) : accountId;

// Create a 32-byte buffer and copy account bytes into it
const accountBuffer = new Uint8Array(32);
accountBuffer.set(accountBytes.slice(0, 32));

if (isEthDerived(accountBytes)) {
// This was originally an eth address
// We just strip the 0xEE suffix to get the original address
return '0x' + Buffer.from(accountBuffer.slice(0, 20)).toString('hex');
} else {
// This is an (ed|sr)25519 derived address
// Avoid truncating the public key by hashing it first
const accountHash = ethers.keccak256(accountBuffer);
return ethers.getAddress('0x' + accountHash.slice(2 + 24, 2 + 24 + 40)); // Skip '0x' prefix, then skip 12 bytes, take 20 bytes
}
}

export function fromEthAddress(ethAddress: string): string {
// Remove '0x' prefix if it exists
const cleanAddress = ethAddress.startsWith('0x') ? ethAddress.slice(2) : ethAddress;

// Convert the hex string to bytes
const addressBytes = Buffer.from(cleanAddress, 'hex');

// Check if the address is the expected length (20 bytes)
if (addressBytes.length !== 20) {
throw new Error('Invalid Ethereum address: must be 20 bytes');
}

// Create a 32-byte buffer
const result = new Uint8Array(32).fill(0xee);

// Set the first 20 bytes to the Ethereum address
result.set(addressBytes, 0);

return u8aToHex(result);
}

/**
* Determines if an account ID is derived from an Ethereum address
* @param accountId The account ID bytes
* @returns True if the account is derived from an Ethereum address
*/
export function isEthDerived(accountId: Uint8Array): boolean {
if (accountId.length >= 32) {
return accountId[20] === 0xee && accountId[21] === 0xee;
}
return false;
}
4 changes: 3 additions & 1 deletion src/lib/getContractFromPatron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-3.0-only

import { Buffer } from 'buffer';
import { getVersion } from 'ui/contexts';

function getFromPatron(field: string, hash: string) {
const options = {
Expand Down Expand Up @@ -29,7 +30,8 @@ function getFromPatron(field: string, hash: string) {

export function getContractFromPatron(codeHash: string): Promise<File> {
const metadataPromise = getFromPatron('metadata', codeHash);
const wasmPromise = getFromPatron('wasm', codeHash);
const field = getVersion() === 'v6' ? 'contract_binary' : 'wasm';
const wasmPromise = getFromPatron(field, codeHash);
return Promise.all([metadataPromise, wasmPromise]).then(([metadataResponse, wasmResponse]) => {
const result = Buffer.from(wasmResponse as ArrayBuffer).toString('hex');

Expand Down
15 changes: 13 additions & 2 deletions src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
// SPDX-License-Identifier: GPL-3.0-only

import { decodeAddress, encodeAddress } from '@polkadot/keyring';
import { hexToU8a, isHex } from '@polkadot/util';
import { hexToU8a, u8aToHex, isHex } from '@polkadot/util';
import { keyring } from '@polkadot/ui-keyring';
import format from 'date-fns/format';
import parseISO from 'date-fns/parseISO';
import { twMerge } from 'tailwind-merge';
import { isAddress as isEthAddress } from 'ethers';
import { InkVersion } from 'ui/contexts';

export const classes = twMerge;

Expand Down Expand Up @@ -72,7 +74,16 @@ export function isUndefined(value: unknown): value is undefined {
return value === undefined;
}

export function isValidAddress(address: string | Uint8Array | null | undefined) {
export function isValidAddress(
address: string | Uint8Array | null | undefined,
version: InkVersion,
) {
if (!address) return false;
if (version === 'v6') {
const hex = typeof address === 'string' ? address : u8aToHex(address);
return isEthAddress(hex);
}
// Check for v5 address format
try {
encodeAddress(isHex(address) ? hexToU8a(address) : decodeAddress(address));
return true;
Expand Down
35 changes: 26 additions & 9 deletions src/services/chain/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
InstantiateData,
SubmittableExtrinsic,
} from 'types';
import { InkVersion } from 'ui/contexts';

export function createInstantiateTx(
api: ApiPromise,
Expand Down Expand Up @@ -52,22 +53,38 @@ export function createInstantiateTx(
}
}

export async function getContractInfo(api: ApiPromise, address: string) {
if (isValidAddress(address)) {
return (await api.query.contracts.contractInfoOf(address)).unwrapOr(null);
export async function getContractInfo(api: ApiPromise, address: string, version: InkVersion) {
if (isValidAddress(address, version)) {
if (version === 'v6') {
return (await api.query.revive.contractInfoOf(address)).unwrapOr(null);
} else {
return (await api.query.contracts.contractInfoOf(address)).unwrapOr(null);
}
}
}

export async function checkOnChainCode(api: ApiPromise, codeHash: string): Promise<boolean> {
return isValidCodeHash(codeHash)
? (await api.query.contracts.pristineCode(codeHash)).isSome
: false;
export async function checkOnChainCode(
api: ApiPromise,
codeHash: string,
version: InkVersion,
): Promise<boolean> {
if (!isValidCodeHash(codeHash)) return false;

if (version === 'v6') {
return (await api.query.revive.pristineCode(codeHash)).isSome;
} else {
return (await api.query.contracts.pristineCode(codeHash)).isSome;
}
}

export async function filterOnChainCode(api: ApiPromise, items: CodeBundleDocument[]) {
export async function filterOnChainCode(
api: ApiPromise,
items: CodeBundleDocument[],
version: InkVersion,
) {
const codes: CodeBundleDocument[] = [];
for (const item of items) {
const isOnChain = await checkOnChainCode(api, item.codeHash);
const isOnChain = await checkOnChainCode(api, item.codeHash, version);
isOnChain && codes.push(item);
}
return codes;
Expand Down
Loading
Loading