remitmd 0.1.8 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -15
- data/lib/remitmd/a2a.rb +1 -1
- data/lib/remitmd/cli_signer.rb +136 -0
- data/lib/remitmd/errors.rb +2 -2
- data/lib/remitmd/http.rb +3 -3
- data/lib/remitmd/keccak.rb +1 -1
- data/lib/remitmd/mock.rb +1 -1
- data/lib/remitmd/signer.rb +7 -7
- data/lib/remitmd/wallet.rb +25 -21
- data/lib/remitmd/x402_client.rb +1 -1
- data/lib/remitmd/x402_paywall.rb +4 -4
- data/lib/remitmd.rb +3 -3
- metadata +2 -2
- data/lib/remitmd/http_signer.rb +0 -200
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 75be0a5249f87323f7c007b13b7b89f9e2153557b355381000b7d3d007adcfc0
|
|
4
|
+
data.tar.gz: e0f9b80bc11f7eb9888f037cba3589548a3344bdba9f621994142f5b20a3b14d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5198b515870956a99eccee4152e991d4fc32f6f03acd5a91a499b9c853b57da2c0b3cad4657381e1524553f10d021415b1d9202ce35848eb06331e5420758c73
|
|
7
|
+
data.tar.gz: 5bcaab9bc1d75389f524ad3ab36920a02bf9dbacf68792c6492f8bc04a6ebb2197725a73342191a26c2debf7dceddf31a3247968e82f470c228fe76203a51df0
|
data/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> [Skill MD](https://remit.md) · [Docs](https://remit.md/docs) · [Agent Spec](https://remit.md/agent.md)
|
|
4
4
|
|
|
5
|
-
Universal payment protocol for AI agents
|
|
5
|
+
Universal payment protocol for AI agents - Ruby client library.
|
|
6
6
|
|
|
7
7
|
[](https://github.com/remit-md/sdk/actions/workflows/ci.yml)
|
|
8
8
|
[](https://badge.fury.io/rb/remitmd)
|
|
@@ -39,31 +39,35 @@ Or from environment variables:
|
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
41
|
wallet = Remitmd::RemitWallet.from_env
|
|
42
|
-
#
|
|
42
|
+
# Auto-detects: CliSigner (remit CLI) > REMITMD_KEY
|
|
43
43
|
# Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Permits are auto-signed. Every payment method fetches the on-chain USDC nonce, signs an EIP-2612 permit, and includes it automatically.
|
|
47
47
|
|
|
48
|
-
##
|
|
48
|
+
## CLI Signer (Recommended)
|
|
49
49
|
|
|
50
|
-
The
|
|
50
|
+
The CLI signer delegates key management to the `remit` CLI binary, which holds your encrypted keystore at `~/.remit/keys/`. No private key in your environment -- just install the CLI and set a password.
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
# Install the CLI
|
|
54
|
+
# macOS: brew install remit-md/tap/remit
|
|
55
|
+
# Windows: winget install remit-md.remit
|
|
56
|
+
# Linux: curl -fsSL https://remit.md/install.sh | sh
|
|
57
|
+
|
|
58
|
+
export REMIT_KEY_PASSWORD=your-keystore-password
|
|
55
59
|
```
|
|
56
60
|
|
|
57
61
|
```ruby
|
|
58
62
|
# Explicit
|
|
59
|
-
signer = Remitmd::
|
|
63
|
+
signer = Remitmd::CliSigner.new
|
|
60
64
|
wallet = Remitmd::RemitWallet.new(signer: signer)
|
|
61
65
|
|
|
62
66
|
# Or auto-detect from env (recommended)
|
|
63
|
-
wallet = Remitmd::RemitWallet.from_env # detects
|
|
67
|
+
wallet = Remitmd::RemitWallet.from_env # detects remit CLI automatically
|
|
64
68
|
```
|
|
65
69
|
|
|
66
|
-
`RemitWallet.from_env` detects
|
|
70
|
+
`RemitWallet.from_env` detects signing methods automatically. Priority: `CliSigner` (CLI + keystore + password) > `REMITMD_KEY`.
|
|
67
71
|
|
|
68
72
|
## Payment Models
|
|
69
73
|
|
|
@@ -93,7 +97,7 @@ contracts = wallet.get_contracts
|
|
|
93
97
|
sig = wallet.sign_tab_charge(contracts.tab, tab.id, 3_000_000, 1)
|
|
94
98
|
wallet.charge_tab(tab.id, 0.003, 0.003, 1, sig)
|
|
95
99
|
|
|
96
|
-
# Close when done
|
|
100
|
+
# Close when done - unused funds return
|
|
97
101
|
wallet.close_tab(tab.id)
|
|
98
102
|
```
|
|
99
103
|
|
|
@@ -156,9 +160,9 @@ end
|
|
|
156
160
|
|
|
157
161
|
```ruby
|
|
158
162
|
mock.was_paid?(address, amount) # true/false
|
|
159
|
-
mock.total_paid_to(address) # BigDecimal
|
|
163
|
+
mock.total_paid_to(address) # BigDecimal - sum of all payments to address
|
|
160
164
|
mock.transaction_count # Integer
|
|
161
|
-
mock.balance # BigDecimal
|
|
165
|
+
mock.balance # BigDecimal - current balance
|
|
162
166
|
mock.transactions # Array<Transaction>
|
|
163
167
|
mock.set_balance(amount) # Override starting balance
|
|
164
168
|
mock.reset # Clear all state
|
|
@@ -274,11 +278,11 @@ Remitmd::RemitWallet.new(private_key: key, chain: "base_sepolia") # Base Sepoli
|
|
|
274
278
|
Permits are auto-signed by default. If you need manual control (custom deadline, pre-signed permits, or offline signing), pass a `PermitSignature` explicitly:
|
|
275
279
|
|
|
276
280
|
```ruby
|
|
277
|
-
# sign_permit: convenience
|
|
281
|
+
# sign_permit: convenience - auto-fetches nonce, converts amount to base units
|
|
278
282
|
permit = wallet.sign_permit("0xRouterAddress...", 5.00, deadline: Time.now.to_i + 7200)
|
|
279
283
|
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
280
284
|
|
|
281
|
-
# sign_usdc_permit: full control
|
|
285
|
+
# sign_usdc_permit: full control - raw base units, explicit nonce
|
|
282
286
|
permit = wallet.sign_usdc_permit(
|
|
283
287
|
"0xRouterAddress...", # spender
|
|
284
288
|
5_000_000, # value in base units (6 decimals)
|
|
@@ -291,6 +295,6 @@ tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
|
291
295
|
|
|
292
296
|
## License
|
|
293
297
|
|
|
294
|
-
MIT
|
|
298
|
+
MIT - see [LICENSE](LICENSE)
|
|
295
299
|
|
|
296
300
|
[Documentation](https://remit.md/docs) · [Protocol Spec](https://remit.md) · [GitHub](https://github.com/remit-md/sdk)
|
data/lib/remitmd/a2a.rb
CHANGED
|
@@ -135,7 +135,7 @@ module Remitmd
|
|
|
135
135
|
|
|
136
136
|
# ─── A2A JSON-RPC client ────────────────────────────────────────────────────
|
|
137
137
|
|
|
138
|
-
# A2A JSON-RPC client
|
|
138
|
+
# A2A JSON-RPC client - send payments and manage tasks via the A2A protocol.
|
|
139
139
|
#
|
|
140
140
|
# @example
|
|
141
141
|
# card = Remitmd::AgentCard.discover("https://remit.md")
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
|
|
5
|
+
module Remitmd
|
|
6
|
+
# Signer backed by the `remit sign` CLI command.
|
|
7
|
+
#
|
|
8
|
+
# No key material in this process -- signing happens in a subprocess.
|
|
9
|
+
# Address is cached at construction time via `remit address`.
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# signer = Remitmd::CliSigner.new
|
|
13
|
+
# wallet = Remitmd::RemitWallet.new(signer: signer, chain: "base")
|
|
14
|
+
#
|
|
15
|
+
class CliSigner
|
|
16
|
+
include Signer
|
|
17
|
+
|
|
18
|
+
# Default timeout for CLI subprocess calls (seconds).
|
|
19
|
+
CLI_TIMEOUT = 10
|
|
20
|
+
|
|
21
|
+
# Create a CliSigner, fetching and caching the wallet address.
|
|
22
|
+
#
|
|
23
|
+
# @param cli_path [String] path or name of the remit CLI binary (default: "remit")
|
|
24
|
+
# @raise [RemitError] if the CLI fails or returns an invalid address
|
|
25
|
+
def initialize(cli_path: "remit")
|
|
26
|
+
@cli_path = cli_path
|
|
27
|
+
@address = fetch_address
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Sign a 32-byte digest (raw binary bytes).
|
|
31
|
+
# Pipes the hex-encoded digest to `remit sign --digest` on stdin.
|
|
32
|
+
# Returns a 0x-prefixed 65-byte hex signature.
|
|
33
|
+
#
|
|
34
|
+
# @param digest_bytes [String] 32-byte binary digest
|
|
35
|
+
# @return [String] 0x-prefixed 130-char hex signature (65 bytes)
|
|
36
|
+
# @raise [RemitError] on CLI failure or invalid output
|
|
37
|
+
def sign(digest_bytes)
|
|
38
|
+
hex = digest_bytes.unpack1("H*")
|
|
39
|
+
stdout, stderr, status = run_cli("sign", "--digest", stdin_data: hex)
|
|
40
|
+
|
|
41
|
+
unless status.success?
|
|
42
|
+
raise RemitError.new(
|
|
43
|
+
RemitError::SERVER_ERROR,
|
|
44
|
+
"CliSigner: signing failed: #{stderr.strip}"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
sig = stdout.strip
|
|
49
|
+
unless sig.start_with?("0x") && sig.length == 132
|
|
50
|
+
raise RemitError.new(
|
|
51
|
+
RemitError::SERVER_ERROR,
|
|
52
|
+
"CliSigner: invalid signature from CLI: #{sig}"
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
sig
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The cached Ethereum address (0x-prefixed).
|
|
60
|
+
# @return [String]
|
|
61
|
+
attr_reader :address
|
|
62
|
+
|
|
63
|
+
# Never expose internals in inspect/to_s output.
|
|
64
|
+
def inspect
|
|
65
|
+
"#<Remitmd::CliSigner address=#{@address}>"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
alias to_s inspect
|
|
69
|
+
|
|
70
|
+
# Check all three conditions for CliSigner activation.
|
|
71
|
+
#
|
|
72
|
+
# 1. CLI binary found on PATH (via `which` / `where`)
|
|
73
|
+
# 2. Keystore file exists at ~/.remit/keys/default.enc
|
|
74
|
+
# 3. REMIT_KEY_PASSWORD env var is set
|
|
75
|
+
#
|
|
76
|
+
# @param cli_path [String] path or name of the remit CLI binary
|
|
77
|
+
# @return [Boolean]
|
|
78
|
+
def self.available?(cli_path: "remit")
|
|
79
|
+
# 1. CLI binary on PATH
|
|
80
|
+
which_cmd = Gem.win_platform? ? "where" : "which"
|
|
81
|
+
_out, _err, st = Open3.capture3(which_cmd, cli_path)
|
|
82
|
+
return false unless st.success?
|
|
83
|
+
|
|
84
|
+
# 2. Keystore file exists
|
|
85
|
+
keystore = File.join(Dir.home, ".remit", "keys", "default.enc")
|
|
86
|
+
return false unless File.exist?(keystore)
|
|
87
|
+
|
|
88
|
+
# 3. REMIT_KEY_PASSWORD set
|
|
89
|
+
password = ENV["REMIT_KEY_PASSWORD"]
|
|
90
|
+
return false if password.nil? || password.empty?
|
|
91
|
+
|
|
92
|
+
true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
# Fetch the wallet address from `remit address` during construction.
|
|
98
|
+
# @return [String] the 0x-prefixed Ethereum address
|
|
99
|
+
# @raise [RemitError] on any failure
|
|
100
|
+
def fetch_address
|
|
101
|
+
stdout, stderr, status = run_cli("address")
|
|
102
|
+
|
|
103
|
+
unless status.success?
|
|
104
|
+
raise RemitError.new(
|
|
105
|
+
RemitError::SERVER_ERROR,
|
|
106
|
+
"CliSigner: failed to get address: #{stderr.strip}"
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
addr = stdout.strip
|
|
111
|
+
unless addr.match?(/\A0x[0-9a-fA-F]{40}\z/)
|
|
112
|
+
raise RemitError.new(
|
|
113
|
+
RemitError::SERVER_ERROR,
|
|
114
|
+
"CliSigner: invalid address from CLI: #{addr}"
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
addr
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Run the remit CLI with given arguments.
|
|
122
|
+
# @param args [Array<String>] CLI arguments
|
|
123
|
+
# @param stdin_data [String, nil] data to pipe to stdin
|
|
124
|
+
# @return [Array<String, String, Process::Status>] stdout, stderr, status
|
|
125
|
+
# @raise [RemitError] on timeout or execution failure
|
|
126
|
+
def run_cli(*args, stdin_data: nil)
|
|
127
|
+
Open3.capture3(@cli_path, *args, stdin_data: stdin_data.to_s)
|
|
128
|
+
rescue Errno::ENOENT
|
|
129
|
+
raise RemitError.new(
|
|
130
|
+
RemitError::SERVER_ERROR,
|
|
131
|
+
"CliSigner: remit CLI not found at '#{@cli_path}'. " \
|
|
132
|
+
"Install: https://remit.md/install"
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
data/lib/remitmd/errors.rb
CHANGED
|
@@ -15,7 +15,7 @@ module Remitmd
|
|
|
15
15
|
# puts e.doc_url # => "https://remit.md/docs/api-reference/error-codes#invalid_address"
|
|
16
16
|
# end
|
|
17
17
|
class RemitError < StandardError
|
|
18
|
-
# Error code constants
|
|
18
|
+
# Error code constants - matches TS SDK (28 codes)
|
|
19
19
|
# Auth errors
|
|
20
20
|
INVALID_SIGNATURE = "INVALID_SIGNATURE"
|
|
21
21
|
NONCE_REUSED = "NONCE_REUSED"
|
|
@@ -80,7 +80,7 @@ module Remitmd
|
|
|
80
80
|
@code = code
|
|
81
81
|
@doc_url = "https://remit.md/docs/api-reference/error-codes##{code.downcase}"
|
|
82
82
|
@context = context
|
|
83
|
-
super("[#{code}] #{message}
|
|
83
|
+
super("[#{code}] #{message} - #{@doc_url}")
|
|
84
84
|
end
|
|
85
85
|
end
|
|
86
86
|
end
|
data/lib/remitmd/http.rb
CHANGED
|
@@ -74,7 +74,7 @@ module Remitmd
|
|
|
74
74
|
nonce_hex = "0x#{nonce_bytes.unpack1("H*")}"
|
|
75
75
|
timestamp = Time.now.to_i
|
|
76
76
|
|
|
77
|
-
# Strip query string before signing
|
|
77
|
+
# Strip query string before signing - only the path is included in EIP-712.
|
|
78
78
|
sign_path = full_path.split("?").first
|
|
79
79
|
http_method = method.to_s.upcase
|
|
80
80
|
digest = eip712_hash(http_method, sign_path, timestamp, nonce_bytes)
|
|
@@ -100,7 +100,7 @@ module Remitmd
|
|
|
100
100
|
# Domain: name="remit.md", version="0.1", chainId, verifyingContract
|
|
101
101
|
# Struct: APIRequest(string method, string path, uint256 timestamp, bytes32 nonce)
|
|
102
102
|
def eip712_hash(method, path, timestamp, nonce_bytes)
|
|
103
|
-
# Type hashes (string constants
|
|
103
|
+
# Type hashes (string constants - keccak256 of the type string)
|
|
104
104
|
domain_type_hash = keccak256_bytes(
|
|
105
105
|
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
106
106
|
)
|
|
@@ -162,7 +162,7 @@ module Remitmd
|
|
|
162
162
|
raise RemitError.new(code, err["message"] || "Bad request", context: parsed)
|
|
163
163
|
when 401
|
|
164
164
|
raise RemitError.new(RemitError::UNAUTHORIZED,
|
|
165
|
-
"Authentication failed
|
|
165
|
+
"Authentication failed - check your private key and chain ID")
|
|
166
166
|
when 429
|
|
167
167
|
raise RemitError.new(RemitError::RATE_LIMITED,
|
|
168
168
|
"Rate limit exceeded. See https://remit.md/docs/api-reference/rate-limits")
|
data/lib/remitmd/keccak.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Remitmd
|
|
4
|
-
# Pure-Ruby Keccak-256 (Ethereum variant
|
|
4
|
+
# Pure-Ruby Keccak-256 (Ethereum variant - NOT SHA-3).
|
|
5
5
|
#
|
|
6
6
|
# SHA-3 uses different padding (0x06 instead of 0x01).
|
|
7
7
|
# This implementation matches Ethereum's keccak256.
|
data/lib/remitmd/mock.rb
CHANGED
|
@@ -175,7 +175,7 @@ module Remitmd
|
|
|
175
175
|
@state[:pending_invoices][id] = b
|
|
176
176
|
{ "id" => id, "status" => "pending" }
|
|
177
177
|
|
|
178
|
-
# Escrow create (step 2
|
|
178
|
+
# Escrow create (step 2 - fund with invoice_id)
|
|
179
179
|
in ["POST", "/escrows"]
|
|
180
180
|
invoice_id = fetch!(b, :invoice_id)
|
|
181
181
|
inv = @state[:pending_invoices].delete(invoice_id)
|
data/lib/remitmd/signer.rb
CHANGED
|
@@ -4,12 +4,12 @@ require "openssl"
|
|
|
4
4
|
require "securerandom"
|
|
5
5
|
|
|
6
6
|
module Remitmd
|
|
7
|
-
# secp256k1 field prime p (constant
|
|
7
|
+
# secp256k1 field prime p (constant - never changes)
|
|
8
8
|
SECP256K1_P = OpenSSL::BN.new(
|
|
9
9
|
"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16
|
|
10
10
|
).freeze
|
|
11
11
|
|
|
12
|
-
# Precomputed (p + 1) / 4
|
|
12
|
+
# Precomputed (p + 1) / 4 - the modular square root exponent for p ≡ 3 (mod 4).
|
|
13
13
|
# Avoids BN division at runtime (which returns Integer on some OpenSSL versions).
|
|
14
14
|
SECP256K1_SQRT_EXP = OpenSSL::BN.new(
|
|
15
15
|
"3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF0C", 16
|
|
@@ -66,7 +66,7 @@ module Remitmd
|
|
|
66
66
|
group = @key.group
|
|
67
67
|
n = group.order
|
|
68
68
|
|
|
69
|
-
# ECDSA sign
|
|
69
|
+
# ECDSA sign - dsa_sign_asn1 uses the input directly as the hash (no pre-hashing)
|
|
70
70
|
der = @key.dsa_sign_asn1(digest_bytes)
|
|
71
71
|
asn1 = OpenSSL::ASN1.decode(der)
|
|
72
72
|
bn_r = asn1.value[0].value
|
|
@@ -104,7 +104,7 @@ module Remitmd
|
|
|
104
104
|
break
|
|
105
105
|
end
|
|
106
106
|
end
|
|
107
|
-
raise "Could not determine recovery ID
|
|
107
|
+
raise "Could not determine recovery ID - key or hash may be invalid" if v.nil?
|
|
108
108
|
|
|
109
109
|
# Build 65-byte Ethereum signature: r (32) || s (32) || v (1)
|
|
110
110
|
r_bytes = [bn_r.to_s(16).rjust(64, "0")].pack("H*")
|
|
@@ -129,13 +129,13 @@ module Remitmd
|
|
|
129
129
|
def recover_r_point(group, bn_r, parity)
|
|
130
130
|
p = SECP256K1_P
|
|
131
131
|
x = bn_r
|
|
132
|
-
# y² = x³ + 7 (mod p)
|
|
132
|
+
# y² = x³ + 7 (mod p) - secp256k1 curve equation
|
|
133
133
|
x3 = x.mod_exp(OpenSSL::BN.new("3"), p)
|
|
134
134
|
rhs = x3 + OpenSSL::BN.new("7")
|
|
135
135
|
y_squared = rhs % p
|
|
136
136
|
# Tonelli–Shanks: since p ≡ 3 mod 4, sqrt = y²^((p+1)/4) mod p
|
|
137
137
|
y = y_squared.mod_exp(SECP256K1_SQRT_EXP, p)
|
|
138
|
-
# Verify that y² ≡ y_squared (mod p)
|
|
138
|
+
# Verify that y² ≡ y_squared (mod p) - i.e., a square root exists
|
|
139
139
|
return nil unless y.mod_mul(y, p) == y_squared
|
|
140
140
|
|
|
141
141
|
y = p - y if (y.to_i & 1) != parity
|
|
@@ -148,7 +148,7 @@ module Remitmd
|
|
|
148
148
|
end
|
|
149
149
|
|
|
150
150
|
def derive_address(public_key)
|
|
151
|
-
# Uncompressed public key: 04 || x (32) || y (32)
|
|
151
|
+
# Uncompressed public key: 04 || x (32) || y (32) - skip the 0x04 prefix
|
|
152
152
|
pub_bytes = [public_key.to_octet_string(:uncompressed).unpack1("H*")[2..]].pack("H*")
|
|
153
153
|
keccak = keccak256_hex(pub_bytes)
|
|
154
154
|
"0x#{keccak[-40..]}"
|
data/lib/remitmd/wallet.rb
CHANGED
|
@@ -28,7 +28,7 @@ module Remitmd
|
|
|
28
28
|
|
|
29
29
|
# @param private_key [String, nil] 0x-prefixed hex private key
|
|
30
30
|
# @param signer [Signer, nil] custom signer (pass instead of private_key)
|
|
31
|
-
# @param chain [String] chain name
|
|
31
|
+
# @param chain [String] chain name - "base", "base_sepolia"
|
|
32
32
|
# @param api_url [String, nil] override API base URL
|
|
33
33
|
# @param transport [Object, nil] inject mock transport (used by MockRemit)
|
|
34
34
|
def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil, transport: nil)
|
|
@@ -67,21 +67,16 @@ module Remitmd
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Build a RemitWallet from environment variables.
|
|
70
|
-
#
|
|
71
|
-
# or REMITMD_KEY (primary) / REMITMD_PRIVATE_KEY (deprecated fallback).
|
|
70
|
+
# Priority: CliSigner (if available) > REMITMD_KEY / REMITMD_PRIVATE_KEY.
|
|
72
71
|
# Also reads: REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
|
|
73
72
|
def self.from_env
|
|
74
73
|
chain = ENV.fetch("REMITMD_CHAIN", "base")
|
|
75
74
|
api_url = ENV["REMITMD_API_URL"]
|
|
76
75
|
router_address = ENV["REMITMD_ROUTER_ADDRESS"]
|
|
77
76
|
|
|
78
|
-
# Priority 1:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
signer_token = ENV["REMIT_SIGNER_TOKEN"]
|
|
82
|
-
raise ArgumentError, "REMIT_SIGNER_TOKEN must be set when REMIT_SIGNER_URL is set" unless signer_token
|
|
83
|
-
|
|
84
|
-
signer = HttpSigner.new(url: signer_url, token: signer_token)
|
|
77
|
+
# Priority 1: CLI signer (remit binary + keystore + password)
|
|
78
|
+
if CliSigner.available?
|
|
79
|
+
signer = CliSigner.new
|
|
85
80
|
return new(signer: signer, chain: chain, api_url: api_url, router_address: router_address)
|
|
86
81
|
end
|
|
87
82
|
|
|
@@ -90,7 +85,16 @@ module Remitmd
|
|
|
90
85
|
if ENV["REMITMD_PRIVATE_KEY"] && !ENV["REMITMD_KEY"]
|
|
91
86
|
warn "[remitmd] REMITMD_PRIVATE_KEY is deprecated, use REMITMD_KEY instead"
|
|
92
87
|
end
|
|
93
|
-
|
|
88
|
+
|
|
89
|
+
unless key
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"No signing method available. Either:\n" \
|
|
92
|
+
" 1. Install the remit CLI and set REMIT_KEY_PASSWORD:\n" \
|
|
93
|
+
" macOS: brew install remit-md/tap/remit\n" \
|
|
94
|
+
" Windows: winget install remit-md.remit\n" \
|
|
95
|
+
" Linux: curl -fsSL https://remit.md/install.sh | sh\n" \
|
|
96
|
+
" 2. Set REMITMD_KEY to a raw private key (hex)"
|
|
97
|
+
end
|
|
94
98
|
|
|
95
99
|
new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
|
|
96
100
|
end
|
|
@@ -157,7 +161,7 @@ module Remitmd
|
|
|
157
161
|
# @param to [String] recipient 0x-prefixed address
|
|
158
162
|
# @param amount [Numeric, BigDecimal] amount in USDC (e.g. 1.50)
|
|
159
163
|
# @param memo [String, nil] optional note
|
|
160
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
164
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
161
165
|
# @return [Transaction]
|
|
162
166
|
def pay(to, amount, memo: nil, permit: nil)
|
|
163
167
|
validate_address!(to)
|
|
@@ -176,7 +180,7 @@ module Remitmd
|
|
|
176
180
|
# @param amount [Numeric] amount in USDC
|
|
177
181
|
# @param memo [String, nil] optional note
|
|
178
182
|
# @param expires_in_secs [Integer, nil] optional expiry in seconds from now
|
|
179
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
183
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
180
184
|
# @return [Escrow]
|
|
181
185
|
def create_escrow(payee, amount, memo: nil, expires_in_secs: nil, permit: nil)
|
|
182
186
|
validate_address!(payee)
|
|
@@ -237,7 +241,7 @@ module Remitmd
|
|
|
237
241
|
# @param limit_amount [Numeric] maximum tab credit in USDC
|
|
238
242
|
# @param per_unit [Numeric] USDC per API call
|
|
239
243
|
# @param expires_in_secs [Integer] optional expiry duration in seconds (default: 86400)
|
|
240
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
244
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
241
245
|
# @return [Tab]
|
|
242
246
|
def create_tab(provider, limit_amount, per_unit = 0.0, expires_in_secs: 86_400, permit: nil)
|
|
243
247
|
validate_address!(provider)
|
|
@@ -430,7 +434,7 @@ module Remitmd
|
|
|
430
434
|
# @param payee [String] 0x-prefixed address of the stream recipient
|
|
431
435
|
# @param rate_per_second [Numeric] USDC per second
|
|
432
436
|
# @param max_total [Numeric] maximum total USDC for the stream
|
|
433
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
437
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
434
438
|
# @return [Stream]
|
|
435
439
|
def create_stream(payee, rate_per_second, max_total, permit: nil)
|
|
436
440
|
validate_address!(payee)
|
|
@@ -468,7 +472,7 @@ module Remitmd
|
|
|
468
472
|
# @param task_description [String] task description
|
|
469
473
|
# @param deadline [Integer] deadline as Unix timestamp
|
|
470
474
|
# @param max_attempts [Integer] maximum submission attempts (default: 10)
|
|
471
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
475
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
472
476
|
# @return [Bounty]
|
|
473
477
|
def create_bounty(amount, task_description, deadline, max_attempts: 10, permit: nil)
|
|
474
478
|
validate_amount!(amount)
|
|
@@ -522,7 +526,7 @@ module Remitmd
|
|
|
522
526
|
# @param provider [String] 0x-prefixed provider address
|
|
523
527
|
# @param amount [Numeric] amount in USDC
|
|
524
528
|
# @param expires_in_secs [Integer] expiry duration in seconds (default: 3600)
|
|
525
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
529
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
526
530
|
# @return [Deposit]
|
|
527
531
|
def place_deposit(provider, amount, expires_in_secs: 3600, permit: nil)
|
|
528
532
|
validate_address!(provider)
|
|
@@ -556,7 +560,7 @@ module Remitmd
|
|
|
556
560
|
# Propose a payment intent for counterpart approval before execution.
|
|
557
561
|
# @param to [String] 0x-prefixed address
|
|
558
562
|
# @param amount [Numeric] amount in USDC
|
|
559
|
-
# @param type [String] payment type
|
|
563
|
+
# @param type [String] payment type - "direct", "escrow", "tab"
|
|
560
564
|
# @return [Intent]
|
|
561
565
|
def propose_intent(to, amount, type: "direct")
|
|
562
566
|
validate_address!(to)
|
|
@@ -583,7 +587,7 @@ module Remitmd
|
|
|
583
587
|
# Generate a one-time URL for the operator to fund this wallet.
|
|
584
588
|
# @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
|
|
585
589
|
# @param agent_name [String, nil] agent display name shown on the funding page
|
|
586
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
590
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
587
591
|
# @return [LinkResponse]
|
|
588
592
|
def create_fund_link(messages: nil, agent_name: nil, permit: nil)
|
|
589
593
|
body = {}
|
|
@@ -601,7 +605,7 @@ module Remitmd
|
|
|
601
605
|
# Generate a one-time URL for the operator to withdraw funds.
|
|
602
606
|
# @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
|
|
603
607
|
# @param agent_name [String, nil] agent display name shown on the withdraw page
|
|
604
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
608
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
605
609
|
# @return [LinkResponse]
|
|
606
610
|
def create_withdraw_link(messages: nil, agent_name: nil, permit: nil)
|
|
607
611
|
body = {}
|
|
@@ -710,7 +714,7 @@ module Remitmd
|
|
|
710
714
|
|
|
711
715
|
# Auto-sign a permit for the given contract type and amount.
|
|
712
716
|
# Returns nil on failure instead of raising, so callers can proceed without a permit.
|
|
713
|
-
# @param contract [String] contract key
|
|
717
|
+
# @param contract [String] contract key - "router", "escrow", "tab", etc.
|
|
714
718
|
# @param amount [Numeric] amount in USDC
|
|
715
719
|
# @return [PermitSignature, nil]
|
|
716
720
|
def auto_permit(contract, amount)
|
data/lib/remitmd/x402_client.rb
CHANGED
|
@@ -21,7 +21,7 @@ module Remitmd
|
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
23
|
|
|
24
|
-
# x402 client
|
|
24
|
+
# x402 client - fetch wrapper that auto-pays HTTP 402 Payment Required responses.
|
|
25
25
|
#
|
|
26
26
|
# On receiving a 402, the client:
|
|
27
27
|
# 1. Decodes the PAYMENT-REQUIRED header (base64 JSON)
|
data/lib/remitmd/x402_paywall.rb
CHANGED
|
@@ -6,7 +6,7 @@ require "json"
|
|
|
6
6
|
require "base64"
|
|
7
7
|
|
|
8
8
|
module Remitmd
|
|
9
|
-
# x402 paywall for service providers
|
|
9
|
+
# x402 paywall for service providers - gate HTTP endpoints behind payments.
|
|
10
10
|
#
|
|
11
11
|
# Providers use this class to:
|
|
12
12
|
# - Return HTTP 402 responses with properly formatted PAYMENT-REQUIRED headers
|
|
@@ -29,9 +29,9 @@ module Remitmd
|
|
|
29
29
|
# @param facilitator_url [String] base URL of the remit.md facilitator
|
|
30
30
|
# @param facilitator_token [String] bearer JWT for authenticating calls to /api/v1/x402/verify
|
|
31
31
|
# @param max_timeout_seconds [Integer] how long the payment authorization remains valid
|
|
32
|
-
# @param resource [String, nil] V2
|
|
33
|
-
# @param description [String, nil] V2
|
|
34
|
-
# @param mime_type [String, nil] V2
|
|
32
|
+
# @param resource [String, nil] V2 - URL or path of the resource being protected
|
|
33
|
+
# @param description [String, nil] V2 - human-readable description
|
|
34
|
+
# @param mime_type [String, nil] V2 - MIME type of the resource
|
|
35
35
|
def initialize( # rubocop:disable Metrics/ParameterLists
|
|
36
36
|
wallet_address:,
|
|
37
37
|
amount_usdc:,
|
data/lib/remitmd.rb
CHANGED
|
@@ -4,7 +4,7 @@ require_relative "remitmd/errors"
|
|
|
4
4
|
require_relative "remitmd/models"
|
|
5
5
|
require_relative "remitmd/keccak"
|
|
6
6
|
require_relative "remitmd/signer"
|
|
7
|
-
require_relative "remitmd/
|
|
7
|
+
require_relative "remitmd/cli_signer"
|
|
8
8
|
require_relative "remitmd/http"
|
|
9
9
|
require_relative "remitmd/wallet"
|
|
10
10
|
require_relative "remitmd/mock"
|
|
@@ -12,7 +12,7 @@ require_relative "remitmd/a2a"
|
|
|
12
12
|
require_relative "remitmd/x402_client"
|
|
13
13
|
require_relative "remitmd/x402_paywall"
|
|
14
14
|
|
|
15
|
-
# remit.md Ruby SDK
|
|
15
|
+
# remit.md Ruby SDK - universal payment protocol for AI agents.
|
|
16
16
|
#
|
|
17
17
|
# @example Direct payment
|
|
18
18
|
# wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_PRIVATE_KEY"])
|
|
@@ -26,5 +26,5 @@ require_relative "remitmd/x402_paywall"
|
|
|
26
26
|
# mock.was_paid?("0x0000000000000000000000000000000000000001", 1.00) # => true
|
|
27
27
|
#
|
|
28
28
|
module Remitmd
|
|
29
|
-
VERSION = "0.1
|
|
29
|
+
VERSION = "0.2.1"
|
|
30
30
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: remitmd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- remit.md
|
|
@@ -51,9 +51,9 @@ files:
|
|
|
51
51
|
- README.md
|
|
52
52
|
- lib/remitmd.rb
|
|
53
53
|
- lib/remitmd/a2a.rb
|
|
54
|
+
- lib/remitmd/cli_signer.rb
|
|
54
55
|
- lib/remitmd/errors.rb
|
|
55
56
|
- lib/remitmd/http.rb
|
|
56
|
-
- lib/remitmd/http_signer.rb
|
|
57
57
|
- lib/remitmd/keccak.rb
|
|
58
58
|
- lib/remitmd/mock.rb
|
|
59
59
|
- lib/remitmd/models.rb
|
data/lib/remitmd/http_signer.rb
DELETED
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "net/http"
|
|
4
|
-
require "uri"
|
|
5
|
-
require "json"
|
|
6
|
-
|
|
7
|
-
module Remitmd
|
|
8
|
-
# Signer backed by a local HTTP signing server.
|
|
9
|
-
#
|
|
10
|
-
# Delegates digest signing to an HTTP server (typically
|
|
11
|
-
# `http://127.0.0.1:7402`). The signer server holds the encrypted key;
|
|
12
|
-
# this adapter only needs a bearer token and URL.
|
|
13
|
-
#
|
|
14
|
-
# @example
|
|
15
|
-
# signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
|
|
16
|
-
# wallet = Remitmd::RemitWallet.new(signer: signer, chain: "base")
|
|
17
|
-
#
|
|
18
|
-
class HttpSigner
|
|
19
|
-
include Signer
|
|
20
|
-
|
|
21
|
-
# Create an HttpSigner, fetching and caching the wallet address.
|
|
22
|
-
#
|
|
23
|
-
# @param url [String] signer server URL (e.g. "http://127.0.0.1:7402")
|
|
24
|
-
# @param token [String] bearer token for authentication
|
|
25
|
-
# @raise [RemitError] if the server is unreachable, returns an error, or returns no address
|
|
26
|
-
def initialize(url:, token:)
|
|
27
|
-
@url = url.chomp("/")
|
|
28
|
-
@token = token
|
|
29
|
-
@address = fetch_address
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Sign a 32-byte digest (raw binary bytes).
|
|
33
|
-
# Posts to /sign/digest with the hex-encoded digest.
|
|
34
|
-
# Returns a 0x-prefixed 65-byte hex signature.
|
|
35
|
-
#
|
|
36
|
-
# @param digest_bytes [String] 32-byte binary digest
|
|
37
|
-
# @return [String] 0x-prefixed 65-byte hex signature
|
|
38
|
-
# @raise [RemitError] on network, auth, policy, or server errors
|
|
39
|
-
def sign(digest_bytes)
|
|
40
|
-
hex = "0x#{digest_bytes.unpack1("H*")}"
|
|
41
|
-
uri = URI("#{@url}/sign/digest")
|
|
42
|
-
http = build_http(uri)
|
|
43
|
-
|
|
44
|
-
req = Net::HTTP::Post.new(uri.path)
|
|
45
|
-
req["Content-Type"] = "application/json"
|
|
46
|
-
req["Authorization"] = "Bearer #{@token}"
|
|
47
|
-
req.body = { digest: hex }.to_json
|
|
48
|
-
|
|
49
|
-
resp = begin
|
|
50
|
-
http.request(req)
|
|
51
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
|
|
52
|
-
raise RemitError.new(
|
|
53
|
-
RemitError::NETWORK_ERROR,
|
|
54
|
-
"HttpSigner: cannot reach signer server at #{@url}: #{e.message}"
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
handle_sign_response(resp)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# The cached Ethereum address (0x-prefixed).
|
|
62
|
-
# @return [String]
|
|
63
|
-
attr_reader :address
|
|
64
|
-
|
|
65
|
-
# Never expose the bearer token in inspect/to_s output.
|
|
66
|
-
def inspect
|
|
67
|
-
"#<Remitmd::HttpSigner address=#{@address}>"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
alias to_s inspect
|
|
71
|
-
|
|
72
|
-
private
|
|
73
|
-
|
|
74
|
-
# Fetch the wallet address from GET /address during construction.
|
|
75
|
-
# @return [String] the 0x-prefixed Ethereum address
|
|
76
|
-
# @raise [RemitError] on any failure
|
|
77
|
-
def fetch_address
|
|
78
|
-
uri = URI("#{@url}/address")
|
|
79
|
-
http = build_http(uri)
|
|
80
|
-
|
|
81
|
-
req = Net::HTTP::Get.new(uri.path)
|
|
82
|
-
req["Authorization"] = "Bearer #{@token}"
|
|
83
|
-
|
|
84
|
-
resp = begin
|
|
85
|
-
http.request(req)
|
|
86
|
-
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
|
|
87
|
-
raise RemitError.new(
|
|
88
|
-
RemitError::NETWORK_ERROR,
|
|
89
|
-
"HttpSigner: cannot reach signer server at #{@url}: #{e.message}"
|
|
90
|
-
)
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
status = resp.code.to_i
|
|
94
|
-
|
|
95
|
-
if status == 401
|
|
96
|
-
raise RemitError.new(
|
|
97
|
-
RemitError::UNAUTHORIZED,
|
|
98
|
-
"HttpSigner: unauthorized -- check your REMIT_SIGNER_TOKEN"
|
|
99
|
-
)
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
unless (200..299).cover?(status)
|
|
103
|
-
raise RemitError.new(
|
|
104
|
-
RemitError::SERVER_ERROR,
|
|
105
|
-
"HttpSigner: GET /address failed (#{status})"
|
|
106
|
-
)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
body = begin
|
|
110
|
-
JSON.parse(resp.body.to_s)
|
|
111
|
-
rescue JSON::ParserError
|
|
112
|
-
raise RemitError.new(
|
|
113
|
-
RemitError::SERVER_ERROR,
|
|
114
|
-
"HttpSigner: GET /address returned malformed JSON"
|
|
115
|
-
)
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
addr = body["address"]
|
|
119
|
-
if addr.nil? || addr.to_s.empty?
|
|
120
|
-
raise RemitError.new(
|
|
121
|
-
RemitError::SERVER_ERROR,
|
|
122
|
-
"HttpSigner: GET /address returned no address"
|
|
123
|
-
)
|
|
124
|
-
end
|
|
125
|
-
|
|
126
|
-
addr.to_s
|
|
127
|
-
end
|
|
128
|
-
|
|
129
|
-
# Handle the response from POST /sign/digest.
|
|
130
|
-
# @param resp [Net::HTTPResponse]
|
|
131
|
-
# @return [String] the 0x-prefixed hex signature
|
|
132
|
-
# @raise [RemitError] on any error
|
|
133
|
-
def handle_sign_response(resp)
|
|
134
|
-
status = resp.code.to_i
|
|
135
|
-
|
|
136
|
-
if status == 401
|
|
137
|
-
raise RemitError.new(
|
|
138
|
-
RemitError::UNAUTHORIZED,
|
|
139
|
-
"HttpSigner: unauthorized -- check your REMIT_SIGNER_TOKEN"
|
|
140
|
-
)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
if status == 403
|
|
144
|
-
reason = begin
|
|
145
|
-
data = JSON.parse(resp.body.to_s)
|
|
146
|
-
data["reason"] || "unknown"
|
|
147
|
-
rescue JSON::ParserError
|
|
148
|
-
"unknown"
|
|
149
|
-
end
|
|
150
|
-
raise RemitError.new(
|
|
151
|
-
RemitError::UNAUTHORIZED,
|
|
152
|
-
"HttpSigner: policy denied -- #{reason}"
|
|
153
|
-
)
|
|
154
|
-
end
|
|
155
|
-
|
|
156
|
-
unless (200..299).cover?(status)
|
|
157
|
-
detail = begin
|
|
158
|
-
data = JSON.parse(resp.body.to_s)
|
|
159
|
-
data["reason"] || data["error"] || "server error"
|
|
160
|
-
rescue JSON::ParserError
|
|
161
|
-
"server error"
|
|
162
|
-
end
|
|
163
|
-
raise RemitError.new(
|
|
164
|
-
RemitError::SERVER_ERROR,
|
|
165
|
-
"HttpSigner: sign failed (#{status}): #{detail}"
|
|
166
|
-
)
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
body = begin
|
|
170
|
-
JSON.parse(resp.body.to_s)
|
|
171
|
-
rescue JSON::ParserError
|
|
172
|
-
raise RemitError.new(
|
|
173
|
-
RemitError::SERVER_ERROR,
|
|
174
|
-
"HttpSigner: POST /sign/digest returned malformed JSON"
|
|
175
|
-
)
|
|
176
|
-
end
|
|
177
|
-
|
|
178
|
-
sig = body["signature"]
|
|
179
|
-
if sig.nil? || sig.to_s.empty?
|
|
180
|
-
raise RemitError.new(
|
|
181
|
-
RemitError::SERVER_ERROR,
|
|
182
|
-
"HttpSigner: server returned no signature"
|
|
183
|
-
)
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
sig.to_s
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Build a Net::HTTP client for the given URI.
|
|
190
|
-
# @param uri [URI] the target URI
|
|
191
|
-
# @return [Net::HTTP]
|
|
192
|
-
def build_http(uri)
|
|
193
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
194
|
-
http.use_ssl = uri.scheme == "https"
|
|
195
|
-
http.open_timeout = 5
|
|
196
|
-
http.read_timeout = 10
|
|
197
|
-
http
|
|
198
|
-
end
|
|
199
|
-
end
|
|
200
|
-
end
|