Skip to content

Commit b49629e

Browse files
committed
feat: contract address calculation
1 parent 2a26924 commit b49629e

File tree

4 files changed

+302
-42
lines changed

4 files changed

+302
-42
lines changed

src/services/chain/contract.ts

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

4-
import { BlueprintPromise, CodePromise } from '@polkadot/api-contract';
4+
import { BlueprintPromise, CodePromise, ContractPromise } from '@polkadot/api-contract';
55
import { isValidAddress, isValidCodeHash, isNumber } from 'lib/util';
66
import { transformUserInput } from 'lib/callOptions';
77
import {
@@ -12,6 +12,9 @@ import {
1212
SubmittableExtrinsic,
1313
} from 'types';
1414
import { stringToU8a, compactAddLength, u8aToU8a } from '@polkadot/util';
15+
import { ISubmittableResult } from '@polkadot/types/types';
16+
import { BlueprintSubmittableResult, Contract } from '@polkadot/api-contract/base';
17+
import { contractsAbi } from '@polkadot/types/interfaces/definitions';
1518

1619
export function createInstantiateTx(
1720
api: ApiPromise,
@@ -40,26 +43,31 @@ export function createInstantiateTx(
4043
};
4144

4245
const parsed_wasm = compactAddLength(wasm.slice(0));
43-
// const codeOrBlueprint = codeHash
44-
// ? new BlueprintPromise(api, metadata, codeHash)
45-
// : new CodePromise(api, metadata, wasm && wasm);
46+
const codeOrBlueprint = codeHash
47+
? new BlueprintPromise(api, metadata, codeHash)
48+
: new CodePromise(api, metadata, wasm && wasm);
49+
// const transformed = transformUserInput(api.registry, constructor.args, argValues);
50+
4651
const transformed = transformUserInput(api.registry, constructor.args, argValues);
4752

48-
const data = constructor.toU8a(transformed);
53+
const tmp = constructor.toU8a(transformed);
54+
console.log('constructor.toU8a', tmp);
55+
console.log('constructor.', transformed);
56+
//
57+
// const tx = api.tx.revive.instantiateWithCode(
58+
// value!,
59+
// gasLimit!,
60+
// storageDepositLimit!,
61+
// parsed_wasm,
62+
// data,
63+
// salt,
64+
// );
4965

50-
const tx = api.tx.revive.instantiateWithCode(
51-
value!,
52-
gasLimit!,
53-
storageDepositLimit!,
54-
parsed_wasm,
55-
data,
56-
salt,
57-
);
66+
// return tx;
5867

59-
return tx;
60-
// return constructor.args.length > 0
61-
// ? tx[constructor.method](options, ...transformed)
62-
// : tx[constructor.method](options);
68+
return constructor.args.length > 0
69+
? codeOrBlueprint.tx[constructor.method](options, ...transformed)
70+
: codeOrBlueprint.tx[constructor.method](options);
6371
} else {
6472
throw new Error('Error creating instantiate tx');
6573
}

src/ui/components/instantiate/Step3.tsx

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@ import { createInstantiateTx } from 'services/chain';
1010
import { SubmittableResult } from 'types';
1111
import { useApi, useInstantiate, useTransactions } from 'ui/contexts';
1212
import { useNewContract } from 'ui/hooks';
13+
import { stringToU8a } from '@polkadot/util';
14+
import { transformUserInput } from 'lib/callOptions';
1315

1416
export function Step3() {
1517
const { codeHash: codeHashUrlParam } = useParams<{ codeHash: string }>();
1618
const { data, step, setStep } = useInstantiate();
1719
const { api } = useApi();
18-
const { accountId, value, metadata, gasLimit, name, constructorIndex } = data;
20+
const { accountId, value, metadata, gasLimit, name, constructorIndex, salt } = data;
21+
22+
// const transformed = transformUserInput(api.registry, data.constructor.args, argValues);
1923
const { queue, process, txs, dismiss } = useTransactions();
2024
const [txId, setTxId] = useState<number>(0);
2125
const onSuccess = useNewContract();
@@ -29,10 +33,24 @@ export function Step3() {
2933
const tx = createInstantiateTx(api, data);
3034

3135
if (!txId) {
36+
const constructor = metadata.findConstructor(constructorIndex);
37+
const transformed = transformUserInput(api.registry, constructor.args, data.argValues);
38+
const inputData = constructor.toU8a(transformed).slice(1); // exclude the first byte (the length byte)
39+
3240
const newId = queue({
3341
extrinsic: tx,
3442
accountId: data.accountId,
35-
onSuccess,
43+
onSuccess: result => {
44+
// Pass the contract data and extrinsic to onSuccess
45+
return onSuccess({
46+
...result,
47+
contractData: {
48+
salt: salt, // Using codeHash as salt for demonstration
49+
data: inputData, // The contract initialization data
50+
code: metadata?.json.source.contract_binary,
51+
},
52+
});
53+
},
3654
isValid,
3755
});
3856
setTxId(newId);

src/ui/hooks/useNewContract.ts

Lines changed: 238 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,258 @@
33

44
import { useNavigate } from 'react-router';
55
import type { BlueprintSubmittableResult } from 'types';
6-
import { useDatabase, useInstantiate } from 'ui/contexts';
6+
import { useApi, useDatabase, useInstantiate } from 'ui/contexts';
7+
import { ethers } from 'ethers';
8+
import { ApiTypes } from '@polkadot/api/types';
9+
import { stringToU8a } from '@polkadot/util';
10+
import { keccak256 } from 'ethers';
11+
import { decodeAddress, encodeAddress } from '@polkadot/keyring';
12+
13+
interface ExtendedBlueprintSubmittableResult<T extends ApiTypes>
14+
extends BlueprintSubmittableResult<T> {
15+
contractData?: {
16+
salt: Uint8Array;
17+
data: string;
18+
code: Uint8Array;
19+
originIsCaller?: boolean;
20+
};
21+
}
22+
23+
/**
24+
* TypeScript equivalent of H160 (20-byte Ethereum address)
25+
*/
26+
type Address = string;
27+
28+
/**
29+
* Determine the address of a contract using CREATE semantics.
30+
* @param deployer The address of the deployer
31+
* @param nonce The nonce value
32+
* @returns The contract address
33+
*/
34+
export function create1(deployer: Address, nonce: bigint | number): Address {
35+
// Normalize the deployer address
36+
const deployerAddress = ethers.getAddress(deployer);
37+
38+
// Create RLP encoding of the deployer address and nonce
39+
const encodedData = ethers.encodeRlp([deployerAddress, ethers.toBeHex(nonce)]);
40+
41+
// Calculate keccak256 hash of the RLP encoded data
42+
const hash = ethers.keccak256(encodedData);
43+
44+
// Take the last 20 bytes (40 hex chars + 0x prefix)
45+
return ethers.getAddress('0x' + hash.substring(26));
46+
}
47+
48+
// /**
49+
// * Determine the address of a contract using the CREATE2 semantics.
50+
// * @param deployer The address of the deployer
51+
// * @param code The initialization code
52+
// * @param inputData Additional input data
53+
// * @param salt A 32-byte salt value
54+
// * @returns The contract address
55+
// */
56+
// export function create2(
57+
// deployer: Address,
58+
// code: Uint8Array | string,
59+
// inputData: Uint8Array | string,
60+
// salt: string
61+
// ): Address {
62+
// console.log("info");
63+
// console.log(code);
64+
// console.log("data");
65+
// console.log(inputData);
66+
// console.log("salt");
67+
// console.log(salt);
68+
//
69+
// // Normalize inputs to Uint8Array
70+
// const codeBytes = typeof code === 'string'
71+
// ? stringToU8a(code)
72+
// : code;
73+
//
74+
// const inputDataBytes = typeof inputData === 'string'
75+
// ? stringToU8a(inputData)
76+
// : inputData;
77+
//
78+
// const normalizedSalt = stringToU8a(salt.substring(2));
79+
//
80+
// // Concatenate code and input data
81+
// const initCode = new Uint8Array(codeBytes.length + inputDataBytes.length);
82+
// initCode.set(codeBytes);
83+
// initCode.set(inputDataBytes, codeBytes.length);
84+
//
85+
// // Calculate init code hash
86+
// const initCodeHash = ethers.keccak256(initCode);
87+
//
88+
// // Build the input buffer: 0xff + deployer + salt + initCodeHash
89+
// const buffer = ethers.concat([
90+
// new Uint8Array([0xff]),
91+
// ethers.getBytes(deployer),
92+
// ethers.getBytes(normalizedSalt),
93+
// ethers.getBytes(initCodeHash)
94+
// ]);
95+
//
96+
// // Calculate keccak256 hash of the buffer
97+
// const hash = ethers.keccak256(buffer);
98+
//
99+
// // Take the last 20 bytes (40 hex chars + 0x prefix)
100+
// return ethers.getAddress("0x" + hash.substring(26));
101+
// }
102+
103+
// function create2(deployer: Address, code: Uint8Array, input_data: Uint8Array, salt: Uint8Array): Address {
104+
// const init_code = Uint8Array.from([...code, ...input_data]);
105+
// const init_code_hash = keccak256(init_code);
106+
// const bytes = new Uint8Array(85);
107+
// bytes[0] = 0xff;
108+
// bytes.set(stringToU8a(deployer.toString()), 1);
109+
// console.log(salt);
110+
// console.log(salt.length);
111+
// bytes.set(salt.slice(0, 32), 21);
112+
// console.log(init_code_hash.length);
113+
// bytes.set(stringToU8a(init_code_hash.substring(2)), 53);
114+
// const hash = stringToU8a(keccak256(bytes).substring(2));
115+
// const addressBytes = hash.slice(12);
116+
// // const address = new (addressBytes);
117+
// console.log("addressBytes");
118+
// console.log(addressBytes);
119+
// console.log(addressBytes.toString());
120+
// return addressBytes.toString();
121+
// }
122+
123+
export function create2(
124+
deployer: string,
125+
code: Uint8Array,
126+
inputData: Uint8Array,
127+
salt: Uint8Array,
128+
): string {
129+
// if (salt.length !== 32) {
130+
// throw new Error('Salt must be 32 bytes');
131+
// }
132+
133+
const initCode = new Uint8Array([...code, ...inputData]);
134+
const initCodeHash = stringToU8a(keccak256(initCode));
135+
136+
const parts = new Uint8Array(1 + (20 + 32 + 32) * 2); // 0xff + deployer + salt + initCodeHash
137+
parts[0] = 0xff;
138+
parts.set(stringToU8a(deployer.slice(2)), 1);
139+
console.log('HERE');
140+
console.log(initCodeHash);
141+
parts.set(salt.slice(2), 21);
142+
parts.set(initCodeHash.slice(2), 53);
143+
144+
const hash = keccak256(parts);
145+
146+
// Return last 20 bytes as 0x-prefixed hex string
147+
// return '0x' + Buffer.from(hash.slice(12, 42)).toString('hex');
148+
console.log(hash);
149+
console.log(hash.toString());
150+
return ethers.getAddress('0x' + hash.substring(26));
151+
}
152+
153+
/**
154+
* Converts an account ID to an Ethereum address (H160)
155+
* @param accountId The account ID bytes
156+
* @returns The Ethereum address
157+
*/
158+
export function toEthAddress(accountId: Uint8Array | string): string {
159+
// Convert string input to Uint8Array if needed
160+
const accountBytes = typeof accountId === 'string' ? stringToU8a(accountId) : accountId;
161+
162+
// Create a 32-byte buffer and copy account bytes into it
163+
const accountBuffer = new Uint8Array(32);
164+
accountBuffer.set(accountBytes.slice(0, 32));
165+
166+
if (isEthDerived(accountBytes)) {
167+
// This was originally an eth address
168+
// We just strip the 0xEE suffix to get the original address
169+
return '0x' + Buffer.from(accountBuffer.slice(0, 20)).toString('hex');
170+
} else {
171+
// This is an (ed|sr)25519 derived address
172+
// Avoid truncating the public key by hashing it first
173+
const accountHash = ethers.keccak256(accountBuffer);
174+
return ethers.getAddress('0x' + accountHash.slice(2 + 24, 2 + 24 + 40)); // Skip '0x' prefix, then skip 12 bytes, take 20 bytes
175+
}
176+
}
177+
178+
/**
179+
* Determines if an account ID is derived from an Ethereum address
180+
* @param accountId The account ID bytes
181+
* @returns True if the account is derived from an Ethereum address
182+
*/
183+
function isEthDerived(accountId: Uint8Array): boolean {
184+
if (accountId.length >= 32) {
185+
return accountId[20] === 0xee && accountId[21] === 0xee;
186+
}
187+
return false;
188+
}
7189

8190
export function useNewContract() {
9191
const { db } = useDatabase();
10192
const navigate = useNavigate();
193+
const instantiate = useInstantiate();
194+
const { api } = useApi();
195+
196+
console.log('Instantiate', instantiate);
197+
11198
const {
12199
data: { accountId, name },
13-
} = useInstantiate();
200+
} = instantiate;
201+
202+
async function getNonce() {
203+
try {
204+
const nonce = await api.call.accountNonceApi.accountNonce(accountId);
205+
return nonce.toNumber();
206+
} catch (error) {
207+
console.error('Error fetching nonce:', error);
208+
return null;
209+
}
210+
}
211+
212+
return async function ({
213+
contract,
214+
contractData,
215+
}: ExtendedBlueprintSubmittableResult<'promise'>): Promise<void> {
216+
console.log('Processing contract submission');
217+
console.log(contract);
218+
console.log(contractData);
219+
console.log(contractData?.code.toString());
14220

15-
return async function ({ contract }: BlueprintSubmittableResult<'promise'>): Promise<void> {
16221
if (accountId && contract?.abi.json) {
222+
// Calculate the expected contract address based on the Rust logic
223+
let calculatedAddress;
224+
225+
if (contractData) {
226+
const { salt, code, data, originIsCaller = false } = contractData;
227+
const mappedAccount = toEthAddress(decodeAddress(accountId));
228+
console.log('Mapped account address:', mappedAccount);
229+
console.log(mappedAccount);
230+
231+
if (salt) {
232+
// Use CREATE2 if salt is provided
233+
calculatedAddress = create2(mappedAccount, code, data, salt);
234+
console.log('CREATE2 calculated address:', calculatedAddress);
235+
} else {
236+
// Use CREATE1 if no salt is provided
237+
const nonce = await getNonce();
238+
239+
if (nonce !== null) {
240+
const adjustedNonce = originIsCaller ? Math.max(0, nonce - 1) : nonce;
241+
calculatedAddress = create1(mappedAccount, BigInt(adjustedNonce));
242+
console.log('CREATE1 calculated address with nonce:', adjustedNonce);
243+
console.log('Calculated address:', calculatedAddress);
244+
}
245+
}
246+
}
247+
17248
const codeHash = contract.abi.info.source.wasmHash.toHex();
249+
18250
const document = {
19251
abi: contract.abi.json,
20-
address: contract.address.toString(),
252+
address: calculatedAddress,
21253
codeHash,
22254
date: new Date().toISOString(),
23255
name,
256+
// Store the calculated address
257+
calculatedAddress: calculatedAddress || undefined,
24258
};
25259

26260
await Promise.all([

0 commit comments

Comments
 (0)