remitmd 0.1.4 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d343ece392d469fe2aa4c2388d912506654744486007eabc5dd8f030e19c00e3
4
- data.tar.gz: 7b5ff1660f2dc09b105db50d29c0d9ffa6085a0537e1dce8faf3b6546de6c817
3
+ metadata.gz: 77a3a0333d3f1afc8315ad1d523bc9adb70e74d2f5af29bc20b608233b0f67b1
4
+ data.tar.gz: b04b273b6340c25f8adcc5e755e99a4f32c9e804aec91d2e28058a66f507e9e9
5
5
  SHA512:
6
- metadata.gz: 238bc729c10f5e79104d6d139dff95fc87aaa824f4a6dc75bf024c63ced60f2f86b5d1a01ae16c8a47418e8ab37d4443a940c63bfd1f6424d65fba46e96b5c88
7
- data.tar.gz: ac95ef3316ad9684c6e8996fb0704a23423a77fa4083f98e70d1294c2157a91fda366214aaded0f8648241dabfc41ae7b92b2056f7690a0a8e467ebc8b5fac72
6
+ metadata.gz: 3e343856eb6688f7a5bee1a10fa85ae119e9ac548eeedfb4bb95a21f4a5565d8ad4921efccfcad940f3f8a89fb7a0fd313243c7dd511f132c0540b69e05b2f82
7
+ data.tar.gz: 284848e90b0c9c53bc80a07fa7a2bc90ea7f3e1afef282b10308a9fc5805b9c750392d75a6055bf152660d15a1b3948d06fddbf179f5daf2ae3b586f0e18a35a
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # remit.md Ruby SDK
2
2
 
3
+ > [Skill MD](https://remit.md) · [Docs](https://remit.md/docs) · [Agent Spec](https://remit.md/agent.md)
4
+
3
5
  Universal payment protocol for AI agents — Ruby client library.
4
6
 
5
7
  [![CI](https://github.com/remit-md/sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/remit-md/sdk/actions/workflows/ci.yml)
@@ -38,7 +40,7 @@ Or from environment variables:
38
40
  ```ruby
39
41
  wallet = Remitmd::RemitWallet.from_env
40
42
  # Requires: REMITMD_PRIVATE_KEY
41
- # Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL, REMITMD_RPC_URL
43
+ # Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL
42
44
  ```
43
45
 
44
46
  Permits are auto-signed. Every payment method fetches the on-chain USDC nonce, signs an EIP-2612 permit, and includes it automatically.
@@ -267,19 +269,6 @@ permit = wallet.sign_usdc_permit(
267
269
  tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
268
270
  ```
269
271
 
270
- ### Custom RPC URL
271
-
272
- Override the JSON-RPC endpoint used for nonce fetching:
273
-
274
- ```ruby
275
- wallet = Remitmd::RemitWallet.new(
276
- private_key: key,
277
- chain: "base_sepolia",
278
- rpc_url: "https://your-rpc-provider.com/v1/base-sepolia"
279
- )
280
- # Or via environment: REMITMD_RPC_URL
281
- ```
282
-
283
272
  ## License
284
273
 
285
274
  MIT — see [LICENSE](LICENSE)
@@ -30,12 +30,12 @@ module Remitmd
30
30
  SERVER_ERROR = "SERVER_ERROR"
31
31
  NONCE_REUSED = "NONCE_REUSED"
32
32
  SIGNATURE_INVALID = "SIGNATURE_INVALID"
33
- ESCROW_ALREADY_RELEASED = "ESCROW_ALREADY_RELEASED"
33
+ ESCROW_ALREADY_COMPLETED = "ESCROW_ALREADY_COMPLETED"
34
34
  ESCROW_EXPIRED = "ESCROW_EXPIRED"
35
35
  TAB_LIMIT_EXCEEDED = "TAB_LIMIT_EXCEEDED"
36
36
  BOUNTY_ALREADY_AWARDED = "BOUNTY_ALREADY_AWARDED"
37
37
  STREAM_NOT_ACTIVE = "STREAM_NOT_ACTIVE"
38
- DEPOSIT_ALREADY_SETTLED = "DEPOSIT_ALREADY_SETTLED"
38
+ DEPOSIT_ALREADY_RESOLVED = "DEPOSIT_ALREADY_RESOLVED"
39
39
  USDC_TRANSFER_FAILED = "USDC_TRANSFER_FAILED"
40
40
  CHAIN_UNAVAILABLE = "CHAIN_UNAVAILABLE"
41
41
 
data/lib/remitmd/http.rb CHANGED
@@ -8,9 +8,11 @@ require "openssl"
8
8
 
9
9
  module Remitmd
10
10
  # Chain configuration: maps chain names to (api_url, chain_id) pairs.
11
+ # Canonical keys use hyphens; underscored variants are accepted as aliases.
11
12
  CHAIN_CONFIG = {
12
- "base" => { url: "https://remit.md/api/v1", chain_id: 8453 },
13
- "base_sepolia" => { url: "https://testnet.remit.md/api/v1", chain_id: 84532 },
13
+ "base" => { url: "https://remit.md/api/v1", chain_id: 8453 },
14
+ "base-sepolia" => { url: "https://testnet.remit.md/api/v1", chain_id: 84532 },
15
+ "base_sepolia" => { url: "https://testnet.remit.md/api/v1", chain_id: 84532 },
14
16
  }.freeze
15
17
 
16
18
  # HTTP transport layer. Signs each request with EIP-712 auth headers and
@@ -40,9 +42,11 @@ module Remitmd
40
42
 
41
43
  def request(method, path, body)
42
44
  attempt = 0
45
+ # Generate idempotency key once per request (stable across retries).
46
+ idempotency_key = (method == :post || method == :put || method == :patch) ? SecureRandom.uuid : nil
43
47
  begin
44
48
  attempt += 1
45
- req = build_request(method, path, body)
49
+ req = build_request(method, path, body, idempotency_key)
46
50
  resp = @http.request(req)
47
51
  handle_response(resp, path)
48
52
  rescue RemitError => e
@@ -58,7 +62,7 @@ module Remitmd
58
62
  end
59
63
  end
60
64
 
61
- def build_request(method, path, body)
65
+ def build_request(method, path, body, idempotency_key = nil)
62
66
  full_path = "#{@uri.path}#{path}"
63
67
  req = case method
64
68
  when :get then Net::HTTP::Get.new(full_path)
@@ -81,6 +85,7 @@ module Remitmd
81
85
  req["X-Remit-Nonce"] = nonce_hex
82
86
  req["X-Remit-Timestamp"] = timestamp.to_s
83
87
  req["X-Remit-Signature"] = signature
88
+ req["X-Idempotency-Key"] = idempotency_key if idempotency_key
84
89
 
85
90
  if body
86
91
  req.body = body.to_json
data/lib/remitmd/mock.rb CHANGED
@@ -148,7 +148,6 @@ module Remitmd
148
148
  "deposit" => "0xMockDeposit00000000000000000000000001",
149
149
  "fee_calculator" => "0xMockFeeCalc0000000000000000000000001",
150
150
  "key_registry" => "0xMockKeyReg000000000000000000000000001",
151
- "arbitration" => "0xMockArbitr000000000000000000000000001",
152
151
  }
153
152
 
154
153
  # Balance
@@ -200,7 +199,7 @@ module Remitmd
200
199
  in ["POST", path] if path.end_with?("/release") && path.include?("/escrows/")
201
200
  id = extract_id(path, "/escrows/", "/release")
202
201
  esc = @state[:escrows].fetch(id) { raise not_found(RemitError::ESCROW_NOT_FOUND, id) }
203
- new_esc = update_escrow(esc, status: EscrowStatus::RELEASED)
202
+ new_esc = update_escrow(esc, status: EscrowStatus::COMPLETED)
204
203
  @state[:escrows][id] = new_esc
205
204
  tx = make_tx(from: esc.payer, to: esc.payee, amount: esc.amount)
206
205
  @state[:transactions] << tx
@@ -269,7 +268,7 @@ module Remitmd
269
268
  in ["POST", path] if path.end_with?("/close") && path.include?("/tabs/")
270
269
  id = extract_id(path, "/tabs/", "/close")
271
270
  tab = @state[:tabs].fetch(id) { raise not_found(RemitError::TAB_NOT_FOUND, id) }
272
- new_tab = update_tab(tab, status: TabStatus::SETTLED)
271
+ new_tab = update_tab(tab, status: TabStatus::CLOSED)
273
272
  @state[:tabs][id] = new_tab
274
273
  tab_hash(new_tab)
275
274
 
@@ -16,29 +16,30 @@ module Remitmd
16
16
  module EscrowStatus
17
17
  PENDING = "pending"
18
18
  FUNDED = "funded"
19
- RELEASED = "released"
19
+ ACTIVE = "active"
20
+ COMPLETED = "completed"
20
21
  CANCELLED = "cancelled"
21
- EXPIRED = "expired"
22
+ FAILED = "failed"
22
23
  end
23
24
 
24
25
  module TabStatus
25
26
  OPEN = "open"
26
27
  CLOSED = "closed"
27
- SETTLED = "settled"
28
+ EXPIRED = "expired"
28
29
  end
29
30
 
30
31
  module StreamStatus
31
32
  ACTIVE = "active"
32
- PAUSED = "paused"
33
- ENDED = "ended"
34
- CANCELLED = "cancelled"
33
+ CLOSED = "closed"
34
+ COMPLETED = "completed"
35
35
  end
36
36
 
37
37
  module BountyStatus
38
38
  OPEN = "open"
39
+ CLAIMED = "claimed"
39
40
  AWARDED = "awarded"
40
41
  EXPIRED = "expired"
41
- RECLAIMED = "reclaimed"
42
+ CANCELLED = "cancelled"
42
43
  end
43
44
 
44
45
  module DepositStatus
@@ -116,12 +117,11 @@ module Remitmd
116
117
  @deposit = h["deposit"]
117
118
  @fee_calculator = h["fee_calculator"]
118
119
  @key_registry = h["key_registry"]
119
- @arbitration = h["arbitration"]
120
120
  @relayer = h["relayer"]
121
121
  end
122
122
 
123
123
  attr_reader :chain_id, :usdc, :router, :escrow, :tab, :stream,
124
- :bounty, :deposit, :fee_calculator, :key_registry, :arbitration,
124
+ :bounty, :deposit, :fee_calculator, :key_registry,
125
125
  :relayer
126
126
  end
127
127
 
@@ -2,25 +2,16 @@
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
10
8
  # Known USDC contract addresses per chain (EIP-2612 compatible).
11
9
  USDC_ADDRESSES = {
12
- "base-sepolia" => "0x142aD61B8d2edD6b3807D9266866D97C35Ee0317",
10
+ "base-sepolia" => "0x2d846325766921935f37d5b4478196d3ef93707c",
13
11
  "base" => "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
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, rpc_url: nil, transport: 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,7 +50,7 @@ module Remitmd
61
50
  end
62
51
 
63
52
  @signer = signer || PrivateKeySigner.new(private_key)
64
- # Normalize chain key for USDC/RPC lookups (underscore → hyphen).
53
+ # Normalize chain key for USDC lookups (underscore → hyphen).
65
54
  @chain_key = chain.tr("_", "-")
66
55
  # Normalize to the base chain name (strip testnet suffix) for use in pay body.
67
56
  # The server accepts "base" — not "base_sepolia" etc.
@@ -72,7 +61,6 @@ module Remitmd
72
61
  base_url = api_url || cfg[:url]
73
62
  @chain_id = cfg[:chain_id]
74
63
  router_address ||= ""
75
- @rpc_url = rpc_url || ENV["REMITMD_RPC_URL"] || DEFAULT_RPC_URLS[@chain_key] || DEFAULT_RPC_URLS["base-sepolia"]
76
64
  @transport = HttpTransport.new(
77
65
  base_url: base_url,
78
66
  signer: @signer,
@@ -82,14 +70,19 @@ module Remitmd
82
70
  end
83
71
 
84
72
  # Build a RemitWallet from environment variables.
85
- # Reads: REMITMD_PRIVATE_KEY, REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS, REMITMD_RPC_URL.
73
+ # Reads: REMITMD_KEY (primary) or REMITMD_PRIVATE_KEY (deprecated fallback),
74
+ # REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
86
75
  def self.from_env
87
- key = ENV.fetch("REMITMD_PRIVATE_KEY") { raise ArgumentError, "REMITMD_PRIVATE_KEY not set" }
76
+ key = ENV["REMITMD_KEY"] || ENV["REMITMD_PRIVATE_KEY"]
77
+ if ENV["REMITMD_PRIVATE_KEY"] && !ENV["REMITMD_KEY"]
78
+ warn "[remitmd] REMITMD_PRIVATE_KEY is deprecated, use REMITMD_KEY instead"
79
+ end
80
+ raise ArgumentError, "REMITMD_KEY not set" unless key
81
+
88
82
  chain = ENV.fetch("REMITMD_CHAIN", "base")
89
83
  api_url = ENV["REMITMD_API_URL"]
90
84
  router_address = ENV["REMITMD_ROUTER_ADDRESS"]
91
- rpc_url = ENV["REMITMD_RPC_URL"]
92
- new(private_key: key, chain: chain, api_url: api_url, router_address: router_address, rpc_url: rpc_url)
85
+ new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
93
86
  end
94
87
 
95
88
  # The Ethereum address associated with this wallet.
@@ -350,7 +343,15 @@ module Remitmd
350
343
  # @param usdc_address [String, nil] override the USDC contract address
351
344
  # @return [PermitSignature]
352
345
  def sign_usdc_permit(spender, value, deadline, nonce = 0, usdc_address: nil)
353
- usdc_addr = usdc_address || USDC_ADDRESSES[@chain_key] || ""
346
+ usdc_addr = usdc_address || USDC_ADDRESSES[@chain_key]
347
+ if usdc_addr.nil? || usdc_addr.empty?
348
+ raise RemitError.new(
349
+ RemitError::INVALID_ADDRESS,
350
+ "No USDC address configured for chain #{@chain_key.inspect}. " \
351
+ "Valid chains: #{USDC_ADDRESSES.keys.join(", ")}",
352
+ context: { chain: @chain_key }
353
+ )
354
+ end
354
355
  chain_id = @chain_id || ChainId::BASE_SEPOLIA
355
356
 
356
357
  # Domain separator for USDC (EIP-2612)
@@ -397,8 +398,16 @@ module Remitmd
397
398
  # @param deadline [Integer, nil] optional Unix timestamp; defaults to 1 hour from now
398
399
  # @return [PermitSignature]
399
400
  def sign_permit(spender, amount, deadline: nil)
400
- usdc_addr = USDC_ADDRESSES[@chain_key] || ""
401
- nonce = fetch_usdc_nonce(usdc_addr)
401
+ usdc_addr = USDC_ADDRESSES[@chain_key]
402
+ if usdc_addr.nil? || usdc_addr.empty?
403
+ raise RemitError.new(
404
+ RemitError::INVALID_ADDRESS,
405
+ "No USDC address configured for chain #{@chain_key.inspect}. " \
406
+ "Valid chains: #{USDC_ADDRESSES.keys.join(", ")}",
407
+ context: { chain: @chain_key }
408
+ )
409
+ end
410
+ nonce = fetch_permit_nonce(usdc_addr)
402
411
  dl = deadline || (Time.now.to_i + 3600)
403
412
  raw = (amount * 1_000_000).round.to_i
404
413
  sign_usdc_permit(spender, raw, dl, nonce, usdc_address: usdc_addr)
@@ -572,8 +581,8 @@ module Remitmd
572
581
  begin
573
582
  resolved = permit || auto_permit("relayer", 999_999_999.0)
574
583
  body[:permit] = resolved.to_h
575
- rescue StandardError
576
- # permit signing failed — proceed without permit (custodial fallback)
584
+ rescue StandardError => e
585
+ warn "[remitmd] create_fund_link: auto-permit failed: #{e.message}"
577
586
  end
578
587
  LinkResponse.new(@transport.post("/links/fund", body))
579
588
  end
@@ -590,8 +599,8 @@ module Remitmd
590
599
  begin
591
600
  resolved = permit || auto_permit("relayer", 999_999_999.0)
592
601
  body[:permit] = resolved.to_h
593
- rescue StandardError
594
- # permit signing failed — proceed without permit (custodial fallback)
602
+ rescue StandardError => e
603
+ warn "[remitmd] create_withdraw_link: auto-permit failed: #{e.message}"
595
604
  end
596
605
  LinkResponse.new(@transport.post("/links/withdraw", body))
597
606
  end
@@ -652,41 +661,23 @@ module Remitmd
652
661
 
653
662
  # ─── Permit helpers ──────────────────────────────────────────────────
654
663
 
655
- # Fetch the current EIP-2612 nonce for this wallet from the USDC contract.
656
- # Uses JSON-RPC eth_call with selector 0x7ecebe00 (nonces(address)).
664
+ # Fetch the EIP-2612 permit nonce from the API.
657
665
  # @param usdc_address [String] the USDC contract address
658
666
  # @return [Integer] current nonce
659
- def fetch_usdc_nonce(usdc_address)
667
+ def fetch_permit_nonce(usdc_address)
660
668
  return 0 if @mock_mode
661
669
 
662
- padded = address.downcase.delete_prefix("0x").rjust(64, "0")
663
- data = "0x7ecebe00#{padded}"
664
- payload = {
665
- jsonrpc: "2.0",
666
- id: 1,
667
- method: "eth_call",
668
- params: [{ to: usdc_address, data: data }, "latest"]
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}")
670
+ data = @transport.get("/status/#{address}")
671
+ nonce = data.is_a?(Hash) ? data["permit_nonce"] : nil
672
+ if nonce.nil?
673
+ raise RemitError.new(
674
+ RemitError::NETWORK_ERROR,
675
+ "permit_nonce not available from API for #{address}. " \
676
+ "Ensure the server supports the permit_nonce field in GET /api/v1/status.",
677
+ context: { address: address }
678
+ )
687
679
  end
688
-
689
- (result["result"] || "0x0").to_i(16)
680
+ nonce.to_i
690
681
  end
691
682
 
692
683
  # Auto-sign a permit for the given contract type and amount.
data/lib/remitmd.rb CHANGED
@@ -22,5 +22,5 @@ require_relative "remitmd/mock"
22
22
  # mock.was_paid?("0x0000000000000000000000000000000000000001", 1.00) # => true
23
23
  #
24
24
  module Remitmd
25
- VERSION = "0.1.4"
25
+ VERSION = "0.1.6"
26
26
  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
4
+ version: 0.1.6
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-22 00:00:00.000000000 Z
11
+ date: 2026-03-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec