@ledgerhq/coin-near
Version:
190 lines (164 loc) • 6.08 kB
text/typescript
import { formatCurrencyUnit } from "@ledgerhq/coin-module-framework/currencies/index";
import { getCryptoCurrencyById } from "@ledgerhq/cryptoassets/index";
import {
NotEnoughBalance,
RecipientRequired,
InvalidAddress,
FeeNotLoaded,
AmountRequired,
InvalidAddressBecauseDestinationIsAlsoSource,
} from "@ledgerhq/errors";
import { AccountBridge } from "@ledgerhq/types-live";
import { BigNumber } from "bignumber.js";
import { fetchAccountDetails } from "./api";
import { NEW_ACCOUNT_SIZE, YOCTO_THRESHOLD_VARIATION } from "./constants";
import {
NearNewAccountWarning,
NearActivationFeeNotCovered,
NearNewNamedAccountError,
NearUseAllAmountStakeWarning,
NearNotEnoughStaked,
NearNotEnoughAvailable,
NearRecommendUnstake,
NearStakingThresholdNotMet,
} from "./errors";
import {
isValidAddress,
isImplicitAccount,
getMaxAmount,
getTotalSpent,
getYoctoThreshold,
} from "./logic";
import { getCurrentNearPreloadData } from "./preload-data";
import type { Transaction, StatusErrorMap, NearAccount, TransactionStatus } from "./types";
export const getTransactionStatus: AccountBridge<
Transaction,
NearAccount,
TransactionStatus
>["getTransactionStatus"] = async (account, transaction) => {
if (transaction.mode === "send") {
return await getSendTransactionStatus(account, transaction);
}
const errors: StatusErrorMap = {};
const warnings: StatusErrorMap = {};
const useAllAmount = !!transaction.useAllAmount;
if (!transaction.fees) {
errors.fees = new FeeNotLoaded();
}
const estimatedFees = transaction.fees || new BigNumber(0);
const maxAmount = getMaxAmount(account, transaction, estimatedFees);
const maxAmountWithFees = getMaxAmount(account, transaction);
const spendableBalanceWithFees = getMaxAmount(account, { ...transaction, mode: "stake" });
const stakingThreshold = getYoctoThreshold();
const totalSpent = getTotalSpent(account, transaction, estimatedFees);
const amount = useAllAmount ? maxAmount : new BigNumber(transaction.amount);
const isStakeAndNotEnoughBalance =
transaction.mode === "stake" &&
(totalSpent.gt(maxAmountWithFees) || maxAmountWithFees.lt(estimatedFees));
const isUnstakeOrWithdrawAndNotEnoughBalance =
["unstake", "withdraw"].includes(transaction.mode) &&
(totalSpent.gt(spendableBalanceWithFees) || spendableBalanceWithFees.lt(estimatedFees));
if (isStakeAndNotEnoughBalance || isUnstakeOrWithdrawAndNotEnoughBalance) {
errors.amount = new NotEnoughBalance();
} else if (
["stake", "unstake", "withdraw"].includes(transaction.mode) &&
amount.lt(stakingThreshold)
) {
const currency = getCryptoCurrencyById("near");
const formattedStakingThreshold = formatCurrencyUnit(
currency.units[0],
stakingThreshold.plus(YOCTO_THRESHOLD_VARIATION),
{
showCode: true,
},
);
errors.amount = new NearStakingThresholdNotMet(undefined, {
threshold: formattedStakingThreshold,
});
} else if (transaction.mode === "unstake" && amount.gt(maxAmount)) {
errors.amount = new NearNotEnoughStaked();
} else if (transaction.mode === "withdraw" && amount.gt(maxAmount)) {
errors.amount = new NearNotEnoughAvailable();
} else if (amount.lte(0) && !transaction.useAllAmount) {
errors.amount = new AmountRequired();
}
if (transaction.mode === "stake" && !errors.amount && transaction.useAllAmount) {
warnings.amount = new NearUseAllAmountStakeWarning();
}
return {
errors,
warnings,
estimatedFees,
amount,
totalSpent,
};
};
const getSendTransactionStatus = async (
account: NearAccount,
transaction: Transaction,
): Promise<TransactionStatus> => {
const errors: StatusErrorMap = {};
const warnings: StatusErrorMap = {};
const useAllAmount = !!transaction.useAllAmount;
const { storageCost } = getCurrentNearPreloadData();
const newAccountStorageCost = storageCost.multipliedBy(NEW_ACCOUNT_SIZE);
const currency = getCryptoCurrencyById("near");
const formattedNewAccountStorageCost = formatCurrencyUnit(
currency.units[0],
newAccountStorageCost,
{
showCode: true,
},
);
let recipientIsNewAccount;
if (!transaction.recipient) {
errors.recipient = new RecipientRequired();
} else if (!isValidAddress(transaction.recipient)) {
errors.recipient = new InvalidAddress("", {
currencyName: account.currency.name,
});
} else {
const accountDetails = await fetchAccountDetails(transaction.recipient);
if (!accountDetails) {
recipientIsNewAccount = true;
if (isImplicitAccount(transaction.recipient)) {
warnings.recipient = new NearNewAccountWarning(undefined, {
formattedNewAccountStorageCost,
});
} else {
errors.recipient = new NearNewNamedAccountError();
}
}
}
if (account.freshAddress === transaction.recipient) {
warnings.recipient = new InvalidAddressBecauseDestinationIsAlsoSource();
}
if (!transaction.fees) {
errors.fees = new FeeNotLoaded();
}
const estimatedFees = transaction.fees || new BigNumber(0);
const totalSpent = getTotalSpent(account, transaction, estimatedFees);
const maxAmount = getMaxAmount(account, transaction, estimatedFees);
const maxAmountWithFees = getMaxAmount(account, transaction);
const amount = useAllAmount ? maxAmount : new BigNumber(transaction.amount);
if (totalSpent.gt(maxAmountWithFees) || maxAmountWithFees.lt(estimatedFees)) {
errors.amount = new NotEnoughBalance();
} else if (amount.lte(0) && !transaction.useAllAmount) {
errors.amount = new AmountRequired();
} else if (recipientIsNewAccount && amount.lt(newAccountStorageCost)) {
errors.amount = new NearActivationFeeNotCovered(undefined, {
formattedNewAccountStorageCost,
});
}
if (account.nearResources?.stakingPositions.length > 0 && useAllAmount) {
warnings.amount = new NearRecommendUnstake();
}
return {
errors,
warnings,
estimatedFees,
amount,
totalSpent,
};
};
export default getTransactionStatus;