From 2d6c80c94a31d324f5c629f6f9ae62bf4999595b Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 27 Nov 2024 13:07:44 +0100 Subject: [PATCH 1/3] Support direct navigation in Ledger Live App To ensure users are directed to the correct LiveApp page for specific actions (such as staking/unstaking) triggered from Ledger Live, the Acre dapp must implement URL parameters to support direct navigation. We want to connect to the account passed via URL. The Ledger Live App passes the `accountId` parameter in URL when redirecting to Acre dapp. This parameter has the following pattern `js:2:bitcoin_testnet::`. To connect to a given account we need to get the extended public key from this parameter. To connect to a given account by xpub we define the new option in the provider: `tryConnectToAccountByXpub`. The Ledger Live Bitcoin provider will try to find this account and connect. --- dapp/src/utils/orangekit/index.ts | 28 +++++++++++++++++++ .../orangekit/ledger-live/bitcoin-provider.ts | 27 ++++++++++++++---- .../tests/bitcoin-provider.test.ts | 2 ++ dapp/src/wagmiConfig.ts | 14 ++++++++-- 4 files changed, 63 insertions(+), 8 deletions(-) diff --git a/dapp/src/utils/orangekit/index.ts b/dapp/src/utils/orangekit/index.ts index 18f355df4..af5317b7c 100644 --- a/dapp/src/utils/orangekit/index.ts +++ b/dapp/src/utils/orangekit/index.ts @@ -107,6 +107,33 @@ async function verifySignInWithWalletMessage( return result.data } +/** + * Finds the extended public key (xpub) of the user's account from URL. Users + * can be redirected to the exact app in the Ledger Live application. One of the + * parameters passed via URL is `accountId` - the ID of the user's account in + * Ledger Live. + * @see https://developers.ledger.com/docs/ledger-live/exchange/earn/liveapp#url-parameters-for-direct-navigation + * + * @param {string} url Request url + * @returns The extended public key (xpub) of the user's account if the search + * parameter `accountId` exists in the URL. Otherwise `undefined`. + */ +function findXpubFromUrl(url: string): string | undefined { + const parsedUrl = new URL(url) + + const accountId = parsedUrl.searchParams.get("accountId") + + if (!accountId) return undefined + + // The fourth value separated by `:` is extended public key. See the + // account ID template: `js:2:bitcoin_testnet::`. + const xpubFromAccountId = accountId.split(":")[3] + + if (!xpubFromAccountId) return undefined + + return xpubFromAccountId +} + export default { getWalletInfo, isWalletInstalled, @@ -119,4 +146,5 @@ export default { isWalletConnectionRejectedError, verifySignInWithWalletMessage, getOrangeKitLedgerLiveConnector, + findXpubFromUrl, } diff --git a/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts b/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts index 7dab9c5ad..316e628a4 100644 --- a/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts +++ b/dapp/src/utils/orangekit/ledger-live/bitcoin-provider.ts @@ -58,9 +58,12 @@ function numberToValidHexString(value: number): string { return `0x${hex}` } -export type AcreLedgerLiveBitcoinProviderOptions = { - tryConnectToAddress: string | undefined -} +export type AcreLedgerLiveBitcoinProviderOptions = + | { + tryConnectToAddress?: string + tryConnectToAccountByXpub?: never + } + | { tryConnectToAddress?: never; tryConnectToAccountByXpub?: string } /** * Ledger Live Wallet API Bitcoin Provider. @@ -90,6 +93,7 @@ export default class AcreLedgerLiveBitcoinProvider network: BitcoinNetwork, options: AcreLedgerLiveBitcoinProviderOptions = { tryConnectToAddress: undefined, + tryConnectToAccountByXpub: undefined, }, ) { const windowMessageTransport = new WindowMessageTransport() @@ -115,6 +119,7 @@ export default class AcreLedgerLiveBitcoinProvider walletApiClient: WalletAPIClient, options: AcreLedgerLiveBitcoinProviderOptions = { tryConnectToAddress: undefined, + tryConnectToAccountByXpub: undefined, }, ) { this.#network = network @@ -140,12 +145,24 @@ export default class AcreLedgerLiveBitcoinProvider currencyIds, }) - if (this.#options.tryConnectToAddress) { + if ( + this.#options.tryConnectToAddress || + this.#options.tryConnectToAccountByXpub + ) { for (let i = 0; i < accounts.length; i += 1) { const acc = accounts[i] + if ( + this.#options.tryConnectToAccountByXpub && + // eslint-disable-next-line no-await-in-loop + (await this.#walletApiClient.bitcoin.getXPub(acc.id)) === + this.#options.tryConnectToAccountByXpub + ) { + this.#account = acc + break + } + // eslint-disable-next-line no-await-in-loop const address = await this.#getAddress(acc.id) - if (address === this.#options.tryConnectToAddress) { this.#account = acc break diff --git a/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts b/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts index 58d1d0f89..6ba9cb41a 100644 --- a/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts +++ b/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts @@ -473,6 +473,8 @@ describe("AcreLedgerLiveBitcoinProvider", () => { }) }) + it("should get") + it("should get zero address for all accounts", () => { expect( mockedWalletApiClient.bitcoin.getAddress, diff --git a/dapp/src/wagmiConfig.ts b/dapp/src/wagmiConfig.ts index 19d1cf4e4..972a38fda 100644 --- a/dapp/src/wagmiConfig.ts +++ b/dapp/src/wagmiConfig.ts @@ -5,6 +5,7 @@ import { env } from "./constants" import { getLastUsedBtcAddress } from "./hooks/useLastUsedBtcAddress" import referralProgram, { EmbedApp } from "./utils/referralProgram" import { orangeKit } from "./utils" +import { AcreLedgerLiveBitcoinProviderOptions } from "./utils/orangekit/ledger-live/bitcoin-provider" const isTestnet = env.USE_TESTNET const CHAIN_ID = isTestnet ? sepolia.id : mainnet.id @@ -34,12 +35,19 @@ async function getWagmiConfig() { let createEmbedConnectorFn const embeddedApp = referralProgram.getEmbeddedApp() if (referralProgram.isEmbedApp(embeddedApp)) { + const lastUsedBtcAddress = getLastUsedBtcAddress() + const xpub = orangeKit.findXpubFromUrl(window.location.href) + const ledgerLiveConnectorOptions: AcreLedgerLiveBitcoinProviderOptions = + xpub + ? { tryConnectToAccountByXpub: xpub } + : { + tryConnectToAddress: lastUsedBtcAddress, + } + const orangeKitLedgerLiveConnector = orangeKit.getOrangeKitLedgerLiveConnector({ ...connectorConfig, - options: { - tryConnectToAddress: getLastUsedBtcAddress(), - }, + options: ledgerLiveConnectorOptions, }) const embedConnectorsMap: Record< From dbc653d49daae860190c1b3c132f963ab67497c4 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 27 Nov 2024 13:28:46 +0100 Subject: [PATCH 2/3] Update Ledger Live manifest Add the `bitcoin.getXpub` method to the `permissions` list. We use this method to connect to an account by `xpub`. --- dapp/manifests/ledger-live/ledger-live-manifest-development.json | 1 + dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json | 1 + dapp/manifests/ledger-live/ledger-live-manifest-testnet.json | 1 + dapp/manifests/ledger-live/ledger-manifest-template.json | 1 + 4 files changed, 4 insertions(+) diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-development.json b/dapp/manifests/ledger-live/ledger-live-manifest-development.json index 84e58fcad..bffaf7026 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-development.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-development.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json b/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json index 281992191..22f74bbc5 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-mainnet.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json b/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json index 32dd4a45f..a6ea2019c 100644 --- a/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json +++ b/dapp/manifests/ledger-live/ledger-live-manifest-testnet.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" diff --git a/dapp/manifests/ledger-live/ledger-manifest-template.json b/dapp/manifests/ledger-live/ledger-manifest-template.json index e999601d2..980c407d2 100644 --- a/dapp/manifests/ledger-live/ledger-manifest-template.json +++ b/dapp/manifests/ledger-live/ledger-manifest-template.json @@ -23,6 +23,7 @@ "account.list", "bitcoin.getAddress", "bitcoin.getPublicKey", + "bitcoin.getXPub", "transaction.signAndBroadcast", "custom.acre.messageSign", "custom.acre.transactionSignAndBroadcast" From 34a33d530d86e0f32dd10b0f431fa5a6dec2ffe8 Mon Sep 17 00:00:00 2001 From: Rafal Czajkowski Date: Wed, 27 Nov 2024 16:38:18 +0100 Subject: [PATCH 3/3] Remove unnecessary `it` in test --- .../utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts b/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts index 6ba9cb41a..58d1d0f89 100644 --- a/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts +++ b/dapp/src/utils/orangekit/ledger-live/tests/bitcoin-provider.test.ts @@ -473,8 +473,6 @@ describe("AcreLedgerLiveBitcoinProvider", () => { }) }) - it("should get") - it("should get zero address for all accounts", () => { expect( mockedWalletApiClient.bitcoin.getAddress,