Module: X402::Payments

Defined in:
lib/x402/payments.rb,
lib/x402/payments.rb,
lib/x402/payments/chains.rb,
lib/x402/payments/version.rb,
lib/x402/payments/networks.rb,
lib/x402/payments/generator.rb,
lib/x402/payments/v1/headers.rb,
lib/x402/payments/v2/headers.rb,
lib/x402/payments/configuration.rb,
lib/x402/payments/solana/generator.rb,
lib/x402/payments/v1/payload_builder.rb,
lib/x402/payments/v2/payload_builder.rb

Defined Under Namespace

Modules: Networks, Solana, V1, V2 Classes: Configuration, ConfigurationError, Error, Generator

Constant Summary collapse

SUPPORTED_VERSIONS =
[1, 2].freeze
CHAINS =

Chain configurations for supported networks

{
  "base-sepolia" => {
    chain_id: 84532,
    rpc_url: "https://clean-snowy-hexagon.base-sepolia.quiknode.pro",
    usdc_address: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    explorer_url: "https://sepolia.basescan.org"
  },
  "base" => {
    chain_id: 8453,
    rpc_url: "https://snowy-compatible-ensemble.base-mainnet.quiknode.pro",
    usdc_address: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    explorer_url: "https://basescan.org"
  },
  "avalanche-fuji" => {
    chain_id: 43113,
    rpc_url: "https://muddy-sly-field.avalanche-testnet.quiknode.pro/ext/bc/C/rpc",
    usdc_address: "0x5425890298aed601595a70AB815c96711a31Bc65",
    explorer_url: "https://testnet.snowtrace.io"
  },
  "avalanche" => {
    chain_id: 43114,
    rpc_url: "https://floral-patient-panorama.avalanche-mainnet.quiknode.pro/ext/bc/C/rpc",
    usdc_address: "0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E",
    explorer_url: "https://snowtrace.io"
  },
  "solana-devnet" => {
    chain_id: 103,
    rpc_url: "https://bitter-twilight-vineyard.solana-devnet.quiknode.pro",
    usdc_address: "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
    explorer_url: "https://explorer.solana.com/?cluster=devnet",
    fee_payer: "CKPKJWNdJEqa81x7CkZ14BVPiY6y16Sxs7owznqtWYp5"
  },
  "solana" => {
    chain_id: 101,
    rpc_url: "https://alien-burned-energy.solana-mainnet.quiknode.pro",
    usdc_address: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
    explorer_url: "https://explorer.solana.com",
    fee_payer: "CKPKJWNdJEqa81x7CkZ14BVPiY6y16Sxs7owznqtWYp5"
  }
}.freeze
SOLANA_CHAINS =
%w[solana solana-devnet].freeze
CURRENCY_BY_CHAIN =

Currency configurations by chain

{
  "base-sepolia" => {
    symbol: "USDC",
    decimals: 6,
    name: "USDC",
    version: "2"
  },
  "base" => {
    symbol: "USDC",
    decimals: 6,
    name: "USD Coin",
    version: "2"
  },
  "avalanche-fuji" => {
    symbol: "USDC",
    decimals: 6,
    name: "USD Coin",
    version: "2"
  },
  "avalanche" => {
    symbol: "USDC",
    decimals: 6,
    name: "USDC",
    version: "2"
  },
  "solana-devnet" => {
    symbol: "USDC",
    decimals: 6,
    name: "USDC",
    version: nil
  },
  "solana" => {
    symbol: "USDC",
    decimals: 6,
    name: "USD Coin",
    version: nil
  }
}.freeze
VERSION =
"1.0.0"

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.configurationObject



65
66
67
# File 'lib/x402/payments/configuration.rb', line 65

def configuration
  @configuration ||= Configuration.new
end

Class Method Details

.asset_address_for(chain_name, symbol = nil) ⇒ Object

Raises:



110
111
112
113
114
115
116
117
118
119
120
121
122
# File 'lib/x402/payments/chains.rb', line 110

def asset_address_for(chain_name, symbol = nil)
  symbol ||= X402::Payments.configuration.currency

  custom = X402::Payments.configuration.token_config(chain_name, symbol)
  return custom[:address] if custom

  if symbol.upcase == "USDC"
    builtin = CHAINS[chain_name]
    return builtin[:usdc_address] if builtin && builtin[:usdc_address]
  end

  raise ConfigurationError, "Unknown token #{symbol} for chain #{chain_name}. Register with config.register_token()"
end

.caip2_for(chain_name) ⇒ Object



148
149
150
# File 'lib/x402/payments/chains.rb', line 148

def caip2_for(chain_name)
  Networks.to_caip2(chain_name)
end

.chain_config(chain_name) ⇒ Object



90
91
92
93
94
95
# File 'lib/x402/payments/chains.rb', line 90

def chain_config(chain_name)
  custom = X402::Payments.configuration.chain_config(chain_name)
  return custom if custom

  CHAINS[chain_name] || raise(ConfigurationError, "Unsupported chain: #{chain_name}. Register with config.register_chain()")
end

.chain_id_for(chain_name) ⇒ Object



137
138
139
140
141
142
# File 'lib/x402/payments/chains.rb', line 137

def chain_id_for(chain_name)
  custom = X402::Payments.configuration.chain_config(chain_name)
  return custom[:chain_id] if custom

  chain_config(chain_name)[:chain_id]
end

.configure {|configuration| ... } ⇒ Object

Yields:



69
70
71
# File 'lib/x402/payments/configuration.rb', line 69

def configure
  yield(configuration)
end

.currency_config_for_chain(chain_name) ⇒ Object



97
98
99
# File 'lib/x402/payments/chains.rb', line 97

def currency_config_for_chain(chain_name)
  CURRENCY_BY_CHAIN[chain_name] || raise(ConfigurationError, "Unsupported chain for currency: #{chain_name}")
end

.currency_decimals_for_chain(chain_name) ⇒ Object



144
145
146
# File 'lib/x402/payments/chains.rb', line 144

def currency_decimals_for_chain(chain_name)
  currency_config_for_chain(chain_name)[:decimals]
end

.fee_payer_for(chain_name) ⇒ Object



173
174
175
176
177
178
179
180
181
# File 'lib/x402/payments/chains.rb', line 173

def fee_payer_for(chain_name)
  return nil unless solana_chain?(chain_name)

  # Priority: 1) ENV variable, 2) Default from CHAINS
  env_fee_payer = ENV["X402_SOLANA_FEE_PAYER"]
  return env_fee_payer if env_fee_payer && !env_fee_payer.empty?

  chain_config(chain_name)[:fee_payer]
end

.generate_header(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil, version: nil, fee_payer: nil) ⇒ Object



38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/x402/payments.rb', line 38

def generate_header(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil, version: nil, fee_payer: nil)
  normalized_version = normalize_version(version)
  chain_name = network || configuration.chain

  if solana_chain?(chain_name)
    generator = Solana::Generator.new
    generator.generate_header(
      amount: amount,
      resource: resource,
      description: description,
      network: network,
      private_key: private_key,
      pay_to: pay_to,
      version: normalized_version,
      fee_payer: fee_payer
    )
  else
    generator = Generator.new
    generator.generate_header(
      amount: amount,
      resource: resource,
      description: description,
      network: network,
      private_key: private_key,
      pay_to: pay_to,
      extra: extra,
      version: normalized_version
    )
  end
end


69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/x402/payments.rb', line 69

def generate_link(amount:, resource:, description: nil, network: nil, private_key: nil, pay_to: nil, extra: nil, version: nil, fee_payer: nil)
  normalized_version = normalize_version(version)
  chain_name = network || configuration.chain
  protocol_version = normalized_version || configuration.protocol_version
  header_name = payment_header_name(protocol_version)

  if solana_chain?(chain_name)
    generator = Solana::Generator.new
    header = generator.generate_header(
      amount: amount,
      resource: resource,
      description: description,
      network: network,
      private_key: private_key,
      pay_to: pay_to,
      version: protocol_version,
      fee_payer: fee_payer
    )
    {
      payment_header: header,
      header_name: header_name,
      curl_command: "curl -s -H \"#{header_name}: #{header}\" #{resource} | jq ."
    }
  else
    generator = Generator.new
    generator.generate_link(
      amount: amount,
      resource: resource,
      description: description,
      network: network,
      private_key: private_key,
      pay_to: pay_to,
      extra: extra,
      version: version
    )
  end
end

.normalize_version(version) ⇒ Integer?

Normalize version parameter to integer and validate

Parameters:

  • version (Integer, String, nil)

    the version to normalize

Returns:

  • (Integer, nil)

    normalized version or nil if input was nil

Raises:

  • (ArgumentError)

    if version is not a supported value



29
30
31
32
33
34
35
36
37
# File 'lib/x402/payments.rb', line 29

def normalize_version(version)
  return nil if version.nil?

  normalized = version.to_i
  unless SUPPORTED_VERSIONS.include?(normalized)
    raise ArgumentError, "Unsupported protocol version: #{version.inspect}. Supported versions: #{SUPPORTED_VERSIONS.join(', ')}"
  end
  normalized
end

.payment_header_name(version = nil) ⇒ Object



107
108
109
110
111
112
113
114
115
# File 'lib/x402/payments.rb', line 107

def payment_header_name(version = nil)
  normalized = normalize_version(version) || configuration.protocol_version
  case normalized
  when 2
    V2::Headers::PAYMENT_HEADER
  else
    V1::Headers::PAYMENT_HEADER
  end
end

.reset_configuration!Object



73
74
75
# File 'lib/x402/payments/configuration.rb', line 73

def reset_configuration!
  @configuration = Configuration.new
end

.rpc_url_for(chain_name) ⇒ Object



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/x402/payments/chains.rb', line 152

def rpc_url_for(chain_name)
  # Priority: 1) Programmatic config, 2) ENV variable, 3) Default from CHAINS
  config = X402::Payments.configuration

  # Check programmatic configuration
  return config.rpc_urls[chain_name] if config.rpc_urls[chain_name]

  # Check environment variable
  env_var_name = "X402_#{chain_name.upcase.gsub('-', '_')}_RPC_URL"
  env_rpc = ENV[env_var_name]
  return env_rpc if env_rpc && !env_rpc.empty?

  # Fall back to default (only for built-in chains)
  builtin = CHAINS[chain_name]
  builtin ? builtin[:rpc_url] : nil
end

.solana_chain?(chain_name) ⇒ Boolean

Returns:

  • (Boolean)


169
170
171
# File 'lib/x402/payments/chains.rb', line 169

def solana_chain?(chain_name)
  SOLANA_CHAINS.include?(chain_name)
end

.supported_chainsObject



101
102
103
# File 'lib/x402/payments/chains.rb', line 101

def supported_chains
  CHAINS.keys + X402::Payments.configuration.custom_chains.keys
end

.token_config_for(chain_name, symbol = nil) ⇒ Object



124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/x402/payments/chains.rb', line 124

def token_config_for(chain_name, symbol = nil)
  symbol ||= X402::Payments.configuration.currency

  custom = X402::Payments.configuration.token_config(chain_name, symbol)
  return custom if custom

  if symbol.upcase == "USDC" && CURRENCY_BY_CHAIN[chain_name]
    currency_config_for_chain(chain_name)
  else
    raise ConfigurationError, "Unknown token #{symbol} for chain #{chain_name}. Register with config.register_token()"
  end
end

.usdc_address_for(chain_name) ⇒ Object



105
106
107
108
# File 'lib/x402/payments/chains.rb', line 105

def usdc_address_for(chain_name)
  builtin = CHAINS[chain_name]
  builtin ? builtin[:usdc_address] : nil
end