@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
944 lines (835 loc) • 31 kB
text/typescript
/* eslint-disable no-console */
import {
getMainAccount,
getParentAccount,
makeEmptyTokenAccount,
} from "@ledgerhq/ledger-wallet-framework/account/index";
import { getCryptoAssetsStore } from "@ledgerhq/cryptoassets/state";
import { decodeSwapPayload } from "@ledgerhq/hw-app-exchange";
import { CryptoOrTokenCurrency } from "@ledgerhq/types-cryptoassets";
import { Account, AccountLike, getCurrencyForAccount, TokenAccount } from "@ledgerhq/types-live";
import {
createAccountNotFound,
createCurrencyNotFound,
createUnknownError,
deserializeTransaction,
ServerError,
} from "@ledgerhq/wallet-api-core";
import {
ExchangeCompleteParams,
ExchangeCompleteResult,
ExchangeStartParams,
ExchangeStartResult,
ExchangeStartSellParams,
ExchangeStartSwapParams,
ExchangeStartFundParams,
ExchangeSwapParams,
ExchangeType,
SwapLiveError,
SwapResult,
} from "@ledgerhq/wallet-api-exchange-module";
import { customWrapper, RPCHandler } 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 { ExchangeSwap, FeatureFlags } from "../../exchange/swap/types";
import { Exchange } from "../../exchange/types";
import { Transaction } from "../../generated/types";
import {
getAccountIdFromWalletAccountId,
getWalletAPITransactionSignFlowInfos,
} from "../converters";
import { AppManifest } from "../types";
import {
createAccounIdNotFound,
createWrongSellParams,
createWrongSwapParams,
createWrongFundParams,
ExchangeError,
} from "./error";
import { TrackingAPI } from "./tracking";
import { CompleteExchangeError, getErrorDetails, getSwapStepFromError } from "../../exchange/error";
import { postSwapCancelled } from "../../exchange/swap";
import { DeviceModelId } from "@ledgerhq/types-devices";
import { setBroadcastTransaction } from "../../exchange/swap/setBroadcastTransaction";
import { Transaction as EvmTransaction } from "@ledgerhq/coin-evm/types/index";
import { padHexString } from "@ledgerhq/hw-app-eth";
import { createStepError, StepError, toError } from "./parser";
import { handleErrors } from "./handleSwapErrors";
import get from "lodash/get";
import { SwapError } from "./SwapError";
export { ExchangeType };
type Handlers = {
"custom.exchange.start": RPCHandler<
ExchangeStartResult,
ExchangeStartParams | ExchangeStartSwapParams | ExchangeStartSellParams
>;
"custom.exchange.complete": RPCHandler<ExchangeCompleteResult, ExchangeCompleteParams>;
"custom.exchange.error": RPCHandler<void, SwapLiveError>;
"custom.isReady": RPCHandler<void, void>;
"custom.exchange.swap": RPCHandler<SwapResult, ExchangeSwapParams>;
};
export type CompleteExchangeUiRequest = {
provider: string;
exchange: Exchange;
transaction: Transaction;
binaryPayload: string;
signature: string;
feesStrategy: string;
exchangeType: number;
swapId?: string;
amountExpectedTo?: number;
magnitudeAwareRate?: BigNumber;
refundAddress?: string;
payoutAddress?: string;
sponsored?: boolean;
isEmbeddedSwap?: boolean;
};
type FundStartParamsUiRequest = {
exchangeType: "FUND";
provider: string;
exchange: Partial<Exchange> | undefined;
};
type SellStartParamsUiRequest = {
exchangeType: "SELL";
provider: string;
exchange: Partial<Exchange> | undefined;
};
type SwapStartParamsUiRequest = {
exchangeType: "SWAP";
provider: string;
exchange: Partial<ExchangeSwap>;
};
type ExchangeStartParamsUiRequest =
| FundStartParamsUiRequest
| SellStartParamsUiRequest
| SwapStartParamsUiRequest;
export type SwapUiRequest = CompleteExchangeUiRequest & {
provider?: string;
fromAccountId?: string;
toAccountId?: string;
tokenCurrency?: string;
};
type ExchangeUiHooks = {
"custom.exchange.start": (params: {
exchangeParams: ExchangeStartParamsUiRequest;
onSuccess: (nonce: string, device?: ExchangeStartResult["device"]) => void;
onCancel: (error: Error, device?: ExchangeStartResult["device"]) => void;
}) => void;
"custom.exchange.complete": (params: {
exchangeParams: CompleteExchangeUiRequest;
onSuccess: (hash: string) => void;
onCancel: (error: Error) => void;
}) => void;
"custom.exchange.error": (params: {
error: SwapLiveError | undefined;
onSuccess: () => void;
onCancel: () => void;
}) => void;
"custom.isReady": (params: { onSuccess: () => void; onCancel: () => void }) => void;
"custom.exchange.swap": (params: {
exchangeParams: SwapUiRequest;
onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => void;
onCancel: (error: Error) => void;
}) => void;
};
export 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,
},
}: {
accounts: AccountLike[];
tracking: TrackingAPI;
manifest: AppManifest;
flags?: FeatureFlags;
uiHooks: ExchangeUiHooks;
}) =>
({
"custom.exchange.start": customWrapper<ExchangeStartParams, ExchangeStartResult>(
async params => {
if (!params) {
tracking.startExchangeNoParams(manifest);
return { transactionId: "" };
}
const trackingParams = {
provider: params.provider,
exchangeType: params.exchangeType,
};
tracking.startExchangeRequested(trackingParams);
let exchangeParams: ExchangeStartParamsUiRequest;
// 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: string, device) => {
tracking.startExchangeSuccess(trackingParams);
resolve({ transactionId: nonce, device });
},
onCancel: error => {
tracking.startExchangeFail(trackingParams);
reject(error);
},
}),
);
},
),
"custom.exchange.complete": customWrapper<ExchangeCompleteParams, ExchangeCompleteResult>(
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: 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: TokenAccount | undefined;
if (params.tokenCurrency) {
const currency = await getCryptoAssetsStore().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: string) => {
tracking.completeExchangeSuccess({
...trackingParams,
currency: params.rawTransaction.family,
});
resolve({ transactionHash });
},
onCancel: error => {
tracking.completeExchangeFail(trackingParams);
reject(error);
},
}),
);
},
),
"custom.exchange.error": customWrapper<SwapLiveError, void>(async params => {
return new Promise((resolve, reject) =>
uiError({
error: params,
onSuccess: () => {
resolve();
},
onCancel: () => {
reject();
},
}),
);
}),
"custom.exchange.swap": customWrapper<ExchangeSwapParams, SwapResult>(async params => {
try {
if (!params) {
tracking.startExchangeNoParams(manifest);
throw new ServerError(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: ExchangeStartParamsUiRequest = (await extractSwapStartParam(
params,
accounts,
)) as SwapStartParamsUiRequest;
const {
fromCurrency,
fromAccount,
fromParentAccount,
toCurrency,
toAccount,
toParentAccount,
} = exchangeStartParams.exchange;
if (!fromAccount || !fromCurrency) {
throw new ServerError(createAccountNotFound(params.fromAccountId));
}
const fromAccountAddress = fromParentAccount
? fromParentAccount.freshAddress
: (fromAccount as Account).freshAddress;
const toAccountAddress = toParentAccount
? toParentAccount.freshAddress
: (toAccount as Account).freshAddress;
// Step 1: Open the drawer and open exchange app
const startExchange = async () => {
return new Promise<{ transactionId: string; device?: ExchangeStartResult["device"] }>(
(resolve, reject) => {
uiExchangeStart({
exchangeParams: exchangeStartParams,
onSuccess: (nonce, device) => {
tracking.startExchangeSuccess(trackingParams);
resolve({ transactionId: nonce, device });
},
onCancel: error => {
tracking.startExchangeFail(trackingParams);
reject(error);
},
});
},
);
};
let transactionId: string;
let deviceInfo: ExchangeStartResult["device"];
try {
const result = await startExchange();
transactionId = result.transactionId;
deviceInfo = result.device;
} catch (error) {
const rawError = get(error, "response.data.error", error);
const wrappedError = createStepError({
error: toError(rawError),
step: 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 retrieveSwapPayload({
provider,
deviceTransactionId: transactionId,
fromAccountAddress,
toAccountAddress,
fromAccountCurrency: fromCurrency!.id,
toAccountCurrency: toCurrency!.id,
amount: fromAmount,
amountInAtomicUnit: fromAmountAtomic,
quoteId,
toNewTokenId,
flags,
}).catch((error: Error) => {
const wrappedError = createStepError({
error: get(error, "response.data.error", error),
step: 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 as CryptoOrTokenCurrency,
customFeeConfig: customFeeConfig ?? {},
payinExtraId,
extraTransactionParameters,
sponsored,
};
const transaction: Transaction = await getStrategy(strategyData, "swap");
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,
sponsored,
isEmbeddedSwap: isEmbedded,
},
onSuccess: ({ operationHash, swapId }: { operationHash: string; swapId: string }) => {
tracking.completeExchangeSuccess({
...trackingParams,
currency: transaction.family,
});
setBroadcastTransaction({
provider,
result: { operation: operationHash, swapId },
sourceCurrencyId: fromCurrency.id,
targetCurrencyId: toCurrency?.id,
hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
swapAppVersion,
fromAccountAddress,
toAccountAddress,
fromAmount,
flags,
});
resolve({ operationHash, swapId });
},
onCancel: error => {
const {
name: rawErrorName,
message: rawErrorMessage,
cause: rawErrorCause,
} = 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 CompleteExchangeError
? error
: new CompleteExchangeError("INIT", rawErrorName, errorMessageWithCause);
postSwapCancelled({
provider: provider,
swapId: swapId,
swapStep: getSwapStepFromError(completeExchangeError),
statusCode: completeExchangeError.title || completeExchangeError.name,
errorMessage: completeExchangeError.message || errorMessageWithCause,
sourceCurrencyId: fromCurrency.id,
targetCurrencyId: toCurrency?.id,
hardwareWalletType: deviceInfo?.modelId as DeviceModelId,
swapType: quoteId ? "fixed" : "float",
swapAppVersion,
fromAccountAddress,
toAccountAddress,
refundAddress,
payoutAddress,
fromAmount,
seedIdFrom: mainFromAccount.seedIdentifier,
seedIdTo: toParentAccount?.seedIdentifier || (toAccount as Account)?.seedIdentifier,
data: (transaction as EvmTransaction).data
? `0x${padHexString((transaction as EvmTransaction).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: SwapError, reject: (error: SwapError) => void) => {
return () => reject(error);
};
const displayError = (error: SwapError): Promise<void> =>
new Promise((resolve, reject) => {
const rejectWithError = createErrorRejector(error, reject);
uiError({
error,
onSuccess: rejectWithError,
onCancel: rejectWithError,
});
});
await handleErrors(error, {
onDisplayError: displayError,
});
throw error;
}
}),
"custom.isReady": customWrapper<void, void>(async () => {
return new Promise((resolve, reject) =>
uiIsReady({
onSuccess: () => {
resolve();
},
onCancel: () => {
reject();
},
}),
);
}),
}) as const satisfies Handlers;
async function extractSwapStartParam(
params: ExchangeStartSwapParams,
accounts: AccountLike[],
): Promise<ExchangeStartParamsUiRequest> {
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
? await getCryptoAssetsStore().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: ExchangeStartSellParams,
accounts: AccountLike[],
): ExchangeStartParamsUiRequest {
if (!("provider" in params)) {
throw new ExchangeError(createWrongSellParams(params));
}
if (!params.fromAccountId) {
return {
exchangeType: params.exchangeType,
provider: params.provider,
} as ExchangeStartParamsUiRequest;
}
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: ExchangeStartFundParams,
accounts: AccountLike[],
): ExchangeStartParamsUiRequest {
if (!("provider" in params)) {
throw new ExchangeError(createWrongFundParams(params));
}
if (!params.fromAccountId) {
return {
exchangeType: params.exchangeType,
provider: params.provider,
} as ExchangeStartParamsUiRequest;
}
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: string,
toAccount: AccountLike,
newTokenAccount?: TokenAccount,
): Promise<CryptoOrTokenCurrency> {
const { payoutAddress: tokenAddress, currencyTo } = await 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 getCryptoAssetsStore().findTokenByAddressInCurrency(
tokenAddress,
"solana",
);
if (splTokenCurrency && splTokenCurrency.ticker === currencyTo) return splTokenCurrency;
}
return newTokenAccount?.token ?? getCurrencyForAccount(toAccount);
}
interface StrategyParams {
recipient: string;
amount: BigNumber | number | string;
currency: CryptoOrTokenCurrency;
customFeeConfig?: Record<string, unknown>;
payinExtraId?: string;
extraTransactionParameters?: string;
sponsored?: boolean;
}
async function getStrategy(
{
recipient,
amount,
currency,
customFeeConfig,
payinExtraId,
extraTransactionParameters,
sponsored,
}: StrategyParams,
customErrorType?: any,
): Promise<Transaction> {
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: { [key: string]: BigNumber } = {};
if (customFeeConfig) {
for (const [key, value] of Object.entries(customFeeConfig)) {
convertedCustomFeeConfig[key] = new BigNumber(value?.toString() || 0);
}
}
return strategy({
family,
amount: new BigNumber(amount),
recipient,
customFeeConfig: convertedCustomFeeConfig,
payinExtraId,
extraTransactionParameters,
customErrorType,
sponsored,
});
}
function isDrawerClosedError(error: unknown) {
if (!error || typeof error !== "object") return false;
const details = getErrorDetails(error);
return details.name === "DrawerClosedError" || details.cause?.name === "DrawerClosedError";
}