From fbfd7613fd3a31a737c06c57a40fc50f07360823 Mon Sep 17 00:00:00 2001 From: Daniel Porteous Date: Thu, 25 Aug 2022 15:09:34 -0700 Subject: [PATCH] Reduce duplication of specialized clients in TS SDK, make transaction submission functions more uniform --- .../docs/tutorials/first-transaction-sdk.md | 20 +- .../sdk/examples/typescript/transfer_coin.ts | 9 +- ecosystem/typescript/sdk/src/aptos_client.ts | 182 +++++++++++------- .../typescript/sdk/src/coin_client.test.ts | 2 +- ecosystem/typescript/sdk/src/coin_client.ts | 18 +- .../typescript/sdk/src/token_client.test.ts | 36 ++-- ecosystem/typescript/sdk/src/token_client.ts | 52 ++--- 7 files changed, 161 insertions(+), 158 deletions(-) diff --git a/developer-docs-site/docs/tutorials/first-transaction-sdk.md b/developer-docs-site/docs/tutorials/first-transaction-sdk.md index 116d3b86c4c99..42ac5be50fb8f 100644 --- a/developer-docs-site/docs/tutorials/first-transaction-sdk.md +++ b/developer-docs-site/docs/tutorials/first-transaction-sdk.md @@ -304,14 +304,14 @@ Like the previous step, this is another helper step that constructs a transactio :!: static/sdks/typescript/examples/typescript/transfer_coin.ts section_5 ``` -Behind the scenes, the `transfer` function generates a transaction payload and has the client sign, send, and wait for it: +Behind the scenes, the `transfer` function generates a transaction payload and has the client generate, sign, and send a transaction containing it: ```ts :!: static/sdks/typescript/src/coin_client.ts transfer ``` -Within the client, generateSignSendWaitForTransaction is doing this: +Within the client, `generateSignSubmitTransaction` is doing this: ```ts -:!: static/sdks/typescript/src/aptos_client.ts generateSignSendWaitForTransactionInner +:!: static/sdks/typescript/src/aptos_client.ts generateSignSubmitTransactionInner ``` Breaking the above down into pieces: @@ -364,29 +364,29 @@ Breaking the above down into pieces: ### Step 4.6: Waiting for transaction resolution +The transaction hash can be used to query the status of a transaction: + -In Typescript, just calling `coinClient.transfer` is sufficient to wait for the transaction to complete. The function will return the `Transaction` returned by the API once it is processed (either successfully or unsuccessfully) or throw an error if processing time exceeds the timeout. +```ts +:!: static/sdks/typescript/examples/typescript/transfer_coin.ts section_6a +``` -You can set `checkSuccess` to true when calling `transfer` if you'd like it to throw if the transaction was not committed successfully: +You can set `checkSuccess` to true when calling `waitForTransaction` if you'd like it to throw if the transaction was not committed successfully. Otherwise, you must check this yourself. ```ts -:!: static/sdks/typescript/examples/typescript/transfer_coin.ts section_6 +:!: static/sdks/typescript/examples/typescript/transfer_coin.ts section_6b ``` -The transaction hash can be used to query the status of a transaction: - ```python :!: static/sdks/python/examples/transfer-coin.py section_6 ``` -The transaction hash can be used to query the status of a transaction: - ```rust :!: static/sdks/rust/examples/transfer-coin.rs section_6 ``` diff --git a/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts b/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts index 77f2cc5bb40f8..20df57d5d5cb8 100644 --- a/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts +++ b/ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts @@ -44,7 +44,9 @@ import { aptosCoinStore, NODE_URL, FAUCET_URL } from "./common"; // Have Alice send Bob some AptosCoins. // :!:>section_5 - await coinClient.transfer(alice, bob, 1_000); // <:!:section_5 + const txnHash = await coinClient.transfer(alice, bob, 1_000); // <:!:section_5 + // :!:>section_6a + await client.waitForTransaction(txnHash); // <:!:section_6a // Print out intermediate balances. console.log("=== Intermediate Balances ==="); @@ -53,8 +55,9 @@ import { aptosCoinStore, NODE_URL, FAUCET_URL } from "./common"; console.log(""); // Have Alice send Bob some more AptosCoins. - // :!:>section_6 - await coinClient.transfer(alice, bob, 1_000, { checkSuccess: true }); // <:!:section_6 + await coinClient.transfer(alice, bob, 1_000); + // :!:>section_6b + await client.waitForTransaction(txnHash, { checkSuccess: true }); // <:!:section_6b // Print out final balances. console.log("=== Final Balances ==="); diff --git a/ecosystem/typescript/sdk/src/aptos_client.ts b/ecosystem/typescript/sdk/src/aptos_client.ts index 09dd6a0786a79..5413a7202fcc2 100644 --- a/ecosystem/typescript/sdk/src/aptos_client.ts +++ b/ecosystem/typescript/sdk/src/aptos_client.ts @@ -389,50 +389,54 @@ export class AptosClient { } /** - * Waits up to 10 seconds for a transaction to move past pending state. - * @param txnHash A hash of transaction - * @returns A Promise, that will resolve if transaction is accepted to the - * blockchain and reject if more then 10 seconds passed + * 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 timeoutSecs Timeout in seconds. Defaults to 10 seconds. + * @param checkSuccess See above. Defaults to false. + * @returns See above. + * * @example * ``` - * const signedTxn = await this.aptosClient.signTransaction(account, txnRequest); - * const res = await this.aptosClient.submitTransaction(signedTxn); - * await this.aptosClient.waitForTransaction(res.hash); - * // do smth after transaction is accepted into blockchain + * 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 waitForTransaction(txnHash: string) { - let count = 0; - // eslint-disable-next-line no-await-in-loop - while (await this.transactionPending(txnHash)) { - // eslint-disable-next-line no-await-in-loop - await sleep(1000); - count += 1; - if (count >= 10) { - throw new Error(`Waiting for transaction ${txnHash} timed out!`); - } - } - } + async waitForTransactionWithResult( + txnHash: string, + extraArgs?: { timeoutSecs?: number; checkSuccess?: boolean }, + ): Promise { + const timeoutSecs = extraArgs?.timeoutSecs ?? 10; + const checkSuccess = extraArgs?.checkSuccess ?? false; - /** - * Waits up to 10 seconds for a transaction to move past pending state. - * @param txnHash A hash of transaction - * @returns A Promise, that will resolve if transaction is accepted to the - * blockchain, and reject if more then 10 seconds passed. The return value - * contains the last transaction returned by the blockchain. - * @example - * ``` - * const signedTxn = await this.aptosClient.signTransaction(account, txnRequest); - * const res = await this.aptosClient.submitTransaction(signedTxn); - * const waitResult = await this.aptosClient.waitForTransaction(res.hash); - * ``` - */ - async waitForTransactionWithResult(txnHash: string): Promise { let isPending = true; let count = 0; let lastTxn: Gen.Transaction | undefined; while (isPending) { - if (count >= 10) { + if (count >= timeoutSecs) { break; } try { @@ -440,11 +444,17 @@ export class AptosClient { lastTxn = await this.client.transactions.getTransactionByHash(txnHash); isPending = lastTxn.type === "pending_transaction"; if (!isPending) { - return lastTxn; + break; } } catch (e) { if (e instanceof Gen.ApiError) { - isPending = e.status === 404; + if (e.status === 404) { + isPending = true; + break; + } + if (e.status >= 400) { + throw e; + } } else { throw e; } @@ -453,11 +463,36 @@ export class AptosClient { await sleep(1000); count += 1; } - throw new WaitForTransactionError(`Waiting for transaction ${txnHash} timed out!`, lastTxn); + 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 ${lastTxn.hash} 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 { + await this.waitForTransactionWithResult(txnHash, extraArgs); } - // TODO: For some reason this endpoint doesn't appear in the generated client - // if we use --modular, so I'm not using it for now. /** * Queries the latest ledger information * @param params Request params @@ -541,49 +576,48 @@ export class AptosClient { } /** - * Helper for generating, submitting, and waiting for a transaction, and then - * checking whether it was committed successfully. This has the same failure - * semantics as `submitTransaction` and `waitForTransactionWithResult`, see - * those for information about how this can fail (throw errors). - * - * If you set checkSuccess there are additional error cases, see the - * documentation for `checkSuccess` below. + * Helper for generating, signing, and submitting a transaction. * - * @param sender AptosAccount of transaction sender - * @param payload Transaction payload - * @param extraArgs Extra args for building transaction payload and configuring - * behavior of this function. - * @param checkSuccess If set, check whether the transaction was successful and - * throw a TransactionNotCommittedError if not. + * @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 generateSignSendWaitForTransaction( + async generateSignSubmitTransaction( sender: AptosAccount, payload: TxnBuilderTypes.TransactionPayload, extraArgs?: { maxGasAmount?: BCS.Uint64; gasUnitPrice?: BCS.Uint64; expireTimestamp?: BCS.Uint64; - checkSuccess?: boolean; }, - ): Promise { - /* eslint-disable max-len */ - // :!:>generateSignSendWaitForTransactionInner + ): Promise { + // :!:>generateSignSubmitTransactionInner const rawTransaction = await this.generateRawTransaction(sender.address(), payload, extraArgs); const bcsTxn = AptosClient.generateBCSTransaction(sender, rawTransaction); const pendingTransaction = await this.submitSignedBCSTransaction(bcsTxn); - const transactionResponse = await this.waitForTransactionWithResult(pendingTransaction.hash); // <:!:generateSignSendWaitForTransactionInner - /* eslint-enable max-len */ - if (extraArgs?.checkSuccess === undefined || extraArgs?.checkSuccess === null || !extraArgs.checkSuccess) { - return transactionResponse; - } - if (!(transactionResponse as any)?.success) { - throw new TransactionNotCommittedError( - `Transaction ${pendingTransaction.hash} processed by blockchain but not committed successfully`, - transactionResponse, - ); - } - return transactionResponse; + return pendingTransaction.hash; // <:!:generateSignSubmitTransactionInner + } + + /** + * 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?: { + maxGasAmount?: BCS.Uint64; + gasUnitPrice?: BCS.Uint64; + expireTimestamp?: BCS.Uint64; + checkSuccess?: boolean; + timeoutSecs?: number; + }, + ): Promise { + const txnHash = await this.generateSignSubmitTransaction(sender, payload, extraArgs); + return this.waitForTransactionWithResult(txnHash, extraArgs); } } @@ -601,13 +635,13 @@ export class WaitForTransactionError extends Error { } /** - * This error is used by `generateSignSendWaitForTransaction` when a transaction - * is processed by the API, but it was not committed successfully. + * This error is used by `waitForTransactionWithResult` if `checkSuccess` is true. + * See that function for more information. */ -export class TransactionNotCommittedError extends Error { +export class FailedTransactionError extends Error { public readonly transaction: Gen.Transaction; - constructor(message: string, transaction: Gen.Transaction | undefined) { + constructor(message: string, transaction: Gen.Transaction) { super(message); this.transaction = transaction; } diff --git a/ecosystem/typescript/sdk/src/coin_client.test.ts b/ecosystem/typescript/sdk/src/coin_client.test.ts index 878b0ceef2867..7a15dbf51ff31 100644 --- a/ecosystem/typescript/sdk/src/coin_client.test.ts +++ b/ecosystem/typescript/sdk/src/coin_client.test.ts @@ -19,7 +19,7 @@ test( await faucetClient.fundAccount(alice.address(), 50000); await faucetClient.fundAccount(bob.address(), 0); - await coinClient.transfer(alice, bob, 42, { checkSuccess: true }); + await client.waitForTransaction(await coinClient.transfer(alice, bob, 42), { checkSuccess: true }); expect(await coinClient.checkBalance(bob)).toBe(42n); }, diff --git a/ecosystem/typescript/sdk/src/coin_client.ts b/ecosystem/typescript/sdk/src/coin_client.ts index 97f7397a01f96..23076dffb3072 100644 --- a/ecosystem/typescript/sdk/src/coin_client.ts +++ b/ecosystem/typescript/sdk/src/coin_client.ts @@ -3,7 +3,6 @@ import { AptosAccount } from "./aptos_account"; import { AptosClient } from "./aptos_client"; -import * as Gen from "./generated/index"; import { HexString } from "./hex_string"; import { BCS, TransactionBuilderABI } from "./transaction_builder"; import { COIN_ABIS } from "./abis"; @@ -29,18 +28,15 @@ export class CoinClient { } /** - * Generate, submit, and wait for a transaction to transfer AptosCoin from - * one account to another. - * - * If the transaction is submitted successfully, it returns the response - * from the API indicating that the transaction was submitted. + * Generate, sign, and submit a transaction to the Aptos blockchain API to + * transfer AptosCoin from one account to another. * * @param from Account sending the coins * @param from Account to receive the coins * @param amount Number of coins to transfer * @param extraArgs Extra args for building the transaction or configuring how - * the client should submit and wait for the transaction. - * @returns Promise that resolves to the response from the API + * the client should submit and wait for the transaction + * @returns The hash of the transaction submitted to the API */ // :!:>transfer async transfer( @@ -53,17 +49,15 @@ export class CoinClient { maxGasAmount?: BCS.Uint64; gasUnitPrice?: BCS.Uint64; expireTimestamp?: BCS.Uint64; - // If true, this function will throw if the transaction is not committed succesfully. - checkSuccess?: boolean; }, - ): Promise { + ): Promise { const coinTypeToTransfer = extraArgs?.coinType ?? APTOS_COIN; const payload = this.transactionBuilder.buildTransactionPayload( "0x1::coin::transfer", [coinTypeToTransfer], [to.address(), amount], ); - return this.aptosClient.generateSignSendWaitForTransaction(from, payload, extraArgs); + return this.aptosClient.generateSignSubmitTransaction(from, payload, extraArgs); } // <:!:transfer /** diff --git a/ecosystem/typescript/sdk/src/token_client.test.ts b/ecosystem/typescript/sdk/src/token_client.test.ts index f257d2fab6bc1..51c3614aa9660 100644 --- a/ecosystem/typescript/sdk/src/token_client.test.ts +++ b/ecosystem/typescript/sdk/src/token_client.test.ts @@ -25,19 +25,14 @@ test( const collectionName = "AliceCollection"; const tokenName = "Alice Token"; - async function ensureTxnSuccess(txnHashPromise: Promise) { - const txnHash = await txnHashPromise; - const txn = await client.waitForTransactionWithResult(txnHash); - expect((txn as any)?.success).toBe(true); - } - // Create collection and token on Alice's account - await ensureTxnSuccess( - tokenClient.createCollection(alice, collectionName, "Alice's simple collection", "https://aptos.dev"), + await client.waitForTransaction( + await tokenClient.createCollection(alice, collectionName, "Alice's simple collection", "https://aptos.dev"), + { checkSuccess: true }, ); - await ensureTxnSuccess( - tokenClient.createToken( + await client.waitForTransaction( + await tokenClient.createToken( alice, collectionName, tokenName, @@ -52,6 +47,7 @@ test( ["2"], ["int"], ), + { checkSuccess: true }, ); const tokenId = { @@ -70,26 +66,30 @@ test( const tokenData = await tokenClient.getTokenData(alice.address().hex(), collectionName, tokenName); expect(tokenData.name).toBe(tokenName); - await ensureTxnSuccess( - tokenClient.offerToken(alice, bob.address().hex(), alice.address().hex(), collectionName, tokenName, 1), + await client.waitForTransaction( + await tokenClient.offerToken(alice, bob.address().hex(), alice.address().hex(), collectionName, tokenName, 1), + { checkSuccess: true }, ); aliceBalance = await tokenClient.getTokenForAccount(alice.address().hex(), tokenId); expect(aliceBalance.amount).toBe("0"); - await ensureTxnSuccess( - tokenClient.cancelTokenOffer(alice, bob.address().hex(), alice.address().hex(), collectionName, tokenName), + await client.waitForTransaction( + await tokenClient.cancelTokenOffer(alice, bob.address().hex(), alice.address().hex(), collectionName, tokenName), + { checkSuccess: true }, ); aliceBalance = await tokenClient.getTokenForAccount(alice.address().hex(), tokenId); expect(aliceBalance.amount).toBe("1"); - await ensureTxnSuccess( - tokenClient.offerToken(alice, bob.address().hex(), alice.address().hex(), collectionName, tokenName, 1), + await client.waitForTransaction( + await tokenClient.offerToken(alice, bob.address().hex(), alice.address().hex(), collectionName, tokenName, 1), + { checkSuccess: true }, ); aliceBalance = await tokenClient.getTokenForAccount(alice.address().hex(), tokenId); expect(aliceBalance.amount).toBe("0"); - await ensureTxnSuccess( - tokenClient.claimToken(bob, alice.address().hex(), alice.address().hex(), collectionName, tokenName), + await client.waitForTransaction( + await tokenClient.claimToken(bob, alice.address().hex(), alice.address().hex(), collectionName, tokenName), + { checkSuccess: true }, ); const bobBalance = await tokenClient.getTokenForAccount(bob.address().hex(), tokenId); diff --git a/ecosystem/typescript/sdk/src/token_client.ts b/ecosystem/typescript/sdk/src/token_client.ts index 34cd74a0ecf70..7632c046d8275 100644 --- a/ecosystem/typescript/sdk/src/token_client.ts +++ b/ecosystem/typescript/sdk/src/token_client.ts @@ -6,7 +6,7 @@ import { AptosClient } from "./aptos_client"; import * as TokenTypes from "./token_types"; import * as Gen from "./generated/index"; import { HexString, MaybeHexString } from "./hex_string"; -import { BCS, TxnBuilderTypes, TransactionBuilderABI } from "./transaction_builder"; +import { BCS, TransactionBuilderABI } from "./transaction_builder"; import { MAX_U64_BIG_INT } from "./transaction_builder/bcs/consts"; import { TOKEN_ABIS } from "./abis"; @@ -27,34 +27,6 @@ export class TokenClient { this.transactionBuilder = new TransactionBuilderABI(TOKEN_ABIS.map((abi) => new HexString(abi).toUint8Array())); } - /** - * Brings together methods for generating, signing and submitting transaction - * @param account AptosAccount which will sign a transaction - * @param payload Transaction payload. It depends on transaction type you want to send - * @returns Promise that resolves to transaction hash - */ - async submitTransactionHelper(account: AptosAccount, payload: TxnBuilderTypes.TransactionPayload) { - const [{ sequence_number: sequnceNumber }, chainId] = await Promise.all([ - this.aptosClient.getAccount(account.address()), - this.aptosClient.getChainId(), - ]); - - const rawTxn = new TxnBuilderTypes.RawTransaction( - TxnBuilderTypes.AccountAddress.fromHex(account.address()), - BigInt(sequnceNumber), - payload, - 500000n, - 1n, - BigInt(Math.floor(Date.now() / 1000) + 20), - new TxnBuilderTypes.ChainId(chainId), - ); - - const bcsTxn = AptosClient.generateBCSTransaction(account, rawTxn); - const transactionRes = await this.aptosClient.submitSignedBCSTransaction(bcsTxn); - - return transactionRes.hash; - } - /** * Creates a new NFT collection within the specified account * @param account AptosAccount where collection will be created @@ -62,7 +34,7 @@ export class TokenClient { * @param description Collection description * @param uri URL to additional info about collection * @param maxAmount Maximum number of `token_data` allowed within this collection - * @returns A hash of transaction + * @returns The hash of the transaction submitted to the API */ async createCollection( account: AptosAccount, @@ -77,7 +49,7 @@ export class TokenClient { [name, description, uri, maxAmount, [false, false, false]], ); - return this.submitTransactionHelper(account, payload); + return this.aptosClient.generateSignSubmitTransaction(account, payload); } /** @@ -95,7 +67,7 @@ export class TokenClient { * @param property_keys the property keys for storing on-chain properties * @param property_values the property values to be stored on-chain * @param property_types the type of property values - * @returns A hash of transaction + * @returns The hash of the transaction submitted to the API */ async createToken( account: AptosAccount, @@ -111,7 +83,7 @@ export class TokenClient { property_keys: Array = [], property_values: Array = [], property_types: Array = [], - ): Promise { + ): Promise { const payload = this.transactionBuilder.buildTransactionPayload( "0x3::token::create_token_script", [], @@ -132,7 +104,7 @@ export class TokenClient { ], ); - return this.submitTransactionHelper(account, payload); + return this.aptosClient.generateSignSubmitTransaction(account, payload); } /** @@ -144,7 +116,7 @@ export class TokenClient { * @param name Token name * @param amount Amount of tokens which will be transfered * @param property_version the version of token PropertyMap with a default value 0. - * @returns A hash of transaction + * @returns The hash of the transaction submitted to the API */ async offerToken( account: AptosAccount, @@ -161,7 +133,7 @@ export class TokenClient { [receiver, creator, collectionName, name, property_version, amount], ); - return this.submitTransactionHelper(account, payload); + return this.aptosClient.generateSignSubmitTransaction(account, payload); } /** @@ -172,7 +144,7 @@ export class TokenClient { * @param collectionName Name of collection where token is stored * @param name Token name * @param property_version the version of token PropertyMap with a default value 0. - * @returns A hash of transaction + * @returns The hash of the transaction submitted to the API */ async claimToken( account: AptosAccount, @@ -188,7 +160,7 @@ export class TokenClient { [sender, creator, collectionName, name, property_version], ); - return this.submitTransactionHelper(account, payload); + return this.aptosClient.generateSignSubmitTransaction(account, payload); } /** @@ -199,7 +171,7 @@ export class TokenClient { * @param collectionName Name of collection where token is strored * @param name Token name * @param property_version the version of token PropertyMap with a default value 0. - * @returns A hash of transaction + * @returns The hash of the transaction submitted to the API */ async cancelTokenOffer( account: AptosAccount, @@ -215,7 +187,7 @@ export class TokenClient { [receiver, creator, collectionName, name, property_version], ); - return this.submitTransactionHelper(account, payload); + return this.aptosClient.generateSignSubmitTransaction(account, payload); } /**