Skip to content

Commit

Permalink
Reduce duplication of specialized clients in TS SDK, make transaction…
Browse files Browse the repository at this point in the history
… submission functions more uniform
  • Loading branch information
banool committed Aug 25, 2022
1 parent e6e2f9f commit fbfd761
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 158 deletions.
20 changes: 10 additions & 10 deletions developer-docs-site/docs/tutorials/first-transaction-sdk.md
Original file line number Diff line number Diff line change
Expand Up @@ -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, <code>generateSignSendWaitForTransaction</code> 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:
Expand Down Expand Up @@ -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:

<Tabs groupId="sdk-examples">
<TabItem value="typescript" label="Typescript">

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
```

</TabItem>
<TabItem value="python" label="Python">

The transaction hash can be used to query the status of a transaction:

```python
:!: static/sdks/python/examples/transfer-coin.py section_6
```
</TabItem>
<TabItem value="rust" label="Rust">

The transaction hash can be used to query the status of a transaction:

```rust
:!: static/sdks/rust/examples/transfer-coin.rs section_6
```
Expand Down
9 changes: 6 additions & 3 deletions ecosystem/typescript/sdk/examples/typescript/transfer_coin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===");
Expand All @@ -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 ===");
Expand Down
182 changes: 108 additions & 74 deletions ecosystem/typescript/sdk/src/aptos_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,62 +389,72 @@ 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<Gen.Transaction> {
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<Gen.Transaction> {
let isPending = true;
let count = 0;
let lastTxn: Gen.Transaction | undefined;
while (isPending) {
if (count >= 10) {
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) {
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;
}
Expand All @@ -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<void> {
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
Expand Down Expand Up @@ -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<Gen.Transaction> {
/* eslint-disable max-len */
// :!:>generateSignSendWaitForTransactionInner
): Promise<string> {
// :!:>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<Gen.Transaction> {
const txnHash = await this.generateSignSubmitTransaction(sender, payload, extraArgs);
return this.waitForTransactionWithResult(txnHash, extraArgs);
}
}

Expand All @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion ecosystem/typescript/sdk/src/coin_client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
Expand Down
18 changes: 6 additions & 12 deletions ecosystem/typescript/sdk/src/coin_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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(
Expand All @@ -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<Gen.Transaction> {
): Promise<string> {
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

/**
Expand Down
Loading

0 comments on commit fbfd761

Please sign in to comment.