Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion docs/paykit-interface.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,6 @@ conformant.
- Add `accepts?(protocol)` on `Gate`; keep `x402_accepted?` / `mpp_accepted?`
as deprecated aliases for one release.
- Add `payer` and ensure `scheme` on `Payment`.
- Add `PayKit.configure_from_env`.
- Stablecoin set: add `USDG`, `CASH`.
- Wire `external_id` through every flow (currently inconsistent).

Expand Down
37 changes: 37 additions & 0 deletions ruby/lib/pay_kit/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,26 @@ def configure
@config
end

def configure_from_env(prefix = "PAY_KIT_", env: ENV)
configure do |c|
env.each do |name, value|
next unless name.start_with?(prefix)

case name.delete_prefix(prefix)
when "NETWORK" then c.network = value
when "RPC_URL" then c.rpc_url = value unless value.empty?
when "ACCEPT" then c.accept = value.split(",").map(&:strip) - [""]
when "STABLECOINS" then c.stablecoins = value.split(",").map(&:strip) - [""]
when "PREFLIGHT" then c.preflight = env_bool(name, value)
when "MPP_REALM" then c.mpp.realm = value
when "MPP_CHALLENGE_BINDING_SECRET" then c.mpp.challenge_binding_secret = value
when "MPP_EXPIRES_IN" then c.mpp.expires_in = env_positive_int(name, value)
when "X402_FACILITATOR_URL" then c.x402.facilitator_url = value
end
end
end
end

def config
@config ||= Config.new
end
Expand All @@ -279,5 +299,22 @@ def reset!
@config = nil
@pricing = nil
end

private

def env_bool(name, value)
case value.downcase
when "1", "true", "yes", "on" then true
when "0", "false", "no", "off" then false
else raise ConfigurationError, "#{name} must be one of 1/true/yes/on or 0/false/no/off, got #{value.inspect}"
end
end

def env_positive_int(name, value)
int = Integer(value, 10, exception: false)
return int if int&.positive?

raise ConfigurationError, "#{name} must be a positive integer, got #{value.inspect}"
end
end
end
81 changes: 81 additions & 0 deletions ruby/test/pay_kit/config_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,87 @@ def test_x402_unknown_scheme_raises
end
end

# --- configure_from_env ----------------------------------------------

def test_configure_from_env_reads_scalars_onto_frozen_config
config = PayKit.configure_from_env(env: {
"PAY_KIT_NETWORK" => "solana_devnet",
"PAY_KIT_RPC_URL" => "https://rpc.example.com",
"PAY_KIT_MPP_REALM" => "Shop",
"PAY_KIT_MPP_CHALLENGE_BINDING_SECRET" => "rotate-me",
"PAY_KIT_MPP_EXPIRES_IN" => "90",
"PAY_KIT_X402_FACILITATOR_URL" => "https://facilitator.example.com"
})
assert_same PayKit.config, config
assert config.frozen?
assert_equal :solana_devnet, config.network
assert_equal "https://rpc.example.com", config.rpc_url
assert_equal "Shop", config.mpp.realm
assert_equal "rotate-me", config.mpp.challenge_binding_secret
assert_equal 90, config.mpp.expires_in
assert_equal "https://facilitator.example.com", config.x402.facilitator_url
end

def test_configure_from_env_splits_comma_lists
PayKit.configure_from_env(env: {"PAY_KIT_ACCEPT" => "x402, mpp", "PAY_KIT_STABLECOINS" => "USDC,PYUSD"})
assert_equal %i[x402 mpp], PayKit.config.accept
assert_equal %i[USDC PYUSD], PayKit.config.stablecoins
end

def test_configure_from_env_with_empty_env_keeps_defaults
PayKit.configure_from_env(env: {})
assert_equal :solana_localnet, PayKit.config.network
assert_equal %i[x402 mpp], PayKit.config.accept
assert_equal %i[USDC], PayKit.config.stablecoins
assert_equal 300, PayKit.config.mpp.expires_in
end

def test_configure_from_env_custom_prefix_ignores_default_prefix
PayKit.configure_from_env("APP_", env: {"APP_NETWORK" => "solana_devnet", "PAY_KIT_NETWORK" => "solana_mainnet"})
assert_equal :solana_devnet, PayKit.config.network
end

def test_configure_from_env_rejects_bad_values
[
{"PAY_KIT_NETWORK" => "bitcoin"},
{"PAY_KIT_PREFLIGHT" => "maybe"},
{"PAY_KIT_MPP_EXPIRES_IN" => "soon"},
{"PAY_KIT_STABLECOINS" => " , "}
].each do |env|
PayKit.reset!
assert_raises(PayKit::ConfigurationError, env.inspect) { PayKit.configure_from_env(env: env) }
end
end

def test_configure_from_env_preflight_accepts_python_boolean_words
{"1" => true, "True" => true, "YES" => true, "on" => true,
"0" => false, "false" => false, "No" => false, "OFF" => false}.each do |value, expected|
PayKit.reset!
PayKit.configure_from_env(env: {"PAY_KIT_PREFLIGHT" => value})
assert_equal expected, PayKit.config.preflight, value
end
end

def test_configure_from_env_empty_rpc_url_leaves_rpc_url_unset
PayKit.configure_from_env(env: {"PAY_KIT_RPC_URL" => ""})
assert_nil PayKit.config.rpc_url
assert PayKit.config.using_public_rpc_default?
end

def test_configure_from_env_rejects_non_positive_expires_in
%w[0 -5].each do |value|
PayKit.reset!
assert_raises(PayKit::ConfigurationError, value) do
PayKit.configure_from_env(env: {"PAY_KIT_MPP_EXPIRES_IN" => value})
end
end
end

def test_configure_from_env_ignores_lowercase_names
PayKit.configure_from_env(env: {"pay_kit_network" => "solana_devnet"})
assert_equal :solana_localnet, PayKit.config.network
end

private

# Replace `PayKit::Preflight.run` with a no-op spy for the duration of
Expand Down
Loading