From 6929a3371cb5710f56cfe4271fe87a3867237043 Mon Sep 17 00:00:00 2001 From: Pavel Hornak Date: Wed, 1 Apr 2026 11:06:15 +0200 Subject: [PATCH] refactor(cli): migrate base, utils, and test-utils to viem - base.ts: remove web3 from base command class - utils/cli.ts: replace displaySendTx with displayViemTx - utils/command.ts: migrate transaction helpers to viem - utils/checks.ts, governance.ts, release-gold-base.ts: remove web3 types - test-utils/: chain-setup, cliUtils, mockRpc updated for viem - Remove exchange.ts and exchange.test.ts (dead code) --- packages/cli/package.json | 4 +- packages/cli/src/base.test.ts | 70 ++++----- packages/cli/src/base.ts | 41 +++-- packages/cli/src/test-utils/chain-setup.ts | 140 +++++++++++------- packages/cli/src/test-utils/cliUtils.ts | 36 ++--- .../test-utils/deterministic-test-helpers.ts | 57 +------ packages/cli/src/test-utils/mockRpc.ts | 24 --- packages/cli/src/test-utils/multicall.ts | 6 +- packages/cli/src/test-utils/multisigUtils.ts | 93 ++++++++---- packages/cli/src/test-utils/release-gold.ts | 42 ++++-- packages/cli/src/utils/checks.ts | 11 +- packages/cli/src/utils/cli.ts | 60 +------- packages/cli/src/utils/command.ts | 29 ---- packages/cli/src/utils/exchange.test.ts | 41 ----- packages/cli/src/utils/exchange.ts | 67 --------- packages/cli/src/utils/fee-currency.test.ts | 7 +- packages/cli/src/utils/governance.ts | 17 ++- packages/cli/src/utils/identity.ts | 4 +- packages/cli/src/utils/release-gold-base.ts | 4 +- packages/cli/src/utils/require.ts | 10 -- packages/cli/src/utils/safe.ts | 25 ++-- packages/cli/tsconfig.json | 2 +- 22 files changed, 303 insertions(+), 487 deletions(-) delete mode 100644 packages/cli/src/utils/exchange.test.ts delete mode 100644 packages/cli/src/utils/exchange.ts diff --git a/packages/cli/package.json b/packages/cli/package.json index 086e160356..2d835991c6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -54,7 +54,6 @@ "@celo/wallet-hsm-azure": "^8.0.3", "@celo/wallet-ledger": "^8.0.3", "@celo/wallet-local": "^8.0.3", - "@ethereumjs/util": "8.0.5", "@ledgerhq/hw-transport-node-hid": "^6.28.5", "@oclif/core": "^3.27.0", "@oclif/plugin-autocomplete": "^3.2.0", @@ -74,8 +73,7 @@ "fs-extra": "^8.1.0", "humanize-duration": "^3.32.1", "prompts": "^2.0.1", - "viem": "^2.33.2", - "web3": "1.10.4" + "viem": "^2.33.2" }, "devDependencies": { "@celo/dev-utils": "workspace:^", diff --git a/packages/cli/src/base.test.ts b/packages/cli/src/base.test.ts index fef8f219c4..e59f61d753 100644 --- a/packages/cli/src/base.test.ts +++ b/packages/cli/src/base.test.ts @@ -7,11 +7,10 @@ import http from 'http' import { tmpdir } from 'os' import { MethodNotFoundRpcError } from 'viem' import { privateKeyToAddress } from 'viem/accounts' -import Web3 from 'web3' import { BaseCommand } from './base' import Set from './commands/config/set' import CustomHelp from './help' -import { stripAnsiCodesFromNestedArray, testLocallyWithWeb3Node } from './test-utils/cliUtils' +import { stripAnsiCodesFromNestedArray, testLocallyWithNode } from './test-utils/cliUtils' import { mockRpcFetch } from './test-utils/mockRpc' import { CustomFlags } from './utils/command' import * as config from './utils/config' @@ -62,17 +61,17 @@ describe('flags', () => { describe('--node celo-sepolia', () => { it('it connects to 11_142_220', async () => { const command = new BasicCommand(['--node', 'celo-sepolia'], config) - const runnerWeb3 = await command.getWeb3() - const connectdChain = await runnerWeb3.eth.getChainId() - expect(connectdChain).toBe(11_142_220) + const runnerClient = await command.getPublicClient() + const connectdChain = runnerClient.chain + expect(connectdChain.id).toBe(11_142_220) }) }) describe.each(['celo', 'mainnet'])('--node %s', (node) => { it('it connects to 42220', async () => { const command = new BasicCommand(['--node', node], config) - const runnerWeb3 = await command.getWeb3() - const connectdChain = await runnerWeb3.eth.getChainId() - expect(connectdChain).toBe(42220) + const runnerClient = await command.getPublicClient() + const connectdChain = runnerClient.chain + expect(connectdChain.id).toBe(42220) }) }) describe('--node websockets', () => { @@ -105,7 +104,7 @@ jest.mock('../package.json', () => ({ version: '5.2.3', })) -testWithAnvilL2('BaseCommand', (web3: Web3) => { +testWithAnvilL2('BaseCommand', (provider) => { const logSpy = jest.spyOn(console, 'log').mockImplementation() beforeEach(() => { @@ -118,7 +117,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { const storedDerivationPath = readConfig(tmpdir()).derivationPath console.info('storedDerivationPath', storedDerivationPath) expect(storedDerivationPath).not.toBe(undefined) - await testLocallyWithWeb3Node(BasicCommand, ['--useLedger'], web3) + await testLocallyWithNode(BasicCommand, ['--useLedger'], provider) expect(WalletLedgerExports.newLedgerWalletWithSetup).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ @@ -134,8 +133,8 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { it('uses custom derivationPath', async () => { const storedDerivationPath = readConfig(tmpdir()).derivationPath const customPath = "m/44'/9000'/0'" - await testLocallyWithWeb3Node(Set, ['--derivationPath', customPath], web3) - await testLocallyWithWeb3Node(BasicCommand, ['--useLedger'], web3) + await testLocallyWithNode(Set, ['--derivationPath', customPath], provider) + await testLocallyWithNode(BasicCommand, ['--useLedger'], provider) expect(WalletLedgerExports.newLedgerWalletWithSetup).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ @@ -147,12 +146,12 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { baseDerivationPath: customPath, }) ) - await testLocallyWithWeb3Node(Set, ['--derivationPath', storedDerivationPath], web3) + await testLocallyWithNode(Set, ['--derivationPath', storedDerivationPath], provider) }) }) it('--ledgerAddresses passes derivationPathIndexes to LedgerWallet', async () => { - await testLocallyWithWeb3Node(BasicCommand, ['--useLedger', '--ledgerAddresses', '5'], web3) + await testLocallyWithNode(BasicCommand, ['--useLedger', '--ledgerAddresses', '5'], provider) expect(WalletLedgerExports.newLedgerWalletWithSetup).toHaveBeenCalledWith( expect.anything(), @@ -197,10 +196,10 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { describe('with --ledgerLiveMode', () => { it('--ledgerAddresses passes changeIndexes to LedgerWallet', async () => { - await testLocallyWithWeb3Node( + await testLocallyWithNode( BasicCommand, ['--useLedger', '--ledgerLiveMode', '--ledgerAddresses', '5'], - web3 + provider ) expect(WalletLedgerExports.newLedgerWalletWithSetup).toHaveBeenCalledWith( @@ -246,10 +245,10 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { }) describe('with --ledgerCustomAddresses', () => { it('passes custom changeIndexes to LedgerWallet', async () => { - await testLocallyWithWeb3Node( + await testLocallyWithNode( BasicCommand, ['--useLedger', '--ledgerLiveMode', '--ledgerCustomAddresses', '[1,8,9]'], - web3 + provider ) expect(WalletLedgerExports.newLedgerWalletWithSetup).toHaveBeenCalledWith( @@ -293,10 +292,10 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { }) describe('with --ledgerCustomAddresses', () => { it('passes custom derivationPathIndexes to LedgerWallet', async () => { - await testLocallyWithWeb3Node( + await testLocallyWithNode( BasicCommand, ['--useLedger', '--ledgerCustomAddresses', '[1,8,9]'], - web3 + provider ) expect(WalletLedgerExports.newLedgerWalletWithSetup).toHaveBeenCalledWith( @@ -341,7 +340,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { describe('with --from', () => { it('uses it as the default account', async () => { - await testLocallyWithWeb3Node( + await testLocallyWithNode( BasicCommand, [ '--useLedger', @@ -350,7 +349,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { '--from', '0x1234567890123456789012345678901234567890', ], - web3 + provider ) expect(ViemAccountLedgerExports.ledgerToWalletClient).toHaveBeenCalledWith( @@ -381,7 +380,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { const errorSpy = jest.spyOn(console, 'error').mockImplementation() await expect( - testLocallyWithWeb3Node(TestErrorCommand, [], web3) + testLocallyWithNode(TestErrorCommand, [], provider) ).rejects.toThrowErrorMatchingInlineSnapshot( `"Unable to create an RPC Wallet Client, the node is not unlocked. Did you forget to use \`--privateKey\` or \`--useLedger\`?"` ) @@ -399,7 +398,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { const errorSpy = jest.spyOn(console, 'error').mockImplementation() await expect( - testLocallyWithWeb3Node(TestErrorCommand, [], web3) + testLocallyWithNode(TestErrorCommand, [], provider) ).rejects.toThrowErrorMatchingInlineSnapshot(`"test error"`) expect(errorSpy.mock.calls).toMatchInlineSnapshot(` @@ -432,7 +431,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { const errorSpy = jest.spyOn(console, 'error').mockImplementation() await expect( - testLocallyWithWeb3Node(TestErrorCommand, ['--output', 'csv'], web3) + testLocallyWithNode(TestErrorCommand, ['--output', 'csv'], provider) ).rejects.toThrowErrorMatchingInlineSnapshot(`"test error"`) expect(errorSpy.mock.calls).toMatchInlineSnapshot(`[]`) @@ -453,7 +452,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { throw new Error('Mock connection stop error') }) - await testLocallyWithWeb3Node(TestConnectionStopErrorCommand, [], web3) + await testLocallyWithNode(TestConnectionStopErrorCommand, [], provider) expect(logSpy.mock.calls).toMatchInlineSnapshot(` [ @@ -489,10 +488,10 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { } await expect( - testLocallyWithWeb3Node( + testLocallyWithNode( TestPrivateKeyCommand, ['--privateKey', privateKey, '--from', wrongFromAddress], - web3 + provider ) ).rejects.toThrowErrorMatchingInlineSnapshot( `"The --from address ${wrongFromAddress} does not match the address derived from the provided private key 0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb."` @@ -515,10 +514,10 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { } await expect( - testLocallyWithWeb3Node( + testLocallyWithNode( TestPrivateKeyCommand, ['--privateKey', privateKey, '--from', correctFromAddress], - web3 + provider ) ).resolves.not.toThrow() }) @@ -538,7 +537,7 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { } await expect( - testLocallyWithWeb3Node(TestPrivateKeyCommand, ['--privateKey', privateKey], web3) + testLocallyWithNode(TestPrivateKeyCommand, ['--privateKey', privateKey], provider) ).resolves.not.toThrow() }) }) @@ -687,7 +686,6 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { }) delete process.env.TELEMETRY_ENABLED - process.env.TELEMETRY_URL = 'http://localhost:3000/' const fetchSpy = jest.spyOn(global, 'fetch') @@ -697,13 +695,17 @@ testWithAnvilL2('BaseCommand', (web3: Web3) => { }, 5000) // Higher timeout than the telemetry logic uses }) - server.listen(3000, async () => { + server.listen(0, async () => { + const address = server.address() as { port: number } + const telemetryUrl = `http://localhost:${address.port}/` + process.env.TELEMETRY_URL = telemetryUrl + // Make sure the command actually returns await expect(TestTelemetryCommand.run([])).resolves.toBe(EXPECTED_COMMAND_RESULT) expect(fetchSpy.mock.calls.length).toEqual(1) - expect(fetchSpy.mock.calls[0][0]).toMatchInlineSnapshot(`"http://localhost:3000/"`) + expect(fetchSpy.mock.calls[0][0]).toEqual(telemetryUrl) expect(fetchSpy.mock.calls[0][1]?.body).toMatchInlineSnapshot(` " celocli_invocation{success="true", version="5.2.3", command="test:telemetry-timeout"} 1 diff --git a/packages/cli/src/base.ts b/packages/cli/src/base.ts index 3856b79271..13a756b7c8 100644 --- a/packages/cli/src/base.ts +++ b/packages/cli/src/base.ts @@ -5,8 +5,9 @@ import { ETHEREUM_DERIVATION_PATH, StrongAddress, } from '@celo/base' -import { ReadOnlyWallet } from '@celo/connect' -import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' +import { type Provider, ReadOnlyWallet } from '@celo/connect' +import { ContractKit, newKitFromProvider } from '@celo/contractkit' +import { getProviderForKit } from '@celo/contractkit/lib/setupForKits' import { ledgerToWalletClient } from '@celo/viem-account-ledger' import { AzureHSMWallet } from '@celo/wallet-hsm-azure' import { AddressValidation, newLedgerWalletWithSetup } from '@celo/wallet-ledger' @@ -16,7 +17,6 @@ import { Command, Flags, ux } from '@oclif/core' import { CLIError } from '@oclif/core/lib/errors' import { ArgOutput, FlagOutput, Input, ParserOutput } from '@oclif/core/lib/interfaces/parser' import chalk from 'chalk' -import net from 'net' import { createPublicClient, createWalletClient, @@ -30,7 +30,6 @@ import { import { privateKeyToAccount } from 'viem/accounts' import { celo, celoSepolia } from 'viem/chains' import { ipc } from 'viem/node' -import Web3 from 'web3' import createRpcWalletClient from './packages-to-be/rpc-client' import { failWith } from './utils/cli' import { CustomFlags } from './utils/command' @@ -143,7 +142,7 @@ export abstract class BaseCommand extends Command { // useful for the LedgerWalletClient which sometimes needs user input on reads public isOnlyReadingWallet = false - private _web3: Web3 | null = null + private _provider: Provider | null = null private _kit: ContractKit | null = null private publicClient: PublicCeloClient | null = null @@ -151,13 +150,6 @@ export abstract class BaseCommand extends Command { private _parseResult: null | ParserOutput = null private ledgerTransport: Awaited> | null = null - async getWeb3() { - if (!this._web3) { - this._web3 = await this.newWeb3() - } - return this._web3 - } - get _wallet(): ReadOnlyWallet | undefined { return this._wallet } @@ -172,17 +164,17 @@ export abstract class BaseCommand extends Command { return (res.flags && res.flags.node) || getNodeUrl(this.config.configDir) } - async newWeb3() { - const nodeUrl = await this.getNodeUrl() - - return nodeUrl && nodeUrl.endsWith('.ipc') - ? new Web3(new Web3.providers.IpcProvider(nodeUrl, net)) - : new Web3(nodeUrl) + async newProvider(): Promise { + if (!this._provider) { + const nodeUrl = await this.getNodeUrl() + this._provider = getProviderForKit(nodeUrl, undefined) + } + return this._provider } async getKit() { if (!this._kit) { - this._kit = newKitFromWeb3(await this.getWeb3()) + this._kit = newKitFromProvider(await this.newProvider()) } const res = await this.parse() @@ -324,7 +316,10 @@ export abstract class BaseCommand extends Command { } catch (e) { let code: number | undefined try { - const error = JSON.parse((e as any).details) as { code: number; message: string } + const error = JSON.parse((e as Error & { details: string }).details) as { + code: number + message: string + } code = error.code } catch (_) { // noop @@ -348,7 +343,7 @@ export abstract class BaseCommand extends Command { const res = await this.parse(BaseCommand) const isLedgerLiveMode = res.flags.ledgerLiveMode const indicesToIterateOver: number[] = res.raw.some( - (value: any) => value.flag === 'ledgerCustomAddresses' + (value) => (value as { flag?: string }).flag === 'ledgerCustomAddresses' ) ? JSON.parse(res.flags.ledgerCustomAddresses) : Array.from(new Array(res.flags.ledgerAddresses).keys()) @@ -401,7 +396,7 @@ export abstract class BaseCommand extends Command { try { const isLedgerLiveMode = res.flags.ledgerLiveMode const indicesToIterateOver: number[] = res.raw.some( - (value) => (value as any).flag === 'ledgerCustomAddresses' + (value) => (value as { flag?: string }).flag === 'ledgerCustomAddresses' ) ? JSON.parse(res.flags.ledgerCustomAddresses) : Array.from(new Array(res.flags.ledgerAddresses).keys()) @@ -511,7 +506,7 @@ export abstract class BaseCommand extends Command { return false } - async finally(arg: Error | undefined): Promise { + async finally(arg: Error | undefined): Promise { const hideExtraOutput = await this.shouldHideExtraOutput(arg) try { diff --git a/packages/cli/src/test-utils/chain-setup.ts b/packages/cli/src/test-utils/chain-setup.ts index 5f61aaf08a..bd33bfca79 100644 --- a/packages/cli/src/test-utils/chain-setup.ts +++ b/packages/cli/src/test-utils/chain-setup.ts @@ -8,15 +8,16 @@ import { withImpersonatedAccount, } from '@celo/dev-utils/anvil-test' import { mineBlocks, timeTravel } from '@celo/dev-utils/ganache-test' +import { Provider } from '@celo/connect' import { addressToPublicKey } from '@celo/utils/lib/signatureUtils' import BigNumber from 'bignumber.js' -import Web3 from 'web3' +import { decodeFunctionResult, encodeFunctionData, parseEther } from 'viem' import Switch from '../commands/epochs/switch' -import { testLocallyWithWeb3Node } from './cliUtils' +import { testLocallyWithNode } from './cliUtils' -export const MIN_LOCKED_CELO_VALUE = new BigNumber(Web3.utils.toWei('10000', 'ether')) // 10k CELO for the group +export const MIN_LOCKED_CELO_VALUE = new BigNumber(parseEther('10000').toString()) // 10k CELO for the group export const MIN_PRACTICAL_LOCKED_CELO_VALUE = MIN_LOCKED_CELO_VALUE.plus( - Web3.utils.toWei('1', 'ether') + parseEther('1').toString() ) // 10k CELO for the group and 1 for gas const GROUP_COMMISION = new BigNumber(0.1) @@ -25,7 +26,8 @@ export const registerAccount = async (kit: ContractKit, address: string) => { const accounts = await kit.contracts.getAccounts() if (!(await accounts.isAccount(address))) { - await accounts.createAccount().sendAndWaitForReceipt({ from: address }) + const hash = await accounts.createAccount({ from: address }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) } } @@ -38,7 +40,8 @@ export const registerAccountWithLockedGold = async ( const lockedGold = await kit.contracts.getLockedGold() - await lockedGold.lock().sendAndWaitForReceipt({ from: address, value }) + const hash = await lockedGold.lock({ from: address, value }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) } export const setupGroup = async ( @@ -54,9 +57,10 @@ export const setupGroup = async ( const validators = await kit.contracts.getValidators() - await (await validators.registerValidatorGroup(groupCommission)).sendAndWaitForReceipt({ + const hash = await validators.registerValidatorGroup(groupCommission, { from: groupAccount, }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) } export const setupValidator = async (kit: ContractKit, validatorAccount: string) => { @@ -65,9 +69,10 @@ export const setupValidator = async (kit: ContractKit, validatorAccount: string) const ecdsaPublicKey = await addressToPublicKey(validatorAccount, kit.connection.sign) const validators = await kit.contracts.getValidators() - await validators.registerValidatorNoBls(ecdsaPublicKey).sendAndWaitForReceipt({ + const hash = await validators.registerValidatorNoBls(ecdsaPublicKey, { from: validatorAccount, }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) } export const setupGroupAndAffiliateValidator = async ( @@ -87,7 +92,8 @@ export const voteForGroupFrom = async ( ) => { const election = await kit.contracts.getElection() - await (await election.vote(groupAddress, amount)).sendAndWaitForReceipt({ from: fromAddress }) + const hash = await election.vote(groupAddress, amount, { from: fromAddress }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) } export const voteForGroupFromAndActivateVotes = async ( @@ -96,20 +102,22 @@ export const voteForGroupFromAndActivateVotes = async ( groupAddress: string, amount: BigNumber ) => { - const accounts = await kit.web3.eth.getAccounts() + const accounts = await kit.connection.getAccounts() await voteForGroupFrom(kit, fromAddress, groupAddress, amount) - await timeTravel(24 * 60 * 60, kit.web3) // wait for 24 hours to - await testLocallyWithWeb3Node(Switch, ['--from', accounts[0]], kit.web3) + await timeTravel(24 * 60 * 60, kit.connection.currentProvider) // wait for 24 hours to + await testLocallyWithNode(Switch, ['--from', accounts[0]], kit.connection.currentProvider) const election = await kit.contracts.getElection() - const txos = await election.activate(fromAddress, false) - - await Promise.all(txos.map((txo) => txo.sendAndWaitForReceipt({ from: fromAddress }))) + // activate returns hashes directly (transactions already sent) + const hashes = await election.activate(fromAddress, false, { from: fromAddress }) + for (const hash of hashes) { + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) + } } export const mineEpoch = async (kit: ContractKit) => { - await mineBlocks(100, kit.web3) + await mineBlocks(100, kit.connection.currentProvider) } export const topUpWithToken = async ( @@ -120,11 +128,12 @@ export const topUpWithToken = async ( ) => { const token = await kit.contracts.getStableToken(stableToken) - await impersonateAccount(kit.web3, STABLES_ADDRESS) - await token.transfer(account, amount.toFixed()).sendAndWaitForReceipt({ + await impersonateAccount(kit.connection.currentProvider, STABLES_ADDRESS) + const hash = await token.transfer(account, amount.toFixed(), { from: STABLES_ADDRESS, }) - await stopImpersonatingAccount(kit.web3, STABLES_ADDRESS) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) + await stopImpersonatingAccount(kit.connection.currentProvider, STABLES_ADDRESS) } // replace the original owner in the devchain, so we can act as the multisig owner @@ -132,20 +141,20 @@ export const topUpWithToken = async ( export const changeMultiSigOwner = async (kit: ContractKit, toAccount: StrongAddress) => { const governance = await kit.contracts.getGovernance() const multisig = await governance.getApproverMultisig() - await ( - await kit.sendTransaction({ - from: toAccount, - to: multisig.address, - value: kit.web3.utils.toWei('1', 'ether'), - }) - ).waitReceipt() + const hash = await kit.sendTransaction({ + from: toAccount, + to: multisig.address, + value: parseEther('1').toString(), + }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) - await impersonateAccount(kit.web3, multisig.address) + await impersonateAccount(kit.connection.currentProvider, multisig.address) - await multisig - .replaceOwner(DEFAULT_OWNER_ADDRESS, toAccount) - .sendAndWaitForReceipt({ from: multisig.address }) - await stopImpersonatingAccount(kit.web3, multisig.address) + const replaceHash = await multisig.replaceOwner(DEFAULT_OWNER_ADDRESS, toAccount, { + from: multisig.address, + }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: replaceHash as `0x${string}` }) + await stopImpersonatingAccount(kit.connection.currentProvider, multisig.address) } export async function setupValidatorAndAddToGroup( @@ -157,16 +166,22 @@ export async function setupValidatorAndAddToGroup( const validators = await kit.contracts.getValidators() - await validators.affiliate(groupAccount).sendAndWaitForReceipt({ from: validatorAccount }) + const affiliateHash = await validators.affiliate(groupAccount, { from: validatorAccount }) + await kit.connection.viemClient.waitForTransactionReceipt({ + hash: affiliateHash as `0x${string}`, + }) - await (await validators.addMember(groupAccount, validatorAccount)).sendAndWaitForReceipt({ + const addMemberHash = await validators.addMember(groupAccount, validatorAccount, { from: groupAccount, }) + await kit.connection.viemClient.waitForTransactionReceipt({ + hash: addMemberHash as `0x${string}`, + }) } // you MUST call clearMock after using this function! -export async function mockTimeForwardBy(seconds: number, web3: Web3) { +export async function mockTimeForwardBy(seconds: number, provider: Provider) { const now = Date.now() - await timeTravel(seconds, web3) + await timeTravel(seconds, provider) const spy = jest.spyOn(global.Date, 'now').mockImplementation(() => now + seconds * 1000) console.warn('mockTimeForwardBy', seconds, 'seconds', 'call clearMock after using this function') @@ -174,37 +189,60 @@ export async function mockTimeForwardBy(seconds: number, web3: Web3) { } export const activateAllValidatorGroupsVotes = async (kit: ContractKit) => { - const [sender] = await kit.web3.eth.getAccounts() + const [sender] = await kit.connection.getAccounts() const validatorsContract = await kit.contracts.getValidators() const electionWrapper = await kit.contracts.getElection() const epochManagerWrapper = await kit.contracts.getEpochManager() const validatorGroups = await validatorsContract.getRegisteredValidatorGroupsAddresses() - await timeTravel((await epochManagerWrapper.epochDuration()) + 1, kit.web3) + await timeTravel((await epochManagerWrapper.epochDuration()) + 1, kit.connection.currentProvider) // Make sure we are in the next epoch to activate the votes - await epochManagerWrapper.startNextEpochProcess().sendAndWaitForReceipt({ from: sender }) - await (await epochManagerWrapper.finishNextEpochProcessTx()).sendAndWaitForReceipt({ - from: sender, - }) + const startHash = await epochManagerWrapper.startNextEpochProcess({ from: sender }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: startHash as `0x${string}` }) + const finishHash = await epochManagerWrapper.finishNextEpochProcessTx({ from: sender }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: finishHash as `0x${string}` }) for (const validatorGroup of validatorGroups) { - const pendingVotesForGroup = new BigNumber( - // @ts-expect-error we need to call the method directly as it's not exposed (and no need to) via the wrapper - await electionWrapper.contract.methods.getPendingVotesForGroup(validatorGroup).call() - ) + const getPendingCallData = encodeFunctionData({ + // @ts-expect-error we need to call the method directly as it's not exposed via the wrapper + abi: electionWrapper.contract.abi, + functionName: 'getPendingVotesForGroup', + args: [validatorGroup as `0x${string}`], + }) + const { data: getPendingResultData } = await kit.connection.viemClient.call({ + // @ts-expect-error we need to call the method directly as it's not exposed via the wrapper + to: electionWrapper.contract.address, + data: getPendingCallData, + }) + const pendingVotesRaw = decodeFunctionResult({ + // @ts-expect-error we need to call the method directly as it's not exposed via the wrapper + abi: electionWrapper.contract.abi, + functionName: 'getPendingVotesForGroup', + data: getPendingResultData!, + }) + const pendingVotesForGroup = new BigNumber(String(pendingVotesRaw)) if (pendingVotesForGroup.gt(0)) { await withImpersonatedAccount( - kit.web3, + kit.connection.currentProvider, validatorGroup, async () => { - // @ts-expect-error here as well - await electionWrapper.contract.methods - .activate(validatorGroup) - .send({ from: validatorGroup }) + const activateData = encodeFunctionData({ + // @ts-expect-error here as well + abi: electionWrapper.contract.abi, + functionName: 'activate', + args: [validatorGroup as `0x${string}`], + }) + const hash = await kit.connection.sendTransaction({ + // @ts-expect-error here as well + to: electionWrapper.contract.address, + data: activateData, + from: validatorGroup, + }) + await kit.connection.viemClient.waitForTransactionReceipt({ hash: hash as `0x${string}` }) }, - new BigNumber(kit.web3.utils.toWei('1', 'ether')) + new BigNumber(parseEther('1').toString()) ) } } diff --git a/packages/cli/src/test-utils/cliUtils.ts b/packages/cli/src/test-utils/cliUtils.ts index e503c8951f..3f3ab3539c 100644 --- a/packages/cli/src/test-utils/cliUtils.ts +++ b/packages/cli/src/test-utils/cliUtils.ts @@ -1,7 +1,8 @@ import { PublicCeloClient } from '@celo/actions' +import { Provider } from '@celo/connect' +import { CeloProvider } from '@celo/connect/lib/celo-provider' import { TestClientExtended } from '@celo/dev-utils/viem/anvil-test' import { Interfaces } from '@oclif/core' -import Web3 from 'web3' import { BaseCommand } from '../base' type AbstractConstructor = new (...args: any[]) => T @@ -10,31 +11,32 @@ interface Runner extends AbstractConstructor { flags: typeof BaseCommand.flags } -export async function testLocallyWithWeb3Node( +export async function testLocallyWithNode( command: Runner, argv: string[], - web3: Web3, + client: Provider, config?: Interfaces.LoadOptions ) { - return testLocally(command, [...argv, '--node', extractHostFromWeb3(web3)], config) + return testLocally(command, [...argv, '--node', extractHostFromProvider(client)], config) } -export const extractHostFromWeb3 = (web3: Web3): string => { - // why would the constructor name be HttpProvider but it not be considered an instance of HttpProvider? idk but it happens - if ( - web3.currentProvider instanceof Web3.providers.HttpProvider || - web3.currentProvider?.constructor.name === 'HttpProvider' - ) { - // @ts-ignore - return web3.currentProvider.host +export const extractHostFromProvider = (provider: Provider): string => { + // CeloProvider wraps the underlying provider + if (provider instanceof CeloProvider) { + const inner = provider.existingProvider as { host?: string; url?: string } + return inner?.host || inner?.url || 'http://localhost:8545' } - // CeloProvider is not exported from @celo/connect, but it's injected into web3 - if (web3.currentProvider !== null && web3.currentProvider.constructor.name === 'CeloProvider') { - return (web3.currentProvider as any).existingProvider.host + // Direct provider (HttpProvider or SimpleHttpProvider) + const rawProvider = provider as Provider & { host?: string; url?: string } + if (rawProvider.host) { + return rawProvider.host + } + if (rawProvider.url) { + return rawProvider.url } - throw new Error(`Unsupported provider, ${web3.currentProvider?.constructor.name}`) + throw new Error(`Unsupported provider, ${provider.constructor.name}`) } export async function testLocallyWithViemNode( @@ -85,6 +87,6 @@ export function stripAnsiCodesFromNestedArray(arrays: string[][]) { } export const LONG_TIMEOUT_MS = 10 * 1000 -export const EXTRA_LONG_TIMEOUT_MS = 60 * 1000 +export const EXTRA_LONG_TIMEOUT_MS = 120 * 1000 export const TEST_SANCTIONED_ADDRESS = '0x01e2919679362dfbc9ee1644ba9c6da6d6245bb1' diff --git a/packages/cli/src/test-utils/deterministic-test-helpers.ts b/packages/cli/src/test-utils/deterministic-test-helpers.ts index abb8e743fc..4b61fa34bb 100644 --- a/packages/cli/src/test-utils/deterministic-test-helpers.ts +++ b/packages/cli/src/test-utils/deterministic-test-helpers.ts @@ -1,54 +1,7 @@ -import { HttpRpcCaller } from '@celo/connect' - /** - * Mock gas prices to be deterministic across environments + * Deterministic test helpers. + * + * NOTE: The old HttpRpcCaller-based mocks (mockDeterministicGas, mockDeterministicBalance) + * were removed along with HttpRpcCaller. Use mockRpcFetch from ./mockRpc instead. */ -export const mockDeterministicGas = () => { - return jest.spyOn(HttpRpcCaller.prototype, 'call').mockImplementation(async (method, _args) => { - // Set deterministic gas prices that match CI environment - if (method === 'eth_gasPrice') { - return { - result: '0x5d21dba00', // 25000000000 - deterministic gas price - id: 1, - jsonrpc: '2.0', - } - } - if (method === 'eth_maxPriorityFeePerGas') { - return { - result: '0x4e3b29200', // 20000000000 - deterministic priority fee - id: 1, - jsonrpc: '2.0', - } - } - if (method === 'eth_feeHistory') { - return { - result: { - baseFeePerGas: ['0x5d21dba00'], // Same as gas price - gasUsedRatio: [0.5], - reward: [['0x4e3b29200']], - }, - id: 1, - jsonrpc: '2.0', - } - } - // For other methods, call through to original implementation - return HttpRpcCaller.prototype.call.call(this, method, _args) - }) -} - -/** - * Mock balance queries to return deterministic values - */ -export const mockDeterministicBalance = (expectedBalance: string) => { - return jest.spyOn(HttpRpcCaller.prototype, 'call').mockImplementation(async (method, args) => { - if (method === 'eth_getBalance') { - return { - result: expectedBalance, // Use the expected balance from CI snapshots - id: 1, - jsonrpc: '2.0', - } - } - // For other methods, call through to original implementation - return HttpRpcCaller.prototype.call.call(this, method, args) - }) -} +export {} diff --git a/packages/cli/src/test-utils/mockRpc.ts b/packages/cli/src/test-utils/mockRpc.ts index 8f57a9ab8a..8fdb0180ec 100644 --- a/packages/cli/src/test-utils/mockRpc.ts +++ b/packages/cli/src/test-utils/mockRpc.ts @@ -1,29 +1,5 @@ -import { HttpRpcCaller } from '@celo/connect' import { RpcErrorCode } from 'viem' -export const mockRpc = () => - jest.spyOn(HttpRpcCaller.prototype, 'call').mockImplementation(async (method, _args) => { - if (method === 'eth_maxPriorityFeePerGas') { - return { - result: '20000', - id: 1, - jsonrpc: '2.0', - } - } - if (method === 'eth_gasPrice') { - return { - result: '30000', - id: 1, - jsonrpc: '2.0', - } - } - return { - result: 0, - id: Math.random(), - jsonrpc: '2.0', - } - }) - const actualFetch = global.fetch export const mockRpcFetch = ({ method, diff --git a/packages/cli/src/test-utils/multicall.ts b/packages/cli/src/test-utils/multicall.ts index 4935764ae0..ccc19efbd8 100644 --- a/packages/cli/src/test-utils/multicall.ts +++ b/packages/cli/src/test-utils/multicall.ts @@ -1,9 +1,9 @@ import { StrongAddress } from '@celo/base' +import { Provider } from '@celo/connect' import { setCode } from '@celo/dev-utils/anvil-test' -import Web3 from 'web3' -export async function deployMultiCall(web3: Web3, address: StrongAddress) { - return setCode(web3, address, bytecode) +export async function deployMultiCall(provider: Provider, address: StrongAddress) { + return setCode(provider, address, bytecode) } // SOURCE https://celo.blockscout.com/address/0xcA11bde05977b3631167028862bE2a173976CA11?tab=contract_bytecode diff --git a/packages/cli/src/test-utils/multisigUtils.ts b/packages/cli/src/test-utils/multisigUtils.ts index cd58730b67..33969fe96a 100644 --- a/packages/cli/src/test-utils/multisigUtils.ts +++ b/packages/cli/src/test-utils/multisigUtils.ts @@ -1,9 +1,11 @@ import { multiSigABI, proxyABI } from '@celo/abis' import { StrongAddress } from '@celo/base' +import { AbiItem, Provider } from '@celo/connect' import { ContractKit } from '@celo/contractkit' import { setCode } from '@celo/dev-utils/anvil-test' import { TEST_GAS_PRICE } from '@celo/dev-utils/test-utils' -import Web3 from 'web3' +import { encodeFunctionData, parseUnits } from 'viem' +import { waitForTransactionReceipt } from 'viem/actions' import { multiSigBytecode, proxyBytecode, @@ -19,56 +21,91 @@ import { SAFE_PROXY_FACTORY_CODE, } from './constants' +interface RpcBlockResponse { + baseFeePerGas: string +} + export async function createMultisig( kit: ContractKit, owners: StrongAddress[], requiredSignatures: number, requiredInternalSignatures: number ): Promise { - const accounts = (await kit.web3.eth.getAccounts()) as StrongAddress[] + const accounts = (await kit.connection.getAccounts()) as StrongAddress[] kit.defaultAccount = accounts[0] // Deploy Proxy contract - const proxyDeploymentTx = await kit.sendTransaction({ + const proxyHash = await kit.sendTransaction({ data: proxyBytecode, maxFeePerGas: TEST_GAS_PRICE, }) - const { contractAddress: proxyAddress } = await proxyDeploymentTx.waitReceipt() + const proxyReceipt = await waitForTransactionReceipt(kit.connection.viemClient, { + hash: proxyHash, + }) + const { contractAddress: proxyAddress } = proxyReceipt // Deploy MultiSig contract - const multisigDeploymentTx = await kit.sendTransaction({ + const multisigHash = await kit.sendTransaction({ data: multiSigBytecode, maxFeePerGas: TEST_GAS_PRICE, }) - const { contractAddress: multiSigAddress } = await multisigDeploymentTx.waitReceipt() + const multisigReceipt = await waitForTransactionReceipt(kit.connection.viemClient, { + hash: multisigHash, + }) + const { contractAddress: multiSigAddress } = multisigReceipt // Configure and initialize MultiSig const initializerAbi = multiSigABI.find( (abi) => abi.type === 'function' && abi.name === 'initialize' ) - const proxy = new kit.web3.eth.Contract(proxyABI as any, proxyAddress) - const baseFee = await kit.web3.eth.getBlock('latest').then((block: any) => block.baseFeePerGas) - const priorityFee = kit.web3.utils.toWei('25', 'gwei') - const initMethod = proxy.methods._setAndInitializeImplementation - const callData = kit.web3.eth.abi.encodeFunctionCall(initializerAbi as any, [ - owners as any, - requiredSignatures as any, - requiredInternalSignatures as any, - ]) - const initTx = initMethod(multiSigAddress, callData) - await initTx.send({ + const proxy = kit.connection.getCeloContract(proxyABI as unknown as AbiItem[], proxyAddress!) + const blockResp = await kit.connection.viemClient.request({ + method: 'eth_getBlockByNumber', + params: ['latest', false], + }) + const baseFee = (blockResp as RpcBlockResponse).baseFeePerGas + const priorityFee = parseUnits('25', 9).toString() + const callData = encodeFunctionData({ + abi: [initializerAbi] as any, + args: [owners, requiredSignatures, requiredInternalSignatures] as any, + }) + const initData = encodeFunctionData({ + abi: proxy.abi, + functionName: '_setAndInitializeImplementation', + args: [multiSigAddress, callData], + }) + const initGas = await kit.connection.estimateGas({ + from: kit.defaultAccount, + to: proxy.address, + data: initData, + }) + await kit.connection.sendTransaction({ from: kit.defaultAccount, - gas: await initTx.estimateGas({ from: kit.defaultAccount }), + to: proxy.address, + data: initData, + gas: initGas, maxPriorityFeePerGas: priorityFee, maxFeePerGas: (BigInt(baseFee) + BigInt(priorityFee)).toString(), }) - const transferOwnershipMethod = proxy.methods._transferOwnership - const changeOwnerTx = transferOwnershipMethod(proxyAddress) - await changeOwnerTx.send({ + // Hash is returned directly from sendTransaction + const changeOwnerData = encodeFunctionData({ + abi: proxy.abi, + functionName: '_transferOwnership', + args: [proxyAddress], + }) + const changeOwnerGas = await kit.connection.estimateGas({ + from: kit.defaultAccount, + to: proxy.address, + data: changeOwnerData, + }) + await kit.connection.sendTransaction({ from: kit.defaultAccount, - gas: await changeOwnerTx.estimateGas({ from: kit.defaultAccount }), + to: proxy.address, + data: changeOwnerData, + gas: changeOwnerGas, maxPriorityFeePerGas: priorityFee, maxFeePerGas: (BigInt(baseFee) + BigInt(priorityFee)).toString(), }) + // Hash is returned directly from sendTransaction return proxyAddress as StrongAddress } @@ -87,11 +124,11 @@ export async function createMultisig( * * A working example can be found in packages/cli/src/commands/governance/approve-l2.test.ts` */ -export const setupSafeContracts = async (web3: Web3) => { +export const setupSafeContracts = async (provider: Provider) => { // Set up safe 1.3.0 in devchain - await setCode(web3, SAFE_MULTISEND_ADDRESS, SAFE_MULTISEND_CODE) - await setCode(web3, SAFE_MULTISEND_CALL_ONLY_ADDRESS, SAFE_MULTISEND_CALL_ONLY_CODE) - await setCode(web3, SAFE_PROXY_FACTORY_ADDRESS, SAFE_PROXY_FACTORY_CODE) - await setCode(web3, SAFE_PROXY_ADDRESS, SAFE_PROXY_CODE) - await setCode(web3, SAFE_FALLBACK_HANDLER_ADDRESS, SAFE_FALLBACK_HANDLER_CODE) + await setCode(provider, SAFE_MULTISEND_ADDRESS, SAFE_MULTISEND_CODE) + await setCode(provider, SAFE_MULTISEND_CALL_ONLY_ADDRESS, SAFE_MULTISEND_CALL_ONLY_CODE) + await setCode(provider, SAFE_PROXY_FACTORY_ADDRESS, SAFE_PROXY_FACTORY_CODE) + await setCode(provider, SAFE_PROXY_ADDRESS, SAFE_PROXY_CODE) + await setCode(provider, SAFE_FALLBACK_HANDLER_ADDRESS, SAFE_FALLBACK_HANDLER_CODE) } diff --git a/packages/cli/src/test-utils/release-gold.ts b/packages/cli/src/test-utils/release-gold.ts index f8124bfef4..afe2b477b4 100644 --- a/packages/cli/src/test-utils/release-gold.ts +++ b/packages/cli/src/test-utils/release-gold.ts @@ -1,10 +1,11 @@ -import { newReleaseGold } from '@celo/abis/web3/ReleaseGold' +import { releaseGoldABI } from '@celo/abis' import { StrongAddress } from '@celo/base' +import { Connection, Provider } from '@celo/connect' import { REGISTRY_CONTRACT_ADDRESS } from '@celo/contractkit' import { setBalance, setCode, withImpersonatedAccount } from '@celo/dev-utils/anvil-test' import { HOUR, MINUTE, MONTH } from '@celo/dev-utils/test-utils' import BigNumber from 'bignumber.js' -import Web3 from 'web3' +import { encodeFunctionData, parseEther } from 'viem' import { getCurrentTimestamp } from '../utils/cli' // ported from ganache tests @@ -13,7 +14,7 @@ const RELEASE_GOLD_IMPLEMENTATION_CONTRACT_BYTECODE = const RELEASE_GOLD_IMPLEMENTATION_CONTRACT_ADDRESS = '0xDdbe68bEae54dd94465C6bbA2477EE9500ce1974' export async function deployReleaseGoldContract( - web3: Web3, + provider: Provider, ownerMultisigAddress: StrongAddress, beneficiary: StrongAddress, releaseOwner: StrongAddress, @@ -21,22 +22,29 @@ export async function deployReleaseGoldContract( canValidate: boolean = false ): Promise { await setCode( - web3, + provider, RELEASE_GOLD_IMPLEMENTATION_CONTRACT_ADDRESS, RELEASE_GOLD_IMPLEMENTATION_CONTRACT_BYTECODE ) - const contract = newReleaseGold(web3, RELEASE_GOLD_IMPLEMENTATION_CONTRACT_ADDRESS) + // Create contract using Connection's getCeloContract + const connection = new Connection(provider) + const contract = connection.getCeloContract( + releaseGoldABI as any, + RELEASE_GOLD_IMPLEMENTATION_CONTRACT_ADDRESS + ) const releasePeriods = 4 - const amountReleasedPerPeriod = new BigNumber(web3.utils.toWei('10', 'ether')) + const amountReleasedPerPeriod = new BigNumber(parseEther('10').toString()) await withImpersonatedAccount( - web3, + provider, ownerMultisigAddress, async () => { // default values taken from https://github.com/celo-org/celo-monorepo/blob/master/packages/protocol/test-sol/unit/governance/voting/ReleaseGold.t.sol#L146 - await contract.methods - .initialize( + const initData = encodeFunctionData({ + abi: contract.abi, + functionName: 'initialize', + args: [ getCurrentTimestamp() + 5 * MINUTE, HOUR, releasePeriods, @@ -50,15 +58,21 @@ export async function deployReleaseGoldContract( 500, // distribution ratio canValidate, true, - REGISTRY_CONTRACT_ADDRESS - ) - .send({ from: ownerMultisigAddress }) + REGISTRY_CONTRACT_ADDRESS, + ], + }) + await connection.sendTransaction({ + to: contract.address, + data: initData, + from: ownerMultisigAddress, + }) + // Hash is returned directly from sendTransaction }, - new BigNumber(web3.utils.toWei('1', 'ether')) + new BigNumber(parseEther('1').toString()) ) await setBalance( - web3, + provider, RELEASE_GOLD_IMPLEMENTATION_CONTRACT_ADDRESS, amountReleasedPerPeriod.multipliedBy(releasePeriods) ) diff --git a/packages/cli/src/utils/checks.ts b/packages/cli/src/utils/checks.ts index 30ae833098..eb2264bdf9 100644 --- a/packages/cli/src/utils/checks.ts +++ b/packages/cli/src/utils/checks.ts @@ -608,13 +608,16 @@ class CheckBuilder { return validators.read.isValidatorGroup([account]) }), this.withValidators(async (validators, _signer, account) => { - const group = await getValidatorGroup(await this.getClient(), account) + const client = await this.getClient() + const group = await getValidatorGroup(client, account) const [_, duration] = await validators.read.getGroupLockedGoldRequirements() - const waitPeriodEnd = group.membersUpdated.plus(bigintToBigNumber(duration)) - const isDeregisterable = waitPeriodEnd.isLessThan(Date.now() / 1000) + const waitPeriodEnd = group.membersUpdated.plus(bigintToBigNumber(duration)).toNumber() + const latestBlock = await client.getBlock({ blockTag: 'latest' }) + const currentTimestamp = Number(latestBlock.timestamp) + const isDeregisterable = waitPeriodEnd < currentTimestamp if (!isDeregisterable) { console.warn( - `Group will be able to be deregistered: ${new Date(waitPeriodEnd.multipliedBy(1000).toNumber()).toUTCString()}` + `Group will be able to be deregistered: ${new Date(waitPeriodEnd * 1000).toUTCString()}` ) } return isDeregisterable diff --git a/packages/cli/src/utils/cli.ts b/packages/cli/src/utils/cli.ts index c9e2b3c73c..65520f663f 100644 --- a/packages/cli/src/utils/cli.ts +++ b/packages/cli/src/utils/cli.ts @@ -1,10 +1,4 @@ -import { - CeloTransactionObject, - CeloTx, - EventLog, - parseDecodedParams, - TransactionResult, -} from '@celo/connect' +import { CeloTx } from '@celo/connect' import { LockedGoldRequirements } from '@celo/contractkit/lib/wrappers/Validators' import { Errors, ux } from '@oclif/core' import { TransactionResult as SafeTransactionResult } from '@safe-global/types-kit' @@ -24,8 +18,7 @@ import { const CLIError = Errors.CLIError -// TODO: How can we deploy contracts with the Celo provider w/o a CeloTransactionObject? -export async function displayWeb3Tx(name: string, txObj: any, tx?: Omit) { +export async function displayTx(name: string, txObj: any, tx?: Omit) { ux.action.start(`Sending Transaction: ${name}`) const result = await txObj.send(tx) console.log(result) @@ -137,51 +130,6 @@ export async function displayViemTx( - name: string, - txObj: CeloTransactionObject, - tx?: Omit, - displayEventName?: string | string[] -) { - ux.action.start(`Sending Transaction: ${name}`) - try { - const txResult = await txObj.send(tx) - await innerDisplaySendTx(name, txResult, displayEventName) - } catch (e) { - ux.action.stop(`failed: ${(e as Error).message}`) - throw e - } -} - -// to share between displaySendTx and displaySendEthersTxViaCK -async function innerDisplaySendTx( - name: string, - txResult: TransactionResult, - displayEventName?: string | string[] | undefined -) { - const txHash = await txResult.getHash() - - console.log(chalk`SendTransaction: {red.bold ${name}}`) - printValueMap({ txHash }) - - const txReceipt = await txResult.waitReceipt() - ux.action.stop() - - if (displayEventName && txReceipt.events) { - Object.entries(txReceipt.events) - .filter( - ([eventName]) => - (typeof displayEventName === 'string' && eventName === displayEventName) || - displayEventName.includes(eventName) - ) - .forEach(([eventName, log]) => { - const { params } = parseDecodedParams((log as EventLog).returnValues) - console.log(chalk.magenta.bold(`${eventName}:`)) - printValueMap(params, chalk.magenta) - }) - } -} - export function printValueMap(valueMap: Record, color = chalk.yellowBright.bold) { console.log( Object.keys(valueMap) @@ -190,10 +138,6 @@ export function printValueMap(valueMap: Record, color = chalk.yello ) } -export function printValueMap2(valueMap: Map, color = chalk.yellowBright.bold) { - valueMap.forEach((value, key) => console.log(color(`${key}: `) + value)) -} - export function printValueMapRecursive(valueMap: Record) { console.log(toStringValueMapRecursive(valueMap, '')) } diff --git a/packages/cli/src/utils/command.ts b/packages/cli/src/utils/command.ts index c667c8eb61..5d0a90a136 100644 --- a/packages/cli/src/utils/command.ts +++ b/packages/cli/src/utils/command.ts @@ -118,35 +118,6 @@ function parseArray(parseElement: ParseFn): ParseFn { } export const parseAddressArray = parseArray(parseAddress) -export const parseIntRange = (input: string) => { - const range = input - .slice(1, input.length - 1) - .split(':') - .map((s) => parseInt(s, 10)) - if (range.length !== 2) { - throw new Error('range input must be two integers separated by a ":"') - } - - let start: number - if (input.startsWith('[')) { - start = range[0] - } else if (input.startsWith('(')) { - start = range[0] + 1 - } else { - throw new Error('range input must begin with "[" (inclusive) or "(" (exclusive)') - } - - let end: number - if (input.endsWith(']')) { - end = range[1] - } else if (input.endsWith(')')) { - end = range[1] - 1 - } else { - throw new Error('range input must end with "]" (inclusive) or ")" (exclusive)') - } - - return { start, end } -} export function argBuilder(parser: ParseFn) { return (name: string, args?: Parameters[0]) => diff --git a/packages/cli/src/utils/exchange.test.ts b/packages/cli/src/utils/exchange.test.ts deleted file mode 100644 index 3f73b247e4..0000000000 --- a/packages/cli/src/utils/exchange.test.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { calculateExpectedSlippage } from './exchange' - -import BigNumber from 'bignumber.js' - -describe('calculateExpectedSlippage', () => { - describe('when amount is the same', () => { - it('gives zero', () => { - const sellingAmount = new BigNumber(100) - const quotedAmountToReceiveWithBuffer = new BigNumber(110) - const oracleMedianRate = new BigNumber('1.1') - const slippage = 0 // % slippage - // (Executed Price – Expected Price) / Expected Price * 100 - expect( - calculateExpectedSlippage(sellingAmount, quotedAmountToReceiveWithBuffer, oracleMedianRate) - ).toEqual(slippage) - }) - }) - describe('when quotedAmountToReceiveWithBuffer is less than oracle rate', () => { - it('gives a negative amount', () => { - const sellingAmount = new BigNumber(100) - const quotedAmountToReceiveWithBuffer = new BigNumber(105) - const oracleMedianRate = new BigNumber('1.1') - const slippage = -4.761904761904762 // % slippage - // (Executed Price – Expected Price) / Expected Price * 100 - expect( - calculateExpectedSlippage(sellingAmount, quotedAmountToReceiveWithBuffer, oracleMedianRate) - ).toEqual(slippage) - }) - }) - describe('when quotedAmountToReceiveWithBuffer is higher than oracle rate', () => { - it('gives a positive amount', () => { - const sellingAmount = new BigNumber(100) - const quotedAmountToReceiveWithBuffer = new BigNumber(115) - const oracleMedianRate = new BigNumber('1.1') - const slippage = 4.3478260869565215 // % slippage - expect( - calculateExpectedSlippage(sellingAmount, quotedAmountToReceiveWithBuffer, oracleMedianRate) - ).toEqual(slippage) - }) - }) -}) diff --git a/packages/cli/src/utils/exchange.ts b/packages/cli/src/utils/exchange.ts deleted file mode 100644 index fd60614e48..0000000000 --- a/packages/cli/src/utils/exchange.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { ContractKit } from '@celo/contractkit' -import { StableToken, StableTokenInfo, stableTokenInfos } from '@celo/contractkit/lib/celo-tokens' -import BigNumber from 'bignumber.js' -import { binaryPrompt } from './cli' -export const swapArguments = [ - { - name: 'sellAmount', - required: true, - description: 'the amount of sellToken (in wei) to sell', - }, - { - name: 'minBuyAmount', - required: true, - description: 'the minimum amount of buyToken (in wei) expected', - }, - { - name: 'from', - required: true, - }, -] - -export async function checkNotDangerousExchange( - kit: ContractKit, - sellAmount: BigNumber, - quotedAmountToReceiveWithBuffer: BigNumber, - maxDepegPricePercentage: number, - stableTokenInfo: StableTokenInfo = stableTokenInfos[StableToken.USDm], - flipOracle = false -): Promise { - const oracles = await kit.contracts.getSortedOracles() - const oracleMedianRateRaw = (await oracles.medianRate(stableTokenInfo.contract)).rate - const oracleMedianRate = flipOracle - ? new BigNumber(1).div(oracleMedianRateRaw) - : oracleMedianRateRaw - const expectedSlippage = calculateExpectedSlippage( - sellAmount, - quotedAmountToReceiveWithBuffer, - oracleMedianRate - ) - if (Math.abs(expectedSlippage) > Math.abs(maxDepegPricePercentage)) { - const check = await binaryPrompt( - `Warning ${ - stableTokenInfo.symbol - } price here (i.e. on-chain) would be depegged by ${expectedSlippage}% from the oracle prices ${oracleMedianRate.toString()} (i.e. swap prices). Are you sure you want to continue?`, - true - ) - return check - } - - return true -} - -// (Quoted Price – MarketPrice Price) / Quoted Price * 100 -export function calculateExpectedSlippage( - sellAmount: BigNumber, - quotedAmountToReceiveWithBuffer: BigNumber, - oracleMedianRate: BigNumber -) { - const marketPrice = oracleMedianRate - const quotedPrice = quotedAmountToReceiveWithBuffer.dividedBy(sellAmount) - - const priceDifference = quotedPrice.minus(marketPrice) - const slippage = priceDifference.dividedBy(quotedPrice).multipliedBy(100) - console.info(`Quoted Price: ${quotedPrice.decimalPlaces(8).toString()} per token`) - - return slippage.toNumber() -} diff --git a/packages/cli/src/utils/fee-currency.test.ts b/packages/cli/src/utils/fee-currency.test.ts index 13cddecf74..62a6327500 100644 --- a/packages/cli/src/utils/fee-currency.test.ts +++ b/packages/cli/src/utils/fee-currency.test.ts @@ -1,12 +1,11 @@ -import { newKitFromWeb3 } from '@celo/contractkit' +import { newKitFromProvider } from '@celo/contractkit' import { FeeCurrencyDirectoryWrapper } from '@celo/contractkit/lib/wrappers/FeeCurrencyDirectoryWrapper' import { testWithAnvilL2 } from '@celo/dev-utils/anvil-test' -import Web3 from 'web3' import { getFeeCurrencyContractWrapper } from './fee-currency' -testWithAnvilL2('getFeeCurrencyContractWrapper', async (web3: Web3) => { +testWithAnvilL2('getFeeCurrencyContractWrapper', async (provider) => { it('returns FeeCurrencyDirectory for L2 context', async () => { - const kit = newKitFromWeb3(web3) + const kit = newKitFromProvider(provider) const wrapper = await getFeeCurrencyContractWrapper(kit) expect(wrapper).toBeInstanceOf(FeeCurrencyDirectoryWrapper) diff --git a/packages/cli/src/utils/governance.ts b/packages/cli/src/utils/governance.ts index c1dd228c05..770fdd2378 100644 --- a/packages/cli/src/utils/governance.ts +++ b/packages/cli/src/utils/governance.ts @@ -1,8 +1,8 @@ -import { toTxResult } from '@celo/connect' import { ContractKit } from '@celo/contractkit' import { ProposalTransaction } from '@celo/contractkit/lib/wrappers/Governance' import { ProposalBuilder, proposalToJSON, ProposalTransactionJSON } from '@celo/governance' import chalk from 'chalk' +import { waitForTransactionReceipt } from 'viem/actions' import { readJsonSync } from 'fs-extra' export async function checkProposal(proposal: ProposalTransaction[], kit: ContractKit) { @@ -33,17 +33,20 @@ async function tryProposal( try { if (call) { - await kit.web3.eth.call({ + await kit.connection.viemClient.request({ + method: 'eth_call', + params: [{ to: tx.to, from, value: tx.value, data: tx.input }, 'latest'] as any, + }) + } else { + const hash = await kit.connection.sendTransaction({ to: tx.to, from, value: tx.value, data: tx.input, }) - } else { - const txRes = toTxResult( - kit.web3.eth.sendTransaction({ to: tx.to, from, value: tx.value, data: tx.input }) - ) - await txRes.waitReceipt() + await waitForTransactionReceipt(kit.connection.viemClient, { + hash, + }) } console.log(chalk.green(` ${chalk.bold('✔')} Transaction ${i} success!`)) } catch (err: any) { diff --git a/packages/cli/src/utils/identity.ts b/packages/cli/src/utils/identity.ts index fd45ed694b..6ec3be55cc 100644 --- a/packages/cli/src/utils/identity.ts +++ b/packages/cli/src/utils/identity.ts @@ -10,7 +10,7 @@ import { verifyClaim } from '@celo/metadata-claims/lib/verify' import { eqAddress } from '@celo/utils/lib/address' import { concurrentMap } from '@celo/utils/lib/async' import { NativeSigner } from '@celo/utils/lib/signatureUtils' -import { toChecksumAddress } from '@ethereumjs/util' +import { getAddress } from 'viem' import { ux } from '@oclif/core' import humanizeDuration from 'humanize-duration' @@ -70,7 +70,7 @@ export abstract class ClaimCommand extends BaseCommand { protected async getSigner() { const res = await this.parse(this.self) const kit = await this.getKit() - const address = toChecksumAddress(res.flags.from) + const address = getAddress(res.flags.from) return NativeSigner(kit.connection.sign, address) } diff --git a/packages/cli/src/utils/release-gold-base.ts b/packages/cli/src/utils/release-gold-base.ts index bed1d5ceab..b585a2e250 100644 --- a/packages/cli/src/utils/release-gold-base.ts +++ b/packages/cli/src/utils/release-gold-base.ts @@ -1,4 +1,4 @@ -import { newReleaseGold } from '@celo/abis/web3/ReleaseGold' +import { releaseGoldABI } from '@celo/abis' import { StrongAddress } from '@celo/base' import { ReleaseGoldWrapper } from '@celo/contractkit/lib/wrappers/ReleaseGold' import { BaseCommand } from '../base' @@ -37,7 +37,7 @@ export abstract class ReleaseGoldBaseCommand extends BaseCommand { if (!this._releaseGoldWrapper) { this._releaseGoldWrapper = new ReleaseGoldWrapper( kit.connection, - newReleaseGold(kit.connection.web3, await this.contractAddress()), + kit.connection.getCeloContract(releaseGoldABI as any, await this.contractAddress()) as any, kit.contracts ) // Call arbitrary release gold fn to verify `contractAddress` is a releasegold contract. diff --git a/packages/cli/src/utils/require.ts b/packages/cli/src/utils/require.ts index bd6a86fe65..520afcd59d 100644 --- a/packages/cli/src/utils/require.ts +++ b/packages/cli/src/utils/require.ts @@ -1,4 +1,3 @@ -import { CeloTxObject } from '@celo/connect' import { failWith } from './cli' export enum Op { @@ -23,12 +22,3 @@ export function requireOp(value: A, op: Op, expected: A, ctx: string) { failWith(`require(${ctx}) => [${value}, ${expected}]`) } } -export async function requireCall( - callPromise: CeloTxObject, - op: Op, - expected: A, - ctx: string -) { - const value = await callPromise.call() - requireOp(value, op, expected, ctx) -} diff --git a/packages/cli/src/utils/safe.ts b/packages/cli/src/utils/safe.ts index 4782b960e7..5e8b983268 100644 --- a/packages/cli/src/utils/safe.ts +++ b/packages/cli/src/utils/safe.ts @@ -1,46 +1,45 @@ import { StrongAddress } from '@celo/base' -import { CeloTransactionObject } from '@celo/connect' +import { type Provider } from '@celo/connect' import { CeloProvider } from '@celo/connect/lib/celo-provider' import Safe from '@safe-global/protocol-kit' import { MetaTransactionData, TransactionResult } from '@safe-global/types-kit' -import Web3 from 'web3' import { displaySafeTx } from './cli' -export const createSafeFromWeb3 = async ( - web3: Web3, +export const createSafe = async ( + provider: Provider, signer: StrongAddress, safeAddress: StrongAddress ) => { - if (!(web3.currentProvider instanceof CeloProvider)) { - throw new Error('Unexpected web3 provider') + if (!(provider instanceof CeloProvider)) { + throw new Error('Expected CeloProvider') } return await Safe.init({ - provider: web3.currentProvider.toEip1193Provider(), + provider: provider as any, signer, safeAddress, }) } -export const safeTransactionMetadataFromCeloTransactionObject = async ( - tx: CeloTransactionObject, +export const safeTransactionMetadata = ( + encodedData: `0x${string}`, toAddress: StrongAddress, value = '0' -): Promise => { +): MetaTransactionData => { return { to: toAddress, - data: tx.txo.encodeABI(), + data: encodedData, value, } } export const performSafeTransaction = async ( - web3: Web3, + provider: Provider, safeAddress: StrongAddress, safeSigner: StrongAddress, txData: MetaTransactionData ) => { - const safe = await createSafeFromWeb3(web3, safeSigner, safeAddress) + const safe = await createSafe(provider, safeSigner, safeAddress) const approveTxPromise = await createApproveSafeTransactionIfNotApproved(safe, txData, safeSigner) if (approveTxPromise) { diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json index 03f551678e..6be74b4595 100644 --- a/packages/cli/tsconfig.json +++ b/packages/cli/tsconfig.json @@ -11,7 +11,7 @@ "target": "es2020" }, "include": ["src/**/*", "src/commands/dkg/DKG.json", "../dev-utils/dist/cjs/matchers.d.ts"], - "exclude": ["src/**.test.ts"], + "exclude": ["**/*.test.ts"], "ts-node": { "esm": true }