Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,3 +407,6 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) for the full guide.

<!-- handsoff-issue-912 -->
- #912: Implement advanced filter DSL for invoice queries

<!-- handsoff-issue-1169 -->
- #1169: Implement query optimization with indexes
183 changes: 174 additions & 9 deletions examples/batch-payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,144 @@ interface PayrollItem {
amountUsdc: string;
}

interface BatchOperation<T> {
id: string;
payload: T;
}

interface BatchResult<T> {
id: string;
success: boolean;
value?: T;
error?: string;
attempts: number;
}

interface BatchAnalytics {
totalOperations: number;
totalBatches: number;
succeeded: number;
failed: number;
retried: number;
durationMs: number;
}

interface BatchProcessorOptions {
batchSize: number;
maxRetries: number;
retryDelayMs: number;
}

/**
* Groups operations into fixed-size batches to reduce per-operation overhead.
*/
function chunkOperations<T>(operations: BatchOperation<T>[], batchSize: number): BatchOperation<T>[][] {
if (!Number.isInteger(batchSize) || batchSize <= 0) {
throw new Error(`Invalid batch size: ${batchSize}. Must be a positive integer.`);
}
const batches: BatchOperation<T>[][] = [];
for (let i = 0; i < operations.length; i += batchSize) {
batches.push(operations.slice(i, i + batchSize));
}
return batches;
}

/**
* Validates a batch before execution, rejecting invalid operations up front.
*/
function validateBatch<T>(
batch: BatchOperation<T>[],
validate: (op: BatchOperation<T>) => string | null,
): void {
const seen = new Set<string>();
const errors: string[] = [];
for (const op of batch) {
if (!op.id) {
errors.push("operation missing id");
continue;
}
if (seen.has(op.id)) {
errors.push(`duplicate operation id: ${op.id}`);
}
seen.add(op.id);
const reason = validate(op);
if (reason) {
errors.push(`${op.id}: ${reason}`);
}
}
if (errors.length > 0) {
throw new Error(`Batch validation failed:\n - ${errors.join("\n - ")}`);
}
}

/**
* Executes a batch with per-operation retry and graceful error handling.
*/
async function executeBatch<T>(
batch: BatchOperation<T>[],
execute: (op: BatchOperation<T>) => Promise<T>,
options: BatchProcessorOptions,
): Promise<BatchResult<T>[]> {
const results: BatchResult<T>[] = [];
for (const op of batch) {
let attempts = 0;
let lastError: unknown;
let succeeded = false;
let value: T | undefined;
while (attempts <= options.maxRetries && !succeeded) {
attempts += 1;
try {
value = await execute(op);
succeeded = true;
} catch (err) {
lastError = err;
if (attempts <= options.maxRetries) {
await new Promise((resolve) => setTimeout(resolve, options.retryDelayMs));
}
}
}
results.push({
id: op.id,
success: succeeded,
value,
error: succeeded ? undefined : lastError instanceof Error ? lastError.message : String(lastError),
attempts,
});
}
return results;
}

/**
* Processes all operations in batches, collecting analytics across the run.
*/
async function processBatches<T>(
operations: BatchOperation<T>[],
validate: (op: BatchOperation<T>) => string | null,
execute: (op: BatchOperation<T>) => Promise<T>,
options: BatchProcessorOptions,
): Promise<{ results: BatchResult<T>[]; analytics: BatchAnalytics }> {
const startedAt = Date.now();
const batches = chunkOperations(operations, options.batchSize);
const results: BatchResult<T>[] = [];

for (const batch of batches) {
validateBatch(batch, validate);
const batchResults = await executeBatch(batch, execute, options);
results.push(...batchResults);
}

const analytics: BatchAnalytics = {
totalOperations: operations.length,
totalBatches: batches.length,
succeeded: results.filter((r) => r.success).length,
failed: results.filter((r) => !r.success).length,
retried: results.filter((r) => r.attempts > 1).length,
durationMs: Date.now() - startedAt,
};

return { results, analytics };
}

async function main() {
console.log("==================================================================");
console.log(" StellarSplit SDK: Batch Payments & Payout Processing");
Expand Down Expand Up @@ -83,26 +221,53 @@ async function main() {
amount: parseAmount(p.amountUsdc),
}));

const simulatedResults = batchPayments.map((bp, idx) => ({
invoiceId: bp.invoiceId,
txHash: `0x7f${idx}a9bc8d7e6f5${idx}4b3c2d1e0f`,
success: true,
amount: bp.amount,
const operations: BatchOperation<{ invoiceId: string; amount: bigint }>[] = batchPayments.map((bp) => ({
id: bp.invoiceId,
payload: bp,
}));

const processorOptions: BatchProcessorOptions = {
batchSize: 10,
maxRetries: 2,
retryDelayMs: 100,
};

const { results, analytics } = await processBatches(
operations,
(op) => (op.payload.amount <= 0n ? "amount must be positive" : null),
async (op) => {
// Simulated submission; replace with client.batchPay in production.
return {
invoiceId: op.payload.invoiceId,
txHash: `0x7f${op.id.slice(-3)}a9bc8d7e6f5${op.id.slice(-1)}4b3c2d1e0f`,
amount: op.payload.amount,
};
},
processorOptions,
);

console.log(" Submitting atomic batch payment transaction...");
for (const res of simulatedResults) {
console.log(` ✓ Payment to ${res.invoiceId} confirmed: ${res.txHash} (${formatAmount(res.amount)} USDC)`);
for (const res of results) {
if (res.success && res.value) {
console.log(` ✓ Payment to ${res.value.invoiceId} confirmed: ${res.value.txHash} (${formatAmount(res.value.amount)} USDC)`);
} else {
console.log(` ✗ Payment to ${res.id} failed after ${res.attempts} attempt(s): ${res.error}`);
}
}
console.log();

// Step 4: Verification and Reconciliation
console.log("4. Verifying Batch Integrity:");
const allSucceeded = simulatedResults.every((r) => r.success);
const allSucceeded = results.every((r) => r.success);
console.log(` Batch status: ${allSucceeded ? "ALL PAYMENTS CONFIRMED" : "PARTIAL FAILURE DETECTED"}`);

const totalDisbursed = simulatedResults.reduce((sum, r) => sum + r.amount, 0n);
const totalDisbursed = results.reduce((sum, r) => sum + (r.value?.amount ?? 0n), 0n);
console.log(` Total Disbursed: ${formatAmount(totalDisbursed)} USDC`);

console.log("5. Batch Analytics:");
console.log(` Batches: ${analytics.totalBatches} | Operations: ${analytics.totalOperations}`);
console.log(` Succeeded: ${analytics.succeeded} | Failed: ${analytics.failed} | Retried: ${analytics.retried}`);
console.log(` Duration: ${analytics.durationMs}ms`);
console.log("==================================================================");
console.log(" ✓ Batch Payment application completed successfully.");
console.log("==================================================================");
Expand Down
Loading