This guide explains the architecture, development practices, and how to extend the Stellar bulk payment system.
The project follows a clean layered architecture with clear separation of concerns:
┌─────────────────────────────────────────────────────────┐
│ UI Layer (Web & CLI) │
│ (pages, components, cli/index.ts) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Application Layer (API Routes) │
│ (app/api/batch-submit/route.ts) │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ Core Business Logic (lib/stellar/) │
│ - Types, Validation, Parsing, Batching, Services │
└─────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────┐
│ External Services (Stellar SDK) │
│ (stellar-sdk) │
└─────────────────────────────────────────────────────────┘
types.ts
- Defines all TypeScript interfaces
- Single source of truth for data structures
- No dependencies, pure types
validator.ts
- Input validation logic
- Validates payments, configuration, and batches
- Clear error messages for debugging
- Used by both CLI and Web UI
parser.ts
- Parses JSON and CSV files
- Flexible input handling
- Validates structure and format
batcher.ts
- Splits payments into transaction batches
- Respects Stellar's 100-operation limit
- Utility functions for asset parsing and summaries
stellar-service.ts
- Main service for Stellar operations
- Handles transaction building, signing, and submission
- Manages sequence numbers and error handling
- Returns structured results
The service directly uses stellar-sdk without intermediate layers. This keeps the code simple and maintainable.
- Input Parsing: Handled separately from validation
- Validation: Independent of business logic
- Batching: Pure function with no side effects
- Stellar Operations: Isolated in StellarService
All operations return structured result objects rather than throwing exceptions. This allows:
- Partial success reporting (some payments succeed, others fail)
- Detailed error information per recipient
- Easy JSON serialization
Secret keys come from environment variables, never from configuration files. This enforces security best practices.
If you need special handling for a specific asset:
- Update
parseAsset()inlib/stellar/utils.ts— this is the single shared parser used by bothbatcher.tsandserver.ts - Add validation in
validator.ts - Update type definitions in
types.ts - Add tests in
tests/
Important — server-side signing:
StellarServiceinserver.ts(used by/api/batch-submitwithSTELLAR_SECRET_KEY) builds payment operations by callingparseAssetfromlib/stellar/utils.ts. This shared parser handles"XLM","native", and"CODE:ISSUER"strings. Do not introduce a localparseAssetinserver.ts; doing so will break issued-asset batches (USDC, etc.) on the server-submit path.
To add rate limiting per account:
- Create
lib/stellar/rate-limiter.ts - Track submission times per public key
- Return error if limit exceeded
- Update StellarService to use it
For production deployments wanting to track batch history:
- Create
lib/stellar/persistence.ts - Use database adapter pattern
- Store results before returning to user
- Add endpoint to retrieve historical results
For advanced security:
- Create
lib/stellar/multisig.ts - Handle multiple signers
- Update transaction signing in StellarService
- Add configuration for signers
To work on the Soroban smart contracts, you need:
- Rust & WASM: Install Rust and add the WASM target:
rustup target add wasm32-unknown-unknown
- Stellar CLI: Install the Stellar CLI:
cargo install --locked stellar-cli --features opt
- Docker: Required for running a local Stellar node.
We use the Stellar Quickstart Docker image to run a local network with Soroban RPC enabled.
-
Start the local node:
docker-compose up -d
This starts a standalone network with Horizon on port
8000and Soroban RPC on port8001. -
Configure Stellar CLI for Local:
stellar network add --rpc-url http://localhost:8001 --network-passphrase "Standalone Network ; February 2017" local
-
Generate and Fund Test Keys: Create a local identity for deployment and testing:
stellar keys generate --network local aliceNote: In standalone mode, the CLI will automatically try to fund the account via the local friendbot. If you need to fund it manually:
curl "http://localhost:8000/friendbot?addr=$(stellar keys address alice)"
The contracts are located in the contracts/ directory.
-
Build the contract:
cd contracts/batch-vesting cargo build --target wasm32-unknown-unknown --release -
Run tests:
cargo test
Use the provided script to deploy to your local node:
./scripts/deploy-contract.sh local alice- Configure Testnet:
stellar network add --rpc-url https://soroban-testnet.stellar.org:443 --network-passphrase "Test SDF Test Network ; September 2015" testnet - Generate/Add Testnet Account:
stellar keys generate --network testnet deployer
- Deploy:
./scripts/deploy-contract.sh testnet deployer
The batch-vesting contract allows a sender to deposit tokens for multiple recipients with a shared unlock time.
- Storage: Uses
Persistentstorage for vesting records. - Limits: Enforces
MAX_BATCH_SIZE = 100to ensure transactions fit within ledger limits. - Security: Only the recipient can claim their funds, and only after the
unlock_time.
The contract uses Soroban's standard upgradeability pattern. Upgrades are gated by admin authorization and a configurable timelock to ensure security and transparency.
- Propose Upgrade: The admin calls
propose_upgrade(new_wasm_hash). This records the hash and calculates an execution timestamp based on the currentupgrade_timelockconfiguration. - Wait: The community/users have a window to review the new code before the timelock expires.
- Execute Upgrade: After the timelock expires, the admin calls
execute_upgrade(). This replaces the contract code with the new WASM while preserving all current storage.
Events for Auditability:
UpgradeProposed(new_wasm_hash, execute_at)UpgradeExecuted(new_wasm_hash)
Test individual functions in isolation:
// Test validator
validatePaymentInstruction(...)
validatePaymentInstructions(...)
// Test parser
parseJSON(...)
parseCSV(...)
// Test batcher
createBatches(...)
getBatchSummary(...)Test the full flow with mock Stellar server:
// Would require mocking stellar-sdk
const service = new StellarService(config);
const result = await service.submitBatch(payments);
// Assert result structure-
CLI Testing:
STELLAR_SECRET_KEY="..." node cli/index.ts \ --input examples/payments.json \ --network testnet -
Web UI Testing:
npm run dev # Open http://localhost:3000 # Upload test file
-
Testnet Validation:
- Create testnet account at friendbot.stellar.org
- Fund it with test lumens
- Run against testnet
- Verify transactions on https://stellar.expert/explorer/testnet
- Functions:
camelCase, verbs for actions (validatePayment,createBatch) - Variables:
camelCase, nouns for data - Types:
PascalCase - Constants:
UPPER_SNAKE_CASE
Write comments for:
- Complex algorithms (e.g., sequence number management)
- Why, not what (code shows what it does)
- Edge cases and limitations
- Transaction building logic
- Throw errors for programmer mistakes (invalid config)
- Return errors in results for user input issues
- Provide context in error messages
- Include what was expected and what was received
- Validate at entry points (parseInput, API routes)
- Validate early, fail fast
- Provide specific error messages for each field
- Test validation with both valid and invalid inputs
The current implementation increments sequence numbers in memory. For production:
// Current approach (in-memory)
let sequenceNumber = BigInt(sourceAccount.sequenceNumber);
// Production approach
// - Use database to track sequence numbers
// - Or implement read-modify-write with retry logicCurrent implementation uses hardcoded max of 100 operations. Consider:
// Could be configurable based on fee market
const maxOps = network === 'testnet' ? 100 : 50;Consider implementing retry for transient failures:
async submitWithRetry(transaction, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await this.submitTransaction(transaction);
} catch (error) {
if (isTransientError(error) && i < maxRetries - 1) {
await sleep(Math.pow(2, i) * 1000); // exponential backoff
} else {
throw;
}
}
}
}Add to stellar-service.ts:
private log(message: string, data?: any) {
if (process.env.DEBUG) {
console.error(`[StellarService] ${message}`, data);
}
}Before submitting:
const transactionEnvelope = transaction.toXDR();
console.log('Transaction XDR:', transactionEnvelope);- Testnet Explorer: https://stellar.expert/explorer/testnet
- Mainnet Explorer: https://stellar.expert/explorer/public
- Horizon API: https://developers.stellar.org/docs/learn/fundamentals/stellar-data-structure
- All tests pass
- No console.log statements (use proper logging)
- Environment variables documented
- Rate limiting configured
- Error messages don't expose internal details
- Testnet validation completed
- Monitoring/alerting configured
- Backup and recovery plan in place
- Security review completed
-
Secret Key Management
- Never log secret keys
- Always use environment variables
- Consider using secret management service
-
Input Validation
- Validate all external input
- Check transaction limits
- Verify addresses are valid Stellar keys
-
Error Messages
- Don't reveal internal details
- Don't expose secrets in errors
- Log detailed info server-side only
-
Transaction Safety
- Always use sequence numbers
- Implement idempotency if needed
- Consider transaction timeouts
-
Network Security
- Use HTTPS for web UI
- Validate Stellar server certificates
- Use testnet for development
The account hasn't been funded yet.
- Testnet: Use friendbot.stellar.org
- Mainnet: Fund with at least 1 XLM
Sequence number mismatch. Likely causes:
- Multiple submissions in parallel
- Account state changed between reads
- Need to implement sequence number tracking
Recipient address is invalid:
- Check Stellar address format
- Verify address exists and is funded
Batch is too large for single transaction:
- Reduce
maxOperationsPerTransaction - Split into multiple batches
- Consider network congestion
- Event Sourcing: Store all actions for audit trail
- WebSocket Support: Real-time progress updates
- GraphQL API: More flexible querying
- Multi-language SDKs: Python, Go, Rust versions
- Scheduler: Automated batch submissions at intervals
- Analytics Dashboard: Track batch history and metrics