// Copyright © Endless // Copyright © Aptos Foundation // SPDX-License-Identifier: Apache-2.0 /** * This file contains the underlying implementations for exposed API surface in * the {@link api/account}. By moving the methods out into a separate file, * other namespaces and processes can access these methods without depending on the entire * account namespace and without having a dependency cycle error. */ import { EndlessConfig } from "../api/endlessConfig"; import { EndlessApiError, getEndlessFullNode, paginateWithCursor, getEndlessIndexer, postEndlessIndexer } from "../client"; import { AccountAddress, AccountAddressInput } from "../core/accountAddress"; import { Account } from "../account"; import { AnyPublicKey, Ed25519PublicKey, PrivateKey } from "../core/crypto"; import { AccountBalancesResponse, AccountData, GetAccountsBalancesRequest, GetAccountsCoinBalanceResponse, GetAccountCoinsDataResponse, GetCreatedCoinDataResponse, LedgerVersionArg, MoveModuleBytecode, MoveResource, MoveStructId, PageableCollectionResponse, PageableNftOwnedCollectionResponse, PageableNftTokenResponse, PaginationArgs, PaginationArgsOfIndexer, TransactionResponse, } from "../types"; import { memoizeAsync } from "../utils/memoize"; import { AuthenticationKey, Ed25519PrivateKey, Secp256k1PrivateKey } from "../core"; import { getTableItem } from "./table"; export async function getInfo(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; }): Promise { const { endlessConfig, accountAddress } = args; const { data } = await getEndlessFullNode<{}, AccountData>({ endlessConfig, originMethod: "getInfo", path: `accounts/${AccountAddress.from(accountAddress).toString()}`, }); return data; } export async function getModules(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgs & LedgerVersionArg; }): Promise { const { endlessConfig, accountAddress, options } = args; return paginateWithCursor<{}, MoveModuleBytecode[]>({ endlessConfig, originMethod: "getModules", path: `accounts/${AccountAddress.from(accountAddress).toString()}/modules`, params: { ledger_version: options?.ledgerVersion, start: options?.offset, limit: options?.limit ?? 1000, }, }); } /** * Queries for a move module given account address and module name * * @param args.accountAddress Hex-encoded 32 byte Endless account address * @param args.moduleName The name of the module * @param args.query.ledgerVersion Specifies ledger version of transactions. By default, latest version will be used * @returns The move module. */ export async function getModule(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; moduleName: string; options?: LedgerVersionArg; }): Promise { // We don't memoize the account module by ledger version, as it's not a common use case, this would be handled // by the developer directly if (args.options?.ledgerVersion !== undefined) { return getModuleInner(args); } return memoizeAsync( async () => getModuleInner(args), `module-${args.accountAddress}-${args.moduleName}`, 1000 * 60 * 5, // 5 minutes )(); } async function getModuleInner(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; moduleName: string; options?: LedgerVersionArg; }): Promise { const { endlessConfig, accountAddress, moduleName, options } = args; const { data } = await getEndlessFullNode<{}, MoveModuleBytecode>({ endlessConfig, originMethod: "getModule", path: `accounts/${AccountAddress.from(accountAddress).toString()}/module/${moduleName}`, params: { ledger_version: options?.ledgerVersion }, }); return data; } export async function getTransactions(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgs; }): Promise { const { endlessConfig, accountAddress, options } = args; return paginateWithCursor<{}, TransactionResponse[]>({ endlessConfig, originMethod: "getTransactions", path: `accounts/${AccountAddress.from(accountAddress).toString()}/transactions`, params: { start: options?.offset, limit: options?.limit }, }); } export async function getResources(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgs & LedgerVersionArg; }): Promise { const { endlessConfig, accountAddress, options } = args; return paginateWithCursor<{}, MoveResource[]>({ endlessConfig, originMethod: "getResources", path: `accounts/${AccountAddress.from(accountAddress).toString()}/resources`, params: { ledger_version: options?.ledgerVersion, start: options?.offset, limit: options?.limit ?? 999, }, }); } export async function getResource(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; resourceType: MoveStructId; options?: LedgerVersionArg; }): Promise { const { endlessConfig, accountAddress, resourceType, options } = args; const { data } = await getEndlessFullNode<{}, MoveResource>({ endlessConfig, originMethod: "getResource", path: `accounts/${AccountAddress.from(accountAddress).toString()}/resource/${resourceType}`, params: { ledger_version: options?.ledgerVersion }, }); return data.data as T; } export async function lookupOriginalAccountAddress(args: { endlessConfig: EndlessConfig; authenticationKey: AccountAddressInput; options?: LedgerVersionArg; }): Promise { const { endlessConfig, authenticationKey, options } = args; type OriginatingAddress = { address_map: { handle: string }; }; const resource = await getResource({ endlessConfig, accountAddress: "0x1", resourceType: "0x1::account::OriginatingAddress", options, }); const { address_map: { handle }, } = resource; const authKeyAddress = AccountAddress.from(authenticationKey); // If the address is not found in the address map, which means its not rotated // then return the address as is try { const originalAddress = await getTableItem({ endlessConfig, handle, data: { key: authKeyAddress.toString(), key_type: "address", value_type: "address", }, options, }); return AccountAddress.from(originalAddress); } catch (err) { if (err instanceof EndlessApiError && err.data.error_code === "table_item_not_found") { return authKeyAddress; } throw err; } } export async function getAccountCoinAmount(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; coinId: string; }): Promise { const { endlessConfig, accountAddress, coinId } = args; const address = AccountAddress.from(accountAddress).toStringLong(); const data = await getAccountCoinsData({ endlessConfig, accountAddress: address, options: { coin: coinId, }, }); const first = data.data[0]; if (!first || !first.balance || first.metadata.decimals === undefined) { return 0; } return parseInt(first.balance, 10) / (10 ** first.metadata.decimals); } export async function getCoinsDataCreatedBy(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgsOfIndexer & { coin?: string }; }): Promise { const { endlessConfig, accountAddress, options } = args; const address = AccountAddress.from(accountAddress).toStringLong(); const { data } = await getEndlessIndexer<{}, GetCreatedCoinDataResponse>({ endlessConfig, originMethod: "getCoinsDataCreatedBy", path: `coins/created_by/${address}`, params: { page: options?.page, pageSize: options?.pageSize, coin: options?.coin, }, }); return data; } export async function getAccountCoinsData(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgsOfIndexer & { coin?: string }; }): Promise { const { endlessConfig, accountAddress, options } = args; const address = AccountAddress.from(accountAddress).toStringLong(); const { data } = await getEndlessIndexer<{}, GetAccountCoinsDataResponse>({ endlessConfig, originMethod: "getAccountCoinsData", path: `accounts/${address}/coins`, params: { page: options?.page, pageSize: options?.pageSize, coin: options?.coin, }, }); return data; } /** * NOTE: There is a potential issue once unified single signer scheme will be adopted * by the community. * * Becuase on could create 2 accounts with the same private key with this new authenticator type, * we’ll need to determine the order in which we lookup the accounts. First unified * scheme and then legacy scheme vs first legacy scheme and then unified scheme. * */ export async function deriveAccountFromPrivateKey(args: { endlessConfig: EndlessConfig; privateKey: PrivateKey; }): Promise { const { endlessConfig, privateKey } = args; const publicKey = new AnyPublicKey(privateKey.publicKey()); if (privateKey instanceof Secp256k1PrivateKey) { // private key is secp256k1, therefore we know it for sure uses a single signer key const authKey = AuthenticationKey.fromPublicKey({ publicKey }); const address = authKey.derivedAddress(); return Account.fromPrivateKey({ privateKey, address }); } if (privateKey instanceof Ed25519PrivateKey) { // lookup single sender ed25519 const singleSenderTransactionAuthenticatorAuthKey = AuthenticationKey.fromPublicKey({ publicKey, }); const isSingleSenderTransactionAuthenticator = await isAccountExist({ authKey: singleSenderTransactionAuthenticatorAuthKey, endlessConfig, }); if (isSingleSenderTransactionAuthenticator) { const address = singleSenderTransactionAuthenticatorAuthKey.derivedAddress(); return Account.fromPrivateKey({ privateKey, address, legacy: false }); } // lookup legacy ed25519 const legacyAuthKey = AuthenticationKey.fromPublicKey({ publicKey: publicKey.publicKey as Ed25519PublicKey, }); const isLegacyEd25519 = await isAccountExist({ authKey: legacyAuthKey, endlessConfig }); if (isLegacyEd25519) { const address = legacyAuthKey.derivedAddress(); return Account.fromPrivateKey({ privateKey, address, legacy: true }); } } // if we are here, it means we couldn't find an address with an // auth key that matches the provided private key throw new Error(`Can't derive account from private key ${privateKey}`); } export async function isAccountExist(args: { endlessConfig: EndlessConfig; authKey: AuthenticationKey }): Promise { const { endlessConfig, authKey } = args; const accountAddress = await lookupOriginalAccountAddress({ endlessConfig, authenticationKey: authKey.derivedAddress(), }); try { await getInfo({ endlessConfig, accountAddress, }); return true; } catch (error: any) { // account not found if (error.status === 404) { return false; } throw new Error(`Error while looking for an account info ${accountAddress.toString()}`); } } export async function getAccountsCoinBalance(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput[]; coinAddress: AccountAddressInput; }): Promise { const { endlessConfig, accountAddress, coinAddress } = args; const { data } = await postEndlessIndexer<{}, GetAccountsCoinBalanceResponse>({ endlessConfig, originMethod: "getAccountsCoinBalance", path: `accounts/coins/${coinAddress}`, body: accountAddress, }); return data; } export async function getAccountsCoinsBalances(args: { endlessConfig: EndlessConfig; accounts: AccountAddressInput[]; coins: string[]; }): Promise { const { endlessConfig, accounts, coins } = args; const accountAddresses = accounts.map((account) => AccountAddress.from(account).toString()); const request: GetAccountsBalancesRequest = { accounts: accountAddresses, coins, }; const { data } = await postEndlessIndexer<{}, AccountBalancesResponse[]>({ endlessConfig, originMethod: "getAccountsCoinsBalances", path: "accounts/coins", body: request, }); return data; } export async function getAccountCollections(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgsOfIndexer & { collection?: string }; }): Promise { const { endlessConfig, accountAddress, options } = args; const address = AccountAddress.from(accountAddress).toStringLong(); const { data } = await getEndlessIndexer<{}, PageableCollectionResponse>({ endlessConfig, originMethod: "getAccountCollections", path: `accounts/${address}/collections`, params: { page: options?.page, pageSize: options?.pageSize, name: options?.collection ?? "", }, }); return data; } export async function getAccountNfts(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgsOfIndexer & { token?: string; collection?: string }; }): Promise { const { endlessConfig, accountAddress, options } = args; const address = AccountAddress.from(accountAddress).toStringLong(); const { data } = await getEndlessIndexer<{}, PageableNftTokenResponse>({ endlessConfig, originMethod: "getAccountNfts", path: `accounts/${address}/nfts`, params: { page: options?.page, pageSize: options?.pageSize, nft: options?.token, collection: options?.collection, }, }); return data; } export async function getAccountNftCollections(args: { endlessConfig: EndlessConfig; accountAddress: AccountAddressInput; options?: PaginationArgsOfIndexer & { collection?: string }; }): Promise { const { endlessConfig, accountAddress, options } = args; const address = AccountAddress.from(accountAddress).toStringLong(); const { data } = await getEndlessIndexer<{}, PageableNftOwnedCollectionResponse>({ endlessConfig, originMethod: "getAccountNftCollections", path: `accounts/${address}/nfts/collections`, params: { page: options?.page, pageSize: options?.pageSize, collection: options?.collection, }, }); return data; }