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::WalletClient+ 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) ⇒ 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) ⇒ BRC121Gateway
Returns a new instance of BRC121Gateway.
68 69 70 71 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 68 def initialize(wallet:, txid_store: nil) @wallet = wallet @txid_store = txid_store || TxidStore::Memory.new end |
Instance Method Details
#challenge_headers(_rack_request, route) ⇒ Hash
Issue a 402 challenge with BRC-121 headers.
78 79 80 81 82 83 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 78 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.
88 89 90 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 88 def proof_header_names [PROOF_HEADER] end |
#settle!(_header_name, _proof_payload, rack_request, route) ⇒ SettlementResult
Verify and internalise a BRC-121 payment.
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 102 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"]) subject_tx = parse_beef_transaction(headers["x-bsv-beef"]) check_txid_unique!(subject_tx.txid_hex) paid_sats = verify_payment_output!(subject_tx, output_index, required_sats) 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 |