Class: X402::BSV::BRC121Gateway
- Inherits:
-
Object
- Object
- X402::BSV::BRC121Gateway
- Defined in:
- lib/x402/bsv/brc121_gateway.rb
Overview
BRC-121 ("Simple 402 Payments") gateway for BSV settlement-gated HTTP.
BRC-121 is the BSV Association's simple HTTP payment protocol. The server is stateless: replay protection comes from a 30-second timestamp freshness window (§5 step 2) plus the wallet's handling of duplicate incoming transactions.
Unlike BRC-105, BRC-121 does not use server-generated derivation prefixes or a PrefixStore. The client generates the derivation prefix, chooses a timestamp as the suffix, constructs a BRC-29 payment, and submits it all in one round trip with no prior handshake.
Spec: https://hub.bsvblockchain.org/brc/payments/0121
== Headers
Challenge (server → client): x-bsv-sats — required satoshi amount x-bsv-server — server's identity public key (compressed hex)
Proof (client → server): x-bsv-beef — base64 BEEF transaction (the proof header) x-bsv-sender — client's identity public key (compressed hex) x-bsv-nonce — base64 BRC-29 derivation prefix x-bsv-time — decimal Unix millisecond timestamp x-bsv-vout — decimal output index of the payment
== Replay protection
BRC-121 §5 step 2: the +x-bsv-time+ header MUST be within 30 seconds of the server's current time. This is the primary defence against capture and delayed replay.
BRC-121 §5 step 5 also specifies checking +isMerge+ on the wallet's internalization result. The current Ruby +BSV::Wallet::Client+ does not return an +isMerge+ field, so this gateway additionally uses an +X402::BSV::TxidStore+ to reject duplicate txids within the freshness window.
Implements the three-method gateway interface required by Middleware:
challenge_headers(rack_request, route) → Hash
proof_header_names → Array
Constant Summary collapse
- PROOF_HEADER =
"x-bsv-beef"- FRESHNESS_WINDOW_MS =
30_000- COMPRESSED_PUBKEY_HEX =
/\A0[23][0-9a-f]{64}\z/- BASE64_NONCE =
BRC-29 derivation prefix is base64. Cap length at 128 chars (~96 bytes decoded) — well above realistic nonce sizes.
%r{\A[A-Za-z0-9+/]{1,128}={0,2}\z}- PROTOCOL =
"wallet payment"- CLIENT_HEADERS =
%w[x-bsv-beef x-bsv-sender x-bsv-nonce x-bsv-time x-bsv-vout].freeze
Instance Method Summary collapse
-
#challenge_headers(_rack_request, route) ⇒ Hash
Issue a 402 challenge with BRC-121 headers.
-
#initialize(wallet:, txid_store: nil, arc_client: nil) ⇒ BRC121Gateway
constructor
A new instance of BRC121Gateway.
-
#proof_header_names ⇒ Array<String>
Header names that carry the proof/payment from the client.
-
#settle!(_header_name, _proof_payload, rack_request, route) ⇒ SettlementResult
Verify and internalise a BRC-121 payment.
Constructor Details
#initialize(wallet:, txid_store: nil, arc_client: nil) ⇒ BRC121Gateway
Returns a new instance of BRC121Gateway.
90 91 92 93 94 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 90 def initialize(wallet:, txid_store: nil, arc_client: nil) @wallet = wallet @txid_store = txid_store || TxidStore::Memory.new @arc_client = arc_client end |
Instance Method Details
#challenge_headers(_rack_request, route) ⇒ Hash
Issue a 402 challenge with BRC-121 headers.
101 102 103 104 105 106 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 101 def challenge_headers(_rack_request, route) { "x-bsv-sats" => route.resolve_amount_sats.to_s, "x-bsv-server" => server_identity_key } end |
#proof_header_names ⇒ Array<String>
Header names that carry the proof/payment from the client.
111 112 113 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 111 def proof_header_names [PROOF_HEADER] end |
#settle!(_header_name, _proof_payload, rack_request, route) ⇒ SettlementResult
Verify and internalise a BRC-121 payment.
125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 125 def settle!(_header_name, _proof_payload, rack_request, route) required_sats = route.resolve_amount_sats headers = extract_client_headers!(rack_request) validate_sender_identity_key!(headers["x-bsv-sender"]) validate_nonce!(headers["x-bsv-nonce"]) (headers["x-bsv-time"]) output_index = parse_output_index!(headers["x-bsv-vout"]) beef, subject_tx = parse_beef_transaction(headers["x-bsv-beef"]) paid_sats = verify_payment_output!(subject_tx, output_index, required_sats) settle_payment!(beef, subject_tx, route) # Replay guard AFTER settle_payment! so a transient ARC failure # (503, timeout, propagation lag on the Atomic BEEF status path) # doesn't consume the txid — the client can retry with the same # transaction. Mirrors BRC105Gateway's consume-after-settle ordering. check_txid_unique!(subject_tx.txid_hex) result = internalize_payment!( beef_b64: headers["x-bsv-beef"], output_index: output_index, derivation_prefix: headers["x-bsv-nonce"], derivation_suffix: Base64.strict_encode64(headers["x-bsv-time"]), sender_identity_key: headers["x-bsv-sender"] ) check_internalization_result!(result) log_settlement_success(subject_tx, paid_sats, required_sats) build_settlement_result(subject_tx, paid_sats) end |