diff --git a/packages/cli/src/commands/governance/build-proposals.test.ts b/packages/cli/src/commands/governance/build-proposals.test.ts new file mode 100644 index 000000000..cd557a3d2 --- /dev/null +++ b/packages/cli/src/commands/governance/build-proposals.test.ts @@ -0,0 +1,54 @@ +import CeloTokenABI from '@celo/abis/GoldToken.json' +import { testWithAnvilL2 } from '@celo/dev-utils/lib/anvil-test' +import { readJSON } from 'fs-extra' +import inquirer from 'inquirer' +import Web3 from 'web3' +import { testLocallyWithWeb3Node } from '../../test-utils/cliUtils' +import BuildProposal from './build-proposal' + +process.env.NO_SYNCCHECK = 'true' + +jest.mock('inquirer') + +testWithAnvilL2('governance:build-proposal cmd', (web3: Web3) => { + describe('building proposal to transfer funds from governance', () => { + beforeEach(async () => { + const promptSpy = jest + .spyOn(inquirer, 'prompt') + .mockResolvedValueOnce({ 'Celo Contract': 'GoldToken' }) + .mockResolvedValueOnce({ 'GoldToken Function': 'transfer' }) + CeloTokenABI.abi + .find((f) => f.name === 'transfer')! + .inputs!.forEach((input) => { + switch (input.type) { + case 'address': + promptSpy.mockResolvedValueOnce({ + [input.name!]: '0x19F78d207493Bf6f7E8D54900d01bb387F211b28', + }) + break + case 'uint256': + promptSpy.mockResolvedValueOnce({ [input.name!]: '1000000000000000000' }) + break + } + }) + promptSpy.mockResolvedValueOnce({ 'Celo Contract': '✔ done' }) + }) + it('generates the json', async () => { + await testLocallyWithWeb3Node(BuildProposal, ['--output', './transactions.json'], web3) + const result = await readJSON('./transactions.json') + expect(result).toMatchInlineSnapshot(` + [ + { + "args": [ + "0x19F78d207493Bf6f7E8D54900d01bb387F211b28", + "1000000000000000000", + ], + "contract": "GoldToken", + "function": "transfer", + "value": "0", + }, + ] + `) + }) + }) +}) diff --git a/packages/sdk/governance/package.json b/packages/sdk/governance/package.json index 132424292..8f25d9c20 100644 --- a/packages/sdk/governance/package.json +++ b/packages/sdk/governance/package.json @@ -22,6 +22,7 @@ }, "dependencies": { "@celo/abis": "11.0.0", + "@celo/abis-12": "npm:@celo/abis@12.0.0-canary.60", "@celo/base": "^7.0.0-beta.0", "@celo/connect": "^6.0.3-beta.0", "@celo/contractkit": "^9.0.0-beta.0", diff --git a/packages/sdk/governance/src/interactive-proposal-builder.test.ts b/packages/sdk/governance/src/interactive-proposal-builder.test.ts index 308a173e9..8e53485bb 100644 --- a/packages/sdk/governance/src/interactive-proposal-builder.test.ts +++ b/packages/sdk/governance/src/interactive-proposal-builder.test.ts @@ -1,11 +1,22 @@ -import { newKitFromWeb3 } from '@celo/contractkit' +import { newKitFromWeb3, RegisteredContracts } from '@celo/contractkit' import inquirer from 'inquirer' -import { InteractiveProposalBuilder } from './interactive-proposal-builder' -import { ProposalBuilder } from './proposals' +import { InteractiveProposalBuilder, requireABI } from './interactive-proposal-builder' +import { ProposalBuilder } from './proposal-builder' jest.mock('inquirer') import { testWithAnvilL2 } from '@celo/dev-utils/lib/anvil-test' +describe('all registered contracts can be required', () => { + RegisteredContracts.forEach((contract) => { + it(`required ${contract} contract`, async () => { + const contractABI = requireABI(contract) + expect(contractABI).toBeDefined() + expect(Array.isArray(contractABI)).toBeTruthy() + expect(contractABI.filter).toBeDefined() + }) + }) +}) + testWithAnvilL2('InteractiveProposalBuilder', (web3) => { let builder: ProposalBuilder let interactiveBuilder: InteractiveProposalBuilder diff --git a/packages/sdk/governance/src/interactive-proposal-builder.ts b/packages/sdk/governance/src/interactive-proposal-builder.ts index f0ab0a23f..6fed2e976 100644 --- a/packages/sdk/governance/src/interactive-proposal-builder.ts +++ b/packages/sdk/governance/src/interactive-proposal-builder.ts @@ -4,8 +4,9 @@ import { CeloContract, RegisteredContracts } from '@celo/contractkit' import { isValidAddress } from '@celo/utils/lib/address' import BigNumber from 'bignumber.js' import inquirer from 'inquirer' -import type { ProposalTransactionJSON } from './proposals' -import { ProposalBuilder } from './proposals' +import { ProposalBuilder } from './proposal-builder' + +import type { ProposalTransactionJSON } from './' const DONE_CHOICE = '✔ done' @@ -36,9 +37,8 @@ export class InteractiveProposalBuilder { } const contractName = choice as CeloContract - console.warn('DEBUG', contractPromptName, contractName, contractAnswer) - const contractABI = require('@celo/abis/web3/' + contractName).ABI as ABIDefinition[] + const contractABI = requireABI(contractName) const txMethods = contractABI.filter( (def) => def.type === 'function' && def.stateMutability !== 'view' @@ -119,3 +119,21 @@ export class InteractiveProposalBuilder { return transactions } } +export function requireABI(contractName: CeloContract): ABIDefinition[] { + // search thru multiple paths to find the ABI + 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-12/web3/${subPath ?? ''}${contractName}`).ABI as ABIDefinition[] + } catch { + return null + } +} diff --git a/packages/sdk/governance/src/proposal-builder.test.ts b/packages/sdk/governance/src/proposal-builder.test.ts new file mode 100644 index 000000000..1b6c64032 --- /dev/null +++ b/packages/sdk/governance/src/proposal-builder.test.ts @@ -0,0 +1,138 @@ +import { AbiItem } from '@celo/connect' +import { CeloContract, ContractKit, newKit } from '@celo/contractkit' +import BigNumber from 'bignumber.js' +import { ProposalBuilder } from './proposal-builder' +describe('ProposalBuilder', () => { + let kit: ContractKit + let proposalBuilder: ProposalBuilder + + beforeEach(() => { + kit = newKit('https://alfajores-forno.celo-testnet.org') + proposalBuilder = new ProposalBuilder(kit) + }) + + describe('build', () => { + it('when no tx are in memory builds an empty proposal', async () => { + const proposal = await proposalBuilder.build() + expect(proposal).toEqual([]) + }) + }) + + describe('addWeb3Tx', () => { + it('adds and builds a Web3 transaction', async () => { + const wrapper = await kit.contracts.getGovernance() + const tx = await wrapper.approve(new BigNumber('125')) + proposalBuilder.addWeb3Tx(tx.txo, { to: '0x5678', value: '1000' }) + const proposal = await proposalBuilder.build() + expect(proposal).toEqual([ + { + to: '0x5678', + value: '1000', + input: + '0x5d35a3d9000000000000000000000000000000000000000000000000000000000000007d0000000000000000000000000000000000000000000000000000000000000038', + }, + ]) + }) + }) + + describe('addProxyRepointingTx', () => { + it('adds and builds a proxy repointing transaction', async () => { + const contract = CeloContract.GoldToken + const newImplementationAddress = '0x471ece3750da237f93b8e339c536989b8978a438' + + proposalBuilder.addProxyRepointingTx(contract, newImplementationAddress) + const proposal = await proposalBuilder.build() + + expect(proposal.length).toBe(1) + expect(proposal[0].to).toBeDefined() + expect(proposal[0].value).toBe('0') + }) + }) + + describe('setRegistryAddition', () => { + it('sets and gets registry addition', () => { + const contract = CeloContract.GoldToken + const address = '0x471ece3750da237f93b8e339c536989b8978a438' + + proposalBuilder.setRegistryAddition(contract, address) + const result = proposalBuilder.getRegistryAddition(contract) + + expect(result).toBe(address) + }) + }) + + describe('isRegistryContract', () => { + it('identifies registry contracts', () => { + const contract = CeloContract.GoldToken + const address = '0x471ece3750da237f93b8e339c536989b8978a438' + + proposalBuilder.setRegistryAddition(contract, address) + const result = proposalBuilder.isRegistryContract(contract) + + expect(result).toBe(true) + }) + }) + + describe('buildCallToExternalContract', () => { + it('builds call to external contract', async () => { + const tx = { + function: 'testFunction', + args: [], + value: '0', + address: '0xa435d2BaBDF80A66eD06A8D981edFE6f5DdAeCfB', + } + + const abiItem: AbiItem = { + name: 'testFunction', + type: 'function', + inputs: [], + outputs: [], + } + + proposalBuilder.lookupExternalMethodABI = async () => abiItem + + const result = await proposalBuilder.buildCallToExternalContract(tx) + + expect(result).toEqual({ + to: '0xa435d2BaBDF80A66eD06A8D981edFE6f5DdAeCfB', + value: '0', + input: '0xe16b4a9b', + }) + }) + }) + + describe('buildCallToCoreContract', () => { + it('builds call to core contract', async () => { + const tx = { + contract: CeloContract.GoldToken, + function: 'transfer', + args: ['0xa435d2BaBDF80A66eD06A8D981edFE6f5DdAeCfB', '1000'], + value: '0', + } + const result = await proposalBuilder.buildCallToCoreContract(tx) + + expect(result.to).toBeDefined() + expect(result.value).toBe('0') + expect(result.input).toBeDefined() + }) + }) + + describe('addJsonTx', () => { + it('adds and builds a JSON transaction', async () => { + const tx = { + contract: CeloContract.GoldToken, + function: 'transfer', + args: ['0xa435d2BaBDF80A66eD06A8D981edFE6f5DdAeCfB', '1000'], + value: '0', + } + + proposalBuilder.addJsonTx(tx) + const proposal = await proposalBuilder.build() + + expect(proposal.length).toBe(1) + expect(proposal[0].to).toBeDefined() + expect(proposal[0].value).toBe('0') + expect(proposal[0].input).toBeDefined() + }) + }) +}) diff --git a/packages/sdk/governance/src/proposal-builder.ts b/packages/sdk/governance/src/proposal-builder.ts new file mode 100644 index 000000000..b91df95f6 --- /dev/null +++ b/packages/sdk/governance/src/proposal-builder.ts @@ -0,0 +1,262 @@ +import { + AbiItem, + CeloTransactionObject, + CeloTxObject, + Contract, + signatureToAbiDefinition, +} from '@celo/connect' +import { + CeloContract, + ContractKit, + RegisteredContracts, + SET_AND_INITIALIZE_IMPLEMENTATION_ABI, + getInitializeAbiOfImplementation, + 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 { + ExternalProposalTransactionJSON, + ProposalTransactionJSON, + ProposalTxParams, + RegistryAdditions, + debug, + isProxySetAndInitFunction, + isProxySetFunction, + isRegistryRepoint, + registryRepointArgs, +} from './proposals' + +/** + * Builder class to construct proposals from JSON or transaction objects. + */ + +export class ProposalBuilder { + externalCallProxyRepoint: Map = new Map() + + constructor( + private readonly kit: ContractKit, + private readonly builders: (() => Promise)[] = [], + public readonly registryAdditions: RegistryAdditions = {} + ) {} + + /** + * Build calls all of the added build steps and returns the final proposal. + * @returns A constructed Proposal object (i.e. a list of ProposalTransaction) + */ + build = async () => { + const ret = [] + for (const builder of this.builders) { + ret.push(await builder()) + } + return ret + } + + /** + * Converts a Web3 transaction into a proposal transaction object. + * @param tx A Web3 transaction object to convert. + * @param params Parameters for how the transaction should be executed. + */ + fromWeb3tx = (tx: CeloTxObject, params: ProposalTxParams): ProposalTransaction => ({ + value: params.value, + to: params.to, + input: tx.encodeABI(), + }) + + /** + * Adds a transaction to set the implementation on a proxy to the given address. + * @param contract Celo contract name of the proxy which should have its implementation set. + * @param newImplementationAddress Address of the new contract implementation. + */ + 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', + } + ) + }) + } + + /** + * Adds a Web3 transaction to the list for proposal construction. + * @param tx A Web3 transaction object to add to the proposal. + * @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") + } + // TODO fix type of value + this.addWeb3Tx(tx.txo, { to, value: valueToString(value.toString()) }) + } + + setRegistryAddition = (contract: CeloContract, address: string) => + (this.registryAdditions[stripProxy(contract)] = address) + + getRegistryAddition = (contract: CeloContract): string | undefined => + this.registryAdditions[stripProxy(contract)] + + isRegistryContract = (contract: CeloContract) => + 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 potentialABIs = metadata?.abiForMethod(tx.function) ?? [] + return ( + potentialABIs.find((abi) => { + try { + abiCoder.encodeFunctionCall(abi, this.transformArgs(abi, tx.args)) + return true + } catch { + return false + } + }) || null + ) + } + + buildCallToExternalContract = async ( + tx: ExternalProposalTransactionJSON + ): Promise => { + if (!tx.address || !isValidAddress(tx.address)) { + throw new Error(`${tx.contract} is not a core celo contract so address must be specified`) + } + + if (tx.function === '') { + return { input: '', to: tx.address, value: tx.value } + } + + let methodABI: AbiItem | null = await this.lookupExternalMethodABI(tx.address, tx) + if (methodABI === null) { + const proxyImpl = this.externalCallProxyRepoint.has(tx.address) + ? this.externalCallProxyRepoint.get(tx.address) + : await tryGetProxyImplementation(this.kit.connection, tx.address) + + if (proxyImpl) { + methodABI = await this.lookupExternalMethodABI(proxyImpl, tx) + } + } + + if (methodABI === null) { + methodABI = signatureToAbiDefinition(tx.function) + } + + const input = this.kit.connection + .getAbiCoder() + .encodeFunctionCall(methodABI, this.transformArgs(methodABI, tx.args)) + 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( + `ABI inputs length ${abi.inputs?.length} does not match args length ${args.length}` + ) + } + const res = [] + for (let i = 0; i < args.length; i++) { + const input = abi.inputs![i] + if (input.type === 'tuple') { + // support of structs and tuples + res.push(JSON.parse(args[i])) + } else { + res.push(args[i]) + } + } + return res + } + + buildCallToCoreContract = async (tx: ProposalTransactionJSON): Promise => { + // Account for canonical registry addresses from current proposal + const address = + this.getRegistryAddition(tx.contract) ?? (await this.kit.registry.addressFor(tx.contract)) + + if (tx.address && address !== tx.address) { + throw new Error(`Address mismatch for ${tx.contract}: ${address} !== ${tx.address}`) + } + + 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]) + } + + const contract = await this.kit._web3Contracts.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 }) + } + + fromJsonTx = async ( + tx: ProposalTransactionJSON | ExternalProposalTransactionJSON + ): Promise => { + if (isRegistryRepoint(tx)) { + // Update canonical registry addresses + const args = registryRepointArgs(tx) + this.setRegistryAddition(args.name, args.address) + } + + if (isProxySetAndInitFunction(tx) || isProxySetFunction(tx)) { + console.log(tx.address + ' is a proxy, repointing to ' + tx.args[0]) + this.externalCallProxyRepoint.set(tx.address || (tx.contract as string), tx.args[0] as string) + } + + const strategies = [this.buildCallToCoreContract, this.buildCallToExternalContract] + + for (const strategy of strategies) { + try { + return await strategy(tx as ProposalTransactionJSON) + } catch (e) { + debug("Couldn't build transaction with strategy %s: %O", strategy.name, e) + } + } + + throw new Error(`Couldn't build call for transaction: ${JSON.stringify(tx)}`) + } + + addJsonTx = (tx: ProposalTransactionJSON) => this.builders.push(async () => this.fromJsonTx(tx)) +} diff --git a/packages/sdk/governance/src/proposals.ts b/packages/sdk/governance/src/proposals.ts index 928ce7b46..85add6f3c 100644 --- a/packages/sdk/governance/src/proposals.ts +++ b/packages/sdk/governance/src/proposals.ts @@ -2,47 +2,29 @@ import { ABI as GovernanceABI } from '@celo/abis/web3/Governance' import { ABI as RegistryABI } from '@celo/abis/web3/Registry' import { Address, trimLeading0x } from '@celo/base/lib/address' -import { - AbiCoder, - AbiItem, - CeloTransactionObject, - CeloTxObject, - CeloTxPending, - Contract, - getAbiByName, - parseDecodedParams, - signatureToAbiDefinition, -} from '@celo/connect' -import { - CeloContract, - ContractKit, - RegisteredContracts, - REGISTRY_CONTRACT_ADDRESS, -} from '@celo/contractkit' +import { AbiCoder, CeloTxPending, getAbiByName, parseDecodedParams } from '@celo/connect' +import { CeloContract, ContractKit, REGISTRY_CONTRACT_ADDRESS } from '@celo/contractkit' import { stripProxy, suffixProxy } from '@celo/contractkit/lib/base' import { getInitializeAbiOfImplementation, SET_AND_INITIALIZE_IMPLEMENTATION_ABI, SET_IMPLEMENTATION_ABI, - setImplementationOnProxy, } from '@celo/contractkit/lib/proxy' -import { valueToString } from '@celo/contractkit/lib/wrappers/BaseWrapper' import { hotfixToParams, Proposal, ProposalTransaction, } from '@celo/contractkit/lib/wrappers/Governance' import { newBlockExplorer } from '@celo/explorer' -import { fetchMetadata, tryGetProxyImplementation } from '@celo/explorer/lib/sourcify' -import { isValidAddress } from '@celo/utils/lib/address' +import { fetchMetadata } from '@celo/explorer/lib/sourcify' import { fromFixed } from '@celo/utils/lib/fixidity' import { keccak_256 } from '@noble/hashes/sha3' import { utf8ToBytes } from '@noble/hashes/utils' import { BigNumber } from 'bignumber.js' import debugFactory from 'debug' -const debug = debugFactory('governance:proposals') +export const debug = debugFactory('governance:proposals') export const hotfixExecuteAbi = getAbiByName(GovernanceABI, 'executeHotfix') @@ -77,13 +59,21 @@ export interface ProposalTransactionJSON { value: string } -const isRegistryRepoint = (tx: ProposalTransactionJSON) => - tx.contract === 'Registry' && tx.function === 'setAddressFor' +export type ExternalProposalTransactionJSON = Omit & { + contract?: string +} -const isGovernanceConstitutionSetter = (tx: ProposalTransactionJSON) => - tx.contract === 'Governance' && tx.function === 'setConstitution' +export const isRegistryRepoint = ( + tx: Pick +) => tx.contract === 'Registry' && tx.function === 'setAddressFor' -const registryRepointArgs = (tx: ProposalTransactionJSON) => { +const isGovernanceConstitutionSetter = ( + tx: Pick +) => tx.contract === 'Governance' && tx.function === 'setConstitution' + +export const registryRepointArgs = ( + tx: Pick +) => { if (!isRegistryRepoint(tx)) { throw new Error(`Proposal transaction not a registry repoint:\n${JSON.stringify(tx, null, 2)}`) } @@ -110,10 +100,10 @@ const registryRepointRawArgs = (abiCoder: AbiCoder, tx: ProposalTransaction) => } } -const isProxySetAndInitFunction = (tx: ProposalTransactionJSON) => +export const isProxySetAndInitFunction = (tx: Pick) => tx.function === SET_AND_INITIALIZE_IMPLEMENTATION_ABI.name! -const isProxySetFunction = (tx: ProposalTransactionJSON) => +export const isProxySetFunction = (tx: Pick) => tx.function === SET_IMPLEMENTATION_ABI.name! /** @@ -225,237 +215,11 @@ export const proposalToJSON = async ( return proposalJson } -type ProposalTxParams = Pick -interface RegistryAdditions { +export type ProposalTxParams = Pick +export interface RegistryAdditions { [contractName: string]: Address } -/** - * Builder class to construct proposals from JSON or transaction objects. - */ -export class ProposalBuilder { - externalCallProxyRepoint: Map = new Map() - - constructor( - private readonly kit: ContractKit, - private readonly builders: (() => Promise)[] = [], - public readonly registryAdditions: RegistryAdditions = {} - ) {} - - /** - * Build calls all of the added build steps and returns the final proposal. - * @returns A constructed Proposal object (i.e. a list of ProposalTransaction) - */ - build = async () => { - const ret = [] - for (const builder of this.builders) { - ret.push(await builder()) - } - return ret - } - - /** - * Converts a Web3 transaction into a proposal transaction object. - * @param tx A Web3 transaction object to convert. - * @param params Parameters for how the transaction should be executed. - */ - fromWeb3tx = (tx: CeloTxObject, params: ProposalTxParams): ProposalTransaction => ({ - value: params.value, - to: params.to, - input: tx.encodeABI(), - }) - - /** - * Adds a transaction to set the implementation on a proxy to the given address. - * @param contract Celo contract name of the proxy which should have its implementation set. - * @param newImplementationAddress Address of the new contract implementation. - */ - 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', - } - ) - }) - } - - /** - * Adds a Web3 transaction to the list for proposal construction. - * @param tx A Web3 transaction object to add to the proposal. - * @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") - } - // TODO fix type of value - this.addWeb3Tx(tx.txo, { to, value: valueToString(value.toString()) }) - } - - setRegistryAddition = (contract: CeloContract, address: string) => - (this.registryAdditions[stripProxy(contract)] = address) - - getRegistryAddition = (contract: CeloContract): string | undefined => - this.registryAdditions[stripProxy(contract)] - - isRegistryContract = (contract: CeloContract) => - RegisteredContracts.includes(stripProxy(contract)) || - this.getRegistryAddition(contract) !== undefined - - /* - * @deprecated - use isRegistryContract - */ - isRegistered = this.isRegistryContract - - lookupExternalMethodABI = async ( - address: string, - tx: ProposalTransactionJSON - ): Promise => { - const abiCoder = this.kit.connection.getAbiCoder() - const metadata = await fetchMetadata( - this.kit.connection, - this.kit.web3.utils.toChecksumAddress(address) - ) - const potentialABIs = metadata?.abiForMethod(tx.function) ?? [] - return ( - potentialABIs.find((abi) => { - try { - abiCoder.encodeFunctionCall(abi, this.transformArgs(abi, tx.args)) - return true - } catch { - return false - } - }) || null - ) - } - - buildCallToExternalContract = async ( - tx: ProposalTransactionJSON - ): Promise => { - if (!tx.address || !isValidAddress(tx.address)) { - throw new Error(`${tx.contract} is not a core celo contract so address must be specified`) - } - - if (tx.function === '') { - return { input: '', to: tx.address, value: tx.value } - } - - let methodABI: AbiItem | null = await this.lookupExternalMethodABI(tx.address, tx) - if (methodABI === null) { - const proxyImpl = this.externalCallProxyRepoint.has(tx.address) - ? this.externalCallProxyRepoint.get(tx.address) - : await tryGetProxyImplementation(this.kit.connection, tx.address) - - if (proxyImpl) { - methodABI = await this.lookupExternalMethodABI(proxyImpl, tx) - } - } - - if (methodABI === null) { - methodABI = signatureToAbiDefinition(tx.function) - } - - const input = this.kit.connection - .getAbiCoder() - .encodeFunctionCall(methodABI, this.transformArgs(methodABI, tx.args)) - 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( - `ABI inputs length ${abi.inputs?.length} does not match args length ${args.length}` - ) - } - const res = [] - for (let i = 0; i < args.length; i++) { - const input = abi.inputs![i] - if (input.type === 'tuple') { - // support of structs and tuples - res.push(JSON.parse(args[i])) - } else { - res.push(args[i]) - } - } - return res - } - - buildCallToCoreContract = async (tx: ProposalTransactionJSON): Promise => { - // Account for canonical registry addresses from current proposal - const address = - this.getRegistryAddition(tx.contract) ?? (await this.kit.registry.addressFor(tx.contract)) - - if (tx.address && address !== tx.address) { - throw new Error(`Address mismatch for ${tx.contract}: ${address} !== ${tx.address}`) - } - - 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]) - } - - const contract = await this.kit._web3Contracts.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 }) - } - - fromJsonTx = async (tx: ProposalTransactionJSON): Promise => { - if (isRegistryRepoint(tx)) { - // Update canonical registry addresses - const args = registryRepointArgs(tx) - this.setRegistryAddition(args.name, args.address) - } - - if (isProxySetAndInitFunction(tx) || isProxySetFunction(tx)) { - console.log(tx.address + ' is a proxy, repointing to ' + tx.args[0]) - this.externalCallProxyRepoint.set(tx.address || tx.contract, tx.args[0] as string) - } - - const strategies = [this.buildCallToCoreContract, this.buildCallToExternalContract] - - for (const strategy of strategies) { - try { - return await strategy(tx) - } catch (e) { - debug("Couldn't build transaction with strategy %s: %O", strategy.name, e) - } - } - - throw new Error(`Couldn't build call for transaction: ${JSON.stringify(tx)}`) - } - - addJsonTx = (tx: ProposalTransactionJSON) => this.builders.push(async () => this.fromJsonTx(tx)) -} - +// reexport for backwards compatibility export { InteractiveProposalBuilder } from './interactive-proposal-builder' +export { ProposalBuilder } from './proposal-builder' diff --git a/packages/sdk/governance/src/test-utils/globals.d.ts b/packages/sdk/governance/src/test-utils/globals.d.ts new file mode 100644 index 000000000..b7f02cdc5 --- /dev/null +++ b/packages/sdk/governance/src/test-utils/globals.d.ts @@ -0,0 +1,6 @@ +/* eslint import/no-extraneous-dependencies:off */ +import { FetchMockSandbox } from 'fetch-mock' + +declare global { + const fetchMock: FetchMockSandbox +}