diff --git a/packages/cli/src/base.ts b/packages/cli/src/base.ts index cb048faf6..f02b3b710 100644 --- a/packages/cli/src/base.ts +++ b/packages/cli/src/base.ts @@ -1,32 +1,107 @@ -import { FeeCurrencyDirectory } from '@celo/abis-12/web3/FeeCurrencyDirectory' -import { FeeCurrencyWhitelist } from '@celo/abis/web3/FeeCurrencyWhitelist' import { StrongAddress } from '@celo/base' import { ReadOnlyWallet, isCel2 } from '@celo/connect' import { ContractKit, newKitFromWeb3 } from '@celo/contractkit' -import { - AbstractFeeCurrencyWrapper, - FeeCurrencyInformation, -} from '@celo/contractkit/lib/wrappers/AbstractFeeCurrencyWrapper' +import { AzureHSMWallet } from '@celo/wallet-hsm-azure' import { AddressValidation, newLedgerWalletWithSetup } from '@celo/wallet-ledger' import { LocalWallet } from '@celo/wallet-local' import _TransportNodeHid from '@ledgerhq/hw-transport-node-hid' +import { Command, Flags } from '@oclif/core' +import { CLIError } from '@oclif/core/lib/errors' +import chalk from 'chalk' import net from 'net' import Web3 from 'web3' -import { CeloCommand } from './celo' +import { CustomFlags } from './utils/command' +import { getNodeUrl } from './utils/config' import { getFeeCurrencyContractWrapper } from './utils/fee-currency' -import { nodeIsSynced } from './utils/helpers' +import { requireNodeIsSynced } from './utils/helpers' + +export abstract class BaseCommand extends Command { + static flags = { + privateKey: Flags.string({ + char: 'k', + description: 'Use a private key to sign local transactions with', + hidden: true, + }), + node: Flags.string({ + char: 'n', + description: "URL of the node to run commands against (defaults to 'http://localhost:8545')", + hidden: true, + parse: async (nodeUrl: string) => { + switch (nodeUrl) { + case 'local': + case 'localhost': + return 'http://localhost:8545' + case 'baklava': + return 'https://baklava-forno.celo-testnet.org' + case 'alfajores': + return 'https://alfajores-forno.celo-testnet.org' + case 'mainnet': + case 'forno': + return 'https://forno.celo.org' + default: + return nodeUrl + } + }, + }), + gasCurrency: CustomFlags.gasCurrency({ + description: + 'Use a specific gas currency for transaction fees (defaults to CELO if no gas currency is supplied). It must be a whitelisted token.', + }), + useLedger: Flags.boolean({ + default: false, + hidden: true, + description: 'Set it to use a ledger wallet', + }), + ledgerAddresses: Flags.integer({ + default: 1, + hidden: true, + exclusive: ['ledgerCustomAddresses'], + description: 'If --useLedger is set, this will get the first N addresses for local signing', + }), + ledgerCustomAddresses: Flags.string({ + default: '[0]', + hidden: true, + exclusive: ['ledgerAddresses'], + description: + 'If --useLedger is set, this will get the array of index addresses for local signing. Example --ledgerCustomAddresses "[4,99]"', + }), + useAKV: Flags.boolean({ + default: false, + hidden: true, + description: 'Set it to use an Azure KeyVault HSM', + }), + azureVaultName: Flags.string({ + hidden: true, + description: 'If --useAKV is set, this is used to connect to the Azure KeyVault', + }), + ledgerConfirmAddress: Flags.boolean({ + default: false, + hidden: true, + description: 'Set it to ask confirmation for the address of the transaction from the ledger', + }), + globalHelp: Flags.boolean({ + default: false, + hidden: false, + description: 'View all available global flags', + }), + } + // This specifies whether the node needs to be synced before the command + // can be run. In most cases, this should be `true`, so that's the default. + // For commands that don't require the node is synced, add the following line + // to its definition: + // requireSynced = false + public requireSynced = true -// For the ease of migration BaseCommand is web3+CK -export abstract class BaseCommand extends CeloCommand { private _web3: Web3 | null = null private _kit: ContractKit | null = null - private feeCurrencyContractWrapper: AbstractFeeCurrencyWrapper< - FeeCurrencyWhitelist | FeeCurrencyDirectory - > | null = null + + // Indicates if celocli running in L2 context + private cel2: boolean | null = null async getWeb3() { if (!this._web3) { - const nodeUrl = await this.getNodeUrl() + const res = await this.parse() + const nodeUrl = (res.flags && res.flags.node) || getNodeUrl(this.config.configDir) this._web3 = nodeUrl && nodeUrl.endsWith('.ipc') ? new Web3(new Web3.providers.IpcProvider(nodeUrl, net)) @@ -44,8 +119,8 @@ export abstract class BaseCommand extends CeloCommand { } async newWeb3() { - const nodeUrl = await this.getNodeUrl() - + const res = await this.parse() + const nodeUrl = (res.flags && res.flags.node) || getNodeUrl(this.config.configDir) return nodeUrl && nodeUrl.endsWith('.ipc') ? new Web3(new Web3.providers.IpcProvider(nodeUrl, net)) : new Web3(nodeUrl) @@ -65,10 +140,19 @@ export abstract class BaseCommand extends CeloCommand { } async init() { - super.init() - + if (this.requireSynced) { + const web3 = await this.getWeb3() + await requireNodeIsSynced(web3) + } const kit = await this.getKit() const res = await this.parse() + if (res.flags.globalHelp) { + console.log(chalk.red.bold('GLOBAL OPTIONS')) + Object.entries(BaseCommand.flags).forEach(([name, flag]) => { + console.log(chalk.black(` --${name}`).padEnd(40) + chalk.gray(`${flag.description}`)) + }) + process.exit(0) + } if (res.flags.useLedger) { try { @@ -99,6 +183,16 @@ export abstract class BaseCommand extends CeloCommand { console.log('Check if the ledger is connected and logged.') throw err } + } else if (res.flags.useAKV) { + try { + const akvWallet = new AzureHSMWallet(res.flags.azureVaultName) + await akvWallet.init() + console.log(`Found addresses: ${akvWallet.getAccounts()}`) + this._wallet = akvWallet + } catch (err) { + console.log(`Failed to connect to AKV ${err}`) + throw err + } } else { this._wallet = new LocalWallet() } @@ -106,10 +200,52 @@ export abstract class BaseCommand extends CeloCommand { if (res.flags.from) { kit.defaultAccount = res.flags.from } + + const gasCurrencyFlag = res.flags.gasCurrency as StrongAddress | undefined + + if (gasCurrencyFlag) { + const feeCurrencyContract = await getFeeCurrencyContractWrapper(kit, await this.isCel2()) + const validFeeCurrencies = await feeCurrencyContract.getAddresses() + + if ( + validFeeCurrencies.map((x) => x.toLocaleLowerCase()).includes(gasCurrencyFlag.toLowerCase()) + ) { + kit.setFeeCurrency(gasCurrencyFlag) + } else { + const pairs = ( + await feeCurrencyContract.getFeeCurrencyInformation(validFeeCurrencies as StrongAddress[]) + ).map( + ({ name, symbol, address, adaptedToken }) => + `${address} - ${name || 'unknown name'} (${symbol || 'N/A'})${ + adaptedToken ? ` (adapted token: ${adaptedToken})` : '' + }` + ) + + throw new Error( + `${gasCurrencyFlag} is not a valid fee currency. Available currencies:\n${pairs.join( + '\n' + )}` + ) + } + } } - async finally(arg?: Error): Promise { + async finally(arg: Error | undefined): Promise { try { + if (arg) { + if (!(arg instanceof CLIError)) { + console.error( + ` +Received an error during command execution, if you believe this is a bug you can create an issue here: + +https://github.com/celo-org/developer-tooling/issues/new?assignees=&labels=bug+report&projects=&template=BUG-FORM.yml + +`, + arg + ) + } + } + if (this._kit !== null) { this._kit.connection.stop() } @@ -117,39 +253,14 @@ export abstract class BaseCommand extends CeloCommand { this.log(`Failed to close the connection: ${error}`) } - super.finally(arg) - } - - protected async checkIfL2(): Promise { - return await isCel2(await this.getWeb3()) + return super.finally(arg) } - protected async getFeeCurrencyContractWrapper() { - if (!this.feeCurrencyContractWrapper) { - this.feeCurrencyContractWrapper = await getFeeCurrencyContractWrapper( - await this.getKit(), - await this.isCel2() - ) + protected async isCel2(): Promise { + if (this.cel2 === null) { + this.cel2 = await isCel2(await this.getWeb3()) } - return this.feeCurrencyContractWrapper - } - - protected async getFeeCurrencyInformation( - addresses: StrongAddress[] - ): Promise { - const feeCurrencyContractWrapper = await this.getFeeCurrencyContractWrapper() - - return await feeCurrencyContractWrapper.getFeeCurrencyInformation(addresses) - } - - protected async getSupportedFeeCurrencyAddresses(): Promise { - const feeCurrencyContractWrapper = await this.getFeeCurrencyContractWrapper() - - return await feeCurrencyContractWrapper.getAddresses() - } - - protected async checkIfSynced() { - return await nodeIsSynced(await this.getWeb3()) + return !!this.cel2 } } diff --git a/packages/cli/src/celo.ts b/packages/cli/src/celo.ts deleted file mode 100644 index 84eeab91d..000000000 --- a/packages/cli/src/celo.ts +++ /dev/null @@ -1,180 +0,0 @@ -import { StrongAddress } from '@celo/base' -import { Command, Flags } from '@oclif/core' -import { CLIError } from '@oclif/core/lib/errors' -import chalk from 'chalk' -import { FeeCurrencyInformation } from './packages-to-be/fee-currency' -import { failWith } from './utils/cli' -import { CustomFlags } from './utils/command' -import { getNodeUrl } from './utils/config' - -export abstract class CeloCommand extends Command { - static flags = { - privateKey: Flags.string({ - char: 'k', - description: 'Use a private key to sign local transactions with', - hidden: true, - }), - node: Flags.string({ - char: 'n', - description: "URL of the node to run commands against (defaults to 'http://localhost:8545')", - hidden: true, - parse: async (nodeUrl: string) => { - switch (nodeUrl) { - case 'local': - case 'localhost': - return 'http://localhost:8545' - case 'baklava': - return 'https://baklava-forno.celo-testnet.org' - case 'alfajores': - return 'https://alfajores-forno.celo-testnet.org' - case 'mainnet': - case 'forno': - return 'https://forno.celo.org' - default: - return nodeUrl - } - }, - }), - gasCurrency: CustomFlags.gasCurrency({ - description: - 'Use a specific gas currency for transaction fees (defaults to CELO if no gas currency is supplied). It must be a whitelisted token.', - }), - useLedger: Flags.boolean({ - default: false, - hidden: true, - description: 'Set it to use a ledger wallet', - }), - ledgerAddresses: Flags.integer({ - default: 1, - hidden: true, - exclusive: ['ledgerCustomAddresses'], - description: 'If --useLedger is set, this will get the first N addresses for local signing', - }), - ledgerCustomAddresses: Flags.string({ - default: '[0]', - hidden: true, - exclusive: ['ledgerAddresses'], - description: - 'If --useLedger is set, this will get the array of index addresses for local signing. Example --ledgerCustomAddresses "[4,99]"', - }), - useAKV: Flags.boolean({ - default: false, - hidden: true, - description: 'Set it to use an Azure KeyVault HSM', - }), - azureVaultName: Flags.string({ - hidden: true, - description: 'If --useAKV is set, this is used to connect to the Azure KeyVault', - }), - ledgerConfirmAddress: Flags.boolean({ - default: false, - hidden: true, - description: 'Set it to ask confirmation for the address of the transaction from the ledger', - }), - globalHelp: Flags.boolean({ - default: false, - hidden: false, - description: 'View all available global flags', - }), - } - - // This specifies whether the node needs to be synced before the command - // can be run. In most cases, this should be `true`, so that's the default. - // For commands that don't require the node is synced, add the following line - // to its definition: - // requireSynced = false - public requireSynced = true - - // Indicates if celocli running in L2 context - private cel2: boolean | null = null - - // TODO a getter? - protected feeCurrencyAddress: StrongAddress | null = null - - protected abstract checkIfSynced(): Promise - - protected abstract getSupportedFeeCurrencyAddresses(): Promise - - protected abstract getFeeCurrencyInformation( - addresses: StrongAddress[] - ): Promise - - protected abstract checkIfL2(): Promise - - protected async getNodeUrl(): Promise { - const res = await this.parse() - - return (res.flags && res.flags.node) || getNodeUrl(this.config.configDir) - } - - async init() { - if (this.requireSynced) { - if (!(await this.checkIfSynced())) { - failWith('Node is not currently synced. Run node:synced to check its status.') - } - } - - const res = await this.parse() - - if (res.flags.globalHelp) { - console.log(chalk.red.bold('GLOBAL OPTIONS')) - Object.entries(CeloCommand.flags).forEach(([name, flag]) => { - console.log(chalk.black(` --${name}`).padEnd(40) + chalk.gray(`${flag.description}`)) - }) - process.exit(0) - } - - const gasCurrencyFlag = res.flags.gasCurrency as StrongAddress | undefined - - if (gasCurrencyFlag) { - const validFeeCurrencies = await this.getSupportedFeeCurrencyAddresses() - - if ( - validFeeCurrencies.map((x) => x.toLocaleLowerCase()).includes(gasCurrencyFlag.toLowerCase()) - ) { - this.feeCurrencyAddress = gasCurrencyFlag - } else { - const pairs = ( - await this.getFeeCurrencyInformation(validFeeCurrencies as StrongAddress[]) - ).map( - ({ name, symbol, address, adaptedToken }) => - `${address} - ${name || 'unknown name'} (${symbol || 'N/A'})${ - adaptedToken ? ` (adapted token: ${adaptedToken})` : '' - }` - ) - - throw new Error( - `${gasCurrencyFlag} is not a valid fee currency. Available currencies:\n${pairs.join( - '\n' - )}` - ) - } - } - } - - async finally(arg: Error | undefined): Promise { - if (arg) { - if (!(arg instanceof CLIError)) { - console.error( - ` -Received an error during command execution, if you believe this is a bug you can create an issue here: - -https://github.com/celo-org/developer-tooling/issues/new?assignees=&labels=bug+report&projects=&template=BUG-FORM.yml - -`, - arg - ) - } - } - - return super.finally(arg) - } - - protected async isCel2(): Promise { - if (this.cel2 === null) { - this.cel2 = await this.checkIfL2() - } - - return !!this.cel2 - } -} diff --git a/packages/cli/src/commands/network/contracts.ts b/packages/cli/src/commands/network/contracts.ts index f7e9ea18b..81d65d027 100644 --- a/packages/cli/src/commands/network/contracts.ts +++ b/packages/cli/src/commands/network/contracts.ts @@ -2,7 +2,6 @@ import { ux } from '@oclif/core' import { iCeloVersionedContractABI, proxyABI } from '@celo/abis' import { concurrentMap, NULL_ADDRESS, StrongAddress } from '@celo/base' -import { CeloCommand } from '../../celo' import { CeloContract, RegisteredContracts } from '../../packages-to-be/contracts' import { ViemCommand } from '../../viem' @@ -20,7 +19,7 @@ export default class Contracts extends ViemCommand { static description = 'Lists Celo core contracts and their addresses.' static flags = { - ...CeloCommand.flags, + ...ViemCommand.flags, ...(ux.table.flags() as object), } diff --git a/packages/cli/src/test-utils/cliUtils.ts b/packages/cli/src/test-utils/cliUtils.ts index 0014a6656..47874ef1d 100644 --- a/packages/cli/src/test-utils/cliUtils.ts +++ b/packages/cli/src/test-utils/cliUtils.ts @@ -1,11 +1,9 @@ -import { Interfaces } from '@oclif/core' +import { Command, Interfaces } from '@oclif/core' import Web3 from 'web3' -import { BaseCommand } from '../base' -import { CeloCommand } from '../celo' type AbstractConstructor = new (...args: any[]) => T -interface Runner extends AbstractConstructor { - run: typeof BaseCommand.run +interface Runner extends AbstractConstructor { + run: typeof Command.run } export async function testLocallyWithWeb3Node( diff --git a/packages/cli/src/viem.ts b/packages/cli/src/viem.ts index 032e151ef..79accf80c 100644 --- a/packages/cli/src/viem.ts +++ b/packages/cli/src/viem.ts @@ -1,35 +1,33 @@ import { StrongAddress } from '@celo/base' -import { FeeCurrencyInformation } from '@celo/contractkit/lib/wrappers/AbstractFeeCurrencyWrapper' +import { Command } from '@oclif/core' +import { CLIError } from '@oclif/core/lib/errors' +import chalk from 'chalk' import { createPublicClient, extractChain, http, HttpTransport, PublicClient } from 'viem' import { celo, celoAlfajores } from 'viem/chains' -import { CeloCommand } from './celo' +import { BaseCommand } from './base' import { ContractAddressResolver, ViemAddressResolver } from './packages-to-be/address-resolver' -import { FeeCurrencyProvider, ViemFeeCurrencyProvider } from './packages-to-be/fee-currency' +import { + FeeCurrencyInformation, + FeeCurrencyProvider, + ViemFeeCurrencyProvider, +} from './packages-to-be/fee-currency' import { L2Resolver, ViemL2Resolver } from './packages-to-be/l2-resolver' +import { failWith } from './utils/cli' +import { getNodeUrl } from './utils/config' + +export abstract class ViemCommand extends Command { + static flags = BaseCommand.flags + + protected requireSynced = true + + // Indicates if celocli running in L2 context + private cel2: boolean | null = null -export abstract class ViemCommand extends CeloCommand { private publicClient?: PublicClient private addressResolver?: ContractAddressResolver private l2Resolver?: L2Resolver private feeCurrencyProvider?: FeeCurrencyProvider - async init() { - super.init() - - const res = await this.parse() - - if (res.flags.useLedger) { - // TODO use viem-account-ledger - } else { - // TODO instantiate local wallet client - } - - if (res.flags.from) { - // TODO set default account? - } - } - - // TODO possibly create wallet client and extend it with public actions protected async getPublicClient(): Promise> { if (!this.publicClient) { const nodeUrl = await this.getNodeUrl() @@ -68,6 +66,66 @@ export abstract class ViemCommand extends CeloCommand { return this.l2Resolver } + protected async checkIfSynced(): Promise { + return true + } + + protected async checkIfL2(): Promise { + const l2Resolver = await this.getL2Resolver() + + return l2Resolver.resolve() + } + + protected async getNodeUrl(): Promise { + const res = await this.parse() + + return (res.flags && res.flags.node) || getNodeUrl(this.config.configDir) + } + + async init() { + if (this.requireSynced) { + if (!(await this.checkIfSynced())) { + failWith('Node is not currently synced. Run node:synced to check its status.') + } + } + + const res = await this.parse() + + if (res.flags.globalHelp) { + console.log(chalk.red.bold('GLOBAL OPTIONS')) + Object.entries(ViemCommand.flags).forEach(([name, flag]) => { + console.log(chalk.black(` --${name}`).padEnd(40) + chalk.gray(`${flag.description}`)) + }) + process.exit(0) + } + } + + async finally(arg: Error | undefined): Promise { + if (arg) { + if (!(arg instanceof CLIError)) { + console.error( + ` +Received an error during command execution, if you believe this is a bug you can create an issue here: + +https://github.com/celo-org/developer-tooling/issues/new?assignees=&labels=bug+report&projects=&template=BUG-FORM.yml + +`, + arg + ) + } + } + + return super.finally(arg) + } + + protected async isCel2(): Promise { + if (this.cel2 === null) { + this.cel2 = await this.checkIfL2() + } + + return !!this.cel2 + } + protected async getFeeCurrencyProvider(): Promise { if (!this.feeCurrencyProvider) { this.feeCurrencyProvider = new ViemFeeCurrencyProvider( @@ -80,12 +138,6 @@ export abstract class ViemCommand extends CeloCommand { return this.feeCurrencyProvider } - protected async checkIfL2(): Promise { - const l2Resolver = await this.getL2Resolver() - - return l2Resolver.resolve() - } - protected async getFeeCurrencyInformation( addresses: StrongAddress[] ): Promise { @@ -99,9 +151,4 @@ export abstract class ViemCommand extends CeloCommand { return feeCurrencyProvider.getAddresses() } - - protected async checkIfSynced() { - // TODO implement - return true - } } diff --git a/packages/dev-utils/src/anvil-test.ts b/packages/dev-utils/src/anvil-test.ts index 0bb972456..3aa2ad778 100644 --- a/packages/dev-utils/src/anvil-test.ts +++ b/packages/dev-utils/src/anvil-test.ts @@ -64,10 +64,9 @@ export function testWithAnvilL2(name: string, fn: (web3: Web3) => void) { function testWithAnvil(stateFilePath: string, name: string, fn: (web3: Web3) => void) { const anvil = createInstance(stateFilePath) - const web3 = new Web3(`http://127.0.0.1:${anvil.port}`) // for each test suite, we start and stop a new anvil instance - return testWithWeb3(name, web3, fn, { + return testWithWeb3(name, `http://127.0.0.1:${anvil.port}`, fn, { runIf: process.env.RUN_ANVIL_TESTS === 'true' || typeof process.env.RUN_ANVIL_TESTS === 'undefined', hooks: { diff --git a/packages/dev-utils/src/ganache-test.ts b/packages/dev-utils/src/ganache-test.ts index ae3587513..4a17a3474 100644 --- a/packages/dev-utils/src/ganache-test.ts +++ b/packages/dev-utils/src/ganache-test.ts @@ -16,7 +16,7 @@ export async function mineBlocks(blocks: number, web3: Web3) { } export function testWithGanache(name: string, fn: (web3: Web3) => void) { - return testWithWeb3(name, new Web3('http://localhost:8545'), fn, { + return testWithWeb3(name, 'http://localhost:8545', fn, { runIf: shouldRunGanacheTests(), }) } diff --git a/packages/dev-utils/src/test-utils.ts b/packages/dev-utils/src/test-utils.ts index b5e5205bf..6353f3d46 100644 --- a/packages/dev-utils/src/test-utils.ts +++ b/packages/dev-utils/src/test-utils.ts @@ -75,13 +75,14 @@ type TestWithWeb3Hooks = { */ export function testWithWeb3( name: string, - web3: Web3, + rpcUrl: string, fn: (web3: Web3) => void, options: { hooks?: TestWithWeb3Hooks 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