From 4bad825a8cd8263d02d182087487bdd22998c2ab Mon Sep 17 00:00:00 2001 From: Greg Nazario Date: Wed, 24 Jan 2024 09:33:42 -0800 Subject: [PATCH 1/3] [README] Fix SDK documentation link --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 52d805ddb..c19976714 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,7 @@ const pendingTransaction = await aptos.signAndSubmitTransaction({ signer: alice, ## Documentation and examples -- For full SDK documentation, check out the [TypeScript SDK documentation](https://aptos.dev/sdks/ts-sdk-v2/) +- For full SDK documentation, check out the [TypeScript SDK documentation](https://aptos.dev/sdks/new-ts-sdk/) - For reference documenation, check out the [API reference documentation](https://aptos-labs.github.io/aptos-ts-sdk/) for the associated version. - For in-depth examples, check out the [examples](./examples) folder with ready-made `package.json` files to get you going quickly! From 95a54f483effa29aa4f5a9798865988930f3d88d Mon Sep 17 00:00:00 2001 From: Maayan Date: Wed, 24 Jan 2024 09:40:01 -0800 Subject: [PATCH 2/3] Remove request url forward slash append (#264) remove request url forward slash append --- CHANGELOG.md | 2 ++ src/client/core.ts | 2 +- tests/e2e/client/aptosRequest.test.ts | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 164d12ea5..dadd74912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T # Unreleased +- Remove request URLs forward slash append + # 1.4.0 (2024-01-08) - Omit `"build" | "simulate" | "submit"` from `aptos` namespace diff --git a/src/client/core.ts b/src/client/core.ts index 346e249c3..cf7df73a6 100644 --- a/src/client/core.ts +++ b/src/client/core.ts @@ -68,7 +68,7 @@ export async function aptosRequest( aptosConfig: AptosConfig, ): Promise> { const { url, path } = options; - const fullUrl = `${url}/${path ?? ""}`; + const fullUrl = path ? `${url}/${path}` : url; const response = await request({ ...options, url: fullUrl }, aptosConfig.client); const result: AptosResponse = { diff --git a/tests/e2e/client/aptosRequest.test.ts b/tests/e2e/client/aptosRequest.test.ts index c3fd5ced8..2f22654aa 100644 --- a/tests/e2e/client/aptosRequest.test.ts +++ b/tests/e2e/client/aptosRequest.test.ts @@ -338,7 +338,7 @@ describe("aptos request", () => { ); } catch (error: any) { expect(error).toBeInstanceOf(AptosApiError); - expect(error.url).toBe(`${NetworkToIndexerAPI[config.network]}/`); + expect(error.url).toBe(`${NetworkToIndexerAPI[config.network]}`); expect(error.status).toBe(200); expect(error.statusText).toBe("OK"); expect(error.data).toHaveProperty("errors"); From 8221132838324bc6f30be08487dbb752e3f37a9c Mon Sep 17 00:00:00 2001 From: Maayan Date: Wed, 24 Jan 2024 14:13:26 -0800 Subject: [PATCH 3/3] `TransactionWorker` to fire events a dapp can listen to (#255) * transaction worker to fire events * type returned events data * address comments --- CHANGELOG.md | 4 + examples/typescript/batch_funds.ts | 32 +++++- examples/typescript/batch_mint.ts | 16 ++- package.json | 1 + pnpm-lock.yaml | 7 ++ src/api/aptos.ts | 2 +- src/api/transaction.ts | 16 ++- src/api/transactionSubmission/management.ts | 106 ++++++++++++++++++ .../management/transactionWorker.ts | 88 ++++++++++++--- 9 files changed, 243 insertions(+), 29 deletions(-) create mode 100644 src/api/transactionSubmission/management.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index dadd74912..db0f78605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,10 @@ All notable changes to the Aptos TypeScript SDK will be captured in this file. T # Unreleased - Remove request URLs forward slash append +- Add events to `TransactionWorker` module that dapps can listen to +- Introduce `aptos.transaction.batch` namespace to handle batch transactions +- Support `aptos.transaction.batch.forSingleAccount()` to send batch transactions for a single account +- Label `aptos.batchTransactionsForSingleAccount()` as `deprecated` to prefer using `aptos.transaction.batch.forSingleAccount()` # 1.4.0 (2024-01-08) diff --git a/examples/typescript/batch_funds.ts b/examples/typescript/batch_funds.ts index fbafd7379..103d069f5 100644 --- a/examples/typescript/batch_funds.ts +++ b/examples/typescript/batch_funds.ts @@ -26,16 +26,17 @@ import { InputGenerateTransactionPayloadData, Network, NetworkToNetworkName, + TransactionWorkerEventsEnum, UserTransactionResponse, } from "@aptos-labs/ts-sdk"; -const APTOS_NETWORK: Network = NetworkToNetworkName[process.env.APTOS_NETWORK] || Network.DEVNET; +const APTOS_NETWORK: Network = NetworkToNetworkName[process.env.APTOS_NETWORK] || Network.LOCAL; const config = new AptosConfig({ network: APTOS_NETWORK }); const aptos = new Aptos(config); async function main() { - const accountsCount = 1; + const accountsCount = 2; const transactionsCount = 10; const totalTransactions = accountsCount * transactionsCount; @@ -97,8 +98,31 @@ async function main() { console.log(`sends ${totalTransactions * senders.length} transactions to ${aptos.config.network}....`); // emit batch transactions - const promises = senders.map((sender) => aptos.batchTransactionsForSingleAccount({ sender, data: payloads })); - await Promise.all(promises); + senders.map((sender) => aptos.transaction.batch.forSingleAccount({ sender, data: payloads })); + + aptos.transaction.batch.on(TransactionWorkerEventsEnum.TransactionSent, async (data) => { + console.log("message:", data.message); + console.log("transaction hash:", data.transactionHash); + }); + + aptos.transaction.batch.on(TransactionWorkerEventsEnum.ExecutionFinish, async (data) => { + // log event output + console.log(data.message); + + // verify accounts sequence number + const accounts = senders.map((sender) => aptos.getAccountInfo({ accountAddress: sender.accountAddress })); + const accountsData = await Promise.all(accounts); + accountsData.forEach((accountData) => { + console.log( + `account sequence number is ${(totalTransactions * senders.length) / 2}: ${ + accountData.sequence_number === "20" + }`, + ); + }); + // worker finished execution, we can now unsubscribe from event listeners + aptos.transaction.batch.removeAllListeners(); + process.exit(0); + }); } main(); diff --git a/examples/typescript/batch_mint.ts b/examples/typescript/batch_mint.ts index 84bd5aa26..3fc60df2b 100644 --- a/examples/typescript/batch_mint.ts +++ b/examples/typescript/batch_mint.ts @@ -26,6 +26,7 @@ import { Network, NetworkToNetworkName, UserTransactionResponse, + TransactionWorkerEventsEnum, } from "@aptos-labs/ts-sdk"; const APTOS_NETWORK: Network = NetworkToNetworkName[process.env.APTOS_NETWORK] || Network.DEVNET; @@ -77,7 +78,20 @@ async function main() { } // batch mint token transactions - aptos.batchTransactionsForSingleAccount({ sender, data: payloads }); + aptos.transaction.batch.forSingleAccount({ sender, data: payloads }); + + aptos.transaction.batch.on(TransactionWorkerEventsEnum.ExecutionFinish, async (data) => { + // log event output + console.log(data); + + // verify account sequence number + const account = await aptos.getAccountInfo({ accountAddress: sender.accountAddress }); + console.log(`account sequence number is 101: ${account.sequence_number === "101"}`); + + // worker finished execution, we can now unsubscribe from event listeners + aptos.transaction.batch.removeAllListeners(); + process.exit(0); + }); } main(); diff --git a/package.json b/package.json index 921ed7371..2b1ffea6d 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@typescript-eslint/eslint-plugin": "^6.14.0", "@typescript-eslint/parser": "^6.14.0", "dotenv": "^16.3.1", + "eventemitter3": "^5.0.1", "eslint": "^8.55.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8efab2c77..2b143f32d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -79,6 +79,9 @@ devDependencies: eslint-plugin-import: specifier: ^2.29.0 version: 2.29.0(@typescript-eslint/parser@6.14.0)(eslint@8.55.0) + eventemitter3: + specifier: ^5.0.1 + version: 5.0.1 graphql: specifier: ^16.8.1 version: 16.8.1 @@ -4203,6 +4206,10 @@ packages: engines: {node: '>=0.10.0'} dev: true + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: true + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} diff --git a/src/api/aptos.ts b/src/api/aptos.ts index 2686dcdf8..92dc0e106 100644 --- a/src/api/aptos.ts +++ b/src/api/aptos.ts @@ -70,7 +70,7 @@ export interface Aptos FungibleAsset, General, Staking, - Omit {} + Omit {} /** In TypeScript, we can’t inherit or extend from more than one class, diff --git a/src/api/transaction.ts b/src/api/transaction.ts index 4a82db440..2978df265 100644 --- a/src/api/transaction.ts +++ b/src/api/transaction.ts @@ -35,10 +35,10 @@ import { SimpleTransaction, } from "../transactions"; import { AccountAddressInput, Account, PrivateKey } from "../core"; -import { TransactionWorker } from "../transactions/management"; import { Build } from "./transactionSubmission/build"; import { Simulate } from "./transactionSubmission/simulate"; import { Submit } from "./transactionSubmission/submit"; +import { TransactionManagement } from "./transactionSubmission/management"; export class Transaction { readonly config: AptosConfig; @@ -49,11 +49,14 @@ export class Transaction { readonly submit: Submit; + readonly batch: TransactionManagement; + constructor(config: AptosConfig) { this.config = config; this.build = new Build(this.config); this.simulate = new Simulate(this.config); this.submit = new Submit(this.config); + this.batch = new TransactionManagement(this.config); } /** @@ -263,6 +266,8 @@ export class Transaction { // TRANSACTION SUBMISSION // /** + * @deprecated Prefer to use `aptos.transaction.batch.forSingleAccount()` + * * Batch transactions for a single account. * * This function uses a transaction worker that receives payloads to be processed @@ -285,14 +290,7 @@ export class Transaction { }): Promise { try { const { sender, data, options } = args; - const transactionWorker = new TransactionWorker(this.config, sender); - - transactionWorker.start(); - - for (const d of data) { - /* eslint-disable no-await-in-loop */ - await transactionWorker.push(d, options); - } + this.batch.forSingleAccount({ sender, data, options }); } catch (error: any) { throw new Error(`failed to submit transactions with error: ${error}`); } diff --git a/src/api/transactionSubmission/management.ts b/src/api/transactionSubmission/management.ts new file mode 100644 index 000000000..39e75a580 --- /dev/null +++ b/src/api/transactionSubmission/management.ts @@ -0,0 +1,106 @@ +import EventEmitter from "eventemitter3"; +import { TransactionWorkerEvents, TransactionWorker, TransactionWorkerEventsEnum } from "../../transactions/management"; +import { InputGenerateTransactionPayloadData, InputGenerateTransactionOptions } from "../../transactions"; +import { AptosConfig } from "../aptosConfig"; +import { Account } from "../../core"; + +export class TransactionManagement extends EventEmitter { + account!: Account; + + transactionWorker!: TransactionWorker; + + readonly config: AptosConfig; + + constructor(config: AptosConfig) { + super(); + this.config = config; + } + + /** + * Internal function to start the transaction worker and + * listen to worker events + * + * @param args.sender The sender account to sign and submit the transaction + */ + private start(args: { sender: Account }): void { + const { sender } = args; + this.account = sender; + this.transactionWorker = new TransactionWorker(this.config, sender); + + this.transactionWorker.start(); + this.registerToEvents(); + } + + /** + * Internal function to push transaction data to the transaction worker. + * + * @param args.data An array of transaction payloads + * @param args.options optional. Transaction generation configurations (excluding accountSequenceNumber) + * + * TODO - make this public once worker supports adding transactions to existing queue + */ + private push(args: { + data: InputGenerateTransactionPayloadData[]; + options?: Omit; + }): void { + const { data, options } = args; + + for (const d of data) { + this.transactionWorker.push(d, options); + } + } + + /** + * Internal function to start listening to transaction worker events + * + * TODO - should we ask events to listen to as an input? + */ + private registerToEvents() { + this.transactionWorker.on(TransactionWorkerEventsEnum.TransactionSent, async (data) => { + this.emit(TransactionWorkerEventsEnum.TransactionSent, data); + }); + this.transactionWorker.on(TransactionWorkerEventsEnum.TransactionSendFailed, async (data) => { + this.emit(TransactionWorkerEventsEnum.TransactionSendFailed, data); + }); + this.transactionWorker.on(TransactionWorkerEventsEnum.TransactionExecuted, async (data) => { + this.emit(TransactionWorkerEventsEnum.TransactionExecuted, data); + }); + this.transactionWorker.on(TransactionWorkerEventsEnum.TransactionExecutionFailed, async (data) => { + this.emit(TransactionWorkerEventsEnum.TransactionExecutionFailed, data); + }); + this.transactionWorker.on(TransactionWorkerEventsEnum.ExecutionFinish, async (data) => { + this.emit(TransactionWorkerEventsEnum.ExecutionFinish, data); + }); + } + + /** + * Send batch transactions for a single account. + * + * This function uses a transaction worker that receives payloads to be processed + * and submitted to chain. + * Note that this process is best for submitting multiple transactions that + * dont rely on each other, i.e batch funds, batch token mints, etc. + * + * If any worker failure, the functions throws an error. + * + * @param args.sender The sender account to sign and submit the transaction + * @param args.data An array of transaction payloads + * @param args.options optional. Transaction generation configurations (excluding accountSequenceNumber) + * + * @return void. Throws if any error + */ + forSingleAccount(args: { + sender: Account; + data: InputGenerateTransactionPayloadData[]; + options?: Omit; + }): void { + try { + const { sender, data, options } = args; + this.start({ sender }); + + this.push({ data, options }); + } catch (error: any) { + throw new Error(`failed to submit transactions with error: ${error}`); + } + } +} diff --git a/src/transactions/management/transactionWorker.ts b/src/transactions/management/transactionWorker.ts index ffec56379..d636e9b41 100644 --- a/src/transactions/management/transactionWorker.ts +++ b/src/transactions/management/transactionWorker.ts @@ -1,5 +1,58 @@ /* eslint-disable no-await-in-loop */ +import EventEmitter from "eventemitter3"; +import { AptosConfig } from "../../api/aptosConfig"; +import { Account } from "../../core"; +import { waitForTransaction } from "../../internal/transaction"; +import { generateTransaction, signAndSubmitTransaction } from "../../internal/transactionSubmission"; +import { PendingTransactionResponse, TransactionResponse } from "../../types"; +import { InputGenerateTransactionOptions, InputGenerateTransactionPayloadData, SimpleTransaction } from "../types"; +import { AccountSequenceNumber } from "./accountSequenceNumber"; +import { AsyncQueue, AsyncQueueCancelledError } from "./asyncQueue"; + +export const promiseFulfilledStatus = "fulfilled"; + +// Event types the worker fires during execution and +// the dapp can listen to +export enum TransactionWorkerEventsEnum { + // fired after a transaction gets sent to the chain + TransactionSent = "transactionSent", + // fired if there is an error sending the transaction to the chain + TransactionSendFailed = "transactionSendFailed", + // fired when a single transaction has executed successfully + TransactionExecuted = "transactionExecuted", + // fired if a single transaction fails in execution + TransactionExecutionFailed = "transactionExecutionFailed", + // fired when the worker has finished its job / when the queue has been emptied + ExecutionFinish = "executionFinish", +} + +// Typed interface of the worker events +export interface TransactionWorkerEvents { + transactionSent: (data: SuccessEventData) => void; + transactionSendFailed: (data: FailureEventData) => void; + transactionExecuted: (data: SuccessEventData) => void; + transactionExecutionFailed: (data: FailureEventData) => void; + executionFinish: (data: ExecutionFinishEventData) => void; +} + +// Type for when the worker has finished its job +export type ExecutionFinishEventData = { + message: string; +}; + +// Type for a success event +export type SuccessEventData = { + message: string; + transactionHash: string; +}; + +// Type for a failure event +export type FailureEventData = { + message: string; + error: string; +}; + /** * TransactionWorker provides a simple framework for receiving payloads to be processed. * @@ -12,19 +65,7 @@ * and 2) waits for the resolution of the execution process or get an execution error. * The worker fires events for any submission and/or execution success and/or failure. */ - -import { AptosConfig } from "../../api/aptosConfig"; -import { Account } from "../../core"; -import { waitForTransaction } from "../../internal/transaction"; -import { generateTransaction, signAndSubmitTransaction } from "../../internal/transactionSubmission"; -import { PendingTransactionResponse, TransactionResponse } from "../../types"; -import { InputGenerateTransactionOptions, InputGenerateTransactionPayloadData, SimpleTransaction } from "../types"; -import { AccountSequenceNumber } from "./accountSequenceNumber"; -import { AsyncQueue, AsyncQueueCancelledError } from "./asyncQueue"; - -const promiseFulfilledStatus = "fulfilled"; - -export class TransactionWorker { +export class TransactionWorker extends EventEmitter { readonly aptosConfig: AptosConfig; readonly account: Account; @@ -79,6 +120,7 @@ export class TransactionWorker { maximumInFlight: number = 100, sleepTime: number = 10, ) { + super(); this.aptosConfig = aptosConfig; this.account = account; this.started = false; @@ -101,7 +143,6 @@ export class TransactionWorker { try { /* eslint-disable no-constant-condition */ while (true) { - if (this.transactionsQueue.isEmpty()) return; const sequenceNumber = await this.accountSequnceNumber.nextSequenceNumber(); if (sequenceNumber === null) return; const transaction = await this.generateNextTransaction(this.account, sequenceNumber); @@ -157,12 +198,23 @@ export class TransactionWorker { // transaction sent to chain this.sentTransactions.push([sentTransaction.value.hash, sequenceNumber, null]); // check sent transaction execution + this.emit(TransactionWorkerEventsEnum.TransactionSent, { + message: `transaction hash ${sentTransaction.value.hash} has been committed to chain`, + transactionHash: sentTransaction.value.hash, + }); await this.checkTransaction(sentTransaction, sequenceNumber); } else { // send transaction failed this.sentTransactions.push([sentTransaction.status, sequenceNumber, sentTransaction.reason]); + this.emit(TransactionWorkerEventsEnum.TransactionSendFailed, { + message: `failed to commit transaction ${this.sentTransactions.length} with error ${sentTransaction.reason}`, + error: sentTransaction.reason, + }); } } + this.emit(TransactionWorkerEventsEnum.ExecutionFinish, { + message: `execute ${sentTransactions.length} transactions finished`, + }); } } catch (error: any) { if (error instanceof AsyncQueueCancelledError) { @@ -188,9 +240,17 @@ export class TransactionWorker { if (executedTransaction.status === promiseFulfilledStatus) { // transaction executed to chain this.executedTransactions.push([executedTransaction.value.hash, sequenceNumber, null]); + this.emit(TransactionWorkerEventsEnum.TransactionExecuted, { + message: `transaction hash ${executedTransaction.value.hash} has been executed on chain`, + transactionHash: sentTransaction.value.hash, + }); } else { // transaction execution failed this.executedTransactions.push([executedTransaction.status, sequenceNumber, executedTransaction.reason]); + this.emit(TransactionWorkerEventsEnum.TransactionExecutionFailed, { + message: `failed to execute transaction ${this.executedTransactions.length} with error ${executedTransaction.reason}`, + error: executedTransaction.reason, + }); } } } catch (error: any) {