@ledgerhq/coin-near
Version:
360 lines (316 loc) • 9.93 kB
text/typescript
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 });