UNPKG

@ledgerhq/live-common

Version:
534 lines (457 loc) • 17.1 kB
import { Account, AccountLike, AnyMessage, getCurrencyForAccount, SignedOperation, TokenAccount, } from "@ledgerhq/types-live"; import { accountToWalletAPIAccount, getWalletAPITransactionSignFlowInfos, getAccountIdFromWalletAccountId, } from "./converters"; import type { TrackingAPI } from "./tracking"; import { AppManifest, TranslatableString, WalletAPITransaction } from "./types"; import { isTokenAccount, isAccount, getMainAccount, makeEmptyTokenAccount, getParentAccount, } from "../account/index"; import { Transaction } from "../generated/types"; import { prepareMessageToSign } from "../hw/signMessage/index"; import { getAccountBridge } from "../bridge"; import { Exchange } from "../exchange/types"; import { findTokenById } from "@ledgerhq/cryptoassets"; import { WalletState } from "@ledgerhq/live-wallet/store"; import { getWalletAccount } from "@ledgerhq/coin-bitcoin/wallet-btc/index"; import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets"; export function translateContent(content: string | TranslatableString, locale = "en"): string { if (!content || typeof content === "string") return content; return content[locale] || content.en; } export type WalletAPIContext = { manifest: AppManifest; accounts: AccountLike[]; tracking: TrackingAPI; }; export function receiveOnAccountLogic( walletState: WalletState, { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, uiNavigation: ( account: AccountLike, parentAccount: Account | undefined, accountAddress: string, ) => Promise<string>, tokenCurrency?: string, ): Promise<string> { tracking.receiveRequested(manifest); const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.receiveFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.receiveFail(manifest); return Promise.reject(new Error("Account required")); } const parentAccount = getParentAccount(account, accounts); const mainAccount = getMainAccount(account, parentAccount); const currency = tokenCurrency ? findTokenById(tokenCurrency) : null; const receivingAccount = currency ? makeEmptyTokenAccount(mainAccount, currency) : account; const accountAddress = accountToWalletAPIAccount(walletState, account, parentAccount).address; return uiNavigation(receivingAccount, parentAccount, accountAddress); } export function signTransactionLogic( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, transaction: WalletAPITransaction, uiNavigation: ( account: AccountLike, parentAccount: Account | undefined, signFlowInfos: { canEditFees: boolean; hasFeesProvided: boolean; liveTx: Partial<Transaction>; }, ) => Promise<SignedOperation>, tokenCurrency?: string, ): Promise<SignedOperation> { tracking.signTransactionRequested(manifest); if (!transaction) { tracking.signTransactionFail(manifest); return Promise.reject(new Error("Transaction required")); } const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.signTransactionFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.signTransactionFail(manifest); return Promise.reject(new Error("Account required")); } const parentAccount = getParentAccount(account, accounts); const accountFamily = isTokenAccount(account) ? parentAccount?.currency.family : account.currency.family; const mainAccount = getMainAccount(account, parentAccount); const currency = tokenCurrency ? findTokenById(tokenCurrency) : null; const signerAccount = currency ? makeEmptyTokenAccount(mainAccount, currency) : account; const { canEditFees, liveTx, hasFeesProvided } = getWalletAPITransactionSignFlowInfos({ walletApiTransaction: transaction, account: mainAccount, }); if (accountFamily !== liveTx.family) { return Promise.reject( new Error( `Account and transaction must be from the same family. Account family: ${accountFamily}, Transaction family: ${liveTx.family}`, ), ); } return uiNavigation(signerAccount, parentAccount, { canEditFees, liveTx, hasFeesProvided, }); } export function signRawTransactionLogic( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, transaction: string, uiNavigation: ( account: AccountLike, parentAccount: Account | undefined, transaction: string, ) => Promise<SignedOperation>, ): Promise<SignedOperation> { tracking.signRawTransactionRequested(manifest); if (!transaction) { tracking.signRawTransactionFail(manifest); return Promise.reject(new Error("Transaction required")); } const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.signRawTransactionFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.signRawTransactionFail(manifest); return Promise.reject(new Error("Account required")); } const parentAccount = getParentAccount(account, accounts); return uiNavigation(account, parentAccount, transaction); } export function broadcastTransactionLogic( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, signedOperation: SignedOperation, uiNavigation: ( account: AccountLike, parentAccount: Account | undefined, signedOperation: SignedOperation, ) => Promise<string>, tokenCurrency?: string, ): Promise<string> { if (!signedOperation) { tracking.broadcastFail(manifest); return Promise.reject(new Error("Transaction required")); } const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.broadcastFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (!account) { tracking.broadcastFail(manifest); return Promise.reject(new Error("Account required")); } const currency = tokenCurrency ? findTokenById(tokenCurrency) : null; const parentAccount = getParentAccount(account, accounts); const mainAccount = getMainAccount(account, parentAccount); const signerAccount = currency ? makeEmptyTokenAccount(mainAccount, currency) : account; return uiNavigation(signerAccount, parentAccount, signedOperation); } export function signMessageLogic( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, message: string, uiNavigation: (account: AccountLike, message: AnyMessage) => Promise<Buffer>, ): Promise<Buffer> { tracking.signMessageRequested(manifest); const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.signMessageFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (account === undefined) { tracking.signMessageFail(manifest); return Promise.reject(new Error("account not found")); } let formattedMessage: AnyMessage; try { if (isAccount(account)) { formattedMessage = prepareMessageToSign(account, message); } else { throw new Error("account provided should be the main one"); } } catch (error) { tracking.signMessageFail(manifest); return Promise.reject(error); } return uiNavigation(account, formattedMessage); } export const bitcoinFamilyAccountGetAddressLogic = ( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, derivationPath?: string, ): Promise<string> => { tracking.bitcoinFamilyAccountAddressRequested(manifest); const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.bitcoinFamilyAccountAddressFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (account === undefined) { tracking.bitcoinFamilyAccountAddressFail(manifest); return Promise.reject(new Error("account not found")); } if (!isAccount(account) || account.currency.family !== "bitcoin") { tracking.bitcoinFamilyAccountAddressFail(manifest); return Promise.reject(new Error("account requested is not a bitcoin family account")); } if (derivationPath) { const path = derivationPath.split("/"); const accountNumber = Number(path[0]); const index = Number(path[1]); if (Number.isNaN(accountNumber) || Number.isNaN(index)) { tracking.bitcoinFamilyAccountAddressFail(manifest); return Promise.reject(new Error("Invalid derivationPath")); } const walletAccount = getWalletAccount(account); const address = walletAccount.xpub.crypto.getAddress( walletAccount.xpub.derivationMode, walletAccount.xpub.xpub, accountNumber, index, ); tracking.bitcoinFamilyAccountAddressSuccess(manifest); return Promise.resolve(address); } tracking.bitcoinFamilyAccountAddressSuccess(manifest); return Promise.resolve(account.freshAddress); }; function getRelativePath(path: string) { const splitPath = path.split("'/"); return splitPath[splitPath.length - 1]; } export const bitcoinFamilyAccountGetPublicKeyLogic = async ( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, derivationPath?: string, ): Promise<string> => { tracking.bitcoinFamilyAccountPublicKeyRequested(manifest); const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.bitcoinFamilyAccountPublicKeyFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (account === undefined) { tracking.bitcoinFamilyAccountPublicKeyFail(manifest); return Promise.reject(new Error("account not found")); } if (!isAccount(account) || account.currency.family !== "bitcoin") { tracking.bitcoinFamilyAccountPublicKeyFail(manifest); return Promise.reject(new Error("account requested is not a bitcoin family account")); } const path = derivationPath?.split("/") ?? getRelativePath(account.freshAddressPath).split("/"); const accountNumber = Number(path[0]); const index = Number(path[1]); if (Number.isNaN(accountNumber) || Number.isNaN(index)) { tracking.bitcoinFamilyAccountPublicKeyFail(manifest); return Promise.reject(new Error("Invalid derivationPath")); } const walletAccount = getWalletAccount(account); const publicKey = await walletAccount.xpub.crypto.getPubkeyAt( walletAccount.xpub.xpub, accountNumber, index, ); tracking.bitcoinFamilyAccountPublicKeySuccess(manifest); return publicKey.toString("hex"); }; export const bitcoinFamilyAccountGetXPubLogic = ( { manifest, accounts, tracking }: WalletAPIContext, walletAccountId: string, ): Promise<string> => { tracking.bitcoinFamilyAccountXpubRequested(manifest); const accountId = getAccountIdFromWalletAccountId(walletAccountId); if (!accountId) { tracking.bitcoinFamilyAccountXpubFail(manifest); return Promise.reject(new Error(`accountId ${walletAccountId} unknown`)); } const account = accounts.find(account => account.id === accountId); if (account === undefined) { tracking.bitcoinFamilyAccountXpubFail(manifest); return Promise.reject(new Error("account not found")); } if (!isAccount(account) || account.currency.family !== "bitcoin") { tracking.bitcoinFamilyAccountXpubFail(manifest); return Promise.reject(new Error("account requested is not a bitcoin family account")); } if (!account.xpub) { tracking.bitcoinFamilyAccountXpubFail(manifest); return Promise.reject(new Error("account xpub not available")); } tracking.bitcoinFamilyAccountXpubSuccess(manifest); return Promise.resolve(account.xpub); }; export function startExchangeLogic( { manifest, tracking }: WalletAPIContext, exchangeType: "SWAP" | "FUND" | "SELL" | "SWAP_NG" | "SELL_NG" | "FUND_NG", uiNavigation: ( exchangeType: "SWAP" | "FUND" | "SELL" | "SWAP_NG" | "SELL_NG" | "FUND_NG", ) => Promise<string>, ): Promise<string> { tracking.startExchangeRequested(manifest); return uiNavigation(exchangeType); } export type CompleteExchangeRequest = { provider: string; fromAccountId: string; toAccountId?: string; transaction: WalletAPITransaction; binaryPayload: string; signature: string; feesStrategy: string; exchangeType: number; swapId?: string; rate?: number; amountExpectedTo?: number; tokenCurrency?: string; }; export type CompleteExchangeUiRequest = { provider: string; exchange: Exchange; transaction: Transaction; binaryPayload: string; signature: string; feesStrategy: string; exchangeType: number; swapId?: string; rate?: number; amountExpectedTo?: number; tokenCurrency?: string; }; export function completeExchangeLogic( { manifest, accounts, tracking }: WalletAPIContext, { provider, fromAccountId, toAccountId, transaction, binaryPayload, signature, feesStrategy, exchangeType, swapId, rate, tokenCurrency, }: CompleteExchangeRequest, uiNavigation: (request: CompleteExchangeUiRequest) => Promise<string>, ): Promise<string> { tracking.completeExchangeRequested(manifest); const realFromAccountId = getAccountIdFromWalletAccountId(fromAccountId); if (!realFromAccountId) { return Promise.reject(new Error(`accountId ${fromAccountId} unknown`)); } // Nb get a hold of the actual accounts, and parent accounts const fromAccount = accounts.find(a => a.id === realFromAccountId); let toAccount; if (toAccountId) { const realToAccountId = getAccountIdFromWalletAccountId(toAccountId); if (!realToAccountId) { return Promise.reject(new Error(`accountId ${toAccountId} unknown`)); } toAccount = accounts.find(a => a.id === realToAccountId); } if (!fromAccount) { return Promise.reject(); } if (exchangeType === 0x00 && !toAccount) { // if we do a swap, a destination account must be provided return Promise.reject(); } const fromParentAccount = getParentAccount(fromAccount, accounts); const currency = tokenCurrency ? findTokenById(tokenCurrency) : null; const newTokenAccount = currency ? makeEmptyTokenAccount(toAccount, currency) : undefined; const toParentAccount = toAccount ? getParentAccount(toAccount, accounts) : undefined; const exchange = { fromAccount, fromParentAccount: fromAccount !== fromParentAccount ? fromParentAccount : undefined, fromCurrency: getCurrencyForAccount(fromAccount), toAccount: newTokenAccount ? newTokenAccount : toAccount, toParentAccount: newTokenAccount ? toAccount : toParentAccount, toCurrency: toAccount ? getToCurrency(toAccount, newTokenAccount) : undefined, }; const accountBridge = getAccountBridge(fromAccount, fromParentAccount); const mainFromAccount = getMainAccount(fromAccount, fromParentAccount); const mainFromAccountFamily = mainFromAccount.currency.family; const { liveTx } = getWalletAPITransactionSignFlowInfos({ walletApiTransaction: transaction, account: fromAccount, }); if (liveTx.family !== mainFromAccountFamily) { return Promise.reject( new Error( `Account and transaction must be from the same family. Account family: ${mainFromAccountFamily}, Transaction family: ${liveTx.family}`, ), ); } /** * 'subAccountId' is used for ETH and it's ERC-20 tokens. * This field is ignored for BTC */ const subAccountId = exchange.fromParentAccount ? fromAccount.id : undefined; const bridgeTx = accountBridge.createTransaction(fromAccount); /** * We append the `recipient` to the tx created from `createTransaction` * to avoid having userGasLimit reset to null for ETH txs * cf. libs/ledger-live-common/src/families/ethereum/updateTransaction.ts */ const tx = accountBridge.updateTransaction( { ...bridgeTx, recipient: liveTx.recipient, }, { ...liveTx, feesStrategy: feesStrategy.toLowerCase(), subAccountId, }, ); return uiNavigation({ provider, exchange, transaction: tx, binaryPayload, signature, feesStrategy, exchangeType, swapId, rate, }); } function getToCurrency(account: AccountLike, tokenAccount?: TokenAccount): CryptoOrTokenCurrency { return tokenAccount ? getCurrencyForAccount(tokenAccount) : getCurrencyForAccount(account); }