@ledgerhq/coin-aptos
Version:
Ledger Aptos Coin integration
281 lines • 10.8 kB
JavaScript
import { ApolloClient, InMemoryCache } from "@apollo/client";
import { Aptos, AptosConfig, Ed25519PublicKey, MimeType, Hex, postAptosFullNode, Network, } from "@aptos-labs/ts-sdk";
import { getEnv } from "@ledgerhq/live-env";
import network from "@ledgerhq/live-network";
import BigNumber from "bignumber.js";
import isUndefined from "lodash/isUndefined";
import { APTOS_ASSET_ID, DEFAULT_GAS, DEFAULT_GAS_PRICE, ESTIMATE_GAS_MUL, TOKEN_TYPE, } from "../constants";
import { GetAccountTransactionsData, GetAccountTransactionsDataGt } from "./graphql/queries";
import { log } from "@ledgerhq/logs";
import { transactionsToOperations } from "../logic/transactionsToOperations";
import { isTestnet } from "../logic/isTestnet";
import { normalizeAddress } from "../logic/normalizeAddress";
const getApiEndpoint = (currencyId) => isTestnet(currencyId) ? getEnv("APTOS_TESTNET_API_ENDPOINT") : getEnv("APTOS_API_ENDPOINT");
const getIndexerEndpoint = (currencyId) => isTestnet(currencyId)
? getEnv("APTOS_TESTNET_INDEXER_ENDPOINT")
: getEnv("APTOS_INDEXER_ENDPOINT");
const getNetwork = (currencyId) => isTestnet(currencyId) ? Network.TESTNET : Network.MAINNET;
export class AptosAPI {
aptosConfig;
aptosClient;
apolloClient;
constructor(currencyIdOrSettings) {
const appVersion = getEnv("LEDGER_CLIENT_VERSION");
if (typeof currencyIdOrSettings === "string") {
this.aptosConfig = new AptosConfig({
network: getNetwork(currencyIdOrSettings),
fullnode: getApiEndpoint(currencyIdOrSettings),
indexer: getIndexerEndpoint(currencyIdOrSettings),
clientConfig: {
HEADERS: {
"X-Ledger-Client-Version": appVersion,
},
},
});
}
else {
this.aptosConfig = new AptosConfig(currencyIdOrSettings);
}
this.aptosClient = new Aptos(this.aptosConfig);
this.apolloClient = new ApolloClient({
uri: this.aptosConfig.indexer ?? "",
cache: new InMemoryCache(),
headers: {
"X-Ledger-Client-Version": appVersion,
},
});
}
async getAccount(address) {
return this.aptosClient.getAccountInfo({ accountAddress: address });
}
async getAccountInfo(address, startAt) {
const [balance, transactions, blockHeight] = await Promise.all([
this.getBalances(address, APTOS_ASSET_ID),
this.fetchTransactions(address, startAt),
this.getHeight(),
]);
return {
balance: balance[0]?.amount ?? BigNumber(0),
transactions,
blockHeight,
};
}
async estimateGasPrice() {
return this.aptosClient.getGasPriceEstimation();
}
async generateTransaction(address, payload, options) {
const opts = {};
if (!isUndefined(options.maxGasAmount)) {
opts.maxGasAmount = Number(options.maxGasAmount);
}
if (!isUndefined(options.gasUnitPrice)) {
opts.gasUnitPrice = Number(options.gasUnitPrice);
}
try {
const { ledger_timestamp } = await this.aptosClient.getLedgerInfo();
opts.expireTimestamp = Number(Math.ceil(+ledger_timestamp / 1_000_000 + 2 * 60)); // in milliseconds
}
catch {
// skip
}
return this.aptosClient.transaction.build
.simple({
sender: address,
data: payload,
options: opts,
})
.then(t => t.rawTransaction)
.catch(error => {
throw error;
});
}
async simulateTransaction(address, tx, options = {
estimateGasUnitPrice: true,
estimateMaxGasAmount: true,
estimatePrioritizedGasUnitPrice: false,
}) {
return this.aptosClient.transaction.simulate.simple({
signerPublicKey: address,
transaction: { rawTransaction: tx },
options,
});
}
async broadcast(tx) {
const txBytes = Hex.fromHexString(tx).toUint8Array();
const pendingTx = await postAptosFullNode({
aptosConfig: this.aptosClient.config,
body: txBytes,
path: "transactions",
originMethod: "",
contentType: MimeType.BCS_SIGNED_TRANSACTION,
});
return pendingTx.data.hash;
}
async getLastBlock() {
const { block_height } = await this.aptosClient.getLedgerInfo();
const block = await this.aptosClient.getBlockByHeight({ blockHeight: Number(block_height) });
return {
height: Number(block.block_height),
hash: block.block_hash,
time: new Date(Number(block.block_timestamp) / 1_000),
};
}
async estimateFees(transactionIntent) {
const publicKeyEd = new Ed25519PublicKey(transactionIntent?.senderPublicKey ?? "");
const txPayload = {
function: "0x1::aptos_account::transfer_coins",
typeArguments: [APTOS_ASSET_ID],
functionArguments: [transactionIntent.recipient, transactionIntent.amount],
};
if (transactionIntent.asset.type === "token") {
const { standard } = transactionIntent.asset;
if (standard === TOKEN_TYPE.FUNGIBLE_ASSET) {
txPayload.function = "0x1::primary_fungible_store::transfer";
txPayload.typeArguments = ["0x1::fungible_asset::Metadata"];
txPayload.functionArguments = [
transactionIntent.asset.contractAddress,
transactionIntent.recipient,
transactionIntent.amount,
];
}
else if (standard === TOKEN_TYPE.COIN) {
txPayload.function = "0x1::aptos_account::transfer_coins";
txPayload.typeArguments = [transactionIntent.asset.contractAddress];
}
}
const txOptions = {
maxGasAmount: DEFAULT_GAS.toString(),
gasUnitPrice: DEFAULT_GAS_PRICE.toString(),
};
const tx = await this.generateTransaction(transactionIntent.sender, txPayload, txOptions);
const simulation = await this.simulateTransaction(publicKeyEd, tx);
const completedTx = simulation[0];
const gasLimit = new BigNumber(completedTx.gas_used)
.multipliedBy(ESTIMATE_GAS_MUL)
.integerValue();
const gasPrice = new BigNumber(completedTx.gas_unit_price);
const expectedGas = gasPrice.multipliedBy(gasLimit);
return {
value: BigInt(expectedGas.toString()),
parameters: {
storageLimit: BigInt(0),
gasLimit: BigInt(gasLimit.toString()),
gasPrice: BigInt(gasPrice.toString()),
},
};
}
async getNextUnlockTime(stakingPoolAddress) {
const resourceType = "0x1::stake::StakePool";
try {
const resource = await this.aptosClient.getAccountResource({
accountAddress: stakingPoolAddress,
resourceType,
});
return resource.locked_until_secs;
}
catch (error) {
log("error", "Failed to fetch StakePool resource:", { error });
}
}
async getDelegatorBalanceInPool(poolAddress, delegatorAddress) {
try {
// Query the delegator balance in the pool
return await this.aptosClient.view({
payload: {
function: "0x1::delegation_pool::get_stake",
typeArguments: [],
functionArguments: [poolAddress, delegatorAddress],
},
});
}
catch (error) {
log("error", "Failed to fetch delegation_pool::get_stake", { error });
return ["0", "0", "0"];
}
}
async listOperations(rawAddress, pagination) {
const address = normalizeAddress(rawAddress);
const transactions = await this.getAccountInfo(address, pagination.minHeight.toString());
const newOperations = transactionsToOperations(address, transactions.transactions);
return [newOperations, ""];
}
async fetchTransactions(address, gt) {
if (!address) {
return [];
}
let query = GetAccountTransactionsData;
if (gt) {
query = GetAccountTransactionsDataGt;
}
const queryResponse = await this.apolloClient.query({
query,
variables: {
address,
limit: 1000,
gt,
},
fetchPolicy: "network-only",
});
return Promise.all(queryResponse.data.account_transactions.map(({ transaction_version }) => {
return this.richItemByVersion(transaction_version);
}));
}
async richItemByVersion(version) {
try {
const tx = await this.aptosClient.getTransactionByVersion({
ledgerVersion: version,
});
const block = await this.getBlock(version);
return {
...tx,
block,
};
}
catch (error) {
log("error", "richItemByVersion", {
error,
});
return null;
}
}
async getHeight() {
const { data } = await network({
method: "GET",
url: this.aptosConfig.fullnode ?? "",
});
return parseInt(data.block_height);
}
async getBlock(version) {
const block = await this.aptosClient.getBlockByVersion({ ledgerVersion: version });
return {
height: parseInt(block.block_height),
hash: block.block_hash,
};
}
async getBalances(address, contractAddress) {
try {
const whereCondition = {
owner_address: { _eq: address },
};
if (contractAddress !== undefined && contractAddress !== "") {
whereCondition.asset_type = { _eq: contractAddress };
}
const response = await this.aptosClient.getCurrentFungibleAssetBalances({
options: {
where: whereCondition,
},
});
return response.map(x => ({
contractAddress: x.asset_type ?? "",
amount: BigNumber(x.amount),
}));
}
catch (error) {
log("error", "getCoinBalance", {
error,
});
return [{ amount: BigNumber(0), contractAddress: "" }];
}
}
}
//# sourceMappingURL=client.js.map