@ledgerhq/live-common
Version:
Common ground for the Ledger Live apps
429 lines • 16.6 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildOptimisticOperation = void 0;
exports.bigNumberToBigIntDeep = bigNumberToBigIntDeep;
exports.findCryptoCurrencyByNetwork = findCryptoCurrencyByNetwork;
exports.extractBalance = extractBalance;
exports.extractBalances = extractBalances;
exports.cleanedOperation = cleanedOperation;
exports.adaptCoreOperationToLiveOperation = adaptCoreOperationToLiveOperation;
exports.transactionToIntent = transactionToIntent;
exports.applyMemoToIntent = applyMemoToIntent;
const operation_1 = require("@ledgerhq/ledger-wallet-framework/operation");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const utils_1 = require("@ledgerhq/coin-framework/utils");
const currencies_1 = require("@ledgerhq/cryptoassets/currencies");
function bigNumberToBigIntDeep(obj) {
if (bignumber_js_1.default.isBigNumber(obj))
return BigInt(obj.toFixed());
if (Array.isArray(obj))
return obj.map(bigNumberToBigIntDeep);
if (!!obj && typeof obj === "object")
return Object.fromEntries(Object.entries(obj)
.filter(([_, value]) => value !== undefined)
.map(([key, value]) => [key, bigNumberToBigIntDeep(value)]));
return obj;
}
function findCryptoCurrencyByNetwork(network) {
const networksRemap = {
xrp: "ripple",
};
return (0, currencies_1.findCryptoCurrencyById)(networksRemap[network] ?? network);
}
function extractBalance(balances, type) {
return (balances.find(balance => balance.asset.type === type) ?? {
asset: { type },
value: 0n,
});
}
function extractBalances(account, getAssetFromToken) {
const balances = [
{
value: BigInt(account.balance.toFixed()),
asset: { type: "native" },
locked: BigInt(account.balance.minus(account.spendableBalance).toFixed()),
},
];
if (!account.subAccounts?.length || !getAssetFromToken) {
return balances;
}
for (const subAccount of account.subAccounts) {
const asset = getAssetFromToken(subAccount.token, account.freshAddress);
balances.push({
value: BigInt(subAccount.balance.toFixed()),
asset,
locked: BigInt(subAccount.balance.minus(subAccount.spendableBalance).toFixed()),
});
}
return balances;
}
function isStringArray(value) {
return Array.isArray(value) && value.every(item => typeof item === "string");
}
function cleanedOperation(operation) {
if (!operation.extra)
return operation;
const extraToClean = new Set([
"assetReference",
"assetAmount",
"assetOwner",
"assetSenders",
"assetRecipients",
"parentSenders",
"parentRecipients",
"ledgerOpType",
]);
const cleanedExtra = Object.fromEntries(Object.entries(operation.extra).filter(([key]) => !extraToClean.has(key)));
return { ...operation, extra: cleanedExtra };
}
function adaptCoreOperationToLiveOperation(accountId, op) {
const opType = op.type;
const extra = {};
if (op.details?.ledgerOpType !== undefined) {
extra.ledgerOpType = op.details.ledgerOpType;
}
if (op.details?.assetAmount !== undefined) {
extra.assetAmount = op.details.assetAmount;
}
if (isStringArray(op.details?.assetSenders)) {
extra.assetSenders = op.details?.assetSenders;
}
if (isStringArray(op.details?.assetRecipients)) {
extra.assetRecipients = op.details?.assetRecipients;
}
if (isStringArray(op.details?.parentSenders)) {
extra.parentSenders = op.details?.parentSenders;
}
if (isStringArray(op.details?.parentRecipients)) {
extra.parentRecipients = op.details?.parentRecipients;
}
if (op.asset?.type !== "native") {
extra.assetReference =
"assetReference" in (op.asset ?? {}) ? op.asset.assetReference : "";
extra.assetOwner = "assetOwner" in (op.asset ?? {}) ? op.asset.assetOwner : "";
}
if (op.details?.memo) {
extra.memo = op.details.memo;
}
if (op.details?.internal === true) {
extra.internal = op.details?.internal;
}
if (typeof op.tx.feesPayer === "string") {
extra.feePayer = op.tx.feesPayer;
}
const bnFees = new bignumber_js_1.default(op.tx.fees.toString());
const hasFailed = op.tx.failed;
let value;
if (hasFailed) {
value = bnFees;
}
else if (op.asset.type === "native" &&
["OUT", "FEES", "DELEGATE", "UNDELEGATE"].includes(opType)) {
value = new bignumber_js_1.default(op.value.toString()).plus(bnFees);
}
else {
value = new bignumber_js_1.default(op.value.toString());
}
const res = {
id: (0, operation_1.encodeOperationId)(accountId, op.tx.hash, op.type),
hash: op.tx.hash,
accountId,
type: opType,
value,
fee: bnFees,
blockHash: op.tx.block.hash,
blockHeight: op.tx.block.height,
senders: extra.parentSenders ?? op.senders,
recipients: extra.parentRecipients ?? op.recipients,
date: op.tx.date,
transactionSequenceNumber: op.details?.sequence
? new bignumber_js_1.default(op.details?.sequence.toString())
: undefined,
hasFailed,
extra,
};
return res;
}
/**
* Default implementation of `computeIntentType` is a simple whitelist
* with a fallback to "Payment"
*/
function defaultComputeIntentType(transaction) {
if (!transaction.mode)
return "Payment"; // NOTE: assuming payment by default here, can be changed based on transaction.mode
const modeRemap = {
delegate: "stake",
undelegate: "unstake",
};
const mode = modeRemap[transaction.mode] ?? transaction.mode;
if (["changeTrust", "send", "send-legacy", "send-eip1559", "stake", "unstake"].includes(mode))
return mode;
throw new Error(`Unsupported transaction mode: ${transaction.mode}`);
}
/**
* Converts a transaction object into a `TransactionIntent` object, which is used to represent
* the intent of a transaction in a standardized format.
*
* @template MemoType - The type of memo supported by the transaction, defaults to `MemoNotSupported`.
*
* @param account - The account initiating the transaction. Contains details such as the sender's address.
* @param transaction - The transaction object containing details about the operation to be performed.
* - `assetOwner` (optional): The issuer of the asset, if applicable.
* - `assetReference` (optional): The code of the asset, if applicable.
* - `mode` (optional): The mode of the transaction, e.g., "changetrust" or "send".
* - `fees` (optional): The fees associated with the transaction.
* - `memoType` (optional): The type of memo to attach to the transaction.
* - `memoValue` (optional): The value of the memo to attach to the transaction.
* @param computeIntentType - An optional function to compute the intent type that supersedes the default implementation if present
*
* @returns A `TransactionIntent` object containing the standardized representation of the transaction.
* - Includes details such as type, sender, recipient, amount, fees, asset, and an optional memo.
* - If `assetReference` and `assetOwner` are provided, the asset is represented as a token.
* - If `memoType` and `memoValue` are provided, a memo is included; otherwise, a default memo of type "NO_MEMO" is added.
*
* @throws An error if the transaction mode is unsupported.
*/
function transactionToIntent(account, transaction, computeIntentType) {
const intentType = (computeIntentType ?? defaultComputeIntentType)(transaction);
const isStaking = ["stake", "unstake"].includes(intentType);
const amount = isStaking ? 0n : (0, utils_1.fromBigNumberToBigInt)(transaction.amount, 0n);
const useAllAmount = isStaking || !!transaction.useAllAmount;
const res = {
intentType: isStaking ? "staking" : "transaction",
type: intentType,
sender: account.freshAddress,
recipient: transaction.recipient,
amount,
asset: { type: "native", name: account.currency.name, unit: account.currency.units[0] },
useAllAmount,
feesStrategy: transaction.feesStrategy ?? undefined,
data: Buffer.isBuffer(transaction.data)
? { type: "buffer", value: transaction.data }
: { type: "none" },
sequence: transaction.nonce !== null && transaction.nonce !== undefined
? BigInt(transaction.nonce.toString())
: undefined,
sponsored: transaction.sponsored,
};
if (transaction.assetReference && transaction.assetOwner) {
const { subAccountId } = transaction;
const { subAccounts } = account;
const tokenAccount = subAccountId ? subAccounts?.find(ta => ta.id === subAccountId) : null;
res.asset = {
type: tokenAccount?.token.tokenType ?? "token",
assetReference: transaction.assetReference,
name: tokenAccount?.token.name ?? transaction.assetReference, // NOTE: for stellar, assetReference = tokenAccount.name, this is futureproofing
unit: account.currency.units[0],
assetOwner: transaction.assetOwner,
};
}
if (transaction.memoType && transaction.memoValue) {
res.memo = {
type: transaction.memoType,
value: transaction.memoValue,
};
}
else {
res.memo = { type: "NO_MEMO" };
}
return res;
}
function toFeeDataRaw(data) {
return {
gasPrice: data.gasPrice?.toFixed() ?? null,
maxFeePerGas: data.maxFeePerGas?.toFixed() ?? null,
maxPriorityFeePerGas: data.maxPriorityFeePerGas?.toFixed() ?? null,
nextBaseFee: data.nextBaseFee?.toFixed() ?? null,
};
}
function toGasOptionRaw(options) {
return {
fast: toFeeDataRaw(options.fast),
medium: toFeeDataRaw(options.medium),
slow: toFeeDataRaw(options.slow),
};
}
function toGenericTransactionRaw(transaction) {
const raw = {
amount: transaction.amount.toString(),
recipient: transaction.recipient,
family: transaction.family,
};
const booleanFieldsToPropagate = ["useAllAmount", "sponsored"];
for (const field of booleanFieldsToPropagate) {
if (field in transaction) {
raw[field] = transaction[field];
}
}
const stringFieldsToPropagate = [
"memoType",
"memoValue",
"assetReference",
"assetOwner",
];
for (const field of stringFieldsToPropagate) {
if (field in transaction) {
raw[field] = transaction[field];
}
}
const numberFieldsToPropagate = ["tag", "type", "chainId"];
for (const field of numberFieldsToPropagate) {
if (field in transaction) {
raw[field] = transaction[field];
}
}
const bigNumberFieldsToPropagate = [
"fees",
"storageLimit",
"nonce",
"gasLimit",
"gasPrice",
"maxFeePerGas",
"maxPriorityFeePerGas",
"additionalFees",
];
for (const field of bigNumberFieldsToPropagate) {
if (field in transaction) {
raw[field] = transaction[field]?.toFixed();
}
}
if ("customFees" in transaction) {
raw.customFees =
transaction.customFees && "fees" in transaction.customFees.parameters
? {
parameters: { fees: transaction.customFees.parameters.fees?.toFixed() },
}
: { parameters: {} };
}
if ("feesStrategy" in transaction) {
raw.feesStrategy = transaction.feesStrategy;
}
if ("mode" in transaction) {
raw.mode = transaction.mode;
}
if ("data" in transaction) {
raw.data = transaction.data?.toString("hex");
}
if ("networkInfo" in transaction) {
raw.networkInfo = transaction.networkInfo && {
fees: transaction.networkInfo.fees.toFixed(),
};
}
if ("gasOptions" in transaction) {
raw.gasOptions = transaction.gasOptions && toGasOptionRaw(transaction.gasOptions);
}
if ("recipientDomain" in transaction) {
raw.recipientDomain = transaction.recipientDomain;
}
return raw;
}
const buildOptimisticOperation = (account, transaction, sequenceNumber) => {
let type;
switch (transaction.mode) {
case "changeTrust":
type = "OPT_IN";
break;
case "delegate":
case "stake":
type = "DELEGATE";
break;
case "undelegate":
case "unstake":
type = "UNDELEGATE";
break;
default:
type = "OUT";
break;
}
const fees = BigInt(transaction.fees?.toString() || "0");
const { subAccountId } = transaction;
const { subAccounts } = account;
const parentType = subAccountId ? "FEES" : type;
const tokenAccount = subAccountId ? subAccounts?.find(ta => ta.id === subAccountId) : null;
const operation = {
id: (0, operation_1.encodeOperationId)(account.id, "", parentType),
hash: "",
type: parentType,
value: subAccountId ? new bignumber_js_1.default(fees.toString()) : transaction.amount, // match old behavior
fee: new bignumber_js_1.default(fees.toString()),
blockHash: null,
blockHeight: null,
senders: [account.freshAddress.toString()],
recipients: [transaction.recipient],
transactionSequenceNumber: new bignumber_js_1.default(sequenceNumber?.toString() ?? 0),
accountId: account.id,
date: new Date(),
transactionRaw: toGenericTransactionRaw({
...transaction,
nonce: sequenceNumber !== undefined ? new bignumber_js_1.default(sequenceNumber.toString()) : undefined,
...(tokenAccount
? { recipient: tokenAccount.token.contractAddress, amount: new bignumber_js_1.default(0) }
: {}),
}),
extra: {
ledgerOpType: type,
blockTime: new Date(),
index: "0",
},
};
if (tokenAccount && subAccountId) {
operation.subOperations = [
{
id: `${subAccountId}--${type}`,
hash: "",
type,
value: transaction.useAllAmount ? tokenAccount.balance : transaction.amount,
fee: new bignumber_js_1.default(fees.toString()),
blockHash: null,
blockHeight: null,
senders: [account.freshAddress],
recipients: [transaction.recipient],
transactionSequenceNumber: new bignumber_js_1.default(sequenceNumber?.toString() ?? 0),
accountId: subAccountId,
date: new Date(),
transactionRaw: toGenericTransactionRaw({
...transaction,
nonce: sequenceNumber !== undefined ? new bignumber_js_1.default(sequenceNumber.toString()) : undefined,
}),
extra: {
ledgerOpType: type,
},
},
];
}
return operation;
};
exports.buildOptimisticOperation = buildOptimisticOperation;
/**
* Applies memo information to transaction intent
* Handles both destination tags (XRP-like) and Stellar-style memos
*/
function applyMemoToIntent(transactionIntent, transaction) {
// Handle destination tag memo (for XRP-like chains)
if (typeof transaction.tag === "number") {
const txWithMemoTag = transactionIntent;
const txMemo = String(transaction.tag);
txWithMemoTag.memo = {
type: "map",
memos: new Map(),
};
txWithMemoTag.memo.memos.set("destinationTag", txMemo);
return txWithMemoTag;
}
// Handle Stellar-style memo
if (transaction.memoType && transaction.memoValue) {
const txWithMemo = transactionIntent;
const txMemoType = String(transaction.memoType);
const txMemoValue = String(transaction.memoValue);
txWithMemo.memo = {
type: txMemoType,
value: txMemoValue,
};
return txWithMemo;
}
return transactionIntent;
}
//# sourceMappingURL=utils.js.map