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
35 changes: 35 additions & 0 deletions ruby/lib/pay_kit/config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,21 @@ def configure
@config
end

def configure_from_env(prefix = "PAY_KIT_", env: ENV)
vars = env.to_h.select { |k, _| k.start_with?(prefix) }.transform_keys { |k| k.delete_prefix(prefix) }
configure do |c|
c.network = vars["NETWORK"] if vars.key?("NETWORK")
c.rpc_url = vars["RPC_URL"] if vars.key?("RPC_URL")
c.accept = split_env_list(vars["ACCEPT"]) if vars.key?("ACCEPT")
c.stablecoins = split_env_list(vars["STABLECOINS"]) if vars.key?("STABLECOINS")
c.preflight = parse_env_boolean(vars["PREFLIGHT"], "#{prefix}PREFLIGHT") if vars.key?("PREFLIGHT")
c.mpp.realm = vars["MPP_REALM"] if vars.key?("MPP_REALM")
c.mpp.challenge_binding_secret = vars["MPP_CHALLENGE_BINDING_SECRET"] if vars.key?("MPP_CHALLENGE_BINDING_SECRET")
c.mpp.expires_in = parse_env_integer(vars["MPP_EXPIRES_IN"], "#{prefix}MPP_EXPIRES_IN") if vars.key?("MPP_EXPIRES_IN")
c.x402.facilitator_url = vars["X402_FACILITATOR_URL"] if vars.key?("X402_FACILITATOR_URL")
end
end

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

private

def split_env_list(value)
value.split(",").map(&:strip).reject(&:empty?)
end

def parse_env_boolean(value, key)
case value.strip.downcase
when "true", "1" then true
when "false", "0" then false
else raise ConfigurationError, "#{key} must be true, false, 1 or 0, got #{value.inspect}"
end
end

def parse_env_integer(value, key)
Integer(value.strip, 10)
rescue ArgumentError
raise ConfigurationError, "#{key} must be an integer, got #{value.inspect}"
end
end
end
56 changes: 56 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,62 @@ def test_x402_unknown_scheme_raises
end
end

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

def test_configure_from_env_reads_scalars
env = {
"PAY_KIT_NETWORK" => "solana_devnet",
"PAY_KIT_RPC_URL" => "https://helius.example.com",
"PAY_KIT_PREFLIGHT" => "FALSE",
"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"
}
config = PayKit.configure_from_env(env: env)
assert config.frozen?
assert_equal :solana_devnet, config.network
assert_equal "https://helius.example.com", config.rpc_url
refute config.preflight
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_lists
config = PayKit.configure_from_env(env: {"PAY_KIT_ACCEPT" => "x402, mpp", "PAY_KIT_STABLECOINS" => "USDC,PYUSD"})
assert_equal %i[x402 mpp], config.accept
assert_equal %i[USDC PYUSD], config.stablecoins
end

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

def test_configure_from_env_custom_prefix
config = PayKit.configure_from_env("APP_", env: {"APP_NETWORK" => "solana_devnet", "PAY_KIT_NETWORK" => "bitcoin"})
assert_equal :solana_devnet, 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 |key, value|
PayKit.reset!
assert_raises(PayKit::ConfigurationError, key) do
PayKit.configure_from_env(env: {key => value})
end
end
end

private

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