From d70bb7042a01de2191b59337d8a1574e22bd8887 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Prohaszka?= <104785785+sprohaszka-ledger@users.noreply.github.com> Date: Wed, 7 Sep 2022 15:22:19 +0200 Subject: [PATCH] feat: Add EIP-712 management in prepareMessageToSign (#892) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: Add EIP-712 management in prepareMessageToSign Signed-off-by: Stéphane Prohaszka * fix: remove unused import Signed-off-by: Stéphane Prohaszka * fix: resolve import issue Signed-off-by: Stéphane Prohaszka * fix: change expected result of prepareMessage in case of thrown error Signed-off-by: Stéphane Prohaszka * feat: rollback on TypedMessageData properties Signed-off-by: Stéphane Prohaszka * fix: test with EIP712Message Signed-off-by: Stéphane Prohaszka * feat: add tests Signed-off-by: Stéphane Prohaszka * fix: move fixtures folder Signed-off-by: Stéphane Prohaszka * feat: update changelog Signed-off-by: Stéphane Prohaszka * chore: change test object name Signed-off-by: Stéphane Prohaszka * fix: Update preparedMessageType in mobile Signed-off-by: Stéphane Prohaszka * fix: revert unintended modification Signed-off-by: Stéphane Prohaszka * chore: rename createCryptoCurrency into createFixtureCryptoCurrency to avoid confusion Signed-off-by: Stéphane Prohaszka * chore: extract some code to test it Signed-off-by: Stéphane Prohaszka * fix: eslint issue Signed-off-by: Stéphane Prohaszka * feat: review feedback Signed-off-by: Stéphane Prohaszka * fix: review feedback Signed-off-by: Stéphane Prohaszka * fix: rebase issue with mobile platform player Signed-off-by: Stéphane Prohaszka * chore: revert mobile package modification Signed-off-by: Stéphane Prohaszka * fix: unit test Signed-off-by: Stéphane Prohaszka * feat: simplify a little bit eth signMessage * fix: PR feedback Signed-off-by: Stéphane Prohaszka Signed-off-by: Stéphane Prohaszka --- .changeset/real-owls-repair.md | 6 + .../components/WebPlatformPlayer/index.tsx | 30 ++- .../WebPlatformPlayer/liveSDKLogic.test.ts | 177 ++++++++++++++++++ .../WebPlatformPlayer/liveSDKLogic.ts | 23 +++ libs/ledger-live-common/package.json | 2 +- .../src/families/bitcoin/hw-signMessage.ts | 6 +- .../families/ethereum/hw-signMessage.test.ts | 126 ++++++++++--- .../src/families/ethereum/hw-signMessage.ts | 84 +++++++-- .../src/families/ethereum/types.ts | 14 +- .../src/families/filecoin/hw-signMessage.ts | 6 +- .../src/hw/signMessage/index.test.ts | 93 ++++----- .../src/hw/signMessage/index.ts | 32 +++- .../src/hw/signMessage/types.ts | 5 +- .../src/walletconnect/index.ts | 14 +- .../src/walletconnect/walletconnect.test.ts | 2 + libs/ledgerjs/packages/hw-app-eth/src/Eth.ts | 3 +- .../src/modules/EIP712/EIP712.utils.ts | 14 +- .../hw-app-eth/src/modules/EIP712/index.ts | 1 + pnpm-lock.yaml | 8 +- 19 files changed, 505 insertions(+), 141 deletions(-) create mode 100644 .changeset/real-owls-repair.md create mode 100644 apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.test.ts create mode 100644 apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.ts diff --git a/.changeset/real-owls-repair.md b/.changeset/real-owls-repair.md new file mode 100644 index 000000000000..e99f0818133f --- /dev/null +++ b/.changeset/real-owls-repair.md @@ -0,0 +1,6 @@ +--- +"@ledgerhq/live-common": minor +"@ledgerhq/hw-app-eth": patch +--- + +Add EIP-712 capability when preparing message to sign diff --git a/apps/ledger-live-mobile/src/components/WebPlatformPlayer/index.tsx b/apps/ledger-live-mobile/src/components/WebPlatformPlayer/index.tsx index e999f43f7e75..b4b6447220b3 100644 --- a/apps/ledger-live-mobile/src/components/WebPlatformPlayer/index.tsx +++ b/apps/ledger-live-mobile/src/components/WebPlatformPlayer/index.tsx @@ -29,7 +29,6 @@ import type { RawPlatformAccount, } from "@ledgerhq/live-common/platform/rawTypes"; import { getEnv } from "@ledgerhq/live-common/env"; -import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; import { isTokenAccount, flattenAccounts, @@ -39,8 +38,9 @@ import { findCryptoCurrencyById, listAndFilterCurrencies, } from "@ledgerhq/live-common/currencies/index"; -import type { AppManifest } from "@ledgerhq/live-common/platform/types"; +import type { Transaction } from "@ledgerhq/live-common/generated/types"; import type { MessageData } from "@ledgerhq/live-common/hw/signMessage/types"; +import type { AppManifest } from "@ledgerhq/live-common/platform/types"; import { broadcastTransactionLogic, receiveOnAccountLogic, @@ -49,7 +49,6 @@ import { CompleteExchangeUiRequest, signMessageLogic, } from "@ledgerhq/live-common/platform/logic"; - import { useJSONRPCServer } from "@ledgerhq/live-common/platform/JSONRPCServer"; import { accountToPlatformAccount } from "@ledgerhq/live-common/platform/converters"; import { @@ -70,6 +69,7 @@ import UpdateIcon from "../../icons/Update"; import InfoIcon from "../../icons/Info"; import InfoPanel from "./InfoPanel"; import { track } from "../../analytics/segment"; +import prepareSignTransaction from "./liveSDKLogic"; const tracking = trackingWrapper(track); @@ -297,20 +297,16 @@ const WebPlatformPlayer = ({ manifest, inputs }: Props) => { { manifest, accounts, tracking }, accountId, transaction, - (account: AccountLike, parentAccount: Account | null, { liveTx }) => { - const { recipient, ...txData } = liveTx; - - const bridge = getAccountBridge(account, parentAccount); - const t = bridge.createTransaction(account); - const t2 = bridge.updateTransaction(t, { - recipient, - subAccountId: isTokenAccount(account) ? account.id : undefined, - }); - - const tx = bridge.updateTransaction(t2, { - userGasLimit: txData.gasLimit, - ...txData, - }); + ( + account: AccountLike, + parentAccount: Account | null, + { + liveTx, + }: { + liveTx: Partial; + }, + ) => { + const tx = prepareSignTransaction(account, parentAccount, liveTx); return new Promise((resolve, reject) => { navigation.navigate(NavigatorName.SignTransaction, { diff --git a/apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.test.ts b/apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.test.ts new file mode 100644 index 000000000000..cb255fbfd5a2 --- /dev/null +++ b/apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.test.ts @@ -0,0 +1,177 @@ +import BigNumber from "bignumber.js"; +import { CryptoCurrency, TokenCurrency } from "@ledgerhq/types-cryptoassets"; +import { Account, TokenAccount } from "@ledgerhq/types-live"; +import { PlatformTransaction } from "@ledgerhq/live-common/platform/types"; +import { Transaction } from "@ledgerhq/live-common/generated/types"; +import prepareSignTransaction from "./liveSDKLogic"; + +// Fake the support of the test currency +jest.mock("@ledgerhq/live-common/currencies/support", () => ({ + isCurrencySupported: () => true, +})); + +describe("prepareSignTransaction", () => { + it("returns a Transaction", () => { + // Given + const parentAccount = createAccount("12"); + const childAccount = createTokenAccount( + "22", + "ethereumjs:2:ethereum:0x012:", + ); + const expectedResult = { + amount: new BigNumber("1000"), + data: Buffer.from([]), + estimatedGasLimit: null, + family: "ethereum", + feeCustomUnit: { code: "Gwei", magnitude: 9, name: "Gwei" }, + feesStrategy: "medium", + gasPrice: new BigNumber("700000"), + gasLimit: new BigNumber("1200000"), + userGasLimit: new BigNumber("1200000"), + mode: "send", + networkInfo: null, + nonce: 8, + recipient: "0x0123456", + subAccountId: "ethereumjs:2:ethereum:0x022:", + useAllAmount: false, + }; + + // When + const result = prepareSignTransaction( + childAccount, + parentAccount, + createEtherumTransaction() as Partial, + ); + + // Then + expect(result).toEqual(expectedResult); + }); +}); + +// *** UTIL FUNCTIONS *** +function createEtherumTransaction(): PlatformTransaction { + return { + family: "ethereum" as any, + amount: new BigNumber("1000"), + recipient: "0x0123456", + nonce: 8, + data: Buffer.from("Some data...", "hex"), + gasPrice: new BigNumber("700000"), + gasLimit: new BigNumber("1200000"), + }; +} + +const createCryptoCurrency = (family: string): CryptoCurrency => ({ + type: "CryptoCurrency", + id: "testCoinId", + coinType: 8008, + name: "MyCoin", + managerAppName: "MyCoin", + ticker: "MYC", + countervalueTicker: "MYC", + scheme: "mycoin", + color: "#ff0000", + family, + units: [ + { + name: "MYC", + code: "MYC", + magnitude: 8, + }, + { + name: "SmallestUnit", + code: "SMALLESTUNIT", + magnitude: 0, + }, + ], + explorerViews: [ + { + address: "https://mycoinexplorer.com/account/$address", + tx: "https://mycoinexplorer.com/transaction/$hash", + token: "https://mycoinexplorer.com/token/$contractAddress/?a=$address", + }, + ], +}); + +const defaultEthCryptoFamily = createCryptoCurrency("ethereum"); +const createAccount = ( + id: string, + crypto: CryptoCurrency = defaultEthCryptoFamily, +): Account => ({ + type: "Account", + id: `ethereumjs:2:ethereum:0x0${id}:`, + seedIdentifier: "0x01", + derivationMode: "ethM", + index: 0, + freshAddress: "0x01", + freshAddressPath: "44'/60'/0'/0/0", + freshAddresses: [], + name: "Ethereum 1", + starred: false, + used: false, + balance: new BigNumber("51281813126095913"), + spendableBalance: new BigNumber("51281813126095913"), + creationDate: new Date(), + blockHeight: 8168983, + currency: crypto, + unit: { + name: "satoshi", + code: "BTC", + magnitude: 5, + }, + operationsCount: 0, + operations: [], + pendingOperations: [], + lastSyncDate: new Date(), + balanceHistoryCache: { + HOUR: { + balances: [], + latestDate: undefined, + }, + DAY: { + balances: [], + latestDate: undefined, + }, + WEEK: { + balances: [], + latestDate: undefined, + }, + }, + swapHistory: [], +}); + +function createTokenAccount(id = "32", parentId = "whatever"): TokenAccount { + return { + type: "TokenAccount", + id: `ethereumjs:2:ethereum:0x0${id}:`, + parentId, + token: createTokenCurrency(), + balance: new BigNumber(0), + spendableBalance: new BigNumber(0), + creationDate: new Date(), + operationsCount: 0, + operations: [], + pendingOperations: [], + starred: false, + balanceHistoryCache: { + WEEK: { latestDate: null, balances: [] }, + HOUR: { latestDate: null, balances: [] }, + DAY: { latestDate: null, balances: [] }, + }, + swapHistory: [], + }; +} + +function createTokenCurrency(): TokenCurrency { + return { + type: "TokenCurrency", + id: "3", + contractAddress: "", + parentCurrency: defaultEthCryptoFamily, + tokenType: "", + // -- CurrencyCommon + name: "", + ticker: "", + units: [], + }; +} diff --git a/apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.ts b/apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.ts new file mode 100644 index 000000000000..15e40e885bde --- /dev/null +++ b/apps/ledger-live-mobile/src/components/WebPlatformPlayer/liveSDKLogic.ts @@ -0,0 +1,23 @@ +import { isTokenAccount } from "@ledgerhq/live-common/account/index"; +import { getAccountBridge } from "@ledgerhq/live-common/bridge/index"; +import { Transaction } from "@ledgerhq/live-common/lib/generated/types"; +import { Account, AccountLike, TransactionCommon } from "@ledgerhq/types-live"; + +export default function prepareSignTransaction( + account: AccountLike, + parentAccount: Account | null, + liveTx: Partial, +): TransactionCommon { + const bridge = getAccountBridge(account, parentAccount); + const t = bridge.createTransaction(account); + const { recipient, ...txData } = liveTx; + const t2 = bridge.updateTransaction(t, { + recipient, + subAccountId: isTokenAccount(account) ? account.id : undefined, + }); + + return bridge.updateTransaction(t2, { + userGasLimit: txData.gasLimit, + ...txData, + }); +} diff --git a/libs/ledger-live-common/package.json b/libs/ledger-live-common/package.json index 48b5c1a64bcf..5bd065e078e5 100644 --- a/libs/ledger-live-common/package.json +++ b/libs/ledger-live-common/package.json @@ -142,7 +142,7 @@ "@ledgerhq/hw-transport-mocker": "workspace:^", "@ledgerhq/hw-transport-node-speculos": "workspace:^", "@ledgerhq/json-bignumber": "^1.1.0", - "@ledgerhq/live-app-sdk": "^0.7.0", + "@ledgerhq/live-app-sdk": "^0.8.1", "@ledgerhq/logs": "workspace:^", "@polkadot/types": "8.12.2", "@polkadot/types-known": "8.12.2", diff --git a/libs/ledger-live-common/src/families/bitcoin/hw-signMessage.ts b/libs/ledger-live-common/src/families/bitcoin/hw-signMessage.ts index a84374fc6368..0e3252b12b45 100644 --- a/libs/ledger-live-common/src/families/bitcoin/hw-signMessage.ts +++ b/libs/ledger-live-common/src/families/bitcoin/hw-signMessage.ts @@ -1,7 +1,7 @@ import Btc from "@ledgerhq/hw-app-btc"; -import type { Resolver } from "../../hw/signMessage/types"; +import type { SignMessage } from "../../hw/signMessage/types"; -const resolver: Resolver = async (transport, { path, message }) => { +const signMessage: SignMessage = async (transport, { path, message }) => { const btc = new Btc(transport); const hexMessage = Buffer.from(message).toString("hex"); const result = await btc.signMessageNew(path, hexMessage); @@ -16,4 +16,4 @@ const resolver: Resolver = async (transport, { path, message }) => { }; }; -export default resolver; +export default { signMessage }; diff --git a/libs/ledger-live-common/src/families/ethereum/hw-signMessage.test.ts b/libs/ledger-live-common/src/families/ethereum/hw-signMessage.test.ts index 18e7afd99907..48a21e836bad 100644 --- a/libs/ledger-live-common/src/families/ethereum/hw-signMessage.test.ts +++ b/libs/ledger-live-common/src/families/ethereum/hw-signMessage.test.ts @@ -1,8 +1,10 @@ import "../../__tests__/test-helpers/setup"; -import EIP712Message from "@ledgerhq/hw-app-eth/tests/sample-messages/0.json"; -import hwSignMessage from "./hw-signMessage"; +import { EIP712Message } from "@ledgerhq/hw-app-eth/lib/modules/EIP712"; +import testEIP712Message from "@ledgerhq/hw-app-eth/tests/sample-messages/0.json"; +import ethSign from "./hw-signMessage"; import { setEnv } from "../../env"; +import { createFixtureCryptoCurrency } from "../../mock/fixtures/cryptoCurrencies"; const signPersonalMessage = jest.fn(() => Promise.resolve({ @@ -25,14 +27,81 @@ const signEIP712Message = jest.fn(() => v: 28, }) ); +// We only need to mock the defaut class returned jest.mock("@ledgerhq/hw-app-eth", () => { - return class { - signPersonalMessage = signPersonalMessage; - signEIP712HashedMessage = signEIP712HashedMessage; - signEIP712Message = signEIP712Message; + const originalModule = jest.requireActual("@ledgerhq/hw-app-eth"); + return { + ...originalModule, + default: class { + signPersonalMessage = signPersonalMessage; + signEIP712HashedMessage = signEIP712HashedMessage; + signEIP712Message = signEIP712Message; + }, }; }); +describe("prepareMessageToSign", () => { + it("returns a MessageData object when message to sign is a simple string", () => { + // Given + const currency = createFixtureCryptoCurrency("ethereum"); + const path = "44'/60'/0'/0/0"; + const derivationMode = "ethM"; + const message = "4d6573736167652064652074657374"; + const expectedRawMessage = "0x4d6573736167652064652074657374"; + + // When + const result = ethSign.prepareMessageToSign( + currency, + path, + derivationMode, + message + ); + + // Then + expect(result).toEqual({ + currency, + path: "44'/60'/0'/0/0", + derivationMode: "ethM", + message: "Message de test", + rawMessage: expectedRawMessage, + }); + }); + + it("returns a TypedMessageData object when message to sign is an EIP712Message", () => { + // Given + const currency = createFixtureCryptoCurrency("ethereum"); + const path = "44'/60'/0'/0/0"; + const derivationMode = "ethM"; + // testEIP712MessageHex + const message = + "7b0d0a2020202022646f6d61696e223a207b0d0a202020202020202022636861696e4964223a20352c0d0a2020202020202020226e616d65223a20224574686572204d61696c222c0d0a202020202020202022766572696679696e67436f6e7472616374223a2022307843634343636363634343434363434343434343634363436363436343434363436363636363636343222c0d0a20202020202020202276657273696f6e223a202231220d0a202020207d2c0d0a20202020226d657373616765223a207b0d0a202020202020202022636f6e74656e7473223a202248656c6c6f2c20426f6221222c0d0a20202020202020202266726f6d223a207b0d0a202020202020202020202020226e616d65223a2022436f77222c0d0a2020202020202020202020202277616c6c657473223a205b0d0a2020202020202020202020202020202022307843443261336439463933384531334344393437456330354162433746453733344466384444383236222c0d0a2020202020202020202020202020202022307844656144626565666445416462656566644561646245454664656164626545466445614462656546220d0a2020202020202020202020205d0d0a20202020202020207d2c0d0a202020202020202022746f223a207b0d0a202020202020202020202020226e616d65223a2022426f62222c0d0a2020202020202020202020202277616c6c657473223a205b0d0a2020202020202020202020202020202022307862426242424242626242424262626242626242626262624242624262626262426242626242426242222c0d0a2020202020202020202020202020202022307842304264614265613537423042444142654135376230626441424541353762304244616245613537222c0d0a2020202020202020202020202020202022307842304230623062306230623042303030303030303030303030303030303030303030303030303030220d0a2020202020202020202020205d0d0a20202020202020207d0d0a202020207d2c0d0a20202020227072696d61727954797065223a20224d61696c222c0d0a20202020227479706573223a207b0d0a202020202020202022454950373132446f6d61696e223a205b0d0a2020202020202020202020207b20226e616d65223a20226e616d65222c202274797065223a2022737472696e6722207d2c0d0a2020202020202020202020207b20226e616d65223a202276657273696f6e222c202274797065223a2022737472696e6722207d2c0d0a2020202020202020202020207b20226e616d65223a2022636861696e4964222c202274797065223a202275696e7432353622207d2c0d0a2020202020202020202020207b20226e616d65223a2022766572696679696e67436f6e7472616374222c202274797065223a20226164647265737322207d0d0a20202020202020205d2c0d0a2020202020202020224d61696c223a205b0d0a2020202020202020202020207b20226e616d65223a202266726f6d222c202274797065223a2022506572736f6e22207d2c0d0a2020202020202020202020207b20226e616d65223a2022746f222c202274797065223a2022506572736f6e22207d2c0d0a2020202020202020202020207b20226e616d65223a2022636f6e74656e7473222c202274797065223a2022737472696e6722207d0d0a20202020202020205d2c0d0a202020202020202022506572736f6e223a205b0d0a2020202020202020202020207b20226e616d65223a20226e616d65222c202274797065223a2022737472696e6722207d2c0d0a2020202020202020202020207b20226e616d65223a202277616c6c657473222c202274797065223a2022616464726573735b5d22207d0d0a20202020202020205d0d0a202020207d0d0a7d0d0a"; + + // When + const result = ethSign.prepareMessageToSign( + currency, + path, + derivationMode, + message + ); + + // Then + expect(result).toEqual({ + currency, + path, + derivationMode, + message: testEIP712Message, + rawMessage: "0x" + message, + hashes: { + stringHash: "", + domainHash: + "0x6137beb405d9ff777172aa879e33edb34a1460e701802746c5ef96e741710e59", + messageHash: + "0x5476346eb09179b1f7d245c11a27ae6c498a419d7fad4d984d5b1e0ad081662a", + }, + }); + }); +}); + describe("Eth hw-signMessage", () => { beforeEach(() => { jest.clearAllMocks(); @@ -40,7 +109,7 @@ describe("Eth hw-signMessage", () => { describe("parsing", () => { it("should be using the signPersonalMessage method with string message", async () => { - await hwSignMessage({} as any, { + await ethSign.signMessage({} as any, { path: "", message: "test", rawMessage: "0xtest", @@ -50,32 +119,47 @@ describe("Eth hw-signMessage", () => { }); it("should be using the signEIP712HashedMessage method with stringified message", async () => { - await hwSignMessage({} as any, { + await ethSign.signMessage({} as any, { path: "", - message: JSON.stringify(EIP712Message), + message: JSON.stringify(testEIP712Message), rawMessage: "0xtest", }); expect(signEIP712HashedMessage).toHaveBeenCalledTimes(1); }); - it("should be using the signEIP712Message method with stringified message", async () => { - setEnv("EXPERIMENTAL_EIP712", true); - - await hwSignMessage({} as any, { + it("should be using the signEIP712HashedMessage method with EIP712Message message", async () => { + await ethSign.signMessage({} as any, { path: "", - message: EIP712Message, + message: testEIP712Message as EIP712Message, rawMessage: "0xtest", }); - expect(signEIP712Message).toHaveBeenCalledTimes(1); - setEnv("EXPERIMENTAL_EIP712", false); + expect(signEIP712HashedMessage).toHaveBeenCalledTimes(1); + }); + + describe("when EXPERIMENTAL_EIP712 env variable is set to true", () => { + beforeAll(() => { + setEnv("EXPERIMENTAL_EIP712", true); + }); + + afterAll(() => { + setEnv("EXPERIMENTAL_EIP712", false); + }); + + it("should be using the signEIP712Message method with EIP712Message message", async () => { + await ethSign.signMessage({} as any, { + path: "", + message: testEIP712Message as EIP712Message, + rawMessage: "0xtest", + }); + }); }); }); describe("value of v", () => { it("should returning parity for signPersonalMessage", async () => { - const { rsv } = await hwSignMessage({} as any, { + const { rsv } = await ethSign.signMessage({} as any, { path: "", message: "test", rawMessage: "0xtest", @@ -85,9 +169,9 @@ describe("Eth hw-signMessage", () => { }); it("should not be returning parity for signEIP712HashedMessage", async () => { - const { rsv } = await hwSignMessage({} as any, { + const { rsv } = await ethSign.signMessage({} as any, { path: "", - message: JSON.stringify(EIP712Message), + message: JSON.stringify(testEIP712Message), rawMessage: "0xtest", }); @@ -97,9 +181,9 @@ describe("Eth hw-signMessage", () => { it("should not be returning parity for signEIP712Message", async () => { setEnv("EXPERIMENTAL_EIP712", true); - const { rsv } = await hwSignMessage({} as any, { + const { rsv } = await ethSign.signMessage({} as any, { path: "", - message: EIP712Message, + message: testEIP712Message, rawMessage: "0xtest", }); diff --git a/libs/ledger-live-common/src/families/ethereum/hw-signMessage.ts b/libs/ledger-live-common/src/families/ethereum/hw-signMessage.ts index 3f5e36fac3e0..e86ca7941a81 100644 --- a/libs/ledger-live-common/src/families/ethereum/hw-signMessage.ts +++ b/libs/ledger-live-common/src/families/ethereum/hw-signMessage.ts @@ -1,15 +1,20 @@ +import Eth, { isEIP712Message } from "@ledgerhq/hw-app-eth"; import { EIP712Message } from "@ledgerhq/hw-app-eth/lib/modules/EIP712/EIP712.types"; -import Eth from "@ledgerhq/hw-app-eth"; import Transport from "@ledgerhq/hw-transport"; import { TypedDataUtils } from "eth-sig-util"; import { bufferToHex } from "ethereumjs-util"; import { getEnv } from "../../env"; import type { MessageData, Result } from "../../hw/signMessage/types"; import type { TypedMessageData } from "./types"; -type EthResolver = ( - arg0: Transport, - arg1: Pick & - Partial> +import { DerivationMode } from "../../derivation"; +import { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; + +type EthSignMessage = ( + transport: Transport, + message: Pick< + MessageData | TypedMessageData, + "path" | "message" | "rawMessage" + > ) => Promise; export const domainHash = (message: EIP712Message): Buffer => { @@ -29,18 +34,65 @@ export const messageHash = (message: EIP712Message): Buffer => { ); }; -const resolver: EthResolver = async ( - transport, - { path, message, rawMessage } +function tryConvertToJSON(message: string): string | EIP712Message { + try { + const parsedMessage = JSON.parse(message); + if (isEIP712Message(parsedMessage)) { + return parsedMessage as EIP712Message; + } + } catch { + // Not a JSON message + } + return message; +} + +const prepareMessageToSign = ( + currency: CryptoCurrency, + path: string, + derivationMode: DerivationMode, + message: string +): MessageData | TypedMessageData => { + const hexToStringMessage = Buffer.from(message, "hex").toString(); + const parsedMessage = tryConvertToJSON(hexToStringMessage); + + const messageData = { + currency, + path, + derivationMode, + rawMessage: "0x" + message, + }; + if (typeof parsedMessage === "string") { + return { + ...messageData, + message: parsedMessage, + }; + } else { + return { + ...messageData, + message: parsedMessage, + hashes: { + stringHash: "", + domainHash: bufferToHex(domainHash(parsedMessage)), + messageHash: bufferToHex(messageHash(parsedMessage)), + }, + }; + } +}; + +type PartialMessageData = { + path: string; + message: string | EIP712Message; + rawMessage: string; +}; +const signMessage: EthSignMessage = async ( + transport: Transport, + { path, message, rawMessage }: PartialMessageData ) => { const eth = new Eth(transport); - const parsedMessage = (() => { - try { - return JSON.parse(message as string); - } catch (e) { - return message; - } - })(); + let parsedMessage = message; + if (typeof message === "string") { + parsedMessage = tryConvertToJSON(message); + } let result: Awaited>; if (typeof parsedMessage === "string") { @@ -70,4 +122,4 @@ const resolver: EthResolver = async ( }; }; -export default resolver; +export default { prepareMessageToSign, signMessage }; diff --git a/libs/ledger-live-common/src/families/ethereum/types.ts b/libs/ledger-live-common/src/families/ethereum/types.ts index f63b2d0a133c..4ab7075c0a42 100644 --- a/libs/ledger-live-common/src/families/ethereum/types.ts +++ b/libs/ledger-live-common/src/families/ethereum/types.ts @@ -1,15 +1,15 @@ -import { EIP712Message } from "@ledgerhq/hw-app-eth/lib/modules/EIP712/EIP712.types"; import type { BigNumber } from "bignumber.js"; +import { EIP712Message } from "@ledgerhq/hw-app-eth/lib/modules/EIP712/EIP712.types"; +import type { Unit } from "@ledgerhq/types-cryptoassets"; import type { TransactionMode, ModeModule } from "./modules"; import type { Range, RangeRaw } from "../../range"; -import type { DerivationMode } from "../../derivation"; import type { TransactionCommon, TransactionCommonRaw, TransactionStatusCommon, TransactionStatusCommonRaw, } from "@ledgerhq/types-live"; -import type { CryptoCurrency, Unit } from "@ledgerhq/types-cryptoassets"; +import type { MessageData } from "../../hw/signMessage/types"; export type EthereumGasLimitRequest = { from?: string; @@ -61,14 +61,12 @@ export type TransactionRaw = TransactionCommonRaw & { collectionName?: string; quantities?: string[]; }; -export type TypedMessageData = { - currency: CryptoCurrency; - path: string; - verify?: boolean; - derivationMode: DerivationMode; +export type TypedMessageData = Omit & { message: EIP712Message; hashes: { stringHash: string; + domainHash: string; + messageHash: string; }; }; diff --git a/libs/ledger-live-common/src/families/filecoin/hw-signMessage.ts b/libs/ledger-live-common/src/families/filecoin/hw-signMessage.ts index 9ffcf1c2db98..05f9d355ec02 100644 --- a/libs/ledger-live-common/src/families/filecoin/hw-signMessage.ts +++ b/libs/ledger-live-common/src/families/filecoin/hw-signMessage.ts @@ -1,10 +1,10 @@ import Fil from "@zondax/ledger-filecoin"; import { log } from "@ledgerhq/logs"; -import type { Resolver, Result } from "../../hw/signMessage/types"; +import type { SignMessage, Result } from "../../hw/signMessage/types"; import { getBufferFromString, getPath, isError } from "./utils"; -const resolver: Resolver = async ( +const signMessage: SignMessage = async ( transport, { path, message } ): Promise => { @@ -27,4 +27,4 @@ const resolver: Resolver = async ( }; }; -export default resolver; +export default { signMessage }; diff --git a/libs/ledger-live-common/src/hw/signMessage/index.test.ts b/libs/ledger-live-common/src/hw/signMessage/index.test.ts index 5adc26967a56..c6f14312576c 100644 --- a/libs/ledger-live-common/src/hw/signMessage/index.test.ts +++ b/libs/ledger-live-common/src/hw/signMessage/index.test.ts @@ -1,19 +1,38 @@ +import BigNumber from "bignumber.js"; import type { CryptoCurrency } from "@ledgerhq/types-cryptoassets"; import type { Account } from "@ledgerhq/types-live"; -import BigNumber from "bignumber.js"; +import { createFixtureCryptoCurrency } from "../../mock/fixtures/cryptoCurrencies"; +import { TypedMessageData } from "../../families/ethereum/types"; import { prepareMessageToSign } from "./index"; import { MessageData } from "./types"; +const signResult = { + message: "Sign results", + rawMessage: "Sign raw results", +}; +const signFunction = jest.fn(() => signResult); +jest.mock("../../generated/hw-signMessage", () => { + return { + signExistFamily: { + prepareMessageToSign: function () { + return signFunction(); + }, + }, + bitcoin: { + otherMethod: function () {}, + }, + }; +}); + describe("prepareMessageToSign", () => { - it("returns the prepared data from a simple string", () => { + it("calls the perFamily function if it's exist and returns this function results", () => { // Given - const crypto = createCryptoCurrency("ethereum"); + const crypto = createFixtureCryptoCurrency("signExistFamily"); const account = createAccount(crypto); - const message = "4d6573736167652064652074657374"; - const expectedRawMessage = "0x4d6573736167652064652074657374"; + const message = "whatever"; // When - let result: MessageData | null = null; + let result: MessageData | TypedMessageData | undefined; let error: unknown = null; try { result = prepareMessageToSign(account, message); @@ -23,10 +42,26 @@ describe("prepareMessageToSign", () => { // Then expect(error).toBeNull(); + expect(signFunction).toBeCalledTimes(1); + expect(result).toEqual(signResult); + }); + + it("returns a default implementation if account is linked to a crypto able to sign but with no prepareMessageToSign function", () => { + // Given + const currency = createFixtureCryptoCurrency("bitcoin"); + const account = createAccount(currency); + const message = "4d6573736167652064652074657374"; + const expectedPath = "44'/60'/0'/0/0"; + const expectedRawMessage = "0x4d6573736167652064652074657374"; + + // // When + const result = prepareMessageToSign(account, message); + + // // Then expect(result).toEqual({ - currency: crypto, - path: "44'/60'/0'/0/0", - derivationMode: "ethM", + currency, + path: expectedPath, + derivationMode: account.derivationMode, message: "Message de test", rawMessage: expectedRawMessage, }); @@ -34,12 +69,12 @@ describe("prepareMessageToSign", () => { it("returns an error if account is not linked to a crypto able to sign a message", () => { // Given - const crypto = createCryptoCurrency("mycoin"); + const crypto = createFixtureCryptoCurrency("mycoin"); const account = createAccount(crypto); - const message = "4d6573736167652064652074657374"; + const message = "whatever"; // When - let result: MessageData | null = null; + let result: MessageData | TypedMessageData | undefined; let error: Error | null = null; try { result = prepareMessageToSign(account, message); @@ -48,43 +83,11 @@ describe("prepareMessageToSign", () => { } // Then - expect(result).toBeNull(); + expect(result).toBeUndefined(); expect(error).toEqual(Error("Crypto does not support signMessage")); }); }); -const createCryptoCurrency = (family: string): CryptoCurrency => ({ - type: "CryptoCurrency", - id: "testCoinId", - coinType: 8008, - name: "MyCoin", - managerAppName: "MyCoin", - ticker: "MYC", - countervalueTicker: "MYC", - scheme: "mycoin", - color: "#ff0000", - family, - units: [ - { - name: "MYC", - code: "MYC", - magnitude: 8, - }, - { - name: "SmallestUnit", - code: "SMALLESTUNIT", - magnitude: 0, - }, - ], - explorerViews: [ - { - address: "https://mycoinexplorer.com/account/$address", - tx: "https://mycoinexplorer.com/transaction/$hash", - token: "https://mycoinexplorer.com/token/$contractAddress/?a=$address", - }, - ], -}); - const createAccount = (crypto: CryptoCurrency): Account => ({ type: "Account", id: "ethereumjs:2:ethereum:0x01:", diff --git a/libs/ledger-live-common/src/hw/signMessage/index.ts b/libs/ledger-live-common/src/hw/signMessage/index.ts index c58f9fc67d35..ca85a9016a9b 100644 --- a/libs/ledger-live-common/src/hw/signMessage/index.ts +++ b/libs/ledger-live-common/src/hw/signMessage/index.ts @@ -13,17 +13,29 @@ import { createAction as createAppAction } from "../actions/app"; import type { Device } from "../actions/types"; import type { ConnectAppEvent, Input as ConnectAppInput } from "../connectApp"; import { withDevice } from "../deviceAccess"; -import type { MessageData, Resolver, Result } from "./types"; +import type { MessageData, SignMessage, Result } from "./types"; import { DerivationMode } from "../../derivation"; export const prepareMessageToSign = ( - { currency, freshAddressPath, derivationMode }: Account, + account: Account, message: string -): MessageData | null => { +): MessageData => { + const { currency, freshAddressPath, derivationMode } = account; + if (!perFamily[currency.family]) { throw new Error("Crypto does not support signMessage"); } + if ("prepareMessageToSign" in perFamily[currency.family]) { + return perFamily[currency.family].prepareMessageToSign( + currency, + freshAddressPath, + derivationMode, + message + ); + } + + // Default implementation return { currency: currency, path: freshAddressPath, @@ -33,9 +45,9 @@ export const prepareMessageToSign = ( }; }; -const dispatch: Resolver = (transport, opts) => { +const signMessage: SignMessage = (transport, opts) => { const { currency, verify } = opts; - const signMessage = perFamily[currency.family]; + const signMessage = perFamily[currency.family].signMessage; invariant(signMessage, `signMessage is not implemented for ${currency.id}`); return signMessage(transport, opts) .then((result) => { @@ -87,7 +99,7 @@ export const signMessageExec = ({ deviceId, }: Input): Observable => { const result: Observable = withDevice(deviceId)((transport) => - from(dispatch(transport, request.message)) + from(signMessage(transport, request.message)) ); return result; }; @@ -99,8 +111,10 @@ const initialState: BaseState = { }; export const createAction = ( - connectAppExec: (arg0: ConnectAppInput) => Observable, - signMessage: (arg0: Input) => Observable = signMessageExec + connectAppExec: ( + connectAppInput: ConnectAppInput + ) => Observable, + signMessage: (input: Input) => Observable = signMessageExec ) => { const useHook = ( reduxDevice: Device | null | undefined, @@ -175,4 +189,4 @@ export const createAction = ( }), }; }; -export default dispatch; +export default signMessage; diff --git a/libs/ledger-live-common/src/hw/signMessage/types.ts b/libs/ledger-live-common/src/hw/signMessage/types.ts index 5f31e0c1944f..6137c8e19ce3 100644 --- a/libs/ledger-live-common/src/hw/signMessage/types.ts +++ b/libs/ledger-live-common/src/hw/signMessage/types.ts @@ -17,4 +17,7 @@ export type MessageData = { message: string; rawMessage: string; }; -export type Resolver = (arg0: Transport, arg1: MessageData) => Promise; +export type SignMessage = ( + transport: Transport, + message: MessageData +) => Promise; diff --git a/libs/ledger-live-common/src/walletconnect/index.ts b/libs/ledger-live-common/src/walletconnect/index.ts index e523823fb05b..1792576dde04 100644 --- a/libs/ledger-live-common/src/walletconnect/index.ts +++ b/libs/ledger-live-common/src/walletconnect/index.ts @@ -39,7 +39,7 @@ export type WCCallRequest = method: "send" | "sign"; data: Transaction; }; -type Parser = (arg0: Account, arg1: WCPayload) => Promise; +type Parser = (account: Account, payload: WCPayload) => Promise; export const parseCallRequest: Parser = async (account, payload) => { let wcTransactionData, bridge, transaction, message, rawMessage, hashes; @@ -52,12 +52,11 @@ export const parseCallRequest: Parser = async (account, payload) => { // @dev: Today, `eth_signTypedData` is versionned. We can't only check `eth_signTypedData` // This regex matches `eth_signTypedData` and `eth_signTypedData_v[0-9]` + // https://docs.metamask.io/guide/signing-data.html case payload.method.match(/eth_signTypedData(_v.)?$/)?.input: message = JSON.parse(payload.params[1]); hashes = { - // $FlowFixMe domainHash: bufferToHex(domainHash(message)), - // $FlowFixMe messageHash: bufferToHex(messageHash(message)), }; case "eth_sign": @@ -67,7 +66,6 @@ export const parseCallRequest: Parser = async (account, payload) => { stringHash: "0x" + sha("sha256") - // $FlowFixMe .update(Buffer.from(payload.params[1].slice(2), "hex")) .digest("hex"), }; @@ -76,16 +74,10 @@ export const parseCallRequest: Parser = async (account, payload) => { message || Buffer.from(payload.params[0].slice(2), "hex").toString(); rawMessage = rawMessage || payload.params[0]; hashes = hashes || { - stringHash: - "0x" + - sha("sha256") - // $FlowFixMe - .update(message) - .digest("hex"), + stringHash: "0x" + sha("sha256").update(message).digest("hex"), }; return { type: "message", - // $FlowFixMe (can't figure out MessageData | TypedMessageData) data: { path: account.freshAddressPath, message, diff --git a/libs/ledger-live-common/src/walletconnect/walletconnect.test.ts b/libs/ledger-live-common/src/walletconnect/walletconnect.test.ts index 9917298c7200..cf9e247ec2c2 100644 --- a/libs/ledger-live-common/src/walletconnect/walletconnect.test.ts +++ b/libs/ledger-live-common/src/walletconnect/walletconnect.test.ts @@ -51,6 +51,7 @@ describe("walletconnect", () => { }) ).rejects.toThrow("wrong payload"); }); + test("should parse personal_sign payloads", async () => { expect( await parseCallRequest(account, { @@ -132,6 +133,7 @@ describe("walletconnect", () => { type: "message", }); }); + test("should parse eth_sendTransaction payloads", async () => { const raw: WCPayloadTransaction = { data: "0x", diff --git a/libs/ledgerjs/packages/hw-app-eth/src/Eth.ts b/libs/ledgerjs/packages/hw-app-eth/src/Eth.ts index b292a226a94f..f655c686f8ae 100644 --- a/libs/ledgerjs/packages/hw-app-eth/src/Eth.ts +++ b/libs/ledgerjs/packages/hw-app-eth/src/Eth.ts @@ -29,9 +29,10 @@ import { signEIP712HashedMessage, signEIP712Message, EIP712Message, + isEIP712Message, } from "./modules/EIP712"; -export { ledgerService }; +export { ledgerService, isEIP712Message }; export type StarkQuantizationType = | "eth" diff --git a/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/EIP712.utils.ts b/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/EIP712.utils.ts index 8169cdd75f81..34fa3f4f6b93 100644 --- a/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/EIP712.utils.ts +++ b/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/EIP712.utils.ts @@ -1,5 +1,5 @@ import { hexBuffer, intAsHexBytes } from "../../utils"; -import { EIP712MessageTypesEntry } from "./EIP712.types"; +import { EIP712Message, EIP712MessageTypesEntry } from "./EIP712.types"; /** * @ignore for the README @@ -249,3 +249,15 @@ export const makeTypeEntryStructBuffer = ({ return Buffer.concat(bufferArray); }; + +// As defined in [spec](https://eips.ethereum.org/EIPS/eip-712), the properties below are all required. +export function isEIP712Message( + message: Record +): message is EIP712Message { + return ( + "types" in message && + "primaryType" in message && + "domain" in message && + "message" in message + ); +} diff --git a/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/index.ts b/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/index.ts index a7adf9fd9343..ea0a66e7c2a4 100644 --- a/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/index.ts +++ b/libs/ledgerjs/packages/hw-app-eth/src/modules/EIP712/index.ts @@ -407,3 +407,4 @@ export const signEIP712HashedMessage = ( }; export { EIP712Message } from "./EIP712.types"; +export { isEIP712Message } from "./EIP712.utils"; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index eece867949dc..0bf5509e8120 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -931,7 +931,7 @@ importers: '@ledgerhq/hw-transport-mocker': workspace:^ '@ledgerhq/hw-transport-node-speculos': workspace:^ '@ledgerhq/json-bignumber': ^1.1.0 - '@ledgerhq/live-app-sdk': ^0.7.0 + '@ledgerhq/live-app-sdk': ^0.8.1 '@ledgerhq/logs': workspace:^ '@ledgerhq/types-cryptoassets': workspace:^ '@ledgerhq/types-devices': workspace:^ @@ -1105,7 +1105,7 @@ importers: '@ledgerhq/hw-transport-mocker': link:../ledgerjs/packages/hw-transport-mocker '@ledgerhq/hw-transport-node-speculos': link:../ledgerjs/packages/hw-transport-node-speculos '@ledgerhq/json-bignumber': 1.1.0 - '@ledgerhq/live-app-sdk': 0.7.0 + '@ledgerhq/live-app-sdk': 0.8.1 '@ledgerhq/logs': link:../ledgerjs/packages/logs '@polkadot/types': 8.12.2 '@polkadot/types-known': 8.12.2 @@ -11603,8 +11603,8 @@ packages: json-rpc-2.0: 0.2.19 dev: false - /@ledgerhq/live-app-sdk/0.7.0: - resolution: {integrity: sha512-gmIFEKnXHtNXbD3dAK9mkLkE2yFX6/M1HwGoTTd0VEOl5ZLgKiKGaaXVcDpjLwVBvhegfRmSlDlpjCV3OLw74g==} + /@ledgerhq/live-app-sdk/0.8.1: + resolution: {integrity: sha512-RYQS76VPu/e0+KHi8/Q1yFjdxOUENxxP21ncSjHBRbyLbF/CfD7WDGTsoYv3dCMS7em14Sg3mxOv1P2uaCbCVw==} dependencies: bignumber.js: 9.0.2 json-rpc-2.0: 1.1.0