@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
520 lines • 24.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.handlers = exports.ExchangeType = void 0;
/* eslint-disable no-console */
const index_1 = require("@ledgerhq/coin-framework/account/index");
const cryptoassets_1 = require("@ledgerhq/cryptoassets");
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 handlers = ({ accounts, tracking, manifest, 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 = 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 = (0, cryptoassets_1.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) => {
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, } = params;
const trackingParams = {
provider: params.provider,
exchangeType: params.exchangeType,
};
tracking.startExchangeRequested(trackingParams);
const exchangeStartParams = 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);
},
});
});
};
const { transactionId, device: deviceInfo } = await startExchange();
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,
}).catch((error) => {
throw error;
});
// Complete Swap
const trackingCompleteParams = {
provider: params.provider,
exchangeType: params.exchangeType,
};
tracking.completeExchangeRequested(trackingCompleteParams);
const strategyData = {
recipient: payinAddress,
amount: fromAmountAtomic,
currency: fromCurrency,
customFeeConfig: customFeeConfig ?? {},
payinExtraId,
extraTransactionParameters,
};
const transaction = await getStrategy(strategyData, "swap").catch(async (error) => {
throw error;
});
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,
},
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,
});
resolve({ operationHash, swapId });
},
onCancel: error => {
(0, swap_1.postSwapCancelled)({
provider: provider,
swapId: swapId,
swapStep: (0, error_2.getSwapStepFromError)(error),
statusCode: error.name,
errorMessage: error.message,
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,
});
reject(error);
},
}));
}),
"custom.isReady": (0, wallet_api_server_1.customWrapper)(async () => {
return new Promise((resolve, reject) => uiIsReady({
onSuccess: () => {
resolve();
},
onCancel: () => {
reject();
},
}));
}),
});
exports.handlers = handlers;
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 ? (0, cryptoassets_1.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 no be a SPL Token
toAccount.currency.id === "solana" && // the target account must be a SOL Account
tokenAddress !== toAccount.freshAddress) {
const splTokenCurrency = (0, cryptoassets_1.listTokensForCryptoCurrency)(toAccount.currency).find(tk => tk.tokenType === "spl" && tk.ticker === currencyTo);
return splTokenCurrency;
}
return newTokenAccount?.token ?? (0, types_live_1.getCurrencyForAccount)(toAccount);
}
async function getStrategy({ recipient, amount, currency, customFeeConfig, payinExtraId, extraTransactionParameters, }, 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);
}
}
try {
return await strategy({
family,
amount: new bignumber_js_1.BigNumber(amount),
recipient,
customFeeConfig: convertedCustomFeeConfig,
payinExtraId,
extraTransactionParameters,
customErrorType,
});
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
throw new Error(`Failed to execute transaction strategy for family: ${family}. Reason: ${errorMessage}`);
}
}
//# sourceMappingURL=server.js.map