From 07400b049bc48187c41682a24b84cb2afdb14a5a Mon Sep 17 00:00:00 2001 From: Aaron DeRuvo Date: Tue, 12 Sep 2023 16:43:06 +0200 Subject: [PATCH] Add support for CIP42 and EIP1559 (#10483) * remove deprecated functions / params for gasPrice / fee currency in kit * improve types / format txs * signing tests, add viem to compare with in tests. * fix type issue, and split the first 12 (not the last 3) * parallelize async calls. * do not extract signer from cip42 txns this doesnt seem to be a very important feature so punting on it to get CK released * update test * revert removal of signer recovery * feedback from sep * add back signer test for faster deving * it works. gandolf rides over the crest as the sun rises blinding the enemy * show error if the rpc caller does not attach * fix binding * lint fix, and remove the call to set FeeMarket (i dont think its actually need as this will happen in tx params normalizer) * cheat and pay with CELO to avoid the ganache issue where when paying in fee token querying for gasPrice give and error due to price lookup having an arg and ganache expecting 0 args * it was never supposed to be like this * why is it gasCurrency here? * add some logging for that bug debug -- dont judge me! * fix but where transfer was not adhering to the gasFeeCurrency flag * fix #10025 js numbers cause impresission * hex must be even * cleanup extra logs / commented out code. * add warning for gatewayfee * add more logging to try to debug test failure * fix test description typos * Dont use custom signing code in protocol test * remove underscore dep * protocol now depends on local wallet * revert back logging to how it was, add back ensure 0x to data if it exists throw an error (like web3 does) if storage key is bad remove temp file for changes (now in PR description) * use named types for address and hex values, mark gateway fee as deprecated fix versioning * publish sdk 5 and pnp-common 3.0.1 * add back dev suffix * use node 18 in container --- .github/workflows/cron-npm-install.yml | 2 +- dependency-graph.json | 3 +- packages/celotool/package.json | 18 +- packages/cli/package.json | 20 +- packages/cli/src/base.ts | 3 - .../releasegold/transfer-dollars.test.ts | 4 + .../commands/releasegold/transfer-dollars.ts | 1 - packages/cli/src/transfer-stable-base.ts | 18 +- packages/env-tests/package.json | 14 +- packages/metadata-crawler/package.json | 6 +- .../combiner/package.json | 14 +- .../phone-number-privacy/common/package.json | 14 +- .../phone-number-privacy/monitor/package.json | 16 +- .../phone-number-privacy/signer/package.json | 14 +- packages/protocol/lib/signing-utils.ts | 84 +-- packages/protocol/lib/web3-utils.ts | 53 +- .../migrations_ts/28_elect_validators.ts | 2 +- packages/protocol/package.json | 12 +- packages/sdk/base/package.json | 2 +- packages/sdk/base/src/address.ts | 5 +- packages/sdk/connect/package.json | 6 +- .../sdk/connect/src/celo-provider.test.ts | 5 +- packages/sdk/connect/src/connection.ts | 71 ++- packages/sdk/connect/src/types.ts | 113 +++- .../sdk/connect/src/utils/formatter.test.ts | 274 ++++++++ packages/sdk/connect/src/utils/formatter.ts | 160 ++++- .../src/utils/tx-params-normalizer.test.ts | 60 +- .../connect/src/utils/tx-params-normalizer.ts | 75 ++- packages/sdk/contractkit/package.json | 12 +- packages/sdk/contractkit/src/kit.test.ts | 54 +- packages/sdk/contractkit/src/kit.ts | 29 - packages/sdk/cryptographic-utils/package.json | 6 +- packages/sdk/encrypted-backup/package.json | 10 +- packages/sdk/explorer/package.json | 10 +- packages/sdk/governance/package.json | 12 +- packages/sdk/identity/package.json | 12 +- packages/sdk/keystores/package.json | 6 +- packages/sdk/network-utils/package.json | 2 +- packages/sdk/phone-utils/package.json | 6 +- packages/sdk/transactions-uri/package.json | 8 +- packages/sdk/utils/package.json | 4 +- .../sdk/wallets/wallet-base/jest.config.js | 5 + packages/sdk/wallets/wallet-base/package.json | 12 +- .../wallet-base/src/signing-utils.test.ts | 602 ++++++++++++++++++ .../wallets/wallet-base/src/signing-utils.ts | 567 ++++++++++++++--- .../wallets/wallet-base/src/wallet-base.ts | 3 +- .../sdk/wallets/wallet-base/tsconfig.json | 1 + .../sdk/wallets/wallet-hsm-aws/package.json | 12 +- .../sdk/wallets/wallet-hsm-azure/package.json | 12 +- .../sdk/wallets/wallet-hsm-gcp/package.json | 12 +- packages/sdk/wallets/wallet-hsm/package.json | 4 +- .../sdk/wallets/wallet-ledger/package.json | 10 +- .../sdk/wallets/wallet-local/package.json | 13 +- .../wallet-local/src/local-wallet.test.ts | 253 +++++++- .../wallets/wallet-local/src/signing.test.ts | 326 ++++++---- .../sdk/wallets/wallet-local/tsconfig.json | 1 + .../sdk/wallets/wallet-remote/package.json | 8 +- packages/sdk/wallets/wallet-rpc/package.json | 12 +- .../sdk/wallets/wallet-rpc/src/rpc-signer.ts | 11 +- .../wallets/wallet-rpc/src/rpc-wallet.test.ts | 22 +- yarn.lock | 242 ++++++- 61 files changed, 2663 insertions(+), 705 deletions(-) create mode 100644 packages/sdk/connect/src/utils/formatter.test.ts create mode 100644 packages/sdk/wallets/wallet-base/jest.config.js create mode 100644 packages/sdk/wallets/wallet-base/src/signing-utils.test.ts diff --git a/.github/workflows/cron-npm-install.yml b/.github/workflows/cron-npm-install.yml index 5ec884dad4..f4ac8c43d4 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 aefa286dd8..0e826914ea 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 8463432050..42d1976d6c 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 fe53c405ad..4446ca2e52 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 6ca0e7753c..b97492c5ff 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 e2e554a604..560101ce2b 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 a8d29ef046..72ba701750 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 293f494cdf..fe18e297a3 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 388ea146b1..b34005f50e 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 748e0b8d6b..eee036d360 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 4ad16b5a84..04c88952ce 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 505d6b3313..0fe23a973e 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 98f4dd599b..65b175990f 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 c7f9e5b59f..74951f461d 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 6f470c1fa3..64e5b143fa 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 7de0c6b706..2752955598 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 844aa628ad..742fff4791 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 68b31a4eeb..caa6f5495a 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 710a7a809f..c3b6dc9360 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 d49e641a2a..9eea852b30 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 daaa46656d..05b7d5d363 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 252c5d1c34..daa2b2668d 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 bb5fc7f455..fa11244f12 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 4f3d4493d6..87201c9159 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 0000000000..dbd2b3a86a --- /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 dfe3c07f65..ebba6760dc 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 2bfa4b0710..b43b01954b 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 7ef8168f67..986bd3d4b6 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 11ae3796af..4f15b54875 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 951e7fd056..f2d7694667 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 49ec754fc2..bd4061fc8f 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 0bbada347d..46eb3b5000 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 2c8c9ebe94..416a462248 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 751094b0b7..3158c10518 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 5ddc9289c0..7f2eb1b717 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 304278231f..04064ee416 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 d15a241ef1..6f92d01795 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 85e8bf560a..080861ca12 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 ff50fc223f..d47b5a4061 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 d50c8cb5f0..d315eac459 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 7977f28d9b..e4c9f240bc 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 0000000000..2681a75d5a --- /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 5ff32097b5..53ac7ceaab 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 0000000000..ed2ebb402f --- /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 c711de1195..f0a6b87488 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 9d0cd2575a..acab8d7fab 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 e6a754088b..51debe8913 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 c9143b7b6a..3b463ce62f 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 ba4957bb0b..814fecf9a9 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 7d8625b5db..a13a35aedd 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 f3b57ff51d..5db5375efb 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 09ef11ed9c..2251060681 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 2f9394c9e6..eec63cd4ee 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 4f34b5e927..7f447b18fd 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 ac867abb21..0fa9af00ff 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 e267672350..b37d430f5c 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 c59bb1090d..4bcacb62e1 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 846b1e3d71..a3b4b3a865 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 2e6cca39f0..00fce4de19 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 cb23c4942e..8f25fe84d2 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 bc941833aa..4f97f9380e 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"