UNPKG

@ledgerhq/coin-near

Version:
360 lines (316 loc) • 9.93 kB
import liveNetwork from "@ledgerhq/live-network"; import { makeLRUCache } from "@ledgerhq/live-network/cache"; import network from "@ledgerhq/live-network/network"; import { log } from "@ledgerhq/logs"; import { BigNumber } from "bignumber.js"; import * as nearAPI from "near-api-js"; import { getCoinConfig } from "../config"; import { MIN_ACCOUNT_BALANCE_BUFFER } from "../constants"; import { canUnstake, canWithdraw, getYoctoThreshold } from "../logic"; import { getCurrentNearPreloadData } from "../preload-data"; import { NearAccount } from "../types"; import { NearAccessKey, NearAccountDetails, NearContract, NearProtocolConfig, NearRawValidator, NearStakingPosition, } from "./sdk.types"; export const fetchAccountDetails = async (address: string): Promise<NearAccountDetails> => { const currencyConfig = getCoinConfig(); const { data } = await network<{ result: NearAccountDetails }>({ method: "POST", url: currencyConfig.infra.API_NEAR_PRIVATE_NODE, data: { jsonrpc: "2.0", id: "id", method: "query", params: { request_type: "view_account", finality: "final", account_id: address, }, }, }); return data.result; }; export const getAccount = async ( address: string, ): Promise<Pick<NearAccount, "blockHeight" | "balance" | "spendableBalance" | "nearResources">> => { let accountDetails: NearAccountDetails; accountDetails = await fetchAccountDetails(address); if (!accountDetails) { accountDetails = { amount: "0", block_height: 0, storage_usage: 0, }; } const { stakingPositions, totalStaked, totalAvailable, totalPending } = await getStakingPositions(address); const { storageCost } = getCurrentNearPreloadData(); const balance = new BigNumber(accountDetails.amount); const storageUsage = storageCost.multipliedBy(accountDetails.storage_usage); const minBalanceBuffer = new BigNumber(MIN_ACCOUNT_BALANCE_BUFFER); let spendableBalance = balance.minus(storageUsage).minus(minBalanceBuffer); if (spendableBalance.lt(0)) { spendableBalance = new BigNumber(0); } return { blockHeight: accountDetails.block_height, balance: balance.plus(totalStaked).plus(totalAvailable).plus(totalPending), spendableBalance, nearResources: { stakedBalance: totalStaked, availableBalance: totalAvailable, pendingBalance: totalPending, storageUsageBalance: storageUsage.plus(minBalanceBuffer), stakingPositions, }, }; }; export const getProtocolConfig = async (): Promise<NearProtocolConfig> => { const currencyConfig = getCoinConfig(); const { data } = await network<{ result: NearProtocolConfig }>({ method: "POST", url: currencyConfig.infra.API_NEAR_PRIVATE_NODE, data: { jsonrpc: "2.0", id: "id", method: "EXPERIMENTAL_protocol_config", params: { finality: "final", }, }, }); return data.result; }; type statsResponseType = { stats: { id: number; total_supply: string; circulating_supply: string; avg_block_time: string; gas_price: string; nodes_online: number; near_price: string; near_btc_price: string; market_cap: string; volume: string; high_24h: string; high_all: string; low_24h: string; low_all: string; change_24: string; total_txns: string; tps: number; }[]; }; export const getGasPrice = async (): Promise<string> => { const currencyConfig = getCoinConfig(); const response = await liveNetwork<statsResponseType>({ url: `${currencyConfig.infra.API_NEARBLOCKS_INDEXER}/v1/stats`, }); return response.data.stats[0].gas_price; }; export const getAccessKey = async ({ address, publicKey, }: { address: string; publicKey: string; }): Promise<NearAccessKey> => { const currencyConfig = getCoinConfig(); const { data } = await network<{ result: NearAccessKey }>({ method: "POST", url: currencyConfig.infra.API_NEAR_PRIVATE_NODE, data: { jsonrpc: "2.0", id: "id", method: "query", params: { request_type: "view_access_key", finality: "final", account_id: address, public_key: publicKey, }, }, }); return data.result || {}; }; /** * Implements a retry mechanism for broadcasting a transaction * based on the near documentation: https://docs.near.org/api/rpc/transactions#what-could-go-wrong-send-tx * * `TIMEOUT_ERROR` can be thrown when the transaction is not yet executed in less than 10 seconds. * Documentation advises to "re-submit the request with the identical transaction" in this case. */ export const broadcastTransaction = async (transaction: string, retries = 6): Promise<string> => { const currencyConfig = getCoinConfig(); const { data } = await network<{ result: { transaction: { hash: string } }; error: { cause: { name: string }; message: string }; }>({ method: "POST", url: currencyConfig.infra.API_NEAR_PRIVATE_NODE, data: { jsonrpc: "2.0", id: "id", method: "send_tx", params: { signed_tx_base64: transaction, wait_until: "EXECUTED_OPTIMISTIC", }, }, }); if (data.error) { if (data.error?.cause?.name === "TIMEOUT_ERROR" && retries > 0) { log("Near", "broadcastTransaction retrying after error", { data, payload: { jsonrpc: "2.0", id: "id", method: "send_tx", params: { signed_tx_base64: transaction, wait_until: "EXECUTED_OPTIMISTIC", }, }, retries, }); return broadcastTransaction(transaction, retries - 1); } log("Near", "broadcastTransaction error", data.error); throw new Error((data.error?.cause?.name || "UNKOWWN CAUSE") + ": " + data.error.message); } return data.result.transaction.hash; }; export const getStakingPositions = async ( address: string, ): Promise<{ stakingPositions: NearStakingPosition[]; totalStaked: BigNumber; totalAvailable: BigNumber; totalPending: BigNumber; }> => { const currencyConfig = getCoinConfig(); const { connect, keyStores } = nearAPI; const config = { networkId: "mainnet", keyStore: new keyStores.InMemoryKeyStore(), nodeUrl: currencyConfig.infra.API_NEAR_PRIVATE_NODE, headers: {}, }; const near = await connect(config); const account = await near.account(address); let totalStaked = new BigNumber(0); let totalAvailable = new BigNumber(0); let totalPending = new BigNumber(0); const stakingThreshold = getYoctoThreshold(); const delegatedValidators = await liveNetwork<{ deposit: string; validator_id: string }[]>({ url: `${currencyConfig.infra.API_NEARBLOCKS_INDEXER}/v1/kitwallet/staking-deposits/${address}`, }); const stakingPositions = await Promise.all( delegatedValidators.data.map(async ({ validator_id: validatorId }) => { const contract = new nearAPI.Contract(account, validatorId, { viewMethods: [ "get_account_staked_balance", "get_account_unstaked_balance", "is_account_unstaked_balance_available", ], changeMethods: [], useLocalViewExecution: false, }) as NearContract; const [rawStaked, rawUnstaked, isAvailable] = await Promise.all([ contract.get_account_staked_balance({ account_id: address, }), contract.get_account_unstaked_balance({ account_id: address, }), contract.is_account_unstaked_balance_available({ account_id: address, }), ]); const unstaked = new BigNumber(rawUnstaked); let available = new BigNumber(0); let pending = unstaked; if (isAvailable) { available = unstaked; pending = new BigNumber(0); } const staked = new BigNumber(rawStaked); available = new BigNumber(available); pending = new BigNumber(pending); if (staked.gte(stakingThreshold)) { totalStaked = totalStaked.plus(staked); } if (available.gte(stakingThreshold)) { totalAvailable = totalAvailable.plus(available); } if (pending.gte(stakingThreshold)) { totalPending = totalPending.plus(pending); } return { staked, available, pending, validatorId, }; }), ); return { stakingPositions: stakingPositions.filter( sp => canUnstake(sp) || canWithdraw(sp) || sp.pending.gt(0), ), totalStaked, totalAvailable, totalPending, }; }; type validatorsType = { validatorFullData: { deposit: string; validator_id: string; percent: string; accountId: string; poolInfo: { fee: { numerator: number; denominator: number; }; delegatorsCount: number; }; cumulativeStake: { accumulatedPercent: string; cumulativePercent: string; ownPercentage: string; }; currentEpoch: { stake: string; }; }[]; }; type NearValidator = NearRawValidator & { commission: number; }; type fetchValidators = { per_page: number; page: number; }; async function fetchValidators({ per_page, page }: fetchValidators): Promise<NearValidator[]> { const currencyConfig = getCoinConfig(); const delegatedValidators = await liveNetwork<validatorsType>({ url: `${currencyConfig.infra.API_NEARBLOCKS_INDEXER}/v1/validators?per_page=${per_page}&page=${page}`, }); const validators = delegatedValidators.data.validatorFullData.map( ({ accountId, currentEpoch, poolInfo }) => ({ account_id: accountId, stake: currentEpoch.stake, commission: Math.round((poolInfo.fee.numerator / poolInfo.fee.denominator) * 100), }), ); return validators || []; } export const getValidators = makeLRUCache(fetchValidators, () => "", { ttl: 30 * 60 * 1000 });