UNPKG

@iyonger/aptos-web3-bip44.js

Version:
1,060 lines (979 loc) 36.6 kB
// Copyright (c) Aptos // SPDX-License-Identifier: Apache-2.0 import { HexString, MaybeHexString } from "./hex_string"; import { clear, DEFAULT_TXN_EXP_SEC_FROM_NOW, DEFAULT_MAX_GAS_AMOUNT, DEFAULT_TXN_TIMEOUT_SEC, fixNodeUrl, Memoize, sleep, APTOS_COIN, } from "./utils"; import { AptosAccount } from "./aptos_account"; import * as Gen from "./generated/index"; import { TxnBuilderTypes, TransactionBuilderEd25519, TransactionBuilderRemoteABI, RemoteABIBuilderConfig, } from "./transaction_builder"; import { bcsSerializeBytes, bcsSerializeU8, bcsToBytes, Bytes, Seq, Serializer, serializeVector, Uint64, AnyNumber, } from "./bcs"; export interface OptionalTransactionArgs { maxGasAmount?: Uint64; gasUnitPrice?: Uint64; expireTimestamp?: Uint64; } interface PaginationArgs { start?: AnyNumber; limit?: number; } /** * Provides methods for retrieving data from Aptos node. * For more detailed API specification see {@link https://fullnode.devnet.aptoslabs.com/v1/spec} */ export class AptosClient { client: Gen.AptosGeneratedClient; nodeUrl: string; /** * Build a client configured to connect to an Aptos node at the given URL. * * Note: If you forget to append `/v1` to the URL, the client constructor * will automatically append it. If you don't want this URL processing to * take place, set doNotFixNodeUrl to true. * * @param nodeUrl URL of the Aptos Node API endpoint. * @param config Additional configuration options for the generated Axios client. */ constructor( nodeUrl: string, config?: Partial<Gen.OpenAPIConfig>, doNotFixNodeUrl: boolean = false ) { if (!nodeUrl) { throw new Error("Node URL cannot be empty."); } const conf = config === undefined || config === null ? {} : { ...config }; if (doNotFixNodeUrl) { this.nodeUrl = nodeUrl; } else { this.nodeUrl = fixNodeUrl(nodeUrl); } conf.BASE = this.nodeUrl; // Do not carry cookies when `WITH_CREDENTIALS` is explicitly set to `false`. By default, cookies will be sent if (config?.WITH_CREDENTIALS === false) { conf.WITH_CREDENTIALS = false; } else { conf.WITH_CREDENTIALS = true; } this.client = new Gen.AptosGeneratedClient(conf); } /** * Queries an Aptos account by address * @param accountAddress Hex-encoded 32 byte Aptos account address * @returns Core account resource, used for identifying account and transaction execution * @example An example of the returned account * ``` * { * sequence_number: "1", * authentication_key: "0x5307b5f4bc67829097a8ba9b43dba3b88261eeccd1f709d9bde240fc100fbb69" * } * ``` */ @parseApiError async getAccount(accountAddress: MaybeHexString): Promise<Gen.AccountData> { return this.client.accounts.getAccount( HexString.ensure(accountAddress).hex() ); } /** * Queries transactions sent by given account * @param accountAddress Hex-encoded 32 byte Aptos account address * @param query Optional pagination object * @param query.start The sequence number of the start transaction of the page. Default is 0. * @param query.limit The max number of transactions should be returned for the page. Default is 25. * @returns An array of on-chain transactions, sent by account */ @parseApiError async getAccountTransactions( accountAddress: MaybeHexString, query?: PaginationArgs ): Promise<Gen.Transaction[]> { return this.client.transactions.getAccountTransactions( HexString.ensure(accountAddress).hex(), query?.start?.toString(), query?.limit ); } /** * Queries modules associated with given account * @param accountAddress Hex-encoded 32 byte Aptos account address * @param query.ledgerVersion Specifies ledger version of transactions. By default latest version will be used * @returns Account modules array for a specific ledger version. * Module is represented by MoveModule interface. It contains module `bytecode` and `abi`, * which is JSON representation of a module */ @parseApiError async getAccountModules( accountAddress: MaybeHexString, query?: { ledgerVersion?: AnyNumber } ): Promise<Gen.MoveModuleBytecode[]> { return this.client.accounts.getAccountModules( HexString.ensure(accountAddress).hex(), query?.ledgerVersion?.toString() ); } /** * Queries module associated with given account by module name * @param accountAddress Hex-encoded 32 byte Aptos account address * @param moduleName The name of the module * @param query.ledgerVersion Specifies ledger version of transactions. By default latest version will be used * @returns Specified module. * Module is represented by MoveModule interface. It contains module `bytecode` and `abi`, * which JSON representation of a module */ @parseApiError async getAccountModule( accountAddress: MaybeHexString, moduleName: string, query?: { ledgerVersion?: AnyNumber } ): Promise<Gen.MoveModuleBytecode> { return this.client.accounts.getAccountModule( HexString.ensure(accountAddress).hex(), moduleName, query?.ledgerVersion?.toString() ); } /** * Queries all resources associated with given account * @param accountAddress Hex-encoded 32 byte Aptos account address * @param query.ledgerVersion Specifies ledger version of transactions. By default latest version will be used * @returns Account resources for a specific ledger version */ @parseApiError async getAccountResources( accountAddress: MaybeHexString, query?: { ledgerVersion?: AnyNumber } ): Promise<Gen.MoveResource[]> { return this.client.accounts.getAccountResources( HexString.ensure(accountAddress).hex(), query?.ledgerVersion?.toString() ); } /** * Queries resource associated with given account by resource type * @param accountAddress Hex-encoded 32 byte Aptos account address * @param resourceType String representation of an on-chain Move struct type * @param query.ledgerVersion Specifies ledger version of transactions. By default latest version will be used * @returns Account resource of specified type and ledger version * @example An example of an account resource * ``` * { * type: "0x1::AptosAccount::Coin", * data: { value: 6 } * } * ``` */ @parseApiError async getAccountResource( accountAddress: MaybeHexString, resourceType: Gen.MoveStructTag, query?: { ledgerVersion?: AnyNumber } ): Promise<Gen.MoveResource> { return this.client.accounts.getAccountResource( HexString.ensure(accountAddress).hex(), resourceType, query?.ledgerVersion?.toString() ); } /** Generates a signed transaction that can be submitted to the chain for execution. */ static generateBCSTransaction( accountFrom: AptosAccount, rawTxn: TxnBuilderTypes.RawTransaction ): Uint8Array { const txnBuilder = new TransactionBuilderEd25519( (signingMessage: TxnBuilderTypes.SigningMessage) => { // @ts-ignore const sigHexStr = accountFrom.signBuffer(signingMessage); return new TxnBuilderTypes.Ed25519Signature(sigHexStr.toUint8Array()); }, accountFrom.pubKey().toUint8Array() ); return txnBuilder.sign(rawTxn); } /** * Note: Unless you have a specific reason for using this, it'll probably be simpler * to use `simulateTransaction`. * * Generates a BCS transaction that can be submitted to the chain for simulation. * * @param accountFrom The account that will be used to send the transaction * for simulation. * @param rawTxn The raw transaction to be simulated, likely created by calling * the `generateTransaction` function. * @returns The BCS encoded signed transaction, which you should then pass into * the `submitBCSSimulation` function. */ static generateBCSSimulation( accountPublicKey: HexString, rawTxn: TxnBuilderTypes.RawTransaction ): Uint8Array { const txnBuilder = new TransactionBuilderEd25519( // eslint-disable-next-line @typescript-eslint/no-unused-vars (_signingMessage: TxnBuilderTypes.SigningMessage) => { // @ts-ignore const invalidSigBytes = new Uint8Array(64); return new TxnBuilderTypes.Ed25519Signature(invalidSigBytes); }, accountPublicKey.toUint8Array() ); return txnBuilder.sign(rawTxn); } /** Generates a transaction request that can be submitted to produce a raw transaction that * can be signed, which upon being signed can be submitted to the blockchain * @param sender Hex-encoded 32 byte Aptos account address of transaction sender * @param payload Transaction payload. It depends on transaction type you want to send * @param options Options allow to overwrite default transaction options. * @returns A transaction object */ async generateTransaction( sender: MaybeHexString, payload: Gen.EntryFunctionPayload, options?: Partial<Gen.SubmitTransactionRequest> ): Promise<TxnBuilderTypes.RawTransaction> { const config: RemoteABIBuilderConfig = { sender }; // for multisig transaction if (options?.sender) { config.sender = options.sender; } if (options?.sequence_number) { config.sequenceNumber = options.sequence_number; } if (options?.gas_unit_price) { config.gasUnitPrice = options.gas_unit_price; } if (options?.max_gas_amount) { config.maxGasAmount = options.max_gas_amount; } if (options?.expiration_timestamp_secs) { const timestamp = BigInt(parseInt(options.expiration_timestamp_secs, 10)); config.expTimestampSec = timestamp; config.expSecFromNow = 0; } const builder = new TransactionBuilderRemoteABI(this, config); const generatedPayload = await builder.build( payload.function, payload.type_arguments, payload.arguments ); if (options?.max_gas_amount || !options?.publicKey) return generatedPayload; try { // simulate txn const simulateResponse: any = await this.simulateTransaction( HexString.ensure(options.publicKey), generatedPayload, { estimateGasUnitPrice: true, estimateMaxGasAmount: true, } ); // 2 is safety_factor const maxGasAmount = ( Math.min(parseInt(simulateResponse[0].gas_used, 10) * 2, parseInt(simulateResponse[0].max_gas_amount, 10)) ).toString(); config.maxGasAmount = maxGasAmount; const updatedBuilder = new TransactionBuilderRemoteABI(this, config); return await updatedBuilder.build( payload.function, payload.type_arguments, payload.arguments ); } catch (err) { return generatedPayload; } } /** Converts a transaction request produced by `generateTransaction` into a properly * signed transaction, which can then be submitted to the blockchain * @param accountFrom AptosAccount of transaction sender * @param rawTransaction A raw transaction generated by `generateTransaction` method * @returns A transaction, signed with sender account */ // eslint-disable-next-line class-methods-use-this async signTransaction( accountFrom: AptosAccount, rawTransaction: TxnBuilderTypes.RawTransaction ): Promise<Uint8Array> { return Promise.resolve( AptosClient.generateBCSTransaction(accountFrom, rawTransaction) ); } /** * Event types are globally identifiable by an account `address` and * monotonically increasing `creation_number`, one per event type emitted * to the given account. This API returns events corresponding to that * that event type. * @param address Hex-encoded 32 byte Aptos account, with or without a `0x` prefix, * for which events are queried. This refers to the account that events were emitted * to, not the account hosting the move module that emits that event type. * @param creationNumber Creation number corresponding to the event type. * @returns Array of events assotiated with the given account and creation number. */ @parseApiError async getEventsByCreationNumber( address: MaybeHexString, creationNumber: AnyNumber | string, query?: PaginationArgs ): Promise<Gen.Event[]> { return this.client.events.getEventsByCreationNumber( HexString.ensure(address).hex(), creationNumber.toString(), query?.start?.toString(), query?.limit ); } /** * This API uses the given account `address`, `eventHandle`, and `fieldName` * to build a key that can globally identify an event types. It then uses this * key to return events emitted to the given account matching that event type. * @param address Hex-encoded 32 byte Aptos account, with or without a `0x` prefix, * for which events are queried. This refers to the account that events were emitted * to, not the account hosting the move module that emits that event type. * @param eventHandleStruct String representation of an on-chain Move struct type. * (e.g. `0x1::Coin::CoinStore<0x1::aptos_coin::AptosCoin>`) * @param fieldName The field name of the EventHandle in the struct * @param query Optional query object * @param query.start The start sequence number in the EVENT STREAM, defaulting to the latest event. * The events are returned in the reverse order of sequence number * @param query.limit The number of events to be returned for the page default is 5 * @returns Array of events */ @parseApiError async getEventsByEventHandle( address: MaybeHexString, eventHandleStruct: Gen.MoveStructTag, fieldName: string, query?: PaginationArgs ): Promise<Gen.Event[]> { return this.client.events.getEventsByEventHandle( HexString.ensure(address).hex(), eventHandleStruct, fieldName, query?.start?.toString(), query?.limit ); } /** * Submits a signed transaction to the transaction endpoint. * @param signedTxn A transaction, signed by `signTransaction` method * @returns Transaction that is accepted and submitted to mempool */ async submitTransaction( signedTxn: Uint8Array ): Promise<Gen.PendingTransaction> { return this.submitSignedBCSTransaction(signedTxn); } /** * Generates and submits a transaction to the transaction simulation * endpoint. For this we generate a transaction with a fake signature. * * @param accountFrom The account that will be used to send the transaction * for simulation. * @param rawTransaction The raw transaction to be simulated, likely created * by calling the `generateTransaction` function. * @param query.estimateGasUnitPrice If set to true, the gas unit price in the * transaction will be ignored and the estimated value will be used. * @param query.estimateMaxGasAmount If set to true, the max gas value in the * transaction will be ignored and the maximum possible gas will be used. * @returns The BCS encoded signed transaction, which you should then provide * */ async simulateTransaction( accountPublicKey: MaybeHexString, rawTransaction: TxnBuilderTypes.RawTransaction, query?: { estimateGasUnitPrice?: boolean; estimateMaxGasAmount?: boolean } ): Promise<Gen.UserTransaction[]> { const signedTxn = AptosClient.generateBCSSimulation( HexString.ensure(accountPublicKey), rawTransaction ); return this.submitBCSSimulation(signedTxn, query); } /** * Submits a signed transaction to the endpoint that takes BCS payload * * @param signedTxn A BCS transaction representation * @returns Transaction that is accepted and submitted to mempool */ @parseApiError async submitSignedBCSTransaction( signedTxn: Uint8Array ): Promise<Gen.PendingTransaction> { // Need to construct a customized post request for transactions in BCS payload return this.client.request.request<Gen.PendingTransaction>({ url: "/transactions", method: "POST", body: signedTxn, mediaType: "application/x.aptos.signed_transaction+bcs", }); } /** * Submits the BCS serialization of a signed transaction to the simulation endpoint. * * @param bcsBody The output of `generateBCSSimulation`. * @param query?.estimateGasUnitPrice If set to true, the gas unit price in the * transaction will be ignored and the estimated value will be used. * @param query?.estimateMaxGasAmount If set to true, the max gas value in the * transaction will be ignored and the maximum possible gas will be used. * @returns Simulation result in the form of UserTransaction. */ @parseApiError async submitBCSSimulation( bcsBody: Uint8Array, query?: { estimateGasUnitPrice?: boolean; estimateMaxGasAmount?: boolean } ): Promise<Gen.UserTransaction[]> { // Need to construct a customized post request for transactions in BCS payload. const queryParams = { estimate_gas_unit_price: query?.estimateGasUnitPrice ?? false, estimate_max_gas_amount: query?.estimateMaxGasAmount ?? false, }; return this.client.request.request<Gen.UserTransaction[]>({ url: "/transactions/simulate", query: queryParams, method: "POST", body: bcsBody, mediaType: "application/x.aptos.signed_transaction+bcs", }); } /** * Queries on-chain transactions * @param query Optional pagination object * @param query.start The start transaction version of the page. Default is the latest ledger version * @param query.limit The max number of transactions should be returned for the page. Default is 25 * @returns Array of on-chain transactions */ @parseApiError async getTransactions(query?: PaginationArgs): Promise<Gen.Transaction[]> { return this.client.transactions.getTransactions( query?.start?.toString(), query?.limit ); } /** * @param txnHash - Transaction hash should be hex-encoded bytes string with 0x prefix. * @returns Transaction from mempool or on-chain transaction */ @parseApiError async getTransactionByHash(txnHash: string): Promise<Gen.Transaction> { return this.client.transactions.getTransactionByHash(txnHash); } /** * @param txnVersion - Transaction version is an uint64 number. * @returns Transaction from mempool or on-chain transaction */ @parseApiError async getTransactionByVersion( txnVersion: AnyNumber ): Promise<Gen.Transaction> { return this.client.transactions.getTransactionByVersion( txnVersion.toString() ); } /** * Defines if specified transaction is currently in pending state * @param txnHash A hash of transaction * * To create a transaction hash: * * 1. Create hash message bytes: "Aptos::Transaction" bytes + BCS bytes of Transaction. * 2. Apply hash algorithm SHA3-256 to the hash message bytes. * 3. Hex-encode the hash bytes with 0x prefix. * * @returns `true` if transaction is in pending state and `false` otherwise */ async transactionPending(txnHash: string): Promise<boolean> { try { const response = await this.client.transactions.getTransactionByHash( txnHash ); return response.type === "pending_transaction"; } catch (e: any) { if (e?.status === 404) { return true; } throw e; } } /** * Wait for a transaction to move past pending state. * * There are 4 possible outcomes: * 1. Transaction is processed and successfully committed to the blockchain. * 2. Transaction is rejected for some reason, and is therefore not committed * to the blockchain. * 3. Transaction is committed but execution failed, meaning no changes were * written to the blockchain state. * 4. Transaction is not processed within the specified timeout. * * In case 1, this function resolves with the transaction response returned * by the API. * * In case 2, the function will throw an ApiError, likely with an HTTP status * code indicating some problem with the request (e.g. 400). * * In case 3, if `checkSuccess` is false (the default), this function returns * the transaction response just like in case 1, in which the `success` field * will be false. If `checkSuccess` is true, it will instead throw a * FailedTransactionError. * * In case 4, this function throws a WaitForTransactionError. * * @param txnHash The hash of a transaction previously submitted to the blockchain. * @param extraArgs.timeoutSecs Timeout in seconds. Defaults to 20 seconds. * @param extraArgs.checkSuccess See above. Defaults to false. * @returns See above. * * @example * ``` * const rawTransaction = await this.generateRawTransaction(sender.address(), payload, extraArgs); * const bcsTxn = AptosClient.generateBCSTransaction(sender, rawTransaction); * const pendingTransaction = await this.submitSignedBCSTransaction(bcsTxn); * const transasction = await this.aptosClient.waitForTransactionWithResult(pendingTransaction.hash); * ``` */ async waitForTransactionWithResult( txnHash: string, extraArgs?: { timeoutSecs?: number; checkSuccess?: boolean } ): Promise<Gen.Transaction> { const timeoutSecs = extraArgs?.timeoutSecs ?? DEFAULT_TXN_TIMEOUT_SEC; const checkSuccess = extraArgs?.checkSuccess ?? false; let isPending = true; let count = 0; let lastTxn: Gen.Transaction | undefined; while (isPending) { if (count >= timeoutSecs) { break; } try { // eslint-disable-next-line no-await-in-loop lastTxn = await this.client.transactions.getTransactionByHash(txnHash); isPending = lastTxn.type === "pending_transaction"; if (!isPending) { break; } } catch (e) { // In short, this means we will retry if it was an ApiError and the code was 404 or 5xx. const isApiError = e instanceof Gen.ApiError; const isRequestError = isApiError && e.status !== 404 && e.status >= 400 && e.status < 500; if (!isApiError || isRequestError) { throw e; } } // eslint-disable-next-line no-await-in-loop await sleep(1000); count += 1; } // There is a chance that lastTxn is still undefined. Let's throw some error here if (lastTxn === undefined) { throw new Error(`Waiting for transaction ${txnHash} failed`); } if (isPending) { throw new WaitForTransactionError( `Waiting for transaction ${txnHash} timed out after ${timeoutSecs} seconds`, lastTxn ); } if (!checkSuccess) { return lastTxn; } if (!(lastTxn as any)?.success) { throw new FailedTransactionError( `Transaction ${txnHash} committed to the blockchain but execution failed`, lastTxn ); } return lastTxn; } /** * This function works the same as `waitForTransactionWithResult` except it * doesn't return the transaction in those cases, it returns nothing. For * more information, see the documentation for `waitForTransactionWithResult`. */ async waitForTransaction( txnHash: string, extraArgs?: { timeoutSecs?: number; checkSuccess?: boolean } ): Promise<void> { await this.waitForTransactionWithResult(txnHash, extraArgs); } /** * Queries the latest ledger information * @returns Latest ledger information * @example Example of returned data * ``` * { * chain_id: 15, * epoch: 6, * ledgerVersion: "2235883", * ledger_timestamp:"1654580922321826" * } * ``` */ @parseApiError async getLedgerInfo(): Promise<Gen.IndexResponse> { return this.client.general.getLedgerInfo(); } /** * @returns Current chain id */ @Memoize() async getChainId(): Promise<number> { const result = await this.getLedgerInfo(); return result.chain_id; } /** * Gets a table item for a table identified by the handle and the key for the item. * Key and value types need to be passed in to help with key serialization and value deserialization. * @param handle A pointer to where that table is stored * @param data Object, that describes table item * @param data.key_type Move type of table key (e.g. `vector<u8>`) * @param data.value_type Move type of table value (e.g. `u64`) * @param data.key Value of table key * @returns Table item value rendered in JSON */ @parseApiError async getTableItem( handle: string, data: Gen.TableItemRequest, query?: { ledgerVersion?: AnyNumber } ): Promise<any> { const tableItem = await this.client.tables.getTableItem( handle, data, query?.ledgerVersion?.toString() ); return tableItem; } /** * Generates a raw transaction out of a transaction payload * @param accountFrom * @param payload * @param extraArgs * @returns */ async generateRawTransaction( accountFrom: HexString, payload: TxnBuilderTypes.TransactionPayload, extraArgs?: OptionalTransactionArgs ): Promise<TxnBuilderTypes.RawTransaction> { const [ { sequence_number: sequenceNumber }, chainId, { gas_estimate: gasEstimate }, ] = await Promise.all([ this.getAccount(accountFrom), this.getChainId(), extraArgs?.gasUnitPrice ? Promise.resolve({ gas_estimate: extraArgs.gasUnitPrice }) : this.estimateGasPrice(), ]); const { maxGasAmount, gasUnitPrice, expireTimestamp } = { maxGasAmount: BigInt(DEFAULT_MAX_GAS_AMOUNT), gasUnitPrice: BigInt(gasEstimate), expireTimestamp: BigInt( Math.floor(Date.now() / 1000) + DEFAULT_TXN_EXP_SEC_FROM_NOW ), ...extraArgs, }; return new TxnBuilderTypes.RawTransaction( TxnBuilderTypes.AccountAddress.fromHex(accountFrom), BigInt(sequenceNumber), payload, maxGasAmount, gasUnitPrice, expireTimestamp, new TxnBuilderTypes.ChainId(chainId) ); } /** * Helper for generating, signing, and submitting a transaction. * * @param sender AptosAccount of transaction sender. * @param payload Transaction payload. * @param extraArgs Extra args for building the transaction payload. * @returns The transaction response from the API. */ async generateSignSubmitTransaction( sender: AptosAccount, payload: TxnBuilderTypes.TransactionPayload, extraArgs?: OptionalTransactionArgs ): Promise<string> { // :!:>generateSignSubmitTransactionInner const rawTransaction = await this.generateRawTransaction( sender.address(), payload, extraArgs ); const bcsTxn = AptosClient.generateBCSTransaction(sender, rawTransaction); const pendingTransaction = await this.submitSignedBCSTransaction(bcsTxn); return pendingTransaction.hash; // <:!:generateSignSubmitTransactionInner } /** * Publishes a move package. `packageMetadata` and `modules` can be generated with command * `aptos move compile --save-metadata [ --included-artifacts=<...> ]`. * @param sender * @param packageMetadata package metadata bytes * @param modules bytecodes of modules * @param extraArgs * @returns Transaction hash */ async publishPackage( sender: AptosAccount, packageMetadata: Bytes, modules: Seq<TxnBuilderTypes.Module>, extraArgs?: OptionalTransactionArgs ): Promise<string> { const codeSerializer = new Serializer(); serializeVector(modules, codeSerializer); const payload = new TxnBuilderTypes.TransactionPayloadEntryFunction( TxnBuilderTypes.EntryFunction.natural( "0x1::code", "publish_package_txn", [], [bcsSerializeBytes(packageMetadata), codeSerializer.getBytes()] ) ); return this.generateSignSubmitTransaction(sender, payload, extraArgs); } /** * Helper for generating, submitting, and waiting for a transaction, and then * checking whether it was committed successfully. Under the hood this is just * `generateSignSubmitTransaction` and then `waitForTransactionWithResult`, see * those for information about the return / error semantics of this function. */ async generateSignSubmitWaitForTransaction( sender: AptosAccount, payload: TxnBuilderTypes.TransactionPayload, extraArgs?: OptionalTransactionArgs & { checkSuccess?: boolean; timeoutSecs?: number; } ): Promise<Gen.Transaction> { const txnHash = await this.generateSignSubmitTransaction( sender, payload, extraArgs ); return this.waitForTransactionWithResult(txnHash, extraArgs); } @parseApiError @Memoize({ ttlMs: 5 * 60 * 1000, // cache result for 5min tags: ["gas_estimates"], }) async estimateGasPrice(): Promise<Gen.GasEstimation> { return this.client.transactions.estimateGasPrice(); } @parseApiError async estimateMaxGasAmount(forAccount: MaybeHexString): Promise<Uint64> { // Only Aptos utility coin is accepted as gas const typeTag = `0x1::coin::CoinStore<${APTOS_COIN}>`; const [{ gas_estimate: gasUnitPrice }, resources] = await Promise.all([ this.estimateGasPrice(), this.getAccountResources(forAccount), ]); const accountResource = resources.find((r) => r.type === typeTag); const balance = BigInt((accountResource!.data as any).coin.value); return balance / BigInt(gasUnitPrice); } /** * Rotate an account's auth key. After rotation, only the new private key can be used to sign txns for * the account. * WARNING: You must create a new instance of AptosAccount after using this function. * @param forAccount Account of which the auth key will be rotated * @param toPrivateKeyBytes New private key * @param extraArgs Extra args for building the transaction payload. * @returns PendingTransaction */ async rotateAuthKeyEd25519( forAccount: AptosAccount, toPrivateKeyBytes: Uint8Array, extraArgs?: OptionalTransactionArgs ): Promise<Gen.PendingTransaction> { const { sequence_number: sequenceNumber, authentication_key: authKey } = await this.getAccount(forAccount.address()); const helperAccount = new AptosAccount(toPrivateKeyBytes); const challenge = new TxnBuilderTypes.RotationProofChallenge( TxnBuilderTypes.AccountAddress.CORE_CODE_ADDRESS, "account", "RotationProofChallenge", BigInt(sequenceNumber), TxnBuilderTypes.AccountAddress.fromHex(forAccount.address()), new TxnBuilderTypes.AccountAddress(new HexString(authKey).toUint8Array()), helperAccount.pubKey().toUint8Array() ); const challengeHex = HexString.fromUint8Array(bcsToBytes(challenge)); const proofSignedByCurrentPrivateKey = forAccount.signHexString(challengeHex); const proofSignedByNewPrivateKey = helperAccount.signHexString(challengeHex); const payload = new TxnBuilderTypes.TransactionPayloadEntryFunction( TxnBuilderTypes.EntryFunction.natural( "0x1::account", "rotate_authentication_key", [], [ bcsSerializeU8(0), // ed25519 scheme bcsSerializeBytes(forAccount.pubKey().toUint8Array()), bcsSerializeU8(0), // ed25519 scheme bcsSerializeBytes(helperAccount.pubKey().toUint8Array()), bcsSerializeBytes(proofSignedByCurrentPrivateKey.toUint8Array()), bcsSerializeBytes(proofSignedByNewPrivateKey.toUint8Array()), ] ) ); const rawTransaction = await this.generateRawTransaction( forAccount.address(), payload, extraArgs ); const bcsTxn = AptosClient.generateBCSTransaction( forAccount, rawTransaction ); return this.submitSignedBCSTransaction(bcsTxn); } /** * Lookup the original address by the current derived address * @param addressOrAuthKey * @returns original address */ async lookupOriginalAddress( addressOrAuthKey: MaybeHexString ): Promise<HexString> { const resource = await this.getAccountResource( "0x1", "0x1::account::OriginatingAddress" ); const { address_map: { handle }, } = resource.data as any; const origAddress = await this.getTableItem(handle, { key_type: "address", value_type: "address", key: HexString.ensure(addressOrAuthKey).hex(), }); return new HexString(origAddress); } /** * Get block by height * * @param blockHeight Block height to lookup. Starts at 0 * @param withTransactions If set to true, include all transactions in the block * * @returns Block */ @parseApiError async getBlockByHeight( blockHeight: number, withTransactions?: boolean ): Promise<Gen.Block> { return this.client.blocks.getBlockByHeight(blockHeight, withTransactions); } /** * Get block by block transaction version * * @param version Ledger version to lookup block information for * @param withTransactions If set to true, include all transactions in the block * * @returns Block */ @parseApiError async getBlockByVersion( version: number, withTransactions?: boolean ): Promise<Gen.Block> { return this.client.blocks.getBlockByVersion(version, withTransactions); } // eslint-disable-next-line class-methods-use-this clearCache(tags: string[]) { clear(tags); } } export class ApiError extends Error { constructor( public readonly status: number, public readonly message: string, public readonly errorCode?: string, public readonly vmErrorCode?: string ) { super(message); } } /** * This error is used by `waitForTransactionWithResult` when waiting for a * transaction times out. */ export class WaitForTransactionError extends Error { public readonly lastSubmittedTransaction: Gen.Transaction | undefined; constructor( message: string, lastSubmittedTransaction: Gen.Transaction | undefined ) { super(message); this.lastSubmittedTransaction = lastSubmittedTransaction; } } /** * This error is used by `waitForTransactionWithResult` if `checkSuccess` is true. * See that function for more information. */ export class FailedTransactionError extends Error { public readonly transaction: Gen.Transaction; constructor(message: string, transaction: Gen.Transaction) { super(message); this.transaction = transaction; } } /** * Creates a decorator to parse Gen.ApiError and return a wrapped error that is more developer friendly */ function parseApiError( target: unknown, propertyKey: string, descriptor: PropertyDescriptor ) { const childFunction = descriptor.value; // eslint-disable-next-line no-param-reassign descriptor.value = async function wrapper(...args: any[]) { try { // We need to explicitly await here so that the function is called and // potentially throws an error. If we just return without awaiting, the // promise is returned directly and the catch block cannot trigger. const res = await childFunction.apply(this, [...args]); return res; } catch (e) { if (e instanceof Gen.ApiError) { throw new ApiError( e.status, JSON.stringify({ message: e.message, ...e.body }), e.body?.error_code, e.body?.vm_error_code ); } throw e; } }; return descriptor; }