open-standards/openpay/crates/op-bin/src/network.rs
Transaction Science 5ae7e4ef86 ingest wave E: 3 crates · op-bin + smart-byte-tendermint + eoc-safety
OpenPay (1 crate, 1801 LOC, 57 tests)
  op-bin              BIN/IIN database + range matching + card-network
                      classification. 8-digit normalized prefix Bin +
                      BinRange. Sorted-vec interval tree with O(log N) point
                      lookup + overlap rejection. CardNetwork classify()
                      with structural rules (Visa 4, MC 51-55/2221-2720,
                      Amex 34/37, Discover, JCB, Diners, UnionPay, RuPay,
                      Mir, Troy, Maestro, Elo). Static prefix tables +
                      build_default_tree(). ISO/IEC 7812-1 Annex B Luhn
                      (validate / is_valid / check_digit). Card-type +
                      issuer-country + Durbin regulated-debit. PCI-safe:
                      8-digit cap, no full-PAN APIs.

Smart Byte (1 crate, ~1500 LOC, 20 tests)
  smart-byte-tendermint  BFT consensus. Step enum + Round + propose/prevote/
                         precommit/commit FSM with voting-power tallies.
                         ValidatorSet with 2/3+1 quorum, 1/3+1 trust
                         threshold, round-robin proposer. Block / Header /
                         Commit / CommitSig with Blake3 hashing. Light client
                         (sequential + skipping). ABCI Application trait +
                         EchoApp. DuplicateVoteEvidence + LightClientAttackEvidence.
                         IBC ClientState + ConsensusState + light-client
                         update.

EOC (1 crate, 2084 LOC, 23 tests)
  eoc-safety          AI safety primitives. RegexSet over 16 prompt-injection
                      signatures. 13-family jailbreak detector (DAN/DUDE/
                      Grandma/STAN/AIM/dev-mode/etc.). Presidio-style PII
                      redactor with Luhn-validated CC. Toxicity / bias /
                      NSFW classifier traits + heuristic baselines (5-axis
                      bias, 4-category NSFW with hard-reject for minors).
                      Constitutional AI critique→revise loop. Composable
                      InputGuard / OutputGuard pipelines. JSON-Schema-subset
                      output validator. Token-bucket rate limiter +
                      abuse-window detection. Red-team replay harness.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
2026-05-24 16:52:07 -04:00

251 lines
7.1 KiB
Rust

//! Card-network enum + structural-prefix classifier.
//!
//! [`classify`] applies the public prefix rules drawn from
//! ISO/IEC 7812 IIN assignments and each network's developer
//! documentation. It does **not** consult a commercial BIN feed;
//! it returns the network owning the prefix range. For deeper
//! attributes (card type, country, Durbin), use [`RangeTree`]
//! built from [`crate::network_ranges`].
//!
//! [`RangeTree`]: crate::range_tree::RangeTree
use serde::{Deserialize, Serialize};
use crate::bin::Bin;
/// Card networks supported by `op-bin`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CardNetwork {
/// Visa.
Visa,
/// Mastercard.
Mastercard,
/// American Express.
Amex,
/// Discover.
Discover,
/// JCB (Japan Credit Bureau).
Jcb,
/// Diners Club International.
DinersClub,
/// China UnionPay.
UnionPay,
/// RuPay (India).
RuPay,
/// Maestro (Mastercard's international debit brand).
Maestro,
/// Elo (Brazil).
Elo,
/// Mir (Russia).
Mir,
/// Troy (Türkiye).
Troy,
/// Unknown / unclassified.
Unknown,
}
impl CardNetwork {
/// Lowercase wire string (`"visa"`, `"mastercard"`, …).
pub const fn as_str(&self) -> &'static str {
match self {
Self::Visa => "visa",
Self::Mastercard => "mastercard",
Self::Amex => "amex",
Self::Discover => "discover",
Self::Jcb => "jcb",
Self::DinersClub => "diners-club",
Self::UnionPay => "unionpay",
Self::RuPay => "rupay",
Self::Maestro => "maestro",
Self::Elo => "elo",
Self::Mir => "mir",
Self::Troy => "troy",
Self::Unknown => "unknown",
}
}
}
/// Classify a BIN to its card network using structural prefix
/// rules. This is the **fast path** — no table lookup, just
/// integer prefix arithmetic — and is intentionally
/// conservative: any BIN that doesn't match a documented prefix
/// falls through to [`CardNetwork::Unknown`].
///
/// Full annotation (card type, country, Durbin status) requires
/// a [`RangeTree`](crate::range_tree::RangeTree) built from
/// [`crate::network_ranges`].
pub fn classify(bin: &Bin) -> CardNetwork {
// Work in the 8-digit prefix domain so all rules compose.
let p = bin.prefix_8();
// Read the leading `n` digits of `p`.
let lead2 = p / 1_000_000; // first 2 digits
let lead4 = p / 10_000; // first 4 digits
let lead6 = p / 100; // first 6 digits
let lead3 = p / 100_000; // first 3 digits
let lead1 = p / 10_000_000; // first digit
// --- Visa: leading 4 ---
if lead1 == 4 {
return CardNetwork::Visa;
}
// --- Amex: 34 or 37 ---
if lead2 == 34 || lead2 == 37 {
return CardNetwork::Amex;
}
// --- Mastercard:
// classic 51-55, plus the 2017 expansion 2221-2720.
if (51..=55).contains(&lead2) {
return CardNetwork::Mastercard;
}
if (2221..=2720).contains(&lead4) {
return CardNetwork::Mastercard;
}
// --- Discover:
// 6011, 622126-622925, 644-649, 65.
if lead4 == 6011 {
return CardNetwork::Discover;
}
if (622_126..=622_925).contains(&lead6) {
return CardNetwork::Discover;
}
if (644..=649).contains(&lead3) {
return CardNetwork::Discover;
}
if lead2 == 65 {
return CardNetwork::Discover;
}
// --- JCB: 3528-3589 ---
if (3528..=3589).contains(&lead4) {
return CardNetwork::Jcb;
}
// --- Diners Club:
// 300-305, 309, 36, 38-39. Note: Discover acquired the
// Diners US/Canada portfolio in 2008; we still report
// the BIN as DinersClub since the network identifier on
// the card surface is unchanged.
if (300..=305).contains(&lead3) || lead3 == 309 {
return CardNetwork::DinersClub;
}
if lead2 == 36 || lead2 == 38 || lead2 == 39 {
return CardNetwork::DinersClub;
}
// --- UnionPay: 62 ---
if lead2 == 62 {
return CardNetwork::UnionPay;
}
// --- RuPay: 60 (excluding 6011 = Discover), 65 (already
// Discover above), 81, 82.
// The 65 range is shared at the prefix level; downstream
// range-tree lookup distinguishes per sub-range.
if lead2 == 60 && lead4 != 6011 {
return CardNetwork::RuPay;
}
if lead2 == 81 || lead2 == 82 {
return CardNetwork::RuPay;
}
// --- Mir (Russia): 2200-2204 ---
if (2200..=2204).contains(&lead4) {
return CardNetwork::Mir;
}
// --- Troy (Türkiye): 9792 ---
if lead4 == 9792 {
return CardNetwork::Troy;
}
// --- Maestro: 5018, 5020, 5038, 5612, 5893, 6304, 6759,
// 6761, 6762, 6763. (Partial — Maestro publishes the
// classification by sub-range; downstream RangeTree
// covers exhaustively.)
if matches!(
lead4,
5018 | 5020 | 5038 | 5612 | 5893 | 6304 | 6759 | 6761 | 6762 | 6763
) {
return CardNetwork::Maestro;
}
// --- Elo (Brazil): selected 4-digit prefixes (partial). ---
if matches!(
lead4,
4011 | 4312 | 4389 | 4514 | 5041 | 5066 | 5067 | 5090 | 6277 | 6362 | 6363 | 6504 | 6505
) {
return CardNetwork::Elo;
}
CardNetwork::Unknown
}
#[cfg(test)]
mod tests {
use super::*;
fn bin(s: &str) -> Bin {
Bin::parse(s).expect("valid BIN literal")
}
#[test]
fn visa() {
assert_eq!(classify(&bin("411111")), CardNetwork::Visa);
assert_eq!(classify(&bin("400000")), CardNetwork::Visa);
assert_eq!(classify(&bin("499999")), CardNetwork::Visa);
}
#[test]
fn mastercard_classic() {
assert_eq!(classify(&bin("510000")), CardNetwork::Mastercard);
assert_eq!(classify(&bin("559999")), CardNetwork::Mastercard);
}
#[test]
fn mastercard_2_series() {
assert_eq!(classify(&bin("222100")), CardNetwork::Mastercard);
assert_eq!(classify(&bin("272099")), CardNetwork::Mastercard);
// Boundary: 2220 is NOT Mastercard.
assert_ne!(classify(&bin("222000")), CardNetwork::Mastercard);
// Boundary: 2721 is NOT Mastercard.
assert_ne!(classify(&bin("272100")), CardNetwork::Mastercard);
}
#[test]
fn amex() {
assert_eq!(classify(&bin("340000")), CardNetwork::Amex);
assert_eq!(classify(&bin("370000")), CardNetwork::Amex);
}
#[test]
fn discover_6011() {
assert_eq!(classify(&bin("601100")), CardNetwork::Discover);
}
#[test]
fn jcb() {
assert_eq!(classify(&bin("352800")), CardNetwork::Jcb);
assert_eq!(classify(&bin("358900")), CardNetwork::Jcb);
}
#[test]
fn unionpay() {
assert_eq!(classify(&bin("621234")), CardNetwork::UnionPay);
}
#[test]
fn diners_36() {
assert_eq!(classify(&bin("360000")), CardNetwork::DinersClub);
}
#[test]
fn unknown_falls_through() {
// 7xxxxx is unassigned in the BIN space.
assert_eq!(classify(&bin("700000")), CardNetwork::Unknown);
}
}