Module: Blockchain0xX402::Server

Defined in:
lib/blockchain0x_x402/server.rb

Defined Under Namespace

Classes: PricingEntry, RackMiddleware, VerifyFailure, VerifyOk

Constant Summary collapse

CAIP2_BY_NETWORK =
{
  'mainnet' => 'eip155:8453',
  'testnet' => 'eip155:84532',
}.freeze
PAYMENT_ENV_KEY =
'blockchain0x.x402_payment'

Class Method Summary collapse

Class Method Details

.build_402_body(entry:, resource:, default_network:, max_age_seconds:) ⇒ Object

Render the canonical 402 body. Used internally by the middleware; exported for alternative adapters.



96
97
98
99
100
# File 'lib/blockchain0x_x402/server.rb', line 96

def self.build_402_body(entry:, resource:, default_network:, max_age_seconds:)
  req = build_requirement(entry, default_network)
  req.max_age_seconds = max_age_seconds
  Wire::X402Response.new(version: 1, resource: resource, accepts: [req])
end

.build_requirement(entry, default_network) ⇒ Object

Build a PaymentRequirement struct from a pricing entry + network fallback. Exported so alternative adapters (Rails ActionController::API, Grape) can reuse it.



81
82
83
84
85
86
87
88
89
90
91
92
# File 'lib/blockchain0x_x402/server.rb', line 81

def self.build_requirement(entry, default_network)
  network = entry.network || default_network || 'mainnet'
  Wire::PaymentRequirement.new(
    scheme: 'exact-usdc',
    network: network,
    chain_id: CAIP2_BY_NETWORK.fetch(network),
    pay_to_address: entry.pay_to_address,
    amount_wei_usdc: usdc_decimal_to_wei(entry.amount_usdc),
    payment_request_id: entry.payment_request_id,
    max_age_seconds: nil,
  )
end

.usdc_decimal_to_wei(decimal) ⇒ Object

Convert a human decimal USDC amount (“0.10”) to a 6-decimal wei integer string (“100000”). Mirrors the equivalent helper in @blockchain0x/x402’s shared.ts.



70
71
72
73
74
75
76
# File 'lib/blockchain0x_x402/server.rb', line 70

def self.usdc_decimal_to_wei(decimal)
  whole, frac = decimal.split('.', 2)
  whole = '0' if whole.nil? || whole.empty?
  frac ||= ''
  frac = frac.ljust(6, '0')[0, 6]
  (whole.to_i * 1_000_000 + frac.to_i).to_s
end

.verify_x_payment(sdk:, header:, entry:) ⇒ Object



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
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/blockchain0x_x402/server.rb', line 132

def self.verify_x_payment(sdk:, header:, entry:)
  if header.nil? || header.empty?
    return VerifyFailure.new('header_missing', 'X-Payment header is required.')
  end

  begin
    payment = Wire.parse_payment_header(header)
  rescue WireError => e
    return VerifyFailure.new('header_malformed', e.message)
  end

  if payment.payment_request_id != entry.payment_request_id
    return VerifyFailure.new(
      'requirement_mismatch',
      "X-Payment references #{payment.payment_request_id}, " \
        "route quoted #{entry.payment_request_id}.",
    )
  end

  begin
    sdk.payment_requests_settle(
      payment_request_id: payment.payment_request_id,
      body: {
        'txHash' => payment.tx_hash,
        'payerAddress' => payment.payer_address,
        'amountUsdcVerified' => payment.amount_usdc,
      },
    )
  rescue StandardError => e
    msg = e.message
    msg = 'settle() rejected the proof.' if msg.nil? || msg.empty?
    return VerifyFailure.new('settle_rejected', msg)
  end

  VerifyOk.new(payment)
end

.x402_response_to_hash(resp) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/blockchain0x_x402/server.rb', line 102

def self.x402_response_to_hash(resp)
  {
    'version' => resp.version,
    'resource' => resp.resource,
    'accepts' => resp.accepts.map do |r|
      h = {
        'scheme' => r.scheme,
        'network' => r.network,
        'chainId' => r.chain_id,
        'payToAddress' => r.pay_to_address,
        'amountWeiUsdc' => r.amount_wei_usdc,
        'paymentRequestId' => r.payment_request_id,
      }
      h['maxAgeSeconds'] = r.max_age_seconds unless r.max_age_seconds.nil?
      h
    end,
  }
end