From 2e1d3723be3944e18b5b417aa9a52de39a32cf97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Gu=C3=A9rin?= Date: Thu, 26 Feb 2026 16:55:46 +0100 Subject: [PATCH] refactor(coin-evm): remove some dead code --- .changeset/lazy-mayflies-itch.md | 5 + libs/coin-modules/coin-evm/docs/adapters.md | 5 +- libs/coin-modules/coin-evm/docs/logic.md | 16 +- .../coin-modules/coin-evm/docs/transaction.md | 7 - .../coin-evm/src/adapters/ethers.test.ts | 131 ------ .../coin-evm/src/adapters/ethers.ts | 39 -- .../coin-evm/src/adapters/index.ts | 1 - libs/coin-modules/coin-evm/src/errors.ts | 2 - libs/coin-modules/coin-evm/src/logic.test.ts | 416 +----------------- libs/coin-modules/coin-evm/src/logic.ts | 217 +-------- .../coin-evm/src/logic/estimateFees.test.ts | 2 +- .../coin-evm/src/network/node/ledger.test.ts | 87 ++-- .../coin-evm/src/network/node/ledger.ts | 37 +- .../coin-evm/src/network/node/rpc.common.ts | 57 +-- .../coin-evm/src/network/node/rpc.test.ts | 148 +++---- .../coin-evm/src/network/node/types.ts | 10 +- .../coin-evm/src/transaction.test.ts | 228 ---------- libs/coin-modules/coin-evm/src/transaction.ts | 89 ---- libs/coin-modules/coin-evm/src/types/erc20.ts | 15 - libs/coin-modules/coin-evm/src/types/index.ts | 1 - .../coin-modules/coin-evm/src/types/ledger.ts | 7 - 21 files changed, 125 insertions(+), 1395 deletions(-) create mode 100644 .changeset/lazy-mayflies-itch.md delete mode 100644 libs/coin-modules/coin-evm/src/adapters/ethers.test.ts delete mode 100644 libs/coin-modules/coin-evm/src/adapters/ethers.ts delete mode 100644 libs/coin-modules/coin-evm/src/types/erc20.ts diff --git a/.changeset/lazy-mayflies-itch.md b/.changeset/lazy-mayflies-itch.md new file mode 100644 index 000000000000..6f3caefbe8dd --- /dev/null +++ b/.changeset/lazy-mayflies-itch.md @@ -0,0 +1,5 @@ +--- +"@ledgerhq/coin-evm": major +--- + +refactor(coin-evm): remove dead code diff --git a/libs/coin-modules/coin-evm/docs/adapters.md b/libs/coin-modules/coin-evm/docs/adapters.md index 52962e123072..d389c2a63752 100644 --- a/libs/coin-modules/coin-evm/docs/adapters.md +++ b/libs/coin-modules/coin-evm/docs/adapters.md @@ -3,11 +3,8 @@ Set of functions in charge of converting a transaction format specific to a libr ## Files -#### ethers.ts -Functions used to convert transactions for the [ethers.js v6 library](https://docs.ethers.org/v6/) - #### etherscan.ts Functions used to convert transactions coming from the [etherscan-like explorers](https://docs.etherscan.io/api-endpoints/accounts) #### ledger.ts -Functions used to convert transactions coming from the [Ledger explorers](https://explorers.api.live.ledger.com/blockchain/v4/eth/docs/#/address/Transactions%20by%20address) \ No newline at end of file +Functions used to convert transactions coming from the [Ledger explorers](https://explorers.api.live.ledger.com/blockchain/v4/eth/docs/#/address/Transactions%20by%20address) diff --git a/libs/coin-modules/coin-evm/docs/logic.md b/libs/coin-modules/coin-evm/docs/logic.md index 3439129badd4..cca377e2be1f 100644 --- a/libs/coin-modules/coin-evm/docs/logic.md +++ b/libs/coin-modules/coin-evm/docs/logic.md @@ -3,12 +3,6 @@ Set of helpers for the whole coin-evm module. ## Methods -#### legacyTransactionHasFees -Simple function to verify the presence of key and values specific to transactions of type 0 and 1 (not type 2 / EIP-1559). - -#### eip1559TransactionHasFees -Simple function to verify the presence of key and values specific to transactions of type 2 (not type 0/Legacy or type 1/EIP-2930). - #### getGasLimit Helper return the gas limit of a transaction, either customized by the user or the default one. @@ -29,16 +23,8 @@ Helper working with the idea of maintaining the javascript memory references of Method creating a hash that will help triggering or not a full synchronization on an account. Modifying the result of this method will force every ledger live account to do at least 1 full synchronization the next time they use this lib. -#### attachOperations - Helper in charge of linking operations together based on transaction hash. Token operations & NFT operations are the result of a coin operation and if this coin operation is originated by our user we want to link those operations together as main & children operations. - A sub operation should always be linked to a coin operation, even if the user isn't at the origin of the sub operation. "NONE" type coin operations can be added when necessary. - ⚠️ If an NFT operation was found without a coin parent operation just like if it was not initiated by the synced account and we were to find that coin operation during another sync, the NONE operation created would not be removed, creating a duplicate that will cause issues regarding NFT balances & React key duplications. - -#### isNftTransaction -Type gard method narrowing NFT related transactions. - #### padHexString Add necessary "0" in a hexadecimal string in order to make its character an even number, which can be necessary to some implementations. (e.g. 0x123 => 0x0123) #### getMessageProperties -Function returning the properties that will be displayed on the nano in order to sign it, based on the message type/standard. \ No newline at end of file +Function returning the properties that will be displayed on the nano in order to sign it, based on the message type/standard. diff --git a/libs/coin-modules/coin-evm/docs/transaction.md b/libs/coin-modules/coin-evm/docs/transaction.md index 5ba3343d7d72..fe53f5b0bd4d 100644 --- a/libs/coin-modules/coin-evm/docs/transaction.md +++ b/libs/coin-modules/coin-evm/docs/transaction.md @@ -20,12 +20,5 @@ This serializer transforms a stringified transaction into a hydrated transaction ##### toTransactionRaw [standard] This serializer transforms a stringified transaction into a hydrated transaction. -##### getTransactionData -This method returns the *callData* of a smart contract transaction when crafted with a transaction mode (`send` mode can be coins or ERC20 transactions, while `erc721` & `erc1155` modes are NFT transactions). - ##### getTypedTransaction [EIP-2718](https://eips.ethereum.org/EIPS/eip-2718 "eip-2718") defines the capacity of a transaction to have different types, which can introduce new properties compared to the legacy definition. This method acts as a type guard for a transaction, depending on its type, to ensure that you're interacting with a transaction that has all the necessary properties for the [RLP encoding](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ "RLP encoding") to work correctly. As of today, only type 0 (legacy) and type 2 ([EIP-1559](https://eips.ethereum.org/EIPS/eip-1559 "EIP-1559")) transactions are supported. Type 1 ([EIP-2930](https://eips.ethereum.org/EIPS/eip-2930 "EIP-2930")) is compatible but does not add any access lists. - - -##### getSerializedTransaction -This method returns an [RLP encoding](https://ethereum.org/en/developers/docs/data-structures-and-encoding/rlp/ "RLP encoding") hexadecimal string representation of the provided transaction. diff --git a/libs/coin-modules/coin-evm/src/adapters/ethers.test.ts b/libs/coin-modules/coin-evm/src/adapters/ethers.test.ts deleted file mode 100644 index f40ae714a7bb..000000000000 --- a/libs/coin-modules/coin-evm/src/adapters/ethers.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import BigNumber from "bignumber.js"; -import { ethers } from "ethers"; -import { EvmTransactionEIP1559, EvmTransactionLegacy } from "../types"; -import { DEFAULT_NONCE } from "../utils"; -import { transactionToEthersTransaction } from "./ethers"; - -const testData = Buffer.from("testBufferString").toString("hex"); -const eip1559Tx: EvmTransactionEIP1559 = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0xkvn", - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: 0, - gasLimit: new BigNumber(21000), - chainId: 1, - data: Buffer.from(testData, "hex"), - maxFeePerGas: new BigNumber(10000), - maxPriorityFeePerGas: new BigNumber(10000), - type: 2, -}; -const legacyTx: EvmTransactionLegacy = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0xkvn", - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: 0, - gasLimit: new BigNumber(21000), - chainId: 1, - data: Buffer.from(testData, "hex"), - gasPrice: new BigNumber(10000), - type: 0, -}; - -describe("EVM Family", () => { - describe("adapters", () => { - describe("ethers", () => { - describe("transactionToEthersTransaction", () => { - it("should build convert an EIP1559 ledger live transaction to an ethers transaction", () => { - const ethers1559Tx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: 21000n, - data: "0x" + testData, - value: 100n, - chainId: 1n, - type: 2, - maxFeePerGas: 10000n, - maxPriorityFeePerGas: 10000n, - }; - - expect(transactionToEthersTransaction(eip1559Tx)).toEqual(ethers1559Tx); - }); - - it("should build convert an legacy ledger live transaction to an ethers transaction", () => { - const legacyEthersTx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: 21000n, - data: "0x" + testData, - value: 100n, - chainId: 1n, - type: 0, - gasPrice: 10000n, - }; - - expect(transactionToEthersTransaction(legacyTx)).toEqual(legacyEthersTx); - }); - - it("should properly handle floating point numbers", () => { - const txWithFloatingPoint = { - ...eip1559Tx, - maxFeePerGas: new BigNumber("29625091714.5"), - }; - - const ethers1559Tx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: 21000n, - data: "0x" + testData, - value: 100n, - chainId: 1n, - type: 2, - maxFeePerGas: 29625091715n, - maxPriorityFeePerGas: 10000n, - }; - - expect(transactionToEthersTransaction(txWithFloatingPoint)).toEqual(ethers1559Tx); - }); - - it("should replace the usage of DEFAULT_NONCE by a valid nonce (but unrealistic) instead", () => { - const createdTransactionWithDefaultNonce: EvmTransactionLegacy = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0xkvn", - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: DEFAULT_NONCE, - gasLimit: new BigNumber(21000), - chainId: 1, - data: Buffer.from(testData, "hex"), - gasPrice: new BigNumber(10000), - type: 0, - }; - - const ethersTxWithUnrealisticNonce: ethers.TransactionLike = { - to: "0xkvn", - nonce: Number.MAX_SAFE_INTEGER - 1, - gasLimit: 21000n, - data: "0x" + testData, - value: 100n, - chainId: 1n, - type: 0, - gasPrice: 10000n, - }; - - expect(transactionToEthersTransaction(createdTransactionWithDefaultNonce)).toEqual( - ethersTxWithUnrealisticNonce, - ); - }); - }); - }); - }); -}); diff --git a/libs/coin-modules/coin-evm/src/adapters/ethers.ts b/libs/coin-modules/coin-evm/src/adapters/ethers.ts deleted file mode 100644 index 247e501d0c4a..000000000000 --- a/libs/coin-modules/coin-evm/src/adapters/ethers.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ethers } from "ethers"; -import { Transaction as EvmTransaction } from "../types"; -import { DEFAULT_NONCE, getGasLimit } from "../utils"; - -/** - * Adapter to convert a Ledger Live transaction to an Ethers transaction - */ -export const transactionToEthersTransaction = (tx: EvmTransaction): ethers.Transaction => { - const gasLimit = getGasLimit(tx); - - /** - * removing floating point when rounding BigNumber (using 0 as argument to toFixed) - * to avoid errors with ethers.BigNumber.from that does not handle floating point - * since it is using BigNumber v5.7.0 (see https://www.npmjs.com/package/@ethersproject/bignumber) - */ - - const ethersTx = { - to: tx.recipient, - value: BigInt(tx.amount.toFixed(0)), - data: tx.data ? `0x${tx.data.toString("hex")}` : undefined, - gasLimit: BigInt(gasLimit.toFixed(0)), - // When using DEFAULT_NONCE (-1) ethers might break on some methods, - // therefore we replace this by an unrealisticly high nonce - // to prevent any valid signature being crafted here - nonce: tx.nonce === DEFAULT_NONCE ? Number.MAX_SAFE_INTEGER - 1 : tx.nonce, - chainId: BigInt(tx.chainId), - type: tx.type, - } as Partial; - - // is EIP-1559 transaction (type 2) - if (tx.type === 2) { - ethersTx.maxFeePerGas = BigInt(tx.maxFeePerGas.toFixed(0)); - ethersTx.maxPriorityFeePerGas = BigInt(tx.maxPriorityFeePerGas.toFixed(0)); - } else { - // is Legacy transaction (type 0) - ethersTx.gasPrice = BigInt(tx.gasPrice.toFixed(0)); - } - return ethersTx as ethers.Transaction; -}; diff --git a/libs/coin-modules/coin-evm/src/adapters/index.ts b/libs/coin-modules/coin-evm/src/adapters/index.ts index 4265f38e0672..e15b3bc5d00c 100644 --- a/libs/coin-modules/coin-evm/src/adapters/index.ts +++ b/libs/coin-modules/coin-evm/src/adapters/index.ts @@ -1,6 +1,5 @@ /* istanbul ignore file: pure exports, bridge tested by live-common with bridge.integration.test.ts */ -export * from "./ethers"; export * from "./ledger"; export * from "./etherscan"; export * from "./blockOperations"; diff --git a/libs/coin-modules/coin-evm/src/errors.ts b/libs/coin-modules/coin-evm/src/errors.ts index 58229dea04d3..ef6a7c2d3b84 100644 --- a/libs/coin-modules/coin-evm/src/errors.ts +++ b/libs/coin-modules/coin-evm/src/errors.ts @@ -16,7 +16,6 @@ export const InvalidExplorerResponse = createCustomErrorClass("InvalidExplorerRe // Node export const UnknownNode = createCustomErrorClass("UnknownNode"); export const LedgerNodeUsedIncorrectly = createCustomErrorClass("LedgerNodeUsedIncorrectly"); -export const ExternalNodeUsedIncorrectly = createCustomErrorClass("ExternalNodeUsedIncorrectly"); export const UnsupportedRpcMethodError = createCustomErrorClass<{ method: string; rawError: unknown; @@ -35,4 +34,3 @@ export const InsufficientFunds = createCustomErrorClass("InsufficientFunds"); // Nfts export const NotOwnedNft = createCustomErrorClass("NotOwnedNft"); export const NotEnoughNftOwned = createCustomErrorClass("NotEnoughNftOwned"); -export const QuantityNeedsToBePositive = createCustomErrorClass("QuantityNeedsToBePositive"); diff --git a/libs/coin-modules/coin-evm/src/logic.test.ts b/libs/coin-modules/coin-evm/src/logic.test.ts index 83d63ccd2dee..3ead199f7187 100644 --- a/libs/coin-modules/coin-evm/src/logic.test.ts +++ b/libs/coin-modules/coin-evm/src/logic.test.ts @@ -1,16 +1,8 @@ import { getSyncHash as baseGetSyncHash } from "@ledgerhq/coin-framework/account/sync"; import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets"; -import { setupMockCryptoAssetsStore } from "@ledgerhq/cryptoassets/cal-client/test-helpers"; -import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state"; import * as EVM_TOOLS from "@ledgerhq/evm-tools/message/EIP712/index"; import { getEnv, setEnv } from "@ledgerhq/live-env"; -import { - CryptoCurrency, - CryptoCurrencyId, - TokenCurrency, - Unit, -} from "@ledgerhq/types-cryptoassets"; -import type { Operation } from "@ledgerhq/types-live"; +import { CryptoCurrencyId, TokenCurrency } from "@ledgerhq/types-cryptoassets"; import BigNumber from "bignumber.js"; jest.mock("./network/node/rpc.common", () => ({ @@ -22,32 +14,18 @@ jest.mock("./network/node/rpc.common", () => ({ const mockGetOptimismAdditionalFees = getOptimismAdditionalFees as jest.Mock; const mockGetScrollAdditionalFees = getScrollAdditionalFees as jest.Mock; import { getCoinConfig } from "./config"; -import { - deepFreeze, - makeAccount, - makeNftOperation, - makeOperation, - makeTokenAccount, -} from "./fixtures/common.fixtures"; +import { makeAccount, makeOperation, makeTokenAccount } from "./fixtures/common.fixtures"; import usdCoinTokenData from "./fixtures/ethereum-erc20-usd__coin.json"; import wethTokenData from "./fixtures/ethereum-erc20-weth.json"; import { - attachOperations, createSwapHistoryMap, - eip1559TransactionHasFees, getAdditionalLayer2Fees, - getDefaultFeeUnit, getMessageProperties, getSyncHash, - legacyTransactionHasFees, mergeSubAccounts, } from "./logic"; import { getOptimismAdditionalFees, getScrollAdditionalFees } from "./network/node/rpc.common"; -import { - EvmTransactionEIP1559, - EvmTransactionLegacy, - Transaction as EvmTransaction, -} from "./types"; +import { Transaction as EvmTransaction } from "./types"; import { getEstimatedFees, getGasLimit, padHexString, safeEncodeEIP55 } from "./utils"; // eslint-disable-next-line @typescript-eslint/consistent-type-assertions @@ -128,81 +106,6 @@ mockGetConfig.mockImplementation((currency: { id: string }): any => { describe("EVM Family", () => { describe("logic.ts", () => { - describe("legacyTransactionHasFees", () => { - it("should return true for legacy tx with fees", () => { - const tx: Partial = { - type: 0, - gasPrice: new BigNumber(100), - }; - - expect(legacyTransactionHasFees(tx as EvmTransactionLegacy)).toBe(true); - }); - - it("should return true for type 1 (unused in the live for now) tx with fees", () => { - const tx: Partial = { - type: 1, - gasPrice: new BigNumber(100), - }; - - expect(legacyTransactionHasFees(tx as EvmTransactionLegacy)).toBe(true); - }); - - it("should return false for legacy tx without fees", () => { - const tx: Partial = { - type: 0, - }; - - expect(legacyTransactionHasFees(tx as any)).toBe(false); - }); - - it("should return false for legacy tx with wrong fees", () => { - const tx: Partial = { - type: 2, - maxFeePerGas: new BigNumber(100), - maxPriorityFeePerGas: new BigNumber(100), - }; - - expect(legacyTransactionHasFees(tx as any)).toBe(false); - }); - - it("should return true for legacy tx with fees but no type (default being a legacy tx)", () => { - const tx: Partial = { - gasPrice: new BigNumber(100), - }; - - expect(legacyTransactionHasFees(tx as any)).toBe(true); - }); - }); - - describe("eip1559TransactionHasFess", () => { - it("should return true for 1559 tx with fees", () => { - const tx: Partial = { - type: 2, - maxFeePerGas: new BigNumber(100), - maxPriorityFeePerGas: new BigNumber(100), - }; - - expect(eip1559TransactionHasFees(tx as any)).toBe(true); - }); - - it("should return false for 1559 tx without fees", () => { - const tx: Partial = { - type: 2, - }; - - expect(eip1559TransactionHasFees(tx as any)).toBe(false); - }); - - it("should return false for 1559 tx with wrong fees", () => { - const tx: unknown = { - type: 2, - gasPrice: new BigNumber(100), - }; - - expect(eip1559TransactionHasFees(tx as any)).toBe(false); - }); - }); - describe("getGasLimit", () => { it("should return the gasLimit when no customGasLimit provided", () => { const tx: Partial = { @@ -300,40 +203,6 @@ describe("EVM Family", () => { }); }); - describe("getDefaultFeeUnit", () => { - it("should return the unit when currency has only one", () => { - const expectedUnit: Unit = { - name: "name", - code: "code", - magnitude: 18, - }; - - const currency: Partial = { - units: [expectedUnit], - }; - - expect(getDefaultFeeUnit(currency as CryptoCurrency)).toEqual(expectedUnit); - }); - - it("should return the second unit when currency has multiple", () => { - const expectedUnit: Unit = { - name: "name", - code: "code", - magnitude: 18, - }; - - const currency: Partial = { - units: [ - { ...expectedUnit, name: "unit0" }, - expectedUnit, - { ...expectedUnit, name: "unit2" }, - ], - }; - - expect(getDefaultFeeUnit(currency as CryptoCurrency)).toEqual(expectedUnit); - }); - }); - describe("getAdditionalLayer2Fees", () => { const optimism = getCryptoCurrencyById("optimism"); const scroll = getCryptoCurrencyById("scroll"); @@ -652,285 +521,6 @@ describe("EVM Family", () => { }); }); - describe("attachOperations", () => { - it("should attach token & nft operations to coin operations and create 'NONE' coin operations in case of orphans child operations", async () => { - setupMockCryptoAssetsStore({ - findTokenByAddressInCurrency: async ( - address: string, - currencyId: string, - ): Promise => { - if (address === "0xTokenContract" && currencyId === "ethereum") { - return { - type: "TokenCurrency" as const, - id: "ethereum/erc20/usd__coin", - contractAddress: "0xTokenContract", - parentCurrency: getCryptoCurrencyById("ethereum"), - tokenType: "erc20", - name: "USD Coin", - ticker: "USDC", - units: [{ name: "USDC", code: "USDC", magnitude: 6 }], - }; - } - if (address === "0xOtherTokenContract" && currencyId === "ethereum") { - return { - type: "TokenCurrency" as const, - id: "ethereum/erc20/usd__coin", - contractAddress: "0xOtherTokenContract", - parentCurrency: getCryptoCurrencyById("ethereum"), - tokenType: "erc20", - name: "USD Coin", - ticker: "USDC", - units: [{ name: "USDC", code: "USDC", magnitude: 6 }], - }; - } - return undefined; - }, - getTokensSyncHash: async () => "0", - }); - const coinOperation = makeOperation({ - hash: "0xCoinOp3Hash", - }); - const tokenAccountId = - coinOperation.accountId + "+ethereum%2Ferc20%2Fusd~!underscore!~~!underscore!~coin"; - const tokenOperations = [ - makeOperation({ - accountId: tokenAccountId, - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(1), - type: "OUT", - }), - makeOperation({ - accountId: tokenAccountId, - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(2), - type: "IN", - }), - makeOperation({ - accountId: tokenAccountId, - hash: "0xUnknownHash", - contract: "0xOtherTokenContract", - value: new BigNumber(2), - type: "IN", - }), - ]; - const nftOperations = [ - makeNftOperation({ - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(1), - type: "NFT_OUT", - }), - makeNftOperation({ - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(2), - type: "NFT_IN", - }), - makeNftOperation({ - hash: "0xUnknownNftHash", - contract: "0xOtherNftTokenContract", - value: new BigNumber(2), - type: "NFT_IN", - }), - ]; - const internalOperations = [ - makeOperation({ - accountId: coinOperation.accountId, - value: new BigNumber(5), - type: "OUT", - hash: coinOperation.hash, - }), - ]; - - expect( - await attachOperations( - [coinOperation], - tokenOperations, - nftOperations, - internalOperations, - { - blacklistedTokenIds: [], - findToken: async (contractAddress: string) => - getCryptoAssetsStore().findTokenByAddressInCurrency(contractAddress, "ethereum"), - }, - ), - ).toEqual([ - { - ...coinOperation, - subOperations: [tokenOperations[0], tokenOperations[1]], - nftOperations: [nftOperations[0], nftOperations[1]], - internalOperations: [internalOperations[0]], - }, - { - ...tokenOperations[2], - id: `js:2:ethereum:0xkvn:-${tokenOperations[2].hash}-NONE`, - type: "NONE", - value: new BigNumber(0), - fee: new BigNumber(0), - senders: [], - recipients: [], - nftOperations: [], - subOperations: [tokenOperations[2]], - internalOperations: [], - accountId: "", - contract: undefined, - }, - { - ...nftOperations[2], - id: `js:2:ethereum:0xkvn:-${nftOperations[2].hash}-NONE`, - type: "NONE", - value: new BigNumber(0), - fee: new BigNumber(0), - senders: [], - recipients: [], - nftOperations: [nftOperations[2]], - subOperations: [], - internalOperations: [], - accountId: "", - contract: undefined, - }, - ]); - }); - - it("should not mutate the original operations", () => { - const coinOperations = deepFreeze([ - makeOperation({ - hash: "0xCoinOp3Hash", - }), - ]); - const tokenOperations = deepFreeze([ - makeOperation({ - hash: coinOperations[0].hash, - contract: "0xTokenContract", - value: new BigNumber(1), - type: "OUT", - }), - ]); - const nftOperations = deepFreeze([ - makeNftOperation({ - hash: coinOperations[0].hash, - contract: "0xTokenContract", - value: new BigNumber(1), - type: "NFT_OUT", - }), - ]); - const internalOperations = deepFreeze([ - makeOperation({ - hash: "0xCoinOpInternal", - }), - ]); - expect(() => - attachOperations( - coinOperations as Operation[], - tokenOperations as Operation[], - nftOperations as Operation[], - internalOperations as Operation[], - { - blacklistedTokenIds: [], - findToken: async (contractAddress: string) => - getCryptoAssetsStore().findTokenByAddressInCurrency(contractAddress, "ethereum"), - }, - ), - ).not.toThrow(); // mutation prevented by deepFreeze method - }); - - it("should filter blacklisted tokens", async () => { - const coinOperation = makeOperation({ - hash: "0xCoinOp3Hash", - }); - const tokenAccountId = - coinOperation.accountId + "+ethereum%2Ferc20%2Fusd~!underscore!~~!underscore!~coin"; - const tokenOperations = [ - makeOperation({ - accountId: tokenAccountId, - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(1), - type: "OUT", - }), - makeOperation({ - accountId: tokenAccountId, - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(2), - type: "IN", - }), - makeOperation({ - accountId: tokenAccountId, - hash: "0xUnknownHash", - contract: "0xOtherTokenContract", - value: new BigNumber(2), - type: "IN", - }), - ]; - const nftOperations = [ - makeNftOperation({ - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(1), - type: "NFT_OUT", - }), - makeNftOperation({ - hash: coinOperation.hash, - contract: "0xTokenContract", - value: new BigNumber(2), - type: "NFT_IN", - }), - makeNftOperation({ - hash: "0xUnknownNftHash", - contract: "0xOtherNftTokenContract", - value: new BigNumber(2), - type: "NFT_IN", - }), - ]; - const internalOperations = [ - makeOperation({ - accountId: coinOperation.accountId, - value: new BigNumber(5), - type: "OUT", - hash: coinOperation.hash, - }), - ]; - - expect( - await attachOperations( - [coinOperation], - tokenOperations, - nftOperations, - internalOperations, - { - blacklistedTokenIds: [USD_COIN_TOKEN.id], - findToken: async (contractAddress: string) => - getCryptoAssetsStore().findTokenByAddressInCurrency(contractAddress, "ethereum"), - }, - ), - ).toEqual([ - { - ...coinOperation, - subOperations: [], - nftOperations: [nftOperations[0], nftOperations[1]], - internalOperations: [internalOperations[0]], - }, - { - ...nftOperations[2], - id: `js:2:ethereum:0xkvn:-${nftOperations[2].hash}-NONE`, - type: "NONE", - value: new BigNumber(0), - fee: new BigNumber(0), - senders: [], - recipients: [], - nftOperations: [nftOperations[2]], - subOperations: [], - internalOperations: [], - accountId: "", - contract: undefined, - }, - ]); - }); - }); - describe("padHexString", () => { it("should always return an odd number of characters", () => { expect(padHexString("1")).toEqual("01"); diff --git a/libs/coin-modules/coin-evm/src/logic.ts b/libs/coin-modules/coin-evm/src/logic.ts index 4f09d5cf7a30..4cc8a4481c24 100644 --- a/libs/coin-modules/coin-evm/src/logic.ts +++ b/libs/coin-modules/coin-evm/src/logic.ts @@ -1,43 +1,14 @@ -import { - decodeTokenAccountId, - encodeTokenAccountId, - getSyncHash as baseGetSyncHash, -} from "@ledgerhq/coin-framework/account/index"; +import { getSyncHash as baseGetSyncHash } from "@ledgerhq/coin-framework/account/index"; import { mergeOps } from "@ledgerhq/coin-framework/bridge/jsHelpers"; import { isNFTActive } from "@ledgerhq/coin-framework/nft/support"; -import { encodeOperationId } from "@ledgerhq/coin-framework/operation"; import { getEIP712FieldsDisplayedOnNano } from "@ledgerhq/evm-tools/message/EIP712/index"; import { getEnv } from "@ledgerhq/live-env"; -import { TokenCurrency, CryptoCurrency, Unit } from "@ledgerhq/types-cryptoassets"; -import { - Account, - AnyMessage, - MessageProperties, - Operation, - TokenAccount, -} from "@ledgerhq/types-live"; +import { CryptoCurrency, Unit } from "@ledgerhq/types-cryptoassets"; +import { Account, AnyMessage, MessageProperties, TokenAccount } from "@ledgerhq/types-live"; import BigNumber from "bignumber.js"; import murmurhash from "imurmurhash"; import { getCoinConfig } from "./config"; import { getNodeApi } from "./network/node/index"; -import { - EvmNftTransaction, - Transaction as EvmTransaction, - EvmTransactionEIP1559, - EvmTransactionLegacy, -} from "./types"; - -/** - * Helper to check if a legacy transaction has the right fee property - */ -export const legacyTransactionHasFees = (tx: EvmTransactionLegacy): boolean => - Boolean((!tx.type || tx.type < 2) && tx.gasPrice); - -/** - * Helper to check if a legacy transaction has the right fee property - */ -export const eip1559TransactionHasFees = (tx: EvmTransactionEIP1559): boolean => - Boolean(tx.type === 2 && tx.maxFeePerGas && tx.maxPriorityFeePerGas); /** * Helper to get the currency unit to be used for the fee field @@ -51,7 +22,7 @@ export const getDefaultFeeUnit = (currency: CryptoCurrency): Unit => */ export const getAdditionalLayer2Fees = async ( currency: CryptoCurrency, - transaction: EvmTransaction | string, + transaction: string, ): Promise => { switch (currency.id) { case "optimism": @@ -87,6 +58,8 @@ const updatableSubAccountProperties: { name: string; isOps: boolean }[] = [ ]; /** + * NOTE Still imported by `coin-celo` + * * In charge of smartly merging sub accounts while maintaining references as much as possible */ export const mergeSubAccounts = ( @@ -148,33 +121,8 @@ export const mergeSubAccounts = ( }; /** - * Map of Crypto Asset List content hash per currency. - * Used to detect changes between syncs and trigger - * a full synchronization in order to detect - * freshly added token definitions - */ -const CALHashByChainIdMap = new Map(); - -/** - * Getter for the CAL content hash - */ -export const getCALHash = (currency: CryptoCurrency): string => { - return CALHashByChainIdMap.get(currency) || ""; -}; - -/** - * Setter for the CAL content hash - */ -export const setCALHash = (currency: CryptoCurrency, hash: string): string => { - CALHashByChainIdMap.set(currency, hash); - return CALHashByChainIdMap.get(currency)!; -}; - -export const __resetCALHash = (): void => { - CALHashByChainIdMap.clear(); -}; - -/** + * NOTE Still imported by `coin-celo` + * * Method creating a hash that will help triggering or not a full synchronization on an account. * As of now, it's checking if a token has been added, removed of changed regarding important properties * and if the NFTs are activated/supported on this chain @@ -200,152 +148,6 @@ export const getSyncHash = async ( return `0x${murmurhash(stringToHash).result().toString(16)}`; }; -/** - * Helper in charge of linking operations together based on transaction hash. - * Token operations & NFT operations are the result of a coin operation - * and if this coin operation is originated by our user we want - * to link those operations together as main & children ops. - * - * A sub operation should always be linked to a coin operation, - * even if the user isn't at the origin of the sub op. - * "NONE" coin ops can be added when necessary. - * - * ⚠️ If an NFT operation was found without a coin parent op - * just like if it was not initiated by the synced account - * and we were to find that coin op during another sync, - * the NONE operation created would not be removed, - * creating a duplicate that will cause issues. - * (Incorrect NFT balance & React key dup) - */ -export const attachOperations = async ( - _coinOperations: Operation[], - _tokenOperations: Operation[], - _nftOperations: Operation[], - _internalOperations: Operation[], - filters: { - blacklistedTokenIds: string[] | undefined; - findToken: (contractAddress: string) => Promise; - }, -): Promise => { - const { blacklistedTokenIds, findToken } = filters; - - // Creating deep copies of each Operation[] to prevent mutating the originals - const coinOperations = _coinOperations.map(op => ({ ...op })); - const tokenOperations = _tokenOperations.map(op => ({ ...op })); - const nftOperations = _nftOperations.map(op => ({ ...op })); - const internalOperations = _internalOperations.map(op => ({ ...op })); - - type OperationWithRequiredChildren = Operation & - Required>; - - // Helper to create a coin operation with type NONE as a parent of an orphan child operation - const makeCoinOpForOrphanChildOp = async ( - childOp: Operation, - ): Promise => { - const type = "NONE"; - const { accountId } = await decodeTokenAccountId(childOp.accountId); - const id = encodeOperationId(accountId, childOp.hash, type); - - return { - id, - hash: childOp.hash, - type, - value: new BigNumber(0), - fee: new BigNumber(0), - senders: [], - recipients: [], - blockHeight: childOp.blockHeight, - blockHash: childOp.blockHash, - transactionSequenceNumber: childOp.transactionSequenceNumber, - subOperations: [], - nftOperations: [], - internalOperations: [], - accountId: "", - date: childOp.date, - extra: {}, - }; - }; - - // Create a Map of hash => operation - const coinOperationsByHash: Record = {}; - coinOperations.forEach(op => { - if (!coinOperationsByHash[op.hash]) { - coinOperationsByHash[op.hash] = []; - } - - // Adding arrays just in case but this is defined - // by the adapters so it should never be needed - op.subOperations = []; - op.nftOperations = []; - op.internalOperations = []; - coinOperationsByHash[op.hash].push(op as OperationWithRequiredChildren); - }); - - // Looping through token operations to potentially copy them as a child operation of a coin operation - for (const tokenOperation of tokenOperations) { - const token = tokenOperation.contract && (await findToken(tokenOperation.contract)); - if (!token || blacklistedTokenIds?.includes(token.id)) continue; - - const { accountId } = await decodeTokenAccountId(tokenOperation.accountId); - const tokenAccountId = encodeTokenAccountId(accountId, token); - const operationId = encodeOperationId(tokenAccountId, tokenOperation.hash, tokenOperation.type); - tokenOperation.id = operationId; - tokenOperation.accountId = tokenAccountId; - - let mainOperations = coinOperationsByHash[tokenOperation.hash]; - if (!mainOperations?.length) { - const noneOperation = await makeCoinOpForOrphanChildOp(tokenOperation); - mainOperations = [noneOperation]; - coinOperations.push(noneOperation); - } - - // Ugly loop in loop but in theory, this can only be a 2 elements array maximum in the case of a self send - for (const mainOperation of mainOperations) { - mainOperation.subOperations.push(tokenOperation); - } - } - - // Looping through nft operations to potentially copy them as a child operation of a coin operation - for (const nftOperation of nftOperations) { - let mainOperations = coinOperationsByHash[nftOperation.hash]; - if (!mainOperations?.length) { - const noneOperation = await makeCoinOpForOrphanChildOp(nftOperation); - mainOperations = [noneOperation]; - coinOperations.push(noneOperation); - } - - // Ugly loop in loop but in theory, this can only be a 2 elements array maximum in the case of a self send - for (const mainOperation of mainOperations) { - mainOperation.nftOperations.push(nftOperation); - } - } - - // Looping through internal operations to potentially copy them as a child operation of a coin operation - for (const internalOperation of internalOperations) { - let mainOperations = coinOperationsByHash[internalOperation.hash]; - if (!mainOperations?.length) { - const noneOperation = await makeCoinOpForOrphanChildOp(internalOperation); - mainOperations = [noneOperation]; - coinOperations.push(noneOperation); - } - - // Ugly loop in loop but in theory, this can only be a 2 elements array maximum in the case of a self send - for (const mainOperation of mainOperations) { - mainOperation.internalOperations.push(internalOperation); - } - } - - return coinOperations; -}; - -/** - * Type guard for NFT transactions - */ -export const isNftTransaction = ( - transaction: EvmTransaction, -): transaction is EvmTransaction & EvmNftTransaction => - ["erc1155", "erc721"].includes(transaction.mode); - /** * Helper to get the message properties to be displayed on the Nano */ @@ -360,9 +162,10 @@ export const getMessageProperties = async ( }; /** + * NOTE Still imported by `coin-celo` + * * Similar to mergeAccount but used to keep previous data we can't fetch on chain */ -// logic.ts export const createSwapHistoryMap = ( initialAccount: Account | undefined, ): Map => { diff --git a/libs/coin-modules/coin-evm/src/logic/estimateFees.test.ts b/libs/coin-modules/coin-evm/src/logic/estimateFees.test.ts index 40a7413ef654..b497eef882b3 100644 --- a/libs/coin-modules/coin-evm/src/logic/estimateFees.test.ts +++ b/libs/coin-modules/coin-evm/src/logic/estimateFees.test.ts @@ -410,7 +410,7 @@ describe("estimateFees", () => { }); }); - it("gives 0 additional fees if the transaction is not deserializable", async () => { + it("gives 0 additional fees if the transaction is not serializable", async () => { mockNodeApi.getGasEstimation.mockResolvedValue(new BigNumber("21000")); mockNodeApi.getTransactionCount.mockResolvedValue(42); jest.mocked(getNodeApi).mockReturnValue(mockNodeApi as any); diff --git a/libs/coin-modules/coin-evm/src/network/node/ledger.test.ts b/libs/coin-modules/coin-evm/src/network/node/ledger.test.ts index 102a0a1459d4..1ae0016bb30e 100644 --- a/libs/coin-modules/coin-evm/src/network/node/ledger.test.ts +++ b/libs/coin-modules/coin-evm/src/network/node/ledger.test.ts @@ -4,6 +4,7 @@ import { delay } from "@ledgerhq/live-promise"; import { CryptoCurrency, CryptoCurrencyId } from "@ledgerhq/types-cryptoassets"; import axios from "axios"; import BigNumber from "bignumber.js"; +import { Transaction } from "ethers"; import { getCoinConfig } from "../../config"; import { GasEstimationError, LedgerNodeUsedIncorrectly } from "../../errors"; import { makeAccount } from "../../fixtures/common.fixtures"; @@ -679,24 +680,6 @@ describe("EVM Family", () => { ); }); - it("should return 0 for invalid transaction", async () => { - const transaction: EvmTransaction = { - family: "evm", - mode: "send", - recipient: "0xINVALID", - amount: new BigNumber(1), - gasLimit: new BigNumber(2), - chainId: 1, - nonce: 0, - gasPrice: new BigNumber(3), - type: 0, - }; - - expect( - await LEDGER_API.getOptimismAdditionalFees({ ...currency, id: "optimism" }, transaction), - ).toEqual(new BigNumber("0")); - }); - it("should return the expected payload", async () => { jest.spyOn(axios, "request").mockImplementationOnce(async () => ({ data: [ @@ -711,20 +694,27 @@ describe("EVM Family", () => { ], })); - const transaction: EvmTransaction = { - family: "evm", - mode: "send", - recipient: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d", - amount: new BigNumber(1), - gasLimit: new BigNumber(2), + // Build a serialized transaction, the exact same way we do in `estimateFees` + const transaction = Transaction.from({ + to: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d", + value: 1n, + gasLimit: 2n, chainId: 1, nonce: 0, - gasPrice: new BigNumber(3), + gasPrice: 3n, type: 0, - }; + signature: { + r: "0xffffffffffffffffffffffffffffffffffffffff", + s: "0xffffffffffffffffffffffffffffffffffffffff", + v: 27, + }, + }); expect( - await LEDGER_API.getOptimismAdditionalFees({ ...currency, id: "optimism" }, transaction), + await LEDGER_API.getOptimismAdditionalFees( + { ...currency, id: "optimism" }, + transaction.serialized, + ), ).toEqual(new BigNumber("100000000")); }); }); @@ -748,24 +738,6 @@ describe("EVM Family", () => { ); }); - it("should return 0 for invalid transaction", async () => { - const transaction: EvmTransaction = { - family: "evm", - mode: "send", - recipient: "0xINVALID", - amount: new BigNumber(1), - gasLimit: new BigNumber(2), - chainId: 1, - nonce: 0, - gasPrice: new BigNumber(3), - type: 0, - }; - - expect( - await LEDGER_API.getScrollAdditionalFees({ ...currency, id: "scroll" }, transaction), - ).toEqual(new BigNumber("0")); - }); - it("should return the expected payload", async () => { jest.spyOn(axios, "request").mockImplementationOnce(async () => ({ data: [ @@ -780,20 +752,27 @@ describe("EVM Family", () => { ], })); - const transaction: EvmTransaction = { - family: "evm", - mode: "send", - recipient: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d", - amount: new BigNumber(1), - gasLimit: new BigNumber(2), + // Build a serialized transaction, the exact same way we do in `estimateFees` + const transaction = Transaction.from({ + to: "0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d", + value: 1n, + gasLimit: 2n, chainId: 1, nonce: 0, - gasPrice: new BigNumber(3), + gasPrice: 3n, type: 0, - }; + signature: { + r: "0xffffffffffffffffffffffffffffffffffffffff", + s: "0xffffffffffffffffffffffffffffffffffffffff", + v: 27, + }, + }); expect( - await LEDGER_API.getScrollAdditionalFees({ ...currency, id: "scroll" }, transaction), + await LEDGER_API.getScrollAdditionalFees( + { ...currency, id: "scroll" }, + transaction.serialized, + ), ).toEqual(new BigNumber("100000000")); }); }); diff --git a/libs/coin-modules/coin-evm/src/network/node/ledger.ts b/libs/coin-modules/coin-evm/src/network/node/ledger.ts index 8d5edbead522..7f30028266f4 100644 --- a/libs/coin-modules/coin-evm/src/network/node/ledger.ts +++ b/libs/coin-modules/coin-evm/src/network/node/ledger.ts @@ -10,7 +10,6 @@ import { ethers } from "ethers"; import OptimismGasPriceOracleAbi from "../../abis/optimismGasPriceOracle.abi.json"; import { getCoinConfig } from "../../config"; import { GasEstimationError, LedgerNodeUsedIncorrectly } from "../../errors"; -import { getSerializedTransaction } from "../../transaction"; import { LedgerExplorerOperation } from "../../types"; import { padHexString, safeEncodeEIP55 } from "../../utils"; import { getGasOptions } from "../gasTracker/ledger"; @@ -389,26 +388,12 @@ export const getOptimismAdditionalFees: NodeApi["getOptimismAdditionalFees"] = a } // Fake signature is added to get the best approximation possible for the gas on L1 - const serializedTransaction = - typeof transaction === "string" - ? transaction - : ((): string | null => { - try { - return getSerializedTransaction(transaction, { - r: "0xffffffffffffffffffffffffffffffffffffffff", - s: "0xffffffffffffffffffffffffffffffffffffffff", - v: 27, - }); - } catch { - return null; - } - })(); - if (!serializedTransaction) { + if (!transaction) { return new BigNumber(0); } const optimismGasOracle = new ethers.Interface(OptimismGasPriceOracleAbi); - const data = optimismGasOracle.encodeFunctionData("getL1Fee(bytes)", [serializedTransaction]); + const data = optimismGasOracle.encodeFunctionData("getL1Fee(bytes)", [transaction]); const [result] = await fetchWithRetries< Array<{ @@ -458,26 +443,12 @@ export const getScrollAdditionalFees: NodeApi["getScrollAdditionalFees"] = async } // Fake signature is added to get the best approximation possible for the gas on L1 - const serializedTransaction = - typeof transaction === "string" - ? transaction - : ((): string | null => { - try { - return getSerializedTransaction(transaction, { - r: "0xffffffffffffffffffffffffffffffffffffffff", - s: "0xffffffffffffffffffffffffffffffffffffffff", - v: 27, - }); - } catch { - return null; - } - })(); - if (!serializedTransaction) { + if (!transaction) { return new BigNumber(0); } const optimismGasOracle = new ethers.Interface(OptimismGasPriceOracleAbi); - const data = optimismGasOracle.encodeFunctionData("getL1Fee(bytes)", [serializedTransaction]); + const data = optimismGasOracle.encodeFunctionData("getL1Fee(bytes)", [transaction]); const [result] = await fetchWithRetries< Array<{ diff --git a/libs/coin-modules/coin-evm/src/network/node/rpc.common.ts b/libs/coin-modules/coin-evm/src/network/node/rpc.common.ts index aad9d9d07a5a..11dd00fe157b 100644 --- a/libs/coin-modules/coin-evm/src/network/node/rpc.common.ts +++ b/libs/coin-modules/coin-evm/src/network/node/rpc.common.ts @@ -11,7 +11,6 @@ import OptimismGasPriceOracleAbi from "../../abis/optimismGasPriceOracle.abi.jso import ScrollGasPriceOracleAbi from "../../abis/scrollGasPriceOracle.abi.json"; import { getCoinConfig } from "../../config"; import { GasEstimationError, InsufficientFunds, UnsupportedRpcMethodError } from "../../errors"; -import { getSerializedTransaction } from "../../transaction"; import { FeeHistory } from "../../types"; import { safeEncodeEIP55, normalizeAddress } from "../../utils"; import { hasErrorCode, isUnsupportedRpcMethodError } from "./rpc.errors"; @@ -468,25 +467,7 @@ export const getOptimismAdditionalFees: NodeApi["getOptimismAdditionalFees"] = m } // Fake signature is added to get the best approximation possible for the gas on L1 - const serializedTransaction = - typeof transaction === "string" - ? transaction - : ((): string | null => { - try { - return getSerializedTransaction(transaction, { - r: "0xffffffffffffffffffffffffffffffffffffffff", - s: "0xffffffffffffffffffffffffffffffffffffffff", - v: 27, - }); - } catch (error) /* istanbul ignore next: just logs */ { - log("coin-evm", "getOptimismAdditionalFees: Transaction serializing failed", { - error, - }); - return null; - } - })(); - - if (!serializedTransaction) { + if (!transaction) { return new BigNumber(0); } @@ -497,22 +478,11 @@ export const getOptimismAdditionalFees: NodeApi["getOptimismAdditionalFees"] = m OptimismGasPriceOracleAbi, api, ); - const additionalL1Fees = await optimismGasOracle.getL1Fee(serializedTransaction); + const additionalL1Fees = await optimismGasOracle.getL1Fee(transaction); return new BigNumber(additionalL1Fees.toString()); }), (currency, transaction) => { - const serializedTransaction = - typeof transaction === "string" - ? transaction - : ((): string | null => { - try { - return getSerializedTransaction(transaction); - } catch { - return null; - } - })(); - - return "getOptimismL1BaseFee_" + currency.id + "_" + serializedTransaction; + return "getOptimismL1BaseFee_" + currency.id + "_" + transaction; }, { ttl: 15 * 1000 }, // preventing rate limit by caching this for at least 15sec ); @@ -535,24 +505,7 @@ export const getScrollAdditionalFees: NodeApi["getScrollAdditionalFees"] = ( return new BigNumber(0); } - // Fake signature is added to get the best approximation possible for the gas on L1 - const serializedTransaction = - typeof transaction === "string" - ? transaction - : ((): string | null => { - try { - return getSerializedTransaction(transaction, { - r: "0xffffffffffffffffffffffffffffffffffffffff", - s: "0xffffffffffffffffffffffffffffffffffffffff", - v: 27, - }); - } catch (error) /* istanbul ignore next: just logs */ { - log("coin-evm", "getScrollAdditionalFees: Transaction serializing failed", { error }); - return null; - } - })(); - - if (!serializedTransaction) { + if (!transaction) { return new BigNumber(0); } @@ -563,7 +516,7 @@ export const getScrollAdditionalFees: NodeApi["getScrollAdditionalFees"] = ( ScrollGasPriceOracleAbi, api, ); - const additionalL1Fees = await scrollGasOracle.getL1Fee(serializedTransaction); + const additionalL1Fees = await scrollGasOracle.getL1Fee(transaction); return new BigNumber(additionalL1Fees.toString()); }); diff --git a/libs/coin-modules/coin-evm/src/network/node/rpc.test.ts b/libs/coin-modules/coin-evm/src/network/node/rpc.test.ts index 377244b5d93b..b7df7c14d4fa 100644 --- a/libs/coin-modules/coin-evm/src/network/node/rpc.test.ts +++ b/libs/coin-modules/coin-evm/src/network/node/rpc.test.ts @@ -2,15 +2,17 @@ import { AssertionError, fail } from "assert"; import { delay } from "@ledgerhq/live-promise"; import { CryptoCurrency, CryptoCurrencyId, EthereumLikeInfo } from "@ledgerhq/types-cryptoassets"; import BigNumber from "bignumber.js"; -import { JsonRpcProvider, TransactionReceipt, TransactionResponse, ethers } from "ethers"; +import { + JsonRpcProvider, + Transaction, + TransactionReceipt, + TransactionResponse, + ethers, +} from "ethers"; import { getCoinConfig } from "../../config"; import { GasEstimationError, InsufficientFunds, UnsupportedRpcMethodError } from "../../errors"; import { makeAccount } from "../../fixtures/common.fixtures"; -import { - EvmTransactionLegacy, - Transaction as EvmTransaction, - EvmTransactionEIP1559, -} from "../../types"; +import { EvmTransactionLegacy, EvmTransactionEIP1559 } from "../../types"; import * as RPC_API from "./rpc.common"; jest.useFakeTimers(); @@ -802,19 +804,22 @@ describe("EVM Family", () => { expect( await RPC_API.getOptimismAdditionalFees( { ...fakeCurrency, id: "optimism" } as CryptoCurrency, - { - mode: "send", - family: "evm", - recipient: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", - maxFeePerGas: new BigNumber("0x777159126"), - maxPriorityFeePerGas: new BigNumber("0x10c388d00"), - amount: new BigNumber("0x38d7ea4c68000"), - gasLimit: new BigNumber(0), - data: Buffer.from(""), + // Build a serialized transaction, the exact same way we do in `estimateFees` + Transaction.from({ + to: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", + maxFeePerGas: BigInt(new BigNumber("0x777159126").toFixed()), + maxPriorityFeePerGas: BigInt(new BigNumber("0x10c388d00").toFixed()), + value: BigInt(new BigNumber("0x38d7ea4c68000").toFixed()), + gasLimit: 0n, type: 2, chainId: 1, nonce: 52, - } as EvmTransaction, + signature: { + r: "0xffffffffffffffffffffffffffffffffffffffff", + s: "0xffffffffffffffffffffffffffffffffffffffff", + v: 27, + }, + }).serialized, ), ).toEqual(new BigNumber(42069)); }); @@ -823,40 +828,22 @@ describe("EVM Family", () => { expect( await RPC_API.getOptimismAdditionalFees( fakeCurrency as CryptoCurrency, - { - mode: "send", - family: "evm", - recipient: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", - maxFeePerGas: new BigNumber("0x777159126"), - maxPriorityFeePerGas: new BigNumber("0x10c388d00"), - amount: new BigNumber("0x38d7ea4c68000"), - gasLimit: new BigNumber(0), - data: Buffer.from(""), - type: 2, - chainId: 1, - nonce: 52, - } as EvmTransaction, - ), - ).toEqual(new BigNumber(0)); - }); - - it("should return 0 if the transaction is invalid", async () => { - expect( - await RPC_API.getOptimismAdditionalFees( - fakeCurrency as CryptoCurrency, - { - mode: "send", - family: "evm", - recipient: "", // no recipient for example - maxFeePerGas: new BigNumber("0x777159126"), - maxPriorityFeePerGas: new BigNumber("0x10c388d00"), - amount: new BigNumber("0x38d7ea4c68000"), - gasLimit: new BigNumber(0), - data: Buffer.from(""), + // Build a serialized transaction, the exact same way we do in `estimateFees` + Transaction.from({ + to: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", + maxFeePerGas: BigInt(new BigNumber("0x777159126").toFixed()), + maxPriorityFeePerGas: BigInt(new BigNumber("0x10c388d00").toFixed()), + value: BigInt(new BigNumber("0x38d7ea4c68000").toFixed()), + gasLimit: 0n, type: 2, chainId: 1, nonce: 52, - } as EvmTransaction, + signature: { + r: "0xffffffffffffffffffffffffffffffffffffffff", + s: "0xffffffffffffffffffffffffffffffffffffffff", + v: 27, + }, + }).serialized, ), ).toEqual(new BigNumber(0)); }); @@ -867,19 +854,22 @@ describe("EVM Family", () => { expect( await RPC_API.getScrollAdditionalFees( { ...fakeCurrency, id: "scroll" } as CryptoCurrency, - { - mode: "send", - family: "evm", - recipient: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", - maxFeePerGas: new BigNumber("0x777159126"), - maxPriorityFeePerGas: new BigNumber("0x10c388d00"), - amount: new BigNumber("0x38d7ea4c68000"), - gasLimit: new BigNumber(0), - data: Buffer.from(""), + // Build a serialized transaction, the exact same way we do in `estimateFees` + Transaction.from({ + to: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", + maxFeePerGas: BigInt(new BigNumber("0x777159126").toFixed()), + maxPriorityFeePerGas: BigInt(new BigNumber("0x10c388d00").toFixed()), + value: BigInt(new BigNumber("0x38d7ea4c68000").toFixed()), + gasLimit: 0n, type: 2, chainId: 1, nonce: 52, - } as EvmTransaction, + signature: { + r: "0xffffffffffffffffffffffffffffffffffffffff", + s: "0xffffffffffffffffffffffffffffffffffffffff", + v: 27, + }, + }).serialized, ), ).toEqual(new BigNumber(42069)); }); @@ -888,40 +878,22 @@ describe("EVM Family", () => { expect( await RPC_API.getScrollAdditionalFees( fakeCurrency as CryptoCurrency, - { - mode: "send", - family: "evm", - recipient: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", - maxFeePerGas: new BigNumber("0x777159126"), - maxPriorityFeePerGas: new BigNumber("0x10c388d00"), - amount: new BigNumber("0x38d7ea4c68000"), - gasLimit: new BigNumber(0), - data: Buffer.from(""), - type: 2, - chainId: 1, - nonce: 52, - } as EvmTransaction, - ), - ).toEqual(new BigNumber(0)); - }); - - it("should return 0 if the transaction is invalid", async () => { - expect( - await RPC_API.getScrollAdditionalFees( - fakeCurrency as CryptoCurrency, - { - mode: "send", - family: "evm", - recipient: "", // no recipient for example - maxFeePerGas: new BigNumber("0x777159126"), - maxPriorityFeePerGas: new BigNumber("0x10c388d00"), - amount: new BigNumber("0x38d7ea4c68000"), - gasLimit: new BigNumber(0), - data: Buffer.from(""), + // Build a serialized transaction, the exact same way we do in `estimateFees` + Transaction.from({ + to: "0xc2907efcce4011c491bbeda8a0fa63ba7aab596c", + maxFeePerGas: BigInt(new BigNumber("0x777159126").toFixed()), + maxPriorityFeePerGas: BigInt(new BigNumber("0x10c388d00").toFixed()), + value: BigInt(new BigNumber("0x38d7ea4c68000").toFixed()), + gasLimit: 0n, type: 2, chainId: 1, nonce: 52, - } as EvmTransaction, + signature: { + r: "0xffffffffffffffffffffffffffffffffffffffff", + s: "0xffffffffffffffffffffffffffffffffffffffff", + v: 27, + }, + }).serialized, ), ).toEqual(new BigNumber(0)); }); diff --git a/libs/coin-modules/coin-evm/src/network/node/types.ts b/libs/coin-modules/coin-evm/src/network/node/types.ts index 90ea7422fd77..568fc39ebe1c 100644 --- a/libs/coin-modules/coin-evm/src/network/node/types.ts +++ b/libs/coin-modules/coin-evm/src/network/node/types.ts @@ -107,14 +107,8 @@ export type NodeApi = { currency: CryptoCurrency, blockHeight: number | "latest", ) => Promise; - getOptimismAdditionalFees: ( - currency: CryptoCurrency, - transaction: EvmTransaction | string, - ) => Promise; - getScrollAdditionalFees: ( - currency: CryptoCurrency, - transaction: EvmTransaction | string, - ) => Promise; + getOptimismAdditionalFees: (currency: CryptoCurrency, transaction: string) => Promise; + getScrollAdditionalFees: (currency: CryptoCurrency, transaction: string) => Promise; }; type NodeConfig = EvmConfigInfo["node"]; diff --git a/libs/coin-modules/coin-evm/src/transaction.test.ts b/libs/coin-modules/coin-evm/src/transaction.test.ts index 9fa24bcfbecc..574347d997b0 100644 --- a/libs/coin-modules/coin-evm/src/transaction.test.ts +++ b/libs/coin-modules/coin-evm/src/transaction.test.ts @@ -1,13 +1,7 @@ import { toErrorRaw } from "@ledgerhq/coin-framework/lib/serialization/transaction"; import BigNumber from "bignumber.js"; -import { ethers } from "ethers"; -import { transactionToEthersTransaction } from "./adapters"; import { - account, eip1559Tx, - erc1155Transaction, - erc1155TransactionNonFinite, - erc721Transaction, legacyTx, nftEip1559tx, nftLegacyTx, @@ -15,15 +9,10 @@ import { rawEip1559Tx, rawLegacyTx, rawNftEip1559Tx, - testData, - tokenTransaction, } from "./fixtures/transaction.fixtures"; -import { getTransactionCount } from "./network/node/rpc.common"; import { fromTransactionRaw, fromTransactionStatusRaw, - getSerializedTransaction, - getTransactionData, getTypedTransaction, toTransactionRaw, toTransactionStatusRaw, @@ -35,7 +24,6 @@ import { FeeData, } from "./types"; -const mockGetTransactionCount = getTransactionCount as jest.Mock; jest.mock("./network/node/rpc.common", () => ({ getTransactionCount: jest.fn(), })); @@ -225,222 +213,6 @@ describe("EVM Family", () => { }); }); - describe("getTransactionData", () => { - it("should return the data for an ERC20 transaction", () => { - expect(getTransactionData(account, tokenTransaction)).toEqual( - Buffer.from( - // using transfer method to 0x51DF0aF74a0DBae16cB845B46dAF2a35cB1D4168 & value is 0x64 (100) - "a9059cbb00000000000000000000000051df0af74a0dbae16cb845b46daf2a35cb1d41680000000000000000000000000000000000000000000000000000000000000064", - "hex", - ), - ); - }); - - it("should return the data for an ERC721 transaction", () => { - expect(getTransactionData(account, erc721Transaction)).toEqual( - Buffer.from( - // using safeTransferFrom method from 0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d to 0x51DF0aF74a0DBae16cB845B46dAF2a35cB1D4168 & tokenId is 1 (0x01) - "b88d4fde0000000000000000000000006cbcd73cd8e8a42844662f0a0e76d7f79afd933d00000000000000000000000051df0af74a0dbae16cb845b46daf2a35cb1d4168000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000", - "hex", - ), - ); - }); - - it("should return the data for an ERC1155 transaction", () => { - expect(getTransactionData(account, erc1155Transaction)).toEqual( - Buffer.from( - // using safeTransferFrom method from 0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d to 0x51DF0aF74a0DBae16cB845B46dAF2a35cB1D4168, tokenId is 1 (0x01) & quantity is 10 (0x0a) - "f242432a0000000000000000000000006cbcd73cd8e8a42844662f0a0e76d7f79afd933d00000000000000000000000051df0af74a0dbae16cb845b46daf2a35cb1d41680000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000", - "hex", - ), - ); - }); - - it("should return the data for an ERC1155 transaction even if the quantity is Infinity or NaN", () => { - expect(getTransactionData(account, erc1155TransactionNonFinite)).toEqual( - Buffer.from( - // using safeTransferFrom method from 0x6cBCD73CD8e8a42844662f0A0e76D7F79Afd933d to 0x51DF0aF74a0DBae16cB845B46dAF2a35cB1D4168, tokenId is 1 (0x01) & quantity is 0 (0x00) - "f242432a0000000000000000000000006cbcd73cd8e8a42844662f0a0e76d7f79afd933d00000000000000000000000051df0af74a0dbae16cb845b46daf2a35cb1d41680000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000000", - "hex", - ), - ); - }); - }); - - describe("transactionToEthersTransaction", () => { - describe("without customGasLimit", () => { - it("should build convert an EIP1559 ledger live transaction to an ethers transaction", () => { - const ethers1559Tx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: BigInt(21000), - data: "0x" + testData, - value: BigInt(100), - chainId: BigInt(1), - type: 2, - maxFeePerGas: BigInt(10000), - maxPriorityFeePerGas: BigInt(10000), - }; - - expect(transactionToEthersTransaction(eip1559Tx)).toEqual(ethers1559Tx); - }); - - it("should build convert an legacy ledger live transaction to an ethers transaction", () => { - const legacyEthersTx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: BigInt(21000), - data: "0x" + testData, - value: BigInt(100), - chainId: BigInt(1), - type: 0, - gasPrice: BigInt(10000), - }; - - expect(transactionToEthersTransaction(legacyTx)).toEqual(legacyEthersTx); - }); - }); - - describe("with customGasLimit", () => { - it("should build convert an EIP1559 ledger live transaction to an ethers transaction", () => { - const ethers1559Tx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: BigInt(22000), - data: "0x" + testData, - value: BigInt(100), - chainId: BigInt(1), - type: 2, - maxFeePerGas: BigInt(10000), - maxPriorityFeePerGas: BigInt(10000), - }; - - expect( - transactionToEthersTransaction({ ...eip1559Tx, customGasLimit: new BigNumber(22000) }), - ).toEqual(ethers1559Tx); - }); - - it("should build convert an legacy ledger live transaction to an ethers transaction", () => { - const legacyEthersTx: ethers.TransactionLike = { - to: "0xkvn", - nonce: 0, - gasLimit: BigInt(22000), - data: "0x" + testData, - value: BigInt(100), - chainId: BigInt(1), - type: 0, - gasPrice: BigInt(10000), - }; - - expect( - transactionToEthersTransaction({ ...legacyTx, customGasLimit: new BigNumber(22000) }), - ).toEqual(legacyEthersTx); - }); - }); - }); - - describe("getSerializedTransaction", () => { - beforeAll(() => { - mockGetTransactionCount.mockImplementation(() => Promise.resolve(0)); - }); - - describe("without customGasLimit", () => { - it("should serialize a type 0 transaction", async () => { - const transactionLegacy: EvmTransaction = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0x6775e49108cb77cda06Fc3BEF51bcD497602aD88", // obama.eth - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: 0, - gasLimit: new BigNumber(21000), - chainId: 1, - gasPrice: new BigNumber(100), - type: 0, - }; - const serializedTx = await getSerializedTransaction(transactionLegacy); - - expect(serializedTx).toBe( - "0xdf8064825208946775e49108cb77cda06fc3bef51bcd497602ad886480018080", - ); - }); - - it("should serialize a type 2 transaction", async () => { - const transactionEIP1559: EvmTransaction = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0x6775e49108cb77cda06Fc3BEF51bcD497602aD88", // obama.eth - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: 0, - gasLimit: new BigNumber(21000), - chainId: 1, - maxFeePerGas: new BigNumber(100), - maxPriorityFeePerGas: new BigNumber(100), - type: 2, - }; - const serializedTx = await getSerializedTransaction(transactionEIP1559); - - expect(serializedTx).toBe( - "0x02df01806464825208946775e49108cb77cda06fc3bef51bcd497602ad886480c0", - ); - }); - }); - - describe("with customGasLimit", () => { - it("should serialize a type 0 transaction", async () => { - const transactionLegacy: EvmTransaction = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0x6775e49108cb77cda06Fc3BEF51bcD497602aD88", // obama.eth - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: 0, - gasLimit: new BigNumber(21000), - customGasLimit: new BigNumber(22000), - chainId: 1, - gasPrice: new BigNumber(100), - type: 0, - }; - const serializedTx = await getSerializedTransaction(transactionLegacy); - - expect(serializedTx).toBe( - "0xdf80648255f0946775e49108cb77cda06fc3bef51bcd497602ad886480018080", - ); - }); - - it("should serialize a type 2 transaction", async () => { - const transactionEIP1559: EvmTransaction = { - amount: new BigNumber(100), - useAllAmount: false, - subAccountId: "id", - recipient: "0x6775e49108cb77cda06Fc3BEF51bcD497602aD88", // obama.eth - feesStrategy: "custom", - family: "evm", - mode: "send", - nonce: 0, - gasLimit: new BigNumber(21000), - customGasLimit: new BigNumber(22000), - chainId: 1, - maxFeePerGas: new BigNumber(100), - maxPriorityFeePerGas: new BigNumber(100), - type: 2, - }; - const serializedTx = await getSerializedTransaction(transactionEIP1559); - - expect(serializedTx).toBe( - "0x02df018064648255f0946775e49108cb77cda06fc3bef51bcd497602ad886480c0", - ); - }); - }); - }); - describe("getTypedTransaction", () => { const getTransactionToType = (type: number): EvmTransaction => { if (type === 2) { diff --git a/libs/coin-modules/coin-evm/src/transaction.ts b/libs/coin-modules/coin-evm/src/transaction.ts index cc78b05b502a..babcc54c15b1 100644 --- a/libs/coin-modules/coin-evm/src/transaction.ts +++ b/libs/coin-modules/coin-evm/src/transaction.ts @@ -9,11 +9,6 @@ import { } from "@ledgerhq/coin-framework/serialization"; import type { Account } from "@ledgerhq/types-live"; import { BigNumber } from "bignumber.js"; -import { ethers, Transaction } from "ethers"; -import ERC1155ABI from "./abis/erc1155.abi.json"; -import ERC20ABI from "./abis/erc20.abi.json"; -import ERC721ABI from "./abis/erc721.abi.json"; -import { transactionToEthersTransaction } from "./adapters"; import type { Transaction as EvmTransaction, EvmTransactionEIP1559, @@ -203,53 +198,6 @@ export const toTransactionRaw = (tx: EvmTransaction): EvmTransactionRaw => { return txRaw as EvmTransactionRaw; }; -/** - * Returns the data necessary to execute smart contracts. - * As of now, only used to create ERC20 transfers' data - */ -export const getTransactionData = ( - account: Account, - transaction: EvmTransaction, -): Buffer | undefined => { - switch (transaction.mode) { - case "send": { - const contract = new ethers.Interface(ERC20ABI); - const data = contract.encodeFunctionData("transfer", [ - transaction.recipient, - transaction.amount.toFixed(), - ]); - - // removing 0x prefix - return Buffer.from(data.slice(2), "hex"); - } - case "erc721": { - const contract = new ethers.Interface(ERC721ABI); - const data = contract.encodeFunctionData("safeTransferFrom(address,address,uint256,bytes)", [ - account.freshAddress, - transaction.recipient, - transaction.nft.tokenId, - "0x", - ]); - - // removing 0x prefix - return Buffer.from(data.slice(2), "hex"); - } - case "erc1155": { - const contract = new ethers.Interface(ERC1155ABI); - const data = contract.encodeFunctionData("safeTransferFrom", [ - account.freshAddress, - transaction.recipient, - transaction.nft.tokenId, - transaction.nft.quantity.isFinite() ? transaction.nft.quantity.toFixed() : 0, - "0x", - ]); - - // removing 0x prefix - return Buffer.from(data.slice(2), "hex"); - } - } -}; - /** * Returns a transaction with the correct type and entries depending * on the network compatiblity. @@ -281,43 +229,6 @@ export const getTypedTransaction = ( } as EvmTransactionLegacy; }; -/** - * Serialize a Ledger Live transaction into an hex string - */ -export const getSerializedTransaction = ( - tx: EvmTransaction, - signature?: Partial, -): string => { - const unsignedEthersTransaction = transactionToEthersTransaction(tx); - - if (!isValidSignatureStructure(signature)) { - return Transaction.from(unsignedEthersTransaction).unsignedSerialized; - } - const txInput = { - ...unsignedEthersTransaction, - signature: { - r: signature?.r ?? "0x0000000000000000000000000000000000000000000000000000000000000000", // Provide default if r is undefined - s: signature?.s ?? "0x0000000000000000000000000000000000000000000000000000000000000000", // Provide default if s is undefined - v: signature?.v ?? 0, // Provide default if v is undefined - } as ethers.Signature, - }; - - const txSigned = Transaction.from(txInput); - - return txSigned.serialized; -}; - -const isValidSignatureStructure = (signature?: Partial): boolean => { - return ( - signature?.r !== null && - signature?.r !== undefined && - signature?.s !== null && - signature?.s !== undefined && - signature?.v !== null && - signature?.v !== undefined - ); -}; - export const fromTransactionStatusRaw = ( transactionStatusRaw: TransactionStatusRaw, ): TransactionStatus => { diff --git a/libs/coin-modules/coin-evm/src/types/erc20.ts b/libs/coin-modules/coin-evm/src/types/erc20.ts deleted file mode 100644 index ae33d8cfe6d3..000000000000 --- a/libs/coin-modules/coin-evm/src/types/erc20.ts +++ /dev/null @@ -1,15 +0,0 @@ -/** - * ERC20Token type definition - * Represents the structure of an ERC20 token definition - */ -export type ERC20Token = [ - string, // parent currency id - string, // token - string, // ticker - number, // precision - string, // name - string, // ledgerSignature - string, // contract address - false, // [deprecated] disabled counter values - boolean, // delisted -]; diff --git a/libs/coin-modules/coin-evm/src/types/index.ts b/libs/coin-modules/coin-evm/src/types/index.ts index 4441cca7de92..63d731ec2cbc 100644 --- a/libs/coin-modules/coin-evm/src/types/index.ts +++ b/libs/coin-modules/coin-evm/src/types/index.ts @@ -2,7 +2,6 @@ export * from "./assets"; export * from "./editTransaction"; -export * from "./erc20"; export * from "./etherscan"; export * from "./ledger"; export * from "./transaction"; diff --git a/libs/coin-modules/coin-evm/src/types/ledger.ts b/libs/coin-modules/coin-evm/src/types/ledger.ts index 86de7146bd59..46ca4732c23d 100644 --- a/libs/coin-modules/coin-evm/src/types/ledger.ts +++ b/libs/coin-modules/coin-evm/src/types/ledger.ts @@ -69,10 +69,3 @@ export type LedgerExplorerOperation = { time: string; }; }; - -export type LedgerExplorerBlock = { - hash: string; - height: number; - time: string; - txs: string[]; -};