OpenPay (1 crate, 2174 LOC, 14 tests)
op-payouts Multi-rail payout engine. Visa Direct, Mastercard Send,
ACH credits (NACHA), Fedwire + SWIFT MT103 + ISO 20022
pacs.008, SEPA SCT + SCT Inst (mod-97 IBAN), UK FPS,
FedNow, RTP, PayPal, Wise. Crypto: USDC/USDT on
ETH/Polygon/Base/Solana + BTC + Lightning with real
ERC-20 transfer(address,uint256) calldata. Offline-pure
drivers (emit wire payloads — operators transmit).
Smart Byte (1 crate, 2605 LOC, 38 tests)
smart-byte-nostr Nostr adapter. NIP-01 canonical event id + BIP-340
Schnorr sign/verify; from-scratch bech32 + NIP-19 TLV
(npub/nsec/note/nprofile/nevent/nrelay); NIP-04 legacy
AES-256-CBC DMs; NIP-44 v2 HKDF-SHA256→ChaCha20+HMAC
with bucketed padding; NIP-17 gift-wrap (kind 1059) +
seal (kind 13) + rumor (kind 14); NIP-05/11/13/26/65.
Relay client message vocabulary (REQ/EVENT/CLOSE/AUTH/
EOSE/OK/NOTICE/CLOSED) + in-memory router.
EOC (1 crate, 2024 LOC, 30 tests)
eoc-streaming Token-by-token + tool-call streaming primitives.
SSE framing (data: lines); Anthropic event mapper
(message_start/content_block_*/message_delta/stop/ping);
OpenAI delta mapper; Vercel AI SDK mapper. Bounded-channel
backpressure with watermarks; CancelToken propagation;
Last-Event-ID resumable streams; per-stream joule meter.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
106 lines
4 KiB
Rust
106 lines
4 KiB
Rust
//! FedNow payout driver.
|
|
//!
|
|
//! FedNow accepts ISO 20022 `pacs.008.001.08` over FedLine. Per-payment
|
|
//! cap is USD 1,000,000 as of June 2025. This driver is offline-pure;
|
|
//! the operator's FedLine gateway transmits the XML.
|
|
|
|
use uuid::Uuid;
|
|
|
|
use crate::error::{Error, Result};
|
|
use crate::payout::{
|
|
BeneficiaryAccount, Payout, PayoutRequest, PayoutResult, PayoutStatus,
|
|
};
|
|
use crate::visa_direct::format_amount;
|
|
|
|
/// FedNow per-payment cap as of June 2025 (USD).
|
|
pub const FEDNOW_MAX_AMOUNT_USD: i64 = 1_000_000 * 100;
|
|
|
|
/// FedNow driver.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct FedNowDriver {
|
|
/// Sender's 9-digit ABA RTN (must be a FedNow participant).
|
|
pub sender_aba: String,
|
|
/// Sender's account number.
|
|
pub sender_account: String,
|
|
/// Sender's name (FedNow caps at 140 chars).
|
|
pub sender_name: String,
|
|
}
|
|
|
|
impl Payout for FedNowDriver {
|
|
fn rail(&self) -> &'static str {
|
|
"fednow"
|
|
}
|
|
|
|
fn submit(&self, req: &PayoutRequest) -> Result<PayoutResult> {
|
|
if req.amount.currency != op_core::Currency::USD {
|
|
return Err(Error::LimitViolation {
|
|
rail: "fednow",
|
|
detail: "FedNow is USD-only".to_string(),
|
|
});
|
|
}
|
|
if !req.amount.is_positive() {
|
|
return Err(Error::LimitViolation {
|
|
rail: "fednow",
|
|
detail: "amount must be positive".to_string(),
|
|
});
|
|
}
|
|
if req.amount.minor_units > FEDNOW_MAX_AMOUNT_USD {
|
|
return Err(Error::LimitViolation {
|
|
rail: "fednow",
|
|
detail: "FedNow per-payment cap is USD 1,000,000".to_string(),
|
|
});
|
|
}
|
|
let (aba, account) = match &req.beneficiary.account {
|
|
BeneficiaryAccount::UsBank { aba, account, .. } => (aba, account),
|
|
_ => return Err(Error::UnsupportedMethod { rail: "fednow" }),
|
|
};
|
|
if aba.len() != 9 || !aba.chars().all(|c| c.is_ascii_digit()) {
|
|
return Err(Error::InvalidBeneficiary {
|
|
rail: "fednow",
|
|
detail: "ABA must be 9 digits".to_string(),
|
|
});
|
|
}
|
|
let amt = format_amount(req.amount);
|
|
let xml = format!(
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
|
|
<Document xmlns=\"urn:iso:std:iso:20022:tech:xsd:pacs.008.001.08\">\
|
|
<FIToFICstmrCdtTrf><GrpHdr><MsgId>{msg}</MsgId><NbOfTxs>1</NbOfTxs>\
|
|
<SttlmInf><SttlmMtd>CLRG</SttlmMtd><ClrSys><Cd>FDN</Cd></ClrSys></SttlmInf></GrpHdr>\
|
|
<CdtTrfTxInf><PmtId><EndToEndId>{e2e}</EndToEndId><UETR>{uetr}</UETR></PmtId>\
|
|
<IntrBkSttlmAmt Ccy=\"USD\">{amt}</IntrBkSttlmAmt>\
|
|
<Dbtr><Nm>{dn}</Nm></Dbtr>\
|
|
<DbtrAcct><Id><Othr><Id>{da}</Id></Othr></Id></DbtrAcct>\
|
|
<DbtrAgt><FinInstnId><ClrSysMmbId><MmbId>{daba}</MmbId></ClrSysMmbId></FinInstnId></DbtrAgt>\
|
|
<CdtrAgt><FinInstnId><ClrSysMmbId><MmbId>{caba}</MmbId></ClrSysMmbId></FinInstnId></CdtrAgt>\
|
|
<Cdtr><Nm>{cn}</Nm></Cdtr>\
|
|
<CdtrAcct><Id><Othr><Id>{ca}</Id></Othr></Id></CdtrAcct>\
|
|
</CdtTrfTxInf></FIToFICstmrCdtTrf></Document>",
|
|
msg = req.idempotency_key,
|
|
e2e = req.idempotency_key,
|
|
uetr = Uuid::new_v4(),
|
|
dn = self.sender_name,
|
|
da = self.sender_account,
|
|
daba = self.sender_aba,
|
|
caba = aba,
|
|
cn = req.beneficiary.name,
|
|
ca = account,
|
|
);
|
|
Ok(PayoutResult {
|
|
idempotency_key: req.idempotency_key.clone(),
|
|
payout_id: Uuid::now_v7().to_string(),
|
|
status: PayoutStatus::PreparedOffline,
|
|
raw_status: None,
|
|
reason_code: None,
|
|
reason_text: None,
|
|
rail_txn_id: None,
|
|
settled_amount: Some(req.amount),
|
|
wire_payload: Some(xml.into_bytes()),
|
|
})
|
|
}
|
|
|
|
fn status(&self, _payout_id: &str) -> Result<PayoutResult> {
|
|
Err(Error::DriverValidation(
|
|
"FedNow status arrives as pacs.002 from the FRB FedLine gateway".to_string(),
|
|
))
|
|
}
|
|
}
|