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.
78 79 80 81 82 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 78 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.
89 90 91 92 93 94 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 89 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.
99 100 101 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 99 def proof_header_names [PROOF_HEADER] end |
#settle!(_header_name, _proof_payload, rack_request, route) ⇒ SettlementResult
Verify and internalise a BRC-121 payment.
113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 |
# File 'lib/x402/bsv/brc121_gateway.rb', line 113 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"]) paid_sats = verify_payment_output!(subject_tx, output_index, required_sats) # NO PAY -> NO CONTENT: verify the tx is known to ARC before # mutating wallet state. BroadcastError = not on-chain = 402. verify_on_chain!(subject_tx.txid_hex) # Replay guard AFTER verify_on_chain! so a transient ARC failure # doesn't consume the txid — the client can retry. 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 |