remitmd 0.1.0
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 +7 -0
- data/LICENSE +21 -0
- data/README.md +256 -0
- data/lib/remitmd/a2a.rb +58 -0
- data/lib/remitmd/errors.rb +51 -0
- data/lib/remitmd/http.rb +187 -0
- data/lib/remitmd/keccak.rb +115 -0
- data/lib/remitmd/mock.rb +623 -0
- data/lib/remitmd/models.rb +416 -0
- data/lib/remitmd/signer.rb +162 -0
- data/lib/remitmd/wallet.rb +532 -0
- data/lib/remitmd.rb +26 -0
- metadata +87 -0
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "bigdecimal"
|
|
4
|
+
require "bigdecimal/util"
|
|
5
|
+
|
|
6
|
+
module Remitmd
|
|
7
|
+
# Primary remit.md client. All payment operations are methods on RemitWallet.
|
|
8
|
+
#
|
|
9
|
+
# @example Quickstart
|
|
10
|
+
# wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_KEY"])
|
|
11
|
+
# tx = wallet.pay("0xRecipient...", 1.50)
|
|
12
|
+
# puts tx.tx_hash
|
|
13
|
+
#
|
|
14
|
+
# @example From environment
|
|
15
|
+
# wallet = Remitmd::RemitWallet.from_env
|
|
16
|
+
#
|
|
17
|
+
# Private keys are held only by the Signer and never appear in inspect/to_s.
|
|
18
|
+
class RemitWallet
|
|
19
|
+
MIN_AMOUNT = BigDecimal("0.000001") # 1 micro-USDC
|
|
20
|
+
|
|
21
|
+
# @param private_key [String, nil] 0x-prefixed hex private key
|
|
22
|
+
# @param signer [Signer, nil] custom signer (pass instead of private_key)
|
|
23
|
+
# @param chain [String] chain name — "base", "base_sepolia"
|
|
24
|
+
# @param api_url [String, nil] override API base URL
|
|
25
|
+
# @param transport [Object, nil] inject mock transport (used by MockRemit)
|
|
26
|
+
def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil, transport: nil)
|
|
27
|
+
if transport
|
|
28
|
+
# MockRemit path: transport + signer injected directly
|
|
29
|
+
@signer = signer
|
|
30
|
+
@transport = transport
|
|
31
|
+
return
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if private_key.nil? && signer.nil?
|
|
35
|
+
raise ArgumentError, "Provide either :private_key or :signer"
|
|
36
|
+
end
|
|
37
|
+
if !private_key.nil? && !signer.nil?
|
|
38
|
+
raise ArgumentError, "Provide :private_key OR :signer, not both"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
@signer = signer || PrivateKeySigner.new(private_key)
|
|
42
|
+
# Normalize to the base chain name (strip testnet suffix) for use in pay body.
|
|
43
|
+
# The server accepts "base" — not "base_sepolia" etc.
|
|
44
|
+
@chain = chain.sub(/_sepolia\z/, "").sub(/-sepolia\z/, "")
|
|
45
|
+
cfg = CHAIN_CONFIG.fetch(chain) do
|
|
46
|
+
raise ArgumentError, "Unknown chain: #{chain}. Valid: #{CHAIN_CONFIG.keys.join(", ")}"
|
|
47
|
+
end
|
|
48
|
+
base_url = api_url || cfg[:url]
|
|
49
|
+
chain_id = cfg[:chain_id]
|
|
50
|
+
router_address ||= ""
|
|
51
|
+
@transport = HttpTransport.new(
|
|
52
|
+
base_url: base_url,
|
|
53
|
+
signer: @signer,
|
|
54
|
+
chain_id: chain_id,
|
|
55
|
+
router_address: router_address
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Build a RemitWallet from environment variables.
|
|
60
|
+
# Reads: REMITMD_PRIVATE_KEY, REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
|
|
61
|
+
def self.from_env
|
|
62
|
+
key = ENV.fetch("REMITMD_PRIVATE_KEY") { raise ArgumentError, "REMITMD_PRIVATE_KEY not set" }
|
|
63
|
+
chain = ENV.fetch("REMITMD_CHAIN", "base")
|
|
64
|
+
api_url = ENV["REMITMD_API_URL"]
|
|
65
|
+
router_address = ENV["REMITMD_ROUTER_ADDRESS"]
|
|
66
|
+
new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The Ethereum address associated with this wallet.
|
|
70
|
+
def address
|
|
71
|
+
@signer.address
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def inspect
|
|
75
|
+
"#<Remitmd::RemitWallet address=#{address}>"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
alias to_s inspect
|
|
79
|
+
|
|
80
|
+
# ─── Contracts ─────────────────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
# Get deployed contract addresses. Cached for the lifetime of this client.
|
|
83
|
+
# @return [ContractAddresses]
|
|
84
|
+
def get_contracts
|
|
85
|
+
@contracts_cache ||= ContractAddresses.new(@transport.get("/contracts"))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# ─── Balance & Analytics ─────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
# Fetch the current USDC balance.
|
|
91
|
+
# @return [Balance]
|
|
92
|
+
def balance
|
|
93
|
+
Balance.new(@transport.get("/wallet/balance"))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Fetch transaction history.
|
|
97
|
+
# @param limit [Integer] max results per page
|
|
98
|
+
# @param offset [Integer] pagination offset
|
|
99
|
+
# @return [TransactionList]
|
|
100
|
+
def history(limit: 50, offset: 0)
|
|
101
|
+
data = @transport.get("/wallet/history?limit=#{limit}&offset=#{offset}")
|
|
102
|
+
TransactionList.new(data)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Fetch the on-chain reputation for an address.
|
|
106
|
+
# @param addr [String] 0x-prefixed Ethereum address
|
|
107
|
+
# @return [Reputation]
|
|
108
|
+
def reputation(addr)
|
|
109
|
+
validate_address!(addr)
|
|
110
|
+
Reputation.new(@transport.get("/reputation/#{addr}"))
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Fetch monthly spending summary.
|
|
114
|
+
# @return [SpendingSummary]
|
|
115
|
+
def spending_summary
|
|
116
|
+
SpendingSummary.new(@transport.get("/wallet/spending"))
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Fetch operator-set budget limits and remaining allowances.
|
|
120
|
+
# @return [Budget]
|
|
121
|
+
def remaining_budget
|
|
122
|
+
Budget.new(@transport.get("/wallet/budget"))
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# ─── Direct Payment ───────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
# Send USDC directly to another address.
|
|
128
|
+
# @param to [String] recipient 0x-prefixed address
|
|
129
|
+
# @param amount [Numeric, BigDecimal] amount in USDC (e.g. 1.50)
|
|
130
|
+
# @param memo [String, nil] optional note
|
|
131
|
+
# @param permit [PermitSignature, nil] optional EIP-2612 permit for gasless approval
|
|
132
|
+
# @return [Transaction]
|
|
133
|
+
def pay(to, amount, memo: nil, permit: nil)
|
|
134
|
+
validate_address!(to)
|
|
135
|
+
validate_amount!(amount)
|
|
136
|
+
nonce = SecureRandom.hex(16)
|
|
137
|
+
body = { to: to, amount: amount.to_s, task: memo || "", chain: @chain, nonce: nonce, signature: "0x" }
|
|
138
|
+
body[:permit] = permit.to_h if permit
|
|
139
|
+
Transaction.new(@transport.post("/payments/direct", body))
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# ─── Escrow ───────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
# Create a new escrow (funds held until release or cancel).
|
|
145
|
+
# @param payee [String] 0x-prefixed payee address
|
|
146
|
+
# @param amount [Numeric] amount in USDC
|
|
147
|
+
# @param memo [String, nil] optional note
|
|
148
|
+
# @param expires_in_secs [Integer, nil] optional expiry in seconds from now
|
|
149
|
+
# @param permit [PermitSignature, nil] optional EIP-2612 permit for gasless approval
|
|
150
|
+
# @return [Escrow]
|
|
151
|
+
def create_escrow(payee, amount, memo: nil, expires_in_secs: nil, permit: nil)
|
|
152
|
+
validate_address!(payee)
|
|
153
|
+
validate_amount!(amount)
|
|
154
|
+
|
|
155
|
+
# Step 1: create invoice on server.
|
|
156
|
+
invoice_id = SecureRandom.hex(16)
|
|
157
|
+
nonce = SecureRandom.hex(16)
|
|
158
|
+
inv_body = {
|
|
159
|
+
id: invoice_id, chain: @chain,
|
|
160
|
+
from_agent: address.downcase, to_agent: payee.downcase,
|
|
161
|
+
amount: amount.to_s, type: "escrow",
|
|
162
|
+
task: memo || "", nonce: nonce, signature: "0x"
|
|
163
|
+
}
|
|
164
|
+
inv_body[:escrow_timeout] = expires_in_secs if expires_in_secs
|
|
165
|
+
@transport.post("/invoices", inv_body)
|
|
166
|
+
|
|
167
|
+
# Step 2: fund the escrow.
|
|
168
|
+
esc_body = { invoice_id: invoice_id }
|
|
169
|
+
esc_body[:permit] = permit.to_h if permit
|
|
170
|
+
Escrow.new(@transport.post("/escrows", esc_body))
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Release an escrow to the payee.
|
|
174
|
+
# @param escrow_id [String]
|
|
175
|
+
# @param memo [String, nil]
|
|
176
|
+
# @return [Transaction]
|
|
177
|
+
def release_escrow(escrow_id, memo: nil)
|
|
178
|
+
body = memo ? { memo: memo } : {}
|
|
179
|
+
Transaction.new(@transport.post("/escrows/#{escrow_id}/release", body))
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Cancel an escrow and refund the payer.
|
|
183
|
+
# @param escrow_id [String]
|
|
184
|
+
# @return [Transaction]
|
|
185
|
+
def cancel_escrow(escrow_id)
|
|
186
|
+
Transaction.new(@transport.post("/escrows/#{escrow_id}/cancel", {}))
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Signal the provider has started work on an escrow.
|
|
190
|
+
# @param escrow_id [String]
|
|
191
|
+
# @return [Escrow]
|
|
192
|
+
def claim_start(escrow_id)
|
|
193
|
+
Escrow.new(@transport.post("/escrows/#{escrow_id}/claim-start", {}))
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Fetch escrow details.
|
|
197
|
+
# @param escrow_id [String]
|
|
198
|
+
# @return [Escrow]
|
|
199
|
+
def get_escrow(escrow_id)
|
|
200
|
+
Escrow.new(@transport.get("/escrows/#{escrow_id}"))
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# ─── Tabs (Metered Billing) ───────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
# Open a payment tab for off-chain metered billing.
|
|
206
|
+
# @param provider [String] 0x-prefixed provider address
|
|
207
|
+
# @param limit_amount [Numeric] maximum tab credit in USDC
|
|
208
|
+
# @param per_unit [Numeric] USDC per API call
|
|
209
|
+
# @param expires_in_secs [Integer] optional expiry duration in seconds (default: 86400)
|
|
210
|
+
# @param permit [PermitSignature, nil] optional EIP-2612 permit for gasless approval
|
|
211
|
+
# @return [Tab]
|
|
212
|
+
def create_tab(provider, limit_amount, per_unit = 0.0, expires_in_secs: 86_400, permit: nil)
|
|
213
|
+
validate_address!(provider)
|
|
214
|
+
validate_amount!(limit_amount)
|
|
215
|
+
body = {
|
|
216
|
+
chain: @chain,
|
|
217
|
+
provider: provider,
|
|
218
|
+
limit_amount: limit_amount.to_f,
|
|
219
|
+
per_unit: per_unit.to_f,
|
|
220
|
+
expiry: Time.now.to_i + expires_in_secs
|
|
221
|
+
}
|
|
222
|
+
body[:permit] = permit.to_h if permit
|
|
223
|
+
Tab.new(@transport.post("/tabs", body))
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Charge a tab using an EIP-712 signed provider authorization.
|
|
227
|
+
# @param tab_id [String]
|
|
228
|
+
# @param amount [Numeric] charge amount in USDC
|
|
229
|
+
# @param cumulative [Numeric] cumulative total charged so far
|
|
230
|
+
# @param call_count [Integer] total number of calls so far
|
|
231
|
+
# @param provider_sig [String] EIP-712 signature from the provider
|
|
232
|
+
# @return [TabDebit]
|
|
233
|
+
def charge_tab(tab_id, amount, cumulative, call_count, provider_sig)
|
|
234
|
+
body = {
|
|
235
|
+
amount: amount.to_f,
|
|
236
|
+
cumulative: cumulative.to_f,
|
|
237
|
+
call_count: call_count,
|
|
238
|
+
provider_sig: provider_sig
|
|
239
|
+
}
|
|
240
|
+
TabDebit.new(@transport.post("/tabs/#{tab_id}/charge", body))
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Record a debit against an open tab (off-chain, no gas).
|
|
244
|
+
# @param tab_id [String]
|
|
245
|
+
# @param amount [Numeric] amount in USDC
|
|
246
|
+
# @param memo [String] description of this debit
|
|
247
|
+
# @return [TabDebit]
|
|
248
|
+
def debit_tab(tab_id, amount, memo = "")
|
|
249
|
+
validate_amount!(amount)
|
|
250
|
+
body = { amount: amount.to_s, memo: memo }
|
|
251
|
+
TabDebit.new(@transport.post("/tabs/#{tab_id}/debit", body))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Close a tab on-chain, settling the final balance.
|
|
255
|
+
# @param tab_id [String]
|
|
256
|
+
# @param final_amount [Numeric, nil] final settlement amount in USDC
|
|
257
|
+
# @param provider_sig [String, nil] EIP-712 signature from the provider
|
|
258
|
+
# @return [Tab]
|
|
259
|
+
def close_tab(tab_id, final_amount: nil, provider_sig: nil)
|
|
260
|
+
body = {}
|
|
261
|
+
body[:final_amount] = final_amount.to_f if final_amount
|
|
262
|
+
body[:provider_sig] = provider_sig if provider_sig
|
|
263
|
+
Tab.new(@transport.post("/tabs/#{tab_id}/close", body))
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Settle a tab on-chain, paying the net balance.
|
|
267
|
+
# @param tab_id [String]
|
|
268
|
+
# @return [Tab]
|
|
269
|
+
# @deprecated Use {#close_tab} instead
|
|
270
|
+
def settle_tab(tab_id)
|
|
271
|
+
close_tab(tab_id)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
# Sign an EIP-712 TabCharge message as the provider.
|
|
275
|
+
# Domain: RemitTab/1/<chainId>/<tabContract>
|
|
276
|
+
# Type: TabCharge(bytes32 tabId, uint96 totalCharged, uint32 callCount)
|
|
277
|
+
# @param tab_contract [String] tab contract address
|
|
278
|
+
# @param tab_id [String] UUID of the tab
|
|
279
|
+
# @param total_charged_base_units [Integer] total charged in USDC base units (6 decimals)
|
|
280
|
+
# @param call_count [Integer] total call count
|
|
281
|
+
# @param chain_id [Integer] chain ID (default: 84532 for Base Sepolia)
|
|
282
|
+
# @return [String] 0x-prefixed 65-byte hex signature
|
|
283
|
+
def sign_tab_charge(tab_contract, tab_id, total_charged_base_units, call_count, chain_id: 84_532)
|
|
284
|
+
# Domain separator for RemitTab
|
|
285
|
+
domain_type_hash = keccak256_raw(
|
|
286
|
+
"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
|
|
287
|
+
)
|
|
288
|
+
name_hash = keccak256_raw("RemitTab")
|
|
289
|
+
version_hash = keccak256_raw("1")
|
|
290
|
+
chain_id_enc = abi_uint256(chain_id)
|
|
291
|
+
contract_enc = abi_address(tab_contract)
|
|
292
|
+
|
|
293
|
+
domain_data = domain_type_hash + name_hash + version_hash + chain_id_enc + contract_enc
|
|
294
|
+
domain_sep = keccak256_raw(domain_data)
|
|
295
|
+
|
|
296
|
+
# TabCharge struct hash
|
|
297
|
+
type_hash = keccak256_raw(
|
|
298
|
+
"TabCharge(bytes32 tabId,uint96 totalCharged,uint32 callCount)"
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Encode tabId as bytes32: ASCII bytes right-padded with zeroes
|
|
302
|
+
tab_id_bytes = tab_id.b.ljust(32, "\x00".b)
|
|
303
|
+
|
|
304
|
+
struct_data = type_hash + tab_id_bytes + abi_uint256(total_charged_base_units) + abi_uint256(call_count)
|
|
305
|
+
struct_hash = keccak256_raw(struct_data)
|
|
306
|
+
|
|
307
|
+
# EIP-712 digest
|
|
308
|
+
digest = keccak256_raw("\x19\x01".b + domain_sep + struct_hash)
|
|
309
|
+
|
|
310
|
+
@signer.sign(digest)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# ─── Streams (Payment Streaming) ─────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
# Create a real-time payment stream.
|
|
316
|
+
# @param payee [String] 0x-prefixed address of the stream recipient
|
|
317
|
+
# @param rate_per_second [Numeric] USDC per second
|
|
318
|
+
# @param max_total [Numeric] maximum total USDC for the stream
|
|
319
|
+
# @param permit [PermitSignature, nil] optional EIP-2612 permit for gasless approval
|
|
320
|
+
# @return [Stream]
|
|
321
|
+
def create_stream(payee, rate_per_second, max_total, permit: nil)
|
|
322
|
+
validate_address!(payee)
|
|
323
|
+
validate_amount!(rate_per_second)
|
|
324
|
+
validate_amount!(max_total)
|
|
325
|
+
body = {
|
|
326
|
+
chain: @chain,
|
|
327
|
+
payee: payee,
|
|
328
|
+
rate_per_second: rate_per_second.to_s,
|
|
329
|
+
max_total: max_total.to_s
|
|
330
|
+
}
|
|
331
|
+
body[:permit] = permit.to_h if permit
|
|
332
|
+
Stream.new(@transport.post("/streams", body))
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Close an active payment stream.
|
|
336
|
+
# @param stream_id [String]
|
|
337
|
+
# @return [Stream]
|
|
338
|
+
def close_stream(stream_id)
|
|
339
|
+
Stream.new(@transport.post("/streams/#{stream_id}/close", {}))
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
# Withdraw accrued funds from a stream.
|
|
343
|
+
# @param stream_id [String]
|
|
344
|
+
# @return [Transaction]
|
|
345
|
+
def withdraw_stream(stream_id)
|
|
346
|
+
Transaction.new(@transport.post("/streams/#{stream_id}/withdraw", {}))
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# ─── Bounties ─────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
# Post a bounty for any agent to claim by completing a task.
|
|
352
|
+
# @param amount [Numeric] bounty amount in USDC
|
|
353
|
+
# @param task_description [String] task description
|
|
354
|
+
# @param deadline [Integer] deadline as Unix timestamp
|
|
355
|
+
# @param max_attempts [Integer] maximum submission attempts (default: 10)
|
|
356
|
+
# @param permit [PermitSignature, nil] optional EIP-2612 permit for gasless approval
|
|
357
|
+
# @return [Bounty]
|
|
358
|
+
def create_bounty(amount, task_description, deadline, max_attempts: 10, permit: nil)
|
|
359
|
+
validate_amount!(amount)
|
|
360
|
+
body = {
|
|
361
|
+
chain: @chain,
|
|
362
|
+
amount: amount.to_f,
|
|
363
|
+
task_description: task_description,
|
|
364
|
+
deadline: deadline,
|
|
365
|
+
max_attempts: max_attempts
|
|
366
|
+
}
|
|
367
|
+
body[:permit] = permit.to_h if permit
|
|
368
|
+
Bounty.new(@transport.post("/bounties", body))
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Submit evidence to claim a bounty.
|
|
372
|
+
# @param bounty_id [String]
|
|
373
|
+
# @param evidence_hash [String] 0x-prefixed hash of the evidence
|
|
374
|
+
# @return [BountySubmission]
|
|
375
|
+
def submit_bounty(bounty_id, evidence_hash)
|
|
376
|
+
BountySubmission.new(@transport.post("/bounties/#{bounty_id}/submit", { evidence_hash: evidence_hash }))
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Award a bounty to a specific submission.
|
|
380
|
+
# @param bounty_id [String]
|
|
381
|
+
# @param submission_id [Integer] ID of the winning submission
|
|
382
|
+
# @return [Bounty]
|
|
383
|
+
def award_bounty(bounty_id, submission_id)
|
|
384
|
+
Bounty.new(@transport.post("/bounties/#{bounty_id}/award", { submission_id: submission_id }))
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# List bounties with optional filters.
|
|
388
|
+
# @param status [String, nil] filter by status (open, claimed, awarded, expired)
|
|
389
|
+
# @param poster [String, nil] filter by poster wallet address
|
|
390
|
+
# @param submitter [String, nil] filter by submitter wallet address
|
|
391
|
+
# @param limit [Integer] max results (default 20, max 100)
|
|
392
|
+
# @return [Array<Bounty>]
|
|
393
|
+
def list_bounties(status: "open", poster: nil, submitter: nil, limit: 20)
|
|
394
|
+
params = ["limit=#{limit}"]
|
|
395
|
+
params << "status=#{status}" if status
|
|
396
|
+
params << "poster=#{poster}" if poster
|
|
397
|
+
params << "submitter=#{submitter}" if submitter
|
|
398
|
+
data = @transport.get("/bounties?#{params.join('&')}")
|
|
399
|
+
items = data.is_a?(Hash) ? (data["data"] || []) : data
|
|
400
|
+
items.map { |d| Bounty.new(d) }
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# ─── Deposits ─────────────────────────────────────────────────────────────
|
|
404
|
+
|
|
405
|
+
# Place a security deposit with a provider.
|
|
406
|
+
# @param provider [String] 0x-prefixed provider address
|
|
407
|
+
# @param amount [Numeric] amount in USDC
|
|
408
|
+
# @param expires_in_secs [Integer] expiry duration in seconds (default: 3600)
|
|
409
|
+
# @param permit [PermitSignature, nil] optional EIP-2612 permit for gasless approval
|
|
410
|
+
# @return [Deposit]
|
|
411
|
+
def place_deposit(provider, amount, expires_in_secs: 3600, permit: nil)
|
|
412
|
+
validate_address!(provider)
|
|
413
|
+
validate_amount!(amount)
|
|
414
|
+
body = {
|
|
415
|
+
chain: @chain,
|
|
416
|
+
provider: provider,
|
|
417
|
+
amount: amount.to_f,
|
|
418
|
+
expiry: Time.now.to_i + expires_in_secs
|
|
419
|
+
}
|
|
420
|
+
body[:permit] = permit.to_h if permit
|
|
421
|
+
Deposit.new(@transport.post("/deposits", body))
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Return a deposit to the payer.
|
|
425
|
+
# @param deposit_id [String]
|
|
426
|
+
# @return [Transaction]
|
|
427
|
+
def return_deposit(deposit_id)
|
|
428
|
+
Transaction.new(@transport.post("/deposits/#{deposit_id}/return", {}))
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Lock a security deposit.
|
|
432
|
+
# @deprecated Use {#place_deposit} instead
|
|
433
|
+
def lock_deposit(provider, amount, expires_in_secs, permit: nil)
|
|
434
|
+
place_deposit(provider, amount, expires_in_secs: expires_in_secs, permit: permit)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# ─── Payment Intents ─────────────────────────────────────────────────────
|
|
438
|
+
|
|
439
|
+
# Propose a payment intent for counterpart approval before execution.
|
|
440
|
+
# @param to [String] 0x-prefixed address
|
|
441
|
+
# @param amount [Numeric] amount in USDC
|
|
442
|
+
# @param type [String] payment type — "direct", "escrow", "tab"
|
|
443
|
+
# @return [Intent]
|
|
444
|
+
def propose_intent(to, amount, type: "direct")
|
|
445
|
+
validate_address!(to)
|
|
446
|
+
validate_amount!(amount)
|
|
447
|
+
body = { to: to, amount: amount.to_s, type: type }
|
|
448
|
+
Intent.new(@transport.post("/intents", body))
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
# ─── Webhooks ─────────────────────────────────────────────────────────────
|
|
452
|
+
|
|
453
|
+
# Register a webhook endpoint to receive event notifications.
|
|
454
|
+
# @param url [String] the HTTPS endpoint that will receive POST notifications
|
|
455
|
+
# @param events [Array<String>] event types to subscribe to (e.g. ["payment.sent", "escrow.funded"])
|
|
456
|
+
# @param chains [Array<String>, nil] optional chain names to filter by
|
|
457
|
+
# @return [Webhook]
|
|
458
|
+
def register_webhook(url, events, chains: nil)
|
|
459
|
+
body = { url: url, events: events }
|
|
460
|
+
body[:chains] = chains if chains
|
|
461
|
+
Webhook.new(@transport.post("/webhooks", body))
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
# ─── One-time operator links ───────────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
# Generate a one-time URL for the operator to fund this wallet.
|
|
467
|
+
# @return [LinkResponse]
|
|
468
|
+
def create_fund_link
|
|
469
|
+
LinkResponse.new(@transport.post("/links/fund", {}))
|
|
470
|
+
end
|
|
471
|
+
|
|
472
|
+
# Generate a one-time URL for the operator to withdraw funds.
|
|
473
|
+
# @return [LinkResponse]
|
|
474
|
+
def create_withdraw_link
|
|
475
|
+
LinkResponse.new(@transport.post("/links/withdraw", {}))
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# ─── Testnet ──────────────────────────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
# Mint testnet USDC. Max $2,500 per call, once per hour per wallet.
|
|
481
|
+
# @param amount [Numeric] amount in USDC
|
|
482
|
+
# @return [Hash] { "tx_hash" => "0x...", "balance" => 1234.56 }
|
|
483
|
+
def mint(amount)
|
|
484
|
+
@transport.post("/mint", { wallet: address, amount: amount })
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
private
|
|
488
|
+
|
|
489
|
+
ADDRESS_RE = /\A0x[0-9a-fA-F]{40}\z/
|
|
490
|
+
|
|
491
|
+
def validate_address!(addr)
|
|
492
|
+
return if addr.match?(ADDRESS_RE)
|
|
493
|
+
raise RemitError.new(
|
|
494
|
+
RemitError::INVALID_ADDRESS,
|
|
495
|
+
"Invalid address #{addr.inspect}: expected 0x-prefixed 40-character hex string. " \
|
|
496
|
+
"Got #{addr.length} characters.",
|
|
497
|
+
context: { address: addr }
|
|
498
|
+
)
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def validate_amount!(amount)
|
|
502
|
+
d = BigDecimal(amount.to_s)
|
|
503
|
+
return if d >= MIN_AMOUNT
|
|
504
|
+
raise RemitError.new(
|
|
505
|
+
RemitError::INVALID_AMOUNT,
|
|
506
|
+
"Amount #{amount} is below the minimum of #{MIN_AMOUNT} USDC.",
|
|
507
|
+
context: { amount: amount.to_s, minimum: MIN_AMOUNT.to_s }
|
|
508
|
+
)
|
|
509
|
+
rescue ArgumentError
|
|
510
|
+
raise RemitError.new(
|
|
511
|
+
RemitError::INVALID_AMOUNT,
|
|
512
|
+
"Invalid amount #{amount.inspect}: must be a numeric value.",
|
|
513
|
+
context: { amount: amount.inspect }
|
|
514
|
+
)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
# ─── EIP-712 helpers (used by sign_tab_charge) ────────────────────────
|
|
518
|
+
|
|
519
|
+
def keccak256_raw(data)
|
|
520
|
+
Remitmd::Keccak.digest(data.b)
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def abi_uint256(value)
|
|
524
|
+
[value.to_i.to_s(16).rjust(64, "0")].pack("H*")
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def abi_address(addr)
|
|
528
|
+
hex = addr.to_s.delete_prefix("0x").rjust(64, "0")
|
|
529
|
+
[hex].pack("H*")
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
data/lib/remitmd.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "remitmd/errors"
|
|
4
|
+
require_relative "remitmd/models"
|
|
5
|
+
require_relative "remitmd/keccak"
|
|
6
|
+
require_relative "remitmd/signer"
|
|
7
|
+
require_relative "remitmd/http"
|
|
8
|
+
require_relative "remitmd/wallet"
|
|
9
|
+
require_relative "remitmd/mock"
|
|
10
|
+
|
|
11
|
+
# remit.md Ruby SDK — universal payment protocol for AI agents.
|
|
12
|
+
#
|
|
13
|
+
# @example Direct payment
|
|
14
|
+
# wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_PRIVATE_KEY"])
|
|
15
|
+
# tx = wallet.pay("0xRecipient0000000000000000000000000000001", 1.50)
|
|
16
|
+
# puts tx.tx_hash
|
|
17
|
+
#
|
|
18
|
+
# @example Using MockRemit in tests
|
|
19
|
+
# mock = Remitmd::MockRemit.new
|
|
20
|
+
# wallet = mock.wallet
|
|
21
|
+
# wallet.pay("0x0000000000000000000000000000000000000001", 1.00)
|
|
22
|
+
# mock.was_paid?("0x0000000000000000000000000000000000000001", 1.00) # => true
|
|
23
|
+
#
|
|
24
|
+
module Remitmd
|
|
25
|
+
VERSION = "0.1.0"
|
|
26
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: remitmd
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- remit.md
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: rspec
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '3.12'
|
|
20
|
+
type: :development
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '3.12'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: rubocop
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '1.60'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '1.60'
|
|
41
|
+
description: Send and receive USDC via the remit.md protocol. Supports direct payments,
|
|
42
|
+
escrow, metered tabs, payment streams, bounties, and security deposits. Includes
|
|
43
|
+
MockRemit for zero-network testing.
|
|
44
|
+
email:
|
|
45
|
+
- hello@remit.md
|
|
46
|
+
executables: []
|
|
47
|
+
extensions: []
|
|
48
|
+
extra_rdoc_files: []
|
|
49
|
+
files:
|
|
50
|
+
- LICENSE
|
|
51
|
+
- README.md
|
|
52
|
+
- lib/remitmd.rb
|
|
53
|
+
- lib/remitmd/a2a.rb
|
|
54
|
+
- lib/remitmd/errors.rb
|
|
55
|
+
- lib/remitmd/http.rb
|
|
56
|
+
- lib/remitmd/keccak.rb
|
|
57
|
+
- lib/remitmd/mock.rb
|
|
58
|
+
- lib/remitmd/models.rb
|
|
59
|
+
- lib/remitmd/signer.rb
|
|
60
|
+
- lib/remitmd/wallet.rb
|
|
61
|
+
homepage: https://remit.md
|
|
62
|
+
licenses:
|
|
63
|
+
- MIT
|
|
64
|
+
metadata:
|
|
65
|
+
homepage_uri: https://remit.md
|
|
66
|
+
source_code_uri: https://github.com/remit-md/sdk
|
|
67
|
+
changelog_uri: https://github.com/remit-md/sdk/releases
|
|
68
|
+
post_install_message:
|
|
69
|
+
rdoc_options: []
|
|
70
|
+
require_paths:
|
|
71
|
+
- lib
|
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '3.0'
|
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
requirements: []
|
|
83
|
+
rubygems_version: 3.4.19
|
|
84
|
+
signing_key:
|
|
85
|
+
specification_version: 4
|
|
86
|
+
summary: remit.md — universal payment protocol SDK for AI agents
|
|
87
|
+
test_files: []
|