@ledgerhq/coin-canton
Version:
146 lines • 6.75 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getTransactionStatus = exports.TO_MANY_UTXOS_WARNING_COUNT = exports.TO_MANY_UTXOS_CRITICAL_COUNT = void 0;
exports.validateTopology = validateTopology;
const index_1 = require("@ledgerhq/coin-module-framework/currencies/index");
const errors_1 = require("@ledgerhq/ledger-wallet-framework/errors");
const bignumber_js_1 = __importDefault(require("bignumber.js"));
const utils_1 = require("../common-logic/utils");
const config_1 = __importDefault(require("../config"));
const constants_1 = require("../constants");
const gateway_1 = require("../network/gateway");
const types_1 = require("../types");
const errors_2 = require("../types/errors");
exports.TO_MANY_UTXOS_CRITICAL_COUNT = 24;
exports.TO_MANY_UTXOS_WARNING_COUNT = 10;
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_js_1.default(config_1.default.getCoinConfig(account.currency.id).minReserve || 0);
const estimatedFees = new bignumber_js_1.default(transaction.fee || 0);
const amount = new bignumber_js_1.default(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_js_1.default(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 errors_1.FeeTooHigh();
}
if (transaction.fee === null || transaction.fee === undefined) {
// if the fee is not loaded, we can't do much
errors.fee = new errors_1.FeeNotLoaded();
}
if (!transaction.recipient) {
errors.recipient = new errors_1.RecipientRequired("");
}
else if (!(0, utils_1.isRecipientValid)(transaction.recipient)) {
// We want to prevent user from sending to an invalid address
errors.recipient = new errors_1.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 errors_1.AmountRequired();
}
// For token transactions, check that parent account has enough native balance to pay fees
if (isTokenTransaction && estimatedFees.gt(account.balance)) {
errors.amount = new errors_1.NotEnoughBalanceInParentAccount();
}
// Check if total spent exceeds available balance
if (!errors.amount && totalSpent.gt(accountBalance)) {
errors.amount = new errors_1.NotEnoughBalance();
}
// For native coin transactions, check reserve amount constraints
if (!isTokenTransaction && !errors.amount) {
if (totalSpent.gt(account.balance.minus(reserveAmount))) {
errors.amount = new errors_1.NotEnoughSpendableBalance("", {
minimumAmount: (0, index_1.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 errors_1.NotEnoughBalanceBecauseDestinationNotCreated("", {
minimalAmount: (0, index_1.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,
};
};
exports.getTransactionStatus = getTransactionStatus;
function validateUtxoCount(account, transaction) {
if (!account.cantonResources?.instrumentUtxoCounts) {
return null;
}
if (!transaction.recipient) {
return null;
}
if (!(0, utils_1.isRecipientValid)(transaction.recipient) || account.xpub === transaction.recipient) {
return null;
}
if (transaction.recipient.includes(constants_1.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 > exports.TO_MANY_UTXOS_CRITICAL_COUNT) {
return new types_1.TooManyUtxosCritical();
}
if (instrumentUtxoCount > exports.TO_MANY_UTXOS_WARNING_COUNT) {
return new types_1.TooManyUtxosWarning("families.canton.tooManyUtxos.warning");
}
return null;
}
async function validateTopology(account) {
const publicKey = account.cantonResources?.publicKey;
if (!publicKey) {
return null;
}
try {
const isTopologyChangeRequired = await (0, gateway_1.isTopologyChangeRequiredCached)(account.currency, publicKey);
if (!isTopologyChangeRequired) {
return null;
}
return new errors_2.TopologyChangeError("Topology change detected. Re-onboarding required.");
}
catch {
// If topology check fails, don't block the transaction
return null;
}
}
//# sourceMappingURL=getTransactionStatus.js.map