remitmd 0.1.7 → 0.1.9
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 +29 -9
- data/lib/remitmd/a2a.rb +1 -1
- data/lib/remitmd/errors.rb +2 -2
- data/lib/remitmd/http.rb +3 -3
- data/lib/remitmd/http_signer.rb +200 -0
- 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 +30 -17
- data/lib/remitmd/x402_client.rb +1 -1
- data/lib/remitmd/x402_paywall.rb +4 -4
- data/lib/remitmd.rb +3 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 05f736527efb603117342dec70fc553420aa04d4dbc3ca8db5fccd8b31b7a9cb
|
|
4
|
+
data.tar.gz: a7f143bc6302d96cc1ca492c7f8393cdd98b039e86747fee1e7bf36a919b83d0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d998125805f98cf6ccbd389ec8df96655cb66a47e51b57d274c6090cdbb00a31c713f915282f166982d85dc2d3db3ce14a9c294f7ff0c00d049252590c661f10
|
|
7
|
+
data.tar.gz: 0dad289442caafe80bcbf83b13f38f5e7d70c7adacd0ad8b6a12172e6c27a6aae872a9b97c29537fb6ee0783eb2d3261da4eb47889f677aba12a15c10bc05ab3
|
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)
|
|
@@ -24,7 +24,7 @@ gem install remitmd
|
|
|
24
24
|
```ruby
|
|
25
25
|
require "remitmd"
|
|
26
26
|
|
|
27
|
-
wallet = Remitmd::RemitWallet.new(private_key: ENV["
|
|
27
|
+
wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_KEY"])
|
|
28
28
|
|
|
29
29
|
# Direct payment
|
|
30
30
|
tx = wallet.pay("0xRecipient0000000000000000000000000000001", 1.50)
|
|
@@ -39,12 +39,32 @@ Or from environment variables:
|
|
|
39
39
|
|
|
40
40
|
```ruby
|
|
41
41
|
wallet = Remitmd::RemitWallet.from_env
|
|
42
|
-
# Requires:
|
|
42
|
+
# Requires: REMITMD_KEY (or REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)
|
|
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
|
+
## Local Signer (Recommended)
|
|
49
|
+
|
|
50
|
+
The local signer delegates key management to `remit signer`, a localhost HTTP server that holds your encrypted key. Your agent only needs a URL and token - no private key in the environment.
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export REMIT_SIGNER_URL=http://127.0.0.1:7402
|
|
54
|
+
export REMIT_SIGNER_TOKEN=rmit_sk_...
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
# Explicit
|
|
59
|
+
signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
|
|
60
|
+
wallet = Remitmd::RemitWallet.new(signer: signer)
|
|
61
|
+
|
|
62
|
+
# Or auto-detect from env (recommended)
|
|
63
|
+
wallet = Remitmd::RemitWallet.from_env # detects REMIT_SIGNER_URL automatically
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`RemitWallet.from_env` detects signer credentials automatically. Priority: `REMIT_SIGNER_URL` > `REMITMD_KEY`.
|
|
67
|
+
|
|
48
68
|
## Payment Models
|
|
49
69
|
|
|
50
70
|
### Direct Payment
|
|
@@ -73,7 +93,7 @@ contracts = wallet.get_contracts
|
|
|
73
93
|
sig = wallet.sign_tab_charge(contracts.tab, tab.id, 3_000_000, 1)
|
|
74
94
|
wallet.charge_tab(tab.id, 0.003, 0.003, 1, sig)
|
|
75
95
|
|
|
76
|
-
# Close when done
|
|
96
|
+
# Close when done - unused funds return
|
|
77
97
|
wallet.close_tab(tab.id)
|
|
78
98
|
```
|
|
79
99
|
|
|
@@ -136,9 +156,9 @@ end
|
|
|
136
156
|
|
|
137
157
|
```ruby
|
|
138
158
|
mock.was_paid?(address, amount) # true/false
|
|
139
|
-
mock.total_paid_to(address) # BigDecimal
|
|
159
|
+
mock.total_paid_to(address) # BigDecimal - sum of all payments to address
|
|
140
160
|
mock.transaction_count # Integer
|
|
141
|
-
mock.balance # BigDecimal
|
|
161
|
+
mock.balance # BigDecimal - current balance
|
|
142
162
|
mock.transactions # Array<Transaction>
|
|
143
163
|
mock.set_balance(amount) # Override starting balance
|
|
144
164
|
mock.reset # Clear all state
|
|
@@ -254,11 +274,11 @@ Remitmd::RemitWallet.new(private_key: key, chain: "base_sepolia") # Base Sepoli
|
|
|
254
274
|
Permits are auto-signed by default. If you need manual control (custom deadline, pre-signed permits, or offline signing), pass a `PermitSignature` explicitly:
|
|
255
275
|
|
|
256
276
|
```ruby
|
|
257
|
-
# sign_permit: convenience
|
|
277
|
+
# sign_permit: convenience - auto-fetches nonce, converts amount to base units
|
|
258
278
|
permit = wallet.sign_permit("0xRouterAddress...", 5.00, deadline: Time.now.to_i + 7200)
|
|
259
279
|
tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
260
280
|
|
|
261
|
-
# sign_usdc_permit: full control
|
|
281
|
+
# sign_usdc_permit: full control - raw base units, explicit nonce
|
|
262
282
|
permit = wallet.sign_usdc_permit(
|
|
263
283
|
"0xRouterAddress...", # spender
|
|
264
284
|
5_000_000, # value in base units (6 decimals)
|
|
@@ -271,6 +291,6 @@ tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
|
|
|
271
291
|
|
|
272
292
|
## License
|
|
273
293
|
|
|
274
|
-
MIT
|
|
294
|
+
MIT - see [LICENSE](LICENSE)
|
|
275
295
|
|
|
276
296
|
[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")
|
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")
|
|
@@ -0,0 +1,200 @@
|
|
|
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
|
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,18 +67,31 @@ module Remitmd
|
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
# Build a RemitWallet from environment variables.
|
|
70
|
-
# Reads:
|
|
71
|
-
#
|
|
70
|
+
# Reads: REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN (preferred, uses HttpSigner),
|
|
71
|
+
# or REMITMD_KEY (primary) / REMITMD_PRIVATE_KEY (deprecated fallback).
|
|
72
|
+
# Also reads: REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
|
|
72
73
|
def self.from_env
|
|
74
|
+
chain = ENV.fetch("REMITMD_CHAIN", "base")
|
|
75
|
+
api_url = ENV["REMITMD_API_URL"]
|
|
76
|
+
router_address = ENV["REMITMD_ROUTER_ADDRESS"]
|
|
77
|
+
|
|
78
|
+
# Priority 1: HTTP signer server
|
|
79
|
+
signer_url = ENV["REMIT_SIGNER_URL"]
|
|
80
|
+
if signer_url
|
|
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)
|
|
85
|
+
return new(signer: signer, chain: chain, api_url: api_url, router_address: router_address)
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Priority 2: raw private key
|
|
73
89
|
key = ENV["REMITMD_KEY"] || ENV["REMITMD_PRIVATE_KEY"]
|
|
74
90
|
if ENV["REMITMD_PRIVATE_KEY"] && !ENV["REMITMD_KEY"]
|
|
75
91
|
warn "[remitmd] REMITMD_PRIVATE_KEY is deprecated, use REMITMD_KEY instead"
|
|
76
92
|
end
|
|
77
|
-
raise ArgumentError, "REMITMD_KEY not set" unless key
|
|
93
|
+
raise ArgumentError, "REMITMD_KEY not set (or set REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)" unless key
|
|
78
94
|
|
|
79
|
-
chain = ENV.fetch("REMITMD_CHAIN", "base")
|
|
80
|
-
api_url = ENV["REMITMD_API_URL"]
|
|
81
|
-
router_address = ENV["REMITMD_ROUTER_ADDRESS"]
|
|
82
95
|
new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
|
|
83
96
|
end
|
|
84
97
|
|
|
@@ -144,7 +157,7 @@ module Remitmd
|
|
|
144
157
|
# @param to [String] recipient 0x-prefixed address
|
|
145
158
|
# @param amount [Numeric, BigDecimal] amount in USDC (e.g. 1.50)
|
|
146
159
|
# @param memo [String, nil] optional note
|
|
147
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
160
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
148
161
|
# @return [Transaction]
|
|
149
162
|
def pay(to, amount, memo: nil, permit: nil)
|
|
150
163
|
validate_address!(to)
|
|
@@ -163,7 +176,7 @@ module Remitmd
|
|
|
163
176
|
# @param amount [Numeric] amount in USDC
|
|
164
177
|
# @param memo [String, nil] optional note
|
|
165
178
|
# @param expires_in_secs [Integer, nil] optional expiry in seconds from now
|
|
166
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
179
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
167
180
|
# @return [Escrow]
|
|
168
181
|
def create_escrow(payee, amount, memo: nil, expires_in_secs: nil, permit: nil)
|
|
169
182
|
validate_address!(payee)
|
|
@@ -224,7 +237,7 @@ module Remitmd
|
|
|
224
237
|
# @param limit_amount [Numeric] maximum tab credit in USDC
|
|
225
238
|
# @param per_unit [Numeric] USDC per API call
|
|
226
239
|
# @param expires_in_secs [Integer] optional expiry duration in seconds (default: 86400)
|
|
227
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
240
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
228
241
|
# @return [Tab]
|
|
229
242
|
def create_tab(provider, limit_amount, per_unit = 0.0, expires_in_secs: 86_400, permit: nil)
|
|
230
243
|
validate_address!(provider)
|
|
@@ -417,7 +430,7 @@ module Remitmd
|
|
|
417
430
|
# @param payee [String] 0x-prefixed address of the stream recipient
|
|
418
431
|
# @param rate_per_second [Numeric] USDC per second
|
|
419
432
|
# @param max_total [Numeric] maximum total USDC for the stream
|
|
420
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
433
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
421
434
|
# @return [Stream]
|
|
422
435
|
def create_stream(payee, rate_per_second, max_total, permit: nil)
|
|
423
436
|
validate_address!(payee)
|
|
@@ -455,7 +468,7 @@ module Remitmd
|
|
|
455
468
|
# @param task_description [String] task description
|
|
456
469
|
# @param deadline [Integer] deadline as Unix timestamp
|
|
457
470
|
# @param max_attempts [Integer] maximum submission attempts (default: 10)
|
|
458
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
471
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
459
472
|
# @return [Bounty]
|
|
460
473
|
def create_bounty(amount, task_description, deadline, max_attempts: 10, permit: nil)
|
|
461
474
|
validate_amount!(amount)
|
|
@@ -509,7 +522,7 @@ module Remitmd
|
|
|
509
522
|
# @param provider [String] 0x-prefixed provider address
|
|
510
523
|
# @param amount [Numeric] amount in USDC
|
|
511
524
|
# @param expires_in_secs [Integer] expiry duration in seconds (default: 3600)
|
|
512
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
525
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
513
526
|
# @return [Deposit]
|
|
514
527
|
def place_deposit(provider, amount, expires_in_secs: 3600, permit: nil)
|
|
515
528
|
validate_address!(provider)
|
|
@@ -543,7 +556,7 @@ module Remitmd
|
|
|
543
556
|
# Propose a payment intent for counterpart approval before execution.
|
|
544
557
|
# @param to [String] 0x-prefixed address
|
|
545
558
|
# @param amount [Numeric] amount in USDC
|
|
546
|
-
# @param type [String] payment type
|
|
559
|
+
# @param type [String] payment type - "direct", "escrow", "tab"
|
|
547
560
|
# @return [Intent]
|
|
548
561
|
def propose_intent(to, amount, type: "direct")
|
|
549
562
|
validate_address!(to)
|
|
@@ -570,7 +583,7 @@ module Remitmd
|
|
|
570
583
|
# Generate a one-time URL for the operator to fund this wallet.
|
|
571
584
|
# @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
|
|
572
585
|
# @param agent_name [String, nil] agent display name shown on the funding page
|
|
573
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
586
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
574
587
|
# @return [LinkResponse]
|
|
575
588
|
def create_fund_link(messages: nil, agent_name: nil, permit: nil)
|
|
576
589
|
body = {}
|
|
@@ -588,7 +601,7 @@ module Remitmd
|
|
|
588
601
|
# Generate a one-time URL for the operator to withdraw funds.
|
|
589
602
|
# @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
|
|
590
603
|
# @param agent_name [String, nil] agent display name shown on the withdraw page
|
|
591
|
-
# @param permit [PermitSignature, nil] EIP-2612 permit
|
|
604
|
+
# @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
|
|
592
605
|
# @return [LinkResponse]
|
|
593
606
|
def create_withdraw_link(messages: nil, agent_name: nil, permit: nil)
|
|
594
607
|
body = {}
|
|
@@ -697,7 +710,7 @@ module Remitmd
|
|
|
697
710
|
|
|
698
711
|
# Auto-sign a permit for the given contract type and amount.
|
|
699
712
|
# Returns nil on failure instead of raising, so callers can proceed without a permit.
|
|
700
|
-
# @param contract [String] contract key
|
|
713
|
+
# @param contract [String] contract key - "router", "escrow", "tab", etc.
|
|
701
714
|
# @param amount [Numeric] amount in USDC
|
|
702
715
|
# @return [PermitSignature, nil]
|
|
703
716
|
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,6 +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/http_signer"
|
|
7
8
|
require_relative "remitmd/http"
|
|
8
9
|
require_relative "remitmd/wallet"
|
|
9
10
|
require_relative "remitmd/mock"
|
|
@@ -11,7 +12,7 @@ require_relative "remitmd/a2a"
|
|
|
11
12
|
require_relative "remitmd/x402_client"
|
|
12
13
|
require_relative "remitmd/x402_paywall"
|
|
13
14
|
|
|
14
|
-
# remit.md Ruby SDK
|
|
15
|
+
# remit.md Ruby SDK - universal payment protocol for AI agents.
|
|
15
16
|
#
|
|
16
17
|
# @example Direct payment
|
|
17
18
|
# wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_PRIVATE_KEY"])
|
|
@@ -25,5 +26,5 @@ require_relative "remitmd/x402_paywall"
|
|
|
25
26
|
# mock.was_paid?("0x0000000000000000000000000000000000000001", 1.00) # => true
|
|
26
27
|
#
|
|
27
28
|
module Remitmd
|
|
28
|
-
VERSION = "0.1.
|
|
29
|
+
VERSION = "0.1.9"
|
|
29
30
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: remitmd
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.9
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- remit.md
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-03-
|
|
11
|
+
date: 2026-03-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rspec
|
|
@@ -53,6 +53,7 @@ files:
|
|
|
53
53
|
- lib/remitmd/a2a.rb
|
|
54
54
|
- lib/remitmd/errors.rb
|
|
55
55
|
- lib/remitmd/http.rb
|
|
56
|
+
- lib/remitmd/http_signer.rb
|
|
56
57
|
- lib/remitmd/keccak.rb
|
|
57
58
|
- lib/remitmd/mock.rb
|
|
58
59
|
- lib/remitmd/models.rb
|