Skip to content

Trade state machine ​

                ┌──────────────── STELLAR_TX_FAILED ────────────────┐
                ▼                                                    │
PENDING_ESCROW ──lock ok──▶ ESCROW_LOCKED ──buyer: MARK_PAID──▶ PAID │
      │                         │    │                           │   │
      │                         │    └──seller: RELEASE──┐       │   │
      │                         │                        ▼       ▼   │
      │                         │                     RELEASING ─────┘
      │                         │                        │
      │                         │                        ▼
      │                         │                    COMPLETED
      │                         │
      │               buyer: CANCEL / timeout
      │                         ▼
      │                     REFUNDING ──▶ CANCELLED
      │
      └── lock rejected / reconciled as unfunded ──▶ CANCELLED

Actions ​

Defined in server/src/services/tradeRules.js — pure functions with no I/O.

ActionRoleAllowed fromExtra rule
MARK_PAIDBuyerESCROW_LOCKEDpaymentDeadline not passed
RELEASESellerESCROW_LOCKED, PAID
CANCELBuyerESCROW_LOCKED

assertCanAct(trade, action, userId, now) throws:

SituationError
Not a participant403 FORBIDDEN
Wrong role403 FORBIDDEN
Wrong status409 CONFLICT
Window closed422 PAYMENT_WINDOW_CLOSED

availableActions(trade, userId) powers the actions array in trade responses, so the client shows only valid buttons.

Atomic transitions ​

Every transition is claimed with a conditional update:

js
const { count } = await prisma.trade.updateMany({
  where: { id, status: { in: fromStatuses } },
  data: { status: 'RELEASING' },
});
if (count === 0) throw conflict('This trade just changed. Refresh and try again.');

Two concurrent releases, or a release racing a timeout refund, can't both proceed — exactly one claim succeeds.

Failure handling ​

Error from StellarAction
STELLAR_TX_FAILED (definitive)Restore the previous status (or cancel, for a failed lock)
Anything else (unknown outcome)Leave the transitional status; reconciliation settles it

Nexlm is a peer-to-peer facilitator and never holds customer Naira.