remitmd 0.1.7 → 0.1.8
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 +22 -2
- data/lib/remitmd/http_signer.rb +200 -0
- data/lib/remitmd/wallet.rb +19 -6
- data/lib/remitmd.rb +2 -1
- 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: 8c2b03f3cb2f1a05e9a33845448154e4d34fda42f65af31823be2f30fb14b6fe
|
|
4
|
+
data.tar.gz: 3d6dc094058152593779b5430899400d7b9d6543693ee3dd35dfd08b88f4c131
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: deca4edd8beb6ac2d798102a83838762d746f4508bf5e852bb1b43d74aed1fa24a7b328bc4880560fc5ab60f5bd2c937277123183c3c45cc430bf02407f5c5c8
|
|
7
|
+
data.tar.gz: c8b3bd0dc87584d00abe0b3acb37a52fa16f1f9af337780dd6aecc861f2cd542d2d684d636b28bef6b0227c13002c3c70388809d2e38a2874255bde0dd1c3932
|
data/README.md
CHANGED
|
@@ -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
|
|
@@ -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/wallet.rb
CHANGED
|
@@ -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
|
|
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"
|
|
@@ -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.8"
|
|
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.8
|
|
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
|