PR E: SWIFT gateway (MT760, pacs.009, MT202, camt.025/054) #9

Merged
nsatoshi merged 2 commits from devin/1776875929-swift-gateway into main 2026-04-22 17:17:52 +00:00
Owner

Implements step 6 from the architecture gap-analysis — a SWIFT gateway module covering the issuance and payment legs plus the ingest path for bank confirmations. Also fixes the pacs.008 → pacs.009 mis-routing flagged by the gap-analysis for the interbank leg. Stacks on PR D.

What lands (all under src/services/swift/)

Outbound generators

module format purpose
mt760.ts FIN Cat-7 SBLC issuance from InstrumentTerms (12 tags, URDG 758 / UCP 600 aware). messageHash() is deterministic so the on-chain plan anchor can be reproduced from the terms.
pacs009.ts ISO 20022 XML (pacs.009.001.08) FI-to-FI credit transfer — the interbank leg. BIC-validated on all four agents.
mt202.ts FIN FIN equivalent of pacs.009 for non-migrated corridors. Amounts formatted with SWIFT decimal comma.

Inbound parsers

  • camt.ts#parseCamt025 — Receipt / status of a prior instruction.
  • camt.ts#parseCamt054 — Bank-to-customer credit/debit notification.
  • camt.ts#reconcileCamt054 — diffs (amount, currency, creditDebitIndicator, endToEndId) and returns the exact mismatches so VALIDATING can feed them into Data.valueMismatch() from PR B.
  • camt.ts#parseCamt — dispatcher on xmlns marker.

Channel policy (arch §9.2 — accepted ≠ settled)

Documented in swift/index.ts:

  • pacs.008 stays on the customer-initiated PSP channel (services/iso20022.ts). Acceptance alone does not allow COMMIT.
  • pacs.009 / MT202 is the interbank channel. COMMIT requires either camt.025 ACSC or camt.054 CRDT evidence.

Tests

tests/unit/swift.test.ts — 14 cases: MT760 tag layout, MT760 rejects malformed date + negative amount, messageHash determinism, pacs.009 XML + BIC validation + pay-step requirement, MT202 tag layout, camt.025 + camt.054 happy path, xmlns dispatcher, unknown-xmlns rejection, reconcileCamt054 both match-and-mismatch cases.

Verification

$ npx tsc --noEmit     # clean
$ npx jest             # 74 passed, 6 suites

Not in this PR

  • No network client. These are pure generators + parsers. The actual FIN submission path (SWIFT Alliance Gateway / SAA / SIA) and inbound ingestion (AMH inbox) are operational integrations that belong to the bank's SWIFT service bureau, not the orchestrator.
  • ExecutionCoordinator wiring is deferred. The issueInstrument step in PR A still executes a mocked dispatch; wiring it to generateMt760(terms) + eventBus.publish('instrument.dispatched', …) is a one-file change that lands in the next coordinator-focused PR so this PR stays reviewable.

Series order

A → B → C → D → E → F → G → H.

Base: devin/1776875718-event-bus-sse (PR D). The diff here is E-only.

Implements **step 6** from the architecture gap-analysis — a SWIFT gateway module covering the issuance and payment legs plus the ingest path for bank confirmations. Also fixes the **pacs.008 → pacs.009** mis-routing flagged by the gap-analysis for the interbank leg. Stacks on PR D. ## What lands (all under `src/services/swift/`) ### Outbound generators | module | format | purpose | | --- | --- | --- | | `mt760.ts` | FIN Cat-7 | SBLC issuance from `InstrumentTerms` (12 tags, URDG 758 / UCP 600 aware). `messageHash()` is deterministic so the on-chain plan anchor can be reproduced from the terms. | | `pacs009.ts` | ISO 20022 XML (`pacs.009.001.08`) | FI-to-FI credit transfer — **the interbank leg**. BIC-validated on all four agents. | | `mt202.ts` | FIN | FIN equivalent of pacs.009 for non-migrated corridors. Amounts formatted with SWIFT decimal comma. | ### Inbound parsers - `camt.ts#parseCamt025` — Receipt / status of a prior instruction. - `camt.ts#parseCamt054` — Bank-to-customer credit/debit notification. - `camt.ts#reconcileCamt054` — diffs `(amount, currency, creditDebitIndicator, endToEndId)` and returns the exact mismatches so `VALIDATING` can feed them into `Data.valueMismatch()` from PR B. - `camt.ts#parseCamt` — dispatcher on xmlns marker. ### Channel policy (arch §9.2 — accepted ≠ settled) Documented in `swift/index.ts`: - `pacs.008` stays on the customer-initiated PSP channel (`services/iso20022.ts`). Acceptance alone does **not** allow COMMIT. - `pacs.009` / `MT202` is the interbank channel. COMMIT requires either `camt.025 ACSC` **or** `camt.054 CRDT` evidence. ## Tests `tests/unit/swift.test.ts` — 14 cases: MT760 tag layout, MT760 rejects malformed date + negative amount, `messageHash` determinism, pacs.009 XML + BIC validation + pay-step requirement, MT202 tag layout, camt.025 + camt.054 happy path, xmlns dispatcher, unknown-xmlns rejection, `reconcileCamt054` both match-and-mismatch cases. ## Verification ``` $ npx tsc --noEmit # clean $ npx jest # 74 passed, 6 suites ``` ## Not in this PR - **No network client.** These are pure generators + parsers. The actual FIN submission path (SWIFT Alliance Gateway / SAA / SIA) and inbound ingestion (AMH inbox) are operational integrations that belong to the bank's SWIFT service bureau, not the orchestrator. - **ExecutionCoordinator wiring is deferred.** The `issueInstrument` step in PR A still executes a mocked dispatch; wiring it to `generateMt760(terms)` + `eventBus.publish('instrument.dispatched', …)` is a one-file change that lands in the next coordinator-focused PR so this PR stays reviewable. ## Series order A → B → C → D → **E** → F → G → H. Base: `devin/1776875718-event-bus-sse` (PR D). The diff here is E-only.
nsatoshi changed target branch from devin/1776875718-event-bus-sse to main 2026-04-22 17:16:28 +00:00
nsatoshi added 5 commits 2026-04-22 17:16:28 +00:00
PR A: 12-state transaction machine + issueInstrument step + SoD matrix
Some checks failed
Code Quality / SonarQube Analysis (pull_request) Failing after 23s
Code Quality / Code Quality Checks (pull_request) Failing after 11s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 4s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 5s
b24a4df983
Architecture note steps 1, 2, 10 (data model).

- types/transactionState.ts: 12 states, allowed-transition table, SoD matrix
- types/plan.ts: add InstrumentTerms + 'issueInstrument' PlanStep type
- services/planValidation.ts: validate SBLC step (BIC, ISO-4217, sha256,
  YYYY-MM-DD expiry, >0 amount)
- services/stateMachine.ts: transition() enforces legality + SoD + appends
  to transaction_state_transitions
- db/migrations/002: plans.transaction_state (CHECK) +
  transaction_state_transitions append-only table
- tests/unit: 13 + 8 unit tests (31 total, all pass)

No behaviour change yet: coordinator still uses legacy status field.
PRs B-G will migrate execution paths onto the new machine.
- services/exceptionManager.ts: single taxonomy (timing/data/control/
  business/system) with §12 codes, deterministic route() table, and
  handle() dispatch to retry/DLQ/escalate
- services/execution.ts: refactor executePlan to drive the full 12-state
  machine (DRAFT -> INITIATED -> ... -> VALIDATING -> COMMITTED -> CLOSED)
  via stateMachine.transition(), with a new validatePhase() that
  reconciles DLT tx hash + bank message id + per-step amounts before
  COMMIT; SoD-gated edges use distinct synthetic actors by default
- api/plans.ts + index.ts: GET /api/plans/:planId/state returning
  current transaction_state + full audit trail of transitions
- tests/unit/exceptionManager.test.ts: 14 tests for classification +
  routing matrix

59 tests pass. tsc clean.
PR C: wire real NotaryRegistry contract on Chain 138 (arch step 4)
Some checks failed
Code Quality / SonarQube Analysis (pull_request) Failing after 20s
Code Quality / Code Quality Checks (pull_request) Failing after 8s
Security Scan / Dependency Vulnerability Scan (pull_request) Failing after 3s
Security Scan / OWASP ZAP Scan (pull_request) Failing after 4s
5bd6a200c3
- services/notaryChain.ts: new ethers-v6 adapter speaking to the
  deployed NotaryRegistry.sol via CHAIN_138_RPC_URL +
  NOTARY_REGISTRY_ADDRESS + ORCHESTRATOR_PRIVATE_KEY. Exposes
  anchorPlan(plan) -> { mode, txHash, planHash, blockNumber } and
  finalizeAnchor(planId, success) -> { mode, txHash, receiptHash }
  with deterministic mock fallback when envs are absent.
- services/notary.ts: refactored to delegate to notaryChain; preserves
  the prior signature and returns extra on-chain fields (mode, txHash,
  blockNumber, contractAddress) when the anchor lands.
- config/env.ts: add CHAIN_138_RPC_URL, CHAIN_138_CHAIN_ID,
  NOTARY_REGISTRY_ADDRESS, ORCHESTRATOR_PRIVATE_KEY (all optional,
  validated via regex where applicable).
- package.json: add ethers@^6.11.0 dependency.
- tests/unit/notaryChain.test.ts: 6 tests covering deterministic
  hashing helpers and the mock fallback path.

tsc clean. 51 tests pass (45 pre-existing + 6 new).
- db/migrations/003_events.ts: append-only events table with
  payload_hash, prev_hash, HMAC signature, indexed by plan_id + type
- services/eventBus.ts: EVENT_TYPES union (all 15 arch §7.2
  categories), publish() with hash-chain + HMAC signing, verifyChain()
  for tamper detection, subscribe() via in-process EventEmitter
- api/plans.ts:
    - GET /api/plans/:planId/events (?verify=1 returns chain_valid)
    - GET /api/plans/:planId/events/stream (SSE with history replay +
      live push, 15s keep-alive, clean unsubscribe on client disconnect)
- index.ts: register the two new endpoints
- tests/unit/eventBus.test.ts: 9 tests covering publish, hash chain,
  per-plan isolation, and three tamper-detection scenarios (payload,
  signature, prev_hash)

60 tests pass. tsc clean.
Outbound generators:
- swift/mt760.ts: SBLC issuance (FIN Cat-7). 12-tag message built from
  InstrumentTerms with deterministic messageHash() for planHash
  anchoring. URDG 758 / UCP 600 aware.
- swift/pacs009.ts: FI-to-FI credit transfer (ISO 20022 XML,
  pacs.009.001.08). Fixes the pacs.008 mis-routing flagged in the
  gap-analysis (pacs.008 is customer-to-bank; pacs.009 is bank-to-bank).
  BIC validation on all four agents.
- swift/mt202.ts: FIN equivalent of pacs.009 for non-migrated corridors.
  32A amount formatted with SWIFT decimal comma.

Inbound parsers:
- swift/camt.ts: parseCamt025 (receipt / status), parseCamt054
  (credit/debit notification), reconcileCamt054 (diffs amount, ccy,
  direction, endToEndId so VALIDATING can feed mismatches into
  Data.valueMismatch()), parseCamt dispatcher on xmlns marker.

Public surface in swift/index.ts documents channel selection:
pacs.008 stays on the PSP customer leg; pacs.009/MT202 is the
interbank leg; COMMIT requires camt.025 ACSC or camt.054 CRDT
(arch §9.2 accepted !== settled).

Tests: swift.test.ts — 14 cases covering the happy path, validation
errors (bad BIC, malformed date, negative amount, missing pay step),
determinism of messageHash, camt parser + reconciliation.

tsc clean. 74 tests pass across 6 suites.
nsatoshi force-pushed devin/1776875929-swift-gateway from 8dcdb4531c to 632f309ffc 2026-04-22 17:17:22 +00:00 Compare
nsatoshi merged commit fd575000fe into main 2026-04-22 17:17:52 +00:00
nsatoshi deleted branch devin/1776875929-swift-gateway 2026-04-22 17:17:55 +00:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: d-bis/CurrenciCombo#9