Skip to content

Commit da48a1f

Browse files
authored
refactor: retrieve contract address from Instantiated event (#589)
1 parent 406bde9 commit da48a1f

File tree

6 files changed

+294
-357
lines changed

6 files changed

+294
-357
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@
3333
"dependencies": {
3434
"@headlessui/react": "^1.7.18",
3535
"@heroicons/react": "^1.0.6",
36-
"@polkadot/api": "^16.4.1",
37-
"@polkadot/api-contract": "^16.4.1",
36+
"@polkadot/api": "^16.4.8",
37+
"@polkadot/api-contract": "^16.4.8",
3838
"@polkadot/extension-dapp": "^0.58.6",
39-
"@polkadot/types": "^16.4.1",
39+
"@polkadot/types": "^16.4.8",
4040
"@polkadot/ui-keyring": "^3.12.2",
4141
"@polkadot/ui-shared": "^3.12.2",
4242
"big.js": "^6.2.1",

src/lib/address.test.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,10 @@
44
import { describe, expect, it } from 'vitest';
55
import { getAddress } from 'ethers';
66
import { decodeAddress } from '@polkadot/keyring';
7-
import { create1, create2, toEthAddress } from './address';
7+
import { toEthAddress } from './address';
88

99
// Similar to pallet_revive tests: https://github.com/paritytech/polkadot-sdk/blob/65ade498b63bf2216d1c444f28c1b48085417f13/substrate/frame/revive/src/address.rs#L257
1010
describe('address utilities', () => {
11-
const deployer = '0x' + '01'.repeat(20);
12-
const code = Uint8Array.from([0x60, 0x00, 0x60, 0x00, 0x55, 0x60, 0x01, 0x60, 0x00]);
13-
const inputData = Uint8Array.from([0x55]);
14-
const salt = '0x1234567890123456789012345678901234567890123456789012345678901234';
15-
16-
it('should compute correct address with create1', () => {
17-
const address = create1(deployer, 1);
18-
expect(getAddress(address)).toBe(getAddress('0xc851da37e4e8d3a20d8d56be2963934b4ad71c3b'));
19-
});
20-
21-
it('should compute correct address with create2', () => {
22-
const address = create2(deployer, code, inputData, salt);
23-
expect(getAddress(address)).toBe(getAddress('0x7f31e795e5836a19a8f919ab5a9de9a197ecd2b6'));
24-
});
25-
2611
it('should convert Substrate account ID to Ethereum address', () => {
2712
const accountId = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
2813
const ethAddress = toEthAddress(decodeAddress(accountId));

src/lib/address.ts

Lines changed: 2 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,8 @@
11
// Copyright 2022-2024 use-ink/contracts-ui authors & contributors
22
// SPDX-License-Identifier: GPL-3.0-only
33

4-
import { BigNumberish, ethers } from 'ethers';
5-
import { hexToU8a, stringToU8a, u8aToHex } from '@polkadot/util';
6-
import { keccak256 } from 'ethers';
7-
8-
/**
9-
* TypeScript equivalent of H160 (20-byte Ethereum address)
10-
*/
11-
type Address = string;
12-
13-
/**
14-
* Determine the address of a contract using CREATE semantics.
15-
* @param deployer The address of the deployer
16-
* @param nonce The nonce value
17-
* @returns The contract address
18-
*/
19-
export function create1(deployer: string, nonce: number): Address {
20-
// Convert deployer to bytes (remove 0x prefix if present)
21-
const deployerBytes = ethers.hexlify(deployer);
22-
ethers.toBeHex(nonce as BigNumberish);
23-
// Convert nonce to hex (minimal encoding)
24-
const nonceBytes = ethers.toBeHex(nonce as BigNumberish);
25-
26-
// RLP encode [deployer, nonce]
27-
const encodedData = ethers.encodeRlp([deployerBytes, nonceBytes]);
28-
29-
// Calculate keccak256 hash of the RLP encoded data
30-
const hash = ethers.keccak256(encodedData);
31-
32-
// Take the last 20 bytes (40 hex chars + 0x prefix)
33-
return ethers.getAddress('0x' + hash.substring(26));
34-
}
35-
36-
/**
37-
* Determine the address of a contract using CREATE2 semantics.
38-
* @param deployer The address of the deployer
39-
* @param code The contract code (WASM or EVM bytecode)
40-
* @param inputData The constructor arguments or init input
41-
* @param salt A 32-byte salt value (as hex string)
42-
* @returns The deterministic contract address
43-
*/
44-
export function create2(
45-
deployer: string,
46-
code: Uint8Array,
47-
inputData: Uint8Array,
48-
salt: string,
49-
): Address {
50-
const initCode = new Uint8Array([...code, ...inputData]);
51-
const initCodeHash = hexToU8a(keccak256(initCode));
52-
53-
const parts = new Uint8Array(1 + 20 + 32 + 32); // 0xff + deployer + salt + initCodeHash
54-
parts[0] = 0xff;
55-
parts.set(hexToU8a(deployer), 1);
56-
parts.set(hexToU8a(salt), 21);
57-
parts.set(initCodeHash, 53);
58-
59-
const hash = keccak256(parts);
60-
61-
// Return last 20 bytes as 0x-prefixed hex string
62-
return ethers.getAddress('0x' + hash.substring(26));
63-
}
4+
import { ethers } from 'ethers';
5+
import { stringToU8a, u8aToHex } from '@polkadot/util';
646

657
/**
668
* Converts an account ID to an Ethereum address (H160)

src/ui/components/instantiate/Step3.tsx

Lines changed: 3 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,14 @@ import { Button, Buttons } from '../common/Button';
88
import { printBN } from 'lib/bn';
99
import { createInstantiateTx } from 'services/chain';
1010
import { SubmittableResult } from 'types';
11-
import { useApi, useInstantiate, useTransactions, useVersion } from 'ui/contexts';
11+
import { useApi, useInstantiate, useTransactions } from 'ui/contexts';
1212
import { useNewContract } from 'ui/hooks';
13-
import { transformUserInput } from 'lib/callOptions';
1413

1514
export function Step3() {
1615
const { codeHash: codeHashUrlParam } = useParams<{ codeHash: string }>();
1716
const { data, step, setStep } = useInstantiate();
1817
const { api } = useApi();
19-
const { version } = useVersion();
20-
const { accountId, value, metadata, gasLimit, name, constructorIndex, salt } = data;
18+
const { accountId, value, metadata, gasLimit, name, constructorIndex } = data;
2119
const { queue, process, txs, dismiss } = useTransactions();
2220
const [txId, setTxId] = useState<number>(0);
2321
const onSuccess = useNewContract();
@@ -35,27 +33,7 @@ export function Step3() {
3533
extrinsic: tx,
3634
accountId: data.accountId,
3735
onSuccess: result => {
38-
if (version !== 'v6') {
39-
return onSuccess(result);
40-
}
41-
const constructor = metadata?.findConstructor(constructorIndex);
42-
const transformed = transformUserInput(
43-
api.registry,
44-
constructor?.args || [],
45-
data.argValues,
46-
);
47-
const inputData = constructor?.toU8a(transformed).slice(1); // exclude the first byte (the length byte)
48-
// Pass the contract data and extrinsic to onSuccess
49-
// @ts-ignore
50-
return onSuccess({
51-
...result,
52-
contractData: {
53-
salt: salt?.toString() || '',
54-
data: inputData || new Uint8Array(),
55-
// @ts-ignore
56-
code: metadata?.json.source.contract_binary,
57-
},
58-
});
36+
return onSuccess(result);
5937
},
6038
isValid,
6139
});

src/ui/hooks/useNewContract.ts

Lines changed: 4 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -3,71 +3,23 @@
33

44
import { useNavigate } from 'react-router';
55
import type { BlueprintSubmittableResult } from 'types';
6-
import { useApi, useDatabase, useInstantiate, useVersion } from 'ui/contexts';
7-
import { ApiTypes } from '@polkadot/api/types';
8-
import { hexToU8a } from '@polkadot/util';
9-
import { decodeAddress } from '@polkadot/keyring';
10-
import { create1, create2, toEthAddress } from 'lib/address';
11-
12-
interface ExtendedBlueprintSubmittableResult<T extends ApiTypes>
13-
extends BlueprintSubmittableResult<T> {
14-
contractData?: {
15-
salt: string;
16-
data: Uint8Array;
17-
code: string;
18-
originIsCaller?: boolean;
19-
};
20-
}
6+
import { useDatabase, useInstantiate } from 'ui/contexts';
217

228
export function useNewContract() {
239
const { db } = useDatabase();
2410
const navigate = useNavigate();
2511
const instantiate = useInstantiate();
26-
const { api } = useApi();
27-
const { version } = useVersion();
2812

2913
const {
3014
data: { accountId, name },
3115
} = instantiate;
3216

33-
async function getNonce() {
34-
try {
35-
const nonce = await api.call.accountNonceApi.accountNonce(accountId);
36-
return nonce.toNumber();
37-
} catch (error) {
38-
console.error('Error fetching nonce:', error);
39-
return null;
40-
}
41-
}
42-
43-
return async function ({
44-
contract,
45-
contractData,
46-
}: ExtendedBlueprintSubmittableResult<'promise'>): Promise<void> {
17+
return async function ({ contract }: BlueprintSubmittableResult<'promise'>): Promise<void> {
4718
if (accountId && contract?.abi.json) {
48-
let calculatedAddress = contract.address.toString();
49-
// Calculate the expected contract address based on the Rust logic
50-
if (version === 'v6' && contractData) {
51-
const { salt, code, data, originIsCaller = false } = contractData;
52-
const mappedAccount = toEthAddress(decodeAddress(accountId));
53-
54-
if (salt) {
55-
// Use CREATE2 if salt is provided
56-
calculatedAddress = create2(mappedAccount, hexToU8a(code), data, salt);
57-
} else {
58-
// Use CREATE1 if no salt is provided
59-
const nonce = await getNonce();
60-
61-
if (nonce !== null) {
62-
const adjustedNonce = originIsCaller ? Math.max(0, nonce - 1) : nonce;
63-
calculatedAddress = create1(mappedAccount, adjustedNonce - 2);
64-
}
65-
}
66-
}
6719
const codeHash = contract.abi.info.source.wasmHash.toHex();
6820
const document = {
6921
abi: contract.abi.json,
70-
address: calculatedAddress!,
22+
address: contract.address.toString(),
7123
codeHash,
7224
date: new Date().toISOString(),
7325
name,
@@ -82,7 +34,7 @@ export function useNewContract() {
8234
}),
8335
]);
8436

85-
navigate(`/contract/${document.address}`);
37+
navigate(`/contract/${contract.address}`);
8638
}
8739
};
8840
}

0 commit comments

Comments
 (0)