UNPKG

@ledgerhq/coin-canton

Version:
138 lines 6.33 kB
import { formatCurrencyUnit } from "@ledgerhq/coin-module-framework/currencies/index"; import { AmountRequired, FeeNotLoaded, FeeTooHigh, InvalidAddress, NotEnoughBalance, NotEnoughBalanceBecauseDestinationNotCreated, NotEnoughBalanceInParentAccount, NotEnoughSpendableBalance, RecipientRequired, } from "@ledgerhq/ledger-wallet-framework/errors"; import BigNumber from "bignumber.js"; import { isRecipientValid } from "../common-logic/utils"; import coinConfig from "../config"; import { CANTON_UNSET_RECIPIENT } from "../constants"; import { isTopologyChangeRequiredCached } from "../network/gateway"; import { TooManyUtxosCritical, TooManyUtxosWarning, } from "../types"; import { TopologyChangeError } from "../types/errors"; export const TO_MANY_UTXOS_CRITICAL_COUNT = 24; export const TO_MANY_UTXOS_WARNING_COUNT = 10; export const getTransactionStatus = async (account, transaction) => { const errors = {}; const warnings = {}; // reserveAmount is the minimum amount of currency that an account must hold in order to stay activated const reserveAmount = new BigNumber(coinConfig.getCoinConfig(account.currency.id).minReserve || 0); const estimatedFees = new BigNumber(transaction.fee || 0); const amount = new BigNumber(transaction.amount); const isTokenTransaction = !!transaction.subAccountId; // For token transactions, fees are paid from parent account, so totalSpent is just amount const totalSpent = isTokenTransaction ? amount : amount.plus(estimatedFees); // Get the account balance to check against (subAccount for tokens, main account for native) let accountBalance = account.spendableBalance; if (isTokenTransaction && account.subAccounts) { const subAccount = account.subAccounts.find(sub => sub.type === "TokenAccount" && sub.id === transaction.subAccountId); accountBalance = subAccount?.spendableBalance ?? new BigNumber(0); } if (amount.gt(0) && estimatedFees.times(10).gt(amount) && !isTokenTransaction) { // if the fee is more than 10 times the amount, we warn the user that fee is high compared to what he is sending warnings.feeTooHigh = new FeeTooHigh(); } if (transaction.fee === null || transaction.fee === undefined) { // if the fee is not loaded, we can't do much errors.fee = new FeeNotLoaded(); } if (!transaction.recipient) { errors.recipient = new RecipientRequired(""); } else if (!isRecipientValid(transaction.recipient)) { // We want to prevent user from sending to an invalid address errors.recipient = new InvalidAddress("", { currencyName: account.currency.name, }); } if (amount.eq(0)) { // if the amount is 0, we prevent the user from sending the tx (even if it's technically feasible) errors.amount = new AmountRequired(); } // For token transactions, check that parent account has enough native balance to pay fees if (isTokenTransaction && estimatedFees.gt(account.balance)) { errors.amount = new NotEnoughBalanceInParentAccount(); } // Check if total spent exceeds available balance if (!errors.amount && totalSpent.gt(accountBalance)) { errors.amount = new NotEnoughBalance(); } // For native coin transactions, check reserve amount constraints if (!isTokenTransaction && !errors.amount) { if (totalSpent.gt(account.balance.minus(reserveAmount))) { errors.amount = new NotEnoughSpendableBalance("", { minimumAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, { disableRounding: true, useGrouping: false, showCode: true, }), }); } else if (transaction.recipient && amount.lt(reserveAmount)) { // if we send an amount lower than reserve amount AND target account is new, we need to warn the user that the target account will not be activated errors.amount = new NotEnoughBalanceBecauseDestinationNotCreated("", { minimalAmount: formatCurrencyUnit(account.currency.units[0], reserveAmount, { disableRounding: true, useGrouping: false, showCode: true, }), }); } } const utxoWarning = validateUtxoCount(account, transaction); if (utxoWarning) { warnings.tooManyUtxos = utxoWarning; } const topologyError = await validateTopology(account); if (topologyError) { errors.topologyChange = topologyError; } return { errors, warnings, estimatedFees, amount, totalSpent, }; }; function validateUtxoCount(account, transaction) { if (!account.cantonResources?.instrumentUtxoCounts) { return null; } if (!transaction.recipient) { return null; } if (!isRecipientValid(transaction.recipient) || account.xpub === transaction.recipient) { return null; } if (transaction.recipient.includes(CANTON_UNSET_RECIPIENT)) { return null; } const { instrumentUtxoCounts } = account.cantonResources; const instrumentKey = transaction.instrumentAdmin ? `${transaction.tokenId}-${transaction.instrumentAdmin}` : transaction.tokenId; const instrumentUtxoCount = instrumentUtxoCounts[instrumentKey] || 0; if (instrumentUtxoCount > TO_MANY_UTXOS_CRITICAL_COUNT) { return new TooManyUtxosCritical(); } if (instrumentUtxoCount > TO_MANY_UTXOS_WARNING_COUNT) { return new TooManyUtxosWarning("families.canton.tooManyUtxos.warning"); } return null; } export async function validateTopology(account) { const publicKey = account.cantonResources?.publicKey; if (!publicKey) { return null; } try { const isTopologyChangeRequired = await isTopologyChangeRequiredCached(account.currency, publicKey); if (!isTopologyChangeRequired) { return null; } return new TopologyChangeError("Topology change detected. Re-onboarding required."); } catch { // If topology check fails, don't block the transaction return null; } } //# sourceMappingURL=getTransactionStatus.js.map