[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
- Validation: Zod validates
tradeId.
- Locking: DB transaction acquires
SELECT FOR UPDATE on tranche_refund_schedules.
- Ledger Sequence Evaluation: Verifies current Stellar ledger sequence exceeds
timeout_ledger_sequence.
- Synchronous DB Commit: Updates
tranche_refund_schedules.status = 'REFUND_EXECUTED', updates cash_requests.status = 'REFUNDED', and commits.
- Async Relayer Offload: Enqueues
submitRefundTx() task to Redis Stream worker.
- 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
- 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.
- 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.
- 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
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.
[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.rsandcontracts/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 triggersrefundEscrow()for remaining unreleased tranches when ledger timeout thresholds are reached.2. Background & Architectural Risks
sum(released_tranches) + buyer_refund + total_fees == original_locked_amount.3. Database Layer Specifications
Migration SQL (
013_add_tranche_refund_alerts.sql)Pessimistic Data Locking (
SELECT FOR UPDATE)4. Backend Route & Service Layer Specifications
Route:
POST /api/v1/cash/tranche-refund/triggertradeId.SELECT FOR UPDATEontranche_refund_schedules.timeout_ledger_sequence.tranche_refund_schedules.status = 'REFUND_EXECUTED', updatescash_requests.status = 'REFUNDED', and commits.submitRefundTx()task to Redis Stream worker.HTTP 200 OKwith refunded tranche summary.Request Schema (Zod)
Exact Error Shapes
{ "error": { "code": "TIMEOUT_NOT_REACHED", "message": "Current Stellar ledger height has not reached the expiration threshold.", "requestId": "req-trf-101" } }{ "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)velo:tranche-refund-queue(tranche-refund-group).sendRefundAlert()100 ledgers (~8.3 minutes) before timeout expiry.refundEscrow()submissions up to 5 times before routing tovelo:tranche-refund-dlq.6. Frontend / UI Component Specifications
Component:
mobile/frontend/src/components/TrancheCountdownBanner.tsx"100 Ledgers Remaining (~8 mins) Until Partial Refund"."Unreleased Tranches Refunded to Buyer".7. Rigor & Test Plan
apps/api/src/routes/__tests__/tranche-refund.test.ts):refundEscrow()calculates unreleased sum correctly.seller_payout + buyer_refund + fees == original_amount.tests/concurrency/tranche_refund_stress.test.ts):releaseTrancheEscrow()andtriggerTrancheRefund()on the same trade (Promise.all()).TrancheCountdownBanner.test.tsx):8. Relevant Files Inventory
New Files to Create
apps/api/src/db/migrations/013_add_tranche_refund_alerts.sqlapps/api/src/routes/tranche-refund.tsapps/api/src/lib/workers/trancheRefundWorker.tsapps/api/src/routes/__tests__/tranche-refund.test.tstests/concurrency/tranche_refund_stress.test.tsmobile/frontend/src/components/TrancheCountdownBanner.tsxExisting Files to Modify
contracts/escrow/src/lib.rscontracts/htlc-core/src/lib.rsapps/api/src/app.tsapps/api/src/lib/timeouts.tsapps/api/src/lib/webhook.tsmobile/frontend/src/pages/ClaimQR.tsx9. Acceptance Criteria
tranche_refund_schedulestable.refundEscrow()for unreleased tranches upon timeout.SELECT FOR UPDATE.10. Contributor / Architectural Notes
escrowsmart contract -> Deploy backend worker & routes -> Update frontend.unreleased_tranchessum before executing on-chain refund.