Skip to content

[FEAT] Tranche-Based Partial Refund Countdown & Alert System #376

Description

@jotel-dev

[FEAT] Tranche-Based Partial Refund Countdown & Automated Alert System

telegram link : t.me/nullifiersystem


1. Summary & Core Promise

Velo's smart contracts support partial escrow releases via tranches in contracts/escrow/src/lib.rs and contracts/htlc-core/src/lib.rs. However, if a buyer or seller abandons a trade after only 1 of 3 tranches is released, unreleased funds remain locked in the contract until manual intervention occurs.

This feature implements a Tranche-Based Partial Refund Countdown & Automated Alert System. It builds a real-time countdown scheduler (apps/api/src/lib/timeouts.ts), an automated webhook alert pipeline (apps/api/src/lib/webhook.ts), PostgreSQL row locking (SELECT FOR UPDATE), and an automated fallback execution worker that triggers refundEscrow() for remaining unreleased tranches when ledger timeout thresholds are reached.


2. Background & Architectural Risks

  • Capital Lockup Friction: Abandoned multi-tranche trades lock user funds indefinitely if neither party executes the next tranche release.
  • Race Condition Double-Refunds: Executing automated partial refunds while a seller simultaneously submits a valid tranche secret can lead to double-payouts without atomic DB row locks.
  • Accounting Invariant Breakdown: Partial refunds must strictly maintain the core accounting invariant: sum(released_tranches) + buyer_refund + total_fees == original_locked_amount.

3. Database Layer Specifications

Migration SQL (013_add_tranche_refund_alerts.sql)

CREATE TYPE alert_notification_status AS ENUM ('PENDING', 'WARNING_SENT', 'REFUND_EXECUTED', 'CANCELLED');

CREATE TABLE tranche_refund_schedules (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    trade_id VARCHAR(64) NOT NULL UNIQUE REFERENCES cash_requests(id) ON DELETE CASCADE,
    total_tranches INT NOT NULL,
    unreleased_tranches INT NOT NULL,
    unreleased_amount BIGINT NOT NULL,
    timeout_ledger_sequence INT NOT NULL,
    status alert_notification_status NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_tranche_timeout ON tranche_refund_schedules(timeout_ledger_sequence, status);

Pessimistic Data Locking (SELECT FOR UPDATE)

BEGIN;

-- Lock target trade schedule to prevent race conditions during partial refund execution
SELECT trade_id, unreleased_tranches, unreleased_amount, status 
FROM tranche_refund_schedules 
WHERE trade_id = $1 
FOR UPDATE;

-- Update status to REFUND_EXECUTED
UPDATE tranche_refund_schedules SET status = 'REFUND_EXECUTED' WHERE trade_id = $1;

COMMIT;

4. Backend Route & Service Layer Specifications

Route: POST /api/v1/cash/tranche-refund/trigger

  1. Validation: Zod validates tradeId.
  2. Locking: DB transaction acquires SELECT FOR UPDATE on tranche_refund_schedules.
  3. Ledger Sequence Evaluation: Verifies current Stellar ledger sequence exceeds timeout_ledger_sequence.
  4. Synchronous DB Commit: Updates tranche_refund_schedules.status = 'REFUND_EXECUTED', updates cash_requests.status = 'REFUNDED', and commits.
  5. Async Relayer Offload: Enqueues submitRefundTx() task to Redis Stream worker.
  6. Response: Returns HTTP 200 OK with refunded tranche summary.

Request Schema (Zod)

export const TriggerTrancheRefundSchema = z.object({
  tradeId: z.string().length(64, "Trade ID must be a 64-character hex string"),
});

Exact Error Shapes

  • HTTP 400 Bad Request (Timeout not yet reached):
    {
      "error": {
        "code": "TIMEOUT_NOT_REACHED",
        "message": "Current Stellar ledger height has not reached the expiration threshold.",
        "requestId": "req-trf-101"
      }
    }
  • HTTP 409 Conflict (Already refunded or released):
    {
      "error": {
        "code": "TRANCHE_ALREADY_SETTLED",
        "message": "Tranche trade has already been fully released or refunded.",
        "requestId": "req-trf-102"
      }
    }

5. Background Processors / Workers

Redis Tranche Refund Worker (apps/api/src/lib/workers/trancheRefundWorker.ts)

  • Queue: Redis Stream velo:tranche-refund-queue (tranche-refund-group).
  • Polling Interval: Checks expiring ledgers every 5,000ms.
  • Webhook Alert Dispatch: Sends push alert via sendRefundAlert() 100 ledgers (~8.3 minutes) before timeout expiry.
  • Retries & DLQ: Retries failed refundEscrow() submissions up to 5 times before routing to velo:tranche-refund-dlq.

6. Frontend / UI Component Specifications

Component: mobile/frontend/src/components/TrancheCountdownBanner.tsx

  • Countdown Display: Renders real-time ledger countdown bar "100 Ledgers Remaining (~8 mins) Until Partial Refund".
  • Progress Fill: Displays animated progress bar showing released vs unreleased tranche fractions.
  • Warning State: Flashes yellow alert icon when less than 50 ledgers remain.
  • Refund Executed State: Displays banner "Unreleased Tranches Refunded to Buyer".

7. Rigor & Test Plan

  1. Unit Tests (apps/api/src/routes/__tests__/tranche-refund.test.ts):
    • Create 3-tranche trade, release 1 tranche, trigger refund after expiration. Assert refundEscrow() calculates unreleased sum correctly.
    • Assert accounting invariant holds: seller_payout + buyer_refund + fees == original_amount.
  2. Concurrency Test (tests/concurrency/tranche_refund_stress.test.ts):
    • Concurrently call releaseTrancheEscrow() and triggerTrancheRefund() on the same trade (Promise.all()).
    • Expectation: Exactly 1 operation succeeds; no double-payout occurs.
  3. Frontend Test (TrancheCountdownBanner.test.tsx):
    • Verify countdown timer formatting and state transitions.

8. Relevant Files Inventory

New Files to Create

  • apps/api/src/db/migrations/013_add_tranche_refund_alerts.sql
  • apps/api/src/routes/tranche-refund.ts
  • apps/api/src/lib/workers/trancheRefundWorker.ts
  • apps/api/src/routes/__tests__/tranche-refund.test.ts
  • tests/concurrency/tranche_refund_stress.test.ts
  • mobile/frontend/src/components/TrancheCountdownBanner.tsx

Existing Files to Modify

  • contracts/escrow/src/lib.rs
  • contracts/htlc-core/src/lib.rs
  • apps/api/src/app.ts
  • apps/api/src/lib/timeouts.ts
  • apps/api/src/lib/webhook.ts
  • mobile/frontend/src/pages/ClaimQR.tsx

9. Acceptance Criteria

  • DB migration creates tranche_refund_schedules table.
  • Webhook alert sends notification 100 ledgers prior to timeout expiry.
  • Automated worker executes refundEscrow() for unreleased tranches upon timeout.
  • Concurrent release and refund calls resolved atomically via SELECT FOR UPDATE.
  • UI component displays real-time countdown timer and refund feedback.

10. Contributor / Architectural Notes

  • ⚠️ Order: Apply SQL migration -> Update escrow smart contract -> Deploy backend worker & routes -> Update frontend.
  • ⚠️ Accounting Rule: NEVER refund already released tranches; always verify unreleased_tranches sum before executing on-chain refund.

Metadata

Metadata

Assignees

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions