diff --git a/.github/workflows/cron-npm-install.yml b/.github/workflows/cron-npm-install.yml index 5ec884dad4b..f4ac8c43d4f 100644 --- a/.github/workflows/cron-npm-install.yml +++ b/.github/workflows/cron-npm-install.yml @@ -15,7 +15,7 @@ jobs: name: ${{ matrix.package }} NPM package install runs-on: ubuntu-latest container: - image: node:14-bullseye + image: node:18-bullseye strategy: fail-fast: false matrix: diff --git a/dependency-graph.json b/dependency-graph.json index aefa286dd8d..0e826914ea7 100644 --- a/dependency-graph.json +++ b/dependency-graph.json @@ -64,7 +64,8 @@ "@celo/cryptographic-utils", "@celo/phone-utils", "@celo/typescript", - "@celo/utils" + "@celo/utils", + "@celo/wallet-local" ] }, "@celo/typescript": { diff --git a/packages/celotool/package.json b/packages/celotool/package.json index 84634320507..42d1976d6c8 100644 --- a/packages/celotool/package.json +++ b/packages/celotool/package.json @@ -6,16 +6,16 @@ "author": "Celo", "license": "Apache-2.0", "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", - "@celo/cryptographic-utils": "4.1.2-dev", - "@celo/contractkit": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", + "@celo/cryptographic-utils": "5.0.3-dev", + "@celo/contractkit": "5.0.3-dev", "@celo/env-tests": "1.0.0", - "@celo/explorer": "4.1.2-dev", - "@celo/governance": "4.1.2-dev", - "@celo/identity": "4.1.2-dev", - "@celo/network-utils": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/explorer": "5.0.3-dev", + "@celo/governance": "5.0.3-dev", + "@celo/identity": "5.0.3-dev", + "@celo/network-utils": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@ethereumjs/rlp": "4.0.1", "@google-cloud/monitoring": "0.7.1", diff --git a/packages/cli/package.json b/packages/cli/package.json index fe53c405ad5..4446ca2e520 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -35,16 +35,16 @@ }, "dependencies": { "@celo/bls12377js": "0.1.1", - "@celo/contractkit": "^4.1.2-dev", - "@celo/explorer": "^4.1.2-dev", - "@celo/governance": "^4.1.2-dev", - "@celo/identity": "^4.1.2-dev", - "@celo/phone-utils": "^4.1.2-dev", - "@celo/utils": "^4.1.2-dev", - "@celo/cryptographic-utils": "^4.1.2-dev", - "@celo/wallet-hsm-azure": "^4.1.2-dev", - "@celo/wallet-ledger": "^4.1.2-dev", - "@celo/wallet-local": "^4.1.2-dev", + "@celo/contractkit": "^5.0.3-dev", + "@celo/explorer": "^5.0.3-dev", + "@celo/governance": "^5.0.3-dev", + "@celo/identity": "^5.0.3-dev", + "@celo/phone-utils": "^5.0.3-dev", + "@celo/utils": "^5.0.3-dev", + "@celo/cryptographic-utils": "^5.0.3-dev", + "@celo/wallet-hsm-azure": "^5.0.3-dev", + "@celo/wallet-ledger": "^5.0.3-dev", + "@celo/wallet-local": "^5.0.3-dev", "@ledgerhq/hw-transport-node-hid": "^6.27.4", "@oclif/command": "^1.6.0", "@oclif/config": "^1.6.0", diff --git a/packages/cli/src/base.ts b/packages/cli/src/base.ts index 6ca0e7753c2..b97492c5ffb 100644 --- a/packages/cli/src/base.ts +++ b/packages/cli/src/base.ts @@ -201,9 +201,6 @@ export abstract class BaseCommand extends Command { const setStableTokenGas = async (stable: StableToken) => { await this.kit.setFeeCurrency(stableTokenInfos[stable].contract) - await this.kit.updateGasPriceInConnectionLayer( - await this.kit.registry.addressFor(stableTokenInfos[stable].contract) - ) } if (Object.keys(StableToken).includes(gasCurrencyConfig)) { await setStableTokenGas(StableToken[gasCurrencyConfig as keyof typeof StableToken]) diff --git a/packages/cli/src/commands/releasegold/transfer-dollars.test.ts b/packages/cli/src/commands/releasegold/transfer-dollars.test.ts index e2e554a604a..560101ce2bb 100644 --- a/packages/cli/src/commands/releasegold/transfer-dollars.test.ts +++ b/packages/cli/src/commands/releasegold/transfer-dollars.test.ts @@ -40,6 +40,8 @@ testWithGanache('releasegold:transfer-dollars cmd', (web3: Web3) => { contractAddress, '--value', cUSDToTransfer, + '--gasCurrency', + 'CELO', ]) // RG cUSD balance should match the amount sent const contractBalance = await kit.getTotalBalance(contractAddress) @@ -52,6 +54,8 @@ testWithGanache('releasegold:transfer-dollars cmd', (web3: Web3) => { accounts[0], '--value', cUSDToTransfer, + '--gasCurrency', + 'CELO', ]) const balanceAfter = await kit.getTotalBalance(accounts[0]) expect(balanceBefore.cUSD).toEqual(balanceAfter.cUSD) diff --git a/packages/cli/src/commands/releasegold/transfer-dollars.ts b/packages/cli/src/commands/releasegold/transfer-dollars.ts index a8d29ef0466..72ba7017505 100644 --- a/packages/cli/src/commands/releasegold/transfer-dollars.ts +++ b/packages/cli/src/commands/releasegold/transfer-dollars.ts @@ -28,7 +28,6 @@ export default class TransferDollars extends ReleaseGoldBaseCommand { this.kit.defaultAccount = isRevoked ? await this.releaseGoldWrapper.getReleaseOwner() : await this.releaseGoldWrapper.getBeneficiary() - await displaySendTx('transfer', this.releaseGoldWrapper.transfer(flags.to, flags.value)) } } diff --git a/packages/cli/src/transfer-stable-base.ts b/packages/cli/src/transfer-stable-base.ts index 293f494cdfc..fe18e297a37 100644 --- a/packages/cli/src/transfer-stable-base.ts +++ b/packages/cli/src/transfer-stable-base.ts @@ -1,5 +1,6 @@ import { StableToken } from '@celo/contractkit' import { StableTokenWrapper } from '@celo/contractkit/lib/wrappers/StableTokenWrapper' +import { stableTokenInfos } from '@celo/contractkit/src/celo-tokens' import { flags } from '@oclif/command' import { ParserOutput } from '@oclif/parser/lib/parse' import BigNumber from 'bignumber.js' @@ -35,7 +36,10 @@ export abstract class TransferStableBase extends BaseCommand { } catch { failWith(`The ${this._stableCurrency} token was not deployed yet`) } - await this.kit.updateGasPriceInConnectionLayer(stableToken.address) + // If gasCurrency is not set, use the transferring token + if (!res.flags.gasCurrency) { + await this.kit.setFeeCurrency(stableTokenInfos[this._stableCurrency].contract) + } const tx = res.flags.comment ? stableToken.transferWithComment(to, value.toFixed(), res.flags.comment) @@ -47,14 +51,12 @@ export abstract class TransferStableBase extends BaseCommand { `Account can afford transfer and gas paid in ${this._stableCurrency}`, this.kit.connection.defaultFeeCurrency === stableToken.address, async () => { - const gas = await tx.txo.estimateGas({ feeCurrency: stableToken.address }) - // TODO: replace with gasPrice rpc once supported by min client version - const { gasPrice } = await this.kit.connection.fillGasPrice({ - gasPrice: '0', - feeCurrency: stableToken.address, - }) + const [gas, gasPrice, balance] = await Promise.all([ + tx.txo.estimateGas({ feeCurrency: stableToken.address }), + this.kit.connection.gasPrice(stableToken.address), + stableToken.balanceOf(from), + ]) const gasValue = new BigNumber(gas).times(gasPrice as string) - const balance = await stableToken.balanceOf(from) return balance.gte(value.plus(gasValue)) }, `Cannot afford transfer with ${this._stableCurrency} gasCurrency; try reducing value slightly or using gasCurrency=CELO` diff --git a/packages/env-tests/package.json b/packages/env-tests/package.json index 388ea146b1e..b34005f50e3 100644 --- a/packages/env-tests/package.json +++ b/packages/env-tests/package.json @@ -5,13 +5,13 @@ "main": "index.js", "license": "MIT", "dependencies": { - "@celo/contractkit": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", - "@celo/base": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", - "@celo/identity": "4.1.2-dev", - "@celo/phone-utils": "4.1.2-dev", - "@celo/cryptographic-utils": "4.1.2-dev", + "@celo/contractkit": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/base": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", + "@celo/identity": "5.0.3-dev", + "@celo/phone-utils": "5.0.3-dev", + "@celo/cryptographic-utils": "5.0.3-dev", "bunyan": "1.8.12", "bunyan-gke-stackdriver": "0.1.2", "bunyan-debug-stream": "2.0.0", diff --git a/packages/metadata-crawler/package.json b/packages/metadata-crawler/package.json index 748e0b8d6ba..eee036d360e 100644 --- a/packages/metadata-crawler/package.json +++ b/packages/metadata-crawler/package.json @@ -9,9 +9,9 @@ "homepage": "https://github.com/celo-org/celo-monorepo/tree/master/packages/metadata-crawler", "repository": "https://github.com/celo-org/celo-monorepo/tree/master/packages/metadata-crawler", "dependencies": { - "@celo/connect": "4.1.2-dev", - "@celo/contractkit": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", + "@celo/contractkit": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", "@types/pg": "^7.14.3", "bunyan": "1.8.12", "bunyan-gke-stackdriver": "0.1.2", diff --git a/packages/phone-number-privacy/combiner/package.json b/packages/phone-number-privacy/combiner/package.json index 4ad16b5a842..04c88952ce6 100644 --- a/packages/phone-number-privacy/combiner/package.json +++ b/packages/phone-number-privacy/combiner/package.json @@ -1,6 +1,6 @@ { "name": "@celo/phone-number-privacy-combiner", - "version": "3.0.1-dev", + "version": "3.0.3", "description": "Orchestrates and combines threshold signatures for use in ODIS", "author": "Celo", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "test:e2e:mainnet": "CONTEXT_NAME=mainnet yarn test:e2e" }, "dependencies": { - "@celo/contractkit": "^4.1.2-dev", - "@celo/phone-number-privacy-common": "^3.0.1-dev", - "@celo/identity": "^4.1.2-dev", - "@celo/encrypted-backup": "^4.1.2-dev", + "@celo/contractkit": "^5.0.3-dev", + "@celo/phone-number-privacy-common": "^3.0.3", + "@celo/identity": "^5.0.3-dev", + "@celo/encrypted-backup": "^5.0.3-dev", "@celo/poprf": "^0.1.9", "@types/bunyan": "^1.8.8", "@opentelemetry/api": "^1.4.1", @@ -55,8 +55,8 @@ }, "devDependencies": { "@types/node": "18.15.13", - "@celo/utils": "^4.1.2-dev", - "@celo/phone-utils": "^4.1.2-dev", + "@celo/utils": "^5.0.3-dev", + "@celo/phone-utils": "^5.0.3-dev", "@types/express": "^4.17.6", "@types/supertest": "^2.0.12", "@types/uuid": "^7.0.3", diff --git a/packages/phone-number-privacy/common/package.json b/packages/phone-number-privacy/common/package.json index 505d6b33135..0fe23a973ea 100644 --- a/packages/phone-number-privacy/common/package.json +++ b/packages/phone-number-privacy/common/package.json @@ -1,6 +1,6 @@ { "name": "@celo/phone-number-privacy-common", - "version": "3.0.1-dev", + "version": "3.0.3", "description": "Common library for the combiner and signer libraries", "author": "Celo", "license": "Apache-2.0", @@ -18,10 +18,10 @@ "lib/**/*" ], "dependencies": { - "@celo/base": "^4.1.2-dev", - "@celo/contractkit": "^4.1.2-dev", - "@celo/utils": "^4.1.2-dev", - "@celo/phone-utils": "^4.1.2-dev", + "@celo/base": "^5.0.3-dev", + "@celo/contractkit": "^5.0.3-dev", + "@celo/utils": "^5.0.3-dev", + "@celo/phone-utils": "^5.0.3-dev", "@types/bunyan": "1.8.8", "bignumber.js": "^9.0.0", "bunyan": "1.8.12", @@ -41,13 +41,13 @@ }, "devDependencies": { "@celo/poprf": "^0.1.9", - "@celo/wallet-local": "^4.1.2-dev", + "@celo/wallet-local": "^5.0.3-dev", "@types/elliptic": "^6.4.12", "@types/express": "^4.17.6", "@types/is-base64": "^1.1.0", "@types/node-fetch": "^2.5.7" }, "engines": { - "node": ">=10" + "node": ">=12" } } \ No newline at end of file diff --git a/packages/phone-number-privacy/monitor/package.json b/packages/phone-number-privacy/monitor/package.json index 98f4dd599be..65b175990fd 100644 --- a/packages/phone-number-privacy/monitor/package.json +++ b/packages/phone-number-privacy/monitor/package.json @@ -1,6 +1,6 @@ { "name": "@celo/phone-number-privacy-monitor", - "version": "3.0.0-beta.2-dev", + "version": "3.0.3", "description": "Regularly queries ODIS to ensure the system is functioning properly", "author": "Celo", "license": "Apache-2.0", @@ -22,13 +22,13 @@ "loadTest": "ts-node src/scripts/run-load-test.ts run" }, "dependencies": { - "@celo/contractkit": "^4.1.2-dev", - "@celo/cryptographic-utils": "^4.1.2-dev", - "@celo/encrypted-backup": "^4.1.2-dev", - "@celo/identity": "^4.1.2-dev", - "@celo/wallet-local": "^4.1.2-dev", - "@celo/phone-number-privacy-common": "^3.0.1-dev", - "@celo/utils": "^4.1.2-dev", + "@celo/contractkit": "^5.0.3-dev", + "@celo/cryptographic-utils": "^5.0.3-dev", + "@celo/encrypted-backup": "^5.0.3-dev", + "@celo/identity": "^5.0.3-dev", + "@celo/wallet-local": "^5.0.3-dev", + "@celo/phone-number-privacy-common": "^3.0.2", + "@celo/utils": "^5.0.3-dev", "firebase-admin": "^9.12.0", "firebase-functions": "^3.15.7" }, diff --git a/packages/phone-number-privacy/signer/package.json b/packages/phone-number-privacy/signer/package.json index c7f9e5b59fe..74951f461d5 100644 --- a/packages/phone-number-privacy/signer/package.json +++ b/packages/phone-number-privacy/signer/package.json @@ -1,6 +1,6 @@ { "name": "@celo/phone-number-privacy-signer", - "version": "3.0.2-dev", + "version": "3.0.3", "description": "Signing participator of ODIS", "author": "Celo", "license": "Apache-2.0", @@ -37,12 +37,12 @@ "ssl:keygen": "./scripts/create-ssl-cert.sh" }, "dependencies": { - "@celo/base": "^4.1.2-dev", - "@celo/contractkit": "^4.1.2-dev", - "@celo/phone-number-privacy-common": "^3.0.1-dev", + "@celo/base": "^5.0.3-dev", + "@celo/contractkit": "^5.0.3-dev", + "@celo/phone-number-privacy-common": "^3.0.3", "@celo/poprf": "^0.1.9", - "@celo/utils": "^4.1.2-dev", - "@celo/wallet-hsm-azure": "^4.1.2-dev", + "@celo/utils": "^5.0.3-dev", + "@celo/wallet-hsm-azure": "^5.0.3-dev", "@google-cloud/secret-manager": "3.0.0", "@opentelemetry/api": "^1.4.1", "@opentelemetry/auto-instrumentations-node": "^0.38.0", @@ -78,4 +78,4 @@ "engines": { "node": ">=10" } -} +} \ No newline at end of file diff --git a/packages/protocol/lib/signing-utils.ts b/packages/protocol/lib/signing-utils.ts index 6f470c1fa37..64e5b143fa9 100644 --- a/packages/protocol/lib/signing-utils.ts +++ b/packages/protocol/lib/signing-utils.ts @@ -1,28 +1,12 @@ // Originally taken from https://github.com/ethereum/web3.js/blob/1.x/packages/web3-eth-accounts/src/index.js -import { inputCeloTxFormatter } from '@celo/connect/lib/utils/formatter' import { parseSignature } from '@celo/utils/lib/signatureUtils' -import { account as Account, bytes, hash, nat, RLP } from 'eth-lib' -import _ from 'underscore' +import { privateKeyToAddress } from '@celo/utils/lib/address' +import { LocalWallet } from '@celo/wallet-local' import Web3 from 'web3' -import { numberToHex } from 'web3-utils' function isNot(value: any) { - return _.isUndefined(value) || _.isNull(value) -} - -function trimLeadingZero(hex: string) { - while (hex && hex.startsWith('0x0')) { - hex = '0x' + hex.slice(3) - } - return hex -} - -function makeEven(hex: string) { - if (hex.length % 2 === 1) { - hex = hex.replace('0x', '0x0') - } - return hex + return value === null || value === undefined } export const getParsedSignatureOfAddress = async (web3: Web3, address: string, signer: string) => { @@ -32,13 +16,12 @@ export const getParsedSignatureOfAddress = async (web3: Web3, address: string, s } export async function signTransaction(web3: Web3, txn: any, privateKey: string) { - let result: any if (!txn) { throw new Error('No transaction object given!') } - const signed = (tx: any) => { + const signed = async (tx: any) => { if (!tx.gas && !tx.gasLimit) { throw new Error('"gas" is missing') } @@ -46,62 +29,17 @@ export async function signTransaction(web3: Web3, txn: any, privateKey: string) if (tx.nonce < 0 || tx.gas < 0 || tx.gasPrice < 0 || tx.chainId < 0) { throw new Error('Gas, gasPrice, nonce or chainId is lower than 0') } - try { - tx = inputCeloTxFormatter(tx) + const wallet = new LocalWallet() - const transaction = tx - transaction.to = tx.to || '0x' - transaction.data = tx.data || '0x' - transaction.value = tx.value || '0x' - transaction.chainId = numberToHex(tx.chainId) - transaction.feeCurrency = tx.feeCurrency || '0x' - transaction.gatewayFeeRecipient = tx.gatewayFeeRecipient || '0x' - transaction.gatewayFee = tx.gatewayFee || '0x' + wallet.addAccount(privateKey) - const rlpEncoded = RLP.encode([ - bytes.fromNat(transaction.nonce), - bytes.fromNat(transaction.gasPrice), - bytes.fromNat(transaction.gas), - transaction.feeCurrency.toLowerCase(), - transaction.gatewayFeeRecipient.toLowerCase(), - bytes.fromNat(transaction.gatewayFee), - transaction.to.toLowerCase(), - bytes.fromNat(transaction.value), - transaction.data, - bytes.fromNat(transaction.chainId || '0x1'), - '0x', - '0x', - ]) + return wallet.signTransaction(tx) - const messageHash = hash.keccak256(rlpEncoded) - - const signature = Account.makeSigner(nat.toNumber(transaction.chainId || '0x1') * 2 + 35)( - hash.keccak256(rlpEncoded), - privateKey - ) - - const rawTx = RLP.decode(rlpEncoded).slice(0, 9).concat(Account.decodeSignature(signature)) - - rawTx[9] = makeEven(trimLeadingZero(rawTx[9])) - rawTx[10] = makeEven(trimLeadingZero(rawTx[10])) - rawTx[11] = makeEven(trimLeadingZero(rawTx[11])) - - const rawTransaction = RLP.encode(rawTx) - - const values = RLP.decode(rawTransaction) - result = { - messageHash, - v: trimLeadingZero(values[9]), - r: trimLeadingZero(values[10]), - s: trimLeadingZero(values[11]), - rawTransaction, - } } catch (e) { + console.info('Error signing transaction', e) throw e } - - return result } // Resolve immediately if nonce, chainId and price are provided @@ -110,10 +48,10 @@ export async function signTransaction(web3: Web3, txn: any, privateKey: string) } // Otherwise, get the missing info from the Ethereum Node - const chainId = isNot(txn.chainId) ? await web3.eth.net.getId() : txn.chainId + const chainId = isNot(txn.chainId) ? await web3.eth.getChainId() : txn.chainId const gasPrice = isNot(txn.gasPrice) ? await web3.eth.getGasPrice() : txn.gasPrice const nonce = isNot(txn.nonce) - ? await web3.eth.getTransactionCount(Account.fromPrivate(privateKey).address) + ? await web3.eth.getTransactionCount(privateKeyToAddress(privateKey)) : txn.nonce if (isNot(chainId) || isNot(gasPrice) || isNot(nonce)) { @@ -122,5 +60,5 @@ export async function signTransaction(web3: Web3, txn: any, privateKey: string) JSON.stringify({ chainId, gasPrice, nonce }) ) } - return signed(_.extend(txn, { chainId, gasPrice, nonce })) + return signed({...txn, chainId, gasPrice, nonce }) } diff --git a/packages/protocol/lib/web3-utils.ts b/packages/protocol/lib/web3-utils.ts index 7de0c6b7068..2752955598a 100644 --- a/packages/protocol/lib/web3-utils.ts +++ b/packages/protocol/lib/web3-utils.ts @@ -1,21 +1,21 @@ /* tslint:disable:no-console */ // TODO(asa): Refactor and rename to 'deployment-utils.ts' -import { Address, CeloTxObject } from '@celo/connect'; -import { setAndInitializeImplementation } from '@celo/protocol/lib/proxy-utils'; -import { CeloContractName } from '@celo/protocol/lib/registry-utils'; -import { signTransaction } from '@celo/protocol/lib/signing-utils'; -import { privateKeyToAddress } from '@celo/utils/lib/address'; -import { BuildArtifacts } from '@openzeppelin/upgrades'; -import { BigNumber } from 'bignumber.js'; - -import { createInterfaceAdapter } from '@truffle/interface-adapter'; -import path from 'path'; -import prompts from 'prompts'; -import { GoldTokenInstance, MultiSigInstance, OwnableInstance, ProxyContract, ProxyInstance, RegistryInstance } from 'types'; -import { StableTokenInstance } from 'types/mento'; -import Web3 from 'web3'; -import { ContractPackage } from '../contractPackages'; -import { ArtifactsSingleton } from './artifactsSingleton'; +import { Address, CeloTxObject } from '@celo/connect' +import { setAndInitializeImplementation } from '@celo/protocol/lib/proxy-utils' +import { CeloContractName } from '@celo/protocol/lib/registry-utils' +import { signTransaction } from '@celo/protocol/lib/signing-utils' +import { privateKeyToAddress } from '@celo/utils/lib/address' +import { BuildArtifacts } from '@openzeppelin/upgrades' +import { BigNumber } from 'bignumber.js' + +import { createInterfaceAdapter } from '@truffle/interface-adapter' +import path from 'path' +import prompts from 'prompts' +import { GoldTokenInstance, MultiSigInstance, OwnableInstance, ProxyContract, ProxyInstance, RegistryInstance } from 'types' +import { StableTokenInstance } from 'types/mento' +import Web3 from 'web3' +import { ContractPackage } from '../contractPackages' +import { ArtifactsSingleton } from './artifactsSingleton' const truffleContract = require('@truffle/contract'); @@ -39,8 +39,7 @@ export async function sendTransactionWithPrivateKey( from: address, }) } - - const signedTx: any = await signTransaction( + const signedTx = await signTransaction( web3, { ...txArgs, @@ -53,7 +52,7 @@ export async function sendTransactionWithPrivateKey( privateKey ) - const rawTransaction = signedTx.rawTransaction.toString('hex') + const rawTransaction = signedTx.raw return web3.eth.sendSignedTransaction(rawTransaction) } @@ -152,7 +151,7 @@ export function checkFunctionArgsLength(args: any[], abi: any) { export async function setInitialProxyImplementation< ContractInstance extends Truffle.ContractInstance >(web3: Web3, artifacts: any, contractName: string, contractPackage?: ContractPackage, ...args: any[]): Promise { - + const Contract = ArtifactsSingleton.getInstance(contractPackage, artifacts).require(contractName) const ContractProxy = ArtifactsSingleton.getInstance(contractPackage, artifacts).require(contractName + 'Proxy') @@ -265,11 +264,11 @@ export const makeTruffleContractForMigration = (contractName: string, contractPa abi: artifact.abi, unlinked_binary: artifact.bytecode, }) - - + + Contract.setProvider(web3.currentProvider) Contract.setNetwork(network.name) - + Contract.interfaceAdapter = createInterfaceAdapter({ networkType: "ethereum", provider: web3.currentProvider @@ -292,7 +291,7 @@ export function deploymentForContract Started deployment for", name) - let Contract + let Contract let ContractProxy if (artifactPath) { Contract = makeTruffleContractForMigration(name, artifactPath, web3) @@ -301,7 +300,7 @@ export function deploymentForContract { console.log("\n-> Deploying", name) @@ -392,12 +391,12 @@ export function getFunctionSelectorsForContract(contract: any, contractName: str export function checkImports(baseContractName: string, derivativeContractArtifact: any, artifacts: any) { const isImport = (astNode: any) => astNode.nodeType === 'ImportDirective' const imports: any[] = derivativeContractArtifact.ast.nodes.filter((astNode: any) => isImport(astNode)) - while (imports.length) { // BFS + while (imports.length) { // BFS const importedContractName = (imports.pop().file as string).split('/').pop().split('.')[0] if (importedContractName === baseContractName) { return true } - const importedContractArtifact = artifacts instanceof BuildArtifacts ? + const importedContractArtifact = artifacts instanceof BuildArtifacts ? artifacts.getArtifactByName(importedContractName) : artifacts.require(importedContractName) imports.unshift(...importedContractArtifact.ast.nodes.filter((astNode: any) => isImport(astNode))) diff --git a/packages/protocol/migrations_ts/28_elect_validators.ts b/packages/protocol/migrations_ts/28_elect_validators.ts index 844aa628ad3..742fff47910 100644 --- a/packages/protocol/migrations_ts/28_elect_validators.ts +++ b/packages/protocol/migrations_ts/28_elect_validators.ts @@ -105,7 +105,7 @@ async function registerValidatorGroup( // Add a premium to cover tx fees const v = lockedGoldValue.times(1.01).integerValue() - console.info(` - send funds ${v} to group address ${account.address}`) + console.info(` - send funds ${v} to group address ${account.address}}`) await sendTransaction(web3, null, privateKey, { to: account.address, value: v, diff --git a/packages/protocol/package.json b/packages/protocol/package.json index 68b31a4eeb0..caa6f5495a2 100644 --- a/packages/protocol/package.json +++ b/packages/protocol/package.json @@ -49,11 +49,12 @@ "@0x/sol-profiler": "^4.1.37", "@0x/sol-trace": "^3.0.47", "@0x/subproviders": "^7.0.1", - "@celo/base": "4.1.2-dev", + "@celo/base": "5.0.3-dev", "@celo/bls12377js": "0.1.1", - "@celo/connect": "4.1.2-dev", - "@celo/cryptographic-utils": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", + "@celo/cryptographic-utils": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-local": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@ethereumjs/vm": "npm:@celo/ethereumjs-vm@6.4.1-unofficial.0", "@ganache/console.log": "0.3.0", @@ -94,7 +95,7 @@ "web3-utils": "1.10.0" }, "devDependencies": { - "@celo/phone-utils": "4.1.2-dev", + "@celo/phone-utils": "5.0.3-dev", "@celo/typechain-target-web3-v1-celo": "0.2.0", "@celo/typescript": "0.0.1", "@types/bn.js": "^5.1.0", @@ -104,7 +105,6 @@ "@types/mocha": "^7.0.2", "@types/targz": "^1.0.0", "@types/tmp": "^0.1.0", - "@types/underscore": "^1.8.8", "@types/yargs": "^13.0.2", "cross-env": "^5.1.6", "eth-gas-reporter": "^0.2.16", diff --git a/packages/sdk/base/package.json b/packages/sdk/base/package.json index 710a7a809f6..c3b6dc93600 100644 --- a/packages/sdk/base/package.json +++ b/packages/sdk/base/package.json @@ -1,6 +1,6 @@ { "name": "@celo/base", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo base common utils, no dependencies", "author": "Celo", "license": "Apache-2.0", diff --git a/packages/sdk/base/src/address.ts b/packages/sdk/base/src/address.ts index d49e641a2af..9eea852b30d 100644 --- a/packages/sdk/base/src/address.ts +++ b/packages/sdk/base/src/address.ts @@ -2,6 +2,8 @@ const HEX_REGEX = /^0x[0-9A-F]*$/i export type Address = string +export type StrongAddress = `0x${string}` + export const eqAddress = (a: Address, b: Address) => normalizeAddress(a) === normalizeAddress(b) export const normalizeAddress = (a: Address) => trimLeading0x(a).toLowerCase() @@ -12,7 +14,8 @@ export const normalizeAddressWith0x = (a: Address) => ensureLeading0x(a).toLower export const trimLeading0x = (input: string) => (input.startsWith('0x') ? input.slice(2) : input) -export const ensureLeading0x = (input: string) => (input.startsWith('0x') ? input : `0x${input}`) +export const ensureLeading0x = (input: string): StrongAddress => + input.startsWith('0x') ? (input as StrongAddress) : (`0x${input}` as const) // Turns '0xce10ce10ce10ce10ce10ce10ce10ce10ce10ce10' // into ['ce10','ce10','ce10','ce10','ce10','ce10','ce10','ce10','ce10','ce10'] diff --git a/packages/sdk/connect/package.json b/packages/sdk/connect/package.json index daaa46656dd..05b7d5d3638 100644 --- a/packages/sdk/connect/package.json +++ b/packages/sdk/connect/package.json @@ -1,6 +1,6 @@ { "name": "@celo/connect", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Light Toolkit for connecting with the Celo network", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -24,8 +24,8 @@ "dependencies": { "@types/debug": "^4.1.5", "@types/utf8": "^2.1.6", - "@celo/base": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", "bignumber.js": "^9.0.0", "debug": "^4.1.1", "utf8": "3.0.0" diff --git a/packages/sdk/connect/src/celo-provider.test.ts b/packages/sdk/connect/src/celo-provider.test.ts index 252c5d1c347..daa2b2668d5 100644 --- a/packages/sdk/connect/src/celo-provider.test.ts +++ b/packages/sdk/connect/src/celo-provider.test.ts @@ -29,8 +29,9 @@ class MockWallet implements ReadOnlyWallet { } signTransaction(_txParams: CeloTx): Promise { return Promise.resolve({ - raw: 'mock', + raw: '0xmock', tx: { + type: 'celo-legacy', nonce: 'nonce', gasPrice: 'gasPrice', gas: 'gas', @@ -193,7 +194,7 @@ describe('CeloProvider', () => { describe('but tries to use it with a different account', () => { interceptedByCeloProvider.forEach((method: string) => { - test(`fowards the call to '${method}' to the original provider`, (done) => { + test(`forwards the call to '${method}' to the original provider`, (done) => { const payload: JsonRpcPayload = { id: 0, jsonrpc: '2.0', diff --git a/packages/sdk/connect/src/connection.ts b/packages/sdk/connect/src/connection.ts index bb5fc7f4553..fa11244f123 100644 --- a/packages/sdk/connect/src/connection.ts +++ b/packages/sdk/connect/src/connection.ts @@ -42,7 +42,6 @@ const debugGasEstimation = debugFactory('connection:gas-estimation') type BN = ReturnType export interface ConnectionOptions { gasInflationFactor: number - gasPrice: string feeCurrency?: Address from?: Address } @@ -58,16 +57,11 @@ export class Connection { readonly paramsPopulator: TxParamsNormalizer rpcCaller!: RpcCaller - /** @deprecated no longer needed since gasPrice is available on node rpc */ - private currencyGasPrice: Map = new Map() - constructor(readonly web3: Web3, public wallet?: ReadOnlyWallet, handleRevert = true) { web3.eth.handleRevert = handleRevert this.config = { gasInflationFactor: 1.3, - // gasPrice:0 means the node will compute gasPrice on its own - gasPrice: '0', } const existingProvider: Provider = web3.currentProvider as Provider @@ -89,7 +83,8 @@ export class Connection { } this.web3.setProvider(provider as any) return true - } catch { + } catch (error) { + console.error(`could not attach provider`, error) return false } } @@ -125,14 +120,6 @@ export class Connection { return this.config.gasInflationFactor } - set defaultGasPrice(price: number) { - this.config.gasPrice = price.toString(10) - } - - get defaultGasPrice() { - return parseInt(this.config.gasPrice, 10) - } - /** * Set the ERC20 address for the token to use to pay for transaction fees. * The ERC20 must be whitelisted for gas. @@ -224,7 +211,6 @@ export class Connection { */ sendTransaction = async (tx: CeloTx): Promise => { tx = this.fillTxDefaults(tx) - tx = this.fillGasPrice(tx) let gas = tx.gas if (gas == null) { @@ -244,7 +230,6 @@ export class Connection { tx?: Omit ): Promise => { tx = this.fillTxDefaults(tx) - tx = this.fillGasPrice(tx) let gas = tx.gas if (gas == null) { @@ -341,20 +326,26 @@ export class Connection { sendSignedTransaction = async (signedTransactionData: string): Promise => { return toTxResult(this.web3.eth.sendSignedTransaction(signedTransactionData)) } + // if neither gas price nor feeMarket fields are present set them. + setFeeMarketGas = async (tx: CeloTx): Promise => { + // default to the current values + const calls = [Promise.resolve(tx.maxFeePerGas), Promise.resolve(tx.maxPriorityFeePerGas)] - /** @deprecated no longer needed since gasPrice is available on node rpc */ - fillGasPrice(tx: CeloTx): CeloTx { - if (tx.feeCurrency && tx.gasPrice === '0' && this.currencyGasPrice.has(tx.feeCurrency)) { - return { - ...tx, - gasPrice: this.currencyGasPrice.get(tx.feeCurrency), - } + if (isEmpty(tx.maxFeePerGas)) { + calls[0] = this.gasPrice(tx.feeCurrency) + } + if (isEmpty(tx.maxPriorityFeePerGas)) { + calls[1] = this.rpcCaller.call('eth_maxPriorityFeePerGas', []).then((rpcResponse) => { + return rpcResponse.result + }) + } + const [maxFeePerGas, maxPriorityFeePerGas] = await Promise.all(calls) + return { + ...tx, + gasPrice: undefined, + maxFeePerGas, + maxPriorityFeePerGas, } - return tx - } - /** @deprecated no longer needed since gasPrice is available on node rpc */ - async setGasPriceForCurrency(address: Address, gasPrice: string) { - this.currencyGasPrice.set(address, gasPrice) } estimateGas = async ( @@ -430,7 +421,6 @@ export class Connection { gasPrice = async (feeCurrency?: Address): Promise => { // Required otherwise is not backward compatible const parameter = feeCurrency ? [feeCurrency] : [] - // Reference: https://eth.wiki/json-rpc/API#eth_gasprice const response = await this.rpcCaller.call('eth_gasPrice', parameter) const gasPriceInHex = response.result.toString() @@ -446,10 +436,7 @@ export class Connection { private isBlockNumberHash = (blockNumber: BlockNumber) => blockNumber instanceof String && blockNumber.indexOf('0x') === 0 - getBlock = async ( - blockHashOrBlockNumber: BlockNumber, - fullTxObjects: boolean = true - ): Promise => { + getBlock = async (blockHashOrBlockNumber: BlockNumber, fullTxObjects = true): Promise => { const endpoint = this.isBlockNumberHash(blockHashOrBlockNumber) ? 'eth_getBlockByHash' // Reference: https://eth.wiki/json-rpc/API#eth_getBlockByHash : 'eth_getBlockByNumber' // Reference: https://eth.wiki/json-rpc/API#eth_getBlockByNumber @@ -508,7 +495,6 @@ export class Connection { const defaultTx: CeloTx = { from: this.config.from, feeCurrency: this.config.feeCurrency, - gasPrice: this.config.gasPrice, } return { @@ -522,3 +508,18 @@ export class Connection { this.web3.currentProvider.stop() } } + +function isEmpty(value: string | undefined | number | BN) { + return ( + value === 0 || + value === undefined || + value === null || + value === '0' || + (typeof value === 'string' && + (value.toLowerCase() === '0x' || value.toLowerCase() === '0x0')) || + Web3.utils.toBN(value.toString()).eq(Web3.utils.toBN(0)) + ) +} +export function isPresent(value: string | undefined | number | BN) { + return !isEmpty(value) +} diff --git a/packages/sdk/connect/src/types.ts b/packages/sdk/connect/src/types.ts index 4f3d4493d6a..87201c91590 100644 --- a/packages/sdk/connect/src/types.ts +++ b/packages/sdk/connect/src/types.ts @@ -1,16 +1,57 @@ -import { PromiEvent, Transaction, TransactionConfig, TransactionReceipt } from 'web3-core' +import { + AccessList, + PromiEvent, + Transaction, + TransactionConfig, + TransactionReceipt, +} from 'web3-core' import { Contract } from 'web3-eth-contract' - export type Address = string +export type Hex = `0x${string}` export interface CeloParams { feeCurrency: string + /* + @deprecated + */ gatewayFeeRecipient: string + /* + @deprecated + */ gatewayFee: string } -export type CeloTx = TransactionConfig & Partial +export type AccessListRaw = Array<[string, string[]]> + +export type HexOrMissing = Hex | undefined +export interface FormattedCeloTx { + chainId: number + from: HexOrMissing + to: HexOrMissing + data: string | undefined + value: HexOrMissing + feeCurrency?: HexOrMissing + /* + @deprecated + */ + gatewayFeeRecipient?: HexOrMissing + /* + @deprecated + */ + gatewayFee?: HexOrMissing + gas: HexOrMissing + gasPrice?: Hex + maxFeePerGas?: Hex + maxPriorityFeePerGas?: Hex + nonce: HexOrMissing | number + accessList?: AccessListRaw + type: TransactionTypes +} +export type CeloTx = TransactionConfig & + Partial & { accessList?: AccessList; type?: TransactionTypes } + +export type CeloTxWithSig = CeloTx & { v: number; s: string; r: string; yParity: 0 | 1 } export interface CeloTxObject { arguments: any[] call(tx?: CeloTx): Promise @@ -24,23 +65,52 @@ export { BlockNumber, EventLog, Log, PromiEvent, Sign } from 'web3-core' export { Block, BlockHeader, Syncing } from 'web3-eth' export { Contract, ContractSendMethod, PastEventOptions } from 'web3-eth-contract' +export type TransactionTypes = 'eip1559' | 'celo-legacy' | 'cip42' + +interface CommonTXProperties { + nonce: string + gas: string + to: string + value: string + input: string + r: string + s: string + v: string + hash: string + type: TransactionTypes +} + +interface FeeMarketAndAccessListTXProperties extends CommonTXProperties { + maxFeePerGas: string + maxPriorityFeePerGas: string + accessList?: AccessList +} + +export interface EIP1559TXProperties extends FeeMarketAndAccessListTXProperties { + type: 'eip1559' +} + +export interface CIP42TXProperties extends FeeMarketAndAccessListTXProperties { + feeCurrency: string + gatewayFeeRecipient?: string + gatewayFee?: string + type: 'cip42' +} + +/* + @deprecated + */ +export interface LegacyTXProperties extends CommonTXProperties { + gasPrice: string + feeCurrency: string + gatewayFeeRecipient: string + gatewayFee: string + type: 'celo-legacy' +} + export interface EncodedTransaction { - raw: string - tx: { - nonce: string - gasPrice: string - gas: string - feeCurrency: string - gatewayFeeRecipient: string - gatewayFee: string - to: string - value: string - input: string - r: string - s: string - v: string - hash: string - } + raw: Hex + tx: LegacyTXProperties | CIP42TXProperties | EIP1559TXProperties } export type CeloTxPending = Transaction & Partial @@ -87,6 +157,7 @@ export interface HttpProvider { } export interface RLPEncodedTx { - transaction: CeloTx - rlpEncode: string + transaction: FormattedCeloTx + rlpEncode: Hex + type: TransactionTypes } diff --git a/packages/sdk/connect/src/utils/formatter.test.ts b/packages/sdk/connect/src/utils/formatter.test.ts new file mode 100644 index 00000000000..dbd2b3a86a8 --- /dev/null +++ b/packages/sdk/connect/src/utils/formatter.test.ts @@ -0,0 +1,274 @@ +import { CeloTx } from '../types' +import { inputAccessListFormatter, inputCeloTxFormatter, outputCeloTxFormatter } from './formatter' + +describe('inputAccessListFormatter', () => { + test('with valid accessList', () => { + const accessList = [ + { + address: '0x0000000000000000000000000000000000000000', + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', + ], + }, + ] + + expect(inputAccessListFormatter(accessList)).toEqual([ + [ + '0x0000000000000000000000000000000000000000', + [ + '0x0000000000000000000000000000000000000000000000000000000000000001', + '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe', + ], + ], + ]) + }) +}) + +describe('inputCeloTxFormatter', () => { + const base: CeloTx = { + chainId: 42220, + nonce: 1, + gas: 1000000, + value: '0x0241', + from: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe', + to: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe', + data: '0x', + } + describe('when address does not pass checksum', () => { + ;['from', 'to', 'feeCurrency'].forEach((property) => { + test(`${property}`, () => { + const faulty = { ...base, [property]: '0x3e8' } + expect(() => inputCeloTxFormatter(faulty)).toThrowError( + `Provided address 0x3e8 is invalid, the capitalization checksum test failed` + ) + }) + }) + }) + + describe('valid celo-legacy tx', () => { + const legacy = { + ...base, + gasPrice: '0x3e8', + feeCurrency: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe', + } + it('formats', () => { + expect(inputCeloTxFormatter(legacy)).toMatchInlineSnapshot(` + { + "data": "0x", + "feeCurrency": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "from": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "gas": "0xf4240", + "gasPrice": "0x3e8", + "nonce": "0x1", + "to": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "value": "0x241", + } + `) + }) + }) + describe('valid cip42 tx', () => { + const cip42 = { + ...base, + maxFeePerGas: '0x3e8', + maxPriorityFeePerGas: '0x3e8', + feeCurrency: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe', + } + it('formats', () => { + expect(inputCeloTxFormatter(cip42)).toMatchInlineSnapshot(` + { + "data": "0x", + "feeCurrency": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "from": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "gas": "0xf4240", + "maxFeePerGas": "0x3e8", + "maxPriorityFeePerGas": "0x3e8", + "nonce": "0x1", + "to": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "value": "0x241", + } + `) + }) + }) + describe('valid eip1559 tx', () => { + const eip1559 = { + ...base, + maxFeePerGas: '0x3e8', + maxPriorityFeePerGas: '0x3e8', + } + it('formats', () => { + expect(inputCeloTxFormatter(eip1559)).toMatchInlineSnapshot(` + { + "data": "0x", + "from": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "gas": "0xf4240", + "maxFeePerGas": "0x3e8", + "maxPriorityFeePerGas": "0x3e8", + "nonce": "0x1", + "to": "0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae", + "value": "0x241", + } + `) + }) + }) +}) + +describe('outputCeloTxFormatter', () => { + const base = { + nonce: '0x4', + data: '0x', + input: '0x3454645634534', + from: '0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe', + to: '0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae', + value: '0x3e8', + gas: '0x3e8', + transactionIndex: '0x1', + blockNumber: '0x3e8', + blockHash: '0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9', + } + describe('with blockNumber', () => { + test('when valid', () => { + expect(outputCeloTxFormatter({ ...base, blockNumber: '0x1' })).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": 1, + "data": "0x", + "from": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "gas": 1000, + "input": "0x3454645634534", + "nonce": 4, + "to": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "transactionIndex": 1, + "value": "1000", + } + `) + }) + test('when invalid', () => { + expect(outputCeloTxFormatter({ ...base, blockNumber: null })).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": null, + "data": "0x", + "from": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "gas": 1000, + "input": "0x3454645634534", + "nonce": 4, + "to": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "transactionIndex": 1, + "value": "1000", + } + `) + }) + }) + describe('with valid celo-legacy tx', () => { + const legacy = { + ...base, + gasPrice: '0x3e8', + } + test('when valid', () => { + expect(outputCeloTxFormatter(legacy)).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": 1000, + "data": "0x", + "from": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "gas": 1000, + "gasPrice": "1000", + "input": "0x3454645634534", + "nonce": 4, + "to": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "transactionIndex": 1, + "value": "1000", + } + `) + }) + }) + describe('with valid cip42 tx', () => { + const cip42 = { + ...base, + gateWayFee: '0x3e8', + feeCurrency: '0x11f4d0a3c12e86b4b5f39b213f7e19d048276dae', + maxFeePerGas: '0x3e8', + maxPriorityFeePerGas: '0x3e8', + } + test('when valid', () => { + expect(outputCeloTxFormatter(cip42)).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": 1000, + "data": "0x", + "feeCurrency": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "from": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "gas": 1000, + "gateWayFee": "0x3e8", + "input": "0x3454645634534", + "maxFeePerGas": "1000", + "maxPriorityFeePerGas": "1000", + "nonce": 4, + "to": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "transactionIndex": 1, + "value": "1000", + } + `) + }) + }) + describe('with valid eip1559 tx', () => { + const eip1559 = { + ...base, + maxFeePerGas: '0x3e8', + maxPriorityFeePerGas: '0x3e8', + } + test('when valid', () => { + expect(outputCeloTxFormatter(eip1559)).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": 1000, + "data": "0x", + "from": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "gas": 1000, + "input": "0x3454645634534", + "maxFeePerGas": "1000", + "maxPriorityFeePerGas": "1000", + "nonce": 4, + "to": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "transactionIndex": 1, + "value": "1000", + } + `) + }) + }) + describe('when properties are missing', () => { + test('without from', () => { + expect(outputCeloTxFormatter({ ...base, from: null })).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": 1000, + "data": "0x", + "from": null, + "gas": 1000, + "input": "0x3454645634534", + "nonce": 4, + "to": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "transactionIndex": 1, + "value": "1000", + } + `) + }) + test('without to', () => { + expect(outputCeloTxFormatter({ ...base, to: null })).toMatchInlineSnapshot(` + { + "blockHash": "0xc9b9cdc2092a9d6589d96662b1fd6949611163fb3910cf8a173cd060f17702f9", + "blockNumber": 1000, + "data": "0x", + "from": "0x11f4d0A3c12e86B4b5F39B213F7E19D048276DAe", + "gas": 1000, + "input": "0x3454645634534", + "nonce": 4, + "to": null, + "transactionIndex": 1, + "value": "1000", + } + `) + }) + }) +}) diff --git a/packages/sdk/connect/src/utils/formatter.ts b/packages/sdk/connect/src/utils/formatter.ts index dfe3c07f650..ebba6760dce 100644 --- a/packages/sdk/connect/src/utils/formatter.ts +++ b/packages/sdk/connect/src/utils/formatter.ts @@ -1,46 +1,85 @@ -import { ensureLeading0x, trimLeading0x } from '@celo/base/lib/address' +import { ensureLeading0x, StrongAddress, trimLeading0x } from '@celo/base/lib/address' import { isValidAddress, toChecksumAddress } from '@celo/utils/lib/address' import { sha3 } from '@celo/utils/lib/solidity' import BigNumber from 'bignumber.js' import { encode } from 'utf8' +import { AccessList } from 'web3-core' import { + AccessListRaw, Block, BlockHeader, BlockNumber, CeloTx, CeloTxPending, CeloTxReceipt, + FormattedCeloTx, + Hex, Log, } from '../types' /** * Formats the input of a transaction and converts all values to HEX */ -export function inputCeloTxFormatter(tx: CeloTx) { - tx.from = inputAddressFormatter(tx.from?.toString()) - tx.to = inputAddressFormatter(tx.to) - tx.feeCurrency = inputAddressFormatter(tx.feeCurrency) - tx.gatewayFeeRecipient = inputAddressFormatter(tx.gatewayFeeRecipient) - - if (tx.data) { - tx.data = ensureLeading0x(tx.data) - } - - if (tx.data && !isHex(tx.data)) { +export function inputCeloTxFormatter(tx: CeloTx): FormattedCeloTx { + const { + from, + chainId, + nonce, + to, + gas, + gasPrice, + maxFeePerGas, + maxPriorityFeePerGas, + feeCurrency, + gatewayFee, + gatewayFeeRecipient, + data, + value, + accessList, + common, + chain, + hardfork, + ...rest + } = tx + const formattedTX: Partial = rest + formattedTX.from = inputAddressFormatter(from?.toString()) + formattedTX.to = inputAddressFormatter(to) + + formattedTX.gas = numberToHex(gas) + + formattedTX.value = numberToHex(value?.toString()) + formattedTX.nonce = numberToHex(nonce?.toString()) + + if (feeCurrency) { + formattedTX.feeCurrency = inputAddressFormatter(feeCurrency) + } + if (gatewayFeeRecipient) { + formattedTX.gatewayFeeRecipient = inputAddressFormatter(gatewayFeeRecipient) + } + if (gatewayFee) { + formattedTX.gatewayFee = numberToHex(gatewayFee) + } + + if (data && !isHex(data)) { throw new Error('The data field must be HEX encoded data.') + } else if (data) { + formattedTX.data = ensureLeading0x(data) } - tx.gas = numberToHex(tx.gas) - tx.gasPrice = numberToHex(tx.gasPrice?.toString()) - tx.value = numberToHex(tx.value?.toString()) - // @ts-ignore - nonce is defined as number, but uses as string (web3) - tx.nonce = numberToHex(tx.nonce?.toString()) - tx.gatewayFee = numberToHex(tx.gatewayFee) - - // @ts-ignore - prune undefines - Object.keys(tx).forEach((key) => tx[key] === undefined && delete tx[key]) + if (gasPrice) { + formattedTX.gasPrice = numberToHex(gasPrice.toString()) + } + if (maxFeePerGas) { + formattedTX.maxFeePerGas = numberToHex(maxFeePerGas.toString()) + } + if (maxPriorityFeePerGas) { + formattedTX.maxPriorityFeePerGas = numberToHex(maxPriorityFeePerGas.toString()) + } + if (accessList) { + formattedTX.accessList = inputAccessListFormatter(accessList) + } - return tx + return formattedTX as FormattedCeloTx } export function outputCeloTxFormatter(tx: any): CeloTxPending { @@ -52,9 +91,21 @@ export function outputCeloTxFormatter(tx: any): CeloTxPending { } tx.nonce = hexToNumber(tx.nonce) tx.gas = hexToNumber(tx.gas) - tx.gasPrice = outputBigNumberFormatter(tx.gasPrice) tx.value = outputBigNumberFormatter(tx.value) - tx.gatewayFee = outputBigNumberFormatter(tx.gatewayFee) + + if (tx.gatewayFee) { + tx.gatewayFee = outputBigNumberFormatter(tx.gatewayFee) + } + + if (tx.gasPrice) { + tx.gasPrice = outputBigNumberFormatter(tx.gasPrice) + } + if (tx.maxFeePerGas) { + tx.maxFeePerGas = outputBigNumberFormatter(tx.maxFeePerGas) + } + if (tx.maxPriorityFeePerGas) { + tx.maxPriorityFeePerGas = outputBigNumberFormatter(tx.maxPriorityFeePerGas) + } tx.to = tx.to && isValidAddress(tx.to) @@ -132,6 +183,7 @@ export function inputBlockNumberFormatter(blockNumber: BlockNumber) { : numberToHex(blockNumber.toString())! } +// TODO prune after gingerbread hardfork export function outputBlockHeaderFormatter(blockHeader: any): BlockHeader { // transform to number blockHeader.gasLimit = hexToNumber(blockHeader.gasLimit) @@ -213,12 +265,64 @@ export function outputBigNumberFormatter(hex: string): string { return new BigNumber(hex).toString(10) } -export function inputAddressFormatter(address?: string): string | undefined { +function isHash(value: string) { + return isHex(value) && value.length === 32 +} + +export function parseAccessList(accessListRaw: AccessListRaw | undefined): AccessList { + const accessList: AccessList = [] + if (!accessListRaw) { + return accessList + } + for (const entry of accessListRaw) { + const [address, storageKeys] = entry + + throwIfInvalidAddress(address) + + accessList.push({ + address, + storageKeys: storageKeys.map((key) => { + if (isHash(key)) { + return key + } else { + // same behavior as web3 + throw new Error(`Invalid storage key: ${key}`) + } + }), + }) + } + return accessList +} + +function throwIfInvalidAddress(address: string) { + if (!isValidAddress(address)) { + throw new Error(`Invalid address: ${address}`) + } +} + +export function inputAccessListFormatter(accessList?: AccessList): AccessListRaw { + if (!accessList || accessList.length === 0) { + return [] + } + return accessList.reduce((acc, { address, storageKeys }) => { + throwIfInvalidAddress(address) + + storageKeys.forEach((storageKey) => { + if (storageKey.length - 2 !== 64) { + throw new Error(`Invalid storage key: ${storageKey}`) + } + }) + acc.push([address, storageKeys]) + return acc + }, [] as AccessListRaw) +} + +export function inputAddressFormatter(address?: string): StrongAddress | undefined { if (!address || address === '0x') { return undefined } if (isValidAddress(address)) { - return ensureLeading0x(address).toLocaleLowerCase() + return ensureLeading0x(address).toLocaleLowerCase() as StrongAddress } throw new Error(`Provided address ${address} is invalid, the capitalization checksum test failed`) } @@ -256,12 +360,12 @@ function isHexStrict(hex: string): boolean { return /^(-)?0x[0-9a-f]*$/i.test(hex) } -function numberToHex(value?: BigNumber.Value) { +function numberToHex(value?: BigNumber.Value): Hex | undefined { if (value) { const numberValue = new BigNumber(value) const result = ensureLeading0x(new BigNumber(value).toString(16)) // Seen in web3, copied just in case - return numberValue.lt(new BigNumber(0)) ? `-${result}` : result + return (numberValue.lt(new BigNumber(0)) ? `-${result}` : result) as Hex } return undefined } diff --git a/packages/sdk/connect/src/utils/tx-params-normalizer.test.ts b/packages/sdk/connect/src/utils/tx-params-normalizer.test.ts index 2bfa4b0710e..b43b01954bd 100644 --- a/packages/sdk/connect/src/utils/tx-params-normalizer.test.ts +++ b/packages/sdk/connect/src/utils/tx-params-normalizer.test.ts @@ -17,6 +17,8 @@ describe('TxParamsNormalizer class', () => { value: 1, gas: 1, gasPrice: 1, + maxFeePerGas: undefined, + maxPriorityFeePerGas: undefined, feeCurrency: undefined, gatewayFeeRecipient: '1', gatewayFee: '1', @@ -92,56 +94,50 @@ describe('TxParamsNormalizer class', () => { expect(mockGasEstimation.mock.calls.length).toBe(1) }) - /* Disabled till the coinbase issue is fixed - - test('will populate the gatewayFeeRecipient', async () => { + test('will not pop maxFeePerGas and maxPriorityFeePerGas when gasPrice is set', async () => { const celoTx: CeloTx = { ...completeCeloTx } - celoTx.gatewayFeeRecipient = undefined + celoTx.gasPrice = 1 const newCeloTx = await populator.populate(celoTx) - expect(newCeloTx.gatewayFeeRecipient).toBe('27') - expect(mockRpcCall.mock.calls.length).toBe(1) - expect(mockRpcCall.mock.calls[0][0]).toBe('eth_coinbase') + expect(newCeloTx.maxFeePerGas).toBe(undefined) + expect(newCeloTx.maxPriorityFeePerGas).toBe(undefined) }) - - test('will retrieve only once the gatewayFeeRecipient', async () => { + test('will not pop maxFeePerGas if it is set', async () => { const celoTx: CeloTx = { ...completeCeloTx } - celoTx.gatewayFeeRecipient = undefined + celoTx.maxFeePerGas = 100 const newCeloTx = await populator.populate(celoTx) - expect(newCeloTx.gatewayFeeRecipient).toBe('27') - - const newCeloTx2 = await populator.populate(celoTx) - expect(newCeloTx2.gatewayFeeRecipient).toBe('27') - - expect(mockRpcCall.mock.calls.length).toBe(1) - expect(mockRpcCall.mock.calls[0][0]).toBe('eth_coinbase') + expect(newCeloTx.maxFeePerGas).toBe(100) }) - */ - - test('will populate the gas price without fee currency', async () => { + test('will not pop maxPriorityFeePerGas if it is set', async () => { const celoTx: CeloTx = { ...completeCeloTx } - celoTx.gasPrice = undefined + celoTx.maxPriorityFeePerGas = 2000 const newCeloTx = await populator.populate(celoTx) - expect(newCeloTx.gasPrice).toBe('0x27') - expect(mockRpcCall.mock.calls.length).toBe(1) - expect(mockRpcCall.mock.calls[0][0]).toBe('eth_gasPrice') + expect(newCeloTx.maxPriorityFeePerGas).toBe(2000) }) - test('will populate the gas price with fee currency', async () => { + test('will populate the maxFeePerGas and maxPriorityFeePerGas without fee currency', async () => { const celoTx: CeloTx = { ...completeCeloTx } celoTx.gasPrice = undefined - celoTx.feeCurrency = 'celoMagic' + celoTx.maxFeePerGas = undefined + celoTx.maxPriorityFeePerGas = undefined + celoTx.feeCurrency = undefined const newCeloTx = await populator.populate(celoTx) - expect(newCeloTx.gasPrice).toBe('0x27') - expect(mockRpcCall.mock.calls[0]).toEqual(['eth_gasPrice', ['celoMagic']]) + expect(newCeloTx.maxFeePerGas).toBe('0x2f') + expect(newCeloTx.maxPriorityFeePerGas).toBe('0x27') + expect(mockRpcCall.mock.calls[0]).toEqual(['eth_gasPrice', []]) + expect(mockRpcCall.mock.calls[1]).toEqual(['eth_maxPriorityFeePerGas', []]) }) - test('will not populate the gas price when fee currency is undefined', async () => { + test('will populate the maxFeePerGas and maxPriorityFeePerGas with fee currency', async () => { const celoTx: CeloTx = { ...completeCeloTx } celoTx.gasPrice = undefined - celoTx.feeCurrency = undefined + celoTx.maxFeePerGas = undefined + celoTx.maxPriorityFeePerGas = undefined + celoTx.feeCurrency = 'celoMagic' const newCeloTx = await populator.populate(celoTx) - expect(newCeloTx.gasPrice).toBe('0x27') - expect(mockRpcCall.mock.calls[0]).toEqual(['eth_gasPrice', []]) + expect(newCeloTx.maxFeePerGas).toBe('0x2f') + expect(newCeloTx.maxPriorityFeePerGas).toBe('0x27') + expect(mockRpcCall.mock.calls[0]).toEqual(['eth_gasPrice', ['celoMagic']]) + expect(mockRpcCall.mock.calls[1]).toEqual(['eth_maxPriorityFeePerGas', []]) }) }) }) diff --git a/packages/sdk/connect/src/utils/tx-params-normalizer.ts b/packages/sdk/connect/src/utils/tx-params-normalizer.ts index 7ef8168f67b..986bd3d4b6e 100644 --- a/packages/sdk/connect/src/utils/tx-params-normalizer.ts +++ b/packages/sdk/connect/src/utils/tx-params-normalizer.ts @@ -1,3 +1,4 @@ +import BigNumber from 'bignumber.js' import { Connection } from '../connection' import { CeloTx } from '../types' @@ -10,6 +11,9 @@ function isEmpty(value: string | undefined) { value.toLowerCase() === '0x0' ) } +function isPresent(value: string | undefined) { + return !isEmpty(value) +} export class TxParamsNormalizer { private chainId: number | null = null @@ -20,20 +24,73 @@ export class TxParamsNormalizer { public async populate(celoTxParams: CeloTx): Promise { const txParams = { ...celoTxParams } - if (txParams.chainId == null) { - txParams.chainId = await this.getChainId() + if (isPresent(txParams.gatewayFeeRecipient) || isPresent(txParams.gatewayFee)) { + console.warn( + 'Gateway fee has been deprecated and will be removed see: https://github.com/celo-org/celo-proposals/blob/master/CIPs/cip-0057.md' + ) } - if (txParams.nonce == null) { - txParams.nonce = await this.connection.nonce(txParams.from!.toString()) - } + const [chainId, nonce, gas, maxFeePerGas] = await Promise.all( + [ + async () => { + if (txParams.chainId == null) { + return this.getChainId() + } + return txParams.chainId + }, + async () => { + if (txParams.nonce == null) { + return this.connection.nonce(txParams.from!.toString()) + } + return txParams.nonce + }, + async () => { + if (!txParams.gas || isEmpty(txParams.gas.toString())) { + return this.connection.estimateGas(txParams) + } + return txParams.gas + }, + async () => { + // if gasPrice is not set and maxFeePerGas is not set, set maxFeePerGas + if ( + isEmpty(txParams.gasPrice?.toString()) && + isEmpty(txParams.maxFeePerGas?.toString()) + ) { + const suggestedPrice = await this.connection.gasPrice(txParams.feeCurrency) + // add small buffer to suggested price like other libraries do + const priceWithRoom = new BigNumber(suggestedPrice) + .times(120) + .dividedBy(100) + .integerValue() + .toString(16) + return `0x${priceWithRoom}` + } + return txParams.maxFeePerGas + }, + ].map(async (fn) => fn()) + ) + txParams.chainId = chainId as number + txParams.nonce = nonce as number + txParams.gas = gas as string + txParams.maxFeePerGas = maxFeePerGas - if (!txParams.gas || isEmpty(txParams.gas.toString())) { - txParams.gas = await this.connection.estimateGas(txParams) + // need to wait until after gas price has been handled + // if maxFeePerGas is set make sure maxPriorityFeePerGas is also set + if ( + isPresent(txParams.maxFeePerGas?.toString()) && + isEmpty(txParams.maxPriorityFeePerGas?.toString()) + ) { + const clientMaxPriorityFeePerGas = await this.connection.rpcCaller.call( + 'eth_maxPriorityFeePerGas', + [] + ) + txParams.maxPriorityFeePerGas = clientMaxPriorityFeePerGas.result } - if (!txParams.gasPrice || isEmpty(txParams.gasPrice.toString())) { - txParams.gasPrice = await this.connection.gasPrice(txParams.feeCurrency) + // remove gasPrice if maxFeePerGas is set + if (isPresent(txParams.gasPrice?.toString()) && isPresent(txParams.maxFeePerGas?.toString())) { + txParams.gasPrice = undefined + delete txParams.gasPrice } return txParams diff --git a/packages/sdk/contractkit/package.json b/packages/sdk/contractkit/package.json index 11ae3796afd..4f15b54875b 100644 --- a/packages/sdk/contractkit/package.json +++ b/packages/sdk/contractkit/package.json @@ -1,6 +1,6 @@ { "name": "@celo/contractkit", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo's ContractKit to interact with Celo network", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -29,10 +29,10 @@ "lint": "tslint -c tslint.json --project ." }, "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", - "@celo/wallet-local": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-local": "5.0.3-dev", "@types/bn.js": "^5.1.0", "@types/debug": "^4.1.5", "bignumber.js": "^9.0.0", @@ -44,7 +44,7 @@ "web3": "1.10.0" }, "devDependencies": { - "@celo/phone-utils": "4.1.2-dev", + "@celo/phone-utils": "5.0.3-dev", "@celo/dev-utils": "0.0.1-dev", "@celo/protocol": "1.0.0", "@types/debug": "^4.1.5", diff --git a/packages/sdk/contractkit/src/kit.test.ts b/packages/sdk/contractkit/src/kit.test.ts index 951e7fd056a..f2d76946676 100644 --- a/packages/sdk/contractkit/src/kit.test.ts +++ b/packages/sdk/contractkit/src/kit.test.ts @@ -1,5 +1,4 @@ import { CeloTx, CeloTxObject, CeloTxReceipt, JsonRpcPayload, PromiEvent } from '@celo/connect' -import { BigNumber } from 'bignumber.js' import Web3 from 'web3' import { HttpProvider } from 'web3-core' import { newKitFromWeb3 as newFullKitFromWeb3, newKitWithApiKey } from './kit' @@ -79,31 +78,46 @@ export function txoStub(): TransactionObjectStub { const txo = txoStub() await kit.connection.sendTransactionObject(txo, { gas: 555, from: '0xAAFFF' }) expect(txo.send).toBeCalledWith({ - gasPrice: '0', + feeCurrency: undefined, gas: 555, from: '0xAAFFF', }) }) - }) -}) -test('should retrieve currency gasPrice with feeCurrency', async () => { - const kit = newFullKitFromWeb3(new Web3('http://')) + test('works with maxFeePerGas and maxPriorityFeePerGas', async () => { + const txo = txoStub() + await kit.connection.sendTransactionObject(txo, { + gas: 1000, + maxFeePerGas: 555, + maxPriorityFeePerGas: 555, + from: '0xAAFFF', + }) + expect(txo.send).toBeCalledWith({ + feeCurrency: undefined, + maxFeePerGas: 555, + maxPriorityFeePerGas: 555, + gas: 1000, + from: '0xAAFFF', + }) + }) - const txo = txoStub() - const gasPrice = 100 - const getGasPriceMin = jest.fn().mockImplementation(() => ({ - getGasPriceMinimum() { - return new BigNumber(gasPrice) - }, - })) - kit.contracts.getGasPriceMinimum = getGasPriceMin.bind(kit.contracts) - await kit.updateGasPriceInConnectionLayer('XXX') - const options: CeloTx = { gas: 555, feeCurrency: 'XXX', from: '0xAAFFF' } - await kit.connection.sendTransactionObject(txo, options) - expect(txo.send).toBeCalledWith({ - gasPrice: `${gasPrice * 5}`, - ...options, + test('when maxFeePerGas and maxPriorityFeePerGas and feeCurrency', async () => { + const txo = txoStub() + await kit.connection.sendTransactionObject(txo, { + gas: 1000, + maxFeePerGas: 555, + maxPriorityFeePerGas: 555, + feeCurrency: '0xe8537a3d056da446677b9e9d6c5db704eaab4787', + from: '0xAAFFF', + }) + expect(txo.send).toBeCalledWith({ + gas: 1000, + maxFeePerGas: 555, + maxPriorityFeePerGas: 555, + feeCurrency: '0xe8537a3d056da446677b9e9d6c5db704eaab4787', + from: '0xAAFFF', + }) + }) }) }) diff --git a/packages/sdk/contractkit/src/kit.ts b/packages/sdk/contractkit/src/kit.ts index 49ec754fc21..bd4061fc8f3 100644 --- a/packages/sdk/contractkit/src/kit.ts +++ b/packages/sdk/contractkit/src/kit.ts @@ -109,9 +109,6 @@ export class ContractKit { /** helper for interacting with CELO & stable tokens */ readonly celoTokens: CeloTokens - /** @deprecated no longer needed since gasPrice is available on node rpc */ - gasPriceSuggestionMultiplier = 5 - constructor(readonly connection: Connection) { this.registry = new AddressRegistry(connection) this._web3Contracts = new Web3ContractCache(this.registry) @@ -200,20 +197,9 @@ export class ContractKit { tokenContract === CeloContract.GoldToken ? undefined : await this.registry.addressFor(tokenContract) - if (address) { - await this.updateGasPriceInConnectionLayer(address) - } this.connection.defaultFeeCurrency = address } - /** @deprecated no longer needed since gasPrice is available on node rpc */ - async updateGasPriceInConnectionLayer(currency: Address) { - const gasPriceMinimum = await this.contracts.getGasPriceMinimum() - const rawGasPrice = await gasPriceMinimum.getGasPriceMinimum(currency) - const gasPrice = rawGasPrice.multipliedBy(this.gasPriceSuggestionMultiplier).toFixed() - await this.connection.setGasPriceForCurrency(currency, gasPrice) - } - async getEpochSize(): Promise { const blockchainParamsWrapper = await this.contracts.getBlockchainParameters() return blockchainParamsWrapper.getEpochSizeNumber() @@ -258,14 +244,6 @@ export class ContractKit { return this.connection.defaultGasInflationFactor } - set gasPrice(price: number) { - this.connection.defaultGasPrice = price - } - - get gasPrice() { - return this.connection.defaultGasPrice - } - set defaultFeeCurrency(address: Address | undefined) { this.connection.defaultFeeCurrency = address } @@ -281,13 +259,6 @@ export class ContractKit { isSyncing(): Promise { return this.connection.isSyncing() } - /** @deprecated no longer needed since gasPrice is available on node rpc */ - async fillGasPrice(tx: CeloTx): Promise { - if (tx.feeCurrency && tx.gasPrice === '0') { - await this.updateGasPriceInConnectionLayer(tx.feeCurrency) - } - return this.connection.fillGasPrice(tx) - } async sendTransaction(tx: CeloTx): Promise { return this.connection.sendTransaction(tx) diff --git a/packages/sdk/cryptographic-utils/package.json b/packages/sdk/cryptographic-utils/package.json index 0bbada347de..46eb3b50000 100644 --- a/packages/sdk/cryptographic-utils/package.json +++ b/packages/sdk/cryptographic-utils/package.json @@ -1,6 +1,6 @@ { "name": "@celo/cryptographic-utils", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Some Celo utils for comment/data encryption, bls, and mnemonics", "author": "Celo", "license": "Apache-2.0", @@ -22,9 +22,9 @@ "lib/**/*" ], "dependencies": { - "@celo/utils": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", "@celo/bls12377js": "0.1.1", - "@celo/base": "4.1.2-dev", + "@celo/base": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@types/bn.js": "^5.1.0", "@types/elliptic": "^6.4.9", diff --git a/packages/sdk/encrypted-backup/package.json b/packages/sdk/encrypted-backup/package.json index 2c8c9ebe94e..416a4622484 100644 --- a/packages/sdk/encrypted-backup/package.json +++ b/packages/sdk/encrypted-backup/package.json @@ -1,6 +1,6 @@ { "name": "@celo/encrypted-backup", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Libraries for implemented password encrypted account backups", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -25,11 +25,11 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/identity": "4.1.2-dev", - "@celo/phone-number-privacy-common": "^3.0.1-dev", + "@celo/base": "5.0.3-dev", + "@celo/identity": "5.0.3-dev", + "@celo/phone-number-privacy-common": "^3.0.3", "@celo/poprf": "^0.1.9", - "@celo/utils": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", "@types/debug": "^4.1.5", "debug": "^4.1.1", "fp-ts": "2.1.1", diff --git a/packages/sdk/explorer/package.json b/packages/sdk/explorer/package.json index 751094b0b78..3158c105185 100644 --- a/packages/sdk/explorer/package.json +++ b/packages/sdk/explorer/package.json @@ -1,6 +1,6 @@ { "name": "@celo/explorer", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo's block explorer consumer", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -22,10 +22,10 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", - "@celo/contractkit": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", + "@celo/contractkit": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", "@types/debug": "^4.1.5", "cross-fetch": "3.0.6", "debug": "^4.1.1" diff --git a/packages/sdk/governance/package.json b/packages/sdk/governance/package.json index 5ddc9289c03..7f2eb1b7174 100644 --- a/packages/sdk/governance/package.json +++ b/packages/sdk/governance/package.json @@ -1,6 +1,6 @@ { "name": "@celo/governance", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo's governance proposals", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -21,11 +21,11 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", - "@celo/contractkit": "4.1.2-dev", - "@celo/explorer": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", + "@celo/contractkit": "5.0.3-dev", + "@celo/explorer": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@types/debug": "^4.1.5", "@types/inquirer": "^6.5.0", diff --git a/packages/sdk/identity/package.json b/packages/sdk/identity/package.json index 304278231f7..04064ee4160 100644 --- a/packages/sdk/identity/package.json +++ b/packages/sdk/identity/package.json @@ -1,6 +1,6 @@ { "name": "@celo/identity", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Utilities for interacting with Celo's identity protocol", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -25,10 +25,10 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", - "@celo/contractkit": "4.1.2-dev", - "@celo/phone-number-privacy-common": "^3.0.1-dev", + "@celo/base": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/contractkit": "5.0.3-dev", + "@celo/phone-number-privacy-common": "^3.0.3", "@types/debug": "^4.1.5", "bignumber.js": "^9.0.0", "blind-threshold-bls": "https://github.com/celo-org/blind-threshold-bls-wasm#e1e2f8a", @@ -41,7 +41,7 @@ }, "devDependencies": { "@celo/dev-utils": "0.0.1-dev", - "@celo/wallet-local": "4.1.2-dev", + "@celo/wallet-local": "5.0.3-dev", "@types/elliptic": "^6.4.12", "fetch-mock": "9.10.4", "ganache": "npm:@celo/ganache@7.8.0-unofficial.0", diff --git a/packages/sdk/keystores/package.json b/packages/sdk/keystores/package.json index d15a241ef1f..6f92d017950 100644 --- a/packages/sdk/keystores/package.json +++ b/packages/sdk/keystores/package.json @@ -1,6 +1,6 @@ { "name": "@celo/keystores", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "keystore implementation", "author": "Celo", "license": "Apache-2.0", @@ -22,8 +22,8 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/utils": "4.1.2-dev", - "@celo/wallet-local": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-local": "5.0.3-dev", "ethereumjs-wallet": "^1.0.1" }, "devDependencies": { diff --git a/packages/sdk/network-utils/package.json b/packages/sdk/network-utils/package.json index 85e8bf560a2..080861ca120 100644 --- a/packages/sdk/network-utils/package.json +++ b/packages/sdk/network-utils/package.json @@ -1,6 +1,6 @@ { "name": "@celo/network-utils", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Utilities for fetching static information about the Celo network", "main": "./lib/index.js", "types": "./lib/index.d.ts", diff --git a/packages/sdk/phone-utils/package.json b/packages/sdk/phone-utils/package.json index ff50fc223f3..d47b5a40612 100644 --- a/packages/sdk/phone-utils/package.json +++ b/packages/sdk/phone-utils/package.json @@ -1,6 +1,6 @@ { "name": "@celo/phone-utils", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo phone utils", "author": "Celo", "license": "Apache-2.0", @@ -22,8 +22,8 @@ "lib/**/*" ], "dependencies": { - "@celo/base": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", "@types/country-data": "^0.0.0", "@types/google-libphonenumber": "^7.4.23", "@types/node": "^10.12.18", diff --git a/packages/sdk/transactions-uri/package.json b/packages/sdk/transactions-uri/package.json index d50c8cb5f04..d315eac4596 100644 --- a/packages/sdk/transactions-uri/package.json +++ b/packages/sdk/transactions-uri/package.json @@ -1,6 +1,6 @@ { "name": "@celo/transactions-uri", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo's transactions uri generation", "main": "./lib/index.js", "types": "./lib/index.d.ts", @@ -26,15 +26,15 @@ "dependencies": { "@types/debug": "^4.1.5", "@types/qrcode": "^1.3.4", - "@celo/base": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", + "@celo/base": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", "bn.js": "4.11.9", "qrcode": "1.4.4", "web3-eth-abi": "1.10.0" }, "devDependencies": { "@celo/dev-utils": "0.0.1-dev", - "@celo/contractkit": "4.1.2-dev", + "@celo/contractkit": "5.0.3-dev", "dotenv": "^8.2.0" }, "engines": { diff --git a/packages/sdk/utils/package.json b/packages/sdk/utils/package.json index 7977f28d9bc..e4c9f240bcb 100644 --- a/packages/sdk/utils/package.json +++ b/packages/sdk/utils/package.json @@ -1,6 +1,6 @@ { "name": "@celo/utils", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Celo common utils", "author": "Celo", "license": "Apache-2.0", @@ -22,7 +22,7 @@ "lib/**/*" ], "dependencies": { - "@celo/base": "4.1.2-dev", + "@celo/base": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@types/bn.js": "^5.1.0", "@types/elliptic": "^6.4.9", diff --git a/packages/sdk/wallets/wallet-base/jest.config.js b/packages/sdk/wallets/wallet-base/jest.config.js new file mode 100644 index 00000000000..2681a75d5a6 --- /dev/null +++ b/packages/sdk/wallets/wallet-base/jest.config.js @@ -0,0 +1,5 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['/src/**/?(*.)+(spec|test).ts?(x)'], +} diff --git a/packages/sdk/wallets/wallet-base/package.json b/packages/sdk/wallets/wallet-base/package.json index 5ff32097b5a..53ac7ceaabb 100644 --- a/packages/sdk/wallets/wallet-base/package.json +++ b/packages/sdk/wallets/wallet-base/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-base", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Wallet base implementation", "author": "Celo", "license": "Apache-2.0", @@ -21,11 +21,15 @@ "lint": "tslint -c tslint.json --project .", "prepublishOnly": "yarn build" }, + "devDependencies": { + "viem": "~1.5.4" + }, "dependencies": { - "@celo/connect": "4.1.2-dev", - "@celo/base": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", + "@celo/base": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", "@ethereumjs/util": "8.0.5", + "ethereum-cryptography": "^2.1.2", "@types/debug": "^4.1.5", "bignumber.js": "^9.0.0", "debug": "^4.1.1", diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts new file mode 100644 index 00000000000..ed2ebb402f7 --- /dev/null +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.test.ts @@ -0,0 +1,602 @@ +import { CeloTx } from '@celo/connect' +import { normalizeAddressWith0x, privateKeyToAddress } from '@celo/utils/lib/address' +import { parseTransaction, serializeTransaction } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' +import { celo } from 'viem/chains' +import Web3 from 'web3' +import { + extractSignature, + getSignerFromTxCIP42, + isPriceToLow, + recoverTransaction, + rlpEncodedTx, + stringNumberOrBNToHex, +} from './signing-utils' +const PRIVATE_KEY1 = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' +const ACCOUNT_ADDRESS1 = normalizeAddressWith0x(privateKeyToAddress(PRIVATE_KEY1)) as `0x${string}` + +describe('rlpEncodedTx', () => { + describe('legacy', () => { + const legacyTransaction = { + feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631', + from: ACCOUNT_ADDRESS1, + to: ACCOUNT_ADDRESS1, + chainId: 2, + value: Web3.utils.toWei('1000', 'ether'), + nonce: 1, + gas: '1500000000', + gasPrice: '9900000000', + data: '0xabcdef', + } + it('convert CeloTx into RLP', () => { + const transaction = { + ...legacyTransaction, + } + const result = rlpEncodedTx(transaction) + expect(result).toMatchInlineSnapshot(` + { + "rlpEncode": "0xf8490185024e1603008459682f00945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdef028080", + "transaction": { + "chainId": 2, + "data": "0xabcdef", + "feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631", + "from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "gas": "0x59682f00", + "gasPrice": "0x024e160300", + "gatewayFee": "0x", + "gatewayFeeRecipient": "0x", + "maxFeePerGas": "0x", + "maxPriorityFeePerGas": "0x", + "nonce": 1, + "to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "value": "0x3635c9adc5dea00000", + }, + "type": "celo-legacy", + } + `) + }) + + describe('when chainId / gasPrice / nonce is invalid', () => { + it('chainId is not a positive number it throws error', () => { + const transaction = { + ...legacyTransaction, + chainId: -1, + } + expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( + `"Gas, nonce or chainId is less than than 0"` + ) + }) + it('gasPrice is not a positive number it throws error', () => { + const transaction = { + ...legacyTransaction, + gasPrice: -1, + } + expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( + `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` + ) + }) + it('nonce is not a positive number it throws error', () => { + const transaction = { + ...legacyTransaction, + nonce: -1, + } + expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( + `"Gas, nonce or chainId is less than than 0"` + ) + }) + it('gas is not a positive number it throws error', () => { + const transaction = { + ...legacyTransaction, + gas: -1, + } + expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( + `"Gas, nonce or chainId is less than than 0"` + ) + }) + }) + }) + + describe('when no gas fields are provided', () => { + it('throws an error', () => { + expect(() => rlpEncodedTx({})).toThrowErrorMatchingInlineSnapshot(`""gas" is missing"`) + }) + }) + + describe('EIP1559 / CIP42', () => { + const eip1559Transaction: CeloTx = { + from: ACCOUNT_ADDRESS1, + to: ACCOUNT_ADDRESS1, + chainId: 2, + value: Web3.utils.toWei('1000', 'ether'), + nonce: 0, + maxFeePerGas: '10', + maxPriorityFeePerGas: '99', + gas: '99', + data: '0xabcdef', + } + + describe('when maxFeePerGas is to low', () => { + it('throws an error', () => { + const transaction = { + ...eip1559Transaction, + maxFeePerGas: Web3.utils.toBN('-5'), + } + expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( + `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` + ) + }) + }) + describe('when maxPriorityFeePerGas is to low', () => { + it('throws an error', () => { + const transaction = { + ...eip1559Transaction, + maxPriorityFeePerGas: Web3.utils.toBN('-5'), + } + expect(() => rlpEncodedTx(transaction)).toThrowErrorMatchingInlineSnapshot( + `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` + ) + }) + }) + + describe('when maxFeePerGas and maxPriorityFeePerGas and feeCurrency are provided', () => { + it('orders fields in RLP as specified by CIP42', () => { + const CIP42Transaction = { + ...eip1559Transaction, + feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631', + } + const result = rlpEncodedTx(CIP42Transaction) + expect(result).toMatchInlineSnapshot(` + { + "rlpEncode": "0x7cf8400280630a63945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0", + "transaction": { + "chainId": 2, + "data": "0xabcdef", + "feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631", + "from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "gas": "0x63", + "gasPrice": "0x", + "gatewayFee": "0x", + "gatewayFeeRecipient": "0x", + "maxFeePerGas": "0x0a", + "maxPriorityFeePerGas": "0x63", + "nonce": 0, + "to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "value": "0x3635c9adc5dea00000", + }, + "type": "cip42", + } + `) + }) + }) + + describe('when maxFeePerGas and maxPriorityFeePerGas are provided', () => { + it('orders fields in RLP as specified by EIP1559', () => { + const CIP42Transaction = { + ...eip1559Transaction, + feeCurrency: '0x5409ED021D9299bf6814279A6A1411A7e866A631', + } + const result = rlpEncodedTx(CIP42Transaction) + expect(result).toMatchInlineSnapshot(` + { + "rlpEncode": "0x7cf8400280630a63945409ed021d9299bf6814279a6a1411a7e866a6318080941be31a94361a391bbafb2a4ccd704f57dc04d4bb893635c9adc5dea0000083abcdefc0", + "transaction": { + "chainId": 2, + "data": "0xabcdef", + "feeCurrency": "0x5409ed021d9299bf6814279a6a1411a7e866a631", + "from": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "gas": "0x63", + "gasPrice": "0x", + "gatewayFee": "0x", + "gatewayFeeRecipient": "0x", + "maxFeePerGas": "0x0a", + "maxPriorityFeePerGas": "0x63", + "nonce": 0, + "to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "value": "0x3635c9adc5dea00000", + }, + "type": "cip42", + } + `) + }) + }) + }) + describe('compared to viem', () => { + it('matches output of viem serializeTransaction with accessList', () => { + const tx = { + type: 'eip1559' as const, + from: ACCOUNT_ADDRESS1, + to: ACCOUNT_ADDRESS1, + chainId: 2, + value: Web3.utils.toWei('1000', 'ether'), + nonce: 0, + maxFeePerGas: '1000', + maxPriorityFeePerGas: '99', + gas: '9900', + data: '0xabcdef' as const, + accessList: [ + { + address: '0x0000000000000000000000000000000000000000' as const, + storageKeys: [ + '0x0000000000000000000000000000000000000000000000000000000000000001' as const, + '0x60fdd29ff912ce880cd3edaf9f932dc61d3dae823ea77e0323f94adb9f6a72fe' as const, + ], + }, + ], + } + const txViem = { + ...tx, + gas: BigInt(tx.gas), + maxFeePerGas: BigInt(tx.maxFeePerGas), + maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas), + value: BigInt(tx.value), + } + const viemSerialized = serializeTransaction(txViem) + const serialized = rlpEncodedTx(tx) + + const parsedCK = parseTransaction(serialized.rlpEncode) + const parsedViem = parseTransaction(viemSerialized) + expect(parsedCK).toEqual(parsedViem) + expect(serialized.rlpEncode).toEqual(viemSerialized) + }) + it('matches output of viem serializeTransaction without accessList', () => { + const tx = { + type: 'eip1559' as const, + from: ACCOUNT_ADDRESS1, + to: ACCOUNT_ADDRESS1, + chainId: 2, + value: Web3.utils.toWei('1000', 'ether'), + nonce: 0, + maxFeePerGas: '1000', + maxPriorityFeePerGas: '99', + gas: '9900', + data: '0xabcdef' as const, + } + const txViem = { + ...tx, + gas: BigInt(tx.gas), + maxFeePerGas: BigInt(tx.maxFeePerGas), + maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas), + value: BigInt(tx.value), + } + const viemSerialized = serializeTransaction(txViem) + const serialized = rlpEncodedTx(tx) + + const parsedCK = parseTransaction(serialized.rlpEncode) + const parsedViem = parseTransaction(viemSerialized) + expect(parsedCK).toEqual(parsedViem) + expect(serialized.rlpEncode).toEqual(viemSerialized) + }) + }) +}) + +function ckToViem(tx: CeloTx & { v?: number }) { + return { + ...tx, + gas: BigInt(tx.gas!), + maxFeePerGas: BigInt(tx.maxFeePerGas?.toString()!), + maxPriorityFeePerGas: BigInt(tx.maxPriorityFeePerGas?.toString()!), + value: BigInt(tx.value?.toString()!), + v: BigInt(tx.v?.toString()! === '0x' ? 0 : tx.v?.toString()!), + } +} + +describe('recoverTransaction', () => { + const ACCOUNT_ADDRESS1 = privateKeyToAddress(PRIVATE_KEY1) + describe('with EIP1559 transactions', () => { + test('ok', async () => { + const account = privateKeyToAccount(PRIVATE_KEY1) + const hash = await account.signTransaction({ + type: 'eip1559' as const, + from: ACCOUNT_ADDRESS1, + to: ACCOUNT_ADDRESS1 as `0x${string}`, + chainId: 2, + value: BigInt(1000), + nonce: 0, + maxFeePerGas: BigInt('1000'), + maxPriorityFeePerGas: BigInt('99'), + gas: BigInt('9900'), + data: '0xabcdef' as const, + }) + + const [transaction, signer] = recoverTransaction(hash) + expect(signer).toEqual(ACCOUNT_ADDRESS1) + expect(transaction).toMatchInlineSnapshot(` + { + "accessList": [], + "chainId": 2, + "data": "0xabcdef", + "gas": 9900, + "maxFeePerGas": 1000, + "maxPriorityFeePerGas": 99, + "nonce": 0, + "r": "0x04ddb2c87a6e0f77aa25da7439c72f978541f74fa1bd20becf2e109301d2f85c", + "s": "0x2d91eec5c0abca75d4df8322677bf43306e90172b77914494bbb7641b6dfc7e9", + "to": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "type": "eip1559", + "v": 28, + "value": 1000, + "yParity": 1, + } + `) + }) + + it('matches output of viem parseTransaction', () => { + const encodedByCK1559TX = + // from packages/sdk/wallets/wallet-local/src/local-wallet.test.ts:211 -- when calling signTransaction succeeds with eip1559 + '0x02f86d82ad5a8063630a94588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc080a02c61b97c545c0a59732adbc497e944818da323a508930996383751d17e0b932ea015666dce65f074f12335ab78e1912f8b83fda75f05a002943459598712e6b17c' + const [transaction, signer] = recoverTransaction(encodedByCK1559TX) + const parsed = parseTransaction(encodedByCK1559TX) + + expect(ckToViem(transaction)).toMatchObject(parsed) + expect(signer).toMatchInlineSnapshot(`"0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb"`) + }) + it('can recover (parse) transactions signed by viem', () => { + // stolen from viems's default eip1559 test result viem/src/accounts/utils/signTransaction.test.ts + const encodedByViem1559TX = + '0x02f850018203118080825208808080c080a04012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1a04e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33' + const recovered = recoverTransaction(encodedByViem1559TX) + expect(recovered).toMatchInlineSnapshot(` + [ + { + "accessList": [], + "chainId": 1, + "data": "0x", + "gas": 21000, + "maxFeePerGas": 0, + "maxPriorityFeePerGas": 0, + "nonce": 785, + "r": "0x4012522854168b27e5dc3d5839bab5e6b39e1a0ffd343901ce1622e3d64b48f1", + "s": "0x4e00902ae0502c4728cbf12156290df99c3ed7de85b1dbfe20b5c36931733a33", + "to": "0x", + "type": "eip1559", + "v": 27, + "value": 0, + "yParity": 0, + }, + "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + ] + `) + }) + }) + it('handles celo-legacy transactions', () => { + const celoLegacyTx = + '0xf88480630a80941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdef83015ad8a09e121a99dc0832a9f4d1d71500b3c8a69a3c064d437c225d6292577ffcc45a71a02c5efa3c4b58953c35968e42d11d3882dacacf45402ee802824268b7cd60daff' + expect(recoverTransaction(celoLegacyTx)).toMatchInlineSnapshot(` + [ + { + "chainId": "0xad5a", + "data": "0xabcdef", + "feeCurrency": "0x", + "gas": 10, + "gasPrice": 99, + "gatewayFee": "0x5678", + "gatewayFeeRecipient": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "nonce": 0, + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "type": "celo-legacy", + "value": "0x0de0b6b3a7640000", + }, + "0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb", + ] + `) + }) + it('handles cip42 transactions', () => { + const cip42TX = + '0x7cf89a82ad5a8063630a94cd2a3d9f938e13cd947ec05abc7fe734df8dd826941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc01ba0c610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1a01799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112' + expect(recoverTransaction(cip42TX)).toMatchInlineSnapshot(` + [ + { + "accessList": [], + "chainId": 44378, + "data": "0xabcdef", + "feeCurrency": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "gas": 10, + "gatewayFee": "0x5678", + "gatewayFeeRecipient": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "maxFeePerGas": 99, + "maxPriorityFeePerGas": 99, + "nonce": 0, + "r": "0xc610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1", + "s": "0x1799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "type": "cip42", + "v": 28, + "value": 1000000000000000000, + "yParity": 1, + }, + "0x90AB065B949165c47Acac34cA9A43171bBeBb1E1", + ] + `) + }) + test('cip42 serialized by viem', async () => { + const account = privateKeyToAccount(PRIVATE_KEY1) + const signed = await account.signTransaction( + { + // @ts-ignore -- types on viem dont quite work for setting the tx type but the actual js execution works fine + type: 'cip42', + accessList: [], + chainId: 44378, + data: '0xabcdef', + feeCurrency: '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', + gas: BigInt(10), + gatewayFee: BigInt('0x5678'), + gatewayFeeRecipient: '0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb', + maxFeePerGas: BigInt(99), + maxPriorityFeePerGas: BigInt(99), + nonce: 0, + to: '0x588e4b68193001e4d10928660ab4165b813717c0', + value: BigInt(1000000000000000000), + }, + { serializer: celo.serializers?.transaction } + ) + + expect(recoverTransaction(signed)).toMatchInlineSnapshot(` + [ + { + "accessList": [], + "chainId": 44378, + "data": "0xabcdef", + "feeCurrency": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "gas": 10, + "gatewayFee": "0x5678", + "gatewayFeeRecipient": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "maxFeePerGas": 99, + "maxPriorityFeePerGas": 99, + "nonce": 0, + "r": "0xc610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1", + "s": "0x1799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "type": "cip42", + "v": 27, + "value": 1000000000000000000, + "yParity": 0, + }, + "0x1Be31A94361a391bBaFB2a4CCd704F57dc04d4bb", + ] + `) + expect(recoverTransaction(signed)[1]).toEqual(account.address) + }) +}) + +describe('isPriceToLow', () => { + test('maxFee and maxPriorityFee are positive', () => { + expect( + isPriceToLow({ + maxFeePerGas: 1_000_000_000, + maxPriorityFeePerGas: Web3.utils.toBN('50000000000000'), + gasPrice: undefined, + }) + ).toBe(false) + }) + test('gasPrice is positive', () => { + expect( + isPriceToLow({ + gasPrice: Web3.utils.toBN('50000000000000'), + }) + ).toBe(false) + }) + test('maxFeePerGas is less than 0 but maxPriorityFeePerGas is positive ', () => { + expect(() => + isPriceToLow({ + maxFeePerGas: -1, + maxPriorityFeePerGas: 1_000_000_000, + gasPrice: undefined, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` + ) + }) + test('maxPriorityFeePerGas is less than 0 but maxFeePerGas is positive ', () => { + expect(() => + isPriceToLow({ + maxFeePerGas: 1_000_000_000, + maxPriorityFeePerGas: -1_000_000_000, + gasPrice: undefined, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` + ) + }) + test('gasPrice is less than 0', () => { + expect(() => + isPriceToLow({ + maxFeePerGas: '0x', + maxPriorityFeePerGas: '0x', + gasPrice: -1, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0"` + ) + }) +}) + +describe('extractSignature', () => { + it('extracts from celo legacy txs', () => { + // packages/sdk/wallets/wallet-local/src/local-wallet.test.ts:176 (succeeds with legacy) + const extracted = extractSignature( + '0xf88480630a80941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdef83015ad8a09e121a99dc0832a9f4d1d71500b3c8a69a3c064d437c225d6292577ffcc45a71a02c5efa3c4b58953c35968e42d11d3882dacacf45402ee802824268b7cd60daff' + ) + expect(extracted).toMatchInlineSnapshot(` + { + "r": "0x9e121a99dc0832a9f4d1d71500b3c8a69a3c064d437c225d6292577ffcc45a71", + "s": "0x2c5efa3c4b58953c35968e42d11d3882dacacf45402ee802824268b7cd60daff", + "v": "0x015ad8", + } + `) + }) + it('extracts from cip42 txs', () => { + // packages/sdk/wallets/wallet-local/src/local-wallet.test.ts:274 (succeeds with cip42) + const extracted = extractSignature( + '0x7cf89a82ad5a8063630a94cd2a3d9f938e13cd947ec05abc7fe734df8dd826941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc01ba0c610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1a01799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112' + ) + expect(extracted).toMatchInlineSnapshot(` + { + "r": "0xc610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1", + "s": "0x1799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112", + "v": "0x1b", + } + `) + }) + it('extracts from eip1559 txs', () => { + // packages/sdk/wallets/wallet-local/src/local-wallet.test.ts:209 ( succeeds with eip1559) + const extracted = extractSignature( + '0x02f87082ad5a8063630a94588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdef8083015ad7a00fd2d0c579a971ebc04207d8c5ff5bb3449052f0c9e946a7146e5ae4d4ec6289a0737423de64cc81a62e700b5ca7970212aaed3d28de4dbce8b054483d361f6ff8' + ) + expect(extracted).toMatchInlineSnapshot(` + { + "r": "0x0fd2d0c579a971ebc04207d8c5ff5bb3449052f0c9e946a7146e5ae4d4ec6289", + "s": "0x737423de64cc81a62e700b5ca7970212aaed3d28de4dbce8b054483d361f6ff8", + "v": "0x015ad7", + } + `) + }) + it('fails when length is wrong', () => { + expect(() => extractSignature('0x')).toThrowErrorMatchingInlineSnapshot( + `"@extractSignature: provided transaction has 0 elements but celo-legacy txs with a signature have 12 []"` + ) + }) +}) + +describe('getSignerFromTx', () => { + const account = privateKeyToAccount(PRIVATE_KEY1) + test('extracts signer address from cip42 tx signed by viem', async () => { + const signed = await account.signTransaction( + { + // @ts-ignore + type: 'cip42', + accessList: [], + chainId: 44378, + data: '0xabcdef', + feeCurrency: '0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826', + gas: BigInt(10), + gatewayFee: BigInt('0x5678'), + gatewayFeeRecipient: '0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb', + maxFeePerGas: BigInt(99), + maxPriorityFeePerGas: BigInt(99), + nonce: 0, + to: '0x588e4b68193001e4d10928660ab4165b813717c0', + value: BigInt(1000000000000000000), + }, + { serializer: celo.serializers?.transaction } + ) + expect(getSignerFromTxCIP42(signed)).toEqual(account.address) + }) +}) + +describe('stringNumberOrBNToHex', () => { + test('string as base 10 number', () => { + expect(stringNumberOrBNToHex('1230000000000020')).toEqual('0x045eadb112e014') + expect(stringNumberOrBNToHex('123')).toEqual('0x7b') + }) + test('string as base 16 number', () => { + expect(stringNumberOrBNToHex('0x7b')).toEqual('0x7b') + }) + test('number', () => { + expect(stringNumberOrBNToHex(1230000000000020)).toEqual('0x045eadb112e014') + expect(stringNumberOrBNToHex(123)).toEqual('0x7b') + }) + test('BN', () => { + const biggie = Web3.utils.toBN('123') + expect(stringNumberOrBNToHex(biggie)).toEqual('0x7b') + }) +}) diff --git a/packages/sdk/wallets/wallet-base/src/signing-utils.ts b/packages/sdk/wallets/wallet-base/src/signing-utils.ts index c711de1195b..f0a6b874886 100644 --- a/packages/sdk/wallets/wallet-base/src/signing-utils.ts +++ b/packages/sdk/wallets/wallet-base/src/signing-utils.ts @@ -1,13 +1,37 @@ import { ensureLeading0x, trimLeading0x } from '@celo/base/lib/address' -import { CeloTx, EncodedTransaction, RLPEncodedTx } from '@celo/connect' -import { inputCeloTxFormatter } from '@celo/connect/lib/utils/formatter' +import { + CeloTx, + CeloTxWithSig, + EncodedTransaction, + Hex, + isPresent, + RLPEncodedTx, + TransactionTypes, +} from '@celo/connect' +import { + hexToNumber, + inputCeloTxFormatter, + parseAccessList, +} from '@celo/connect/lib/utils/formatter' import { EIP712TypedData, generateTypedDataHash } from '@celo/utils/lib/sign-typed-data-utils' import { parseSignatureWithoutPrefix } from '@celo/utils/lib/signatureUtils' -import * as ethUtil from '@ethereumjs/util' +import { + Address, + bufferToHex, + ecrecover, + fromRpcSig, + hashPersonalMessage, + pubToAddress, + toBuffer, + toChecksumAddress, +} from '@ethereumjs/util' import debugFactory from 'debug' // @ts-ignore-next-line eth-lib types not found import { account as Account, bytes as Bytes, hash as Hash, RLP } from 'eth-lib' - +import { keccak256 } from 'ethereum-cryptography/keccak' +import { hexToBytes } from 'ethereum-cryptography/utils.js' +import Web3 from 'web3' // TODO try to do this without web3 direct +import Accounts from 'web3-eth-accounts' const debug = debugFactory('wallet-base:tx:sign') // Original code taken from @@ -19,6 +43,8 @@ export const publicKeyPrefix: number = 0x04 export const sixtyFour: number = 64 export const thirtyTwo: number = 32 +const Y_PARITY_EIP_2098 = 27 + function isNullOrUndefined(value: any): boolean { return value === null || value === undefined } @@ -47,122 +73,319 @@ function makeEven(hex: string) { return hex } -function signatureFormatter(signature: { v: number; r: Buffer; s: Buffer }): { +function signatureFormatter( + signature: { v: number; r: Buffer; s: Buffer }, + type: TransactionTypes +): { v: string r: string s: string } { + let v = signature.v + if (type !== 'celo-legacy') { + v = signature.v === Y_PARITY_EIP_2098 ? 0 : 1 + } return { - v: stringNumberToHex(signature.v), + v: stringNumberToHex(v), r: makeEven(trimLeadingZero(ensureLeading0x(signature.r.toString('hex')))), s: makeEven(trimLeadingZero(ensureLeading0x(signature.s.toString('hex')))), } } -function stringNumberToHex(num?: number | string): string { +export function stringNumberOrBNToHex( + num?: number | string | ReturnType +): Hex { + if (typeof num === 'string' || typeof num === 'number' || num === undefined) { + return stringNumberToHex(num) + } else { + return makeEven(`0x` + num.toString(16)) as Hex + } +} +function stringNumberToHex(num?: number | string): Hex { const auxNumber = Number(num) if (num === '0x' || num === undefined || auxNumber === 0) { return '0x' } - return Bytes.fromNumber(auxNumber) + return makeEven(Web3.utils.numberToHex(num)) as Hex } - export function rlpEncodedTx(tx: CeloTx): RLPEncodedTx { + assertSerializableTX(tx) + const transaction = inputCeloTxFormatter(tx) + transaction.to = Bytes.fromNat(tx.to || '0x').toLowerCase() + transaction.nonce = Number(((tx.nonce as any) !== '0x' ? tx.nonce : 0) || 0) + transaction.data = Bytes.fromNat(tx.data || '0x').toLowerCase() + transaction.value = stringNumberOrBNToHex(tx.value) + transaction.gas = stringNumberOrBNToHex(tx.gas) + transaction.chainId = tx.chainId || 1 + // Celo Specific + transaction.feeCurrency = Bytes.fromNat(tx.feeCurrency || '0x').toLowerCase() + transaction.gatewayFeeRecipient = Bytes.fromNat(tx.gatewayFeeRecipient || '0x').toLowerCase() + transaction.gatewayFee = stringNumberOrBNToHex(tx.gatewayFee) + + // Legacy + transaction.gasPrice = stringNumberOrBNToHex(tx.gasPrice) + // EIP1559 / CIP42 + transaction.maxFeePerGas = stringNumberOrBNToHex(tx.maxFeePerGas) + transaction.maxPriorityFeePerGas = stringNumberOrBNToHex(tx.maxPriorityFeePerGas) + + let rlpEncode: Hex + if (isCIP42(tx)) { + // There shall be a typed transaction with the code 0x7c that has the following format: + // 0x7c || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, feecurrency, gatewayFeeRecipient, gatewayfee, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s]). + // This will be in addition to the type 0x02 transaction as specified in EIP-1559. + rlpEncode = RLP.encode([ + stringNumberToHex(transaction.chainId), + stringNumberToHex(transaction.nonce), + transaction.maxPriorityFeePerGas || '0x', + transaction.maxFeePerGas || '0x', + transaction.gas || '0x', + transaction.feeCurrency || '0x', + transaction.gatewayFeeRecipient || '0x', + transaction.gatewayFee || '0x', + transaction.to || '0x', + transaction.value || '0x', + transaction.data || '0x', + transaction.accessList || [], + ]) + return { transaction, rlpEncode: concatHex(['0x7c', rlpEncode]), type: 'cip42' } + } else if (isEIP1559(tx)) { + // https://eips.ethereum.org/EIPS/eip-1559 + // 0x02 || rlp([chain_id, nonce, max_priority_fee_per_gas, max_fee_per_gas, gas_limit, destination, amount, data, access_list, signature_y_parity, signature_r, signature_s]). + rlpEncode = RLP.encode([ + stringNumberToHex(transaction.chainId), + stringNumberToHex(transaction.nonce), + transaction.maxPriorityFeePerGas || '0x', + transaction.maxFeePerGas || '0x', + transaction.gas || '0x', + transaction.to || '0x', + transaction.value || '0x', + transaction.data || '0x', + transaction.accessList || [], + ]) + return { transaction, rlpEncode: concatHex(['0x02', rlpEncode]), type: 'eip1559' } + } else { + // This order should match the order in Geth. + // https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/core/types/transaction.go#L65 + rlpEncode = RLP.encode([ + stringNumberToHex(transaction.nonce), + transaction.gasPrice, + transaction.gas, + transaction.feeCurrency, + transaction.gatewayFeeRecipient, + transaction.gatewayFee, + transaction.to, + transaction.value, + transaction.data, + stringNumberToHex(transaction.chainId), + '0x', + '0x', + ]) + return { transaction, rlpEncode, type: 'celo-legacy' } + } +} + +enum TxTypeToPrefix { + 'celo-legacy' = '', + cip42 = '0x7c', + eip1559 = '0x02', +} + +function concatTypePrefixHex( + rawTransaction: string, + txType: EncodedTransaction['tx']['type'] +): Hex { + const prefix = TxTypeToPrefix[txType] + if (prefix) { + return concatHex([prefix, rawTransaction]) + } + return rawTransaction as Hex +} + +function assertSerializableTX(tx: CeloTx) { if (!tx.gas) { throw new Error('"gas" is missing') } + // ensure at least gasPrice or maxFeePerGas and maxPriorityFeePerGas are set if ( - isNullOrUndefined(tx.chainId) || - isNullOrUndefined(tx.gasPrice) || - isNullOrUndefined(tx.nonce) + !isPresent(tx.gasPrice) && + (!isPresent(tx.maxFeePerGas) || !isPresent(tx.maxPriorityFeePerGas)) ) { + throw new Error('"gasPrice" or "maxFeePerGas" and "maxPriorityFeePerGas" are missing') + } + + // ensure that gasPrice and maxFeePerGas are not set at the same time + if ( + isPresent(tx.gasPrice) && + (isPresent(tx.maxFeePerGas) || isPresent(tx.maxPriorityFeePerGas)) + ) { + throw new Error( + 'when "maxFeePerGas" or "maxPriorityFeePerGas" are set, "gasPrice" must not be set' + ) + } + + if (isNullOrUndefined(tx.nonce) || isNullOrUndefined(tx.chainId)) { throw new Error( - 'One of the values "chainId", "gasPrice", or "nonce" couldn\'t be fetched: ' + - JSON.stringify({ chainId: tx.chainId, gasPrice: tx.gasPrice, nonce: tx.nonce }) + 'One of the values "chainId" or "nonce" couldn\'t be fetched: ' + + JSON.stringify({ chainId: tx.chainId, nonce: tx.nonce }) ) } - if (tx.nonce! < 0 || tx.gas! < 0 || tx.gasPrice! < 0 || tx.chainId! < 0) { - throw new Error('Gas, gasPrice, nonce or chainId is lower than 0') + if (isLessThanZero(tx.nonce) || isLessThanZero(tx.gas) || isLessThanZero(tx.chainId)) { + throw new Error('Gas, nonce or chainId is less than than 0') } - const transaction: CeloTx = inputCeloTxFormatter(tx) - transaction.to = Bytes.fromNat(tx.to || '0x').toLowerCase() - transaction.nonce = Number(((tx.nonce as any) !== '0x' ? tx.nonce : 0) || 0) - transaction.data = Bytes.fromNat(tx.data || '0x').toLowerCase() - transaction.value = stringNumberToHex(tx.value?.toString()) - transaction.feeCurrency = Bytes.fromNat(tx.feeCurrency || '0x').toLowerCase() - transaction.gatewayFeeRecipient = Bytes.fromNat(tx.gatewayFeeRecipient || '0x').toLowerCase() - transaction.gatewayFee = stringNumberToHex(tx.gatewayFee) - transaction.gasPrice = stringNumberToHex(tx.gasPrice?.toString()) - transaction.gas = stringNumberToHex(tx.gas) - transaction.chainId = tx.chainId || 1 + isPriceToLow(tx) +} - // This order should match the order in Geth. - // https://github.com/celo-org/celo-blockchain/blob/027dba2e4584936cc5a8e8993e4e27d28d5247b8/core/types/transaction.go#L65 - const rlpEncode = RLP.encode([ - stringNumberToHex(transaction.nonce), - transaction.gasPrice, - transaction.gas, - transaction.feeCurrency, - transaction.gatewayFeeRecipient, - transaction.gatewayFee, - transaction.to, - transaction.value, - transaction.data, - stringNumberToHex(transaction.chainId), - '0x', - '0x', - ]) +export function isPriceToLow(tx: CeloTx) { + const prices = [tx.gasPrice, tx.maxFeePerGas, tx.maxPriorityFeePerGas].filter( + (price) => price !== undefined + ) + const isLow = false + for (const price of prices) { + if (isLessThanZero(price)) { + throw new Error('GasPrice or maxFeePerGas or maxPriorityFeePerGas is less than than 0') + } + } + + return isLow +} + +function isEIP1559(tx: CeloTx): boolean { + return isPresent(tx.maxFeePerGas) && isPresent(tx.maxPriorityFeePerGas) +} + +function isCIP42(tx: CeloTx): boolean { + return ( + isEIP1559(tx) && + (isPresent(tx.feeCurrency) || isPresent(tx.gatewayFeeRecipient) || isPresent(tx.gatewayFee)) + ) +} - return { transaction, rlpEncode } +function concatHex(values: string[]): Hex { + return `0x${values.reduce((acc, x) => acc + x.replace('0x', ''), '')}` +} + +function isLessThanZero(value: CeloTx['gasPrice']) { + if (isNullOrUndefined(value)) { + return true + } + switch (typeof value) { + case 'string': + case 'number': + return Number(value) < 0 + default: + return value?.lt(Web3.utils.toBN(0)) || false + } } export async function encodeTransaction( rlpEncoded: RLPEncodedTx, signature: { v: number; r: Buffer; s: Buffer } ): Promise { - const sanitizedSignature = signatureFormatter(signature) + const sanitizedSignature = signatureFormatter(signature, rlpEncoded.type) const v = sanitizedSignature.v const r = sanitizedSignature.r const s = sanitizedSignature.s - const rawTx = RLP.decode(rlpEncoded.rlpEncode).slice(0, 9).concat([v, r, s]) + const decodedTX = prefixAwareRLPDecode(rlpEncoded.rlpEncode, rlpEncoded.type) + // for legacy tx we need to slice but for new ones we do not want to do that + const rawTx = (rlpEncoded.type === 'celo-legacy' ? decodedTX.slice(0, 9) : decodedTX).concat([ + v, + r, + s, + ]) - const rawTransaction = RLP.encode(rawTx) + // After signing, the transaction is encoded again and type prefix added + const rawTransaction = concatTypePrefixHex(RLP.encode(rawTx), rlpEncoded.type) const hash = getHashFromEncoded(rawTransaction) - const result: EncodedTransaction = { - tx: { - nonce: rlpEncoded.transaction.nonce!.toString(), - gasPrice: rlpEncoded.transaction.gasPrice!.toString(), - gas: rlpEncoded.transaction.gas!.toString(), - to: rlpEncoded.transaction.to!.toString(), - value: rlpEncoded.transaction.value!.toString(), - input: rlpEncoded.transaction.data!, + const baseTX = { + nonce: rlpEncoded.transaction.nonce!.toString(), + gas: rlpEncoded.transaction.gas!.toString(), + to: rlpEncoded.transaction.to!.toString(), + value: rlpEncoded.transaction.value!.toString(), + input: rlpEncoded.transaction.data!, + v, + r, + s, + hash, + } + let tx: Partial = baseTX + if (rlpEncoded.type === 'eip1559' || rlpEncoded.type === 'cip42') { + tx = { + ...tx, + // @ts-expect-error -- just a matter of how this tx is built + maxFeePerGas: rlpEncoded.transaction.maxFeePerGas!.toString(), + maxPriorityFeePerGas: rlpEncoded.transaction.maxPriorityFeePerGas!.toString(), + accessList: parseAccessList(rlpEncoded.transaction.accessList || []), + } + } + if (rlpEncoded.type === 'cip42' || rlpEncoded.type === 'celo-legacy') { + tx = { + ...tx, + // @ts-expect-error -- just a matter of how this tx is built feeCurrency: rlpEncoded.transaction.feeCurrency!.toString(), gatewayFeeRecipient: rlpEncoded.transaction.gatewayFeeRecipient!.toString(), gatewayFee: rlpEncoded.transaction.gatewayFee!.toString(), - v, - r, - s, - hash, - }, + } + } + if (rlpEncoded.type === 'celo-legacy') { + tx = { + ...tx, + // @ts-expect-error -- just a matter of how this tx is built + gasPrice: rlpEncoded.transaction.gasPrice!.toString(), + } + } + + const result: EncodedTransaction & { type: TransactionTypes } = { + tx: tx as EncodedTransaction['tx'], raw: rawTransaction, + type: rlpEncoded.type, } return result } +// new types have prefix but legacy does not +function prefixAwareRLPDecode(rlpEncode: string, type: TransactionTypes): string[] { + return type === 'celo-legacy' ? RLP.decode(rlpEncode) : RLP.decode(`0x${rlpEncode.slice(4)}`) +} + +function correctLengthWithSignatureOf(type: TransactionTypes) { + switch (type) { + case 'cip42': + return 15 + case 'celo-legacy': + case 'eip1559': + return 12 + } +} +// Based on the return type of ensureLeading0x this was not a Buffer +export function extractSignature(rawTx: string) { + const type = determineTXType(rawTx) + const rawValues = prefixAwareRLPDecode(rawTx, type) + const length = rawValues.length + if (correctLengthWithSignatureOf(type) !== length) { + throw new Error( + `@extractSignature: provided transaction has ${length} elements but ${type} txs with a signature have ${correctLengthWithSignatureOf( + type + )} ${JSON.stringify(rawValues)}` + ) + } + return extractSignatureFromDecoded(rawValues) +} -export function extractSignature(rawTx: string): { v: number; r: Buffer; s: Buffer } { - const rawValues = RLP.decode(rawTx) - let r = rawValues[10] - let s = rawValues[11] +function extractSignatureFromDecoded(rawValues: string[]) { + // signature is always (for the tx we support so far) the last three elements of the array in order v, r, s, + const v = rawValues.at(-3) + let r = rawValues.at(-2) + let s = rawValues.at(-1) + // https://github.com/wagmi-dev/viem/blob/993321689b3e2220976504e7e170fe47731297ce/src/utils/transaction/parseTransaction.ts#L281 // Account.recover cannot handle canonicalized signatures // A canonicalized signature may have the first byte removed if its value is 0 - r = ensureLeading0x(trimLeading0x(r).padStart(64, '0')) - s = ensureLeading0x(trimLeading0x(s).padStart(64, '0')) + r = ensureLeading0x(trimLeading0x(r as string).padStart(64, '0')) + s = ensureLeading0x(trimLeading0x(s as string).padStart(64, '0')) return { - v: rawValues[9], + v, r, s, } @@ -171,44 +394,185 @@ export function extractSignature(rawTx: string): { v: number; r: Buffer; s: Buff // Recover transaction and sender address from a raw transaction. // This is used for testing. export function recoverTransaction(rawTx: string): [CeloTx, string] { - const rawValues = RLP.decode(rawTx) - debug('signing-utils@recoverTransaction: values are %s', rawValues) - const recovery = Bytes.toNumber(rawValues[9]) - // tslint:disable-next-line:no-bitwise - const chainId = Bytes.fromNumber((recovery - 35) >> 1) - const celoTx: CeloTx = { - nonce: rawValues[0].toLowerCase() === '0x' ? 0 : parseInt(rawValues[0], 16), - gasPrice: rawValues[1].toLowerCase() === '0x' ? 0 : parseInt(rawValues[1], 16), - gas: rawValues[2].toLowerCase() === '0x' ? 0 : parseInt(rawValues[2], 16), - feeCurrency: rawValues[3], - gatewayFeeRecipient: rawValues[4], - gatewayFee: rawValues[5], - to: rawValues[6], - value: rawValues[7], - data: rawValues[8], + if (!rawTx.startsWith('0x')) { + throw new Error('rawTx must start with 0x') + } + + switch (determineTXType(rawTx)) { + case 'cip42': + return recoverTransactionCIP42(rawTx as Hex) + case 'eip1559': + return recoverTransactionEIP1559(rawTx as Hex) + default: + const rawValues = RLP.decode(rawTx) + debug('signing-utils@recoverTransaction: values are %s', rawValues) + const recovery = Bytes.toNumber(rawValues[9]) + // tslint:disable-next-line:no-bitwise + const chainId = Bytes.fromNumber((recovery - 35) >> 1) + const celoTx: CeloTx = { + type: 'celo-legacy', + nonce: rawValues[0].toLowerCase() === '0x' ? 0 : parseInt(rawValues[0], 16), + gasPrice: rawValues[1].toLowerCase() === '0x' ? 0 : parseInt(rawValues[1], 16), + gas: rawValues[2].toLowerCase() === '0x' ? 0 : parseInt(rawValues[2], 16), + feeCurrency: rawValues[3], + gatewayFeeRecipient: rawValues[4], + gatewayFee: rawValues[5], + to: rawValues[6], + value: rawValues[7], + data: rawValues[8], + chainId, + } + const { r, v, s } = extractSignatureFromDecoded(rawValues) + const signature = Account.encodeSignature([v, r, s]) + const extraData = recovery < 35 ? [] : [chainId, '0x', '0x'] + const signingData = rawValues.slice(0, 9).concat(extraData) + const signingDataHex = RLP.encode(signingData) + const signer = Account.recover(getHashFromEncoded(signingDataHex), signature) + return [celoTx, signer] + } +} + +// inspired by @ethereumjs/tx +function getPublicKeyofSignerFromTx(transactionArray: string[]) { + const base = transactionArray.slice(0, 12) // 12 is length of cip42 without vrs fields + const message = concatHex([TxTypeToPrefix.cip42, RLP.encode(base).slice(2)]) + const msgHash = keccak256(hexToBytes(message)) + + const { v, r, s } = extractSignatureFromDecoded(transactionArray) + try { + return ecrecover( + toBuffer(msgHash), + v === '0x' || v === undefined ? BigInt(0) : BigInt(1), + toBuffer(r), + toBuffer(s) + ) + } catch (e: any) { + throw new Error(e) + } +} + +export function getSignerFromTxCIP42(serializedTransaction: string): string { + const transactionArray: any[] = RLP.decode(`0x${serializedTransaction.slice(4)}`) + const signer = getPublicKeyofSignerFromTx(transactionArray) + return toChecksumAddress(Address.fromPublicKey(signer).toString()) +} + +function determineTXType(serializedTransaction: string): TransactionTypes { + const prefix = serializedTransaction.slice(0, 4) + + if (prefix === '0x02') { + return 'eip1559' + } else if (prefix === '0x7c') { + return 'cip42' + } + return 'celo-legacy' +} + +function vrsForRecovery(vRaw: string, r: string, s: string) { + const v = vRaw === '0x' || hexToNumber(vRaw) === 0 ? Y_PARITY_EIP_2098 : Y_PARITY_EIP_2098 + 1 + return { + v, + r, + s, + yParity: v === Y_PARITY_EIP_2098 ? 0 : 1, + } as const +} + +function recoverTransactionCIP42(serializedTransaction: Hex): [CeloTxWithSig, string] { + const transactionArray: any[] = prefixAwareRLPDecode(serializedTransaction, 'cip42') + debug('signing-utils@recoverTransactionCIP42: values are %s', transactionArray) + if (transactionArray.length !== 15 && transactionArray.length !== 12) { + throw new Error( + `Invalid transaction length for type CIP42: ${transactionArray.length} instead of 15 or 12. array: ${transactionArray}` + ) + } + const [ chainId, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gas, + feeCurrency, + gatewayFeeRecipient, + gatewayFee, + to, + value, + data, + accessList, + vRaw, + r, + s, + ] = transactionArray + + const celoTX: CeloTxWithSig = { + type: 'cip42', + nonce: nonce.toLowerCase() === '0x' ? 0 : parseInt(nonce, 16), + maxPriorityFeePerGas: + maxPriorityFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxPriorityFeePerGas, 16), + maxFeePerGas: maxFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxFeePerGas, 16), + gas: gas.toLowerCase() === '0x' ? 0 : parseInt(gas, 16), + feeCurrency, + gatewayFeeRecipient, + gatewayFee, + to, + value: value.toLowerCase() === '0x' ? 0 : parseInt(value, 16), + data, + chainId: chainId.toLowerCase() === '0x' ? 0 : parseInt(chainId, 16), + accessList: parseAccessList(accessList), + ...vrsForRecovery(vRaw, r, s), } - let r = rawValues[10] - let s = rawValues[11] - // Account.recover cannot handle canonicalized signatures - // A canonicalized signature may have the first byte removed if its value is 0 - r = ensureLeading0x(trimLeading0x(r).padStart(64, '0')) - s = ensureLeading0x(trimLeading0x(s).padStart(64, '0')) - const signature = Account.encodeSignature([rawValues[9], r, s]) - const extraData = recovery < 35 ? [] : [chainId, '0x', '0x'] - const signingData = rawValues.slice(0, 9).concat(extraData) - const signingDataHex = RLP.encode(signingData) - const signer = Account.recover(getHashFromEncoded(signingDataHex), signature) + + const signer = + transactionArray.length === 15 ? getSignerFromTxCIP42(serializedTransaction) : 'unsigned' + return [celoTX, signer] +} + +function recoverTransactionEIP1559(serializedTransaction: Hex): [CeloTxWithSig, string] { + const transactionArray: any[] = prefixAwareRLPDecode(serializedTransaction, 'eip1559') + debug('signing-utils@recoverTransactionEIP1559: values are %s', transactionArray) + + const [ + chainId, + nonce, + maxPriorityFeePerGas, + maxFeePerGas, + gas, + to, + value, + data, + accessList, + vRaw, + r, + s, + ] = transactionArray + + const celoTx: CeloTxWithSig = { + type: 'eip1559', + nonce: nonce.toLowerCase() === '0x' ? 0 : parseInt(nonce, 16), + gas: gas.toLowerCase() === '0x' ? 0 : parseInt(gas, 16), + maxPriorityFeePerGas: + maxPriorityFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxPriorityFeePerGas, 16), + maxFeePerGas: maxFeePerGas.toLowerCase() === '0x' ? 0 : parseInt(maxFeePerGas, 16), + to, + value: value.toLowerCase() === '0x' ? 0 : parseInt(value, 16), + data, + chainId: chainId.toLowerCase() === '0x' ? 0 : parseInt(chainId, 16), + accessList: parseAccessList(accessList), + ...vrsForRecovery(vRaw, r, s), + } + const web3Account = new Accounts() + const signer = web3Account.recoverTransaction(serializedTransaction) + return [celoTx, signer] } export function recoverMessageSigner(signingDataHex: string, signedData: string): string { - const dataBuff = ethUtil.toBuffer(signingDataHex) - const msgHashBuff = ethUtil.hashPersonalMessage(dataBuff) - const signature = ethUtil.fromRpcSig(signedData) + const dataBuff = toBuffer(signingDataHex) + const msgHashBuff = hashPersonalMessage(dataBuff) + const signature = fromRpcSig(signedData) - const publicKey = ethUtil.ecrecover(msgHashBuff, signature.v, signature.r, signature.s) - const address = ethUtil.pubToAddress(publicKey, true) + const publicKey = ecrecover(msgHashBuff, signature.v, signature.r, signature.s) + const address = pubToAddress(publicKey, true) return ensureLeading0x(address.toString('hex')) } @@ -217,7 +581,7 @@ export function verifyEIP712TypedDataSigner( signedData: string, expectedAddress: string ): boolean { - const dataHex = ethUtil.bufferToHex(generateTypedDataHash(typedData)) + const dataHex = bufferToHex(generateTypedDataHash(typedData)) return verifySignatureWithoutPrefix(dataHex, signedData, expectedAddress) } @@ -236,9 +600,10 @@ export function verifySignatureWithoutPrefix( export function decodeSig(sig: any) { const [v, r, s] = Account.decodeSignature(sig) + return { v: parseInt(v, 16), - r: ethUtil.toBuffer(r) as Buffer, - s: ethUtil.toBuffer(s) as Buffer, + r: toBuffer(r) as Buffer, + s: toBuffer(s) as Buffer, } } diff --git a/packages/sdk/wallets/wallet-base/src/wallet-base.ts b/packages/sdk/wallets/wallet-base/src/wallet-base.ts index 9d0cd2575aa..acab8d7fab4 100644 --- a/packages/sdk/wallets/wallet-base/src/wallet-base.ts +++ b/packages/sdk/wallets/wallet-base/src/wallet-base.ts @@ -77,7 +77,8 @@ export abstract class WalletBase implements ReadOnlyWall throw new Error('No transaction object given!') } const rlpEncoded = rlpEncodedTx(txParams) - const addToV = chainIdTransformationForSigning(txParams.chainId!) + const addToV = + rlpEncoded.type === 'celo-legacy' ? chainIdTransformationForSigning(txParams.chainId!) : 27 // Get the signer from the 'from' field const fromAddress = txParams.from!.toString() diff --git a/packages/sdk/wallets/wallet-base/tsconfig.json b/packages/sdk/wallets/wallet-base/tsconfig.json index e6a754088ba..51debe8913d 100644 --- a/packages/sdk/wallets/wallet-base/tsconfig.json +++ b/packages/sdk/wallets/wallet-base/tsconfig.json @@ -5,5 +5,6 @@ "outDir": "lib" }, "include": ["src", "types"], + "exclude": ["**/*.test.ts"], "references": [{ "path": "../../utils" }] } diff --git a/packages/sdk/wallets/wallet-hsm-aws/package.json b/packages/sdk/wallets/wallet-hsm-aws/package.json index c9143b7b6ac..3b463ce62fa 100644 --- a/packages/sdk/wallets/wallet-hsm-aws/package.json +++ b/packages/sdk/wallets/wallet-hsm-aws/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-hsm-aws", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "AWS HSM wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -22,10 +22,10 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/utils": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", - "@celo/wallet-remote": "4.1.2-dev", - "@celo/wallet-hsm": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", + "@celo/wallet-remote": "5.0.3-dev", + "@celo/wallet-hsm": "5.0.3-dev", "@types/debug": "^4.1.5", "@types/secp256k1": "^4.0.0", "aws-sdk": "^2.705.0", @@ -36,7 +36,7 @@ "secp256k1": "^4.0.0" }, "devDependencies": { - "@celo/connect": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", "elliptic": "^6.5.4", "web3": "1.10.0" }, diff --git a/packages/sdk/wallets/wallet-hsm-azure/package.json b/packages/sdk/wallets/wallet-hsm-azure/package.json index ba4957bb0b2..814fecf9a95 100644 --- a/packages/sdk/wallets/wallet-hsm-azure/package.json +++ b/packages/sdk/wallets/wallet-hsm-azure/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-hsm-azure", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Azure HSM wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -25,11 +25,11 @@ "@azure/identity": "^1.1.0", "@azure/keyvault-keys": "^4.1.0", "@azure/keyvault-secrets": "^4.1.0", - "@celo/utils": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", - "@celo/wallet-remote": "4.1.2-dev", - "@celo/wallet-hsm": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", + "@celo/wallet-remote": "5.0.3-dev", + "@celo/wallet-hsm": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", "@types/secp256k1": "^4.0.0", "eth-lib": "^0.2.8", "@ethereumjs/util": "8.0.5", diff --git a/packages/sdk/wallets/wallet-hsm-gcp/package.json b/packages/sdk/wallets/wallet-hsm-gcp/package.json index 7d8625b5db9..a13a35aedd1 100644 --- a/packages/sdk/wallets/wallet-hsm-gcp/package.json +++ b/packages/sdk/wallets/wallet-hsm-gcp/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-hsm-gcp", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "GCP HSM wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/utils": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", - "@celo/wallet-remote": "4.1.2-dev", - "@celo/wallet-hsm": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", + "@celo/wallet-remote": "5.0.3-dev", + "@celo/wallet-hsm": "5.0.3-dev", "@google-cloud/kms": "~2.9.0", "@types/debug": "^4.1.5", "@types/secp256k1": "^4.0.0", @@ -34,7 +34,7 @@ "secp256k1": "^4.0.0" }, "devDependencies": { - "@celo/connect": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", "elliptic": "^6.5.4", "web3": "1.10.0" }, diff --git a/packages/sdk/wallets/wallet-hsm/package.json b/packages/sdk/wallets/wallet-hsm/package.json index f3b57ff51df..5db5375efb9 100644 --- a/packages/sdk/wallets/wallet-hsm/package.json +++ b/packages/sdk/wallets/wallet-hsm/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-hsm", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "HSM wallet implementation utils", "author": "Celo", "license": "Apache-2.0", @@ -22,7 +22,7 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/base": "4.1.2-dev", + "@celo/base": "5.0.3-dev", "@types/asn1js": "^0.0.2", "@types/secp256k1": "^4.0.0", "@types/debug": "^4.1.5", diff --git a/packages/sdk/wallets/wallet-ledger/package.json b/packages/sdk/wallets/wallet-ledger/package.json index 09ef11ed9c8..22510606810 100644 --- a/packages/sdk/wallets/wallet-ledger/package.json +++ b/packages/sdk/wallets/wallet-ledger/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-ledger", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Ledger wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -22,10 +22,10 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/utils": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", - "@celo/wallet-remote": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", + "@celo/wallet-remote": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@ledgerhq/hw-app-eth": "~5.11.0", "@ledgerhq/hw-transport": "~5.11.0", diff --git a/packages/sdk/wallets/wallet-local/package.json b/packages/sdk/wallets/wallet-local/package.json index 2f9394c9e63..eec63cd4eec 100644 --- a/packages/sdk/wallets/wallet-local/package.json +++ b/packages/sdk/wallets/wallet-local/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-local", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Local wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -17,19 +17,20 @@ "build": "tsc -b .", "clean": "tsc -b . --clean", "docs": "typedoc", - "test": "jest --runInBand", + "test": "jest", "lint": "tslint -c tslint.json --project .", "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/utils": "4.1.2-dev", - "@celo/connect": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", + "@celo/utils": "5.0.3-dev", + "@celo/connect": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", "eth-lib": "^0.2.8", "@ethereumjs/util": "8.0.5" }, "devDependencies": { - "web3": "1.10.0" + "web3": "1.10.0", + "viem": "~1.5.4" }, "engines": { "node": ">=8.14.2" diff --git a/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts b/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts index 4f34b5e927a..7f447b18fd8 100644 --- a/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts +++ b/packages/sdk/wallets/wallet-local/src/local-wallet.test.ts @@ -1,4 +1,4 @@ -import { CeloTx, EncodedTransaction } from '@celo/connect' +import { CeloTx, EncodedTransaction, Hex } from '@celo/connect' import { normalizeAddressWith0x, privateKeyToAddress, @@ -8,8 +8,11 @@ import { import { Encrypt } from '@celo/utils/lib/ecies' import { verifySignature } from '@celo/utils/lib/signatureUtils' import { recoverTransaction, verifyEIP712TypedDataSigner } from '@celo/wallet-base' +import { TransactionSerializableEIP1559, parseTransaction } from 'viem' +import { privateKeyToAccount } from 'viem/accounts' import Web3 from 'web3' import { LocalWallet } from './local-wallet' +import { StrongAddress } from '@celo/base/lib/address' const CHAIN_ID = 44378 @@ -126,25 +129,37 @@ describe('Local wallet class', () => { }) test('fails calling signTransaction', async () => { - await expect(wallet.signTransaction(celoTransaction)).rejects.toThrowError() + await expect( + wallet.signTransaction(celoTransaction) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not find address 0x588e4b68193001e4d10928660ab4165b813717c0"` + ) }) test('fails calling signPersonalMessage', async () => { const hexStr: string = '0xa1' - await expect(wallet.signPersonalMessage(unknownAddress, hexStr)).rejects.toThrowError() + await expect( + wallet.signPersonalMessage(unknownAddress, hexStr) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not find address 0x588e4b68193001e4d10928660ab4165b813717c0"` + ) }) test('fails calling signTypedData', async () => { - await expect(wallet.signTypedData(unknownAddress, TYPED_DATA)).rejects.toThrowError() + await expect( + wallet.signTypedData(unknownAddress, TYPED_DATA) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Could not find address 0x588e4b68193001e4d10928660ab4165b813717c0"` + ) }) }) describe('using a known address', () => { describe('when calling signTransaction', () => { - let celoTransaction: CeloTx + let celoTransactionWithGasPrice: CeloTx beforeEach(() => { - celoTransaction = { + celoTransactionWithGasPrice = { from: knownAddress, to: otherAddress, chainId: CHAIN_ID, @@ -155,16 +170,146 @@ describe('Local wallet class', () => { feeCurrency: '0x', gatewayFeeRecipient: FEE_ADDRESS, gatewayFee: '0x5678', - data: '0xabcdef', + data: '0xabcdef' as const, } }) - test('succeeds', async () => { - await expect(wallet.signTransaction(celoTransaction)).resolves.not.toBeUndefined() + test('succeeds with legacy', async () => { + await expect(wallet.signTransaction(celoTransactionWithGasPrice)).resolves + .toMatchInlineSnapshot(` + { + "raw": "0xf88480630a80941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdef83015ad8a09e121a99dc0832a9f4d1d71500b3c8a69a3c064d437c225d6292577ffcc45a71a02c5efa3c4b58953c35968e42d11d3882dacacf45402ee802824268b7cd60daff", + "tx": { + "feeCurrency": "0x", + "gas": "0x0a", + "gasPrice": "0x63", + "gatewayFee": "0x5678", + "gatewayFeeRecipient": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "hash": "0xd24898ee3f68caa01fe065784453db7360bf783060fcbd18033f9d254ab8b082", + "input": "0xabcdef", + "nonce": "0", + "r": "0x9e121a99dc0832a9f4d1d71500b3c8a69a3c064d437c225d6292577ffcc45a71", + "s": "0x2c5efa3c4b58953c35968e42d11d3882dacacf45402ee802824268b7cd60daff", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "v": "0x015ad8", + "value": "0x0de0b6b3a7640000", + }, + "type": "celo-legacy", + } + `) + }) + + test('succeeds with eip1559', async () => { + const transaction1559 = { + ...celoTransactionWithGasPrice, + gasPrice: undefined, + feeCurrency: undefined, + maxFeePerGas: '99', + maxPriorityFeePerGas: '99', + } + await expect(wallet.signTransaction(transaction1559)).resolves.toMatchInlineSnapshot(` + { + "raw": "0x7cf88682ad5a8063630a80941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc001a0cfa1e1b30d1e4617ce80922d853c5e8b54b21f5ed6604438f90280ef2f0b7fd0a06fd8eee02fbdd421136fb45e6851ce72b5d87a2c06b2e136ef1a062df9256f4e", + "tx": { + "accessList": [], + "feeCurrency": "0x", + "gas": "0x0a", + "gatewayFee": "0x5678", + "gatewayFeeRecipient": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "hash": "0x29327536ba9901fde64b1b86882fd173517b41cd8bc8245e3761847d9b231c6d", + "input": "0xabcdef", + "maxFeePerGas": "0x63", + "maxPriorityFeePerGas": "0x63", + "nonce": "0", + "r": "0xcfa1e1b30d1e4617ce80922d853c5e8b54b21f5ed6604438f90280ef2f0b7fd0", + "s": "0x6fd8eee02fbdd421136fb45e6851ce72b5d87a2c06b2e136ef1a062df9256f4e", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "v": "0x01", + "value": "0x0de0b6b3a7640000", + }, + "type": "cip42", + } + `) + }) + + test('matches behavior of viem 1559', async () => { + const account = privateKeyToAccount(PRIVATE_KEY2) + const wallet2 = new LocalWallet() + // wallet 1 uses a private key that does not start with 0x which doesnt work for viem + wallet2.addAccount(PRIVATE_KEY2) + + const transaction1559 = { + ...celoTransactionWithGasPrice, + from: ACCOUNT_ADDRESS2, + to: otherAddress, + gasPrice: undefined, + feeCurrency: undefined, + gatewayFeeRecipient: undefined, + gatewayFee: undefined, + maxFeePerGas: '99', + maxPriorityFeePerGas: '99', + data: celoTransactionWithGasPrice.data as Hex, + } + const transaction1559Viem: TransactionSerializableEIP1559 = { + ...transaction1559, + type: 'eip1559', + gas: BigInt(transaction1559.gas as string), + to: transaction1559.to as StrongAddress, + value: BigInt(transaction1559.value as string), + maxFeePerGas: BigInt(transaction1559.maxFeePerGas as string), + maxPriorityFeePerGas: BigInt(transaction1559.maxPriorityFeePerGas as string), + accessList: undefined, + chainId: celoTransactionWithGasPrice.chainId as number, + } + const signedTransaction = await wallet2.signTransaction(transaction1559) + const viemSignedTransaction = await account.signTransaction(transaction1559Viem) + + expect(parseTransaction(signedTransaction.raw)).toEqual( + parseTransaction(viemSignedTransaction) + ) + expect(recoverTransaction(signedTransaction.raw)).toEqual( + recoverTransaction(viemSignedTransaction) + ) + expect(signedTransaction.raw).toEqual(viemSignedTransaction) + }) + + test('succeeds with cip42', async () => { + const transaction42 = { + ...celoTransactionWithGasPrice, + gasPrice: undefined, + maxFeePerGas: '99', + maxPriorityFeePerGas: '99', + feeCurrency: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + } + await expect(wallet.signTransaction(transaction42)).resolves.toMatchInlineSnapshot(` + { + "raw": "0x7cf89a82ad5a8063630a94cd2a3d9f938e13cd947ec05abc7fe734df8dd826941be31a94361a391bbafb2a4ccd704f57dc04d4bb82567894588e4b68193001e4d10928660ab4165b813717c0880de0b6b3a764000083abcdefc080a0c610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1a01799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112", + "tx": { + "accessList": [], + "feeCurrency": "0xcd2a3d9f938e13cd947ec05abc7fe734df8dd826", + "gas": "0x0a", + "gatewayFee": "0x5678", + "gatewayFeeRecipient": "0x1be31a94361a391bbafb2a4ccd704f57dc04d4bb", + "hash": "0x7afcef8db391ff574b7f9c9205399b8ab094fc9fc8afbfb881204cbaaf093365", + "input": "0xabcdef", + "maxFeePerGas": "0x63", + "maxPriorityFeePerGas": "0x63", + "nonce": "0", + "r": "0xc610507b2ac3cff80dd7017419021196807d605efce0970c18cde48db33c27d1", + "s": "0x1799477e0f601f554f0ee6f7ac21490602124801e9f7a99d9605249b90f03112", + "to": "0x588e4b68193001e4d10928660ab4165b813717c0", + "v": "0x", + "value": "0x0de0b6b3a7640000", + }, + "type": "cip42", + } + `) }) test('with same signer', async () => { - const signedTx: EncodedTransaction = await wallet.signTransaction(celoTransaction) + const signedTx: EncodedTransaction = await wallet.signTransaction( + celoTransactionWithGasPrice + ) const [, recoveredSigner] = recoverTransaction(signedTx.raw) expect(normalizeAddressWith0x(recoveredSigner)).toBe( normalizeAddressWith0x(knownAddress) @@ -198,6 +343,94 @@ describe('Local wallet class', () => { ) }) }) + describe('when using signTransaction with type CIP42', () => { + let celoTransactionBase: CeloTx + let feeCurrency = '0x10c892a6ec43a53e45d0b916b4b7d383b1b78c0f' + let maxFeePerGas = '0x100000000' + let maxPriorityFeePerGas = '0x100000000' + + beforeEach(() => { + celoTransactionBase = { + gas: '1000000000', + from: knownAddress, + to: otherAddress, + chainId: CHAIN_ID, + value: Web3.utils.toWei('1', 'ether'), + nonce: 0, + data: '0xabcdef', + } + }) + + describe('when feeCurrency and maxPriorityFeePerGas and maxFeePerGas are set', () => { + it('signs as a CIP42 tx', async () => { + const transaction: CeloTx = { + ...celoTransactionBase, + feeCurrency, + maxFeePerGas, + maxPriorityFeePerGas, + } + const signedTx: EncodedTransaction = await wallet.signTransaction(transaction) + expect(signedTx.raw).toMatch(/^0x7c/) + }) + }) + describe('when feeCurrency and maxFeePerGas but not maxPriorityFeePerGas are set', () => { + it('throws error', async () => { + const transaction: CeloTx = { + ...celoTransactionBase, + feeCurrency, + maxFeePerGas, + maxPriorityFeePerGas: undefined, + } + expect(() => + wallet.signTransaction(transaction) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `""gasPrice" or "maxFeePerGas" and "maxPriorityFeePerGas" are missing"` + ) + }) + }) + + describe('when feeCurrency and maxPriorityFeePerGas but not maxFeePerGas are set', () => { + it('throws error', async () => { + const transaction: CeloTx = { + ...celoTransactionBase, + feeCurrency, + maxFeePerGas: undefined, + maxPriorityFeePerGas, + } + expect(() => + wallet.signTransaction(transaction) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `""gasPrice" or "maxFeePerGas" and "maxPriorityFeePerGas" are missing"` + ) + }) + }) + + describe('when gas and one of maxPriorityFeePerGas or maxFeePerGas are set', () => { + it('throws explaining only one kind of gas fee can be set', async () => { + const transaction: CeloTx = { + ...celoTransactionBase, + maxFeePerGas, + maxPriorityFeePerGas, + gasPrice: '0x100000000', + } + expect(async () => await wallet.signTransaction(transaction)).rejects.toThrowError( + 'when "maxFeePerGas" or "maxPriorityFeePerGas" are set, "gasPrice" must not be set' + ) + }) + }) + + describe('when maxPriorityFeePerGas / maxFeePerGas are set but not feeCurrency', () => { + it('signs as a EIP1559 tx', async () => { + const transaction: CeloTx = { + ...celoTransactionBase, + maxFeePerGas, + maxPriorityFeePerGas, + } + const signedTx: EncodedTransaction = await wallet.signTransaction(transaction) + expect(signedTx.raw).toMatch(/^0x02/) + }) + }) + }) describe('when calling signPersonalMessage', () => { test('succeeds', async () => { diff --git a/packages/sdk/wallets/wallet-local/src/signing.test.ts b/packages/sdk/wallets/wallet-local/src/signing.test.ts index ac867abb21e..0fa9af00ffa 100644 --- a/packages/sdk/wallets/wallet-local/src/signing.test.ts +++ b/packages/sdk/wallets/wallet-local/src/signing.test.ts @@ -25,123 +25,11 @@ debug(`Account Address 1: ${ACCOUNT_ADDRESS1}`) debug(`Private key 2: ${PRIVATE_KEY2}`) debug(`Account Address 2: ${ACCOUNT_ADDRESS2}`) -async function verifyLocalSigning(web3: Web3, celoTransaction: CeloTx): Promise { - debug('Signer Testing using Account: %s', celoTransaction.from) - const signedTransaction = await web3.eth.signTransaction(celoTransaction) - debug('Singer Testing: Signed transaction %o', signedTransaction) - const rawTransaction: string = signedTransaction.raw - const [signedCeloTransaction, recoveredSigner] = recoverTransaction(rawTransaction) - debug( - 'Transaction was signed by "%s", recovered signer is "%s"', - celoTransaction.from, - recoveredSigner - ) - expect(recoveredSigner.toLowerCase()).toEqual(celoTransaction.from!.toString().toLowerCase()) - - if (celoTransaction.nonce != null) { - debug( - 'Checking nonce actual: %o expected: %o', - signedCeloTransaction.nonce, - parseInt(celoTransaction.nonce.toString(), 16) - ) - expect(signedCeloTransaction.nonce).toEqual(parseInt(celoTransaction.nonce.toString(), 16)) - } - if (celoTransaction.gas != null) { - debug( - 'Checking gas actual %o expected %o', - signedCeloTransaction.gas, - parseInt(celoTransaction.gas.toString(), 16) - ) - expect(signedCeloTransaction.gas).toEqual(parseInt(celoTransaction.gas.toString(), 16)) - } - if (celoTransaction.gasPrice != null) { - debug( - 'Checking gas price actual %o expected %o', - signedCeloTransaction.gasPrice, - parseInt(celoTransaction.gasPrice.toString(), 16) - ) - expect(signedCeloTransaction.gasPrice).toEqual( - parseInt(celoTransaction.gasPrice.toString(), 16) - ) - } - if (celoTransaction.feeCurrency != null) { - debug( - 'Checking fee currency actual %o expected %o', - signedCeloTransaction.feeCurrency, - celoTransaction.feeCurrency - ) - expect(signedCeloTransaction.feeCurrency!.toLowerCase()).toEqual( - celoTransaction.feeCurrency.toLowerCase() - ) - } - if (celoTransaction.gatewayFeeRecipient != null) { - debug( - 'Checking gateway fee recipient actual ' + - `${signedCeloTransaction.gatewayFeeRecipient} expected ${celoTransaction.gatewayFeeRecipient}` - ) - expect(signedCeloTransaction.gatewayFeeRecipient!.toLowerCase()).toEqual( - celoTransaction.gatewayFeeRecipient.toLowerCase() - ) - } - if (celoTransaction.gatewayFee != null) { - debug( - 'Checking gateway fee value actual %o expected %o', - signedCeloTransaction.gatewayFee, - celoTransaction.gatewayFee.toString() - ) - expect(signedCeloTransaction.gatewayFee).toEqual(celoTransaction.gatewayFee.toString()) - } - if (celoTransaction.data != null) { - debug(`Checking data actual ${signedCeloTransaction.data} expected ${celoTransaction.data}`) - expect(signedCeloTransaction.data!.toLowerCase()).toEqual(celoTransaction.data.toLowerCase()) - } -} - -async function verifyLocalSigningInAllPermutations( - web3: Web3, - from: string, - to: string -): Promise { - const amountInWei: string = Web3.utils.toWei('1', 'ether') - const nonce = 0 - const badNonce = 100 - const gas = 10 - const gasPrice = 99 - const feeCurrency = ACCOUNT_ADDRESS1 - const gatewayFeeRecipient = ACCOUNT_ADDRESS2 - const gatewayFee = '0x5678' - const data = '0xabcdef' - const chainId = 1 - - // tslint:disable:no-bitwise - // Test all possible combinations for rigor. - for (let i = 0; i < 16; i++) { - const celoTransaction: CeloTx = { - from, - to, - value: amountInWei, - nonce, - gasPrice, - chainId, - gas, - feeCurrency: i & 1 ? feeCurrency : undefined, - gatewayFeeRecipient: i & 2 ? gatewayFeeRecipient : undefined, - gatewayFee: i & 4 ? gatewayFee : undefined, - data: i & 8 ? data : undefined, - } - await verifyLocalSigning(web3, celoTransaction) - } - // tslint:enable:no-bitwise - - // A special case. - // An incorrect nonce will only work, if no implict calls to estimate gas are required. - await verifyLocalSigning(web3, { from, to, nonce: badNonce, gas, gasPrice, chainId }) -} - // These tests verify the signTransaction WITHOUT the ParamsPopulator describe('Transaction Utils', () => { // only needed for the eth_coinbase rcp call let connection: Connection + let web3: Web3 const mockProvider: Provider = { send: (payload: JsonRpcPayload, callback: Callback): void => { if (payload.method === 'eth_coinbase') { @@ -151,44 +39,212 @@ describe('Transaction Utils', () => { result: '0xc94770007dda54cF92009BFF0dE90c06F603a09f', } callback(null, response) + } else if (payload.method === 'eth_gasPrice') { + const response: JsonRpcResponse = { + jsonrpc: payload.jsonrpc, + id: Number(payload.id), + result: '0x09184e72a000', + } + callback(null, response) } else { callback(new Error(payload.method)) } }, } - const web3: Web3 = new Web3() - beforeEach(() => { + const setupConnection = async () => { + web3 = new Web3() web3.setProvider(mockProvider as any) connection = new Connection(web3) connection.wallet = new LocalWallet() - }) + } + async function verifyLocalSigning(celoTransaction: CeloTx): Promise { + let recoveredSigner: string | undefined + let recoveredTransaction: CeloTx | undefined + let signedTransaction: { raw: string; tx: any } | undefined + beforeAll(async () => { + signedTransaction = await web3.eth.signTransaction(celoTransaction) + const recovery = recoverTransaction(signedTransaction.raw) + recoveredTransaction = recovery[0] + recoveredSigner = recovery[1] + }) - afterEach(() => { - connection.stop() - }) + afterAll(async () => { + signedTransaction = undefined + recoveredTransaction = undefined + recoveredSigner = undefined + }) + + test('Signer matches recovered signer', async () => { + expect(recoveredSigner?.toLowerCase()).toEqual(celoTransaction.from!.toString().toLowerCase()) + }) + + test('Checking nonce', async () => { + if (celoTransaction.nonce != null) { + expect(recoveredTransaction?.nonce).toEqual(parseInt(celoTransaction.nonce.toString(), 16)) + } + }) + + test('Checking gas', async () => { + if (celoTransaction.gas != null) { + expect(recoveredTransaction?.gas).toEqual(parseInt(celoTransaction.gas.toString(), 16)) + } + }) + test('Checking gas price', async () => { + if (celoTransaction.gasPrice != null) { + expect(recoveredTransaction?.gasPrice).toEqual( + parseInt(celoTransaction.gasPrice.toString(), 16) + ) + } + }) + test('Checking maxFeePerGas', async () => { + if (celoTransaction.maxFeePerGas != null) { + expect(recoveredTransaction?.maxFeePerGas).toEqual( + parseInt(celoTransaction.maxFeePerGas.toString(), 16) + ) + } + }) + test('Checking maxPriorityFeePerGas', async () => { + if (celoTransaction.maxPriorityFeePerGas != null) { + expect(recoveredTransaction?.maxPriorityFeePerGas).toEqual( + parseInt(celoTransaction.maxPriorityFeePerGas.toString(), 16) + ) + } + }) + test('Checking feeCurrency', async () => { + if (celoTransaction.feeCurrency != null) { + expect(recoveredTransaction?.feeCurrency!.toLowerCase()).toEqual( + celoTransaction.feeCurrency.toLowerCase() + ) + } + }) + test('gatewayFeeRecipient', async () => { + if ( + celoTransaction.gatewayFeeRecipient !== undefined && + celoTransaction.gatewayFeeRecipient !== null + ) { + expect(recoveredTransaction?.gatewayFeeRecipient?.toLowerCase()).toEqual( + celoTransaction.gatewayFeeRecipient.toLowerCase() + ) + } + }) + test('Checking gateway fee value', async () => { + if (celoTransaction.gatewayFee !== undefined && celoTransaction.gatewayFee !== null) { + expect(recoveredTransaction?.gatewayFee).toEqual(celoTransaction.gatewayFee.toString()) + } + }) + test('Checking data', async () => { + if (celoTransaction.data != null) { + expect(recoveredTransaction?.data!.toLowerCase()).toEqual( + celoTransaction.data.toLowerCase() + ) + } + }) + } + + async function verifyLocalSigningInAllPermutations(from: string, to: string): Promise { + const amountInWei: string = Web3.utils.toWei('1', 'ether') + const nonce = 0 + const badNonce = 100 + const gas = 10000 + const gasPrice = 99000000000 + const feeCurrency = ACCOUNT_ADDRESS1 + const gatewayFeeRecipient = ACCOUNT_ADDRESS2 + const gatewayFee = '0x5678' + const data = '0xabcdef' + const chainId = 1 + + // tslint:disable:no-bitwise + // Test all possible combinations for rigor. + for (let i = 0; i < 16; i++) { + const celoTransaction: CeloTx = { + from, + to, + value: amountInWei, + nonce, + gasPrice: i % 2 === 0 ? gasPrice : undefined, + maxFeePerGas: i % 2 === 1 ? gasPrice : undefined, + maxPriorityFeePerGas: i % 2 === 1 ? gasPrice : undefined, + chainId, + gas, + feeCurrency: i % 3 === 0 ? feeCurrency : undefined, + gatewayFeeRecipient: i % 7 === 0 ? gatewayFeeRecipient : undefined, + gatewayFee: i % 7 === 0 ? gatewayFee : undefined, + data: i & 8 ? data : undefined, + } + describe(transactionDescription(celoTransaction), () => { + verifyLocalSigning(celoTransaction) + }) + } + + function transactionDescription(celoTransaction: CeloTx) { + const description: string[] = [] + if (celoTransaction.gasPrice != undefined) { + description.push(`Testing Legacy with gas price ${celoTransaction.gasPrice}`) + } else if ( + celoTransaction.feeCurrency != undefined || + celoTransaction.gatewayFeeRecipient !== undefined || + celoTransaction.gatewayFee !== undefined + ) { + description.push('Testing CIP42 with') + } else { + description.push(`Testing EIP1559 with maxFeePerGas ${celoTransaction.maxFeePerGas}`) + } + if (celoTransaction.data != undefined) { + description.push(`data: ${celoTransaction.data}`) + } + + if (celoTransaction.feeCurrency != undefined) { + description.push(`fee currency: ${celoTransaction.feeCurrency}`) + } + + if (celoTransaction.gatewayFeeRecipient != undefined) { + description.push(`gateway fee recipient: ${celoTransaction.gatewayFeeRecipient}`) + } + if (celoTransaction.gatewayFee != undefined) { + description.push(`gateway fee: ${celoTransaction.gatewayFee}`) + } + + return description.join(' ') + } + + // A special case. + // An incorrect nonce will only work, if no implicit calls to estimate gas are required. + describe('Testing with bad nonce', () => { + verifyLocalSigning({ from, to, nonce: badNonce, gas, gasPrice, chainId }) + }) + } describe('Signer Testing with single local account and pay gas in CELO', () => { - it('Test1 should be able to sign and get the signer back with single local account', async () => { - jest.setTimeout(60 * 1000) - connection.addAccount(PRIVATE_KEY1) - await verifyLocalSigningInAllPermutations(web3, ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) + describe('Test1 should be able to sign and get the signer back with single local account', () => { + beforeAll(async () => { + await setupConnection() + connection.addAccount(PRIVATE_KEY1) + }) + verifyLocalSigningInAllPermutations(ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) + afterAll(() => connection.stop()) }) }) describe('Signer Testing with multiple local accounts', () => { - it('Test2 should be able to sign with first account and get the signer back with multiple local accounts', async () => { - jest.setTimeout(60 * 1000) - connection.addAccount(PRIVATE_KEY1) - connection.addAccount(PRIVATE_KEY2) - await verifyLocalSigningInAllPermutations(web3, ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) - }) - - it('Test3 should be able to sign with second account and get the signer back with multiple local accounts', async () => { - jest.setTimeout(60 * 1000) - connection.addAccount(PRIVATE_KEY1) - connection.addAccount(PRIVATE_KEY2) - await verifyLocalSigningInAllPermutations(web3, ACCOUNT_ADDRESS2, ACCOUNT_ADDRESS1) + describe('Test2 should be able to sign with first account and get the signer back with multiple local accounts', () => { + beforeAll(async () => { + await setupConnection() + connection.addAccount(PRIVATE_KEY1) + connection.addAccount(PRIVATE_KEY2) + }) + verifyLocalSigningInAllPermutations(ACCOUNT_ADDRESS1, ACCOUNT_ADDRESS2) + afterAll(() => connection.stop()) + }) + + describe('Test3 should be able to sign with second account and get the signer back with multiple local accounts', () => { + beforeAll(async () => { + await setupConnection() + connection.addAccount(PRIVATE_KEY1) + connection.addAccount(PRIVATE_KEY2) + }) + verifyLocalSigningInAllPermutations(ACCOUNT_ADDRESS2, ACCOUNT_ADDRESS1) + afterAll(() => connection.stop()) }) }) }) diff --git a/packages/sdk/wallets/wallet-local/tsconfig.json b/packages/sdk/wallets/wallet-local/tsconfig.json index e267672350c..b37d430f5cc 100644 --- a/packages/sdk/wallets/wallet-local/tsconfig.json +++ b/packages/sdk/wallets/wallet-local/tsconfig.json @@ -5,5 +5,6 @@ "outDir": "lib" }, "include": ["src"], + "exclude": ["**/*.test.ts"], "references": [{ "path": "../../utils" }] } diff --git a/packages/sdk/wallets/wallet-remote/package.json b/packages/sdk/wallets/wallet-remote/package.json index c59bb1090db..4bcacb62e1c 100644 --- a/packages/sdk/wallets/wallet-remote/package.json +++ b/packages/sdk/wallets/wallet-remote/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-remote", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Remote wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -22,9 +22,9 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/connect": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", "@ethereumjs/util": "8.0.5", "@types/debug": "^4.1.5", "eth-lib": "^0.2.8" diff --git a/packages/sdk/wallets/wallet-rpc/package.json b/packages/sdk/wallets/wallet-rpc/package.json index 846b1e3d71d..a3b4b3a8650 100644 --- a/packages/sdk/wallets/wallet-rpc/package.json +++ b/packages/sdk/wallets/wallet-rpc/package.json @@ -1,6 +1,6 @@ { "name": "@celo/wallet-rpc", - "version": "4.1.2-dev", + "version": "5.0.3-dev", "description": "Geth RPC wallet implementation", "author": "Celo", "license": "Apache-2.0", @@ -22,16 +22,16 @@ "prepublishOnly": "yarn build" }, "dependencies": { - "@celo/connect": "4.1.2-dev", - "@celo/utils": "4.1.2-dev", - "@celo/wallet-base": "4.1.2-dev", - "@celo/wallet-remote": "4.1.2-dev", + "@celo/connect": "5.0.3-dev", + "@celo/utils": "5.0.3-dev", + "@celo/wallet-base": "5.0.3-dev", + "@celo/wallet-remote": "5.0.3-dev", "bignumber.js": "^9.0.0", "debug": "^4.1.1" }, "devDependencies": { "@celo/dev-utils": "0.0.1-dev", - "@celo/contractkit": "4.1.2-dev" + "@celo/contractkit": "5.0.3-dev" }, "engines": { "node": ">=8.14.2" diff --git a/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts b/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts index 2e6cca39f0f..00fce4de197 100644 --- a/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts +++ b/packages/sdk/wallets/wallet-rpc/src/rpc-signer.ts @@ -82,14 +82,21 @@ export class RpcSigner implements Signer { throw new Error(`RpcSigner cannot sign tx with 'from' ${tx.from}`) } // see geth SendTxArgs type - // https://github.com/celo-org/celo-blockchain/blob/bf2ba25426f9956384220b8b2ce302337e7fa8a4/internal/ethapi/api.go#L1363 + // https://github.com/celo-org/celo-blockchain/blob/fc20d6921478cda68fc88797078f20053bae8866/internal/ethapi/api.go#L1241C6-L1241C20 const rpcTx = { ...tx, nonce: toRpcHex(tx.nonce), value: toRpcHex(tx.value), gas: toRpcHex(tx.gas), - gasPrice: toRpcHex(tx.gasPrice), gatewayFee: toRpcHex(tx.gatewayFee), + ...(tx.gasPrice + ? { + gasPrice: toRpcHex(tx.gasPrice), + } + : { + maxPriorityFeePerGas: toRpcHex(tx.maxPriorityFeePerGas), + maxFeePerGas: toRpcHex(tx.maxFeePerGas), + }), } return this.callAndCheckResponse(RpcSignerEndpoint.SignTransaction, [rpcTx]) } diff --git a/packages/sdk/wallets/wallet-rpc/src/rpc-wallet.test.ts b/packages/sdk/wallets/wallet-rpc/src/rpc-wallet.test.ts index cb23c4942ee..8f25fe84d25 100644 --- a/packages/sdk/wallets/wallet-rpc/src/rpc-wallet.test.ts +++ b/packages/sdk/wallets/wallet-rpc/src/rpc-wallet.test.ts @@ -178,8 +178,26 @@ testWithGanache('rpc-wallet', (web3) => { } }) - test('succeeds', async () => { - await expect(rpcWallet.signTransaction(celoTransaction)).resolves.not.toBeUndefined() + test('succeeds with old school pricing', async () => { + await expect( + rpcWallet.signTransaction(celoTransaction) + ).resolves.toMatchInlineSnapshot( + `"0xf86b8081991094588e4b68193001e4d10928660ab4165b813717c08a0100000000000000000083abcdef25a073bb7eaa60c810af1fad0f68fa15d4714f9990d0202b62797f6134493ec9f6fba046c13e92017228c2c8f0fae74ddd735021817f2f9757cd66debed078daf4070e"` + ) + }) + + test('succeeds with with FeeMarketFields', async () => { + const feeMarketTransaction = { + ...celoTransaction, + gasPrice: undefined, + maxFeePerGas: '1500000000', + maxPriorityFeePerGas: '1500000000', + } + await expect( + rpcWallet.signTransaction(feeMarketTransaction) + ).resolves.toMatchInlineSnapshot( + `"0xf86a80801094588e4b68193001e4d10928660ab4165b813717c08a0100000000000000000083abcdef26a05e9c1e7690d05f3e1433c824fbd948643ff6c618e347ea8c23a6363f3b17cdffa072dc1c22d6147be7b4b7b3cf51eb73b8bedd7940d7b668dcd7ef688a2354a631"` + ) }) // TODO(yorke): enable once fixed: https://github.com/celo-org/celo-monorepo/issues/4077 diff --git a/yarn.lock b/yarn.lock index bc941833aaa..4f97f9380e0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -267,6 +267,11 @@ ethers "~4.0.4" lodash "^4.17.21" +"@adraffy/ens-normalize@1.9.0": + version "1.9.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.0.tgz#223572538f6bea336750039bb43a4016dcc8182d" + integrity sha512-iowxq3U30sghZotgl4s/oJRci6WPBfNO5YYgk2cIOMCHr3LeGPcsZjCEr+33Q4N+oV3OABDAtA+pyvWjbvBifQ== + "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" @@ -933,6 +938,11 @@ resolved "https://registry.yarnpkg.com/@celo/base/-/base-1.5.2.tgz#168ab5e4e30b374079d8d139fafc52ca6bfd4100" integrity sha512-KGf6Dl9E6D01vAfkgkjL2sG+zqAjspAogILIpWstljWdG5ifyA75jihrnDEHaMCoQS0KxHvTdP1XYS/GS6BEyQ== +"@celo/base@5.0.1", "@celo/base@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/base/-/base-5.0.1.tgz#406727217afceec479aa1c0fc8231194595ce84e" + integrity sha512-R0n+nkBv9HPl9IxXkxCGZS20waKWbidA1jyz5a9W5GHxPh6ooTv69KGBIsj1xAdbtlqdvaPbeWJHPxgr5X7nXg== + "@celo/bls12377js@0.1.1": version "0.1.1" resolved "https://registry.yarnpkg.com/@celo/bls12377js/-/bls12377js-0.1.1.tgz#ba3574f41697cdba96c10ae96bb1aac057285798" @@ -953,6 +963,19 @@ debug "^4.1.1" utf8 "3.0.0" +"@celo/connect@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/connect/-/connect-5.0.1.tgz#88a92f4f15fb2cb92fe711243e44deaf02a8ce7b" + integrity sha512-cN1hgdAzNP03Czgc70OkWepHSxsM21L0rpAU5nAagv4NTNfb8nuX89UwEIo1C4804gxR/MqGayHf48tZnnBZ5g== + dependencies: + "@celo/base" "5.0.1" + "@celo/utils" "5.0.1" + "@types/debug" "^4.1.5" + "@types/utf8" "^2.1.6" + bignumber.js "^9.0.0" + debug "^4.1.1" + utf8 "3.0.0" + "@celo/contractkit@1.5.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@celo/contractkit/-/contractkit-1.5.2.tgz#be15d570f3044a190dabb6bbe53d5081c78ea605" @@ -971,6 +994,25 @@ semver "^7.3.5" web3 "1.3.6" +"@celo/contractkit@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/contractkit/-/contractkit-5.0.1.tgz#2c76522784de0f9b03b3724b2cd9911a008f82ec" + integrity sha512-b9biRPA8+grwYkdkt6VYOuC+h2cN/MFcTSdqmAh4I4o5pxfHmTejbolVALjssSSC/3quV1XmTzeh9UMpPWJoDA== + dependencies: + "@celo/base" "5.0.1" + "@celo/connect" "5.0.1" + "@celo/utils" "5.0.1" + "@celo/wallet-local" "5.0.1" + "@types/bn.js" "^5.1.0" + "@types/debug" "^4.1.5" + bignumber.js "^9.0.0" + cross-fetch "3.0.6" + debug "^4.1.1" + fp-ts "2.1.1" + io-ts "2.0.1" + semver "^7.3.5" + web3 "1.10.0" + "@celo/phone-number-privacy-common@1.0.39": version "1.0.39" resolved "https://registry.yarnpkg.com/@celo/phone-number-privacy-common/-/phone-number-privacy-common-1.0.39.tgz#3c9568f70378d24d11afcc4306024c5cf4f8efe9" @@ -989,6 +1031,47 @@ elliptic "^6.5.4" is-base64 "^1.1.0" +"@celo/phone-number-privacy-common@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@celo/phone-number-privacy-common/-/phone-number-privacy-common-3.0.2.tgz#c6e1857635e1922e4f232cd44980ac5c67541856" + integrity sha512-WYTlx2UDn3ZQoQ4hXADfp16Gw5iUiHkEikReUilxGSRIgAH1MkGVMh2U2nKCU7/9RhaLxUVMXrfu3LulKet9WQ== + dependencies: + "@celo/base" "^5.0.1" + "@celo/contractkit" "^5.0.1" + "@celo/phone-utils" "^5.0.1" + "@celo/utils" "^5.0.1" + "@opentelemetry/api" "^1.4.1" + "@opentelemetry/auto-instrumentations-node" "^0.38.0" + "@opentelemetry/propagator-ot-trace" "^0.27.0" + "@opentelemetry/sdk-metrics" "^1.15.1" + "@opentelemetry/sdk-node" "^0.41.1" + "@opentelemetry/sdk-trace-web" "^1.15.1" + "@opentelemetry/semantic-conventions" "^1.15.1" + "@types/bunyan" "1.8.8" + bignumber.js "^9.0.0" + bunyan "1.8.12" + bunyan-debug-stream "2.0.0" + bunyan-gke-stackdriver "0.1.2" + dotenv "^8.2.0" + elliptic "^6.5.4" + io-ts "2.0.1" + is-base64 "^1.1.0" + +"@celo/phone-utils@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/phone-utils/-/phone-utils-5.0.1.tgz#ccfdd302fdc36d7f414f23d08ab133937bce8d5e" + integrity sha512-cExztDocm2Wu2fvPPt2bu2mi7mnorjSaWTmL/Kxl4GNIpKf5Q3k5iEOb/bPNAMROAdiOqipge7ro1sTP9d9iQg== + dependencies: + "@celo/base" "5.0.1" + "@celo/utils" "5.0.1" + "@types/country-data" "^0.0.0" + "@types/google-libphonenumber" "^7.4.23" + "@types/node" "^10.12.18" + country-data "^0.0.31" + fp-ts "2.1.1" + google-libphonenumber "^3.2.27" + io-ts "2.0.1" + "@celo/poprf@^0.1.9": version "0.1.9" resolved "https://registry.yarnpkg.com/@celo/poprf/-/poprf-0.1.9.tgz#38c514ce0f572b80edeb9dc280b6cf5e9d7c2a75" @@ -1035,6 +1118,23 @@ web3-eth-abi "1.3.6" web3-utils "1.3.6" +"@celo/utils@5.0.1", "@celo/utils@^5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/utils/-/utils-5.0.1.tgz#561725779e7ac39c029c81d3d175eaf70faef755" + integrity sha512-EHue0t0/ge8j59WivJ7PnFnT3V7omKPCbHSymx+ehauz01nT8Xt6q64ubqyS5hfPeWYL9JmuDJG7VmIaWpj8HQ== + dependencies: + "@celo/base" "5.0.1" + "@ethereumjs/util" "8.0.5" + "@types/bn.js" "^5.1.0" + "@types/elliptic" "^6.4.9" + "@types/node" "^10.12.18" + bignumber.js "^9.0.0" + elliptic "^6.5.4" + ethereum-cryptography "1.2.0" + io-ts "2.0.1" + web3-eth-abi "1.10.0" + web3-utils "1.10.0" + "@celo/wallet-base@1.5.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@celo/wallet-base/-/wallet-base-1.5.2.tgz#ae8df425bf3c702277bb1b63a761a2ec8429e7aa" @@ -1050,6 +1150,21 @@ eth-lib "^0.2.8" ethereumjs-util "^5.2.0" +"@celo/wallet-base@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/wallet-base/-/wallet-base-5.0.1.tgz#0da3a072169c9877988287006386bf05371d0470" + integrity sha512-MWYwloV0MbP7sFXAYGNilz3QgrIDkcmv0M4NhV+zLyZbuj/iyn+Kw5wU0x85YalO+jDDA7eFYvly1DYlEM2H9Q== + dependencies: + "@celo/base" "5.0.1" + "@celo/connect" "5.0.1" + "@celo/utils" "5.0.1" + "@ethereumjs/util" "8.0.5" + "@types/debug" "^4.1.5" + bignumber.js "^9.0.0" + debug "^4.1.1" + eth-lib "^0.2.8" + ethereum-cryptography "^2.1.2" + "@celo/wallet-local@1.5.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@celo/wallet-local/-/wallet-local-1.5.2.tgz#66ea5fb763e19724309e3d56f312f1a342e12b91" @@ -1062,6 +1177,17 @@ eth-lib "^0.2.8" ethereumjs-util "^5.2.0" +"@celo/wallet-local@5.0.1": + version "5.0.1" + resolved "https://registry.yarnpkg.com/@celo/wallet-local/-/wallet-local-5.0.1.tgz#61f0242d55c4258c3fdfc3bf0d887cb27877caa0" + integrity sha512-BQvxyLB85t5foyzFm5+zhTtlKUCDzPPAp+Ltpj7FUOYSy1bybuGys7iuhYWNg4hwTolEb6aOYh4ZSN5jZN958w== + dependencies: + "@celo/connect" "5.0.1" + "@celo/utils" "5.0.1" + "@celo/wallet-base" "5.0.1" + "@ethereumjs/util" "8.0.5" + eth-lib "^0.2.8" + "@chainsafe/as-sha256@^0.3.1": version "0.3.1" resolved "https://registry.yarnpkg.com/@chainsafe/as-sha256/-/as-sha256-0.3.1.tgz#3639df0e1435cab03f4d9870cc3ac079e57a6fc9" @@ -3529,11 +3655,35 @@ call-me-maybe "^1.0.1" glob-to-regexp "^0.3.0" +"@noble/curves@1.0.0", "@noble/curves@~1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.0.0.tgz#e40be8c7daf088aaf291887cbc73f43464a92932" + integrity sha512-2upgEu0iLiDVDZkNLeFV2+ht0BAVgQnEmCk6JsOch9Rp8xfkMCbvbAZlA2pBHQc73dbl+vFOXfqkf4uemdn0bw== + dependencies: + "@noble/hashes" "1.3.0" + +"@noble/curves@1.1.0", "@noble/curves@~1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.1.0.tgz#f13fc667c89184bc04cccb9b11e8e7bae27d8c3d" + integrity sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA== + dependencies: + "@noble/hashes" "1.3.1" + "@noble/hashes@1.2.0", "@noble/hashes@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12" integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ== +"@noble/hashes@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.0.tgz#085fd70f6d7d9d109671090ccae1d3bec62554a1" + integrity sha512-ilHEACi9DwqJB0pw7kv+Apvh50jiiSyR/cQ3y4W7lOR5mhvn/50FLUfsnfJz0BDZtl/RR16kXvptiv6q1msYZg== + +"@noble/hashes@1.3.1", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9" + integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA== + "@noble/secp256k1@1.7.1", "@noble/secp256k1@~1.7.0": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -5033,6 +5183,24 @@ "@noble/secp256k1" "~1.7.0" "@scure/base" "~1.1.0" +"@scure/bip32@1.3.0": + version "1.3.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.0.tgz#6c8d980ef3f290987736acd0ee2e0f0d50068d87" + integrity sha512-bcKpo1oj54hGholplGLpqPHRbIsnbixFtc06nwuNM5/dwSXOq/AAYoIBRsBmnZJSdfeNW5rnff7NTAz3ZCqR9Q== + dependencies: + "@noble/curves" "~1.0.0" + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + +"@scure/bip32@1.3.1": + version "1.3.1" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.1.tgz#7248aea723667f98160f593d621c47e208ccbb10" + integrity sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A== + dependencies: + "@noble/curves" "~1.1.0" + "@noble/hashes" "~1.3.1" + "@scure/base" "~1.1.0" + "@scure/bip39@1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.1.1.tgz#b54557b2e86214319405db819c4b6a370cf340c5" @@ -5041,6 +5209,22 @@ "@noble/hashes" "~1.2.0" "@scure/base" "~1.1.0" +"@scure/bip39@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.0.tgz#a207e2ef96de354de7d0002292ba1503538fc77b" + integrity sha512-SX/uKq52cuxm4YFXWFaVByaSHJh2w3BnokVSeUJVCv6K7WulT9u2BuNRBhuFl8vAuYnzx9bEu9WgpcNYTrYieg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + +"@scure/bip39@1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" + integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg== + dependencies: + "@noble/hashes" "~1.3.0" + "@scure/base" "~1.1.0" + "@sideway/address@^4.1.3": version "4.1.4" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.4.tgz#03dccebc6ea47fdc226f7d3d1ad512955d4783f0" @@ -6805,11 +6989,6 @@ resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.2.tgz#38ecb64f01aa0d02b7c8f4222d7c38af6316fef8" integrity sha512-txGIh+0eDFzKGC25zORnswy+br1Ha7hj5cMVwKIU7+s0U2AxxJru/jZSMU6OC9MJWP6+pc/hc6ZjyZShpsyY2g== -"@types/underscore@^1.8.8": - version "1.11.4" - resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.11.4.tgz#62e393f8bc4bd8a06154d110c7d042a93751def3" - integrity sha512-uO4CD2ELOjw8tasUrAhvnn2W4A0ZECOvMjCivJr4gA9pGgjv+qxKWY9GLTMVEK8ej85BxQOocUyE7hImmSQYcg== - "@types/utf8@^2.1.6": version "2.1.6" resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-2.1.6.tgz#430cabb71a42d0a3613cce5621324fe4f5a25753" @@ -6834,6 +7013,13 @@ dependencies: web3 "*" +"@types/ws@^8.5.4": + version "8.5.5" + resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.5.tgz#af587964aa06682702ee6dcbc7be41a80e4b28eb" + integrity sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg== + dependencies: + "@types/node" "*" + "@types/yargs-parser@*": version "21.0.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" @@ -6858,6 +7044,11 @@ dependencies: "@types/yargs-parser" "*" +"@wagmi/chains@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@wagmi/chains/-/chains-1.6.0.tgz#eb992ad28dbaaab729b5bcab3e5b461e8a035656" + integrity sha512-5FRlVxse5P4ZaHG3GTvxwVANSmYJas1eQrTBHhjxVtqXoorm0aLmCHbhmN8Xo1yu09PaWKlleEvfE98yH4AgIw== + "@xmldom/xmldom@^0.8.3": version "0.8.7" resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.8.7.tgz#8b1e39c547013941974d83ad5e9cf5042071a9a0" @@ -6917,6 +7108,11 @@ abi-to-sol@^0.6.6: prettier "^2.7.1" prettier-plugin-solidity "^1.0.0-dev.23" +abitype@0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-0.9.3.tgz#294d25288ee683d72baf4e1fed757034e3c8c277" + integrity sha512-dz4qCQLurx97FQhnb/EIYTk/ldQ+oafEDUqC0VVIeQS1Q48/YWt/9YNfMmp9SLFqN41ktxny3c8aYxHjmFIB/w== + abort-controller@3.0.0, abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -11762,6 +11958,16 @@ ethereum-cryptography@^0.1.3: secp256k1 "^4.0.1" setimmediate "^1.0.5" +ethereum-cryptography@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ethereum-cryptography/-/ethereum-cryptography-2.1.2.tgz#18fa7108622e56481157a5cb7c01c0c6a672eb67" + integrity sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug== + dependencies: + "@noble/curves" "1.1.0" + "@noble/hashes" "1.3.1" + "@scure/bip32" "1.3.1" + "@scure/bip39" "1.2.1" + ethereum-types@^3.7.1: version "3.7.1" resolved "https://registry.yarnpkg.com/ethereum-types/-/ethereum-types-3.7.1.tgz#8fa75e5d9f5da3c85535ea0d4bcd2614b1d650a8" @@ -15928,6 +16134,11 @@ isomorphic-fetch@^3.0.0: node-fetch "^2.6.1" whatwg-fetch "^3.4.1" +isomorphic-ws@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz#e5529148912ecb9b451b46ed44d53dae1ce04bbf" + integrity sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw== + isomorphic-ws@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/isomorphic-ws/-/isomorphic-ws-4.0.1.tgz#55fd4cd6c5e6491e76dc125938dd863f5cd4f2dc" @@ -25623,6 +25834,22 @@ verror@1.10.0: core-util-is "1.0.2" extsprintf "^1.2.0" +viem@~1.5.4: + version "1.5.4" + resolved "https://registry.yarnpkg.com/viem/-/viem-1.5.4.tgz#4ba43cda4be5ec193d9f1c092955743705a37450" + integrity sha512-/B2KbAiTqiPd6fzJgz4pgS879IXbHfO44RP/0nsRvBEuFJvHQlekNIAHTa4d3LPlsHWAM8GcH4m2P5ZvtEHVxA== + dependencies: + "@adraffy/ens-normalize" "1.9.0" + "@noble/curves" "1.0.0" + "@noble/hashes" "1.3.0" + "@scure/bip32" "1.3.0" + "@scure/bip39" "1.2.0" + "@types/ws" "^8.5.4" + "@wagmi/chains" "1.6.0" + abitype "0.9.3" + isomorphic-ws "5.0.0" + ws "8.12.0" + vm2@^3.9.11: version "3.9.19" resolved "https://registry.yarnpkg.com/vm2/-/vm2-3.9.19.tgz#be1e1d7a106122c6c492b4d51c2e8b93d3ed6a4a" @@ -27449,6 +27676,11 @@ ws@7.4.6: resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== +ws@8.12.0: + version "8.12.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" + integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== + ws@8.2.3: version "8.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-8.2.3.tgz#63a56456db1b04367d0b721a0b80cae6d8becbba"