diff --git a/packages/web3-eth-contract/package.json b/packages/web3-eth-contract/package.json index 74edee8f90b..81a3a9f2a3a 100644 --- a/packages/web3-eth-contract/package.json +++ b/packages/web3-eth-contract/package.json @@ -45,6 +45,7 @@ "test:e2e:firefox": "npx cypress run --headless --browser firefox --env grep='ignore',invert=true" }, "dependencies": { + "@ethereumjs/rlp": "^5.0.2", "web3-core": "^4.4.0", "web3-errors": "^1.2.0", "web3-eth": "^4.7.0", diff --git a/packages/web3-eth-contract/src/utils.ts b/packages/web3-eth-contract/src/utils.ts index d2afe72a7b2..748203ffdf7 100644 --- a/packages/web3-eth-contract/src/utils.ts +++ b/packages/web3-eth-contract/src/utils.ts @@ -15,7 +15,8 @@ You should have received a copy of the GNU Lesser General Public License along with web3.js. If not, see . */ -import { Web3ContractError } from 'web3-errors'; +import { RLP } from '@ethereumjs/rlp'; +import { InvalidAddressError, InvalidMethodParamsError, InvalidNumberError, Web3ContractError } from 'web3-errors'; import { TransactionForAccessList, AbiFunctionFragment, @@ -26,8 +27,10 @@ import { NonPayableCallOptions, PayableCallOptions, ContractOptions, + Numbers, } from 'web3-types'; -import { isNullish, mergeDeep, isContractInitOptions } from 'web3-utils'; +import { isNullish, mergeDeep, isContractInitOptions, keccak256, toChecksumAddress, hexToNumber } from 'web3-utils'; +import { isAddress, isHexString } from 'web3-validator'; import { encodeMethodABI } from './encoding.js'; import { Web3ContractContext } from './types.js'; @@ -210,3 +213,42 @@ export const getCreateAccessListParams = ({ return txParams; }; + + +export const createContractAddress = (from: Address, nonce: Numbers): Address => { + if(!isAddress(from)) + throw new InvalidAddressError(`Invalid address given ${from}`); + + let nonceValue = nonce; + if(typeof nonce === "string" && isHexString(nonce)) + nonceValue = hexToNumber(nonce); + else if(typeof nonce === "string" && !isHexString(nonce)) + throw new InvalidNumberError("Invalid nonce value format"); + + const rlpEncoded = RLP.encode( + [from, nonceValue] + ); + const result = keccak256(rlpEncoded); + + const contractAddress = '0x'.concat(result.substring(26)); + + return toChecksumAddress(contractAddress); +} + +export const create2ContractAddress = (from: Address, salt: HexString, initCode: HexString): Address => { + if(!isAddress(from)) + throw new InvalidAddressError(`Invalid address given ${from}`); + + if(!isHexString(salt)) + throw new InvalidMethodParamsError(`Invalid salt value ${salt}`); + + if(!isHexString(initCode)) + throw new InvalidMethodParamsError(`Invalid initCode value ${initCode}`); + + const initCodeHash = keccak256(initCode); + const initCodeHashPadded = initCodeHash.padStart(64, '0'); // Pad to 32 bytes (64 hex characters) + const create2Params = ['0xff', from, salt, initCodeHashPadded].map(x => x.replace(/0x/, '')); + const create2Address = `0x${ create2Params.join('')}`; + + return toChecksumAddress(`0x${ keccak256(create2Address).slice(26)}`); // Slice to get the last 20 bytes (40 hex characters) & checksum + } \ No newline at end of file diff --git a/packages/web3-eth-contract/test/fixtures/create.ts b/packages/web3-eth-contract/test/fixtures/create.ts new file mode 100644 index 00000000000..0297828e5f1 --- /dev/null +++ b/packages/web3-eth-contract/test/fixtures/create.ts @@ -0,0 +1,134 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Numbers } from "web3-types"; + +export interface CreateTestData { + address: string; + input: { + from: string; + nonce: Numbers; + }; + } + +export const testData: CreateTestData[] = [ + { + address: '0x0C1B54fb6fdf63DEe15e65CAdBA8F2e028E26Bd0', + + input: { + from: '0xe2597eb05cf9a87eb1309e86750c903ec38e527e', + nonce: 0, + } + }, + { + address: '0x0C1B54fb6fdf63DEe15e65CAdBA8F2e028E26Bd0', + + input: { + from: '0xe2597eb05cf9a87eb1309e86750c903ec38e527e', + nonce: BigInt(0), + } + }, + { + address: '0x0C1B54fb6fdf63DEe15e65CAdBA8F2e028E26Bd0', + + input: { + from: '0xe2597eb05cf9a87eb1309e86750c903ec38e527e', + nonce: "0x0", + } + }, + { + address: '0x3474627D4F63A678266BC17171D87f8570936622', + + input: { + from: '0xb2682160c482eb985ec9f3e364eec0a904c44c23', + nonce: 10, + } + }, + + { + address: '0x3474627D4F63A678266BC17171D87f8570936622', + + input: { + from: '0xb2682160c482eb985ec9f3e364eec0a904c44c23', + nonce: "0xa", + } + }, + + { + address: '0x3474627D4F63A678266BC17171D87f8570936622', + + input: { + from: '0xb2682160c482eb985ec9f3e364eec0a904c44c23', + nonce: "0x0a", + } + }, + + { + address: '0x271300790813f82638A8A6A8a86d65df6dF33c17', + + input: { + from: '0x8ba1f109551bd432803012645ac136ddd64dba72', + nonce: "0x200", + } + }, + + { + address: '0x271300790813f82638A8A6A8a86d65df6dF33c17', + + input: { + from: '0x8ba1f109551bd432803012645ac136ddd64dba72', + nonce: "0x0200", + } + }, + + { + address: '0x995C25706C407a1F1E84b3777775e3e619764933', + + input: { + from: '0x8ba1f109551bd432803012645ac136ddd64dba72', + nonce: "0x1d", + } + }, + + { + address: '0x995C25706C407a1F1E84b3777775e3e619764933', + + input: { + from: '0x8ba1f109551bd432803012645ac136ddd64dba72', + nonce: "0x001d", + } + }, + + { + address: '0x995C25706C407a1F1E84b3777775e3e619764933', + + input: { + from: '0x8ba1f109551bd432803012645ac136ddd64dba72', + nonce: 29, + } + }, + + + { + address: '0x0CcCC7507aEDf9FEaF8C8D731421746e16b4d39D', + + input: { + from: '0xc6af6e1a78a6752c7f8cd63877eb789a2adb776c', + nonce: 0 + } + }, +]; \ No newline at end of file diff --git a/packages/web3-eth-contract/test/fixtures/create2.ts b/packages/web3-eth-contract/test/fixtures/create2.ts new file mode 100644 index 00000000000..2cceb34f1bc --- /dev/null +++ b/packages/web3-eth-contract/test/fixtures/create2.ts @@ -0,0 +1,70 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { Address, HexString } from "web3-types"; + +export interface Create2TestData { + address: Address; + salt: HexString; + init_code: HexString; + result: Address; + } + + export const create2TestData: Create2TestData[] = [ + { + address: "0x0000000000000000000000000000000000000000", + salt: "0x0000000000000000000000000000000000000000000000000000000000000000", + init_code: "0x00", + result: "0x4D1A2e2bB4F88F0250f26Ffff098B0b30B26BF38" + }, + { + address: "0xdeadbeef00000000000000000000000000000000", + salt: "0x0000000000000000000000000000000000000000000000000000000000000000", + init_code: "0x00", + result: "0xB928f69Bb1D91Cd65274e3c79d8986362984fDA3" + }, + { + address: "0xdeadbeef00000000000000000000000000000000", + salt: "0x000000000000000000000000feed000000000000000000000000000000000000", + init_code: "0x00", + result: "0xD04116cDd17beBE565EB2422F2497E06cC1C9833" + }, + { + address: "0x0000000000000000000000000000000000000000", + salt: "0x0000000000000000000000000000000000000000000000000000000000000000", + init_code: "0xdeadbeef", + result: "0x70f2b2914A2a4b783FaEFb75f459A580616Fcb5e" + }, + { + address: "0x00000000000000000000000000000000deadbeef", + salt: "0x00000000000000000000000000000000000000000000000000000000cafebabe", + init_code: "0xdeadbeef", + result: "0x60f3f640a8508fC6a86d45DF051962668E1e8AC7" + }, + { + address: "0x00000000000000000000000000000000deadbeef", + salt: "0x00000000000000000000000000000000000000000000000000000000cafebabe", + init_code: "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + result: "0x1d8bfDC5D46DC4f61D6b6115972536eBE6A8854C" + }, + { + address: "0x0000000000000000000000000000000000000000", + salt: "0x0000000000000000000000000000000000000000000000000000000000000000", + init_code: "0x", + result: "0xE33C0C7F7df4809055C3ebA6c09CFe4BaF1BD9e0" + } + ]; \ No newline at end of file diff --git a/packages/web3-eth-contract/test/integration/contract_deploy.test.ts b/packages/web3-eth-contract/test/integration/contract_deploy.test.ts index b8036c123a3..2ff87f07177 100644 --- a/packages/web3-eth-contract/test/integration/contract_deploy.test.ts +++ b/packages/web3-eth-contract/test/integration/contract_deploy.test.ts @@ -16,7 +16,7 @@ along with web3.js. If not, see . */ import { Web3Eth } from 'web3-eth'; import { FMT_BYTES, FMT_NUMBER } from 'web3-types'; -import { Contract } from '../../src'; +import { Contract, createContractAddress } from '../../src'; import { sleep } from '../shared_fixtures/utils'; import { ERC721TokenAbi, ERC721TokenBytecode } from '../shared_fixtures/build/ERC721Token'; import { GreeterBytecode, GreeterAbi } from '../shared_fixtures/build/Greeter'; @@ -59,6 +59,18 @@ describe('contract', () => { sendOptions = { from: acc.address, gas: '1000000' }; }); + it('should get correct contract address before deploymet using CREATE', async () => { + const nonce = await web3Eth.getTransactionCount(sendOptions.from as string); + + // get contract address before deployment + const address = createContractAddress(sendOptions.from as string, nonce); + + const deployedContract = await contract.deploy(deployOptions).send(sendOptions); + + expect(deployedContract).toBeDefined(); + expect(deployedContract.options.address).toEqual(address); + }); + afterAll(async () => { await closeOpenConnection(web3Eth); }); diff --git a/packages/web3-eth-contract/test/unit/utils.test.ts b/packages/web3-eth-contract/test/unit/utils.test.ts new file mode 100644 index 00000000000..3b0b69a2bfc --- /dev/null +++ b/packages/web3-eth-contract/test/unit/utils.test.ts @@ -0,0 +1,94 @@ +/* +This file is part of web3.js. + +web3.js is free software: you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +web3.js is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License +along with web3.js. If not, see . +*/ + +import { InvalidAddressError, InvalidNumberError } from "web3-errors"; +import { Address, Numbers } from "web3-types"; +import { CreateTestData, testData } from '../fixtures/create'; +import { create2ContractAddress, createContractAddress } from "../../src/utils"; +import { create2TestData } from "../fixtures/create2"; + + +describe('createContractAddress', () => { + + it.each(testData)('creates correct contract address for input: %o', (testCase: CreateTestData) => { + const { address, input } = testCase; + const result = createContractAddress(input.from , input.nonce ); + expect(result).toBe(address); + }); + + it('should throw InvalidAddressError for invalid address', () => { + expect(() => createContractAddress('invalid_address', 1)).toThrow(InvalidAddressError); + }); + + it('should throw Error for invalid nonce', () => { + expect(() => createContractAddress('0xe2597eb05cf9a87eb1309e86750c903ec38e527e', "")).toThrow(InvalidNumberError); + }); + + it('should handle different nonce types correctly', () => { + const from: Address = '0x6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0'; + const testCases: [Numbers, string][] = [ + [1, '0x343c43A37D37dfF08AE8C4A11544c718AbB4fCF8'], + ['0x2', '0xf778B86FA74E846c4f0a1fBd1335FE81c00a0C91'], + [BigInt(3), '0xffFd933A0bC612844eaF0C6Fe3E5b8E9B6C1d19c'], + ]; + + testCases.forEach(([nonce, expectedAddress]) => { + const result = createContractAddress(from, nonce); + expect(result).toBe(expectedAddress); + }); + }); + + it('should create different addresses for different nonces', () => { + const from: Address = '0x6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0'; + const address1 = createContractAddress(from, 0); + const address2 = createContractAddress(from, 1); + + expect(address1).not.toBe(address2); + }); + + it('should create different addresses for different sender addresses', () => { + const from1: Address = '0x6ac7ea33f8831ea9dcc53393aaa88b25a785dbf0'; + const from2: Address = '0x1234567890123456789012345678901234567890'; + const nonce: Numbers = 0; + + const address1 = createContractAddress(from1, nonce); + const address2 = createContractAddress(from2, nonce); + + expect(address1).not.toBe(address2); + }); +}); + +describe('create2ContractAddress', () => { + + it.each(create2TestData)('creates correct contract address for input: %o', (testCase) => { + const result = create2ContractAddress(testCase.address, testCase.salt, testCase.init_code); + expect(result).toBe(testCase.result); + }); + + it('should throw an InvalidAddressError if the from address is invalid', () => { + expect(() => create2ContractAddress('0xinvalidaddress', create2TestData[0].salt, create2TestData[0].init_code)).toThrow('Invalid address given 0xinvalidaddress'); + }); + + it('should throw an InvalidMethodParamsError if the salt is invalid', () => { + expect(() => create2ContractAddress(create2TestData[0].address, '0xinvalidsalt', create2TestData[0].init_code)).toThrow('Invalid salt value 0xinvalidsalt'); + }); + + it('should throw an InvalidMethodParamsError if the initCode is invalid', () => { + expect(() => create2ContractAddress(create2TestData[0].address, create2TestData[0].salt, '0xinvalidcode')).toThrow('Invalid initCode value 0xinvalidcode'); + }); + +}); \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index b0c3fe19f17..e85fa099abb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -507,6 +507,11 @@ resolved "https://registry.yarnpkg.com/@ethereumjs/rlp/-/rlp-4.0.1.tgz#626fabfd9081baab3d0a3074b0c7ecaf674aaa41" integrity sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw== +"@ethereumjs/rlp@^5.0.2": + version "5.0.2" + resolved "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-5.0.2.tgz#c89bd82f2f3bec248ab2d517ae25f5bbc4aac842" + integrity sha512-DziebCdg4JpGlEqEdGgXmjqcFoJi+JGulUXwEjsZGAscAQ7MyD/7LE/GVCP29vEQxKc7AAwjT3A2ywHp2xfoCA== + "@ethereumjs/tx@^3.3.0": version "3.5.2" resolved "https://registry.yarnpkg.com/@ethereumjs/tx/-/tx-3.5.2.tgz#197b9b6299582ad84f9527ca961466fce2296c1c" @@ -11458,7 +11463,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -11476,6 +11481,15 @@ string-width@^1.0.1: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -11522,7 +11536,7 @@ string_decoder@~1.1.1: dependencies: safe-buffer "~5.1.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -11536,6 +11550,13 @@ strip-ansi@^3.0.0, strip-ansi@^3.0.1: dependencies: ansi-regex "^2.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" @@ -12622,7 +12643,16 @@ workerpool@6.2.1: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343" integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -12721,10 +12751,10 @@ ws@^8.11.0: resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== -ws@^8.8.1: - version "8.8.1" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.8.1.tgz#5dbad0feb7ade8ecc99b830c1d77c913d4955ff0" - integrity sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA== +ws@^8.17.1: + version "8.17.1" + resolved "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" + integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== xhr@^2.2.0: version "2.6.0" @@ -12855,4 +12885,4 @@ yocto-queue@^0.1.0: zod@^3.21.4: version "3.22.3" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.3.tgz#2fbc96118b174290d94e8896371c95629e87a060" - integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug== \ No newline at end of file + integrity sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==