diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 0f9c376b51..0c2429b4a0 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -40,10 +40,7 @@ "fs-extra": "^8.1.0", "targz": "^1.0.1", "tmp": "^0.2.0", - "viem": "^2.33.2", - "web3": "1.10.4", - "web3-core-helpers": "1.10.4", - "web3-utils": "1.10.4" + "viem": "^2.33.2" }, "devDependencies": { "@celo/base": "workspace:^", diff --git a/packages/dev-utils/src/anvil-test.ts b/packages/dev-utils/src/anvil-test.ts index d0183d0269..7e684d2fb5 100644 --- a/packages/dev-utils/src/anvil-test.ts +++ b/packages/dev-utils/src/anvil-test.ts @@ -1,7 +1,7 @@ import { StrongAddress } from '@celo/base' +import { Provider } from '@celo/connect' import { Anvil, CreateAnvilOptions, createAnvil } from '@viem/anvil' import BigNumber from 'bignumber.js' -import Web3 from 'web3' import { TEST_BALANCE, TEST_BASE_FEE, @@ -9,7 +9,7 @@ import { TEST_GAS_PRICE, TEST_MNEMONIC, jsonRpcCall, - testWithWeb3, + testWithProvider, } from './test-utils' let instance: null | Anvil = null @@ -44,6 +44,7 @@ function createInstance(stateFilePath: string, chainId?: number): Anvil { gasLimit: TEST_GAS_LIMIT, blockBaseFeePerGas: TEST_BASE_FEE, codeSizeLimit: 50000000, + startTimeout: 60_000, stopTimeout: 1000, chainId, } @@ -59,7 +60,7 @@ type TestWithAnvilOptions = { export function testWithAnvilL2( name: string, - fn: (web3: Web3) => void, + fn: (provider: Provider) => void, options?: TestWithAnvilOptions ) { return testWithAnvil(require.resolve('@celo/devchain-anvil/l2-devchain.json'), name, fn, options) @@ -68,13 +69,13 @@ export function testWithAnvilL2( function testWithAnvil( stateFilePath: string, name: string, - fn: (web3: Web3) => void, + fn: (provider: Provider) => void, options?: TestWithAnvilOptions ) { const anvil = createInstance(stateFilePath, options?.chainId) // for each test suite, we start and stop a new anvil instance - return testWithWeb3(name, `http://127.0.0.1:${anvil.port}`, fn, { + return testWithProvider(name, `http://127.0.0.1:${anvil.port}`, fn, { runIf: process.env.RUN_ANVIL_TESTS === 'true' || typeof process.env.RUN_ANVIL_TESTS === 'undefined', hooks: { @@ -89,40 +90,40 @@ function testWithAnvil( } export function impersonateAccount( - web3: Web3, + provider: Provider, address: string, withBalance?: number | bigint | BigNumber ) { return Promise.all([ - jsonRpcCall(web3, 'anvil_impersonateAccount', [address]), + jsonRpcCall(provider, 'anvil_impersonateAccount', [address]), withBalance - ? jsonRpcCall(web3, 'anvil_setBalance', [address, `0x${withBalance.toString(16)}`]) + ? jsonRpcCall(provider, 'anvil_setBalance', [address, `0x${withBalance.toString(16)}`]) : undefined, ]) } -export function stopImpersonatingAccount(web3: Web3, address: string) { - return jsonRpcCall(web3, 'anvil_stopImpersonatingAccount', [address]) +export function stopImpersonatingAccount(provider: Provider, address: string) { + return jsonRpcCall(provider, 'anvil_stopImpersonatingAccount', [address]) } export const withImpersonatedAccount = async ( - web3: Web3, + provider: Provider, account: string, fn: () => Promise, withBalance?: number | bigint | BigNumber ) => { - await impersonateAccount(web3, account, withBalance) + await impersonateAccount(provider, account, withBalance) await fn() - await stopImpersonatingAccount(web3, account) + await stopImpersonatingAccount(provider, account) } export const asCoreContractsOwner = async ( - web3: Web3, + provider: Provider, fn: (ownerAddress: StrongAddress) => Promise, withBalance?: number | bigint | BigNumber ) => { await withImpersonatedAccount( - web3, + provider, DEFAULT_OWNER_ADDRESS, async () => { await fn(DEFAULT_OWNER_ADDRESS) @@ -131,18 +132,18 @@ export const asCoreContractsOwner = async ( ) } -export function setCode(web3: Web3, address: string, code: string) { - return jsonRpcCall(web3, 'anvil_setCode', [address, code]) +export function setCode(provider: Provider, address: string, code: string) { + return jsonRpcCall(provider, 'anvil_setCode', [address, code]) } -export function setNextBlockTimestamp(web3: Web3, timestamp: number) { - return jsonRpcCall(web3, 'evm_setNextBlockTimestamp', [timestamp.toString()]) +export function setNextBlockTimestamp(provider: Provider, timestamp: number) { + return jsonRpcCall(provider, 'evm_setNextBlockTimestamp', [timestamp.toString()]) } export function setBalance( - web3: Web3, + provider: Provider, address: StrongAddress, balance: number | bigint | BigNumber ) { - return jsonRpcCall(web3, 'anvil_setBalance', [address, `0x${balance.toString(16)}`]) + return jsonRpcCall(provider, 'anvil_setBalance', [address, `0x${balance.toString(16)}`]) } diff --git a/packages/dev-utils/src/chain-setup.ts b/packages/dev-utils/src/chain-setup.ts index 556ede8557..cd84be5707 100644 --- a/packages/dev-utils/src/chain-setup.ts +++ b/packages/dev-utils/src/chain-setup.ts @@ -1,54 +1,68 @@ import { governanceABI, validatorsABI } from '@celo/abis' import { StrongAddress } from '@celo/base' -import Web3 from 'web3' +import { Connection, Provider } from '@celo/connect' import { DEFAULT_OWNER_ADDRESS, withImpersonatedAccount } from './anvil-test' +import { encodeFunctionData } from 'viem' export async function setCommissionUpdateDelay( - web3: Web3, + provider: Provider, validatorsContractAddress: StrongAddress, delayInBlocks: number ) { - await withImpersonatedAccount(web3, DEFAULT_OWNER_ADDRESS, async () => { - // @ts-expect-error - const validators = new web3.eth.Contract(validatorsABI, validatorsContractAddress) - - const { transactionHash } = await validators.methods - .setCommissionUpdateDelay(delayInBlocks) - .send({ - from: DEFAULT_OWNER_ADDRESS, - }) - await web3.eth.getTransactionReceipt(transactionHash) + const conn = new Connection(provider) + await withImpersonatedAccount(provider, DEFAULT_OWNER_ADDRESS, async () => { + const data = encodeFunctionData({ + abi: validatorsABI, + functionName: 'setCommissionUpdateDelay', + args: [BigInt(delayInBlocks)], + }) + const transactionHash = await conn.sendTransaction({ + to: validatorsContractAddress, + data, + from: DEFAULT_OWNER_ADDRESS, + }) + await conn.viemClient.waitForTransactionReceipt({ hash: transactionHash }) }) } export async function setDequeueFrequency( - web3: Web3, + provider: Provider, governanceContractAddress: StrongAddress, frequency: number ) { - await withImpersonatedAccount(web3, DEFAULT_OWNER_ADDRESS, async () => { - // @ts-expect-error - const governance = new web3.eth.Contract(governanceABI, governanceContractAddress) - - const { transactionHash } = await governance.methods.setDequeueFrequency(frequency).send({ + const conn = new Connection(provider) + await withImpersonatedAccount(provider, DEFAULT_OWNER_ADDRESS, async () => { + const data = encodeFunctionData({ + abi: governanceABI, + functionName: 'setDequeueFrequency', + args: [BigInt(frequency)], + }) + const transactionHash = await conn.sendTransaction({ + to: governanceContractAddress, + data, from: DEFAULT_OWNER_ADDRESS, }) - await web3.eth.getTransactionReceipt(transactionHash) + await conn.viemClient.waitForTransactionReceipt({ hash: transactionHash }) }) } export async function setReferendumStageDuration( - web3: Web3, + provider: Provider, governanceContractAddress: StrongAddress, duration: number ) { - await withImpersonatedAccount(web3, DEFAULT_OWNER_ADDRESS, async () => { - // @ts-expect-error - const governance = new web3.eth.Contract(governanceABI, governanceContractAddress) - - const { transactionHash } = await governance.methods.setReferendumStageDuration(duration).send({ + const conn = new Connection(provider) + await withImpersonatedAccount(provider, DEFAULT_OWNER_ADDRESS, async () => { + const data = encodeFunctionData({ + abi: governanceABI, + functionName: 'setReferendumStageDuration', + args: [BigInt(duration)], + }) + const transactionHash = await conn.sendTransaction({ + to: governanceContractAddress, + data, from: DEFAULT_OWNER_ADDRESS, }) - await web3.eth.getTransactionReceipt(transactionHash) + await conn.viemClient.waitForTransactionReceipt({ hash: transactionHash }) }) } diff --git a/packages/dev-utils/src/contracts.ts b/packages/dev-utils/src/contracts.ts index c751874773..aeb08df9b6 100644 --- a/packages/dev-utils/src/contracts.ts +++ b/packages/dev-utils/src/contracts.ts @@ -1,26 +1,29 @@ import { StrongAddress } from '@celo/base' +import { Connection, Provider } from '@celo/connect' import AttestationsArtifacts from '@celo/celo-devchain/contracts/contracts-0.5/Attestations.json' -import Web3 from 'web3' +import { encodeDeployData } from 'viem' import { LinkedLibraryAddress } from './anvil-test' -import type { AbiItem } from 'web3-utils' export const deployAttestationsContract = async ( - web3: Web3, + provider: Provider, owner: StrongAddress ): Promise => { - const contract = new web3.eth.Contract(AttestationsArtifacts.abi as AbiItem[]) - - const deployTx = contract.deploy({ - data: AttestationsArtifacts.bytecode.replace( - /__Signatures____________________________/g, - LinkedLibraryAddress.Signatures.replace('0x', '') - ), - // By providing true to the contract constructor - // we don't need to call initialize() on the contract - arguments: [true], + const conn = new Connection(provider) + const linkedBytecode = AttestationsArtifacts.bytecode.replace( + /__Signatures____________________________/g, + LinkedLibraryAddress.Signatures.replace('0x', '') + ) + const data = encodeDeployData({ + abi: AttestationsArtifacts.abi, + bytecode: linkedBytecode as `0x${string}`, + args: [true], }) - const txResult = await deployTx.send({ from: owner }) + const txHash = await conn.sendTransaction({ + from: owner, + data, + }) + const receipt = await conn.viemClient.waitForTransactionReceipt({ hash: txHash }) - return txResult.options.address as StrongAddress + return receipt.contractAddress as StrongAddress } diff --git a/packages/dev-utils/src/ganache-test.ts b/packages/dev-utils/src/ganache-test.ts index 26d58c2c39..05d0e956db 100644 --- a/packages/dev-utils/src/ganache-test.ts +++ b/packages/dev-utils/src/ganache-test.ts @@ -1,17 +1,18 @@ -import Web3 from 'web3' +import { Provider } from '@celo/connect' +import { getAddress, keccak256, toBytes } from 'viem' import migrationOverride from './migration-override.json' import { jsonRpcCall } from './test-utils' export const NetworkConfig = migrationOverride -export async function timeTravel(seconds: number, web3: Web3) { - await jsonRpcCall(web3, 'evm_increaseTime', [seconds]) - await jsonRpcCall(web3, 'evm_mine', []) +export async function timeTravel(seconds: number, provider: Provider) { + await jsonRpcCall(provider, 'evm_increaseTime', [seconds]) + await jsonRpcCall(provider, 'evm_mine', []) } -export async function mineBlocks(blocks: number, web3: Web3) { +export async function mineBlocks(blocks: number, provider: Provider) { for (let i = 0; i < blocks; i++) { - await jsonRpcCall(web3, 'evm_mine', []) + await jsonRpcCall(provider, 'evm_mine', []) } } /** @@ -19,29 +20,32 @@ export async function mineBlocks(blocks: number, web3: Web3) { */ export async function getContractFromEvent( eventSignature: string, - web3: Web3, + provider: Provider, filter?: { expectedData?: string index?: number } ): Promise { - const logs = await web3.eth.getPastLogs({ - topics: [web3.utils.sha3(eventSignature)], - fromBlock: 'earliest', - toBlock: 'latest', - }) + const topic = keccak256(toBytes(eventSignature)) + const logs = await jsonRpcCall(provider, 'eth_getLogs', [ + { + topics: [topic], + fromBlock: 'earliest', + toBlock: 'latest', + }, + ]) if (logs.length === 0) { throw new Error(`Error: contract could not be found matching signature ${eventSignature}`) } const logIndex = filter?.index ?? 0 if (!filter?.expectedData) { - return logs[logIndex].address + return getAddress(logs[logIndex].address) } - const filteredLogs = logs.filter((log) => log.data === filter.expectedData) + const filteredLogs = logs.filter((log: { data: string }) => log.data === filter.expectedData) if (filteredLogs.length === 0) { throw new Error( `Error: contract could not be found matching signature ${eventSignature} with data ${filter.expectedData}` ) } - return filteredLogs[logIndex ?? 0].address + return getAddress(filteredLogs[logIndex ?? 0].address) } diff --git a/packages/dev-utils/src/test-utils.ts b/packages/dev-utils/src/test-utils.ts index 915b3bdc45..2ed9ce9c8b 100644 --- a/packages/dev-utils/src/test-utils.ts +++ b/packages/dev-utils/src/test-utils.ts @@ -1,7 +1,73 @@ -import Web3 from 'web3' -import { JsonRpcResponse } from 'web3-core-helpers' +import { Provider } from '@celo/connect' +import type { EIP1193RequestFn } from 'viem' +import * as http from 'http' import migrationOverride from './migration-override.json' +let nextId = 0 + +class SimpleHttpProvider implements Provider { + /** Compat with legacy HttpProvider which exposed .host */ + readonly host: string + + constructor(readonly url: string) { + this.host = url + } + + request: EIP1193RequestFn = async ({ method, params }) => { + const body = JSON.stringify({ + id: ++nextId, + jsonrpc: '2.0', + method, + params: Array.isArray(params) ? params : params != null ? [params] : [], + }) + const parsedUrl = new URL(this.url) + + return new Promise((resolve, reject) => { + const req = http.request( + { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname + parsedUrl.search, + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(body).toString(), + }, + }, + (res) => { + let data = '' + res.on('data', (chunk: string) => { + data += chunk + }) + res.on('end', () => { + try { + const json = JSON.parse(data) + if (json.error) { + reject( + new Error( + `JSON-RPC error: method: ${method} params: ${JSON.stringify(params)} error: ${JSON.stringify(json.error)}` + ) + ) + } else { + resolve(json.result) + } + } catch (e) { + reject(new Error(`Invalid JSON response: ${data}`)) + } + }) + } + ) + + req.on('error', (err) => { + reject(err) + }) + + req.write(body) + req.end() + }) + } +} + export const MINUTE = 60 export const HOUR = 60 * 60 export const DAY = 24 * HOUR @@ -17,79 +83,45 @@ export const TEST_GAS_LIMIT = 20000000 export const NetworkConfig = migrationOverride -export function jsonRpcCall(web3: Web3, method: string, params: any[]): Promise { - return new Promise((resolve, reject) => { - if (web3.currentProvider && typeof web3.currentProvider !== 'string') { - // @ts-expect-error - web3.currentProvider.send( - { - id: new Date().getTime(), - jsonrpc: '2.0', - method, - params, - }, - (err: Error | null, res?: JsonRpcResponse) => { - if (err) { - reject(err) - } else if (!res) { - reject(new Error('no response')) - } else if (res.error) { - reject( - new Error( - `Failed JsonRpcResponse: method: ${method} params: ${JSON.stringify( - params - )} error: ${JSON.stringify(res.error)}` - ) - ) - } else { - resolve(res.result) - } - } - ) - } else { - reject(new Error('Invalid provider')) - } - }) +export function jsonRpcCall(provider: Provider, method: string, params: unknown[]): Promise { + return provider.request({ method, params }) as Promise } -export function evmRevert(web3: Web3, snapId: string): Promise { - return jsonRpcCall(web3, 'evm_revert', [snapId]) +export function evmRevert(provider: Provider, snapId: string): Promise { + return jsonRpcCall(provider, 'evm_revert', [snapId]) } -export function evmSnapshot(web3: Web3) { - return jsonRpcCall(web3, 'evm_snapshot', []) +export function evmSnapshot(provider: Provider) { + return jsonRpcCall(provider, 'evm_snapshot', []) } -type TestWithWeb3Hooks = { +type TestWithProviderHooks = { beforeAll?: () => Promise afterAll?: () => Promise } /** - * Creates a test suite with a given name and provides function with a web3 instance connected to the given rpcUrl. + * Creates a test suite with a given name and provides the test function with a Provider + * connected to the given rpcUrl. * - * It is an equivalent of jest `describe` with the web3 additioon. It also provides hooks for beforeAll and afterAll. + * It is an equivalent of jest `describe` with a Provider. It also provides + * hooks for beforeAll and afterAll. * - * Optionally if a runIf flag is set to false the test suite will be skipped (useful for conditional test suites). By - * default all test suites are run normally, but if the runIf flag is set to false the test suite will be skipped by using - * jest `describe.skip`. It will be reported in the summary as "skipped". + * Optionally if a runIf flag is set to false the test suite will be skipped (useful for + * conditional test suites). By default all test suites are run normally, but if the runIf + * flag is set to false the test suite will be skipped by using jest `describe.skip`. It will + * be reported in the summary as "skipped". */ -export function testWithWeb3( +export function testWithProvider( name: string, rpcUrl: string, - fn: (web3: Web3) => void, + fn: (provider: Provider) => void, options: { - hooks?: TestWithWeb3Hooks + hooks?: TestWithProviderHooks runIf?: boolean } = {} ) { - const web3 = new Web3(rpcUrl) - - // @ts-ignore with anvil setup the tx receipt is apparently not immedietaly - // available after the tx is send, so by default it was waiting for 1000 ms - // before polling again making the tests slow - web3.eth.transactionPollingInterval = 10 - + const provider = new SimpleHttpProvider(rpcUrl) // By default we run all the tests let describeFn = describe @@ -102,19 +134,19 @@ export function testWithWeb3( let snapId: string | null = null if (options.hooks?.beforeAll) { - beforeAll(options.hooks.beforeAll) + beforeAll(options.hooks.beforeAll, 60_000) } beforeEach(async () => { if (snapId != null) { - await evmRevert(web3, snapId) + await evmRevert(provider, snapId) } - snapId = await evmSnapshot(web3) + snapId = await evmSnapshot(provider) }) afterAll(async () => { if (snapId != null) { - await evmRevert(web3, snapId) + await evmRevert(provider, snapId) } if (options.hooks?.afterAll) { // hook must be awaited here or jest doesnt actually wait for it and complains of open handles @@ -122,6 +154,6 @@ export function testWithWeb3( } }) - fn(web3) + fn(provider) }) } diff --git a/packages/dev-utils/src/viem/anvil-test.ts b/packages/dev-utils/src/viem/anvil-test.ts index 3b6bd26122..5065e785b5 100644 --- a/packages/dev-utils/src/viem/anvil-test.ts +++ b/packages/dev-utils/src/viem/anvil-test.ts @@ -27,6 +27,7 @@ import { import { testWithViem } from './test-utils' let instance: null | Anvil = null +let instanceCounter = 0 type chains = typeof celo | typeof celoSepolia export type TestClientExtended = Client< @@ -44,7 +45,7 @@ function createInstance(opts?: { chainId?: number; forkUrl?: string; forkBlockNu const forkUrl = opts?.forkUrl const forkBlockNumber = opts?.forkBlockNumber - const port = ANVIL_PORT + (process.pid - process.ppid) + const port = ANVIL_PORT + (process.pid - process.ppid) + instanceCounter++ const options: CreateAnvilOptions = { port, mnemonic: TEST_MNEMONIC, @@ -52,6 +53,7 @@ function createInstance(opts?: { chainId?: number; forkUrl?: string; forkBlockNu gasPrice: TEST_GAS_PRICE, gasLimit: TEST_GAS_LIMIT, blockBaseFeePerGas: TEST_BASE_FEE, + startTimeout: 60_000, stopTimeout: 3000, chainId: opts?.chainId, ...(forkUrl diff --git a/packages/dev-utils/src/viem/test-utils.ts b/packages/dev-utils/src/viem/test-utils.ts index 3714e38194..0e27857f7b 100644 --- a/packages/dev-utils/src/viem/test-utils.ts +++ b/packages/dev-utils/src/viem/test-utils.ts @@ -7,9 +7,9 @@ type Hooks = { } /** - * Creates a test suite with a given name and provides function with a web3 instance connected to the given rpcUrl. + * Creates a test suite with a given name and provides function with a viem client connected to the given rpcUrl. * - * It is an equivalent of jest `describe` with the web3 additioon. It also provides hooks for beforeAll and afterAll. + * It is an equivalent of jest `describe` with a viem test client. It also provides hooks for beforeAll and afterAll. * * Optionally if a runIf flag is set to false the test suite will be skipped (useful for conditional test suites). By * default all test suites are run normally, but if the runIf flag is set to false the test suite will be skipped by using @@ -37,7 +37,7 @@ export function testWithViem( let snapId: Hex | null = null if (options.hooks?.beforeAll) { - beforeAll(options.hooks.beforeAll, 15_000) + beforeAll(options.hooks.beforeAll, 30_000) } beforeEach(async () => { diff --git a/packages/dev-utils/tsconfig-base.json b/packages/dev-utils/tsconfig-base.json index f131580fae..b2326e96b7 100644 --- a/packages/dev-utils/tsconfig-base.json +++ b/packages/dev-utils/tsconfig-base.json @@ -2,6 +2,7 @@ "compilerOptions": { "rootDir": "src", "declaration": true, + "declarationMap": true, "esModuleInterop": true, "types": ["node", "@types/jest"], "lib": ["esnext"], diff --git a/packages/sdk/explorer/package.json b/packages/sdk/explorer/package.json index 3b094508bf..3a79d82edd 100644 --- a/packages/sdk/explorer/package.json +++ b/packages/sdk/explorer/package.json @@ -32,14 +32,14 @@ "@types/debug": "^4.1.5", "bignumber.js": "9.0.0", "cross-fetch": "3.1.5", - "debug": "^4.1.1" + "debug": "^4.1.1", + "viem": "^2.33.2" }, "devDependencies": { "@celo/dev-utils": "workspace:^", "@celo/typescript": "workspace:^", "@types/debug": "^4.1.12", - "fetch-mock": "^10.0.7", - "web3": "1.10.4" + "fetch-mock": "^10.0.7" }, "engines": { "node": ">=20" diff --git a/packages/sdk/explorer/scripts/driver.ts b/packages/sdk/explorer/scripts/driver.ts index 9187deb9e9..22112e591c 100644 --- a/packages/sdk/explorer/scripts/driver.ts +++ b/packages/sdk/explorer/scripts/driver.ts @@ -1,8 +1,7 @@ -import { newKitFromWeb3 } from '@celo/contractkit' -import Web3 from 'web3' +import { newKit } from '@celo/contractkit' import { newBlockExplorer } from '../src/block-explorer' -const kit = newKitFromWeb3(new Web3('ws://localhost:8545')) +const kit = newKit('ws://localhost:8545') export function listenFor(subscription: any, seconds: number) { console.log(subscription) @@ -35,31 +34,7 @@ async function main() { console.log('Block', block.number) printJSON(blockExplorer.parseBlock(block)) }) - // const pastStableEvents = await stableToken.getPastEvents('allevents', { fromBlock: 0 }) - // const pastGenericEvents = await kit.web3.eth.getPastLogs({ - // address: '0x371b13d97f4bf77d724e78c16b7dc74099f40e84', - // fromBlock: '0x0', - // }) - - // printJSON(pastStableEvents) - // console.log('------------------------------------------------------') - // printJSON(pastGenericEvents) - - // const tokenEvents = await listenFor(stableToken.events.allEvents({ fromBlock: 0 }), 3) - - // console.log(JSON.stringify(tokenEvents[0], null, 2)) - - // const genEvents = await listenFor( - // kit.web3.eth.subscribe('logs', { - // address: '0x371b13d97f4bf77d724e78c16b7dc74099f40e84', - // fromBlock: 0, - // topics: [], - // }), - // 3 - // ) - - // console.log(JSON.stringify(genEvents, null, 2)) kit.connection.stop() } diff --git a/packages/sdk/explorer/src/base.ts b/packages/sdk/explorer/src/base.ts index 95b3b59908..fd9f5ca5fa 100644 --- a/packages/sdk/explorer/src/base.ts +++ b/packages/sdk/explorer/src/base.ts @@ -19,11 +19,11 @@ export const getContractDetailsFromContract: any = async ( celoContract: CeloContract, address?: string ) => { - const contract = await kit._web3Contracts.getContract(celoContract, address) + const contract = await kit._contracts.getContract(celoContract, address) return { name: celoContract, - address: address ?? contract.options.address, - jsonInterface: contract.options.jsonInterface, + address: address ?? contract.address, + jsonInterface: contract.abi, isCore: true, } } diff --git a/packages/sdk/explorer/src/block-explorer.ts b/packages/sdk/explorer/src/block-explorer.ts index 01731efc30..531065f9ec 100644 --- a/packages/sdk/explorer/src/block-explorer.ts +++ b/packages/sdk/explorer/src/block-explorer.ts @@ -1,11 +1,6 @@ -import { - ABIDefinition, - Address, - Block, - CeloTxPending, - parseDecodedParams, - signatureToAbiDefinition, -} from '@celo/connect' +import { ABIDefinition, Address, decodeParametersToObject, parseDecodedParams } from '@celo/connect' +import type { Block, Transaction } from 'viem' +import { toChecksumAddress } from '@celo/utils/lib/address' import { CeloContract, ContractKit } from '@celo/contractkit' import { PROXY_ABI } from '@celo/contractkit/lib/proxy' import { fromFixed } from '@celo/utils/lib/fixidity' @@ -38,7 +33,7 @@ export interface CallDetails { export interface ParsedTx { callDetails: CallDetails - tx: CeloTxPending + tx: Transaction } export interface ParsedBlock { @@ -92,10 +87,10 @@ export class BlockExplorer { } async fetchBlockByHash(blockHash: string): Promise { - return this.kit.connection.getBlock(blockHash) + return this.kit.connection.viemClient.getBlock({ blockHash: blockHash as `0x${string}` }) } async fetchBlock(blockNumber: number): Promise { - return this.kit.connection.getBlock(blockNumber) + return this.kit.connection.viemClient.getBlock({ blockNumber: BigInt(blockNumber) }) } async fetchBlockRange(from: number, to: number): Promise { @@ -123,7 +118,7 @@ export class BlockExplorer { } } - async tryParseTx(tx: CeloTxPending): Promise { + async tryParseTx(tx: Transaction): Promise { const callDetails = await this.tryParseTxInput(tx.to!, tx.input) if (!callDetails) { return null @@ -146,136 +141,15 @@ export class BlockExplorer { return null } - private getContractMethodAbiFromMapping = ( - contractMapping: ContractMapping, - selector: string - ): ContractNameAndMethodAbi | null => { - if (contractMapping === undefined) { - return null - } - - const methodAbi = contractMapping.fnMapping.get(selector) - if (methodAbi === undefined) { - return null - } - - return { - contract: contractMapping.details.address, - contractName: contractMapping.details.name, - abi: methodAbi, - } - } - - /** - * @deprecated use getContractMappingWithSelector instead - * Returns the contract name and ABI of the method by looking up - * the contract address either in all possible contract mappings. - * @param address - * @param selector - * @param onlyCoreContracts - * @returns The contract name and ABI of the method or null if not found - */ - getContractMethodAbi = async ( - address: string, - selector: string, - onlyCoreContracts = false - ): Promise => { - if (onlyCoreContracts) { - return this.getContractMethodAbiFromCore(address, selector) - } - - const contractMapping = await this.getContractMappingWithSelector(address, selector) - if (contractMapping === undefined) { - return null - } - - return this.getContractMethodAbiFromMapping(contractMapping, selector) - } - - /** - * Returns the contract name and ABI of the method by looking up - * the contract address but only in core contracts - * @param address - * @param selector - * @returns The contract name and ABI of the method or null if not found - */ - getContractMethodAbiFromCore = async ( - address: string, - selector: string - ): Promise => { - const contractMapping = await this.getContractMappingWithSelector(address, selector, [ - this.getContractMappingFromCore, - ]) - - if (contractMapping === undefined) { - return null - } - - return this.getContractMethodAbiFromMapping(contractMapping, selector) - } - - /** - * @deprecated use getContractMappingWithSelector instead - * Returns the contract name and ABI of the method by looking up - * the contract address in Sourcify. - * @param address - * @param selector - * @returns The contract name and ABI of the method or null if not found - */ - getContractMethodAbiFromSourcify = async ( - address: string, - selector: string - ): Promise => { - const contractMapping = await this.getContractMappingWithSelector(address, selector, [ - this.getContractMappingFromSourcify, - this.getContractMappingFromSourcifyAsProxy, - ]) - - if (contractMapping === undefined) { - return null - } - - return this.getContractMethodAbiFromMapping(contractMapping, selector) - } - - /** - * @deprecated use getContractMappingWithSelector instead - * Returns the contract name and ABI of the method by looking up - * the selector in a list of known functions. - * @param address - * @param selector - * @param onlyCoreContracts - * @returns The contract name and ABI of the method or null if not found - */ - getContractMethodAbiFallback = ( - address: string, - selector: string - ): ContractNameAndMethodAbi | null => { - // TODO(bogdan): This could be replaced with a call to 4byte.directory - // or a local database of common functions. - const knownFunctions: { [k: string]: string } = { - '0x095ea7b3': 'approve(address to, uint256 value)', - '0x4d49e87d': 'addLiquidity(uint256[] amounts, uint256 minLPToMint, uint256 deadline)', - } - const signature = knownFunctions[selector] - if (signature) { - return { - abi: signatureToAbiDefinition(signature), - contract: `Unknown(${address})`, - } - } - return null - } - buildCallDetails(contract: ContractDetails, abi: ABIDefinition, input: string): CallDetails { const encodedParameters = input.slice(10) const { args, params } = parseDecodedParams( - this.kit.connection.getAbiCoder().decodeParameters(abi.inputs!, encodedParameters) + decodeParametersToObject(abi.inputs!, encodedParameters) ) // transform numbers to big numbers in params abi.inputs!.forEach((abiInput, idx) => { - if (abiInput.type === 'uint256') { + if (abiInput.type === 'uint256' && abiInput.name) { debug('transforming number param') params[abiInput.name] = new BigNumber(args[idx]) } @@ -286,7 +160,7 @@ export class BlockExplorer { .filter((key) => key.includes('fraction')) // TODO: come up with better enumeration .forEach((fractionKey) => { debug('transforming fixed number param') - params[fractionKey] = fromFixed(params[fractionKey]) + params[fractionKey] = fromFixed(params[fractionKey] as BigNumber) }) return { @@ -322,10 +196,7 @@ export class BlockExplorer { if (cached) { return cached } - const metadata = await fetchMetadata( - this.kit.connection, - this.kit.web3.utils.toChecksumAddress(address) - ) + const metadata = await fetchMetadata(this.kit.connection, toChecksumAddress(address)) const mapping = metadata?.toContractMapping() if (mapping) { this.addressMapping.set(address, mapping) diff --git a/packages/sdk/explorer/src/log-explorer.ts b/packages/sdk/explorer/src/log-explorer.ts index 594eec8a85..8da88ce1e2 100644 --- a/packages/sdk/explorer/src/log-explorer.ts +++ b/packages/sdk/explorer/src/log-explorer.ts @@ -1,4 +1,5 @@ -import { ABIDefinition, Address, CeloTxReceipt, EventLog, Log } from '@celo/connect' +import { ABIDefinition, Address, AbiInput, EventLog } from '@celo/connect' +import { decodeEventLog, toEventHash, type TransactionReceipt } from 'viem' import { ContractKit } from '@celo/contractkit' import { ContractDetails, mapFromPairs, obtainKitContractDetails } from './base' @@ -47,11 +48,17 @@ export class LogExplorer { } } - async fetchTxReceipt(txhash: string): Promise { - return this.kit.connection.getTransactionReceipt(txhash) + async fetchTxReceipt(txhash: string): Promise { + try { + return await this.kit.connection.viemClient.getTransactionReceipt({ + hash: txhash as `0x${string}`, + }) + } catch { + return null + } } - getKnownLogs(tx: CeloTxReceipt): EventLog[] { + getKnownLogs(tx: TransactionReceipt): EventLog[] { const res: EventLog[] = [] for (const log of tx.logs || []) { const event = this.tryParseLog(log) @@ -62,7 +69,7 @@ export class LogExplorer { return res } - tryParseLog(log: Log): null | EventLog { + tryParseLog(log: TransactionReceipt['logs'][number]): null | EventLog { if (log.topics.length === 0) { return null } @@ -72,37 +79,55 @@ export class LogExplorer { return null } const logSignature = log.topics[0] + if (logSignature == null) { + return null + } const matchedAbi = contractMapping.logMapping.get(logSignature) if (matchedAbi == null) { return null } - const returnValues = this.kit.connection - .getAbiCoder() - .decodeLog(matchedAbi.inputs || [], log.data || '', log.topics.slice(1)) - delete (returnValues as any).__length__ - Object.keys(returnValues).forEach((key) => { - if (Number.parseInt(key, 10) >= 0) { - delete (returnValues as any)[key] + const eventInputs = (matchedAbi.inputs || []).map((input: AbiInput) => ({ + ...input, + indexed: input.indexed ?? false, + })) + const eventAbi = [ + { type: 'event' as const, name: matchedAbi.name || 'Event', inputs: eventInputs }, + ] + const sig = `${matchedAbi.name || 'Event'}(${eventInputs.map((i: AbiInput) => i.type).join(',')})` + const eventSigHash = toEventHash(sig) + const fullTopics = [eventSigHash, ...log.topics.slice(1)] as [`0x${string}`, ...`0x${string}`[]] + try { + const result = decodeEventLog({ + abi: eventAbi, + data: (log.data || '0x') as `0x${string}`, + topics: fullTopics, + }) + const decoded = { ...(result.args as Record) } + // bigint to string for backward compat + for (const key of Object.keys(decoded)) { + if (typeof decoded[key] === 'bigint') decoded[key] = (decoded[key] as bigint).toString() } - }) - const logEvent: EventLog & { signature: string } = { - address: log.address, - blockHash: log.blockHash, - blockNumber: log.blockNumber, - logIndex: log.logIndex, - transactionIndex: log.transactionIndex, - transactionHash: log.transactionHash, - returnValues, - event: matchedAbi.name!, - signature: logSignature, - raw: { - data: log.data || '', - topics: log.topics || [], - }, - } + const logEvent: EventLog & { signature: string } = { + address: log.address, + blockHash: log.blockHash, + blockNumber: Number(log.blockNumber), + logIndex: log.logIndex, + transactionIndex: log.transactionIndex, + transactionHash: log.transactionHash, + returnValues: decoded, + event: matchedAbi.name!, + signature: logSignature, + raw: { + data: log.data || '', + topics: log.topics || [], + }, + } - return logEvent + return logEvent + } catch { + return null + } } } diff --git a/packages/sdk/explorer/src/sourcify.test.ts b/packages/sdk/explorer/src/sourcify.test.ts index 08d0cf481f..8547652705 100644 --- a/packages/sdk/explorer/src/sourcify.test.ts +++ b/packages/sdk/explorer/src/sourcify.test.ts @@ -1,12 +1,6 @@ -import { - Address, - Callback, - Connection, - JsonRpcPayload, - JsonRpcResponse, - Provider, -} from '@celo/connect' -import Web3 from 'web3' +import * as crypto from 'crypto' +import { Address, Connection, Provider } from '@celo/connect' +import { toFunctionSelector } from 'viem' import { Metadata, fetchMetadata, tryGetProxyImplementation } from './sourcify' // This is taken from protocol/contracts/build/Account.json @@ -14,33 +8,28 @@ const CONTRACT_METADATA = require('../fixtures/contract.metadata.json') describe('sourcify helpers', () => { let connection: Connection - const web3: Web3 = new Web3() - const address: Address = web3.utils.randomHex(20) - const proxyAddress: Address = web3.utils.randomHex(20) - const implAddress: Address = web3.utils.randomHex(20) + const address: Address = '0x' + crypto.randomBytes(20).toString('hex') + const proxyAddress: Address = '0x' + crypto.randomBytes(20).toString('hex') + const implAddress: Address = '0x' + crypto.randomBytes(20).toString('hex') const chainId: number = 42220 const mockProvider: Provider = { - send: (payload: JsonRpcPayload, callback: Callback): void => { - if (payload.params[0].to === proxyAddress) { - callback(null, { - jsonrpc: payload.jsonrpc, - id: Number(payload.id), - result: `0x000000000000000000000000${implAddress}`, - }) + request: (async ({ method, params }: { method: string; params?: any }) => { + if (method === 'eth_chainId') { + return `0x${chainId.toString(16)}` + } + const safeParams = Array.isArray(params) ? params : params != null ? [params] : [] + if (safeParams[0]?.to === proxyAddress) { + return `0x000000000000000000000000${implAddress.slice(2)}` } else { - callback(new Error('revert')) + throw new Error('revert') } - }, + }) as any, } beforeEach(() => { fetchMock.reset() - web3.setProvider(mockProvider as any) - connection = new Connection(web3) - connection.chainId = jest.fn().mockImplementation(async () => { - return chainId - }) + connection = new Connection(mockProvider) }) describe('fetchMetadata()', () => { @@ -198,9 +187,7 @@ describe('sourcify helpers', () => { describe('when the function exists', () => { it('returns the ABI', async () => { - const callSignature = connection - .getAbiCoder() - .encodeFunctionSignature('authorizedBy(address)') + const callSignature = toFunctionSelector('authorizedBy(address)') const abi = contractMetadata.abiForSelector(callSignature) expect(abi).toMatchObject({ constant: true, @@ -234,6 +221,167 @@ describe('sourcify helpers', () => { }) }) + describe('abiForMethod with tuple params (tests abiItemToSignatureString)', () => { + it('matches a function with simple params via full signature', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + ], + outputs: [{ name: 'success', type: 'bool' }], + stateMutability: 'nonpayable', + }, + ], + }, + }) + const results = metadata.abiForMethod('transfer(address,uint256)') + expect(results.length).toEqual(1) + expect(results[0].name).toBe('transfer') + }) + + it('matches a function with tuple params via full signature', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'function', + name: 'complexMethod', + inputs: [ + { + name: 'data', + type: 'tuple', + components: [ + { name: 'addr', type: 'address' }, + { name: 'amount', type: 'uint256' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + ], + }, + }) + const results = metadata.abiForMethod('complexMethod((address,uint256))') + expect(results.length).toEqual(1) + expect(results[0].name).toBe('complexMethod') + }) + + it('matches a function with tuple array params', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'function', + name: 'batchTransfer', + inputs: [ + { + name: 'transfers', + type: 'tuple[]', + components: [ + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + ], + }, + }) + const results = metadata.abiForMethod('batchTransfer((address,uint256)[])') + expect(results.length).toEqual(1) + expect(results[0].name).toBe('batchTransfer') + }) + + it('matches a function with nested tuple params', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'function', + name: 'nested', + inputs: [ + { + name: 'data', + type: 'tuple', + components: [ + { + name: 'inner', + type: 'tuple', + components: [ + { name: 'x', type: 'uint256' }, + { name: 'y', type: 'uint256' }, + ], + }, + { name: 'flag', type: 'bool' }, + ], + }, + ], + outputs: [], + stateMutability: 'nonpayable', + }, + ], + }, + }) + const results = metadata.abiForMethod('nested(((uint256,uint256),bool))') + expect(results.length).toEqual(1) + expect(results[0].name).toBe('nested') + }) + + it('returns empty for mismatched signature', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'function', + name: 'transfer', + inputs: [ + { name: 'to', type: 'address' }, + { name: 'value', type: 'uint256' }, + ], + outputs: [{ name: 'success', type: 'bool' }], + stateMutability: 'nonpayable', + }, + ], + }, + }) + const results = metadata.abiForMethod('transfer(address,bool)') + expect(results.length).toEqual(0) + }) + + it('handles event and constructor types (does not match as function)', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'event', + name: 'Transfer', + inputs: [ + { name: 'from', type: 'address', indexed: true }, + { name: 'to', type: 'address', indexed: true }, + { name: 'value', type: 'uint256', indexed: false }, + ], + }, + { + type: 'constructor', + inputs: [{ name: 'supply', type: 'uint256' }], + }, + ], + }, + }) + // Events and constructors should not be found by abiForMethod + const results = metadata.abiForMethod('Transfer(address,address,uint256)') + expect(results.length).toEqual(0) + }) + }) + describe('tryGetProxyImplementation', () => { describe('with a cLabs proxy', () => { it('fetches the implementation', async () => { @@ -249,5 +397,31 @@ describe('sourcify helpers', () => { }) }) }) + + describe('toContractMapping', () => { + it('returns a mapping with fnMapping populated', () => { + const metadata = new Metadata(connection, address, { + output: { + abi: [ + { + type: 'function', + name: 'foo', + inputs: [], + outputs: [{ name: '', type: 'uint256' }], + stateMutability: 'view', + }, + ], + }, + settings: { + compilationTarget: { 'foo.sol': 'Foo' }, + }, + }) + const mapping = metadata.toContractMapping() + expect(mapping.details.name).toBe('Foo') + expect(mapping.details.address).toBe(address) + expect(mapping.details.isCore).toBe(false) + expect(mapping.fnMapping.size).toBeGreaterThan(0) + }) + }) }) }) diff --git a/packages/sdk/explorer/src/sourcify.ts b/packages/sdk/explorer/src/sourcify.ts index ad572934f8..03615114d0 100644 --- a/packages/sdk/explorer/src/sourcify.ts +++ b/packages/sdk/explorer/src/sourcify.ts @@ -10,10 +10,37 @@ * // do something with it. * } */ -import { AbiCoder, ABIDefinition, AbiItem, Address, Connection } from '@celo/connect' +import { ABIDefinition, AbiItem, AbiInput, Address, Connection } from '@celo/connect' +import { toFunctionSelector } from 'viem' import fetch from 'cross-fetch' import { ContractMapping, mapFromPairs } from './base' +/** + * Convert an ABI item to a function signature string like `transfer(address,uint256)`. + * Replaces the former `_jsonInterfaceMethodToString` helper. + */ +function abiItemToSignatureString(item: AbiItem): string { + if (item.type === 'function' || item.type === 'constructor' || item.type === 'event') { + const inputTypes = (item.inputs || []).map((input: AbiInput) => formatAbiInputType(input)) + return `${item.name || ''}(${inputTypes.join(',')})` + } + return item.name || '' +} + +/** ABI input that may have tuple components (runtime ABI data from Solidity) */ +type AbiInputWithComponents = AbiInput & { components?: readonly AbiInputWithComponents[] } + +function formatAbiInputType(input: AbiInputWithComponents): string { + if (input.type === 'tuple' && input.components) { + return `(${input.components.map((c: AbiInput) => formatAbiInputType(c)).join(',')})` + } + if (input.type.startsWith('tuple[') && input.components) { + const suffix = input.type.slice(5) // e.g. '[]' or '[3]' + return `(${input.components.map((c: AbiInput) => formatAbiInputType(c)).join(',')})${suffix}` + } + return input.type +} + const PROXY_IMPLEMENTATION_GETTERS = [ '_getImplementation', 'getImplementation', @@ -66,17 +93,10 @@ export class Metadata { public contractName: string | null = null public fnMapping: Map = new Map() - private abiCoder: AbiCoder - private jsonInterfaceMethodToString: (item: AbiItem) => string private address: Address - constructor(connection: Connection, address: Address, response: any) { - this.abiCoder = connection.getAbiCoder() - + constructor(_connection: Connection, address: Address, response: any) { this.response = response as MetadataResponse - // XXX: For some reason this isn't exported as it should be - // @ts-ignore - this.jsonInterfaceMethodToString = connection.web3.utils._jsonInterfaceMethodToString this.address = address } @@ -93,7 +113,8 @@ export class Metadata { (this.abi || []) .filter((item) => item.type === 'function') .map((item) => { - const signature = this.abiCoder.encodeFunctionSignature(item) + const sig = `${item.name}(${(item.inputs || []).map((i: AbiInput) => formatAbiInputType(i)).join(',')})` + const signature = toFunctionSelector(sig) return { ...item, signature } }) .map((item) => [item.signature, item]) @@ -136,7 +157,12 @@ export class Metadata { abiForSelector(selector: string): AbiItem | null { return ( this.abi?.find((item) => { - return item.type === 'function' && this.abiCoder.encodeFunctionSignature(item) === selector + return ( + item.type === 'function' && + toFunctionSelector( + `${item.name}(${(item.inputs || []).map((i: AbiInput) => formatAbiInputType(i)).join(',')})` + ) === selector + ) }) || null ) } @@ -154,7 +180,7 @@ export class Metadata { // Method is a full call signature with arguments return ( this.abi?.filter((item) => { - return item.type === 'function' && this.jsonInterfaceMethodToString(item) === query + return item.type === 'function' && abiItemToSignatureString(item) === query }) || [] ) } else { @@ -203,7 +229,7 @@ async function querySourcify( matchType: 'full_match' | 'partial_match', contract: Address ): Promise { - const chainID = await connection.chainId() + const chainID = await connection.viemClient.getChainId() const resp = await fetch( `https://repo.sourcify.dev/contracts/${matchType}/${chainID}/${contract}/metadata.json` ) @@ -229,23 +255,25 @@ export async function tryGetProxyImplementation( connection: Connection, contract: Address ): Promise
{ - const proxyContract = new connection.web3.eth.Contract(PROXY_ABI, contract) + const proxyContract = connection.getCeloContract(PROXY_ABI, contract) for (const fn of PROXY_IMPLEMENTATION_GETTERS) { try { - return await new Promise((resolve, reject) => { - proxyContract.methods[fn]().call().then(resolve).catch(reject) - }) + const result = await (proxyContract as any).read[fn]() + return result as Address } catch { continue } } try { - const hexValue = await connection.web3.eth.getStorageAt( - contract, - PROXY_IMPLEMENTATION_POSITION_UUPS - ) - const address = connection.web3.utils.toChecksumAddress('0x' + hexValue.slice(-40)) + const hexValue = await connection.viemClient.getStorageAt({ + address: contract as `0x${string}`, + slot: PROXY_IMPLEMENTATION_POSITION_UUPS as `0x${string}`, + }) + if (!hexValue) { + return undefined + } + const address = ('0x' + hexValue.slice(-40)) as Address return address } catch { return undefined diff --git a/packages/sdk/governance/package.json b/packages/sdk/governance/package.json index 20c4190029..5d592bd6e5 100644 --- a/packages/sdk/governance/package.json +++ b/packages/sdk/governance/package.json @@ -35,7 +35,8 @@ "@types/inquirer": "^6.5.0", "bignumber.js": "^9.0.0", "debug": "^4.1.1", - "inquirer": "^7.3.3" + "inquirer": "^7.3.3", + "viem": "^2.33.2" }, "engines": { "node": ">=20" diff --git a/packages/sdk/governance/src/interactive-proposal-builder.test.ts b/packages/sdk/governance/src/interactive-proposal-builder.test.ts index 2d1d2e4123..d59039df2f 100644 --- a/packages/sdk/governance/src/interactive-proposal-builder.test.ts +++ b/packages/sdk/governance/src/interactive-proposal-builder.test.ts @@ -1,4 +1,4 @@ -import { newKitFromWeb3, RegisteredContracts } from '@celo/contractkit' +import { newKitFromProvider, RegisteredContracts } from '@celo/contractkit' import inquirer from 'inquirer' import { InteractiveProposalBuilder, requireABI } from './interactive-proposal-builder' import { ProposalBuilder } from './proposal-builder' @@ -17,13 +17,13 @@ describe('all registered contracts can be required', () => { }) }) -testWithAnvilL2('InteractiveProposalBuilder', (web3) => { +testWithAnvilL2('InteractiveProposalBuilder', (provider) => { let builder: ProposalBuilder let interactiveBuilder: InteractiveProposalBuilder let fromJsonTxSpy: jest.SpyInstance beforeEach(() => { - const kit = newKitFromWeb3(web3) + const kit = newKitFromProvider(provider) builder = new ProposalBuilder(kit) fromJsonTxSpy = jest.spyOn(builder, 'fromJsonTx') interactiveBuilder = new InteractiveProposalBuilder(builder) diff --git a/packages/sdk/governance/src/interactive-proposal-builder.ts b/packages/sdk/governance/src/interactive-proposal-builder.ts index 4a52a2293b..e3ff19c0df 100644 --- a/packages/sdk/governance/src/interactive-proposal-builder.ts +++ b/packages/sdk/governance/src/interactive-proposal-builder.ts @@ -6,6 +6,7 @@ import BigNumber from 'bignumber.js' import inquirer from 'inquirer' import type { ProposalTransactionJSON } from './' import { ProposalBuilder } from './proposal-builder' +import { bigintReplacer } from './json-utils' const DONE_CHOICE = '✔ done' @@ -14,7 +15,7 @@ export class InteractiveProposalBuilder { async outputTransactions() { const transactionList = this.builder.build() - console.log(JSON.stringify(transactionList, null, 2)) + console.log(JSON.stringify(transactionList, bigintReplacer, 2)) } async promptTransactions() { @@ -79,7 +80,7 @@ export class InteractiveProposalBuilder { }, }) - const answer: string = inputAnswer[functionInput.name] + const answer: string = inputAnswer[functionInput.name!] // transformedValue may not be in scientific notation const transformedValue = functionInput.type === 'uint256' ? new BigNumber(answer).toString(10) : answer @@ -118,25 +119,11 @@ export class InteractiveProposalBuilder { } } export function requireABI(contractName: CeloContract): ABIDefinition[] { - // search thru multiple paths to find the ABI - if (contractName === CeloContract.CeloToken) { - contractName = CeloContract.GoldToken - } else if (contractName === CeloContract.LockedCelo) { - contractName = CeloContract.LockedGold - } - for (const path of ['', '0.8/', 'mento/']) { - const abi = safeRequire(contractName, path) - if (abi !== null) { - return abi - } - } - throw new Error(`Cannot require ABI for ${contractName}`) -} - -function safeRequire(contractName: CeloContract, subPath?: string) { - try { - return require(`@celo/abis/web3/${subPath ?? ''}${contractName}`).ABI as ABIDefinition[] - } catch { - return null + // eslint-disable-next-line @typescript-eslint/no-require-imports + const mod = require(`@celo/abis/${contractName}`) + const abiKey = Object.keys(mod).find((key) => key.endsWith('ABI')) + if (abiKey) { + return mod[abiKey] as ABIDefinition[] } + throw new Error(`Cannot find ABI export for ${contractName}`) } diff --git a/packages/sdk/governance/src/json-utils.ts b/packages/sdk/governance/src/json-utils.ts new file mode 100644 index 0000000000..4a6d52a8cb --- /dev/null +++ b/packages/sdk/governance/src/json-utils.ts @@ -0,0 +1,12 @@ +/** + * JSON replacer function that handles BigInt serialization. + * viem returns bigint for numeric fields, and JSON.stringify crashes on BigInt + * with "TypeError: Do not know how to serialize a BigInt". + * This replacer converts bigint values to strings. + */ +export const bigintReplacer = (_key: string, value: unknown): unknown => { + if (typeof value === 'bigint') { + return value.toString() + } + return value +} diff --git a/packages/sdk/governance/src/proposal-builder.test.ts b/packages/sdk/governance/src/proposal-builder.test.ts index 99692dc53e..d4261f34da 100644 --- a/packages/sdk/governance/src/proposal-builder.test.ts +++ b/packages/sdk/governance/src/proposal-builder.test.ts @@ -1,14 +1,15 @@ +import { governanceABI } from '@celo/abis' import { AbiItem } from '@celo/connect' -import { CeloContract, ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { CeloContract, ContractKit, newKitFromProvider } from '@celo/contractkit' import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test' -import BigNumber from 'bignumber.js' +import { encodeFunctionData } from 'viem' import { ProposalBuilder } from './proposal-builder' -testWithAnvilL2('ProposalBuilder', (web3) => { +testWithAnvilL2('ProposalBuilder', (provider) => { let kit: ContractKit let proposalBuilder: ProposalBuilder beforeEach(() => { - kit = newKitFromWeb3(web3) + kit = newKitFromProvider(provider) proposalBuilder = new ProposalBuilder(kit) }) @@ -19,15 +20,14 @@ testWithAnvilL2('ProposalBuilder', (web3) => { }) }) - describe('addWeb3Tx', () => { - it('adds and builds a Web3 transaction', async () => { - const wrapper = await kit.contracts.getGovernance() - // if we want to keep input in the expectation the same the dequeue index needs to be same length as it was on alfajores - const dequeue = new Array(56).fill(0) - dequeue.push(125) - jest.spyOn(wrapper, 'getDequeue').mockResolvedValue(dequeue.map((x) => new BigNumber(x))) - const tx = await wrapper.approve(new BigNumber('125')) - proposalBuilder.addWeb3Tx(tx.txo, { to: '0x5678', value: '1000' }) + describe('addEncodedTx', () => { + it('adds and builds an encoded transaction', async () => { + const data = encodeFunctionData({ + abi: governanceABI, + functionName: 'approve', + args: [BigInt(125), BigInt(56)], + }) + proposalBuilder.addEncodedTx(data, { to: '0x5678', value: '1000' }) const proposal = await proposalBuilder.build() expect(proposal).toEqual([ { diff --git a/packages/sdk/governance/src/proposal-builder.ts b/packages/sdk/governance/src/proposal-builder.ts index cce637ac3f..91de6c2d2e 100644 --- a/packages/sdk/governance/src/proposal-builder.ts +++ b/packages/sdk/governance/src/proposal-builder.ts @@ -1,10 +1,6 @@ -import { - AbiItem, - CeloTransactionObject, - CeloTxObject, - Contract, - signatureToAbiDefinition, -} from '@celo/connect' +import { AbiItem, signatureToAbiDefinition } from '@celo/connect' +import { coerceArgsForAbi } from '@celo/connect/lib/viem-abi-coder' +import { toChecksumAddress } from '@celo/utils/lib/address' import { CeloContract, ContractKit, @@ -14,11 +10,11 @@ import { setImplementationOnProxy, } from '@celo/contractkit' import { stripProxy } from '@celo/contractkit/lib/base' -import { valueToString } from '@celo/contractkit/lib/wrappers/BaseWrapper' import { ProposalTransaction } from '@celo/contractkit/lib/wrappers/Governance' import { fetchMetadata, tryGetProxyImplementation } from '@celo/explorer/lib/sourcify' import { isValidAddress } from '@celo/utils/lib/address' import { isNativeError } from 'util/types' +import { encodeFunctionData } from 'viem' import { ExternalProposalTransactionJSON, ProposalTransactionJSON, @@ -29,6 +25,7 @@ import { isRegistryRepoint, registryRepointArgs, } from './proposals' +import { bigintReplacer } from './json-utils' /** * Builder class to construct proposals from JSON or transaction objects. @@ -56,14 +53,14 @@ export class ProposalBuilder { } /** - * Converts a Web3 transaction into a proposal transaction object. - * @param tx A Web3 transaction object to convert. + * Converts encoded function data into a proposal transaction object. + * @param data Hex-encoded function call data. * @param params Parameters for how the transaction should be executed. */ - fromWeb3tx = (tx: CeloTxObject, params: ProposalTxParams): ProposalTransaction => ({ + fromEncodedTx = (data: string, params: ProposalTxParams): ProposalTransaction => ({ value: params.value, to: params.to, - input: tx.encodeABI(), + input: data, }) /** @@ -73,38 +70,21 @@ export class ProposalBuilder { */ addProxyRepointingTx = (contract: CeloContract, newImplementationAddress: string) => { this.builders.push(async () => { - const proxy = await this.kit._web3Contracts.getContract(contract) - return this.fromWeb3tx( - setImplementationOnProxy(newImplementationAddress, this.kit.connection.web3), - { - to: proxy.options.address, - value: '0', - } - ) + const proxy = await this.kit._contracts.getContract(contract) + return this.fromEncodedTx(setImplementationOnProxy(newImplementationAddress), { + to: proxy.address, + value: '0', + }) }) } /** - * Adds a Web3 transaction to the list for proposal construction. - * @param tx A Web3 transaction object to add to the proposal. + * Adds an encoded transaction to the list for proposal construction. + * @param data Hex-encoded function call data. * @param params Parameters for how the transaction should be executed. */ - addWeb3Tx = (tx: CeloTxObject, params: ProposalTxParams) => - this.builders.push(async () => this.fromWeb3tx(tx, params)) - - /** - * Adds a Celo transaction to the list for proposal construction. - * @param tx A Celo transaction object to add to the proposal. - * @param params Optional parameters for how the transaction should be executed. - */ - addTx(tx: CeloTransactionObject, params: Partial = {}) { - const to = params.to ?? tx.defaultParams?.to - const value = params.value ?? tx.defaultParams?.value - if (!to || !value) { - throw new Error("Transaction parameters 'to' and/or 'value' not provided") - } - this.addWeb3Tx(tx.txo, { to, value: valueToString(value.toString()) }) - } + addEncodedTx = (data: string, params: ProposalTxParams) => + this.builders.push(async () => this.fromEncodedTx(data, params)) setRegistryAddition = (contract: CeloContract, address: string) => (this.registryAdditions[stripProxy(contract)] = address) @@ -116,25 +96,16 @@ export class ProposalBuilder { RegisteredContracts.includes(stripProxy(contract)) || this.getRegistryAddition(contract) !== undefined - /* - * @deprecated - use isRegistryContract - */ - isRegistered = this.isRegistryContract - lookupExternalMethodABI = async ( address: string, tx: ExternalProposalTransactionJSON ): Promise => { - const abiCoder = this.kit.connection.getAbiCoder() - const metadata = await fetchMetadata( - this.kit.connection, - this.kit.web3.utils.toChecksumAddress(address) - ) + const metadata = await fetchMetadata(this.kit.connection, toChecksumAddress(address)) const potentialABIs = metadata?.abiForMethod(tx.function) ?? [] return ( potentialABIs.find((abi) => { try { - abiCoder.encodeFunctionCall(abi, this.transformArgs(abi, tx.args)) + encodeFunctionData({ abi: [abi] as any, args: this.transformArgs(abi, tx.args) as any }) return true } catch { return false @@ -169,18 +140,13 @@ export class ProposalBuilder { methodABI = signatureToAbiDefinition(tx.function) } - const input = this.kit.connection - .getAbiCoder() - .encodeFunctionCall(methodABI, this.transformArgs(methodABI, tx.args)) + const input = encodeFunctionData({ + abi: [methodABI] as any, + args: this.transformArgs(methodABI, tx.args) as any, + }) return { input, to: tx.address, value: tx.value } } - /* - * @deprecated use buildCallToExternalContract - * - */ - buildFunctionCallToExternalContract = this.buildCallToExternalContract - transformArgs = (abi: AbiItem, args: any[]) => { if (abi.inputs?.length !== args.length) { throw new Error( @@ -211,23 +177,27 @@ export class ProposalBuilder { if (tx.function === SET_AND_INITIALIZE_IMPLEMENTATION_ABI.name && Array.isArray(tx.args[1])) { // Transform array of initialize arguments (if provided) into delegate call data - tx.args[1] = this.kit.connection - .getAbiCoder() - .encodeFunctionCall(getInitializeAbiOfImplementation(tx.contract as any), tx.args[1]) + tx.args[1] = encodeFunctionData({ + abi: [getInitializeAbiOfImplementation(tx.contract as any)] as any, + args: tx.args[1] as any, + }) } - const contract = await this.kit._web3Contracts.getContract(tx.contract, address) + const contract = await this.kit._contracts.getContract(tx.contract, address) const methodName = tx.function - const method = (contract.methods as Contract['methods'])[methodName] - if (!method) { - throw new Error(`Method ${methodName} not found on ${tx.contract}`) - } - const txo = method(...tx.args) - if (!txo) { - throw new Error(`Arguments ${tx.args} did not match ${methodName} signature`) - } - - return this.fromWeb3tx(txo, { to: address, value: tx.value }) + const abiItem = (contract.abi as AbiItem[]).find( + (item) => item.type === 'function' && item.name === methodName + ) + if (!abiItem) { + throw new Error(`Method ${methodName} not found in ABI for ${tx.contract}`) + } + const coercedArgs = abiItem.inputs ? coerceArgsForAbi(abiItem.inputs, tx.args) : tx.args + const data = encodeFunctionData({ + abi: [abiItem], + functionName: methodName, + args: coercedArgs, + }) + return this.fromEncodedTx(data, { to: address, value: tx.value }) } fromJsonTx = async ( @@ -264,7 +234,7 @@ export class ProposalBuilder { throw new Error( `Couldn't build call for transaction:\n\n${JSON.stringify( tx, - undefined, + bigintReplacer, 2 )}\n\nAt least one of the following issues must be corrected:\n${issues .map((error, index) => ` ${index + 1}. ${error}`) diff --git a/packages/sdk/governance/src/proposals.ts b/packages/sdk/governance/src/proposals.ts index a090f8a057..0ccbb3fd30 100644 --- a/packages/sdk/governance/src/proposals.ts +++ b/packages/sdk/governance/src/proposals.ts @@ -1,7 +1,13 @@ -import { ABI as GovernanceABI } from '@celo/abis/web3/Governance' -import { ABI as RegistryABI } from '@celo/abis/web3/Registry' +import { governanceABI, registryABI } from '@celo/abis' import { Address, trimLeading0x } from '@celo/base/lib/address' -import { AbiCoder, CeloTxPending, getAbiByName, parseDecodedParams } from '@celo/connect' +import { + type AbiItem, + AbiInput, + decodeParametersToObject, + getAbiByName, + parseDecodedParams, +} from '@celo/connect' +import { toChecksumAddress } from '@celo/utils/lib/address' import { CeloContract, ContractKit, REGISTRY_CONTRACT_ADDRESS } from '@celo/contractkit' import { stripProxy, suffixProxy } from '@celo/contractkit/lib/base' import { @@ -22,15 +28,17 @@ import { keccak_256 } from '@noble/hashes/sha3' import { utf8ToBytes } from '@noble/hashes/utils' import { BigNumber } from 'bignumber.js' import debugFactory from 'debug' +import { encodeAbiParameters, toFunctionSelector, type AbiParameter } from 'viem' +import { bigintReplacer } from './json-utils' export const debug = debugFactory('governance:proposals') -export const hotfixExecuteAbi = getAbiByName(GovernanceABI, 'executeHotfix') +export const hotfixExecuteAbi = getAbiByName(governanceABI as unknown as AbiItem[], 'executeHotfix') -export const hotfixToEncodedParams = (kit: ContractKit, proposal: Proposal, salt: Buffer) => - kit.connection.getAbiCoder().encodeParameters( - hotfixExecuteAbi.inputs!.map((input) => input.type), - hotfixToParams(proposal, salt) +export const hotfixToEncodedParams = (_kit: ContractKit, proposal: Proposal, salt: Buffer) => + encodeAbiParameters( + hotfixExecuteAbi.inputs!.map((input) => ({ ...input }) as AbiParameter), + hotfixToParams(proposal, salt) as any ) export const hotfixToHash = (kit: ContractKit, proposal: Proposal, salt: Buffer): Buffer => @@ -74,7 +82,9 @@ export const registryRepointArgs = ( tx: Pick ) => { if (!isRegistryRepoint(tx)) { - throw new Error(`Proposal transaction not a registry repoint:\n${JSON.stringify(tx, null, 2)}`) + throw new Error( + `Proposal transaction not a registry repoint:\n${JSON.stringify(tx, bigintReplacer, 2)}` + ) } return { name: tx.args[0] as CeloContract, @@ -82,20 +92,25 @@ export const registryRepointArgs = ( } } -const setAddressAbi = getAbiByName(RegistryABI, 'setAddressFor') +const setAddressAbi = getAbiByName(registryABI as unknown as AbiItem[], 'setAddressFor') + +const setAddressFnSelector = toFunctionSelector( + `${setAddressAbi.name}(${(setAddressAbi.inputs || []).map((i: AbiInput) => i.type).join(',')})` +) -const isRegistryRepointRaw = (abiCoder: AbiCoder, tx: ProposalTransaction) => - tx.to === REGISTRY_CONTRACT_ADDRESS && - tx.input.startsWith(abiCoder.encodeFunctionSignature(setAddressAbi)) +const isRegistryRepointRaw = (tx: ProposalTransaction) => + tx.to === REGISTRY_CONTRACT_ADDRESS && tx.input.startsWith(setAddressFnSelector) -const registryRepointRawArgs = (abiCoder: AbiCoder, tx: ProposalTransaction) => { - if (!isRegistryRepointRaw(abiCoder, tx)) { - throw new Error(`Proposal transaction not a registry repoint:\n${JSON.stringify(tx, null, 2)}`) +const registryRepointRawArgs = (tx: ProposalTransaction) => { + if (!isRegistryRepointRaw(tx)) { + throw new Error( + `Proposal transaction not a registry repoint:\n${JSON.stringify(tx, bigintReplacer, 2)}` + ) } - const params = abiCoder.decodeParameters(setAddressAbi.inputs!, trimLeading0x(tx.input).slice(8)) + const params = decodeParametersToObject(setAddressAbi.inputs!, trimLeading0x(tx.input).slice(8)) return { name: params.identifier as CeloContract, - address: params.addr, + address: params.addr as string, } } @@ -136,27 +151,26 @@ export const proposalToJSON = async ( }) ) } - const abiCoder = kit.connection.getAbiCoder() const proposalJson: ProposalTransactionJSON[] = [] for (const tx of proposal) { - const parsedTx = await blockExplorer.tryParseTx(tx as CeloTxPending) - if (parsedTx == null) { - throw new Error(`Unable to parse ${JSON.stringify(tx)} with block explorer`) + const callDetails = await blockExplorer.tryParseTxInput(tx.to!, tx.input) + if (callDetails == null) { + throw new Error(`Unable to parse ${JSON.stringify(tx, bigintReplacer)} with block explorer`) } - if (isRegistryRepointRaw(abiCoder, tx) && parsedTx.callDetails.isCoreContract) { - const args = registryRepointRawArgs(abiCoder, tx) + if (isRegistryRepointRaw(tx) && callDetails.isCoreContract) { + const args = registryRepointRawArgs(tx) await updateRegistryMapping(args.name, args.address) } const jsonTx: ProposalTransactionJSON = { - contract: parsedTx.callDetails.contract as CeloContract, - address: parsedTx.callDetails.contractAddress, - function: parsedTx.callDetails.function, - args: parsedTx.callDetails.argList, - params: parsedTx.callDetails.paramMap, - value: parsedTx.tx.value, + contract: callDetails.contract as CeloContract, + address: callDetails.contractAddress, + function: callDetails.function, + args: callDetails.argList, + params: callDetails.paramMap, + value: tx.value, } if (isProxySetFunction(jsonTx)) { @@ -165,15 +179,12 @@ export const proposalToJSON = async ( } else if (isProxySetAndInitFunction(jsonTx)) { await blockExplorer.setProxyOverride(tx.to!, jsonTx.args[0]) let initAbi - if (parsedTx.callDetails.isCoreContract) { + if (callDetails.isCoreContract) { jsonTx.contract = suffixProxy(jsonTx.contract) initAbi = getInitializeAbiOfImplementation(jsonTx.contract as any) } else { const implAddress = jsonTx.args[0] - const metadata = await fetchMetadata( - kit.connection, - kit.web3.utils.toChecksumAddress(implAddress) - ) + const metadata = await fetchMetadata(kit.connection, toChecksumAddress(implAddress)) if (metadata && metadata.abi) { initAbi = metadata?.abiForMethod('initialize')[0] } @@ -186,7 +197,7 @@ export const proposalToJSON = async ( const initArgs = trimLeading0x(jsonTx.args[1]).slice(8) const { params: initParams } = parseDecodedParams( - kit.connection.getAbiCoder().decodeParameters(initAbi.inputs!, initArgs) + decodeParametersToObject(initAbi.inputs!, initArgs) ) jsonTx.params![`initialize@${initSig}`] = initParams } diff --git a/packages/sdk/metadata-claims/src/account.test.ts b/packages/sdk/metadata-claims/src/account.test.ts index 8ac18cb7e4..3e4bd4d5c4 100644 --- a/packages/sdk/metadata-claims/src/account.test.ts +++ b/packages/sdk/metadata-claims/src/account.test.ts @@ -1,4 +1,4 @@ -import { newKitFromWeb3 } from '@celo/contractkit' +import { newKitFromProvider } from '@celo/contractkit' import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test' import { ACCOUNT_ADDRESSES, ACCOUNT_PRIVATE_KEYS } from '@celo/dev-utils/test-accounts' import { privateKeyToAddress, privateKeyToPublicKey } from '@celo/utils/lib/address' @@ -9,8 +9,8 @@ import { IdentityMetadataWrapper } from './metadata' import { AccountMetadataSignerGetters } from './types' import { verifyClaim } from './verify' -testWithAnvilL2('Account claims', (web3) => { - const kit = newKitFromWeb3(web3) +testWithAnvilL2('Account claims', (provider) => { + const kit = newKitFromProvider(provider) const address = ACCOUNT_ADDRESSES[0] const otherAddress = ACCOUNT_ADDRESSES[1] @@ -66,10 +66,16 @@ testWithAnvilL2('Account claims', (web3) => { const myUrl = 'https://www.example.com/' const accounts = await kit.contracts.getAccounts() - await accounts.createAccount().sendAndWaitForReceipt({ from: address }) - await accounts.setMetadataURL(myUrl).sendAndWaitForReceipt({ from: address, gas: 0 }) - await accounts.createAccount().sendAndWaitForReceipt({ from: otherAddress }) - await accounts.setMetadataURL(myUrl).sendAndWaitForReceipt({ from: otherAddress, gas: 0 }) + const publicClient = kit.connection.viemClient + + let hash = await accounts.createAccount({ from: address }) + await publicClient.waitForTransactionReceipt({ hash }) + hash = await accounts.setMetadataURL(myUrl, { from: address }) + await publicClient.waitForTransactionReceipt({ hash }) + hash = await accounts.createAccount({ from: otherAddress }) + await publicClient.waitForTransactionReceipt({ hash }) + hash = await accounts.setMetadataURL(myUrl, { from: otherAddress }) + await publicClient.waitForTransactionReceipt({ hash }) IdentityMetadataWrapper.fetchFromURL = () => Promise.resolve(otherMetadata) @@ -93,9 +99,10 @@ testWithAnvilL2('Account claims', (web3) => { describe('when the metadata URL of the other account has not been set', () => { beforeEach(async () => { - await (await kit.contracts.getAccounts()) - .setMetadataURL('') - .sendAndWaitForReceipt({ from: otherAddress, gas: 0 }) + const h = await (await kit.contracts.getAccounts()).setMetadataURL('', { + from: otherAddress, + }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: h }) }) it('indicates that the metadata url could not be retrieved', async () => { diff --git a/packages/sdk/metadata-claims/src/domain.test.ts b/packages/sdk/metadata-claims/src/domain.test.ts index 02b28cdcf9..91a3760080 100644 --- a/packages/sdk/metadata-claims/src/domain.test.ts +++ b/packages/sdk/metadata-claims/src/domain.test.ts @@ -1,5 +1,5 @@ import { NULL_ADDRESS } from '@celo/base' -import { newKitFromWeb3 } from '@celo/contractkit' +import { newKitFromProvider } from '@celo/contractkit' import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test' import { ACCOUNT_ADDRESSES } from '@celo/dev-utils/test-accounts' import { NativeSigner, Signer, verifySignature } from '@celo/utils/lib/signatureUtils' @@ -8,8 +8,8 @@ import { IdentityMetadataWrapper } from './metadata' import type { AccountMetadataSignerGetters } from './types' import { verifyDomainRecord } from './verify' -testWithAnvilL2('Domain claims', (web3) => { - const kit = newKitFromWeb3(web3) +testWithAnvilL2('Domain claims', (provider) => { + const kit = newKitFromProvider(provider) const address = ACCOUNT_ADDRESSES[0] const secondAddress = ACCOUNT_ADDRESSES[1] diff --git a/packages/sdk/metadata-claims/src/metadata.test.ts b/packages/sdk/metadata-claims/src/metadata.test.ts index 6fb4a2cf64..e09f611cad 100644 --- a/packages/sdk/metadata-claims/src/metadata.test.ts +++ b/packages/sdk/metadata-claims/src/metadata.test.ts @@ -1,4 +1,4 @@ -import { newKitFromWeb3 } from '@celo/contractkit' +import { newKitFromProvider } from '@celo/contractkit' import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test' import { ACCOUNT_ADDRESSES } from '@celo/dev-utils/test-accounts' import { Address } from '@celo/utils/lib/address' @@ -7,8 +7,8 @@ import { Claim, createNameClaim, createRpcUrlClaim } from './claim' import { ClaimTypes, IdentityMetadataWrapper } from './metadata' import { now } from './types' -testWithAnvilL2('Metadata', (web3) => { - const kit = newKitFromWeb3(web3) +testWithAnvilL2('Metadata', (provider) => { + const kit = newKitFromProvider(provider) const address = ACCOUNT_ADDRESSES[0] const otherAddress = ACCOUNT_ADDRESSES[1] @@ -38,7 +38,8 @@ testWithAnvilL2('Metadata', (web3) => { const validatorSigner = ACCOUNT_ADDRESSES[3] const attestationSigner = ACCOUNT_ADDRESSES[4] console.warn('Creating account', address) - await accounts.createAccount().send({ from: address }) + const hash = await accounts.createAccount({ from: address }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash }) const testSigner = async ( signer: Address, action: string, @@ -50,26 +51,27 @@ testWithAnvilL2('Metadata', (web3) => { if (action === 'vote') { const fees = await kit.connection.setFeeMarketGas({}) console.warn('testSigner vote', address, fees) - await (await accounts.authorizeVoteSigner(signer, pop)).sendAndWaitForReceipt({ + const h = await accounts.authorizeVoteSigner(signer, pop, { from: address, gas: 13000000, maxFeePerGas: fees.maxFeePerGas, }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: h }) } else if (action === 'validator') { console.warn('testSigner validator', address) - await ( - await accounts.authorizeValidatorSigner(signer, pop, validator) - ).sendAndWaitForReceipt({ + const h = await accounts.authorizeValidatorSigner(signer, pop, validator, { from: address, gas: 13000000, }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: h }) } else if (action === 'attestation') { console.warn('testSigner attestation', address) - await (await accounts.authorizeAttestationSigner(signer, pop)).sendAndWaitForReceipt({ + const h = await accounts.authorizeAttestationSigner(signer, pop, { from: address, gas: 13000000, }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: h }) } console.warn('testSigner addClaim', address) diff --git a/packages/sdk/transactions-uri/package.json b/packages/sdk/transactions-uri/package.json index f74a6414e4..47318927eb 100644 --- a/packages/sdk/transactions-uri/package.json +++ b/packages/sdk/transactions-uri/package.json @@ -27,12 +27,10 @@ "dependencies": { "@celo/base": "^7.0.3", "@celo/connect": "^7.0.0", - "@types/bn.js": "^5.1.0", "@types/debug": "^4.1.5", "@types/qrcode": "^1.3.4", - "bn.js": "^5.1.0", "qrcode": "1.4.4", - "web3-eth-abi": "1.10.4" + "viem": "^2.33.2" }, "devDependencies": { "@celo/contractkit": "^10.0.2-alpha.0", diff --git a/packages/sdk/transactions-uri/src/tx-uri.test.ts b/packages/sdk/transactions-uri/src/tx-uri.test.ts index 587c7daf29..72dc9e21bb 100644 --- a/packages/sdk/transactions-uri/src/tx-uri.test.ts +++ b/packages/sdk/transactions-uri/src/tx-uri.test.ts @@ -1,9 +1,9 @@ import { CeloTx } from '@celo/connect' -import { CeloContract, newKitFromWeb3 } from '@celo/contractkit' +import { CeloContract, newKitFromProvider } from '@celo/contractkit' import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test' import { buildUri, parseUri } from './tx-uri' -testWithAnvilL2('URI utils', (web3) => { +testWithAnvilL2('URI utils', (provider) => { const recipient = '0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' const value = '100' @@ -19,13 +19,13 @@ testWithAnvilL2('URI utils', (web3) => { let lockGoldUri: string let lockGoldTx: CeloTx - const kit = newKitFromWeb3(web3) + const kit = newKitFromProvider(provider) beforeAll(async () => { const stableTokenAddr = await kit.registry.addressFor(CeloContract.StableToken) stableTokenTransferUri = `celo:${stableTokenAddr}/transfer(address,uint256)?args=[${recipient},${value}]` const stableToken = await kit.contracts.getStableToken() - const transferData = stableToken.transfer(recipient, value).txo.encodeABI() + const transferData = stableToken.encodeFunctionData('transfer', [recipient, value]) stableTokenTransferTx = { to: stableTokenAddr, data: transferData, @@ -34,7 +34,7 @@ testWithAnvilL2('URI utils', (web3) => { const lockedGoldAddr = await kit.registry.addressFor(CeloContract.LockedCelo) lockGoldUri = `celo:${lockedGoldAddr}/lock()?value=${value}` const lockedGold = await kit.contracts.getLockedGold() - const lockData = lockedGold.lock().txo.encodeABI() + const lockData = lockedGold.encodeFunctionData('lock', []) lockGoldTx = { to: lockedGoldAddr, data: lockData, diff --git a/packages/sdk/transactions-uri/src/tx-uri.ts b/packages/sdk/transactions-uri/src/tx-uri.ts index 1374f6cbdb..76875e0925 100644 --- a/packages/sdk/transactions-uri/src/tx-uri.ts +++ b/packages/sdk/transactions-uri/src/tx-uri.ts @@ -1,12 +1,9 @@ import { trimLeading0x } from '@celo/base/lib/address' import { zeroRange } from '@celo/base/lib/collections' -import { AbiCoder, CeloTx } from '@celo/connect' -import BN from 'bn.js' +import { CeloTx } from '@celo/connect' +import { decodeAbiParameters, encodeAbiParameters, toFunctionHash, type AbiParameter } from 'viem' import qrcode from 'qrcode' import querystring from 'querystring' -import abiWeb3 from 'web3-eth-abi' - -const abi = abiWeb3 as unknown as AbiCoder // see https://solidity.readthedocs.io/en/v0.5.3/abi-spec.html#function-selector-and-argument-encoding const ABI_TYPE_REGEX = '(u?int(8|16|32|64|128|256)|address|bool|bytes(4|32)?|string)(\\[\\])?' @@ -15,7 +12,7 @@ const ADDRESS_REGEX_STR = '(?
0x[a-fA-F0-9]{40})' const CHAIN_ID_REGEX = '(?\\d+)' const TX_PARAMS = ['feeCurrency', 'gas', 'gasPrice', 'value'] const PARAM_REGEX = `(${TX_PARAMS.join('|')})=\\w+` -const ARGS_REGEX = 'args=\\[(,?\\w+)*\\]' +const ARGS_REGEX = 'args=\\[(\\w+(,\\w+)*)?\\]' const QUERY_REGEX = `(?(&?(${PARAM_REGEX}|${ARGS_REGEX}))+)` // URI scheme mostly borrowed from https://github.com/ethereum/EIPs/blob/master/EIPS/eip-681.md @@ -42,14 +39,17 @@ export function parseUri(uri: string): CeloTx { const parsedQuery = querystring.parse(namedGroups.query) if (namedGroups.function !== undefined) { - const functionSig = abi.encodeFunctionSignature(namedGroups.function) + const functionSig = toFunctionHash(namedGroups.function).slice(0, 10) tx.data = functionSig if (namedGroups.inputTypes != null && namedGroups.inputTypes !== '') { const abiTypes = namedGroups.inputTypes.split(',') const rawArgs = (parsedQuery.args || '[]') as string const builtArgs = rawArgs.slice(1, rawArgs.length - 1).split(',') - const callSig = abi.encodeParameters(abiTypes, builtArgs) + const callSig = encodeAbiParameters( + abiTypes.map((t: string) => ({ type: t }) as AbiParameter), + builtArgs as any + ) tx.data += trimLeading0x(callSig) } @@ -79,7 +79,7 @@ export function buildUri(tx: CeloTx, functionName?: string, abiTypes: string[] = } const functionSelector = `${functionName}(${abiTypes.join(',')})` - const functionSig = trimLeading0x(abi.encodeFunctionSignature(functionSelector)) + const functionSig = trimLeading0x(toFunctionHash(functionSelector).slice(0, 10)) const txData = trimLeading0x(tx.data) const funcEncoded = txData.slice(0, 8) @@ -91,8 +91,11 @@ export function buildUri(tx: CeloTx, functionName?: string, abiTypes: string[] = if (txData.length > 8) { const argsEncoded = txData.slice(8) - const decoded = abi.decodeParameters(abiTypes, argsEncoded) - functionArgs = zeroRange(decoded.__length__).map((idx) => decoded[idx].toLowerCase()) + const decoded = decodeAbiParameters( + abiTypes.map((t: string) => ({ type: t }) as AbiParameter), + `0x${argsEncoded}` as `0x${string}` + ) + functionArgs = zeroRange(decoded.length).map((idx) => String(decoded[idx]).toLowerCase()) } } @@ -103,7 +106,7 @@ export function buildUri(tx: CeloTx, functionName?: string, abiTypes: string[] = uri += `args=[${functionArgs.join(',')}]` } const params = txQueryParams as { [key: string]: string } - if (txQueryParams.value instanceof BN) { + if (txQueryParams.value != null && typeof txQueryParams.value !== 'string') { params.value = txQueryParams.value.toString() } uri += querystring.stringify({ ...params }) diff --git a/packages/sdk/wallets/wallet-base/package.json b/packages/sdk/wallets/wallet-base/package.json index 6838a18969..a9e7517374 100644 --- a/packages/sdk/wallets/wallet-base/package.json +++ b/packages/sdk/wallets/wallet-base/package.json @@ -33,14 +33,11 @@ "@celo/base": "^7.0.3", "@celo/connect": "^7.0.0", "@celo/utils": "^8.0.3", - "@ethereumjs/rlp": "^5.0.2", - "@ethereumjs/util": "8.0.5", "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", "@types/debug": "^4.1.5", "bignumber.js": "^9.0.0", - "debug": "^4.1.1", - "web3": "1.10.4" + "debug": "^4.1.1" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts index 74e22dc6cf..e02afc676f 100644 --- a/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts @@ -1,10 +1,9 @@ import { CeloTx } from '@celo/connect' import { normalizeAddressWith0x, privateKeyToAddress } from '@celo/utils/lib/address' import { hexToBytes } from '@noble/hashes/utils' -import { parseTransaction, serializeTransaction } from 'viem' +import { parseEther, parseTransaction, serializeTransaction } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { celo } from 'viem/chains' -import Web3 from 'web3' import { extractSignature, getSignerFromTxEIP2718TX, @@ -32,7 +31,7 @@ describe('rlpEncodedTx', () => { from: '0x1daf825EB5C0D9d9FeC33C444e413452A08e04A6', to: '0x43d72ff17701b2da814620735c39c620ce0ea4a1', chainId: 42220, - value: Web3.utils.toWei('0', 'ether'), + value: parseEther('0').toString(), nonce: 619, gas: '504830', gasPrice: '5000000000', @@ -69,7 +68,7 @@ describe('rlpEncodedTx', () => { from: ACCOUNT_ADDRESS1, to: ACCOUNT_ADDRESS1, chainId: 2, - value: Web3.utils.toWei('1000', 'ether'), + value: parseEther('1000').toString(), nonce: 0, maxFeePerGas: '10', maxPriorityFeePerGas: '99', @@ -81,7 +80,7 @@ describe('rlpEncodedTx', () => { it('throws an error', () => { const transaction = { ...eip1559Transaction, - maxFeePerGas: Web3.utils.toBN('-5'), + maxFeePerGas: BigInt('-5'), } expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` @@ -92,7 +91,7 @@ describe('rlpEncodedTx', () => { it('throws an error', () => { const transaction = { ...eip1559Transaction, - maxPriorityFeePerGas: Web3.utils.toBN('-5'), + maxPriorityFeePerGas: BigInt('-5'), } expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` @@ -160,7 +159,7 @@ describe('rlpEncodedTx', () => { const CIP66Transaction = { ...eip1559Transaction, feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631', - maxFeeInFeeCurrency: Web3.utils.toBN('100000000010181646104615494635153636353810897'), + maxFeeInFeeCurrency: BigInt('100000000010181646104615494635153636353810897'), } as const const result = rlpEncodedTx(CIP66Transaction) expect(result).toMatchInlineSnapshot(` @@ -242,7 +241,7 @@ describe('rlpEncodedTx', () => { from: ACCOUNT_ADDRESS1, to: ACCOUNT_ADDRESS1, chainId: 2, - value: Web3.utils.toWei('1000', 'ether'), + value: parseEther('1000').toString(), nonce: 0, maxFeePerGas: '1000', maxPriorityFeePerGas: '99', @@ -279,7 +278,7 @@ describe('rlpEncodedTx', () => { from: ACCOUNT_ADDRESS1, to: ACCOUNT_ADDRESS1, chainId: 2, - value: Web3.utils.toWei('1000', 'ether'), + value: parseEther('1000').toString(), nonce: 0, maxFeePerGas: '1000', maxPriorityFeePerGas: '99', @@ -521,7 +520,7 @@ describe('isPriceToLow', () => { expect( isPriceToLow({ maxFeePerGas: 1_000_000_000, - maxPriorityFeePerGas: Web3.utils.toBN('50000000000000'), + maxPriorityFeePerGas: BigInt('50000000000000'), gasPrice: undefined, }) ).toBe(false) @@ -529,7 +528,7 @@ describe('isPriceToLow', () => { test('gasPrice is positive', () => { expect( isPriceToLow({ - gasPrice: Web3.utils.toBN('50000000000000'), + gasPrice: BigInt('50000000000000'), }) ).toBe(false) }) @@ -619,7 +618,7 @@ describe('extractSignature', () => { }) it('fails when length is empty', () => { expect(() => extractSignature('0x')).toThrowErrorMatchingInlineSnapshot( - `"Invalid byte sequence"` + `"@extractSignature: provided transaction has 0 elements but ethereum-legacy txs with a signature have 9 {}"` ) }) }) @@ -663,7 +662,7 @@ describe('stringNumberOrBNToHex', () => { expect(stringNumberOrBNToHex(123)).toEqual('0x7b') }) test('BN', () => { - const biggie = Web3.utils.toBN('123') + const biggie = BigInt('123') expect(stringNumberOrBNToHex(biggie)).toEqual('0x7b') }) test('bigint', () => { diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.ts index 5f65ebc178..6255598f71 100644 --- a/packages/sdk/wallets/wallet-base/src/signing-utils.ts +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.ts @@ -23,13 +23,12 @@ import { import { publicKeyToAddress } from '@celo/utils/lib/address' import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' import { parseSignatureWithoutPrefix } from '@celo/utils/lib/signatureUtils' -import * as RLP from '@ethereumjs/rlp' -import * as ethUtil from '@ethereumjs/util' +import { fromRlp, toRlp, type Hex as ViemHex } from 'viem' import { secp256k1 } from '@noble/curves/secp256k1' import { keccak_256 } from '@noble/hashes/sha3' import { bytesToHex, hexToBytes } from '@noble/hashes/utils' +import { publicKeyToAddress as viemPublicKeyToAddress } from 'viem/accounts' import debugFactory from 'debug' -import Web3 from 'web3' // TODO try to do this without web3 direct type OldTransactionTypes = 'celo-legacy' | 'cip42' | TransactionTypes type LegacyCeloTx = Omit & { @@ -37,7 +36,6 @@ type LegacyCeloTx = Omit & { } type LegacyCeloTxWithSig = WithSig -const { ecrecover, fromRpcSig, hashPersonalMessage, toBuffer } = ethUtil const debug = debugFactory('wallet-base:tx:sign') // Original code taken from @@ -51,8 +49,8 @@ export const thirtyTwo: number = 32 const Y_PARITY_EIP_2098 = 27 -function rlpEncodeHex(value: RLP.Input): StrongAddress { - return ensureLeading0x(Buffer.from(RLP.encode(value)).toString('hex')) +function rlpEncodeHex(value: unknown[]): StrongAddress { + return toRlp(value as any) as StrongAddress } function isNullOrUndefined(value: any): boolean { @@ -112,9 +110,7 @@ function signatureFormatter( } } -export function stringNumberOrBNToHex( - num?: number | string | ReturnType | bigint -): Hex { +export function stringNumberOrBNToHex(num?: number | string | bigint): Hex { if (typeof num === 'string' || typeof num === 'number' || num === undefined) { return stringNumberToHex(num) } else { @@ -129,7 +125,7 @@ function stringNumberToHex(num?: number | string | bigint): StrongAddress { if (typeof num === 'bigint') { return makeEven(`0x` + num.toString(16)) as StrongAddress } - return makeEven(Web3.utils.numberToHex(num)) as StrongAddress + return makeEven(ensureLeading0x(Number(num).toString(16))) as StrongAddress } export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx { assertSerializableTX(tx) @@ -327,7 +323,7 @@ function isLessThanZero(value: CeloTx['gasPrice']) { case 'number': return Number(value) < 0 default: - return value?.lt(Web3.utils.toBN(0)) || false + return typeof value === 'bigint' ? value < BigInt(0) : false } } @@ -399,12 +395,12 @@ export async function encodeTransaction( } // new types have prefix but legacy does not -function prefixAwareRLPDecode(rlpEncode: string, type: OldTransactionTypes) { +function prefixAwareRLPDecode(rlpEncode: string, type: OldTransactionTypes): Uint8Array[] { if (type === 'celo-legacy' || type === 'ethereum-legacy') { - return RLP.decode(rlpEncode) + return fromRlp(rlpEncode as ViemHex, 'bytes') as Uint8Array[] } - return RLP.decode(`0x${rlpEncode.slice(4)}`) + return fromRlp(`0x${rlpEncode.slice(4)}` as ViemHex, 'bytes') as Uint8Array[] } function correctLengthOf(type: OldTransactionTypes, includeSig: boolean = true) { @@ -482,30 +478,30 @@ export function recoverTransaction(rawTx: string): [CeloTx, string] { function getPublicKeyofSignerFromTx(transactionArray: Uint8Array[], type: OldTransactionTypes) { // this needs to be 10 for cip64, 12 for cip42 and eip1559 const base = transactionArray.slice(0, correctLengthOf(type, false)) - const message = concatHex([TxTypeToPrefix[type], rlpEncodeHex(base).slice(2)]) + const message = concatHex([ + TxTypeToPrefix[type], + rlpEncodeHex(base as unknown as unknown[]).slice(2), + ]) const msgHash = keccak_256(hexToBytes(trimLeading0x(message))) const { v, r, s } = extractSignatureFromDecoded(transactionArray) try { - return ecrecover( - Buffer.from(msgHash), - v === '0x' || v === undefined ? BigInt(0) : BigInt(1), - toBuffer(r), - toBuffer(s) - ) + const recovery = v === '0x' || v === undefined ? 0 : 1 + const sig = new secp256k1.Signature(BigInt(r), BigInt(s)).addRecoveryBit(recovery) + return Buffer.from(sig.recoverPublicKey(msgHash).toRawBytes(false)) } catch (e: any) { throw new Error(e) } } export function getSignerFromTxEIP2718TX(serializedTransaction: string): string { - const transactionArray = RLP.decode(`0x${serializedTransaction.slice(4)}`) + const transactionArray = fromRlp(`0x${serializedTransaction.slice(4)}` as ViemHex, 'bytes') const signer = getPublicKeyofSignerFromTx( transactionArray as Uint8Array[], determineTXType(serializedTransaction) ) - return publicKeyToAddress(signer.toString('hex')) + return viemPublicKeyToAddress(`0x${Buffer.from(signer).toString('hex')}` as `0x${string}`) } export function determineTXType(serializedTransaction: string): OldTransactionTypes { @@ -523,12 +519,11 @@ export function determineTXType(serializedTransaction: string): OldTransactionTy // it is one of the legacy types (Celo or Ethereum), to differentiate between // legacy tx types we have to check the numberof fields - const rawValues = RLP.decode(serializedTransaction) + const rawValues = fromRlp(serializedTransaction as ViemHex, 'bytes') const length = rawValues.length return correctLengthOf('celo-legacy') === length ? 'celo-legacy' : 'ethereum-legacy' } - function vrsForRecovery(vRaw: string, r: string, s: string) { const v = vRaw === '0x' || hexToNumber(vRaw) === 0 || hexToNumber(vRaw) === 27 @@ -724,7 +719,7 @@ function recoverTransactionEIP1559(serializedTransaction: StrongAddress): [CeloT } function recoverCeloLegacy(serializedTransaction: StrongAddress): [CeloTx, string] { - const rawValues = RLP.decode(serializedTransaction) as Uint8Array[] + const rawValues = fromRlp(serializedTransaction as ViemHex, 'bytes') as Uint8Array[] debug('signing-utils@recoverTransaction: values are %s', rawValues) const recovery = handleNumber(rawValues[9]) const chainId = (recovery - 35) >> 1 @@ -765,7 +760,7 @@ function recoverCeloLegacy(serializedTransaction: StrongAddress): [CeloTx, strin } function recoverEthereumLegacy(serializedTransaction: StrongAddress): [CeloTx, string] { - const rawValues = RLP.decode(serializedTransaction) as Uint8Array[] + const rawValues = fromRlp(serializedTransaction as ViemHex, 'bytes') as Uint8Array[] debug('signing-utils@recoverTransaction: values are %s', rawValues) const recovery = handleNumber(rawValues[6]) const chainId = (recovery - 35) >> 1 @@ -802,12 +797,29 @@ function recoverEthereumLegacy(serializedTransaction: StrongAddress): [CeloTx, s } export function recoverMessageSigner(signingDataHex: string, signedData: string): string { - const dataBuff = toBuffer(signingDataHex) - const msgHashBuff = hashPersonalMessage(dataBuff) - const signature = fromRpcSig(signedData) - - const publicKey = ecrecover(msgHashBuff, signature.v, signature.r, signature.s) - const address = publicKeyToAddress(publicKey.toString('hex')) + const dataBytes = hexToBytes(trimLeading0x(signingDataHex)) + // hashPersonalMessage equivalent: keccak256("\x19Ethereum Signed Message:\n" + len + data) + const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${dataBytes.length}`) + const combined = new Uint8Array(prefix.length + dataBytes.length) + combined.set(prefix) + combined.set(dataBytes, prefix.length) + const msgHash = keccak_256(combined) + + // fromRpcSig equivalent + const trimmedSig = trimLeading0x(signedData) + const rBytes = hexToBytes(trimmedSig.slice(0, 64)) + const sBytes = hexToBytes(trimmedSig.slice(64, 128)) + let v = parseInt(trimmedSig.slice(128, 130), 16) + if (v < 27) v += 27 + + const sig = new secp256k1.Signature( + BigInt(ensureLeading0x(Buffer.from(rBytes).toString('hex'))), + BigInt(ensureLeading0x(Buffer.from(sBytes).toString('hex'))) + ).addRecoveryBit(v - 27) + const publicKey = sig.recoverPublicKey(msgHash).toRawBytes(false) + const address = viemPublicKeyToAddress( + `0x${Buffer.from(publicKey).toString('hex')}` as `0x${string}` + ) return ensureLeading0x(address) } @@ -816,7 +828,7 @@ export function verifyEIP712TypedDataSigner( signedData: string, expectedAddress: string ): boolean { - const dataHex = ethUtil.bufferToHex(generateTypedDataHash(typedData)) + const dataHex = ensureLeading0x(Buffer.from(generateTypedDataHash(typedData)).toString('hex')) return verifySignatureWithoutPrefix(dataHex, signedData, expectedAddress) } diff --git a/packages/sdk/wallets/wallet-base/src/wallet-base.ts b/packages/sdk/wallets/wallet-base/src/wallet-base.ts index cfc04b8136..08ff4446ea 100644 --- a/packages/sdk/wallets/wallet-base/src/wallet-base.ts +++ b/packages/sdk/wallets/wallet-base/src/wallet-base.ts @@ -1,7 +1,7 @@ import { isHexString, normalizeAddressWith0x } from '@celo/base/lib/address' import { Address, CeloTx, EncodedTransaction, ReadOnlyWallet, Signer } from '@celo/connect' import { EIP712TypedData } from '@celo/utils/lib/sign-typed-data-utils' -import * as ethUtil from '@ethereumjs/util' +import { ensureLeading0x } from '@celo/base/lib/address' import { chainIdTransformationForSigning, encodeTransaction, rlpEncodedTx } from './signing-utils' type addInMemoryAccount = (privateKey: string) => void @@ -109,7 +109,10 @@ export abstract class WalletBase implements ReadOnlyWall const signer = this.getSigner(address) const sig = await signer.signPersonalMessage(data) - return ethUtil.toRpcSig(BigInt(sig.v), sig.r, sig.s) + const rHex = Buffer.from(sig.r).toString('hex').padStart(64, '0') + const sHex = Buffer.from(sig.s).toString('hex').padStart(64, '0') + const vHex = (sig.v >= 27 ? sig.v - 27 : sig.v).toString(16).padStart(2, '0') + return ensureLeading0x(rHex + sHex + vHex) } /** @@ -126,7 +129,10 @@ export abstract class WalletBase implements ReadOnlyWall const signer = this.getSigner(address) const sig = await signer.signTypedData(typedData) - return ethUtil.toRpcSig(BigInt(sig.v), sig.r, sig.s) + const rHex = Buffer.from(sig.r).toString('hex').padStart(64, '0') + const sHex = Buffer.from(sig.s).toString('hex').padStart(64, '0') + const vHex = (sig.v >= 27 ? sig.v - 27 : sig.v).toString(16).padStart(2, '0') + return ensureLeading0x(rHex + sHex + vHex) } protected getSigner(address: string): TSigner { diff --git a/packages/sdk/wallets/wallet-hsm-aws/package.json b/packages/sdk/wallets/wallet-hsm-aws/package.json index 81b7865bb3..9bcb7e4c07 100644 --- a/packages/sdk/wallets/wallet-hsm-aws/package.json +++ b/packages/sdk/wallets/wallet-hsm-aws/package.json @@ -30,7 +30,6 @@ "@celo/wallet-base": "^8.0.3", "@celo/wallet-hsm": "^8.0.3", "@celo/wallet-remote": "^8.0.3", - "@ethereumjs/util": "8.0.5", "@types/debug": "^4.1.5", "@types/secp256k1": "^4.0.0", "aws-sdk": "^2.705.0", @@ -44,7 +43,7 @@ "@noble/hashes": "1.3.3", "@types/debug": "^4.1.12", "dotenv": "^8.2.0", - "web3": "1.10.4" + "viem": "^2.0.0" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-signer.ts b/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-signer.ts index a81fe40b82..9df55a7a44 100644 --- a/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-signer.ts +++ b/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-signer.ts @@ -11,7 +11,7 @@ import { recoverKeyIndex, thirtyTwo, } from '@celo/wallet-hsm' -import * as ethUtil from '@ethereumjs/util' +import { keccak_256 } from '@noble/hashes/sha3' import { KMS } from 'aws-sdk' import { BigNumber } from 'bignumber.js' @@ -82,8 +82,12 @@ export class AwsHsmSigner implements Signer { } async signPersonalMessage(data: string): Promise { - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) as Buffer + const dataBytes = Buffer.from(trimLeading0x(ensureLeading0x(data)), 'hex') + const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${dataBytes.length}`) + const combined = new Uint8Array(prefix.length + dataBytes.length) + combined.set(prefix) + combined.set(dataBytes, prefix.length) + const msgHashBuff = Buffer.from(keccak_256(combined)) const { v, r, s } = await this.sign(msgHashBuff) return { diff --git a/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-wallet.test.ts b/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-wallet.test.ts index 7cec28d841..54ab4e1f32 100644 --- a/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-wallet.test.ts +++ b/packages/sdk/wallets/wallet-hsm-aws/src/aws-hsm-wallet.test.ts @@ -8,10 +8,10 @@ import { import { verifySignature } from '@celo/utils/lib/signatureUtils' import { recoverTransaction, verifyEIP712TypedDataSigner } from '@celo/wallet-base' import { asn1FromPublicKey } from '@celo/wallet-hsm' -import * as ethUtil from '@ethereumjs/util' +// ethUtil removed — using @noble/curves/secp256k1 instead import { secp256k1 } from '@noble/curves/secp256k1' import { BigNumber } from 'bignumber.js' -import Web3 from 'web3' +import { parseEther } from 'viem' import { AwsHsmWallet } from './aws-hsm-wallet' require('dotenv').config() @@ -120,7 +120,9 @@ describe('AwsHsmWallet class', () => { throw new Error(`Key 'arn:aws:kms:123:key/${KeyId}' does not exist`) } const privateKey = keys.get(KeyId) - const pubKey = ethUtil.privateToPublic(ethUtil.toBuffer(privateKey)) + const pubKey = Buffer.from( + secp256k1.getPublicKey(trimLeading0x(privateKey!), false).subarray(1) + ) const temp = new BigNumber(ensureLeading0x(pubKey.toString('hex'))) const asn1Key = asn1FromPublicKey(temp) return { PublicKey: new Uint8Array(asn1Key) } @@ -174,7 +176,7 @@ describe('AwsHsmWallet class', () => { from: unknownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 0, gas: '10', gasPrice: '99', @@ -231,7 +233,7 @@ describe('AwsHsmWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 0, gas: '10', gasPrice: '99', @@ -257,7 +259,7 @@ describe('AwsHsmWallet class', () => { from: await wallet.getAddressFromKeyId(knownKey), to: ACCOUNT_ADDRESS2, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 65, gas: '10', gasPrice: '99', diff --git a/packages/sdk/wallets/wallet-hsm-azure/package.json b/packages/sdk/wallets/wallet-hsm-azure/package.json index b5c2ec1655..0db2268380 100644 --- a/packages/sdk/wallets/wallet-hsm-azure/package.json +++ b/packages/sdk/wallets/wallet-hsm-azure/package.json @@ -34,7 +34,6 @@ "@celo/wallet-base": "^8.0.3", "@celo/wallet-hsm": "^8.0.3", "@celo/wallet-remote": "^8.0.3", - "@ethereumjs/util": "8.0.5", "@types/secp256k1": "^4.0.0", "bignumber.js": "^9.0.0", "debug": "^4.1.1" @@ -45,8 +44,7 @@ "@noble/curves": "1.3.0", "@noble/hashes": "1.3.3", "@types/debug": "^4.1.12", - "dotenv": "^8.2.0", - "web3": "1.10.4" + "dotenv": "^8.2.0" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-signer.ts b/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-signer.ts index b26486dc20..695ba196f9 100644 --- a/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-signer.ts +++ b/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-signer.ts @@ -2,7 +2,7 @@ import { ensureLeading0x, trimLeading0x } from '@celo/base/lib/address' import { RLPEncodedTx, Signer } from '@celo/connect' import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' import { getHashFromEncoded } from '@celo/wallet-base' -import * as ethUtil from '@ethereumjs/util' +import { keccak_256 } from '@noble/hashes/sha3' import { AzureKeyVaultClient } from './azure-key-vault-client' /** @@ -37,12 +37,13 @@ export class AzureHSMSigner implements Signer { } async signPersonalMessage(data: string): Promise<{ v: number; r: Buffer; s: Buffer }> { - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) - const signature = await AzureHSMSigner.keyVaultClient.signMessage( - Buffer.from(msgHashBuff), - this.keyName - ) + const dataBytes = Buffer.from(trimLeading0x(ensureLeading0x(data)), 'hex') + const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${dataBytes.length}`) + const combined = new Uint8Array(prefix.length + dataBytes.length) + combined.set(prefix) + combined.set(dataBytes, prefix.length) + const msgHashBuff = Buffer.from(keccak_256(combined)) + const signature = await AzureHSMSigner.keyVaultClient.signMessage(msgHashBuff, this.keyName) // Recovery ID should be a byte prefix // https://bitcoin.stackexchange.com/questions/38351/ecdsa-v-r-s-what-is-v const sigV = signature.v + 27 diff --git a/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.test.ts b/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.test.ts index 13b4717a10..08c974291c 100644 --- a/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.test.ts +++ b/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.test.ts @@ -9,9 +9,8 @@ import { import { verifySignature } from '@celo/utils/lib/signatureUtils' import { recoverTransaction, verifyEIP712TypedDataSigner } from '@celo/wallet-base' import { Signature, publicKeyPrefix } from '@celo/wallet-hsm' -import * as ethUtil from '@ethereumjs/util' +import { secp256k1 } from '@noble/curves/secp256k1' import { BigNumber } from 'bignumber.js' -import Web3 from 'web3' import { AzureHSMWallet } from './azure-hsm-wallet' // Env var should hold service principal credentials @@ -120,7 +119,7 @@ describe('AzureHSMWallet class', () => { const privKey = keyVaultAddresses.get(keyName)!.privateKey const pubKey = Buffer.concat([ Buffer.from(new Uint8Array([publicKeyPrefix])), - ethUtil.privateToPublic(ethUtil.toBuffer(privKey)), + Buffer.from(secp256k1.getPublicKey(trimLeading0x(privKey), false).subarray(1)), ]) return new BigNumber(ensureLeading0x(pubKey.toString('hex'))) }, @@ -128,10 +127,14 @@ describe('AzureHSMWallet class', () => { if (keyVaultAddresses.has(keyName)) { const trimmedKey = trimLeading0x(keyVaultAddresses.get(keyName)!.privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(message, pkBuffer) - // Azure HSM doesn't add the byte prefix (+27) while ecsign does - // Subtract 27 to properly mock the HSM signer - return new Signature(Number(signature.v) - 27, signature.r, signature.s) + const signature = secp256k1.sign(message, pkBuffer) + // Azure HSM doesn't add the byte prefix (+27) while secp256k1.sign gives recovery (0 or 1) + // so no subtraction needed here + return new Signature( + signature.recovery, + Buffer.from(signature.r.toString(16).padStart(64, '0'), 'hex'), + Buffer.from(signature.s.toString(16).padStart(64, '0'), 'hex') + ) } throw new Error(`Unable to locate key: ${keyName}`) }, @@ -166,7 +169,7 @@ describe('AzureHSMWallet class', () => { celoTransaction = { from: unknownAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: '10', maxFeePerGas: '99', @@ -228,7 +231,7 @@ describe('AzureHSMWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: '10', gasPrice: '99', @@ -258,7 +261,7 @@ describe('AzureHSMWallet class', () => { from: await wallet.getAddressFromKeyName(knownKey), to: ACCOUNT_ADDRESS2, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 65, gas: '10', gasPrice: '99', diff --git a/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.ts b/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.ts index 557cf84935..7aaee6a80b 100644 --- a/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.ts +++ b/packages/sdk/wallets/wallet-hsm-azure/src/azure-hsm-wallet.ts @@ -1,9 +1,10 @@ import { ReadOnlyWallet } from '@celo/connect' -import { Address, publicKeyToAddress } from '@celo/utils/lib/address' +import { Address } from '@celo/utils/lib/address' import { RemoteWallet } from '@celo/wallet-remote' import debugFactory from 'debug' import { AzureHSMSigner } from './azure-hsm-signer' import { AzureKeyVaultClient } from './azure-key-vault-client' +import { getAddressFromPublicKey } from '@celo/wallet-hsm' const debug = debugFactory('kit:wallet:aws-hsm-wallet') @@ -52,6 +53,6 @@ export class AzureHSMWallet extends RemoteWallet implements Read throw new Error('AzureHSMWallet needs to be initialized first') } const publicKey = await this.keyVaultClient!.getPublicKey(keyName) - return publicKeyToAddress(publicKey.toString(16)) + return getAddressFromPublicKey(publicKey) } } diff --git a/packages/sdk/wallets/wallet-hsm-gcp/package.json b/packages/sdk/wallets/wallet-hsm-gcp/package.json index 7fee0756b6..54747054ab 100644 --- a/packages/sdk/wallets/wallet-hsm-gcp/package.json +++ b/packages/sdk/wallets/wallet-hsm-gcp/package.json @@ -24,7 +24,6 @@ "@celo/wallet-base": "^8.0.3", "@celo/wallet-hsm": "^8.0.3", "@celo/wallet-remote": "^8.0.3", - "@ethereumjs/util": "8.0.5", "@google-cloud/kms": "~2.9.0", "@noble/curves": "^1.3.0", "@types/debug": "^4.1.5", @@ -38,8 +37,7 @@ "@noble/curves": "1.3.0", "@noble/hashes": "1.3.3", "@types/debug": "^4.1.12", - "dotenv": "^8.2.0", - "web3": "1.10.4" + "dotenv": "^8.2.0" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-signer.ts b/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-signer.ts index b2cca9a84a..2d1970de75 100644 --- a/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-signer.ts +++ b/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-signer.ts @@ -12,7 +12,7 @@ import { sixtyFour, thirtyTwo, } from '@celo/wallet-hsm' -import * as ethUtil from '@ethereumjs/util' +import { keccak_256 } from '@noble/hashes/sha3' import { KeyManagementServiceClient } from '@google-cloud/kms' import { BigNumber } from 'bignumber.js' @@ -80,8 +80,12 @@ export class GcpHsmSigner implements Signer { } async signPersonalMessage(data: string): Promise { - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) as Buffer + const dataBytes = Buffer.from(trimLeading0x(ensureLeading0x(data)), 'hex') + const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${dataBytes.length}`) + const combined = new Uint8Array(prefix.length + dataBytes.length) + combined.set(prefix) + combined.set(dataBytes, prefix.length) + const msgHashBuff = Buffer.from(keccak_256(combined)) const { v, r, s } = await this.sign(msgHashBuff) return { diff --git a/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-wallet.test.ts b/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-wallet.test.ts index a3fc3eefb6..4627ec743b 100644 --- a/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-wallet.test.ts +++ b/packages/sdk/wallets/wallet-hsm-gcp/src/gcp-hsm-wallet.test.ts @@ -8,10 +8,9 @@ import { import { verifySignature } from '@celo/utils/lib/signatureUtils' import { recoverTransaction, verifyEIP712TypedDataSigner } from '@celo/wallet-base' import { asn1FromPublicKey } from '@celo/wallet-hsm' -import * as ethUtil from '@ethereumjs/util' +// ethUtil removed — using @noble/curves/secp256k1 instead import { secp256k1 } from '@noble/curves/secp256k1' import { BigNumber } from 'bignumber.js' -import Web3 from 'web3' import { GcpHsmWallet } from './gcp-hsm-wallet' require('dotenv').config() @@ -91,7 +90,9 @@ describe('GcpHsmWallet class', () => { ) } const privateKey = keys.get(versionName) - const pubKey = ethUtil.privateToPublic(ethUtil.toBuffer(privateKey)) + const pubKey = Buffer.from( + secp256k1.getPublicKey(trimLeading0x(privateKey!), false).subarray(1) + ) const temp = new BigNumber(ensureLeading0x(pubKey.toString('hex'))) const asn1Key = asn1FromPublicKey(temp) const prefix = '-----BEGIN PUBLIC KEY-----\n' @@ -159,7 +160,7 @@ describe('GcpHsmWallet class', () => { from: unknownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: '10', gasPrice: '99', @@ -218,7 +219,7 @@ describe('GcpHsmWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: '10', gasPrice: '99', @@ -244,7 +245,7 @@ describe('GcpHsmWallet class', () => { from: await wallet.getAddressFromVersionName(knownKey), to: ACCOUNT_ADDRESS2, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 65, gas: '10', gasPrice: '99', diff --git a/packages/sdk/wallets/wallet-hsm/package.json b/packages/sdk/wallets/wallet-hsm/package.json index 951c0561ed..d5a3c0d1ad 100644 --- a/packages/sdk/wallets/wallet-hsm/package.json +++ b/packages/sdk/wallets/wallet-hsm/package.json @@ -26,14 +26,14 @@ }, "dependencies": { "@celo/base": "^7.0.3", - "@ethereumjs/util": "8.0.5", "@noble/ciphers": "1.1.3", "@noble/curves": "1.3.0", "@noble/hashes": "1.3.3", "@types/debug": "^4.1.5", "@types/secp256k1": "^4.0.0", "asn1js": "^2.4.0", - "bignumber.js": "^9.0.0" + "bignumber.js": "^9.0.0", + "viem": "^2.33.2" }, "devDependencies": { "@celo/typescript": "workspace:^", diff --git a/packages/sdk/wallets/wallet-hsm/src/signature-utils.ts b/packages/sdk/wallets/wallet-hsm/src/signature-utils.ts index 8321324e3f..74945e9cf0 100644 --- a/packages/sdk/wallets/wallet-hsm/src/signature-utils.ts +++ b/packages/sdk/wallets/wallet-hsm/src/signature-utils.ts @@ -1,5 +1,5 @@ import { Address, ensureLeading0x } from '@celo/base/lib/address' -import * as ethUtil from '@ethereumjs/util' +import { publicKeyToAddress as viemPublicKeyToAddress } from 'viem/accounts' import { SignatureType } from '@noble/curves/abstract/weierstrass' import { secp256k1 } from '@noble/curves/secp256k1' import { BigNumber } from 'bignumber.js' @@ -35,7 +35,7 @@ export const bigNumberToBuffer = (input: BigNumber, lengthInBytes: number): Buff if (hex.length < hexLength) { hex = '0'.repeat(hexLength - hex.length) + hex } - return ethUtil.toBuffer(ensureLeading0x(hex)) as Buffer + return Buffer.from(ensureLeading0x(hex).slice(2), 'hex') } export class Signature { @@ -97,10 +97,22 @@ export function recoverKeyIndex( } export function getAddressFromPublicKey(publicKey: BigNumber): Address { - const pkBuffer = ethUtil.toBuffer(ensureLeading0x(publicKey.toString(16))) - if (!ethUtil.isValidPublic(pkBuffer, true)) { - throw new Error(`Invalid secp256k1 public key ${publicKey}`) + let rawHex = publicKey.toString(16) + // If the BigNumber represents a 65-byte uncompressed key (with 04 prefix), + // it will be 130 hex chars. If it's a 64-byte raw key (no prefix), 128 chars. + // We need the full uncompressed key (130 hex chars with 04 prefix). + if (rawHex.length <= 128) { + // Pad to 128 chars (64 bytes) and prepend 04 prefix + rawHex = '04' + rawHex.padStart(128, '0') + } else { + // Already includes prefix, pad to 130 chars (65 bytes) + rawHex = rawHex.padStart(130, '0') } - const address = ethUtil.pubToAddress(pkBuffer, true) - return ensureLeading0x(address.toString('hex')) + const pkHex = ensureLeading0x(rawHex) + try { + secp256k1.ProjectivePoint.fromHex(pkHex.slice(2)) + } catch { + throw new Error(`Invalid secp256k1 public key ${pkHex}`) + } + return viemPublicKeyToAddress(pkHex as `0x${string}`) } diff --git a/packages/sdk/wallets/wallet-ledger/package.json b/packages/sdk/wallets/wallet-ledger/package.json index 7acf2efee4..c75046c0f6 100644 --- a/packages/sdk/wallets/wallet-ledger/package.json +++ b/packages/sdk/wallets/wallet-ledger/package.json @@ -34,7 +34,6 @@ "@celo/utils": "^8.0.3", "@celo/wallet-base": "^8.0.3", "@celo/wallet-remote": "^8.0.3", - "@ethereumjs/util": "8.0.5", "@ledgerhq/errors": "^6.16.4", "@ledgerhq/hw-transport": "^6.30.6", "debug": "^4.1.1", @@ -47,8 +46,7 @@ "@noble/curves": "^1.4.0", "@noble/hashes": "^1.3.3", "@types/debug": "^4.1.12", - "@types/node": "18.7.16", - "web3": "1.10.4" + "@types/node": "18.7.16" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts index 0d0e2a98f7..14a770fe77 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-signer.ts @@ -3,7 +3,7 @@ import { RLPEncodedTx, Signer } from '@celo/connect' import Ledger from '@celo/hw-app-eth' import { EIP712TypedData, structHash } from '@celo/utils/lib/sign-typed-data-utils' import { LegacyEncodedTx } from '@celo/wallet-base' -import * as ethUtil from '@ethereumjs/util' +// ethUtil removed — Buffer.from used for hex→buffer conversion import { TransportStatusError } from '@ledgerhq/errors' import debugFactory from 'debug' import { SemVer } from 'semver' @@ -75,8 +75,8 @@ export class LedgerSigner implements Signer { return { v, - r: ethUtil.toBuffer(ensureLeading0x(r)), - s: ethUtil.toBuffer(ensureLeading0x(s)), + r: Buffer.from(trimLeading0x(ensureLeading0x(r)), 'hex'), + s: Buffer.from(trimLeading0x(ensureLeading0x(s)), 'hex'), } } catch (error: unknown) { if (error instanceof TransportStatusError) { @@ -103,8 +103,8 @@ export class LedgerSigner implements Signer { ) return { v: signature.v, - r: ethUtil.toBuffer(ensureLeading0x(signature.r)), - s: ethUtil.toBuffer(ensureLeading0x(signature.s)), + r: Buffer.from(trimLeading0x(ensureLeading0x(signature.r)), 'hex'), + s: Buffer.from(trimLeading0x(ensureLeading0x(signature.s)), 'hex'), } } catch (error) { if (error instanceof TransportStatusError) { @@ -134,8 +134,8 @@ export class LedgerSigner implements Signer { ) return { v: sig.v, - r: ethUtil.toBuffer(ensureLeading0x(sig.r)), - s: ethUtil.toBuffer(ensureLeading0x(sig.s)), + r: Buffer.from(trimLeading0x(ensureLeading0x(sig.r)), 'hex'), + s: Buffer.from(trimLeading0x(ensureLeading0x(sig.s)), 'hex'), } } catch (error) { if (error instanceof TransportStatusError) { diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts index 3cf205c7ad..fa33c2424e 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.test.ts @@ -4,7 +4,6 @@ import { CeloTx, EncodedTransaction } from '@celo/connect' import { verifySignature } from '@celo/utils/lib/signatureUtils' import { recoverTransaction, verifyEIP712TypedDataSigner } from '@celo/wallet-base' import TransportNodeHid from '@ledgerhq/hw-transport-node-hid' -import Web3 from 'web3' import { AddressValidation, CELO_BASE_DERIVATION_PATH, LedgerWallet } from './ledger-wallet' import { ACCOUNT_ADDRESS1, @@ -115,7 +114,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: knownAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: 99, maxFeePerGas: 99, @@ -279,7 +278,6 @@ describe('LedgerWallet class', () => { // @ts-expect-error currentAppName = await wallet.retrieveAppName() - console.log(currentAppName) }, TEST_TIMEOUT_IN_MS) test('starts 5 accounts', () => { @@ -301,7 +299,7 @@ describe('LedgerWallet class', () => { from: unknownAddress, to: unknownAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: 99, maxFeePerGas: 99, @@ -361,7 +359,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: 99, maxFeePerGas: 99, @@ -449,7 +447,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 65, gas: '10', maxFeePerGas: 99, @@ -475,7 +473,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 1, gas: 99, gasPrice: 99, @@ -515,7 +513,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: 99, maxFeePerGas: 99, @@ -570,7 +568,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: 99, maxFeePerGas: 99, @@ -615,7 +613,7 @@ describe('LedgerWallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: 99, maxFeePerGas: 99, diff --git a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts index bc3dbcae64..f263dfe887 100644 --- a/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts +++ b/packages/sdk/wallets/wallet-ledger/src/ledger-wallet.ts @@ -179,7 +179,7 @@ export class LedgerWallet extends RemoteWallet implements ReadOnly for (const changeIndex of this.changeIndexes) { for (const addressIndex of this.derivationPathIndexes) { const derivationPath = `${purpose}/${coinType}/${account}/${changeIndex}/${addressIndex}` - console.info(`Fetching address for derivation path ${derivationPath}`) + debug(`Fetching address for derivation path ${derivationPath}`) const addressInfo = await this.ledger!.getAddress(derivationPath, validationRequired) addressToSigner.set( addressInfo.address!, diff --git a/packages/sdk/wallets/wallet-ledger/src/test-utils.ts b/packages/sdk/wallets/wallet-ledger/src/test-utils.ts index 8cb4662eea..fbae9fb064 100644 --- a/packages/sdk/wallets/wallet-ledger/src/test-utils.ts +++ b/packages/sdk/wallets/wallet-ledger/src/test-utils.ts @@ -12,7 +12,8 @@ import { getHashFromEncoded, signTransaction, } from '@celo/wallet-base' -import * as ethUtil from '@ethereumjs/util' +import { secp256k1 } from '@noble/curves/secp256k1' +import { keccak_256 } from '@noble/hashes/sha3' import { createVerify, VerifyPublicKeyInput } from 'node:crypto' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' @@ -179,16 +180,20 @@ export class TestLedger { async signPersonalMessage(derivationPath: string, data: string) { if (ledgerAddresses[derivationPath]) { - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) + const dataBytes = Buffer.from(trimLeading0x(ensureLeading0x(data)), 'hex') + const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${dataBytes.length}`) + const combined = new Uint8Array(prefix.length + dataBytes.length) + combined.set(prefix) + combined.set(dataBytes, prefix.length) + const msgHashBuff = keccak_256(combined) const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(msgHashBuff, pkBuffer) + const signature = secp256k1.sign(msgHashBuff, pkBuffer) return { - v: Number(signature.v), - r: signature.r.toString('hex'), - s: signature.s.toString('hex'), + v: signature.recovery + 27, + r: signature.r.toString(16).padStart(64, '0'), + s: signature.s.toString(16).padStart(64, '0'), } } throw new Error('Invalid Path') @@ -203,11 +208,11 @@ export class TestLedger { const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(messageHash, pkBuffer) + const signature = secp256k1.sign(messageHash, pkBuffer) return { - v: Number(signature.v), - r: signature.r.toString('hex'), - s: signature.s.toString('hex'), + v: signature.recovery + 27, + r: signature.r.toString(16).padStart(64, '0'), + s: signature.s.toString(16).padStart(64, '0'), } } diff --git a/packages/sdk/wallets/wallet-local/package.json b/packages/sdk/wallets/wallet-local/package.json index d89248bbbf..d8860d7566 100644 --- a/packages/sdk/wallets/wallet-local/package.json +++ b/packages/sdk/wallets/wallet-local/package.json @@ -29,14 +29,14 @@ "@celo/connect": "^7.0.0", "@celo/utils": "^8.0.3", "@celo/wallet-base": "^8.0.3", - "@ethereumjs/util": "8.0.5" + "@noble/curves": "^1.3.0", + "@noble/hashes": "^1.3.3" }, "devDependencies": { "@celo/typescript": "workspace:^", "@types/debug": "^4.1.12", "debug": "^4.3.5", - "viem": "~2.33.2", - "web3": "1.10.4" + "viem": "~2.33.2" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-local/src/local-signer.ts b/packages/sdk/wallets/wallet-local/src/local-signer.ts index 5f2199f0a2..6f49b3ddf6 100644 --- a/packages/sdk/wallets/wallet-local/src/local-signer.ts +++ b/packages/sdk/wallets/wallet-local/src/local-signer.ts @@ -4,7 +4,8 @@ import { computeSharedSecret as computeECDHSecret } from '@celo/utils/lib/ecdh' import { Decrypt } from '@celo/utils/lib/ecies' import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' import { getHashFromEncoded, signTransaction } from '@celo/wallet-base' -import * as ethUtil from '@ethereumjs/util' +import { keccak_256 } from '@noble/hashes/sha3' +import { secp256k1 } from '@noble/curves/secp256k1' /** * Signs the EVM transaction using the provided private key @@ -28,18 +29,21 @@ export class LocalSigner implements Signer { } async signPersonalMessage(data: string): Promise<{ v: number; r: Buffer; s: Buffer }> { - // ecsign needs a privateKey without 0x const trimmedKey = trimLeading0x(this.privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const dataBuff = ethUtil.toBuffer(ensureLeading0x(data)) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) + const dataBytes = Buffer.from(trimLeading0x(ensureLeading0x(data)), 'hex') + const prefix = Buffer.from(`\x19Ethereum Signed Message:\n${dataBytes.length}`) + const combined = new Uint8Array(prefix.length + dataBytes.length) + combined.set(prefix) + combined.set(dataBytes, prefix.length) + const msgHash = keccak_256(combined) - const sig = ethUtil.ecsign(msgHashBuff, pkBuffer) + const sig = secp256k1.sign(msgHash, pkBuffer) return { - v: Number(sig.v), - r: Buffer.from(sig.r), - s: Buffer.from(sig.s), + v: sig.recovery + 27, + r: Buffer.from(sig.r.toString(16).padStart(64, '0'), 'hex'), + s: Buffer.from(sig.s.toString(16).padStart(64, '0'), 'hex'), } } @@ -48,11 +52,11 @@ export class LocalSigner implements Signer { const trimmedKey = trimLeading0x(this.privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const sig = ethUtil.ecsign(dataBuff, pkBuffer) + const sig = secp256k1.sign(dataBuff, pkBuffer) return { - v: Number(sig.v), - r: Buffer.from(sig.r), - s: Buffer.from(sig.s), + v: sig.recovery + 27, + r: Buffer.from(sig.r.toString(16).padStart(64, '0'), 'hex'), + s: Buffer.from(sig.s.toString(16).padStart(64, '0'), 'hex'), } } diff --git a/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts b/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts index 36f7becd36..1390ac81db 100644 --- a/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts +++ b/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts @@ -9,9 +9,8 @@ import { import { Encrypt } from '@celo/utils/lib/ecies' import { verifySignature } from '@celo/utils/lib/signatureUtils' import { recoverTransaction, verifyEIP712TypedDataSigner } from '@celo/wallet-base' -import { parseTransaction, TransactionSerializableEIP1559 } from 'viem' +import { parseEther, parseTransaction, TransactionSerializableEIP1559 } from 'viem' import { privateKeyToAccount } from 'viem/accounts' -import Web3 from 'web3' import { LocalWallet } from './local-wallet' const CHAIN_ID = 44378 @@ -80,7 +79,7 @@ describe('Local wallet class', () => { wallet.addAccount('this is not a valid private key') throw new Error('Expected exception to be thrown') } catch (e: any) { - expect(e.message).toBe('Expected 32 bytes of private key') + expect(e.message).toMatch(/private key/) } }) @@ -116,7 +115,7 @@ describe('Local wallet class', () => { from: unknownAddress, to: unknownAddress, chainId: 2, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 0, gas: '10', maxFeePerGas: '99', @@ -161,7 +160,7 @@ describe('Local wallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 0, gas: '10', gasPrice: '99', @@ -390,7 +389,7 @@ describe('Local wallet class', () => { from: ACCOUNT_ADDRESS1, to: ACCOUNT_ADDRESS2, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 65, gas: '10', gasPrice: '99', @@ -419,7 +418,7 @@ describe('Local wallet class', () => { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: parseEther('1').toString(), nonce: 0, data: '0xabcdef', } diff --git a/packages/sdk/wallets/wallet-local/src/signing.test.ts b/packages/sdk/wallets/wallet-local/src/signing.test.ts index 5fa2b70f77..efeca14ad6 100644 --- a/packages/sdk/wallets/wallet-local/src/signing.test.ts +++ b/packages/sdk/wallets/wallet-local/src/signing.test.ts @@ -1,16 +1,9 @@ /** biome-ignore-all lint/suspicious/noDoubleEquals: legacy-test-file */ -import { - Callback, - CeloTx, - Connection, - JsonRpcPayload, - JsonRpcResponse, - Provider, -} from '@celo/connect' +import { CeloTx, Connection, Provider } from '@celo/connect' import { privateKeyToAddress } from '@celo/utils/lib/address' import { recoverTransaction } from '@celo/wallet-base' import debugFactory from 'debug' -import Web3 from 'web3' +import { parseEther } from 'viem' import { LocalWallet } from './local-wallet' const debug = debugFactory('kit:txtest:sign') @@ -30,42 +23,34 @@ debug(`Account Address 2: ${ACCOUNT_ADDRESS2}`) describe('Transaction Utils', () => { // only needed for the eth_coinbase rcp call let connection: Connection - let web3: Web3 + let signTransaction: (tx: CeloTx) => Promise<{ raw: string; tx: any }> const mockProvider: Provider = { - send: (payload: JsonRpcPayload, callback: Callback): void => { - if (payload.method === 'eth_coinbase') { - const response: JsonRpcResponse = { - jsonrpc: payload.jsonrpc, - id: Number(payload.id), - result: '0xc94770007dda54cF92009BFF0dE90c06F603a09f', - } - callback(null, response) - } else if (payload.method === 'eth_gasPrice') { - const response: JsonRpcResponse = { - jsonrpc: payload.jsonrpc, - id: Number(payload.id), - result: '0x09184e72a000', - } - callback(null, response) + request: (async ({ method }: any) => { + if (method === 'eth_coinbase') { + return '0xc94770007dda54cF92009BFF0dE90c06F603a09f' + } else if (method === 'eth_gasPrice') { + return '0x09184e72a000' } else { - callback(new Error(payload.method)) + throw new Error(method) } - }, + }) as any, } const setupConnection = async () => { - web3 = new Web3() - web3.setProvider(mockProvider as any) - connection = new Connection(web3) + connection = new Connection(mockProvider) connection.wallet = new LocalWallet() + const provider = connection.currentProvider + signTransaction = async (tx: CeloTx) => { + return provider.request({ method: 'eth_signTransaction', params: [tx] }) + } } const verifyLocalSigning = async (celoTransaction: CeloTx): Promise => { let recoveredSigner: string | undefined let recoveredTransaction: CeloTx | undefined let signedTransaction: { raw: string; tx: any } | undefined beforeAll(async () => { - signedTransaction = await web3.eth.signTransaction(celoTransaction) - const recovery = recoverTransaction(signedTransaction.raw) + signedTransaction = await signTransaction(celoTransaction) + const recovery = recoverTransaction(signedTransaction!.raw) recoveredTransaction = recovery[0] recoveredSigner = recovery[1] }) @@ -80,35 +65,37 @@ describe('Transaction Utils', () => { expect(recoveredSigner?.toLowerCase()).toEqual(celoTransaction.from!.toString().toLowerCase()) }) + // Helper: parse a value that may be a hex string or a number + const toNumber = (val: unknown): number => { + if (typeof val === 'string' && val.startsWith('0x')) return parseInt(val, 16) + return Number(val) + } + test('Checking nonce', async () => { if (celoTransaction.nonce != null) { - expect(recoveredTransaction?.nonce).toEqual(parseInt(celoTransaction.nonce.toString(), 16)) + expect(recoveredTransaction?.nonce).toEqual(toNumber(celoTransaction.nonce)) } }) test('Checking gas', async () => { if (celoTransaction.gas != null) { - expect(recoveredTransaction?.gas).toEqual(parseInt(celoTransaction.gas.toString(), 16)) + expect(recoveredTransaction?.gas).toEqual(toNumber(celoTransaction.gas)) } }) test('Checking gas price', async () => { if (celoTransaction.gasPrice != null) { - expect(recoveredTransaction?.gasPrice).toEqual( - parseInt(celoTransaction.gasPrice.toString(), 16) - ) + expect(recoveredTransaction?.gasPrice).toEqual(toNumber(celoTransaction.gasPrice)) } }) test('Checking maxFeePerGas', async () => { if (celoTransaction.maxFeePerGas != null) { - expect(recoveredTransaction?.maxFeePerGas).toEqual( - parseInt(celoTransaction.maxFeePerGas.toString(), 16) - ) + expect(recoveredTransaction?.maxFeePerGas).toEqual(toNumber(celoTransaction.maxFeePerGas)) } }) test('Checking maxPriorityFeePerGas', async () => { if (celoTransaction.maxPriorityFeePerGas != null) { expect(recoveredTransaction?.maxPriorityFeePerGas).toEqual( - parseInt(celoTransaction.maxPriorityFeePerGas.toString(), 16) + toNumber(celoTransaction.maxPriorityFeePerGas) ) } }) @@ -136,7 +123,7 @@ describe('Transaction Utils', () => { } const verifyLocalSigningInAllPermutations = async (from: string, to: string): Promise => { - const amountInWei: string = Web3.utils.toWei('1', 'ether') + const amountInWei: string = parseEther('1').toString() const nonce = 0 const badNonce = 100 const gas = 10000 diff --git a/packages/sdk/wallets/wallet-remote/package.json b/packages/sdk/wallets/wallet-remote/package.json index ac24bccb23..478ead3ee4 100644 --- a/packages/sdk/wallets/wallet-remote/package.json +++ b/packages/sdk/wallets/wallet-remote/package.json @@ -28,12 +28,10 @@ "@celo/connect": "^7.0.0", "@celo/utils": "^8.0.3", "@celo/wallet-base": "^8.0.3", - "@ethereumjs/util": "8.0.5", "@types/debug": "^4.1.5" }, "devDependencies": { - "@celo/typescript": "workspace:^", - "web3": "1.10.4" + "@celo/typescript": "workspace:^" }, "engines": { "node": ">=20" diff --git a/packages/sdk/wallets/wallet-remote/src/remote-wallet.test.ts b/packages/sdk/wallets/wallet-remote/src/remote-wallet.test.ts index d310318325..d968c6ab12 100644 --- a/packages/sdk/wallets/wallet-remote/src/remote-wallet.test.ts +++ b/packages/sdk/wallets/wallet-remote/src/remote-wallet.test.ts @@ -1,6 +1,5 @@ import { Address, CeloTx, Signer } from '@celo/connect' import { normalizeAddressWith0x, privateKeyToAddress } from '@celo/utils/lib/address' -import Web3 from 'web3' import { RemoteWallet } from './remote-wallet' export const PRIVATE_KEY1 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' @@ -70,7 +69,7 @@ describe('RemoteWallet', () => { from: knownAddress, to: knownAddress, chainId: CHAIN_ID, - value: Web3.utils.toWei('1', 'ether'), + value: '1000000000000000000', nonce: 0, gas: '10', gasPrice: '99', diff --git a/packages/viem-account-ledger/package.json b/packages/viem-account-ledger/package.json index abd1ac2e74..c6a685bbcb 100644 --- a/packages/viem-account-ledger/package.json +++ b/packages/viem-account-ledger/package.json @@ -53,8 +53,8 @@ "@celo/utils": "workspace:^", "@celo/wallet-base": "workspace:^", "@celo/wallet-remote": "workspace:^", - "@ethereumjs/util": "8.0.5", "@ledgerhq/hw-transport-node-hid": "^6.29.5", + "@noble/curves": "^1.3.0", "@types/semver": "^7.7.0", "@vitest/coverage-v8": "^3.1.3", "dotenv": "^8.2.0", diff --git a/packages/viem-account-ledger/src/test-utils.ts b/packages/viem-account-ledger/src/test-utils.ts index 25ed0d0daa..f4547d497b 100644 --- a/packages/viem-account-ledger/src/test-utils.ts +++ b/packages/viem-account-ledger/src/test-utils.ts @@ -2,7 +2,7 @@ import { ensureLeading0x, normalizeAddressWith0x, trimLeading0x } from '@celo/ba import Eth from '@celo/hw-app-eth' import { generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils.js' import { getHashFromEncoded, signTransaction } from '@celo/wallet-base' -import * as ethUtil from '@ethereumjs/util' +import { secp256k1 } from '@noble/curves/secp256k1' import { createVerify, VerifyPublicKeyInput } from 'node:crypto' import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' @@ -175,11 +175,11 @@ export class TestLedger { const trimmedKey = trimLeading0x(ledgerAddresses[derivationPath].privateKey) const pkBuffer = Buffer.from(trimmedKey, 'hex') - const signature = ethUtil.ecsign(messageHash, pkBuffer) + const signature = secp256k1.sign(messageHash, pkBuffer) return { - v: Number(signature.v), - r: signature.r.toString('hex'), - s: signature.s.toString('hex'), + v: signature.recovery + 27, + r: signature.r.toString(16).padStart(64, '0'), + s: signature.s.toString(16).padStart(64, '0'), } }