Add batch-transfer example app (TypeScript + Go)#21
Conversation
Demonstrates sending N APT transfers in parallel using the key pattern of fetching the sender's sequence number once and incrementing locally, avoiding N network round-trips and enabling true parallel submission. Features shown: - Account generation and faucet funding - Gas price estimation - Sequence number pre-fetching (fetch once, increment locally) - Local build + sign before any network submission - Parallel dispatch (Promise.allSettled / goroutines + WaitGroup) - Exponential backoff retry on confirmation - Structured JSON summary output for cross-SDK comparison CLI: --count N --network devnet|testnet|mainnet (default: devnet, 10 txs)
7ca89f6 to
b50ad6b
Compare
There was a problem hiding this comment.
Pull request overview
Adds a new examples/batch-transfer/ reference example demonstrating the “fetch sequence number once, build+sign locally, submit in parallel, then confirm with retry” batch-submission pattern in both TypeScript (ts-sdk) and Go (aptos-go-sdk), with aligned 4-phase console output and a shared JSON summary shape for cross-SDK comparison.
Changes:
- Add TypeScript batch-transfer app (Bun-based) with parallel submit and confirmation retry.
- Add Go batch-transfer app with goroutine-based parallel submit and confirmation retry.
- Add documentation for the batch-transfer example and register it in
examples/README.md.
Reviewed changes
Copilot reviewed 7 out of 9 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| examples/batch-transfer/typescript/src/main.ts | Implements TS batch build/sign/submit/confirm flow and JSON summary output. |
| examples/batch-transfer/typescript/package.json | Declares Bun runnable example and ts-sdk dependency. |
| examples/batch-transfer/typescript/tsconfig.json | Adds strict TS compilation config for the example. |
| examples/batch-transfer/typescript/bun.lock | Locks Bun dependencies for reproducible installs. |
| examples/batch-transfer/go/main.go | Implements Go batch build/sign/submit/confirm flow and JSON summary output. |
| examples/batch-transfer/go/go.mod | Adds standalone Go module for the example pinned to aptos-go-sdk. |
| examples/batch-transfer/go/go.sum | Locks Go dependency checksums. |
| examples/batch-transfer/README.md | Documents the pattern, usage, expected output, and CLI options. |
| examples/README.md | Adds the new example to the examples index and describes example design principles. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Fund the sender via the devnet/testnet faucet | ||
| const fundResult = await aptos.fundAccount({ | ||
| accountAddress: sender.accountAddress, | ||
| amount: FUND_AMOUNT_OCTAS, | ||
| }); |
There was a problem hiding this comment.
--network advertises mainnet, but this example always generates a fresh sender and funds it via aptos.fundAccount(), which won't work on mainnet (no faucet). Consider either rejecting mainnet with a clear error, or supporting mainnet by requiring a user-provided funded sender key (env var / flag) and skipping faucet funding.
| count := flag.Int("count", 10, "Number of transactions to send") | ||
| networkName := flag.String("network", "devnet", "Network: devnet, testnet, or mainnet") | ||
| flag.Parse() | ||
|
|
There was a problem hiding this comment.
--count is not validated. A negative value (e.g. --count=-1) will cause make(..., *count) to panic when creating slices later. Consider validating *count >= 1 immediately after flag.Parse() and exiting with a clear error message (to match the TypeScript example’s behavior).
| if *count < 1 { | |
| fmt.Fprintf(os.Stderr, "Invalid value for --count: %d (must be >= 1)\n", *count) | |
| os.Exit(1) | |
| } |
| if err := client.Fund(sender.Address, fundAmountOctas); err != nil { | ||
| fmt.Fprintf(os.Stderr, "Failed to fund sender: %v\n", err) | ||
| os.Exit(1) | ||
| } | ||
| fmt.Printf(" ✓ Funded sender with 1 APT\n") |
There was a problem hiding this comment.
--network includes mainnet, but this example always creates a new sender and calls client.Fund(...) (faucet), which won't work on mainnet. Consider rejecting mainnet with a clear error, or adding a mode that uses a user-supplied funded sender account and skips faucet funding.
- Reject --network mainnet with clear error in both TS and Go (example uses faucet, which does not exist on mainnet) - Validate --network against allowed values (devnet|testnet); exit with usage error on unknown input instead of silently falling back to devnet - Validate --count >= 1 in Go to prevent slice-allocation panic - Guard totalSpent against negative values in TypeScript with Math.max(0,...) (Go already had this guard via the uint64 underflow check) - Handle json.MarshalIndent error in Go instead of discarding it - Update docs/comments to remove mainnet from advertised options
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 9 changed files in this pull request and generated 1 comment.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| for (let i = 0; i < args.length; i++) { | ||
| if (args[i] === "--count" && i + 1 < args.length) { | ||
| count = parseInt(args[i + 1], 10); | ||
| if (isNaN(count) || count < 1) { | ||
| console.error("--count must be a positive integer"); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| if (args[i] === "--network" && i + 1 < args.length) { | ||
| network = args[i + 1]; | ||
| if (network === "mainnet") { | ||
| console.error( | ||
| "Error: mainnet is not supported by this example because it funds a new account via faucet.", | ||
| ); | ||
| console.error("To use mainnet, extend this example to accept a pre-funded sender key."); | ||
| process.exit(1); | ||
| } | ||
| if (!["devnet", "testnet"].includes(network)) { | ||
| console.error(`Error: unknown network "${network}". Allowed values: devnet, testnet`); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The CLI argument parsing doesn't skip the argument value after processing a flag. When an argument like "--count 20" is encountered, the loop should increment i to skip "20" after processing it. Otherwise, if someone passes "--count --network devnet", the "--network" will be treated as the count value (which would fail the parseInt), but in general this pattern could cause unexpected behavior. Consider adding "i++" after processing each flag's value, or use a proper argument parsing library.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 9 changed files in this pull request and generated no new comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Summary
examples/batch-transfer/with reference implementations in TypeScript and GoKey pattern demonstrated
Test plan
cd examples/batch-transfer/typescript && bun install && bun src/main.tsruns against devnetcd examples/batch-transfer/go && go run main.goruns against devnet--countand--networkflags work as expectedbunx tsc --noEmitpassesgo vet ./...andgo build ./...pass