remitmd 0.1.5 → 0.1.7
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 +3 -14
- data/lib/remitmd/a2a.rb +188 -2
- data/lib/remitmd/errors.rb +52 -17
- data/lib/remitmd/http.rb +16 -8
- data/lib/remitmd/keccak.rb +1 -1
- data/lib/remitmd/mock.rb +8 -7
- data/lib/remitmd/models.rb +88 -58
- data/lib/remitmd/wallet.rb +92 -85
- data/lib/remitmd/x402_client.rb +201 -0
- data/lib/remitmd/x402_paywall.rb +170 -0
- data/lib/remitmd.rb +4 -1
- metadata +4 -2
data/lib/remitmd/models.rb
CHANGED
|
@@ -16,35 +16,40 @@ module Remitmd
|
|
|
16
16
|
module EscrowStatus
|
|
17
17
|
PENDING = "pending"
|
|
18
18
|
FUNDED = "funded"
|
|
19
|
-
|
|
19
|
+
ACTIVE = "active"
|
|
20
|
+
COMPLETED = "completed"
|
|
20
21
|
CANCELLED = "cancelled"
|
|
21
|
-
|
|
22
|
+
FAILED = "failed"
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
module TabStatus
|
|
25
|
-
OPEN
|
|
26
|
-
CLOSED
|
|
27
|
-
|
|
26
|
+
OPEN = "open"
|
|
27
|
+
CLOSED = "closed"
|
|
28
|
+
EXPIRED = "expired"
|
|
29
|
+
SUSPENDED = "suspended"
|
|
28
30
|
end
|
|
29
31
|
|
|
30
32
|
module StreamStatus
|
|
31
33
|
ACTIVE = "active"
|
|
34
|
+
CLOSED = "closed"
|
|
35
|
+
COMPLETED = "completed"
|
|
32
36
|
PAUSED = "paused"
|
|
33
|
-
ENDED = "ended"
|
|
34
37
|
CANCELLED = "cancelled"
|
|
35
38
|
end
|
|
36
39
|
|
|
37
40
|
module BountyStatus
|
|
38
41
|
OPEN = "open"
|
|
42
|
+
CLOSED = "closed"
|
|
39
43
|
AWARDED = "awarded"
|
|
40
44
|
EXPIRED = "expired"
|
|
41
|
-
|
|
45
|
+
CANCELLED = "cancelled"
|
|
42
46
|
end
|
|
43
47
|
|
|
44
48
|
module DepositStatus
|
|
45
49
|
LOCKED = "locked"
|
|
46
50
|
RETURNED = "returned"
|
|
47
51
|
FORFEITED = "forfeited"
|
|
52
|
+
EXPIRED = "expired"
|
|
48
53
|
end
|
|
49
54
|
|
|
50
55
|
# ─── Permit & Contract Addresses ─────────────────────────────────────────
|
|
@@ -92,12 +97,14 @@ module Remitmd
|
|
|
92
97
|
def decimal(value)
|
|
93
98
|
return value if value.is_a?(BigDecimal)
|
|
94
99
|
return BigDecimal(value.to_s) if value
|
|
100
|
+
|
|
95
101
|
nil
|
|
96
102
|
end
|
|
97
103
|
|
|
98
104
|
def parse_time(value)
|
|
99
105
|
return value if value.is_a?(Time)
|
|
100
106
|
return Time.parse(value) if value.is_a?(String)
|
|
107
|
+
|
|
101
108
|
nil
|
|
102
109
|
end
|
|
103
110
|
end
|
|
@@ -116,12 +123,11 @@ module Remitmd
|
|
|
116
123
|
@deposit = h["deposit"]
|
|
117
124
|
@fee_calculator = h["fee_calculator"]
|
|
118
125
|
@key_registry = h["key_registry"]
|
|
119
|
-
@arbitration = h["arbitration"]
|
|
120
126
|
@relayer = h["relayer"]
|
|
121
127
|
end
|
|
122
128
|
|
|
123
129
|
attr_reader :chain_id, :usdc, :router, :escrow, :tab, :stream,
|
|
124
|
-
:bounty, :deposit, :fee_calculator, :key_registry,
|
|
130
|
+
:bounty, :deposit, :fee_calculator, :key_registry,
|
|
125
131
|
:relayer
|
|
126
132
|
end
|
|
127
133
|
|
|
@@ -136,7 +142,7 @@ module Remitmd
|
|
|
136
142
|
@fee = decimal(h["fee"] || "0")
|
|
137
143
|
@memo = h["memo"] || ""
|
|
138
144
|
@chain_id = h["chain_id"]&.to_i
|
|
139
|
-
@block_number = h["block_number"]
|
|
145
|
+
@block_number = h["block_number"].to_i
|
|
140
146
|
@created_at = parse_time(h["created_at"])
|
|
141
147
|
end
|
|
142
148
|
|
|
@@ -164,10 +170,10 @@ module Remitmd
|
|
|
164
170
|
def initialize(attrs)
|
|
165
171
|
h = attrs.transform_keys(&:to_s)
|
|
166
172
|
@address = h["address"]
|
|
167
|
-
@score = h["score"]
|
|
173
|
+
@score = h["score"].to_i
|
|
168
174
|
@total_paid = decimal(h["total_paid"])
|
|
169
175
|
@total_received = decimal(h["total_received"])
|
|
170
|
-
@transaction_count = h["transaction_count"]
|
|
176
|
+
@transaction_count = h["transaction_count"].to_i
|
|
171
177
|
@member_since = parse_time(h["member_since"])
|
|
172
178
|
end
|
|
173
179
|
|
|
@@ -203,21 +209,24 @@ module Remitmd
|
|
|
203
209
|
def initialize(attrs)
|
|
204
210
|
h = attrs.transform_keys(&:to_s)
|
|
205
211
|
@id = h["id"]
|
|
206
|
-
@
|
|
207
|
-
@
|
|
212
|
+
@payer = h["payer"] || h["opener"]
|
|
213
|
+
@payee = h["payee"] || h["provider"] || h["counterpart"]
|
|
208
214
|
@limit = decimal(h["limit_amount"] || h["limit"])
|
|
209
|
-
@
|
|
215
|
+
@spent = decimal(h["spent"] || h["used"] || "0")
|
|
210
216
|
@remaining = decimal(h["remaining"] || h["limit_amount"] || h["limit"])
|
|
211
217
|
@status = h["status"]
|
|
212
218
|
@created_at = parse_time(h["created_at"])
|
|
213
219
|
@closes_at = parse_time(h["closes_at"])
|
|
214
220
|
end
|
|
215
221
|
|
|
216
|
-
attr_reader :id, :
|
|
222
|
+
attr_reader :id, :payer, :payee, :limit, :spent, :remaining,
|
|
217
223
|
:status, :created_at, :closes_at
|
|
218
224
|
|
|
219
|
-
# Backward compatibility
|
|
220
|
-
alias
|
|
225
|
+
# Backward compatibility aliases
|
|
226
|
+
alias opener payer
|
|
227
|
+
alias provider payee
|
|
228
|
+
alias counterpart payee
|
|
229
|
+
alias used spent
|
|
221
230
|
|
|
222
231
|
private :decimal, :parse_time
|
|
223
232
|
end
|
|
@@ -228,9 +237,9 @@ module Remitmd
|
|
|
228
237
|
@tab_id = h["tab_id"]
|
|
229
238
|
@amount = decimal(h["amount"])
|
|
230
239
|
@cumulative = decimal(h["cumulative"])
|
|
231
|
-
@call_count = h["call_count"]
|
|
240
|
+
@call_count = h["call_count"].to_i
|
|
232
241
|
@memo = h["memo"] || ""
|
|
233
|
-
@sequence = h["sequence"]
|
|
242
|
+
@sequence = h["sequence"].to_i
|
|
234
243
|
@signature = h["signature"]
|
|
235
244
|
end
|
|
236
245
|
|
|
@@ -242,19 +251,29 @@ module Remitmd
|
|
|
242
251
|
class Stream < Model
|
|
243
252
|
def initialize(attrs)
|
|
244
253
|
h = attrs.transform_keys(&:to_s)
|
|
245
|
-
@id
|
|
246
|
-
@
|
|
247
|
-
@
|
|
248
|
-
@
|
|
249
|
-
@deposited
|
|
250
|
-
@
|
|
251
|
-
@
|
|
252
|
-
@
|
|
253
|
-
@
|
|
254
|
+
@id = h["id"]
|
|
255
|
+
@payer = h["payer"] || h["sender"]
|
|
256
|
+
@payee = h["payee"] || h["recipient"]
|
|
257
|
+
@rate_per_second = decimal(h["rate_per_second"] || h["rate_per_sec"])
|
|
258
|
+
@deposited = decimal(h["deposited"])
|
|
259
|
+
@total_streamed = decimal(h["total_streamed"] || h["withdrawn"] || "0")
|
|
260
|
+
@max_duration = h["max_duration"]
|
|
261
|
+
@max_total = decimal(h["max_total"])
|
|
262
|
+
@status = h["status"]
|
|
263
|
+
@started_at = parse_time(h["started_at"])
|
|
264
|
+
@ends_at = parse_time(h["ends_at"])
|
|
265
|
+
@closed_at = parse_time(h["closed_at"])
|
|
254
266
|
end
|
|
255
267
|
|
|
256
|
-
attr_reader :id, :
|
|
257
|
-
:
|
|
268
|
+
attr_reader :id, :payer, :payee, :rate_per_second, :deposited,
|
|
269
|
+
:total_streamed, :max_duration, :max_total,
|
|
270
|
+
:status, :started_at, :ends_at, :closed_at
|
|
271
|
+
|
|
272
|
+
# Backward compatibility aliases
|
|
273
|
+
alias sender payer
|
|
274
|
+
alias recipient payee
|
|
275
|
+
alias rate_per_sec rate_per_second
|
|
276
|
+
alias withdrawn total_streamed
|
|
258
277
|
|
|
259
278
|
private :decimal, :parse_time
|
|
260
279
|
end
|
|
@@ -262,22 +281,27 @@ module Remitmd
|
|
|
262
281
|
class Bounty < Model
|
|
263
282
|
def initialize(attrs)
|
|
264
283
|
h = attrs.transform_keys(&:to_s)
|
|
265
|
-
@id
|
|
266
|
-
@poster
|
|
267
|
-
@amount
|
|
268
|
-
@
|
|
269
|
-
@
|
|
270
|
-
@
|
|
271
|
-
@
|
|
272
|
-
@
|
|
284
|
+
@id = h["id"]
|
|
285
|
+
@poster = h["poster"]
|
|
286
|
+
@amount = decimal(h["amount"] || h["award"])
|
|
287
|
+
@task = h["task"] || h["task_description"] || h["description"]
|
|
288
|
+
@submissions = h["submissions"] || []
|
|
289
|
+
@validation = h["validation"]
|
|
290
|
+
@max_attempts = h["max_attempts"]
|
|
291
|
+
@deadline = h["deadline"]
|
|
292
|
+
@status = h["status"]
|
|
293
|
+
@winner = h["winner"] || ""
|
|
294
|
+
@expires_at = parse_time(h["expires_at"])
|
|
295
|
+
@created_at = parse_time(h["created_at"])
|
|
273
296
|
end
|
|
274
297
|
|
|
275
|
-
attr_reader :id, :poster, :amount, :
|
|
276
|
-
:winner, :expires_at, :created_at
|
|
298
|
+
attr_reader :id, :poster, :amount, :task, :submissions, :validation,
|
|
299
|
+
:max_attempts, :deadline, :status, :winner, :expires_at, :created_at
|
|
277
300
|
|
|
278
301
|
# Backward compatibility aliases
|
|
279
302
|
alias award amount
|
|
280
|
-
alias
|
|
303
|
+
alias task_description task
|
|
304
|
+
alias description task
|
|
281
305
|
|
|
282
306
|
private :decimal, :parse_time
|
|
283
307
|
end
|
|
@@ -285,15 +309,19 @@ module Remitmd
|
|
|
285
309
|
class BountySubmission < Model
|
|
286
310
|
def initialize(attrs)
|
|
287
311
|
h = attrs.transform_keys(&:to_s)
|
|
288
|
-
@id
|
|
289
|
-
@bounty_id
|
|
290
|
-
@submitter
|
|
291
|
-
@
|
|
292
|
-
@
|
|
293
|
-
@created_at
|
|
312
|
+
@id = h["id"]
|
|
313
|
+
@bounty_id = h["bounty_id"]
|
|
314
|
+
@submitter = h["submitter"]
|
|
315
|
+
@evidence_uri = h["evidence_uri"] || h["evidence_hash"]
|
|
316
|
+
@accepted = h.key?("accepted") ? h["accepted"] : h["status"]
|
|
317
|
+
@created_at = parse_time(h["created_at"])
|
|
294
318
|
end
|
|
295
319
|
|
|
296
|
-
attr_reader :id, :bounty_id, :submitter, :
|
|
320
|
+
attr_reader :id, :bounty_id, :submitter, :evidence_uri, :accepted, :created_at
|
|
321
|
+
|
|
322
|
+
# Backward compatibility aliases
|
|
323
|
+
alias evidence_hash evidence_uri
|
|
324
|
+
alias status accepted
|
|
297
325
|
|
|
298
326
|
private :parse_time
|
|
299
327
|
end
|
|
@@ -302,19 +330,21 @@ module Remitmd
|
|
|
302
330
|
def initialize(attrs)
|
|
303
331
|
h = attrs.transform_keys(&:to_s)
|
|
304
332
|
@id = h["id"]
|
|
305
|
-
@
|
|
306
|
-
@
|
|
333
|
+
@payer = h["payer"] || h["depositor"]
|
|
334
|
+
@payee = h["payee"] || h["provider"] || h["beneficiary"]
|
|
307
335
|
@amount = decimal(h["amount"])
|
|
308
336
|
@status = h["status"]
|
|
309
337
|
@expires_at = parse_time(h["expires_at"])
|
|
310
338
|
@created_at = parse_time(h["created_at"])
|
|
311
339
|
end
|
|
312
340
|
|
|
313
|
-
attr_reader :id, :
|
|
341
|
+
attr_reader :id, :payer, :payee, :amount,
|
|
314
342
|
:status, :expires_at, :created_at
|
|
315
343
|
|
|
316
|
-
# Backward compatibility
|
|
317
|
-
alias
|
|
344
|
+
# Backward compatibility aliases
|
|
345
|
+
alias depositor payer
|
|
346
|
+
alias provider payee
|
|
347
|
+
alias beneficiary payee
|
|
318
348
|
|
|
319
349
|
private :decimal, :parse_time
|
|
320
350
|
end
|
|
@@ -361,7 +391,7 @@ module Remitmd
|
|
|
361
391
|
@period = h["period"]
|
|
362
392
|
@total_spent = decimal(h["total_spent"])
|
|
363
393
|
@total_fees = decimal(h["total_fees"])
|
|
364
|
-
@tx_count = h["tx_count"]
|
|
394
|
+
@tx_count = h["tx_count"].to_i
|
|
365
395
|
@top_recipients = h["top_recipients"] || []
|
|
366
396
|
end
|
|
367
397
|
|
|
@@ -407,9 +437,9 @@ module Remitmd
|
|
|
407
437
|
h = attrs.transform_keys(&:to_s)
|
|
408
438
|
items_raw = h["items"] || []
|
|
409
439
|
@items = items_raw.map { |tx| Transaction.new(tx) }
|
|
410
|
-
@total = h["total"]
|
|
411
|
-
@page = h["page"]
|
|
412
|
-
@per_page = h["per_page"]
|
|
440
|
+
@total = h["total"].to_i
|
|
441
|
+
@page = (h["page"] || 1).to_i
|
|
442
|
+
@per_page = (h["per_page"] || 50).to_i
|
|
413
443
|
@has_more = h["has_more"] == true
|
|
414
444
|
end
|
|
415
445
|
|
data/lib/remitmd/wallet.rb
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "bigdecimal"
|
|
4
4
|
require "bigdecimal/util"
|
|
5
|
-
require "net/http"
|
|
6
|
-
require "uri"
|
|
7
5
|
require "json"
|
|
8
6
|
|
|
9
7
|
module Remitmd
|
|
@@ -14,13 +12,6 @@ module Remitmd
|
|
|
14
12
|
"localhost" => "0x5FbDB2315678afecb367f032d93F642f64180aa3",
|
|
15
13
|
}.freeze
|
|
16
14
|
|
|
17
|
-
# Default JSON-RPC endpoints per chain (for nonce fetching).
|
|
18
|
-
DEFAULT_RPC_URLS = {
|
|
19
|
-
"base-sepolia" => "https://sepolia.base.org",
|
|
20
|
-
"base" => "https://mainnet.base.org",
|
|
21
|
-
"localhost" => "http://127.0.0.1:8545",
|
|
22
|
-
}.freeze
|
|
23
|
-
|
|
24
15
|
# Primary remit.md client. All payment operations are methods on RemitWallet.
|
|
25
16
|
#
|
|
26
17
|
# @example Quickstart
|
|
@@ -39,16 +30,14 @@ module Remitmd
|
|
|
39
30
|
# @param signer [Signer, nil] custom signer (pass instead of private_key)
|
|
40
31
|
# @param chain [String] chain name — "base", "base_sepolia"
|
|
41
32
|
# @param api_url [String, nil] override API base URL
|
|
42
|
-
# @param rpc_url [String, nil] override JSON-RPC URL for on-chain queries
|
|
43
33
|
# @param transport [Object, nil] inject mock transport (used by MockRemit)
|
|
44
|
-
def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil,
|
|
34
|
+
def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil, transport: nil)
|
|
45
35
|
if transport
|
|
46
36
|
# MockRemit path: transport + signer injected directly
|
|
47
37
|
@signer = signer
|
|
48
38
|
@transport = transport
|
|
49
39
|
@chain_key = "base-sepolia"
|
|
50
40
|
@chain_id = ChainId::BASE_SEPOLIA
|
|
51
|
-
@rpc_url = DEFAULT_RPC_URLS["base-sepolia"]
|
|
52
41
|
@mock_mode = true
|
|
53
42
|
return
|
|
54
43
|
end
|
|
@@ -61,18 +50,14 @@ module Remitmd
|
|
|
61
50
|
end
|
|
62
51
|
|
|
63
52
|
@signer = signer || PrivateKeySigner.new(private_key)
|
|
64
|
-
# Normalize chain key
|
|
53
|
+
# Normalize chain key (underscore → hyphen). Full chain name is sent in API bodies.
|
|
65
54
|
@chain_key = chain.tr("_", "-")
|
|
66
|
-
|
|
67
|
-
# The server accepts "base" — not "base_sepolia" etc.
|
|
68
|
-
@chain = chain.sub(/_sepolia\z/, "").sub(/-sepolia\z/, "")
|
|
69
|
-
cfg = CHAIN_CONFIG.fetch(chain) do
|
|
55
|
+
cfg = CHAIN_CONFIG.fetch(chain) do
|
|
70
56
|
raise ArgumentError, "Unknown chain: #{chain}. Valid: #{CHAIN_CONFIG.keys.join(", ")}"
|
|
71
57
|
end
|
|
72
58
|
base_url = api_url || cfg[:url]
|
|
73
59
|
@chain_id = cfg[:chain_id]
|
|
74
60
|
router_address ||= ""
|
|
75
|
-
@rpc_url = rpc_url || ENV["REMITMD_RPC_URL"] || DEFAULT_RPC_URLS[@chain_key] || DEFAULT_RPC_URLS["base-sepolia"]
|
|
76
61
|
@transport = HttpTransport.new(
|
|
77
62
|
base_url: base_url,
|
|
78
63
|
signer: @signer,
|
|
@@ -82,14 +67,19 @@ module Remitmd
|
|
|
82
67
|
end
|
|
83
68
|
|
|
84
69
|
# Build a RemitWallet from environment variables.
|
|
85
|
-
# Reads: REMITMD_PRIVATE_KEY
|
|
70
|
+
# Reads: REMITMD_KEY (primary) or REMITMD_PRIVATE_KEY (deprecated fallback),
|
|
71
|
+
# REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
|
|
86
72
|
def self.from_env
|
|
87
|
-
key
|
|
73
|
+
key = ENV["REMITMD_KEY"] || ENV["REMITMD_PRIVATE_KEY"]
|
|
74
|
+
if ENV["REMITMD_PRIVATE_KEY"] && !ENV["REMITMD_KEY"]
|
|
75
|
+
warn "[remitmd] REMITMD_PRIVATE_KEY is deprecated, use REMITMD_KEY instead"
|
|
76
|
+
end
|
|
77
|
+
raise ArgumentError, "REMITMD_KEY not set" unless key
|
|
78
|
+
|
|
88
79
|
chain = ENV.fetch("REMITMD_CHAIN", "base")
|
|
89
80
|
api_url = ENV["REMITMD_API_URL"]
|
|
90
81
|
router_address = ENV["REMITMD_ROUTER_ADDRESS"]
|
|
91
|
-
|
|
92
|
-
new(private_key: key, chain: chain, api_url: api_url, router_address: router_address, rpc_url: rpc_url)
|
|
82
|
+
new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
|
|
93
83
|
end
|
|
94
84
|
|
|
95
85
|
# The Ethereum address associated with this wallet.
|
|
@@ -108,7 +98,7 @@ module Remitmd
|
|
|
108
98
|
# Get deployed contract addresses. Cached for the lifetime of this client.
|
|
109
99
|
# @return [ContractAddresses]
|
|
110
100
|
def get_contracts
|
|
111
|
-
@
|
|
101
|
+
@get_contracts ||= ContractAddresses.new(@transport.get("/contracts"))
|
|
112
102
|
end
|
|
113
103
|
|
|
114
104
|
# ─── Balance & Analytics ─────────────────────────────────────────────────
|
|
@@ -161,8 +151,8 @@ module Remitmd
|
|
|
161
151
|
validate_amount!(amount)
|
|
162
152
|
resolved = permit || auto_permit("router", amount.to_f)
|
|
163
153
|
nonce = SecureRandom.hex(16)
|
|
164
|
-
body = { to: to, amount: amount.
|
|
165
|
-
body[:permit] = resolved
|
|
154
|
+
body = { to: to, amount: amount.to_f, task: memo || "", chain: @chain_key, nonce: nonce, signature: "0x" }
|
|
155
|
+
body[:permit] = resolved&.to_h
|
|
166
156
|
Transaction.new(@transport.post("/payments/direct", body))
|
|
167
157
|
end
|
|
168
158
|
|
|
@@ -184,16 +174,16 @@ module Remitmd
|
|
|
184
174
|
invoice_id = SecureRandom.hex(16)
|
|
185
175
|
nonce = SecureRandom.hex(16)
|
|
186
176
|
inv_body = {
|
|
187
|
-
id: invoice_id, chain: @
|
|
177
|
+
id: invoice_id, chain: @chain_key,
|
|
188
178
|
from_agent: address.downcase, to_agent: payee.downcase,
|
|
189
|
-
amount: amount.
|
|
179
|
+
amount: amount.to_f, type: "escrow",
|
|
190
180
|
task: memo || "", nonce: nonce, signature: "0x"
|
|
191
181
|
}
|
|
192
182
|
inv_body[:escrow_timeout] = expires_in_secs if expires_in_secs
|
|
193
183
|
@transport.post("/invoices", inv_body)
|
|
194
184
|
|
|
195
185
|
# Step 2: fund the escrow.
|
|
196
|
-
esc_body = { invoice_id: invoice_id, permit: resolved
|
|
186
|
+
esc_body = { invoice_id: invoice_id, permit: resolved&.to_h }
|
|
197
187
|
Escrow.new(@transport.post("/escrows", esc_body))
|
|
198
188
|
end
|
|
199
189
|
|
|
@@ -241,12 +231,12 @@ module Remitmd
|
|
|
241
231
|
validate_amount!(limit_amount)
|
|
242
232
|
resolved = permit || auto_permit("tab", limit_amount.to_f)
|
|
243
233
|
body = {
|
|
244
|
-
chain: @
|
|
234
|
+
chain: @chain_key,
|
|
245
235
|
provider: provider,
|
|
246
236
|
limit_amount: limit_amount.to_f,
|
|
247
237
|
per_unit: per_unit.to_f,
|
|
248
238
|
expiry: Time.now.to_i + expires_in_secs,
|
|
249
|
-
permit: resolved
|
|
239
|
+
permit: resolved&.to_h
|
|
250
240
|
}
|
|
251
241
|
Tab.new(@transport.post("/tabs", body))
|
|
252
242
|
end
|
|
@@ -285,9 +275,10 @@ module Remitmd
|
|
|
285
275
|
# @param provider_sig [String, nil] EIP-712 signature from the provider
|
|
286
276
|
# @return [Tab]
|
|
287
277
|
def close_tab(tab_id, final_amount: nil, provider_sig: nil)
|
|
288
|
-
body = {
|
|
289
|
-
|
|
290
|
-
|
|
278
|
+
body = {
|
|
279
|
+
final_amount: final_amount ? final_amount.to_f : 0,
|
|
280
|
+
provider_sig: provider_sig || "0x"
|
|
281
|
+
}
|
|
291
282
|
Tab.new(@transport.post("/tabs/#{tab_id}/close", body))
|
|
292
283
|
end
|
|
293
284
|
|
|
@@ -350,7 +341,15 @@ module Remitmd
|
|
|
350
341
|
# @param usdc_address [String, nil] override the USDC contract address
|
|
351
342
|
# @return [PermitSignature]
|
|
352
343
|
def sign_usdc_permit(spender, value, deadline, nonce = 0, usdc_address: nil)
|
|
353
|
-
usdc_addr = usdc_address || USDC_ADDRESSES[@chain_key]
|
|
344
|
+
usdc_addr = usdc_address || USDC_ADDRESSES[@chain_key]
|
|
345
|
+
if usdc_addr.nil? || usdc_addr.empty?
|
|
346
|
+
raise RemitError.new(
|
|
347
|
+
RemitError::INVALID_ADDRESS,
|
|
348
|
+
"No USDC address configured for chain #{@chain_key.inspect}. " \
|
|
349
|
+
"Valid chains: #{USDC_ADDRESSES.keys.join(", ")}",
|
|
350
|
+
context: { chain: @chain_key }
|
|
351
|
+
)
|
|
352
|
+
end
|
|
354
353
|
chain_id = @chain_id || ChainId::BASE_SEPOLIA
|
|
355
354
|
|
|
356
355
|
# Domain separator for USDC (EIP-2612)
|
|
@@ -397,8 +396,16 @@ module Remitmd
|
|
|
397
396
|
# @param deadline [Integer, nil] optional Unix timestamp; defaults to 1 hour from now
|
|
398
397
|
# @return [PermitSignature]
|
|
399
398
|
def sign_permit(spender, amount, deadline: nil)
|
|
400
|
-
usdc_addr = USDC_ADDRESSES[@chain_key]
|
|
401
|
-
|
|
399
|
+
usdc_addr = USDC_ADDRESSES[@chain_key]
|
|
400
|
+
if usdc_addr.nil? || usdc_addr.empty?
|
|
401
|
+
raise RemitError.new(
|
|
402
|
+
RemitError::INVALID_ADDRESS,
|
|
403
|
+
"No USDC address configured for chain #{@chain_key.inspect}. " \
|
|
404
|
+
"Valid chains: #{USDC_ADDRESSES.keys.join(", ")}",
|
|
405
|
+
context: { chain: @chain_key }
|
|
406
|
+
)
|
|
407
|
+
end
|
|
408
|
+
nonce = fetch_permit_nonce(usdc_addr)
|
|
402
409
|
dl = deadline || (Time.now.to_i + 3600)
|
|
403
410
|
raw = (amount * 1_000_000).round.to_i
|
|
404
411
|
sign_usdc_permit(spender, raw, dl, nonce, usdc_address: usdc_addr)
|
|
@@ -418,11 +425,11 @@ module Remitmd
|
|
|
418
425
|
validate_amount!(max_total)
|
|
419
426
|
resolved = permit || auto_permit("stream", max_total.to_f)
|
|
420
427
|
body = {
|
|
421
|
-
chain: @
|
|
428
|
+
chain: @chain_key,
|
|
422
429
|
payee: payee,
|
|
423
430
|
rate_per_second: rate_per_second.to_s,
|
|
424
431
|
max_total: max_total.to_s,
|
|
425
|
-
permit: resolved
|
|
432
|
+
permit: resolved&.to_h
|
|
426
433
|
}
|
|
427
434
|
Stream.new(@transport.post("/streams", body))
|
|
428
435
|
end
|
|
@@ -454,12 +461,12 @@ module Remitmd
|
|
|
454
461
|
validate_amount!(amount)
|
|
455
462
|
resolved = permit || auto_permit("bounty", amount.to_f)
|
|
456
463
|
body = {
|
|
457
|
-
chain: @
|
|
464
|
+
chain: @chain_key,
|
|
458
465
|
amount: amount.to_f,
|
|
459
466
|
task_description: task_description,
|
|
460
467
|
deadline: deadline,
|
|
461
468
|
max_attempts: max_attempts,
|
|
462
|
-
permit: resolved
|
|
469
|
+
permit: resolved&.to_h
|
|
463
470
|
}
|
|
464
471
|
Bounty.new(@transport.post("/bounties", body))
|
|
465
472
|
end
|
|
@@ -509,11 +516,11 @@ module Remitmd
|
|
|
509
516
|
validate_amount!(amount)
|
|
510
517
|
resolved = permit || auto_permit("deposit", amount.to_f)
|
|
511
518
|
body = {
|
|
512
|
-
chain: @
|
|
519
|
+
chain: @chain_key,
|
|
513
520
|
provider: provider,
|
|
514
521
|
amount: amount.to_f,
|
|
515
522
|
expiry: Time.now.to_i + expires_in_secs,
|
|
516
|
-
permit: resolved
|
|
523
|
+
permit: resolved&.to_h
|
|
517
524
|
}
|
|
518
525
|
Deposit.new(@transport.post("/deposits", body))
|
|
519
526
|
end
|
|
@@ -554,7 +561,7 @@ module Remitmd
|
|
|
554
561
|
# @return [Webhook]
|
|
555
562
|
def register_webhook(url, events, chains: nil)
|
|
556
563
|
body = { url: url, events: events }
|
|
557
|
-
body[:chains] = chains
|
|
564
|
+
body[:chains] = chains || [@chain_key]
|
|
558
565
|
Webhook.new(@transport.post("/webhooks", body))
|
|
559
566
|
end
|
|
560
567
|
|
|
@@ -571,9 +578,9 @@ module Remitmd
|
|
|
571
578
|
body[:agent_name] = agent_name if agent_name
|
|
572
579
|
begin
|
|
573
580
|
resolved = permit || auto_permit("relayer", 999_999_999.0)
|
|
574
|
-
body[:permit] = resolved
|
|
575
|
-
rescue StandardError
|
|
576
|
-
|
|
581
|
+
body[:permit] = resolved&.to_h
|
|
582
|
+
rescue StandardError => e
|
|
583
|
+
warn "[remitmd] create_fund_link: auto-permit failed: #{e.message}"
|
|
577
584
|
end
|
|
578
585
|
LinkResponse.new(@transport.post("/links/fund", body))
|
|
579
586
|
end
|
|
@@ -589,9 +596,9 @@ module Remitmd
|
|
|
589
596
|
body[:agent_name] = agent_name if agent_name
|
|
590
597
|
begin
|
|
591
598
|
resolved = permit || auto_permit("relayer", 999_999_999.0)
|
|
592
|
-
body[:permit] = resolved
|
|
593
|
-
rescue StandardError
|
|
594
|
-
|
|
599
|
+
body[:permit] = resolved&.to_h
|
|
600
|
+
rescue StandardError => e
|
|
601
|
+
warn "[remitmd] create_withdraw_link: auto-permit failed: #{e.message}"
|
|
595
602
|
end
|
|
596
603
|
LinkResponse.new(@transport.post("/links/withdraw", body))
|
|
597
604
|
end
|
|
@@ -599,10 +606,25 @@ module Remitmd
|
|
|
599
606
|
# ─── Testnet ──────────────────────────────────────────────────────────────
|
|
600
607
|
|
|
601
608
|
# Mint testnet USDC. Max $2,500 per call, once per hour per wallet.
|
|
609
|
+
# Uses unauthenticated HTTP (no EIP-712 auth headers).
|
|
602
610
|
# @param amount [Numeric] amount in USDC
|
|
603
611
|
# @return [Hash] { "tx_hash" => "0x...", "balance" => 1234.56 }
|
|
604
612
|
def mint(amount)
|
|
605
|
-
@
|
|
613
|
+
if @mock_mode
|
|
614
|
+
@transport.post("/mint", { wallet: address, amount: amount })
|
|
615
|
+
else
|
|
616
|
+
cfg = CHAIN_CONFIG[@chain_key]
|
|
617
|
+
base_url = cfg ? cfg[:url] : "https://testnet.remit.md/api/v1"
|
|
618
|
+
uri = URI("#{base_url}/mint")
|
|
619
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
620
|
+
http.use_ssl = uri.scheme == "https"
|
|
621
|
+
http.read_timeout = 15
|
|
622
|
+
req = Net::HTTP::Post.new(uri.path)
|
|
623
|
+
req["Content-Type"] = "application/json"
|
|
624
|
+
req.body = { wallet: address, amount: amount }.to_json
|
|
625
|
+
resp = http.request(req)
|
|
626
|
+
JSON.parse(resp.body.to_s)
|
|
627
|
+
end
|
|
606
628
|
end
|
|
607
629
|
|
|
608
630
|
private
|
|
@@ -611,6 +633,7 @@ module Remitmd
|
|
|
611
633
|
|
|
612
634
|
def validate_address!(addr)
|
|
613
635
|
return if addr.match?(ADDRESS_RE)
|
|
636
|
+
|
|
614
637
|
raise RemitError.new(
|
|
615
638
|
RemitError::INVALID_ADDRESS,
|
|
616
639
|
"Invalid address #{addr.inspect}: expected 0x-prefixed 40-character hex string. " \
|
|
@@ -622,6 +645,7 @@ module Remitmd
|
|
|
622
645
|
def validate_amount!(amount)
|
|
623
646
|
d = BigDecimal(amount.to_s)
|
|
624
647
|
return if d >= MIN_AMOUNT
|
|
648
|
+
|
|
625
649
|
raise RemitError.new(
|
|
626
650
|
RemitError::INVALID_AMOUNT,
|
|
627
651
|
"Amount #{amount} is below the minimum of #{MIN_AMOUNT} USDC.",
|
|
@@ -652,56 +676,39 @@ module Remitmd
|
|
|
652
676
|
|
|
653
677
|
# ─── Permit helpers ──────────────────────────────────────────────────
|
|
654
678
|
|
|
655
|
-
# Fetch the
|
|
656
|
-
# Uses JSON-RPC eth_call with selector 0x7ecebe00 (nonces(address)).
|
|
679
|
+
# Fetch the EIP-2612 permit nonce from the API.
|
|
657
680
|
# @param usdc_address [String] the USDC contract address
|
|
658
681
|
# @return [Integer] current nonce
|
|
659
|
-
def
|
|
682
|
+
def fetch_permit_nonce(_usdc_address)
|
|
660
683
|
return 0 if @mock_mode
|
|
661
684
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
uri = URI.parse(@rpc_url)
|
|
672
|
-
http = Net::HTTP.new(uri.host, uri.port)
|
|
673
|
-
http.use_ssl = (uri.scheme == "https")
|
|
674
|
-
http.read_timeout = 10
|
|
675
|
-
http.open_timeout = 5
|
|
676
|
-
|
|
677
|
-
req = Net::HTTP::Post.new(uri.request_uri)
|
|
678
|
-
req["Content-Type"] = "application/json"
|
|
679
|
-
req.body = payload.to_json
|
|
680
|
-
|
|
681
|
-
resp = http.request(req)
|
|
682
|
-
result = JSON.parse(resp.body)
|
|
683
|
-
|
|
684
|
-
if result["error"]
|
|
685
|
-
msg = result["error"].is_a?(Hash) ? result["error"]["message"] : result["error"].to_s
|
|
686
|
-
raise RemitError.new(RemitError::NETWORK_ERROR, "RPC error fetching nonce: #{msg}")
|
|
685
|
+
data = @transport.get("/status/#{address}")
|
|
686
|
+
nonce = data.is_a?(Hash) ? data["permit_nonce"] : nil
|
|
687
|
+
if nonce.nil?
|
|
688
|
+
raise RemitError.new(
|
|
689
|
+
RemitError::NETWORK_ERROR,
|
|
690
|
+
"permit_nonce not available from API for #{address}. " \
|
|
691
|
+
"Ensure the server supports the permit_nonce field in GET /api/v1/status.",
|
|
692
|
+
context: { address: address }
|
|
693
|
+
)
|
|
687
694
|
end
|
|
688
|
-
|
|
689
|
-
(result["result"] || "0x0").to_i(16)
|
|
695
|
+
nonce.to_i
|
|
690
696
|
end
|
|
691
697
|
|
|
692
698
|
# Auto-sign a permit for the given contract type and amount.
|
|
699
|
+
# Returns nil on failure instead of raising, so callers can proceed without a permit.
|
|
693
700
|
# @param contract [String] contract key — "router", "escrow", "tab", etc.
|
|
694
701
|
# @param amount [Numeric] amount in USDC
|
|
695
|
-
# @return [PermitSignature]
|
|
702
|
+
# @return [PermitSignature, nil]
|
|
696
703
|
def auto_permit(contract, amount)
|
|
697
704
|
contracts = get_contracts
|
|
698
705
|
spender = contracts.send(contract.to_sym)
|
|
699
|
-
|
|
700
|
-
RemitError::SERVER_ERROR,
|
|
701
|
-
"No #{contract} contract address available"
|
|
702
|
-
) unless spender
|
|
706
|
+
return nil unless spender
|
|
703
707
|
|
|
704
708
|
sign_permit(spender, amount)
|
|
709
|
+
rescue => e
|
|
710
|
+
warn "[remitmd] auto-permit failed for #{contract} (amount=#{amount}): #{e.message}"
|
|
711
|
+
nil
|
|
705
712
|
end
|
|
706
713
|
|
|
707
714
|
# Spender contract mapping for auto_permit.
|