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.
@@ -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: []