Skip to content

feat: Payment Request Expiration with Smart Contract Enforcement (#460)#558

Open
rindicomfort wants to merge 2 commits into
Smartdevs17:mainfrom
rindicomfort:feat/issue-460-payment-request-expiration
Open

feat: Payment Request Expiration with Smart Contract Enforcement (#460)#558
rindicomfort wants to merge 2 commits into
Smartdevs17:mainfrom
rindicomfort:feat/issue-460-payment-request-expiration

Conversation

@rindicomfort

Copy link
Copy Markdown
Contributor

Summary

Implements issue #460 — time-bound payment requests enforced at the Soroban contract level, EVM contract level, backend service level, and frontend dashboard.

Closes #460


Changes

contracts/soroban/payment-request/ — Soroban contract

  • create_request — configurable TTL (60 s – 90 days), payer address (open or specific), SEP-41 token, memo; stores expires_at on-ledger
  • pay — calls env.ledger().timestamp() and reverts with RequestIsExpired if past deadline + grace; executes SEP-41 cross-contract transfer on success
  • expire_request — permissionless; anyone can sweep stale requests after their deadline
  • cancel_request — requester-only cancellation
  • renew_request — creates a new request from expired/cancelled with new amount + TTL; emits req_rnwd event linking old and new IDs
  • Events: req_crtd, req_paid, req_expd, req_cncl, req_rnwd

contracts/evm/contracts/PaymentRequestExpiry.sol — EVM contract

  • createRequest — ERC-20 or native ETH, open or designated payer, TTL 60 s – 90 days
  • pay — checks block.timestamp > expiresAt + gracePeriod, lazily marks expired and reverts with RequestIsExpired
  • expireRequest — permissionless sweep
  • cancelRequest — requester-only
  • renewRequest — new amount + TTL from expired/cancelled request; RequestRenewed event with old→new ID link
  • setDefaultGracePeriod — owner-only; default 60 s (mitigates ±15 s block timestamp variance)
  • Custom errors: RequestIsExpired, RequestNotExpiredYet, UnauthorizedPayer, InvalidTtl, etc.

backend/src/services/payments/expiration.ts — Backend service

  • createRequest — validates TTL, persists expiresAt to database
  • assertNotExpired — pre-relay guard: rejects expired/cancelled/paid requests; applies GRACE_PERIOD_MS = 60_000 buffer to match on-chain grace
  • sweepExpired — batched (100/run) DB sweep; transitions pending→expired, sends notifications to requester + payer
  • startExpirationCron — BullMQ repeating job every 2 minutes via Redis; 3 retries with exponential backoff
  • renewRequest — optional rateMultiplier for new exchange rate on renewal
  • listRequests — paginated with status filter (all / pending / paid / expired / cancelled) for dashboard
  • Typed errors: PaymentRequestExpiredError, PaymentRequestNotFoundError, AlreadyPaidError, CancelledError

Prisma schema PaymentRequest model

  • New fields: expiresAt, expiredAt, paidAt, contractRequestId, status (enum)
  • Indexes: (tenantId, status), expiresAt, (status, expiresAt) for sweep query performance

Migration SQL

  • Creates payment_requests table with all expiration fields and indexes

Frontend components

  • PaymentRequestExpirationBadge — live 1-second countdown ticker; colour-coded urgency (blue > 30 min, yellow < 30 min, red < 5 min); Expired / Paid / Cancelled static badges; accessible title with full timestamp
  • PaymentRequestList — dashboard table with filter tabs (All / Pending / Paid / Expired / Cancelled) + per-row counts; Renew button for expired/cancelled; Cancel button for pending; accessible with role, aria-pressed, focus-visible

Acceptance Criteria

Criterion Status
Configurable expiration per request (minutes, hours, days) ✅ TTL param 60 s – 90 days on both Soroban and EVM
Soroban contract enforces expiration pay checks env.ledger().timestamp()
EVM contract enforces expiration pay checks block.timestamp + gracePeriod
Backend rejects expired requests before relay assertNotExpired guard + grace buffer
Auto status transition pending → expired ✅ BullMQ cron sweep + lazy expiry on pay attempt
Notification to requester and payer sendExpirationNotifications in sweep loop
Expired request renewal with new rate renewRequest with optional rateMultiplier
Dashboard filtering for expired requests PaymentRequestList filter tabs

Edge Cases

  • Block timestamp manipulation — 60 s on-chain grace period on both Soroban and EVM absorbs miner/validator variance
  • Timezone handling — all timestamps stored as UTC; frontend uses toLocaleString() for display; BullMQ cron runs in UTC
  • Grace periods — configurable via set_grace_period (Soroban) / setDefaultGracePeriod (EVM); backend GRACE_PERIOD_MS matches
  • Partial fills / race conditionsassertNotExpired + DB status check prevents double-pay; on-chain status is authoritative
  • Large datasets — sweep processes in batches of 100 with cursor pagination to avoid memory pressure

…ement (Smartdevs17#460)

- Add contracts/soroban/payment-request/ — Soroban expiration contract
  - create_request: configurable TTL (60s–90d), stores expiresAt on-ledger
  - pay: enforces expiration via env.ledger().timestamp() + grace period
  - expire_request: callable by anyone after deadline (lazy expiry)
  - cancel_request: requester-only cancellation
  - renew_request: creates new request from expired/cancelled with new TTL
  - Events: req_crtd, req_paid, req_expd, req_cncl, req_rnwd
  - Admin: initialize, set_grace_period
  - Persistent storage with TTL bumps; Soroban SDK 21.7.6

- Update contracts/evm/contracts/PaymentRequestExpiry.sol — EVM enforcement
  - createRequest: payer (open or specific), ERC-20 or native ETH, TTL bounds
  - pay: reverts with RequestIsExpired if block.timestamp > expiresAt + grace
  - expireRequest: permissionless sweep after deadline
  - cancelRequest: requester-only
  - renewRequest: creates new request from expired/cancelled with new rate
  - defaultGracePeriod: admin-configurable (default 60s)
  - Events: RequestCreated, RequestPaid, RequestExpired, RequestCancelled, RequestRenewed

- Add backend/src/services/payments/expiration.ts — backend enforcement
  - createRequest: validates TTL, persists expiresAt to DB
  - assertNotExpired: guards before chain relay; includes GRACE_PERIOD_MS buffer
  - markPaid / cancelRequest helpers
  - renewRequest: optional rateMultiplier for new exchange rate
  - listRequests: paginated with status filter (all/pending/paid/expired/cancelled)
  - sweepExpired: batched DB sweep, transitions pending→expired, sends notifications
  - startExpirationCron: BullMQ cron every 2 minutes via Redis
  - Typed errors: PaymentRequestExpiredError, NotFoundError, AlreadyPaidError

- Add Prisma schema: PaymentRequest model
  - Fields: expiresAt, expiredAt, paidAt, contractRequestId, status enum
  - Indexes: (tenantId,status), expiresAt, (status,expiresAt) for sweep query

- Add migration SQL: payment_requests table + indexes

- Add frontend components
  - PaymentRequestExpirationBadge: live countdown ticker (updates every 1s)
    red (<5min), yellow (<30min), blue (>30min), expired/paid/cancelled badges
  - PaymentRequestList: dashboard table with status filter tabs + Renew/Cancel actions

Closes Smartdevs17#460
@vercel

vercel Bot commented Jun 29, 2026

Copy link
Copy Markdown

@rindicomfort is attempting to deploy a commit to the smartdevs17's projects Team on Vercel.

A member of the Team first needs to authorize it.

@drips-wave

drips-wave Bot commented Jun 29, 2026

Copy link
Copy Markdown

@rindicomfort 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! 🚀

Learn more about application limits

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Core: Implement Payment Request Expiration with Smart Contract Enforcement

1 participant