Module: Blockchain0xX402::Wire

Defined in:
lib/blockchain0x_x402/wire.rb

Defined Under Namespace

Classes: ExactUsdcPayment, PaymentRequirement, X402Response

Constant Summary collapse

RESPONSE_NOT_402 =

Failure-code constants mirror the Node + Python + Go ports.

'response.not_402'
RESPONSE_BODY_MISSING =
'response.body_missing'
RESPONSE_BODY_MALFORMED =
'response.body_malformed'
HEADER_MISSING =
'header.missing'
HEADER_MALFORMED =
'header.malformed'
HEADER_UNKNOWN_SCHEME =
'header.unknown_scheme'
HEADER_PAYLOAD_MALFORMED =
'header.payload_malformed'
VALID_NETWORKS =
%w[mainnet testnet].freeze
RE_TX_HASH =
/\A0x[0-9a-fA-F]{64}\z/
RE_PAYER =
/\A0x[0-9a-fA-F]{40}\z/
RE_AMOUNT =
/\A[0-9]+(?:\.[0-9]+)?\z/
RE_WEI =
/\A[0-9]+\z/

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Attribute Details

#amount_wei_usdc @return [String] integer text(@return[String]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

#chain_id @return [String] CAIP-2 chain id(@return[String]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

#max_age_seconds @return [Integer, nil](@return[Integer, nil]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

#network @return [String] 'mainnet' | 'testnet'(@return[String]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

#pay_to_address @return [String](@return[String]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

#payment_request_id @return [String](@return[String]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

#scheme @return [String] 'exact-usdc'(@return[String]) ⇒ Object (readonly)



56
57
58
59
60
61
62
63
64
65
# File 'lib/blockchain0x_x402/wire.rb', line 56

PaymentRequirement = Struct.new(
  :scheme,
  :network,
  :chain_id,
  :pay_to_address,
  :amount_wei_usdc,
  :payment_request_id,
  :max_age_seconds,
  keyword_init: true,
)

Class Method Details

.build_payment_header(payment) ⇒ Object

Encode a payment payload as an X-Payment header value. Accepts either an ExactUsdcPayment struct OR a Hash with the same keys (snake_case OR camelCase) for caller convenience.

Raises:



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/blockchain0x_x402/wire.rb', line 138

def build_payment_header(payment)
  scheme, version, payment_request_id, tx_hash, payer_address, amount_usdc, network =
    coerce_payment(payment)

  if scheme != 'exact-usdc'
    raise WireError.new(HEADER_UNKNOWN_SCHEME, "build_payment_header: unsupported scheme #{scheme.inspect}.")
  end

  # Pin the JSON key order explicitly. Ruby >= 1.9 Hashes are
  # insertion-ordered but writing the JSON manually makes the
  # wire contract obvious from this file alone.
  json = String.new
  json << '{"scheme":"exact-usdc","version":' << version.to_s
  json << ',"paymentRequestId":' << JSON.generate(payment_request_id)
  json << ',"txHash":' << JSON.generate(tx_hash.downcase)
  json << ',"payerAddress":' << JSON.generate(payer_address.downcase)
  json << ',"amountUsdc":' << JSON.generate(amount_usdc)
  json << ',"network":' << JSON.generate(network)
  json << '}'

  "exact-usdc:#{Base64.strict_encode64(json)}"
end

.parse_402_body(body) ⇒ Object



101
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
128
129
130
# File 'lib/blockchain0x_x402/wire.rb', line 101

def parse_402_body(body)
  unless body.is_a?(Hash)
    raise WireError.new(RESPONSE_BODY_MISSING, '402 response body is missing or non-object.')
  end
  unless body['version'] == 1
    raise WireError.new(RESPONSE_BODY_MALFORMED, "Unsupported x402 version: #{body['version'].inspect}.")
  end
  unless body['resource'].is_a?(String) && !body['resource'].empty?
    raise WireError.new(RESPONSE_BODY_MALFORMED, '402 body missing `resource` string.')
  end
  accepts_raw = body['accepts']
  unless accepts_raw.is_a?(Array) && !accepts_raw.empty?
    raise WireError.new(RESPONSE_BODY_MALFORMED, '402 body missing `accepts` array or empty.')
  end
  accepts = accepts_raw.map do |entry|
    unless valid_requirement?(entry)
      raise WireError.new(RESPONSE_BODY_MALFORMED, '402 `accepts` entry is not a recognised payment requirement.')
    end
    PaymentRequirement.new(
      scheme: 'exact-usdc',
      network: entry['network'],
      chain_id: entry['chainId'],
      pay_to_address: entry['payToAddress'],
      amount_wei_usdc: entry['amountWeiUsdc'],
      payment_request_id: entry['paymentRequestId'],
      max_age_seconds: entry['maxAgeSeconds'].is_a?(Numeric) ? entry['maxAgeSeconds'].to_i : nil,
    )
  end
  X402Response.new(version: 1, resource: body['resource'], accepts: accepts.freeze)
end

.parse_402_response(response) ⇒ X402Response

Parameters:

  • response (#status, #body, ...)

    any HTTP response that exposes a numeric status and a JSON-parseable body. Faraday responses, Net::HTTPResponse via Net::HTTP, and Webmock stubs all work; for raw hashes use parse_402_body instead.

Returns:

Raises:



88
89
90
91
92
93
94
95
96
97
98
99
# File 'lib/blockchain0x_x402/wire.rb', line 88

def parse_402_response(response)
  status = response.respond_to?(:status) ? response.status : response.code.to_i
  if status != 402
    raise WireError.new(RESPONSE_NOT_402, "Expected status 402, got #{status}.")
  end

  body = response.body
  body = JSON.parse(body) if body.is_a?(String)
  parse_402_body(body)
rescue JSON::ParserError
  raise WireError.new(RESPONSE_BODY_MALFORMED, '402 response body is not JSON.')
end

.parse_payment_header(value) ⇒ Object



161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
# File 'lib/blockchain0x_x402/wire.rb', line 161

def parse_payment_header(value)
  if !value.is_a?(String) || value.empty?
    raise WireError.new(HEADER_MISSING, 'X-Payment header is missing or empty.')
  end
  sep = value.index(':')
  if sep.nil? || sep < 1 || sep == value.length - 1
    raise WireError.new(HEADER_MALFORMED, 'X-Payment header must be `<scheme>:<base64-payload>`.')
  end
  scheme = value[0...sep]
  if scheme != 'exact-usdc'
    raise WireError.new(HEADER_UNKNOWN_SCHEME, "Unsupported X-Payment scheme: #{scheme}.")
  end
  b64 = value[(sep + 1)..]
  text = begin
    Base64.strict_decode64(b64)
  rescue ArgumentError
    raise WireError.new(HEADER_PAYLOAD_MALFORMED, 'X-Payment payload is not valid base64.')
  end
  parsed = begin
    JSON.parse(text)
  rescue JSON::ParserError
    raise WireError.new(HEADER_PAYLOAD_MALFORMED, 'X-Payment payload is not valid JSON.')
  end
  unless parsed.is_a?(Hash)
    raise WireError.new(HEADER_PAYLOAD_MALFORMED, 'X-Payment payload is not an object.')
  end
  unless valid_payload?(parsed)
    raise WireError.new(HEADER_PAYLOAD_MALFORMED, 'X-Payment payload failed shape validation.')
  end

  ExactUsdcPayment.new(
    scheme: 'exact-usdc',
    version: 1,
    payment_request_id: parsed['paymentRequestId'],
    tx_hash: parsed['txHash'].downcase,
    payer_address: parsed['payerAddress'].downcase,
    amount_usdc: parsed['amountUsdc'],
    network: parsed['network'],
  )
end