@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
516 lines • 23 kB
JavaScript
/* eslint-disable no-console */
import { getMainAccount, getParentAccount, makeEmptyTokenAccount, } from "@ledgerhq/coin-framework/account/index";
import { findTokenById, listTokensForCryptoCurrency } from "@ledgerhq/cryptoassets";
import { decodeSwapPayload } from "@ledgerhq/hw-app-exchange";
import { getCurrencyForAccount } from "@ledgerhq/types-live";
import { createAccountNotFound, createCurrencyNotFound, createUnknownError, deserializeTransaction, ServerError, } from "@ledgerhq/wallet-api-core";
import { ExchangeType, } from "@ledgerhq/wallet-api-exchange-module";
import { customWrapper } from "@ledgerhq/wallet-api-server";
import { BigNumber } from "bignumber.js";
import { getAccountBridge } from "../../bridge";
import { retrieveSwapPayload } from "../../exchange/swap/api/v5/actions";
import { transactionStrategy } from "../../exchange/swap/transactionStrategies";
import { getAccountIdFromWalletAccountId, getWalletAPITransactionSignFlowInfos, } from "../converters";
import { createAccounIdNotFound, createWrongSellParams, createWrongSwapParams, createWrongFundParams, ExchangeError, } from "./error";
import { getSwapStepFromError } from "../../exchange/error";
import { postSwapCancelled } from "../../exchange/swap";
import { setBroadcastTransaction } from "../../exchange/swap/setBroadcastTransaction";
export { ExchangeType };
export 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": 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": customWrapper(async (params) => {
if (!params) {
tracking.completeExchangeNoParams(manifest);
return { transactionHash: "" };
}
const trackingParams = {
provider: params.provider,
exchangeType: params.exchangeType,
};
tracking.completeExchangeRequested(trackingParams);
const realFromAccountId = 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 ServerError(createAccountNotFound(params.fromAccountId));
}
const fromParentAccount = getParentAccount(fromAccount, accounts);
let exchange;
if (params.exchangeType === "SWAP") {
const realToAccountId = 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 ServerError(createAccountNotFound(params.toAccountId));
}
// TODO: check logic for EmptyTokenAccount
let toParentAccount = getParentAccount(toAccount, accounts);
let newTokenAccount;
if (params.tokenCurrency) {
const currency = findTokenById(params.tokenCurrency);
if (!currency) {
throw new ServerError(createCurrencyNotFound(params.tokenCurrency));
}
if (toAccount.type === "Account") {
newTokenAccount = makeEmptyTokenAccount(toAccount, currency);
toParentAccount = toAccount;
}
else {
newTokenAccount = makeEmptyTokenAccount(toParentAccount, currency);
}
}
const toCurrency = await getToCurrency(params.hexBinaryPayload, toAccount, newTokenAccount);
exchange = {
fromAccount,
fromParentAccount,
fromCurrency: getCurrencyForAccount(fromAccount),
toAccount: newTokenAccount ? newTokenAccount : toAccount,
toParentAccount,
toCurrency,
};
}
else {
exchange = {
fromAccount,
fromParentAccount,
fromCurrency: getCurrencyForAccount(fromAccount),
};
}
const mainFromAccount = getMainAccount(fromAccount, fromParentAccount);
const mainFromAccountFamily = mainFromAccount.currency.family;
const transaction = deserializeTransaction(params.rawTransaction);
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}`));
}
const accountBridge = 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 decodeSwapPayload(params.hexBinaryPayload);
amountExpectedTo = new 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: 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": customWrapper(async (params) => {
return new Promise((resolve, reject) => uiError({
error: params,
onSuccess: () => {
resolve();
},
onCancel: () => {
reject();
},
}));
}),
"custom.exchange.swap": customWrapper(async (params) => {
if (!params) {
tracking.startExchangeNoParams(manifest);
throw new ServerError(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 ServerError(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 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 = 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 = 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 decodeSwapPayload(binaryPayload);
const amountExpectedTo = new 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(tx.amount);
return new Promise((resolve, reject) => uiSwap({
exchangeParams: {
exchangeType: 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,
});
setBroadcastTransaction({
provider,
result: { operation: operationHash, swapId },
sourceCurrencyId: fromCurrency.id,
targetCurrencyId: toCurrency?.id,
hardwareWalletType: deviceInfo?.modelId,
swapAppVersion,
fromAccountAddress,
toAccountAddress,
fromAmount,
});
resolve({ operationHash, swapId });
},
onCancel: error => {
postSwapCancelled({
provider: provider,
swapId: swapId,
swapStep: 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": customWrapper(async () => {
return new Promise((resolve, reject) => uiIsReady({
onSuccess: () => {
resolve();
},
onCancel: () => {
reject();
},
}));
}),
});
function extractSwapStartParam(params, accounts) {
if (!("fromAccountId" in params && "toAccountId" in params)) {
throw new ExchangeError(createWrongSwapParams(params));
}
const realFromAccountId = getAccountIdFromWalletAccountId(params.fromAccountId);
if (!realFromAccountId) {
throw new ExchangeError(createAccounIdNotFound(params.fromAccountId));
}
const fromAccount = accounts.find(acc => acc.id === realFromAccountId);
if (!fromAccount) {
throw new ServerError(createAccountNotFound(params.fromAccountId));
}
let toAccount;
if (params.exchangeType === "SWAP" && params.toAccountId) {
const realToAccountId = getAccountIdFromWalletAccountId(params.toAccountId);
if (!realToAccountId) {
throw new ExchangeError(createAccounIdNotFound(params.toAccountId));
}
toAccount = accounts.find(a => a.id === realToAccountId);
if (!toAccount) {
throw new ServerError(createAccountNotFound(params.toAccountId));
}
}
const fromParentAccount = getParentAccount(fromAccount, accounts);
const toParentAccount = toAccount ? getParentAccount(toAccount, accounts) : undefined;
const currency = params.tokenCurrency ? findTokenById(params.tokenCurrency) : null;
const newTokenAccount = currency ? makeEmptyTokenAccount(toAccount, currency) : null;
return {
exchangeType: params.exchangeType,
provider: params.provider,
exchange: {
fromAccount,
fromParentAccount,
fromCurrency: getCurrencyForAccount(fromAccount),
toAccount: newTokenAccount ? newTokenAccount : toAccount,
toParentAccount: toParentAccount,
toCurrency: getCurrencyForAccount(newTokenAccount ? newTokenAccount : toAccount),
},
};
}
function extractSellStartParam(params, accounts) {
if (!("provider" in params)) {
throw new ExchangeError(createWrongSellParams(params));
}
if (!params.fromAccountId) {
return {
exchangeType: params.exchangeType,
provider: params.provider,
};
}
const realFromAccountId = getAccountIdFromWalletAccountId(params?.fromAccountId);
if (!realFromAccountId) {
throw new ExchangeError(createAccounIdNotFound(params.fromAccountId));
}
const fromAccount = accounts?.find(acc => acc.id === realFromAccountId);
if (!fromAccount) {
throw new ServerError(createAccountNotFound(params.fromAccountId));
}
const fromParentAccount = getParentAccount(fromAccount, accounts);
return {
exchangeType: params.exchangeType,
provider: params.provider,
exchange: {
fromAccount,
fromParentAccount,
},
};
}
function extractFundStartParam(params, accounts) {
if (!("provider" in params)) {
throw new ExchangeError(createWrongFundParams(params));
}
if (!params.fromAccountId) {
return {
exchangeType: params.exchangeType,
provider: params.provider,
};
}
const realFromAccountId = getAccountIdFromWalletAccountId(params?.fromAccountId);
if (!realFromAccountId) {
throw new ExchangeError(createAccounIdNotFound(params.fromAccountId));
}
const fromAccount = accounts?.find(acc => acc.id === realFromAccountId);
if (!fromAccount) {
throw new ServerError(createAccountNotFound(params.fromAccountId));
}
const fromParentAccount = getParentAccount(fromAccount, accounts);
return {
exchangeType: params.exchangeType,
provider: params.provider,
exchange: {
fromAccount,
fromParentAccount,
},
};
}
async function getToCurrency(binaryPayload, toAccount, newTokenAccount) {
const { payoutAddress: tokenAddress, currencyTo } = await 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 = listTokensForCryptoCurrency(toAccount.currency).find(tk => tk.tokenType === "spl" && tk.ticker === currencyTo);
return splTokenCurrency;
}
return newTokenAccount?.token ?? 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 = 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(value?.toString() || 0);
}
}
try {
return await strategy({
family,
amount: new 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