-
Notifications
You must be signed in to change notification settings - Fork 255
feat: Add auto-mass payouts example #188
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new TypeScript-based CLI example for automating mass payouts on BSC/opBNB networks. The implementation provides a CSV-driven workflow for distributing BNB or ERC-20 tokens to multiple recipients with safeguards including dry-run validation, configurable batching, and transaction delays.
Key changes:
- CSV-based payout automation supporting both native BNB and ERC-20 token transfers
- Comprehensive CLI with dry-run mode, batch processing, and configurable transaction parameters
- Documentation including setup instructions, usage examples, and security best practices
Reviewed changes
Copilot reviewed 8 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web/list.json | Adds the auto-mass-payouts example to the web listing with appropriate tags and metadata |
| typescript/auto-mass-payouts/src/index.ts | Main implementation with CSV parsing, argument handling, balance validation, and batch transaction processing |
| typescript/auto-mass-payouts/package.json | Project configuration with dependencies for ethers, CSV parsing, and TypeScript execution |
| typescript/auto-mass-payouts/tsconfig.json | TypeScript compiler configuration for ES2020/ES2022 with strict mode |
| typescript/auto-mass-payouts/README.md | Comprehensive documentation covering features, setup, usage examples, and security notes |
| typescript/auto-mass-payouts/.env.example | Environment template for RPC URL and private key configuration |
| typescript/auto-mass-payouts/payouts.csv | Sample CSV file demonstrating the required address,amount format |
| typescript/auto-mass-payouts/package-lock.json | Dependency lock file ensuring reproducible installs |
| README.md | Updates root README to include the new auto-mass-payouts example in the project listing |
| package-lock.json | Updates unrelated root dependencies (semantic-release ecosystem) |
Files not reviewed (1)
- typescript/auto-mass-payouts/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| } | ||
| } else { | ||
| const balance = await provider.getBalance(wallet.address); | ||
| if (balance <= total) { |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The balance check uses less than or equal to (balance <= total), which could cause insufficient gas issues. The check should ensure there's a sufficient margin for gas by using strict less than (balance < total) or adding a gas buffer. With the current logic, if balance equals total exactly, there would be no BNB left for gas fees, causing transaction failures.
| for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) { | ||
| const batch = batches[batchIndex]; | ||
| console.log(`Batch ${batchIndex + 1}/${batches.length}`); | ||
|
|
||
| for (const recipient of batch) { | ||
| let tx: ethers.TransactionResponse; | ||
| if (token) { | ||
| tx = await token.transfer(recipient.address, recipient.amount); | ||
| } else { | ||
| tx = await wallet.sendTransaction({ | ||
| to: recipient.address, | ||
| value: recipient.amount | ||
| }); | ||
| } | ||
|
|
||
| console.log( | ||
| `Sent ${ethers.formatUnits(recipient.amount, decimals)} ${symbol} to ${ | ||
| recipient.address | ||
| } | tx ${tx.hash}` | ||
| ); | ||
|
|
||
| if (options.confirmations > 0) { | ||
| await tx.wait(options.confirmations); | ||
| } | ||
|
|
||
| if (options.delayMs > 0) { | ||
| await sleep(options.delayMs); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| console.log("All payouts completed."); |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing error handling for transaction failures within the batch loop. If a transaction fails during batch processing, the script will exit without reporting which recipients were successfully paid and which remain unpaid. Consider adding try-catch blocks around individual transactions and implementing a failure tracking mechanism to allow users to resume from failed transactions or generate a report of completed vs. failed payouts.
| for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) { | |
| const batch = batches[batchIndex]; | |
| console.log(`Batch ${batchIndex + 1}/${batches.length}`); | |
| for (const recipient of batch) { | |
| let tx: ethers.TransactionResponse; | |
| if (token) { | |
| tx = await token.transfer(recipient.address, recipient.amount); | |
| } else { | |
| tx = await wallet.sendTransaction({ | |
| to: recipient.address, | |
| value: recipient.amount | |
| }); | |
| } | |
| console.log( | |
| `Sent ${ethers.formatUnits(recipient.amount, decimals)} ${symbol} to ${ | |
| recipient.address | |
| } | tx ${tx.hash}` | |
| ); | |
| if (options.confirmations > 0) { | |
| await tx.wait(options.confirmations); | |
| } | |
| if (options.delayMs > 0) { | |
| await sleep(options.delayMs); | |
| } | |
| } | |
| } | |
| console.log("All payouts completed."); | |
| const successfulRecipients: typeof recipients = []; | |
| const failedRecipients: { recipient: (typeof recipients)[number]; error: unknown }[] = []; | |
| for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) { | |
| const batch = batches[batchIndex]; | |
| console.log(`Batch ${batchIndex + 1}/${batches.length}`); | |
| for (const recipient of batch) { | |
| try { | |
| let tx: ethers.TransactionResponse; | |
| if (token) { | |
| tx = await token.transfer(recipient.address, recipient.amount); | |
| } else { | |
| tx = await wallet.sendTransaction({ | |
| to: recipient.address, | |
| value: recipient.amount | |
| }); | |
| } | |
| console.log( | |
| `Sent ${ethers.formatUnits(recipient.amount, decimals)} ${symbol} to ${ | |
| recipient.address | |
| } | tx ${tx.hash}` | |
| ); | |
| if (options.confirmations > 0) { | |
| await tx.wait(options.confirmations); | |
| } | |
| if (options.delayMs > 0) { | |
| await sleep(options.delayMs); | |
| } | |
| successfulRecipients.push(recipient); | |
| } catch (error) { | |
| failedRecipients.push({ recipient, error }); | |
| console.error( | |
| `Failed to send payout to ${recipient.address}: ${ | |
| error instanceof Error ? error.message : error | |
| }` | |
| ); | |
| continue; | |
| } | |
| } | |
| } | |
| if (failedRecipients.length === 0) { | |
| console.log("All payouts completed."); | |
| } else { | |
| console.warn( | |
| `Payouts completed with some failures. Successful: ${successfulRecipients.length}, Failed: ${failedRecipients.length}.` | |
| ); | |
| console.warn("Failed recipients:"); | |
| for (const { recipient, error } of failedRecipients) { | |
| console.warn( | |
| ` ${recipient.address}: ${ | |
| error instanceof Error ? error.message : error | |
| }` | |
| ); | |
| } | |
| } |
| { | ||
| "name": "auto-mass-payouts", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "payout": "tsx src/index.ts", | ||
| "dry-run": "tsx src/index.ts --csv payouts.csv --dry-run" | ||
| }, | ||
| "dependencies": { | ||
| "csv-parse": "^5.5.0", | ||
| "dotenv": "^16.4.5", | ||
| "ethers": "^6.12.1" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node": "^22.10.2", | ||
| "tsx": "^4.19.2", | ||
| "typescript": "^5.7.2" | ||
| } | ||
| } |
Copilot
AI
Dec 28, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing .gitignore file for the auto-mass-payouts project. This is a security concern as sensitive files like .env, node_modules, and build artifacts could accidentally be committed. A .gitignore file should be added to exclude at minimum: node_modules/, .env, .env.*, dist/, build/, and *.log files.
Description
Add a new TypeScript example for auto-mass payouts on BSC/opBNB. The example includes a CSV-driven payout CLI, ERC-20/native support, batching, dry-run validation, and documentation updates plus a sample CSV and env template. This aligns with the Cookbook Challenge wishlist and provides a practical, reproducible payout workflow.
Fixes #N/A
Type of change
How Has This Been Tested?
cd typescript/auto-mass-payoutsnpm installPRIVATE_KEY= RPC_URL=https://bsc-testnet.publicnode.com npm run dry-runChecklist: