Course Capstone - Production-Grade Integration
Learning Objectives
Built a complete, production-ready payment processing system
Integrated all major concepts from the course
Demonstrated mastery of XRPL API patterns
Created a reference implementation for future projects
Validated your skills through comprehensive testing
XRP Payment Gateway - A complete system for sending and receiving XRP payments:
┌─────────────────────────────────────────────────────────────────┐
│ XRP Payment Gateway │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Outgoing │ │ Incoming │ │ Admin │ │
│ │ Payments │ │ Monitor │ │ Dashboard │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └───────────────────┴───────────────────┘ │
│ │ │
│ ┌──────────────────────────┴──────────────────────────┐ │
│ │ Core Integration Layer │ │
│ │ • Connection Pool • Error Handling │ │
│ │ • Retry Logic • Caching │ │
│ │ • Rate Limiting • Metrics │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ┌──────────────────────────┴──────────────────────────┐ │
│ │ XRPL Network │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘Functional Requirements:
Send Payments
Receive Payments
Query Operations
Non-Functional Requirements:
Reliability
Security
Observability
Performance
// Project Structure
src/
├── config/
│ ├── index.js // Environment configuration
│ └── xrpl.js // XRPL-specific config
├── core/
│ ├── connection-pool.js // Connection management
│ ├── cache.js // Caching layer
│ ├── error-handler.js // Error classification
│ └── retry.js // Retry logic
├── services/
│ ├── payment-sender.js // Outgoing payments
│ ├── payment-monitor.js // Incoming payment detection
│ └── query-service.js // Balance/history queries
├── api/
│ ├── routes.js // API endpoints
│ ├── validation.js // Input validation
│ └── middleware.js // Auth, rate limiting
├── monitoring/
│ ├── metrics.js // Metrics collection
│ ├── logger.js // Structured logging
│ └── health.js // Health check
├── webhooks/
│ └── delivery.js // Webhook delivery
└── app.js // Application entry pointOUTGOING PAYMENT FLOW:
──────────────────────
- API Request
- Validation
- Payment Service
- Submission with Retry
- Response
INCOMING PAYMENT FLOW:
──────────────────────
- Subscription
- Transaction Event
- Validation
- Processing
- Webhook Delivery
// src/core/xrpl-gateway.js
const xrpl = require('xrpl')
const { ConnectionPool } = require('./connection-pool')
const { Cache } = require('./cache')
const { ErrorHandler } = require('./error-handler')
const { RetryPolicy } = require('./retry')
class XRPLGateway {
constructor(config) {
this.config = config
this.pool = new ConnectionPool(config.servers, {
poolSize: config.poolSize || 3,
healthCheckInterval: 30000
})
this.cache = new Cache(config.redis)
this.errorHandler = new ErrorHandler()
this.retryPolicy = new RetryPolicy({
maxRetries: 3,
baseDelay: 1000,
maxDelay: 30000
})
}
async initialize() {
await this.pool.initialize()
await this.verifyNetwork()
}
async verifyNetwork() {
const conn = await this.pool.acquire()
try {
const info = await conn.client.request({ command: 'server_info' })
const networkId = info.result.info.network_id
if (this.config.expectedNetwork === 'mainnet' && networkId !== 0) {
throw new Error('Expected mainnet but connected to testnet')
}
if (this.config.expectedNetwork === 'testnet' && networkId !== 1) {
throw new Error('Expected testnet but connected to mainnet')
}
} finally {
this.pool.release(conn)
}
}
async request(requestData) {
// Check cache for cacheable requests
if (this.isCacheable(requestData)) {
const cached = await this.cache.get(requestData)
if (cached) return cached
}
// Execute with retry
const result = await this.retryPolicy.execute(async () => {
const conn = await this.pool.acquire()
try {
return await conn.client.request(requestData)
} finally {
this.pool.release(conn)
}
})
// Cache result
if (this.isCacheable(requestData)) {
await this.cache.set(requestData, result)
}
return result
}
isCacheable(request) {
const nonCacheable = ['submit', 'subscribe', 'unsubscribe']
return !nonCacheable.includes(request.command)
}
async shutdown() {
await this.pool.shutdown()
}
}
module.exports = { XRPLGateway }
```
// src/services/payment-sender.js
class PaymentSender {
constructor(gateway, wallet, metrics) {
this.gateway = gateway
this.wallet = wallet
this.metrics = metrics
}
async send(destination, amountXRP, options = {}) {
const startTime = Date.now()
// Validate
this.validate(destination, amountXRP, options)
// Check destination requires tag
await this.checkDestinationTagRequired(destination, options.destinationTag)
// Build transaction
const payment = {
TransactionType: 'Payment',
Account: this.wallet.address,
Destination: destination,
Amount: xrpl.xrpToDrops(amountXRP)
}
if (options.destinationTag !== undefined) {
payment.DestinationTag = parseInt(options.destinationTag)
}
// Autofill
const prepared = await this.autofill(payment)
// Sign
const signed = this.wallet.sign(prepared)
// Submit and wait
const result = await this.submitAndWait(signed)
// Record metrics
this.metrics.recordPayment({
success: result.success,
amount: amountXRP,
latency: Date.now() - startTime
})
return result
}
validate(destination, amount, options) {
if (!xrpl.isValidAddress(destination)) {
throw new ValidationError('Invalid destination address')
}
const numAmount = parseFloat(amount)
if (isNaN(numAmount) || numAmount <= 0) {
throw new ValidationError('Invalid amount')
}
if (numAmount > this.config.maxPayment) {
throw new ValidationError(Amount exceeds maximum (${this.config.maxPayment}))
}
}
async checkDestinationTagRequired(destination, tag) {
const info = await this.gateway.request({
command: 'account_info',
account: destination,
ledger_index: 'validated'
})
const flags = info.result.account_data.Flags
const lsfRequireDestTag = 0x00020000
if ((flags & lsfRequireDestTag) && tag === undefined) {
throw new ValidationError('Destination requires a destination tag')
}
}
async autofill(payment) {
const conn = await this.gateway.pool.acquire()
try {
return await conn.client.autofill(payment)
} finally {
this.gateway.pool.release(conn)
}
}
async submitAndWait(signed) {
const conn = await this.gateway.pool.acquire()
try {
const result = await conn.client.submitAndWait(signed.tx_blob)
const txResult = result.result.meta.TransactionResult
return {
success: txResult === 'tesSUCCESS',
hash: result.result.hash,
result: txResult,
deliveredAmount: result.result.meta.delivered_amount,
ledger: result.result.ledger_index,
fee: result.result.Fee
}
} finally {
this.gateway.pool.release(conn)
}
}
}
```
// src/services/payment-monitor.js
class PaymentMonitor {
constructor(gateway, config) {
this.gateway = gateway
this.config = config
this.webhookDelivery = new WebhookDelivery(config.webhookUrl)
this.deduplicator = new Deduplicator(config.redis)
this.lastLedger = null
this.client = null
}
async start() {
this.client = new xrpl.Client(this.config.servers[0])
this.client.on('disconnected', () => this.handleDisconnect())
this.client.on('ledgerClosed', (l) => this.handleLedger(l))
this.client.on('transaction', (tx) => this.handleTransaction(tx))
await this.client.connect()
await this.subscribe()
}
async subscribe() {
await this.client.request({
command: 'subscribe',
accounts: [this.config.monitorAddress],
streams: ['ledger']
})
}
async handleLedger(ledger) {
const current = ledger.ledger_index
// Gap detection
if (this.lastLedger && current > this.lastLedger + 1) {
await this.fillGap(this.lastLedger + 1, current - 1)
}
this.lastLedger = current
}
async handleTransaction(tx) {
// Must be a Payment
if (tx.transaction.TransactionType !== 'Payment') return
// Must be incoming (to our address)
if (tx.transaction.Destination !== this.config.monitorAddress) return
// Must have succeeded
if (tx.meta?.TransactionResult !== 'tesSUCCESS') return
// Reject partial payments
if (this.isPartialPayment(tx.transaction)) {
console.warn(Rejecting partial payment: ${tx.transaction.hash})
return
}
// Deduplicate
const isNew = await this.deduplicator.checkAndMark(tx.transaction.hash)
if (!isNew) return
// Process payment
const payment = this.extractPayment(tx)
// Deliver webhook
await this.webhookDelivery.deliver(payment)
}
isPartialPayment(tx) {
const tfPartialPayment = 0x00020000
return (tx.Flags & tfPartialPayment) !== 0
}
extractPayment(tx) {
const delivered = tx.meta?.delivered_amount || tx.transaction.Amount
return {
hash: tx.transaction.hash,
from: tx.transaction.Account,
destinationTag: tx.transaction.DestinationTag,
amount: typeof delivered === 'string'
? parseInt(delivered) / 1_000_000
: parseFloat(delivered.value),
currency: typeof delivered === 'string' ? 'XRP' : delivered.currency,
ledger: tx.ledger_index,
timestamp: new Date().toISOString()
}
}
async fillGap(start, end) {
console.log(Filling gap: ledgers ${start}-${end})
const transactions = await this.gateway.request({
command: 'account_tx',
account: this.config.monitorAddress,
ledger_index_min: start,
ledger_index_max: end
})
for (const tx of transactions.result.transactions) {
await this.handleTransaction({
transaction: tx.tx,
meta: tx.meta,
ledger_index: tx.tx.ledger_index
})
}
}
async handleDisconnect() {
console.log('Monitor disconnected, reconnecting...')
setTimeout(() => this.start(), 5000)
}
}
```
// src/api/routes.js
const express = require('express')
const router = express.Router()
module.exports = function(paymentSender, queryService, metrics) {
// Send payment
router.post('/payments', async (req, res) => {
try {
const { destination, amount, destinationTag } = req.body
const result = await paymentSender.send(destination, amount, {
destinationTag
})
res.json(result)
} catch (error) {
if (error instanceof ValidationError) {
res.status(400).json({ error: error.message })
} else {
console.error('Payment error:', error)
res.status(500).json({ error: 'Internal error' })
}
}
})
// Get balance
router.get('/balance/:address', async (req, res) => {
try {
const balance = await queryService.getBalance(req.params.address)
res.json(balance)
} catch (error) {
if (error.data?.error === 'actNotFound') {
res.status(404).json({ error: 'Account not found' })
} else {
res.status(500).json({ error: 'Internal error' })
}
}
})
// Get transaction status
router.get('/transactions/:hash', async (req, res) => {
try {
const tx = await queryService.getTransaction(req.params.hash)
res.json(tx)
} catch (error) {
if (error.data?.error === 'txnNotFound') {
res.status(404).json({ error: 'Transaction not found' })
} else {
res.status(500).json({ error: 'Internal error' })
}
}
})
// Health check
router.get('/health', async (req, res) => {
const health = await metrics.getHealth()
res.status(health.status === 'healthy' ? 200 : 503).json(health)
})
return router
}
```
Your submission must include tests for:
Unit Tests
Integration Tests (on testnet)
System Tests
# Run all tests
npm test
Run with coverage
npm run test:coverage
Run integration tests (requires testnet)
npm run test:integration
```
| Component | Weight | Criteria |
|---|---|---|
| Core Functionality | 30% | Payments send/receive correctly, queries work |
| Reliability | 20% | Reconnection, retry logic, gap detection |
| Security | 20% | Input validation, partial payment protection, rate limiting |
| Code Quality | 15% | Clean architecture, documentation, error handling |
| Testing | 15% | Coverage, test quality, edge cases |
To pass, your submission must:
- Successfully send XRP payments on testnet
- Detect incoming payments with correct amount
- Reject partial payments
- Handle connection failures with reconnection
- Pass all required test cases
- Include health check endpoint
For distinction, also include:
- Connection pooling with health checks
- Comprehensive caching strategy
- Full metrics implementation
- Webhook delivery with retry
- Audit logging
- Docker deployment configuration
- **Source Code** - Complete project with all components
- **README** - Setup instructions, architecture overview
- **Test Results** - Output from test run with coverage
- **Demo** - Screen recording or live demo of functionality
Your README should enable setup with:
# Clone and install
git clone <your-repo>
cd xrp-payment-gateway
npm install
# Configure
cp .env.example .env
# Edit .env with testnet credentials
# Run tests
npm test
# Start
npm start
This capstone project represents the culmination of your XRPL API integration learning. You've progressed from understanding basic queries to building a production-grade payment system. The patterns and practices you've implemented here form the foundation for any serious XRPL application.
- Deep understanding of XRPL API architecture
- Production-ready payment handling
- Comprehensive error handling and reliability
- Security-first integration practices
- Observable and maintainable code
- Consider running your own XRPL nodes for production
- Explore advanced features (escrow, payment channels, hooks)
- Contribute to the XRPL ecosystem
- Build your own applications on this foundation
Congratulations on completing the XRPL APIs & Integration course!
Time Investment: 8-12 hours for full implementation
Submission Deadline: As specified by instructor
End of Lesson 18 - Course Capstone