Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@
"dependencies": {
"@headlessui/react": "^1.7.18",
"@heroicons/react": "^1.0.6",
"@polkadot/api": "^16.4.1",
"@polkadot/api-contract": "^16.4.1",
"@polkadot/api": "^16.4.8",
"@polkadot/api-contract": "^16.4.8",
"@polkadot/extension-dapp": "^0.58.6",
"@polkadot/types": "^16.4.1",
"@polkadot/types": "^16.4.8",
"@polkadot/ui-keyring": "^3.12.2",
"@polkadot/ui-shared": "^3.12.2",
"big.js": "^6.2.1",
Expand Down
17 changes: 1 addition & 16 deletions src/lib/address.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,10 @@
import { describe, expect, it } from 'vitest';
import { getAddress } from 'ethers';
import { decodeAddress } from '@polkadot/keyring';
import { create1, create2, toEthAddress } from './address';
import { 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));
Expand Down
62 changes: 2 additions & 60 deletions src/lib/address.ts
Original file line number Diff line number Diff line change
@@ -1,66 +1,8 @@
// 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 {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Removed these functions as there is no need to keep them anymore.

// 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));
}
import { ethers } from 'ethers';
import { stringToU8a, u8aToHex } from '@polkadot/util';

/**
* Converts an account ID to an Ethereum address (H160)
Expand Down
28 changes: 3 additions & 25 deletions src/ui/components/instantiate/Step3.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@ import { Button, Buttons } from '../common/Button';
import { printBN } from 'lib/bn';
import { createInstantiateTx } from 'services/chain';
import { SubmittableResult } from 'types';
import { useApi, useInstantiate, useTransactions, useVersion } from 'ui/contexts';
import { useApi, useInstantiate, useTransactions } from 'ui/contexts';
import { useNewContract } from 'ui/hooks';
import { transformUserInput } from 'lib/callOptions';

export function Step3() {
const { codeHash: codeHashUrlParam } = useParams<{ codeHash: string }>();
const { data, step, setStep } = useInstantiate();
const { api } = useApi();
const { version } = useVersion();
const { accountId, value, metadata, gasLimit, name, constructorIndex, salt } = data;
const { accountId, value, metadata, gasLimit, name, constructorIndex } = data;
const { queue, process, txs, dismiss } = useTransactions();
const [txId, setTxId] = useState<number>(0);
const onSuccess = useNewContract();
Expand All @@ -35,27 +33,7 @@ export function Step3() {
extrinsic: tx,
accountId: data.accountId,
onSuccess: result => {
if (version !== 'v6') {
return onSuccess(result);
}
const constructor = metadata?.findConstructor(constructorIndex);
const transformed = transformUserInput(
api.registry,
constructor?.args || [],
data.argValues,
);
const inputData = constructor?.toU8a(transformed).slice(1); // exclude the first byte (the length byte)
// Pass the contract data and extrinsic to onSuccess
// @ts-ignore
return onSuccess({
...result,
contractData: {
salt: salt?.toString() || '',
data: inputData || new Uint8Array(),
// @ts-ignore
code: metadata?.json.source.contract_binary,
},
});
return onSuccess(result);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Same approach now for ink! v5 and ink! v6: get the address from the instantiated event

},
isValid,
});
Expand Down
56 changes: 4 additions & 52 deletions src/ui/hooks/useNewContract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,71 +3,23 @@

import { useNavigate } from 'react-router';
import type { BlueprintSubmittableResult } from 'types';
import { useApi, useDatabase, useInstantiate, useVersion } from 'ui/contexts';
import { ApiTypes } from '@polkadot/api/types';
import { hexToU8a } from '@polkadot/util';
import { decodeAddress } from '@polkadot/keyring';
import { create1, create2, toEthAddress } from 'lib/address';

interface ExtendedBlueprintSubmittableResult<T extends ApiTypes>
extends BlueprintSubmittableResult<T> {
contractData?: {
salt: string;
data: Uint8Array;
code: string;
originIsCaller?: boolean;
};
}
import { useDatabase, useInstantiate } from 'ui/contexts';

export function useNewContract() {
const { db } = useDatabase();
const navigate = useNavigate();
const instantiate = useInstantiate();
const { api } = useApi();
const { version } = useVersion();

const {
data: { accountId, name },
} = instantiate;

async function getNonce() {
try {
const nonce = await api.call.accountNonceApi.accountNonce(accountId);
return nonce.toNumber();
} catch (error) {
console.error('Error fetching nonce:', error);
return null;
}
}

return async function ({
contract,
contractData,
}: ExtendedBlueprintSubmittableResult<'promise'>): Promise<void> {
return async function ({ contract }: BlueprintSubmittableResult<'promise'>): Promise<void> {
if (accountId && contract?.abi.json) {
let calculatedAddress = contract.address.toString();
// Calculate the expected contract address based on the Rust logic
if (version === 'v6' && contractData) {
const { salt, code, data, originIsCaller = false } = contractData;
const mappedAccount = toEthAddress(decodeAddress(accountId));

if (salt) {
// Use CREATE2 if salt is provided
calculatedAddress = create2(mappedAccount, hexToU8a(code), data, salt);
} else {
// Use CREATE1 if no salt is provided
const nonce = await getNonce();

if (nonce !== null) {
const adjustedNonce = originIsCaller ? Math.max(0, nonce - 1) : nonce;
calculatedAddress = create1(mappedAccount, adjustedNonce - 2);
}
}
}
const codeHash = contract.abi.info.source.wasmHash.toHex();
const document = {
abi: contract.abi.json,
address: calculatedAddress!,
address: contract.address.toString(),
codeHash,
date: new Date().toISOString(),
name,
Expand All @@ -82,7 +34,7 @@ export function useNewContract() {
}),
]);

navigate(`/contract/${document.address}`);
navigate(`/contract/${contract.address}`);
}
};
}
Loading