[476] Backend: Add per-wallet daily withdrawal limit guard with admin override#655
Conversation
|
@yinkscss Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR adds a daily withdrawal limit guard with admin override, an impersonation session ledger with start/end/list endpoints, and updates the Soroban client to use the newer @stellar/stellar-sdk rpc namespace. It also includes several smaller refactors and cleanups.
Changes:
- New
withdrawalDailyLimitMiddlewareand admin endpoints to enforce/override per-wallet daily withdrawal caps. - New
impersonationSessionServicewith Prisma models, migration, ledger entries, and session-bound/admin/impersonate/:walletaccess. - Migration to
@stellar/stellar-sdkv13rpcnamespace and various small refactors (tracing noop span, redis SET arg order, Prisma.join separator, etc.).
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| backend/src/middleware/withdrawalDailyLimit.ts | New middleware enforcing daily withdrawal caps with override support |
| backend/src/middleware/validate.ts | Adds overrideReason to VaultOperationSchema |
| backend/src/vaultEndpoints.ts | Wires withdrawal limit middleware into /withdrawals |
| backend/src/impersonationSessionService.ts | New session/ledger service with memory/prisma/hybrid storage |
| backend/src/index.ts | New impersonation session and withdrawal-override admin routes |
| backend/src/sorobanClient.ts | Updates to new Stellar SDK rpc namespace and response shape |
| backend/src/tracing.ts | Replaces real span with shared NOOP_SPAN when OTEL disabled |
| backend/src/pagination.ts | Changes invalidCursor from let to const |
| backend/src/exportJobs.ts, apiKeyAudit.ts | Replaces Prisma.sql separator with raw string in Prisma.join |
| backend/src/eventPollingService.ts | Reorders Redis SET options; removes duplicate export |
| backend/src/emailQueue.ts | Uses getPrismaClient(); coerces nullable text/html |
| backend/src/latencyMonitoring.ts, integration-test.ts | Minor regex/let→const cleanups |
| backend/prisma/schema.prisma + migrations | Adds AdminImpersonationSession, AdminImpersonationLedgerEntry, EmailQueue |
| backend/src/tests/*.test.ts | New tests for withdrawal limits and impersonation sessions; governance updated |
| backend/.env.example, package.json | New env vars and @stellar/stellar-sdk dependency |
Files not reviewed (1)
- backend/package-lock.json: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (txResponse.status !== 'PENDING') { | ||
| const errorMessage = `Soroban transaction submission failed: ${ | ||
| txResponse.errorResult?.toXDR?.('base64') || 'Unknown error' | ||
| }`; |
| function createId(prefix: string): string { | ||
| return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`; | ||
| } |
| const limit = query.limit || DEFAULT_PAGINATION_CONFIG.defaultLimit; | ||
| let startIndex = 0; | ||
| let invalidCursor = false; | ||
| const invalidCursor = false; |
| const inMemoryOverrides = new Map<string, WithdrawalLimitOverrideRecord>(); | ||
| const inMemoryAuditLog: Array<Record<string, unknown>> = []; |
| walletAddress: walletAddressSchema, | ||
| email: z.string().email().optional(), | ||
| referralCode: z.string().max(64).optional(), | ||
| overrideReason: z.string().min(1).max(500).optional(), | ||
| }) | ||
| .strict(); |
| async function sumWithdrawalsForDay(wallet: string, start: Date, end: Date): Promise<Decimal> { | ||
| const rows = await prisma.transaction.findMany({ | ||
| where: { | ||
| user: wallet, | ||
| type: 'withdrawal', | ||
| timestamp: { | ||
| gte: start, | ||
| lt: end, | ||
| }, | ||
| }, | ||
| select: { amount: true }, | ||
| }); | ||
|
|
||
| return rows.reduce((total: Decimal, row: { amount: string }) => total.plus(new Decimal(row.amount || '0')), new Decimal(0)); |
| const whereSql = | ||
| whereClauses.length > 0 | ||
| ? Prisma.sql`WHERE ${Prisma.join(whereClauses, Prisma.sql` AND `)}` | ||
| ? Prisma.sql`WHERE ${Prisma.join(whereClauses, ' AND ')}` |
| const NOOP_SPAN = { | ||
| setAttributes: () => {}, | ||
| setStatus: () => {}, | ||
| recordException: () => {}, | ||
| end: () => {}, | ||
| } as unknown as Span; |
|
|
||
| // Try to acquire lock with 30 second expiry | ||
| const result = await client.set(this.lockKey, this.lockValue, 'NX', 'PX', 30000); | ||
| const result = await client.set(this.lockKey, this.lockValue, 'PX', 30000, 'NX'); |
| const limit = typeof req.query.limit === 'string' ? parseInt(req.query.limit, 10) : 50; | ||
| res.status(200).json({ | ||
| entries: listWithdrawalLimitAuditEntries(Number.isFinite(limit) ? limit : 50), | ||
| }); |
Enforces configurable UTC daily caps on withdrawals, returns structured limit metadata on blocks, and supports super-admin overrides with full audit logging.
…ion. Registers default admin keys in test setup and updates test wallets to valid 56-character base32 addresses so governance checks pass in CI.
Aligns validation, referral, and transaction tests with the 56-character base32 address format enforced by request schemas.
5ec24c5 to
726d02d
Compare
Prevents CI migrate deploy from failing when a stale dev.db already contains tables pending migration.
…allets. Sets high rate-limit ceilings before app bootstrap and fixes referral integration fixtures to use valid Stellar addresses.
Summary
WITHDRAWAL_DAILY_LIMIT_USDCx-admin-override-withdrawal+overrideReason) and admin override grants viaPOST /admin/withdrawal-limits/overrideGET /admin/withdrawal-limits/auditCloses #476
Test plan
npm test -- --testPathPattern=withdrawalDailyLimit(3 tests passing)