@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
177 lines (159 loc) • 6.1 kB
text/typescript
import { AccountBridge } from "@ledgerhq/types-live";
import { getAlpacaApi } from "./alpaca";
import { getBridgeApi } from "./bridge";
import { bigNumberToBigIntDeep, extractBalances, transactionToIntent } from "./utils";
import BigNumber from "bignumber.js";
import type { AssetInfo, FeeEstimation } from "@ledgerhq/coin-framework/api/types";
import { decodeTokenAccountId } from "@ledgerhq/ledger-wallet-framework/account/index";
import type { TokenCurrency } from "@ledgerhq/types-cryptoassets";
import type { GenericTransaction } from "./types";
function bnEq(a: BigNumber | null | undefined, b: BigNumber | null | undefined): boolean {
return !a && !b ? true : !a || !b ? false : a.eq(b);
}
function assetInfosFallback(transaction: GenericTransaction): {
assetReference: string;
assetOwner: string;
} {
return {
assetReference: transaction.assetReference ?? "",
assetOwner: transaction.assetOwner ?? "",
};
}
function propagateField(estimation: FeeEstimation, field: string, dest: GenericTransaction): void {
const value = estimation?.parameters?.[field];
if (typeof value !== "bigint" && typeof value !== "number" && typeof value !== "string") return;
switch (field) {
case "type":
dest[field] = Number(value.toString());
return;
case "storageLimit":
case "gasLimit":
case "gasPrice":
case "maxFeePerGas":
case "maxPriorityFeePerGas":
case "additionalFees":
dest[field] = new BigNumber(value.toString());
return;
default:
return;
}
}
export function genericPrepareTransaction(
network: string,
kind: string,
): AccountBridge<GenericTransaction>["prepareTransaction"] {
return async (account, transaction) => {
const { computeIntentType, estimateFees, validateIntent } = getAlpacaApi(
account.currency.id,
kind,
);
const bridgeApi = getBridgeApi(account.currency, network);
const getAssetFromTokenForCurrency = bridgeApi.getAssetFromToken;
const { assetReference, assetOwner } = getAssetFromTokenForCurrency
? await getAssetInfos(transaction, account.freshAddress, getAssetFromTokenForCurrency)
: assetInfosFallback(transaction);
const customParametersFees = transaction.customFees?.parameters?.fees;
/**
* Ticking `useAllAmount` constantly resets the amount to 0. This is problematic
* because some Blockchain need the actual transaction amount to compute the fees
* (Example with EVM and ERC20 transactions)
* In case of `useAllAmount` and token transaction, we read the token account spendable
* balance instead.
*/
let amount = transaction.amount;
if (transaction.useAllAmount && transaction.subAccountId) {
const subAccount = account.subAccounts?.find(acc => acc.id === transaction.subAccountId);
amount = subAccount?.spendableBalance ?? amount;
}
// Pass any parameters that help estimating fees
// This includes `assetOwner` and `assetReference` that are not used by some apps that only rely on `subAccountId`
// TODO Remove `assetOwner` and `assetReference` in order to maintain one unique way of identifying the type of asset
// https://ledgerhq.atlassian.net/browse/LIVE-24044
const intent = transactionToIntent(
account,
{
...transaction,
assetOwner,
assetReference,
amount,
},
computeIntentType,
);
const customFeesParameters = bigNumberToBigIntDeep({
gasPrice: transaction.gasPrice,
maxFeePerGas: transaction.maxFeePerGas,
maxPriorityFeePerGas: transaction.maxPriorityFeePerGas,
gasLimit: transaction.customGasLimit,
gasOptions: transaction.gasOptions,
});
const estimation: FeeEstimation = customParametersFees
? { value: BigInt(customParametersFees.toFixed()) }
: await estimateFees(intent, customFeesParameters);
const fees = new BigNumber(estimation.value.toString());
if (!bnEq(transaction.fees, fees)) {
const next: GenericTransaction = {
...transaction,
fees,
assetReference,
assetOwner,
customFees: {
parameters: {
fees: customParametersFees ? new BigNumber(customParametersFees.toString()) : undefined,
},
},
};
// Propagate needed fields
const fieldsToPropagate = [
"type",
"storageLimit",
"gasPrice",
// gas limit must not change in case it is custom
...(transaction.customGasLimit ? [] : ["gasLimit"]),
"maxFeePerGas",
"maxPriorityFeePerGas",
"additionalFees",
];
for (const field of fieldsToPropagate) {
propagateField(estimation, field, next);
}
// align with stellar/xrp: when send max (or staking intents), reflect validated amount in UI
if (transaction.useAllAmount || ["stake", "unstake"].includes(transaction.mode ?? "")) {
// TODO Remove the call to `validateIntent` https://ledgerhq.atlassian.net/browse/LIVE-22228
const { amount } = await validateIntent(
transactionToIntent(
account,
{
...transaction,
assetOwner,
assetReference,
},
computeIntentType,
),
extractBalances(account, getAssetFromTokenForCurrency),
);
next.amount = new BigNumber(amount.toString());
}
return next;
}
return transaction;
};
}
export async function getAssetInfos(
tr: GenericTransaction,
owner: string,
getAssetFromToken: (token: TokenCurrency, owner: string) => AssetInfo,
): Promise<{
assetReference: string;
assetOwner: string;
}> {
if (tr.subAccountId) {
const { token } = await decodeTokenAccountId(tr.subAccountId);
if (!token) return assetInfosFallback(tr);
const asset = getAssetFromToken(token, owner);
return {
assetOwner: ("assetOwner" in asset && asset.assetOwner) || "",
assetReference: ("assetReference" in asset && asset.assetReference) || "",
};
}
return assetInfosFallback(tr);
}