@ledgerhq/coin-near
Version: 
179 lines (149 loc) • 5.35 kB
text/typescript
import { BigNumber } from "bignumber.js";
import { utils } from "near-api-js";
import { formatCurrencyUnit } from "@ledgerhq/coin-framework/currencies/index";
import type { Unit } from "@ledgerhq/types-cryptoassets";
import {
  NearMappedStakingPosition,
  Transaction,
  NearStakingPosition,
  NearValidatorItem,
  NearAccount,
} from "./types";
import { createTransaction } from "./createTransaction";
import { updateTransaction } from "@ledgerhq/coin-framework/bridge/jsHelpers";
import { getCurrentNearPreloadData } from "./preload";
import { FRACTIONAL_DIGITS, STAKING_GAS_BASE, YOCTO_THRESHOLD_VARIATION } from "./constants";
export const isValidAddress = (address: string): boolean => {
  const readableAddressRegex = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/;
  const hexAddressRegex = /^[a-f0-9]{64}$/;
  if (isImplicitAccount(address)) {
    return hexAddressRegex.test(address);
  }
  return readableAddressRegex.test(address);
};
export const isImplicitAccount = (address: string): boolean => {
  return !address.includes(".");
};
export const getStakingGas = (t?: Transaction, multiplier = 5): BigNumber => {
  const stakingGasBase = new BigNumber(STAKING_GAS_BASE);
  if (t?.mode === "withdraw" && t?.useAllAmount) {
    multiplier = 7;
  }
  return stakingGasBase.multipliedBy(multiplier);
};
/*
 * Get the max amount that can be spent, taking into account tx type and pending operations.
 */
export const getMaxAmount = (
  account: NearAccount,
  transaction: Transaction,
  fees?: BigNumber,
): BigNumber => {
  let maxAmount;
  const selectedValidator = account.nearResources?.stakingPositions.find(
    ({ validatorId }) => validatorId === transaction.recipient,
  );
  let pendingUnstakingAmount = new BigNumber(0);
  let pendingWithdrawingAmount = new BigNumber(0);
  account.pendingOperations.forEach(({ type, value, recipients }) => {
    const recipient = recipients[0];
    if (recipient === selectedValidator?.validatorId) {
      if (type === "UNSTAKE") {
        pendingUnstakingAmount = pendingUnstakingAmount.plus(value);
      } else if (type === "WITHDRAW_UNSTAKED") {
        pendingWithdrawingAmount = pendingWithdrawingAmount.plus(value);
      }
    }
  });
  switch (transaction.mode) {
    case "unstake":
      maxAmount = selectedValidator?.staked.minus(pendingUnstakingAmount);
      break;
    case "withdraw":
      maxAmount = selectedValidator?.available.minus(pendingWithdrawingAmount);
      break;
    default:
      maxAmount = account.spendableBalance;
      if (fees) {
        maxAmount = maxAmount.minus(fees);
      }
  }
  if (maxAmount === undefined || maxAmount.lt(0)) {
    return new BigNumber(0);
  }
  return maxAmount;
};
export const getTotalSpent = (a: NearAccount, t: Transaction, fees: BigNumber): BigNumber => {
  if (["unstake", "withdraw"].includes(t.mode)) {
    return fees;
  }
  if (t.useAllAmount) {
    return a.spendableBalance;
  }
  return new BigNumber(t.amount).plus(fees);
};
export const mapStakingPositions = (
  stakingPositions: NearStakingPosition[],
  validators: NearValidatorItem[],
  unit: Unit,
): NearMappedStakingPosition[] => {
  return stakingPositions.map(sp => {
    const rank = validators.findIndex(v => v.validatorAddress === sp.validatorId);
    const validator = validators[rank] ?? sp;
    const formatConfig = {
      disableRounding: false,
      alwaysShowSign: false,
      showCode: true,
    };
    return {
      ...sp,
      formattedAmount: formatCurrencyUnit(unit, sp.staked, formatConfig),
      formattedPending: formatCurrencyUnit(unit, sp.pending, formatConfig),
      formattedAvailable: formatCurrencyUnit(unit, sp.available, formatConfig),
      rank,
      validator,
    };
  });
};
/*
 * Make sure that an account has enough funds to stake, unstake, AND withdraw before staking.
 */
export const canStake = (account: NearAccount): boolean => {
  let transaction = createTransaction(account);
  transaction = updateTransaction(transaction, {
    mode: "stake",
  });
  const { gasPrice } = getCurrentNearPreloadData();
  const fees = getStakingFees(transaction, gasPrice).multipliedBy(3);
  return getMaxAmount(account, transaction, fees).gt(0);
};
export const canUnstake = (
  stakingPosition: NearMappedStakingPosition | NearStakingPosition,
): boolean => {
  return stakingPosition.staked.gte(getYoctoThreshold());
};
export const canWithdraw = (
  stakingPosition: NearMappedStakingPosition | NearStakingPosition,
): boolean => {
  return stakingPosition.available.gte(getYoctoThreshold());
};
/*
 * The threshold that the NEAR wallet uses for staking, unstaking, and withdrawing.
 * A "1" is subtracted due to the value from the node being 1 yoctoNEAR less than what was staked.
 */
export const getYoctoThreshold = (): BigNumber => {
  return new BigNumber(10)
    .pow(new BigNumber(utils.format.NEAR_NOMINATION_EXP - FRACTIONAL_DIGITS))
    .minus(YOCTO_THRESHOLD_VARIATION);
};
/*
 * An estimation for the fee by using the staking gas and scaling accordingly.
 * Buffer added so that the transaction never fails - we'll always overestimate.
 */
export const getStakingFees = (t: Transaction, gasPrice: BigNumber): BigNumber => {
  const stakingGas = getStakingGas(t);
  return stakingGas
    .plus(STAKING_GAS_BASE) // Buffer
    .multipliedBy(gasPrice)
    .dividedBy(10);
};