UNPKG

@ledgerhq/live-common

Version:
611 lines • 29.1 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.handlers = exports.ExchangeType = void 0; /* eslint-disable no-console */ const index_1 = require("@ledgerhq/ledger-wallet-framework/account/index"); const state_1 = require("@ledgerhq/cryptoassets/state"); const hw_app_exchange_1 = require("@ledgerhq/hw-app-exchange"); const types_live_1 = require("@ledgerhq/types-live"); const wallet_api_core_1 = require("@ledgerhq/wallet-api-core"); const wallet_api_exchange_module_1 = require("@ledgerhq/wallet-api-exchange-module"); Object.defineProperty(exports, "ExchangeType", { enumerable: true, get: function () { return wallet_api_exchange_module_1.ExchangeType; } }); const wallet_api_server_1 = require("@ledgerhq/wallet-api-server"); const bignumber_js_1 = require("bignumber.js"); const bridge_1 = require("../../bridge"); const actions_1 = require("../../exchange/swap/api/v5/actions"); const transactionStrategies_1 = require("../../exchange/swap/transactionStrategies"); const converters_1 = require("../converters"); const error_1 = require("./error"); const error_2 = require("../../exchange/error"); const swap_1 = require("../../exchange/swap"); const setBroadcastTransaction_1 = require("../../exchange/swap/setBroadcastTransaction"); const hw_app_eth_1 = require("@ledgerhq/hw-app-eth"); const parser_1 = require("./parser"); const handleSwapErrors_1 = require("./handleSwapErrors"); const get_1 = __importDefault(require("lodash/get")); const handlers = ({ accounts, tracking, manifest, flags, uiHooks: { "custom.exchange.start": uiExchangeStart, "custom.exchange.complete": uiExchangeComplete, "custom.exchange.error": uiError, "custom.isReady": uiIsReady, "custom.exchange.swap": uiSwap, }, }) => ({ "custom.exchange.start": (0, wallet_api_server_1.customWrapper)(async (params) => { if (!params) { tracking.startExchangeNoParams(manifest); return { transactionId: "" }; } const trackingParams = { provider: params.provider, exchangeType: params.exchangeType, }; tracking.startExchangeRequested(trackingParams); let exchangeParams; // Use `if else` instead of switch to leverage TS type narrowing and avoid `params` force cast. if (params.exchangeType == "SWAP") { exchangeParams = await extractSwapStartParam(params, accounts); } else if (params.exchangeType == "SELL") { exchangeParams = extractSellStartParam(params, accounts); } else { exchangeParams = extractFundStartParam(params, accounts); } return new Promise((resolve, reject) => uiExchangeStart({ exchangeParams, onSuccess: (nonce, device) => { tracking.startExchangeSuccess(trackingParams); resolve({ transactionId: nonce, device }); }, onCancel: error => { tracking.startExchangeFail(trackingParams); reject(error); }, })); }), "custom.exchange.complete": (0, wallet_api_server_1.customWrapper)(async (params) => { if (!params) { tracking.completeExchangeNoParams(manifest); return { transactionHash: "" }; } const trackingParams = { provider: params.provider, exchangeType: params.exchangeType, }; tracking.completeExchangeRequested(trackingParams); const realFromAccountId = (0, converters_1.getAccountIdFromWalletAccountId)(params.fromAccountId); if (!realFromAccountId) { return Promise.reject(new Error(`accountId ${params.fromAccountId} unknown`)); } const fromAccount = accounts.find(acc => acc.id === realFromAccountId); if (!fromAccount) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.fromAccountId)); } const fromParentAccount = (0, index_1.getParentAccount)(fromAccount, accounts); let exchange; if (params.exchangeType === "SWAP") { const realToAccountId = (0, converters_1.getAccountIdFromWalletAccountId)(params.toAccountId); if (!realToAccountId) { return Promise.reject(new Error(`accountId ${params.toAccountId} unknown`)); } const toAccount = accounts.find(a => a.id === realToAccountId); if (!toAccount) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.toAccountId)); } // TODO: check logic for EmptyTokenAccount let toParentAccount = (0, index_1.getParentAccount)(toAccount, accounts); let newTokenAccount; if (params.tokenCurrency) { const currency = await (0, state_1.getCryptoAssetsStore)().findTokenById(params.tokenCurrency); if (!currency) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createCurrencyNotFound)(params.tokenCurrency)); } if (toAccount.type === "Account") { newTokenAccount = (0, index_1.makeEmptyTokenAccount)(toAccount, currency); toParentAccount = toAccount; } else { newTokenAccount = (0, index_1.makeEmptyTokenAccount)(toParentAccount, currency); } } const toCurrency = await getToCurrency(params.hexBinaryPayload, toAccount, newTokenAccount); exchange = { fromAccount, fromParentAccount, fromCurrency: (0, types_live_1.getCurrencyForAccount)(fromAccount), toAccount: newTokenAccount ? newTokenAccount : toAccount, toParentAccount, toCurrency, }; } else { exchange = { fromAccount, fromParentAccount, fromCurrency: (0, types_live_1.getCurrencyForAccount)(fromAccount), }; } const mainFromAccount = (0, index_1.getMainAccount)(fromAccount, fromParentAccount); const mainFromAccountFamily = mainFromAccount.currency.family; const transaction = (0, wallet_api_core_1.deserializeTransaction)(params.rawTransaction); const { liveTx } = (0, converters_1.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}`)); } const accountBridge = (0, bridge_1.getAccountBridge)(fromAccount, fromParentAccount); /** * 'subAccountId' is used for ETH and it's ERC-20 tokens. * This field is ignored for BTC */ const subAccountId = fromParentAccount && fromParentAccount.id !== fromAccount.id ? 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: params.feeStrategy.toLowerCase(), subAccountId, }); let amountExpectedTo; let magnitudeAwareRate; let refundAddress; let payoutAddress; if (params.exchangeType === "SWAP") { // Get amountExpectedTo and magnitudeAwareRate from binary payload const decodePayload = await (0, hw_app_exchange_1.decodeSwapPayload)(params.hexBinaryPayload); amountExpectedTo = new bignumber_js_1.BigNumber(decodePayload.amountToWallet.toString()); magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount); refundAddress = decodePayload.refundAddress; payoutAddress = decodePayload.payoutAddress; } return new Promise((resolve, reject) => uiExchangeComplete({ exchangeParams: { exchangeType: wallet_api_exchange_module_1.ExchangeType[params.exchangeType], provider: params.provider, transaction: tx, signature: params.hexSignature, binaryPayload: params.hexBinaryPayload, exchange, feesStrategy: params.feeStrategy, swapId: params.exchangeType === "SWAP" ? params.swapId : undefined, amountExpectedTo, magnitudeAwareRate, refundAddress, payoutAddress, }, onSuccess: (transactionHash) => { tracking.completeExchangeSuccess({ ...trackingParams, currency: params.rawTransaction.family, }); resolve({ transactionHash }); }, onCancel: error => { tracking.completeExchangeFail(trackingParams); reject(error); }, })); }), "custom.exchange.error": (0, wallet_api_server_1.customWrapper)(async (params) => { return new Promise((resolve, reject) => uiError({ error: params, onSuccess: () => { resolve(); }, onCancel: () => { reject(); }, })); }), "custom.exchange.swap": (0, wallet_api_server_1.customWrapper)(async (params) => { try { if (!params) { tracking.startExchangeNoParams(manifest); throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createUnknownError)({ message: "params is undefined" })); } const { provider, fromAmount, fromAmountAtomic, quoteId, toNewTokenId, customFeeConfig, swapAppVersion, sponsored, isEmbedded, } = params; const trackingParams = { provider: params.provider, exchangeType: params.exchangeType, isEmbeddedSwap: isEmbedded, }; tracking.startExchangeRequested(trackingParams); const exchangeStartParams = (await extractSwapStartParam(params, accounts)); const { fromCurrency, fromAccount, fromParentAccount, toCurrency, toAccount, toParentAccount, } = exchangeStartParams.exchange; if (!fromAccount || !fromCurrency) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.fromAccountId)); } const fromAccountAddress = fromParentAccount ? fromParentAccount.freshAddress : fromAccount.freshAddress; const toAccountAddress = toParentAccount ? toParentAccount.freshAddress : toAccount.freshAddress; // Step 1: Open the drawer and open exchange app const startExchange = async () => { return new Promise((resolve, reject) => { uiExchangeStart({ exchangeParams: exchangeStartParams, onSuccess: (nonce, device) => { tracking.startExchangeSuccess(trackingParams); resolve({ transactionId: nonce, device }); }, onCancel: error => { tracking.startExchangeFail(trackingParams); reject(error); }, }); }); }; let transactionId; let deviceInfo; try { const result = await startExchange(); transactionId = result.transactionId; deviceInfo = result.device; } catch (error) { const rawError = (0, get_1.default)(error, "response.data.error", error); const wrappedError = (0, parser_1.createStepError)({ error: (0, parser_1.toError)(rawError), step: parser_1.StepError.NONCE, }); throw wrappedError; } tracking.swapPayloadRequested({ provider, transactionId, fromAccountAddress, toAccountAddress, fromCurrencyId: fromCurrency.id, toCurrencyId: toCurrency?.id, fromAmount, quoteId, }); const { binaryPayload, signature, payinAddress, swapId, payinExtraId, extraTransactionParameters, } = await (0, actions_1.retrieveSwapPayload)({ provider, deviceTransactionId: transactionId, fromAccountAddress, toAccountAddress, fromAccountCurrency: fromCurrency.id, toAccountCurrency: toCurrency.id, amount: fromAmount, amountInAtomicUnit: fromAmountAtomic, quoteId, toNewTokenId, flags, }).catch((error) => { const wrappedError = (0, parser_1.createStepError)({ error: (0, get_1.default)(error, "response.data.error", error), step: parser_1.StepError.PAYLOAD, }); throw wrappedError; }); tracking.swapResponseRetrieved({ binaryPayload, signature, payinAddress, swapId, payinExtraId, extraTransactionParameters, }); // Complete Swap const trackingCompleteParams = { provider: params.provider, exchangeType: params.exchangeType, isEmbeddedSwap: isEmbedded, }; tracking.completeExchangeRequested(trackingCompleteParams); const strategyData = { recipient: payinAddress, amount: fromAmountAtomic, currency: fromCurrency, customFeeConfig: customFeeConfig ?? {}, payinExtraId, extraTransactionParameters, sponsored, }; const transaction = await getStrategy(strategyData, "swap"); const mainFromAccount = (0, index_1.getMainAccount)(fromAccount, fromParentAccount); if (transaction.family !== mainFromAccount.currency.family) { return Promise.reject(new Error(`Account and transaction must be from the same family. Account family: ${mainFromAccount.currency.family}, Transaction family: ${transaction.family}`)); } const accountBridge = (0, bridge_1.getAccountBridge)(fromAccount, fromParentAccount); /** * 'subAccountId' is used for ETH and it's ERC-20 tokens. * This field is ignored for BTC */ const subAccountId = fromParentAccount && fromParentAccount.id !== fromAccount.id ? 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: transaction.recipient, }, { ...transaction, feesStrategy: params.feeStrategy.toLowerCase(), subAccountId, }); // Get amountExpectedTo and magnitudeAwareRate from binary payload const decodePayload = await (0, hw_app_exchange_1.decodeSwapPayload)(binaryPayload); const amountExpectedTo = new bignumber_js_1.BigNumber(decodePayload.amountToWallet.toString()); const magnitudeAwareRate = tx.amount && amountExpectedTo.dividedBy(tx.amount); const refundAddress = decodePayload.refundAddress; const payoutAddress = decodePayload.payoutAddress; // tx.amount should be BigNumber tx.amount = new bignumber_js_1.BigNumber(tx.amount); return new Promise((resolve, reject) => uiSwap({ exchangeParams: { exchangeType: wallet_api_exchange_module_1.ExchangeType.SWAP, provider: params.provider, transaction: tx, signature: signature, binaryPayload: binaryPayload, exchange: { fromAccount, fromParentAccount, toAccount, toParentAccount, fromCurrency: fromCurrency, toCurrency: toCurrency, }, feesStrategy: params.feeStrategy, swapId: swapId, amountExpectedTo: amountExpectedTo.toNumber(), magnitudeAwareRate, refundAddress, payoutAddress, sponsored, isEmbeddedSwap: isEmbedded, }, onSuccess: ({ operationHash, swapId }) => { tracking.completeExchangeSuccess({ ...trackingParams, currency: transaction.family, }); (0, setBroadcastTransaction_1.setBroadcastTransaction)({ provider, result: { operation: operationHash, swapId }, sourceCurrencyId: fromCurrency.id, targetCurrencyId: toCurrency?.id, hardwareWalletType: deviceInfo?.modelId, swapAppVersion, fromAccountAddress, toAccountAddress, fromAmount, flags, }); resolve({ operationHash, swapId }); }, onCancel: error => { const { name: rawErrorName, message: rawErrorMessage, cause: rawErrorCause, } = (0, error_2.getErrorDetails)(error); const causeSuffix = rawErrorCause ? `, ${JSON.stringify(rawErrorCause)}` : ""; const errorMessageWithCause = rawErrorMessage + causeSuffix; const completeExchangeError = // step provided in libs/ledger-live-common/src/exchange/platform/transfer/completeExchange.ts error instanceof error_2.CompleteExchangeError ? error : new error_2.CompleteExchangeError("INIT", rawErrorName, errorMessageWithCause); (0, swap_1.postSwapCancelled)({ provider: provider, swapId: swapId, swapStep: (0, error_2.getSwapStepFromError)(completeExchangeError), statusCode: completeExchangeError.title || completeExchangeError.name, errorMessage: completeExchangeError.message || errorMessageWithCause, sourceCurrencyId: fromCurrency.id, targetCurrencyId: toCurrency?.id, hardwareWalletType: deviceInfo?.modelId, swapType: quoteId ? "fixed" : "float", swapAppVersion, fromAccountAddress, toAccountAddress, refundAddress, payoutAddress, fromAmount, seedIdFrom: mainFromAccount.seedIdentifier, seedIdTo: toParentAccount?.seedIdentifier || toAccount?.seedIdentifier, data: transaction.data ? `0x${(0, hw_app_eth_1.padHexString)(transaction.data?.toString("hex") || "")}` : "0x", flags, }); reject(completeExchangeError); }, })); } catch (error) { // Skip DrawerClosedError // do not redirect to the error screen if (isDrawerClosedError(error)) { throw error; } // Global catch for any errors during the swap process // moved out as sonarcloud suggested to avoid 4 level nested functions const createErrorRejector = (error, reject) => { return () => reject(error); }; const displayError = (error) => new Promise((resolve, reject) => { const rejectWithError = createErrorRejector(error, reject); uiError({ error, onSuccess: rejectWithError, onCancel: rejectWithError, }); }); await (0, handleSwapErrors_1.handleErrors)(error, { onDisplayError: displayError, }); throw error; } }), "custom.isReady": (0, wallet_api_server_1.customWrapper)(async () => { return new Promise((resolve, reject) => uiIsReady({ onSuccess: () => { resolve(); }, onCancel: () => { reject(); }, })); }), }); exports.handlers = handlers; async function extractSwapStartParam(params, accounts) { if (!("fromAccountId" in params && "toAccountId" in params)) { throw new error_1.ExchangeError((0, error_1.createWrongSwapParams)(params)); } const realFromAccountId = (0, converters_1.getAccountIdFromWalletAccountId)(params.fromAccountId); if (!realFromAccountId) { throw new error_1.ExchangeError((0, error_1.createAccounIdNotFound)(params.fromAccountId)); } const fromAccount = accounts.find(acc => acc.id === realFromAccountId); if (!fromAccount) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.fromAccountId)); } let toAccount; if (params.exchangeType === "SWAP" && params.toAccountId) { const realToAccountId = (0, converters_1.getAccountIdFromWalletAccountId)(params.toAccountId); if (!realToAccountId) { throw new error_1.ExchangeError((0, error_1.createAccounIdNotFound)(params.toAccountId)); } toAccount = accounts.find(a => a.id === realToAccountId); if (!toAccount) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.toAccountId)); } } const fromParentAccount = (0, index_1.getParentAccount)(fromAccount, accounts); const toParentAccount = toAccount ? (0, index_1.getParentAccount)(toAccount, accounts) : undefined; const currency = params.tokenCurrency ? await (0, state_1.getCryptoAssetsStore)().findTokenById(params.tokenCurrency) : null; const newTokenAccount = currency ? (0, index_1.makeEmptyTokenAccount)(toAccount, currency) : null; return { exchangeType: params.exchangeType, provider: params.provider, exchange: { fromAccount, fromParentAccount, fromCurrency: (0, types_live_1.getCurrencyForAccount)(fromAccount), toAccount: newTokenAccount ? newTokenAccount : toAccount, toParentAccount: toParentAccount, toCurrency: (0, types_live_1.getCurrencyForAccount)(newTokenAccount ? newTokenAccount : toAccount), }, }; } function extractSellStartParam(params, accounts) { if (!("provider" in params)) { throw new error_1.ExchangeError((0, error_1.createWrongSellParams)(params)); } if (!params.fromAccountId) { return { exchangeType: params.exchangeType, provider: params.provider, }; } const realFromAccountId = (0, converters_1.getAccountIdFromWalletAccountId)(params?.fromAccountId); if (!realFromAccountId) { throw new error_1.ExchangeError((0, error_1.createAccounIdNotFound)(params.fromAccountId)); } const fromAccount = accounts?.find(acc => acc.id === realFromAccountId); if (!fromAccount) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.fromAccountId)); } const fromParentAccount = (0, index_1.getParentAccount)(fromAccount, accounts); return { exchangeType: params.exchangeType, provider: params.provider, exchange: { fromAccount, fromParentAccount, }, }; } function extractFundStartParam(params, accounts) { if (!("provider" in params)) { throw new error_1.ExchangeError((0, error_1.createWrongFundParams)(params)); } if (!params.fromAccountId) { return { exchangeType: params.exchangeType, provider: params.provider, }; } const realFromAccountId = (0, converters_1.getAccountIdFromWalletAccountId)(params?.fromAccountId); if (!realFromAccountId) { throw new error_1.ExchangeError((0, error_1.createAccounIdNotFound)(params.fromAccountId)); } const fromAccount = accounts?.find(acc => acc.id === realFromAccountId); if (!fromAccount) { throw new wallet_api_core_1.ServerError((0, wallet_api_core_1.createAccountNotFound)(params.fromAccountId)); } const fromParentAccount = (0, index_1.getParentAccount)(fromAccount, accounts); return { exchangeType: params.exchangeType, provider: params.provider, exchange: { fromAccount, fromParentAccount, }, }; } async function getToCurrency(binaryPayload, toAccount, newTokenAccount) { const { payoutAddress: tokenAddress, currencyTo } = await (0, hw_app_exchange_1.decodeSwapPayload)(binaryPayload); // In case of an SPL Token recipient and no TokenAccount exists. if (toAccount.type !== "TokenAccount" && // it must not be a SPL Token toAccount.currency.id === "solana" && // the target account must be a SOL Account tokenAddress !== toAccount.freshAddress) { // tokenAddress is the SPL token mint address for Solana tokens const splTokenCurrency = await (0, state_1.getCryptoAssetsStore)().findTokenByAddressInCurrency(tokenAddress, "solana"); if (splTokenCurrency && splTokenCurrency.ticker === currencyTo) return splTokenCurrency; } return newTokenAccount?.token ?? (0, types_live_1.getCurrencyForAccount)(toAccount); } async function getStrategy({ recipient, amount, currency, customFeeConfig, payinExtraId, extraTransactionParameters, sponsored, }, customErrorType) { const family = currency.type === "TokenCurrency" ? currency.parentCurrency?.family : currency.family; if (!family) { throw new Error(`TokenCurrency missing parentCurrency family: ${currency.id}`); } // Remove unsupported utxoStrategy for now if (customFeeConfig?.utxoStrategy) { delete customFeeConfig.utxoStrategy; } const strategy = transactionStrategies_1.transactionStrategy?.[family]; if (!strategy) { throw new Error(`No transaction strategy found for family: ${family}`); } // Convert customFeeConfig values to BigNumber const convertedCustomFeeConfig = {}; if (customFeeConfig) { for (const [key, value] of Object.entries(customFeeConfig)) { convertedCustomFeeConfig[key] = new bignumber_js_1.BigNumber(value?.toString() || 0); } } return strategy({ family, amount: new bignumber_js_1.BigNumber(amount), recipient, customFeeConfig: convertedCustomFeeConfig, payinExtraId, extraTransactionParameters, customErrorType, sponsored, }); } function isDrawerClosedError(error) { if (!error || typeof error !== "object") return false; const details = (0, error_2.getErrorDetails)(error); return details.name === "DrawerClosedError" || details.cause?.name === "DrawerClosedError"; } //# sourceMappingURL=server.js.map