remitmd 0.1.8 → 0.2.1

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: 8c2b03f3cb2f1a05e9a33845448154e4d34fda42f65af31823be2f30fb14b6fe
4
- data.tar.gz: 3d6dc094058152593779b5430899400d7b9d6543693ee3dd35dfd08b88f4c131
3
+ metadata.gz: 75be0a5249f87323f7c007b13b7b89f9e2153557b355381000b7d3d007adcfc0
4
+ data.tar.gz: e0f9b80bc11f7eb9888f037cba3589548a3344bdba9f621994142f5b20a3b14d
5
5
  SHA512:
6
- metadata.gz: deca4edd8beb6ac2d798102a83838762d746f4508bf5e852bb1b43d74aed1fa24a7b328bc4880560fc5ab60f5bd2c937277123183c3c45cc430bf02407f5c5c8
7
- data.tar.gz: c8b3bd0dc87584d00abe0b3acb37a52fa16f1f9af337780dd6aecc861f2cd542d2d684d636b28bef6b0227c13002c3c70388809d2e38a2874255bde0dd1c3932
6
+ metadata.gz: 5198b515870956a99eccee4152e991d4fc32f6f03acd5a91a499b9c853b57da2c0b3cad4657381e1524553f10d021415b1d9202ce35848eb06331e5420758c73
7
+ data.tar.gz: 5bcaab9bc1d75389f524ad3ab36920a02bf9dbacf68792c6492f8bc04a6ebb2197725a73342191a26c2debf7dceddf31a3247968e82f470c228fe76203a51df0
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  > [Skill MD](https://remit.md) · [Docs](https://remit.md/docs) · [Agent Spec](https://remit.md/agent.md)
4
4
 
5
- Universal payment protocol for AI agents Ruby client library.
5
+ Universal payment protocol for AI agents - Ruby client library.
6
6
 
7
7
  [![CI](https://github.com/remit-md/sdk/actions/workflows/ci.yml/badge.svg)](https://github.com/remit-md/sdk/actions/workflows/ci.yml)
8
8
  [![Gem Version](https://badge.fury.io/rb/remitmd.svg)](https://badge.fury.io/rb/remitmd)
@@ -39,31 +39,35 @@ Or from environment variables:
39
39
 
40
40
  ```ruby
41
41
  wallet = Remitmd::RemitWallet.from_env
42
- # Requires: REMITMD_KEY (or REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)
42
+ # Auto-detects: CliSigner (remit CLI) > REMITMD_KEY
43
43
  # Optional: REMITMD_CHAIN (default: "base"), REMITMD_API_URL
44
44
  ```
45
45
 
46
46
  Permits are auto-signed. Every payment method fetches the on-chain USDC nonce, signs an EIP-2612 permit, and includes it automatically.
47
47
 
48
- ## Local Signer (Recommended)
48
+ ## CLI Signer (Recommended)
49
49
 
50
- The local signer delegates key management to `remit signer`, a localhost HTTP server that holds your encrypted key. Your agent only needs a URL and token no private key in the environment.
50
+ The CLI signer delegates key management to the `remit` CLI binary, which holds your encrypted keystore at `~/.remit/keys/`. No private key in your environment -- just install the CLI and set a password.
51
51
 
52
52
  ```bash
53
- export REMIT_SIGNER_URL=http://127.0.0.1:7402
54
- export REMIT_SIGNER_TOKEN=rmit_sk_...
53
+ # Install the CLI
54
+ # macOS: brew install remit-md/tap/remit
55
+ # Windows: winget install remit-md.remit
56
+ # Linux: curl -fsSL https://remit.md/install.sh | sh
57
+
58
+ export REMIT_KEY_PASSWORD=your-keystore-password
55
59
  ```
56
60
 
57
61
  ```ruby
58
62
  # Explicit
59
- signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
63
+ signer = Remitmd::CliSigner.new
60
64
  wallet = Remitmd::RemitWallet.new(signer: signer)
61
65
 
62
66
  # Or auto-detect from env (recommended)
63
- wallet = Remitmd::RemitWallet.from_env # detects REMIT_SIGNER_URL automatically
67
+ wallet = Remitmd::RemitWallet.from_env # detects remit CLI automatically
64
68
  ```
65
69
 
66
- `RemitWallet.from_env` detects signer credentials automatically. Priority: `REMIT_SIGNER_URL` > `REMITMD_KEY`.
70
+ `RemitWallet.from_env` detects signing methods automatically. Priority: `CliSigner` (CLI + keystore + password) > `REMITMD_KEY`.
67
71
 
68
72
  ## Payment Models
69
73
 
@@ -93,7 +97,7 @@ contracts = wallet.get_contracts
93
97
  sig = wallet.sign_tab_charge(contracts.tab, tab.id, 3_000_000, 1)
94
98
  wallet.charge_tab(tab.id, 0.003, 0.003, 1, sig)
95
99
 
96
- # Close when done unused funds return
100
+ # Close when done - unused funds return
97
101
  wallet.close_tab(tab.id)
98
102
  ```
99
103
 
@@ -156,9 +160,9 @@ end
156
160
 
157
161
  ```ruby
158
162
  mock.was_paid?(address, amount) # true/false
159
- mock.total_paid_to(address) # BigDecimal sum of all payments to address
163
+ mock.total_paid_to(address) # BigDecimal - sum of all payments to address
160
164
  mock.transaction_count # Integer
161
- mock.balance # BigDecimal current balance
165
+ mock.balance # BigDecimal - current balance
162
166
  mock.transactions # Array<Transaction>
163
167
  mock.set_balance(amount) # Override starting balance
164
168
  mock.reset # Clear all state
@@ -274,11 +278,11 @@ Remitmd::RemitWallet.new(private_key: key, chain: "base_sepolia") # Base Sepoli
274
278
  Permits are auto-signed by default. If you need manual control (custom deadline, pre-signed permits, or offline signing), pass a `PermitSignature` explicitly:
275
279
 
276
280
  ```ruby
277
- # sign_permit: convenience auto-fetches nonce, converts amount to base units
281
+ # sign_permit: convenience - auto-fetches nonce, converts amount to base units
278
282
  permit = wallet.sign_permit("0xRouterAddress...", 5.00, deadline: Time.now.to_i + 7200)
279
283
  tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
280
284
 
281
- # sign_usdc_permit: full control raw base units, explicit nonce
285
+ # sign_usdc_permit: full control - raw base units, explicit nonce
282
286
  permit = wallet.sign_usdc_permit(
283
287
  "0xRouterAddress...", # spender
284
288
  5_000_000, # value in base units (6 decimals)
@@ -291,6 +295,6 @@ tx = wallet.pay("0xRecipient...", 5.00, permit: permit)
291
295
 
292
296
  ## License
293
297
 
294
- MIT see [LICENSE](LICENSE)
298
+ MIT - see [LICENSE](LICENSE)
295
299
 
296
300
  [Documentation](https://remit.md/docs) · [Protocol Spec](https://remit.md) · [GitHub](https://github.com/remit-md/sdk)
data/lib/remitmd/a2a.rb CHANGED
@@ -135,7 +135,7 @@ module Remitmd
135
135
 
136
136
  # ─── A2A JSON-RPC client ────────────────────────────────────────────────────
137
137
 
138
- # A2A JSON-RPC client send payments and manage tasks via the A2A protocol.
138
+ # A2A JSON-RPC client - send payments and manage tasks via the A2A protocol.
139
139
  #
140
140
  # @example
141
141
  # card = Remitmd::AgentCard.discover("https://remit.md")
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+
5
+ module Remitmd
6
+ # Signer backed by the `remit sign` CLI command.
7
+ #
8
+ # No key material in this process -- signing happens in a subprocess.
9
+ # Address is cached at construction time via `remit address`.
10
+ #
11
+ # @example
12
+ # signer = Remitmd::CliSigner.new
13
+ # wallet = Remitmd::RemitWallet.new(signer: signer, chain: "base")
14
+ #
15
+ class CliSigner
16
+ include Signer
17
+
18
+ # Default timeout for CLI subprocess calls (seconds).
19
+ CLI_TIMEOUT = 10
20
+
21
+ # Create a CliSigner, fetching and caching the wallet address.
22
+ #
23
+ # @param cli_path [String] path or name of the remit CLI binary (default: "remit")
24
+ # @raise [RemitError] if the CLI fails or returns an invalid address
25
+ def initialize(cli_path: "remit")
26
+ @cli_path = cli_path
27
+ @address = fetch_address
28
+ end
29
+
30
+ # Sign a 32-byte digest (raw binary bytes).
31
+ # Pipes the hex-encoded digest to `remit sign --digest` on stdin.
32
+ # Returns a 0x-prefixed 65-byte hex signature.
33
+ #
34
+ # @param digest_bytes [String] 32-byte binary digest
35
+ # @return [String] 0x-prefixed 130-char hex signature (65 bytes)
36
+ # @raise [RemitError] on CLI failure or invalid output
37
+ def sign(digest_bytes)
38
+ hex = digest_bytes.unpack1("H*")
39
+ stdout, stderr, status = run_cli("sign", "--digest", stdin_data: hex)
40
+
41
+ unless status.success?
42
+ raise RemitError.new(
43
+ RemitError::SERVER_ERROR,
44
+ "CliSigner: signing failed: #{stderr.strip}"
45
+ )
46
+ end
47
+
48
+ sig = stdout.strip
49
+ unless sig.start_with?("0x") && sig.length == 132
50
+ raise RemitError.new(
51
+ RemitError::SERVER_ERROR,
52
+ "CliSigner: invalid signature from CLI: #{sig}"
53
+ )
54
+ end
55
+
56
+ sig
57
+ end
58
+
59
+ # The cached Ethereum address (0x-prefixed).
60
+ # @return [String]
61
+ attr_reader :address
62
+
63
+ # Never expose internals in inspect/to_s output.
64
+ def inspect
65
+ "#<Remitmd::CliSigner address=#{@address}>"
66
+ end
67
+
68
+ alias to_s inspect
69
+
70
+ # Check all three conditions for CliSigner activation.
71
+ #
72
+ # 1. CLI binary found on PATH (via `which` / `where`)
73
+ # 2. Keystore file exists at ~/.remit/keys/default.enc
74
+ # 3. REMIT_KEY_PASSWORD env var is set
75
+ #
76
+ # @param cli_path [String] path or name of the remit CLI binary
77
+ # @return [Boolean]
78
+ def self.available?(cli_path: "remit")
79
+ # 1. CLI binary on PATH
80
+ which_cmd = Gem.win_platform? ? "where" : "which"
81
+ _out, _err, st = Open3.capture3(which_cmd, cli_path)
82
+ return false unless st.success?
83
+
84
+ # 2. Keystore file exists
85
+ keystore = File.join(Dir.home, ".remit", "keys", "default.enc")
86
+ return false unless File.exist?(keystore)
87
+
88
+ # 3. REMIT_KEY_PASSWORD set
89
+ password = ENV["REMIT_KEY_PASSWORD"]
90
+ return false if password.nil? || password.empty?
91
+
92
+ true
93
+ end
94
+
95
+ private
96
+
97
+ # Fetch the wallet address from `remit address` during construction.
98
+ # @return [String] the 0x-prefixed Ethereum address
99
+ # @raise [RemitError] on any failure
100
+ def fetch_address
101
+ stdout, stderr, status = run_cli("address")
102
+
103
+ unless status.success?
104
+ raise RemitError.new(
105
+ RemitError::SERVER_ERROR,
106
+ "CliSigner: failed to get address: #{stderr.strip}"
107
+ )
108
+ end
109
+
110
+ addr = stdout.strip
111
+ unless addr.match?(/\A0x[0-9a-fA-F]{40}\z/)
112
+ raise RemitError.new(
113
+ RemitError::SERVER_ERROR,
114
+ "CliSigner: invalid address from CLI: #{addr}"
115
+ )
116
+ end
117
+
118
+ addr
119
+ end
120
+
121
+ # Run the remit CLI with given arguments.
122
+ # @param args [Array<String>] CLI arguments
123
+ # @param stdin_data [String, nil] data to pipe to stdin
124
+ # @return [Array<String, String, Process::Status>] stdout, stderr, status
125
+ # @raise [RemitError] on timeout or execution failure
126
+ def run_cli(*args, stdin_data: nil)
127
+ Open3.capture3(@cli_path, *args, stdin_data: stdin_data.to_s)
128
+ rescue Errno::ENOENT
129
+ raise RemitError.new(
130
+ RemitError::SERVER_ERROR,
131
+ "CliSigner: remit CLI not found at '#{@cli_path}'. " \
132
+ "Install: https://remit.md/install"
133
+ )
134
+ end
135
+ end
136
+ end
@@ -15,7 +15,7 @@ module Remitmd
15
15
  # puts e.doc_url # => "https://remit.md/docs/api-reference/error-codes#invalid_address"
16
16
  # end
17
17
  class RemitError < StandardError
18
- # Error code constants matches TS SDK (28 codes)
18
+ # Error code constants - matches TS SDK (28 codes)
19
19
  # Auth errors
20
20
  INVALID_SIGNATURE = "INVALID_SIGNATURE"
21
21
  NONCE_REUSED = "NONCE_REUSED"
@@ -80,7 +80,7 @@ module Remitmd
80
80
  @code = code
81
81
  @doc_url = "https://remit.md/docs/api-reference/error-codes##{code.downcase}"
82
82
  @context = context
83
- super("[#{code}] #{message} #{@doc_url}")
83
+ super("[#{code}] #{message} - #{@doc_url}")
84
84
  end
85
85
  end
86
86
  end
data/lib/remitmd/http.rb CHANGED
@@ -74,7 +74,7 @@ module Remitmd
74
74
  nonce_hex = "0x#{nonce_bytes.unpack1("H*")}"
75
75
  timestamp = Time.now.to_i
76
76
 
77
- # Strip query string before signing only the path is included in EIP-712.
77
+ # Strip query string before signing - only the path is included in EIP-712.
78
78
  sign_path = full_path.split("?").first
79
79
  http_method = method.to_s.upcase
80
80
  digest = eip712_hash(http_method, sign_path, timestamp, nonce_bytes)
@@ -100,7 +100,7 @@ module Remitmd
100
100
  # Domain: name="remit.md", version="0.1", chainId, verifyingContract
101
101
  # Struct: APIRequest(string method, string path, uint256 timestamp, bytes32 nonce)
102
102
  def eip712_hash(method, path, timestamp, nonce_bytes)
103
- # Type hashes (string constants keccak256 of the type string)
103
+ # Type hashes (string constants - keccak256 of the type string)
104
104
  domain_type_hash = keccak256_bytes(
105
105
  "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"
106
106
  )
@@ -162,7 +162,7 @@ module Remitmd
162
162
  raise RemitError.new(code, err["message"] || "Bad request", context: parsed)
163
163
  when 401
164
164
  raise RemitError.new(RemitError::UNAUTHORIZED,
165
- "Authentication failed check your private key and chain ID")
165
+ "Authentication failed - check your private key and chain ID")
166
166
  when 429
167
167
  raise RemitError.new(RemitError::RATE_LIMITED,
168
168
  "Rate limit exceeded. See https://remit.md/docs/api-reference/rate-limits")
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Remitmd
4
- # Pure-Ruby Keccak-256 (Ethereum variant NOT SHA-3).
4
+ # Pure-Ruby Keccak-256 (Ethereum variant - NOT SHA-3).
5
5
  #
6
6
  # SHA-3 uses different padding (0x06 instead of 0x01).
7
7
  # This implementation matches Ethereum's keccak256.
data/lib/remitmd/mock.rb CHANGED
@@ -175,7 +175,7 @@ module Remitmd
175
175
  @state[:pending_invoices][id] = b
176
176
  { "id" => id, "status" => "pending" }
177
177
 
178
- # Escrow create (step 2 fund with invoice_id)
178
+ # Escrow create (step 2 - fund with invoice_id)
179
179
  in ["POST", "/escrows"]
180
180
  invoice_id = fetch!(b, :invoice_id)
181
181
  inv = @state[:pending_invoices].delete(invoice_id)
@@ -4,12 +4,12 @@ require "openssl"
4
4
  require "securerandom"
5
5
 
6
6
  module Remitmd
7
- # secp256k1 field prime p (constant never changes)
7
+ # secp256k1 field prime p (constant - never changes)
8
8
  SECP256K1_P = OpenSSL::BN.new(
9
9
  "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F", 16
10
10
  ).freeze
11
11
 
12
- # Precomputed (p + 1) / 4 the modular square root exponent for p ≡ 3 (mod 4).
12
+ # Precomputed (p + 1) / 4 - the modular square root exponent for p ≡ 3 (mod 4).
13
13
  # Avoids BN division at runtime (which returns Integer on some OpenSSL versions).
14
14
  SECP256K1_SQRT_EXP = OpenSSL::BN.new(
15
15
  "3FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFBFFFFF0C", 16
@@ -66,7 +66,7 @@ module Remitmd
66
66
  group = @key.group
67
67
  n = group.order
68
68
 
69
- # ECDSA sign dsa_sign_asn1 uses the input directly as the hash (no pre-hashing)
69
+ # ECDSA sign - dsa_sign_asn1 uses the input directly as the hash (no pre-hashing)
70
70
  der = @key.dsa_sign_asn1(digest_bytes)
71
71
  asn1 = OpenSSL::ASN1.decode(der)
72
72
  bn_r = asn1.value[0].value
@@ -104,7 +104,7 @@ module Remitmd
104
104
  break
105
105
  end
106
106
  end
107
- raise "Could not determine recovery ID key or hash may be invalid" if v.nil?
107
+ raise "Could not determine recovery ID - key or hash may be invalid" if v.nil?
108
108
 
109
109
  # Build 65-byte Ethereum signature: r (32) || s (32) || v (1)
110
110
  r_bytes = [bn_r.to_s(16).rjust(64, "0")].pack("H*")
@@ -129,13 +129,13 @@ module Remitmd
129
129
  def recover_r_point(group, bn_r, parity)
130
130
  p = SECP256K1_P
131
131
  x = bn_r
132
- # y² = x³ + 7 (mod p) secp256k1 curve equation
132
+ # y² = x³ + 7 (mod p) - secp256k1 curve equation
133
133
  x3 = x.mod_exp(OpenSSL::BN.new("3"), p)
134
134
  rhs = x3 + OpenSSL::BN.new("7")
135
135
  y_squared = rhs % p
136
136
  # Tonelli–Shanks: since p ≡ 3 mod 4, sqrt = y²^((p+1)/4) mod p
137
137
  y = y_squared.mod_exp(SECP256K1_SQRT_EXP, p)
138
- # Verify that y² ≡ y_squared (mod p) i.e., a square root exists
138
+ # Verify that y² ≡ y_squared (mod p) - i.e., a square root exists
139
139
  return nil unless y.mod_mul(y, p) == y_squared
140
140
 
141
141
  y = p - y if (y.to_i & 1) != parity
@@ -148,7 +148,7 @@ module Remitmd
148
148
  end
149
149
 
150
150
  def derive_address(public_key)
151
- # Uncompressed public key: 04 || x (32) || y (32) skip the 0x04 prefix
151
+ # Uncompressed public key: 04 || x (32) || y (32) - skip the 0x04 prefix
152
152
  pub_bytes = [public_key.to_octet_string(:uncompressed).unpack1("H*")[2..]].pack("H*")
153
153
  keccak = keccak256_hex(pub_bytes)
154
154
  "0x#{keccak[-40..]}"
@@ -28,7 +28,7 @@ module Remitmd
28
28
 
29
29
  # @param private_key [String, nil] 0x-prefixed hex private key
30
30
  # @param signer [Signer, nil] custom signer (pass instead of private_key)
31
- # @param chain [String] chain name "base", "base_sepolia"
31
+ # @param chain [String] chain name - "base", "base_sepolia"
32
32
  # @param api_url [String, nil] override API base URL
33
33
  # @param transport [Object, nil] inject mock transport (used by MockRemit)
34
34
  def initialize(private_key: nil, signer: nil, chain: "base", api_url: nil, router_address: nil, transport: nil)
@@ -67,21 +67,16 @@ module Remitmd
67
67
  end
68
68
 
69
69
  # Build a RemitWallet from environment variables.
70
- # Reads: REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN (preferred, uses HttpSigner),
71
- # or REMITMD_KEY (primary) / REMITMD_PRIVATE_KEY (deprecated fallback).
70
+ # Priority: CliSigner (if available) > REMITMD_KEY / REMITMD_PRIVATE_KEY.
72
71
  # Also reads: REMITMD_CHAIN, REMITMD_API_URL, REMITMD_ROUTER_ADDRESS.
73
72
  def self.from_env
74
73
  chain = ENV.fetch("REMITMD_CHAIN", "base")
75
74
  api_url = ENV["REMITMD_API_URL"]
76
75
  router_address = ENV["REMITMD_ROUTER_ADDRESS"]
77
76
 
78
- # Priority 1: HTTP signer server
79
- signer_url = ENV["REMIT_SIGNER_URL"]
80
- if signer_url
81
- signer_token = ENV["REMIT_SIGNER_TOKEN"]
82
- raise ArgumentError, "REMIT_SIGNER_TOKEN must be set when REMIT_SIGNER_URL is set" unless signer_token
83
-
84
- signer = HttpSigner.new(url: signer_url, token: signer_token)
77
+ # Priority 1: CLI signer (remit binary + keystore + password)
78
+ if CliSigner.available?
79
+ signer = CliSigner.new
85
80
  return new(signer: signer, chain: chain, api_url: api_url, router_address: router_address)
86
81
  end
87
82
 
@@ -90,7 +85,16 @@ module Remitmd
90
85
  if ENV["REMITMD_PRIVATE_KEY"] && !ENV["REMITMD_KEY"]
91
86
  warn "[remitmd] REMITMD_PRIVATE_KEY is deprecated, use REMITMD_KEY instead"
92
87
  end
93
- raise ArgumentError, "REMITMD_KEY not set (or set REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)" unless key
88
+
89
+ unless key
90
+ raise ArgumentError,
91
+ "No signing method available. Either:\n" \
92
+ " 1. Install the remit CLI and set REMIT_KEY_PASSWORD:\n" \
93
+ " macOS: brew install remit-md/tap/remit\n" \
94
+ " Windows: winget install remit-md.remit\n" \
95
+ " Linux: curl -fsSL https://remit.md/install.sh | sh\n" \
96
+ " 2. Set REMITMD_KEY to a raw private key (hex)"
97
+ end
94
98
 
95
99
  new(private_key: key, chain: chain, api_url: api_url, router_address: router_address)
96
100
  end
@@ -157,7 +161,7 @@ module Remitmd
157
161
  # @param to [String] recipient 0x-prefixed address
158
162
  # @param amount [Numeric, BigDecimal] amount in USDC (e.g. 1.50)
159
163
  # @param memo [String, nil] optional note
160
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
164
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
161
165
  # @return [Transaction]
162
166
  def pay(to, amount, memo: nil, permit: nil)
163
167
  validate_address!(to)
@@ -176,7 +180,7 @@ module Remitmd
176
180
  # @param amount [Numeric] amount in USDC
177
181
  # @param memo [String, nil] optional note
178
182
  # @param expires_in_secs [Integer, nil] optional expiry in seconds from now
179
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
183
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
180
184
  # @return [Escrow]
181
185
  def create_escrow(payee, amount, memo: nil, expires_in_secs: nil, permit: nil)
182
186
  validate_address!(payee)
@@ -237,7 +241,7 @@ module Remitmd
237
241
  # @param limit_amount [Numeric] maximum tab credit in USDC
238
242
  # @param per_unit [Numeric] USDC per API call
239
243
  # @param expires_in_secs [Integer] optional expiry duration in seconds (default: 86400)
240
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
244
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
241
245
  # @return [Tab]
242
246
  def create_tab(provider, limit_amount, per_unit = 0.0, expires_in_secs: 86_400, permit: nil)
243
247
  validate_address!(provider)
@@ -430,7 +434,7 @@ module Remitmd
430
434
  # @param payee [String] 0x-prefixed address of the stream recipient
431
435
  # @param rate_per_second [Numeric] USDC per second
432
436
  # @param max_total [Numeric] maximum total USDC for the stream
433
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
437
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
434
438
  # @return [Stream]
435
439
  def create_stream(payee, rate_per_second, max_total, permit: nil)
436
440
  validate_address!(payee)
@@ -468,7 +472,7 @@ module Remitmd
468
472
  # @param task_description [String] task description
469
473
  # @param deadline [Integer] deadline as Unix timestamp
470
474
  # @param max_attempts [Integer] maximum submission attempts (default: 10)
471
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
475
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
472
476
  # @return [Bounty]
473
477
  def create_bounty(amount, task_description, deadline, max_attempts: 10, permit: nil)
474
478
  validate_amount!(amount)
@@ -522,7 +526,7 @@ module Remitmd
522
526
  # @param provider [String] 0x-prefixed provider address
523
527
  # @param amount [Numeric] amount in USDC
524
528
  # @param expires_in_secs [Integer] expiry duration in seconds (default: 3600)
525
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
529
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
526
530
  # @return [Deposit]
527
531
  def place_deposit(provider, amount, expires_in_secs: 3600, permit: nil)
528
532
  validate_address!(provider)
@@ -556,7 +560,7 @@ module Remitmd
556
560
  # Propose a payment intent for counterpart approval before execution.
557
561
  # @param to [String] 0x-prefixed address
558
562
  # @param amount [Numeric] amount in USDC
559
- # @param type [String] payment type "direct", "escrow", "tab"
563
+ # @param type [String] payment type - "direct", "escrow", "tab"
560
564
  # @return [Intent]
561
565
  def propose_intent(to, amount, type: "direct")
562
566
  validate_address!(to)
@@ -583,7 +587,7 @@ module Remitmd
583
587
  # Generate a one-time URL for the operator to fund this wallet.
584
588
  # @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
585
589
  # @param agent_name [String, nil] agent display name shown on the funding page
586
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
590
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
587
591
  # @return [LinkResponse]
588
592
  def create_fund_link(messages: nil, agent_name: nil, permit: nil)
589
593
  body = {}
@@ -601,7 +605,7 @@ module Remitmd
601
605
  # Generate a one-time URL for the operator to withdraw funds.
602
606
  # @param messages [Array<Hash>, nil] chat-style messages (each with :role and :text)
603
607
  # @param agent_name [String, nil] agent display name shown on the withdraw page
604
- # @param permit [PermitSignature, nil] EIP-2612 permit auto-signed if nil
608
+ # @param permit [PermitSignature, nil] EIP-2612 permit - auto-signed if nil
605
609
  # @return [LinkResponse]
606
610
  def create_withdraw_link(messages: nil, agent_name: nil, permit: nil)
607
611
  body = {}
@@ -710,7 +714,7 @@ module Remitmd
710
714
 
711
715
  # Auto-sign a permit for the given contract type and amount.
712
716
  # Returns nil on failure instead of raising, so callers can proceed without a permit.
713
- # @param contract [String] contract key "router", "escrow", "tab", etc.
717
+ # @param contract [String] contract key - "router", "escrow", "tab", etc.
714
718
  # @param amount [Numeric] amount in USDC
715
719
  # @return [PermitSignature, nil]
716
720
  def auto_permit(contract, amount)
@@ -21,7 +21,7 @@ module Remitmd
21
21
  end
22
22
  end
23
23
 
24
- # x402 client fetch wrapper that auto-pays HTTP 402 Payment Required responses.
24
+ # x402 client - fetch wrapper that auto-pays HTTP 402 Payment Required responses.
25
25
  #
26
26
  # On receiving a 402, the client:
27
27
  # 1. Decodes the PAYMENT-REQUIRED header (base64 JSON)
@@ -6,7 +6,7 @@ require "json"
6
6
  require "base64"
7
7
 
8
8
  module Remitmd
9
- # x402 paywall for service providers gate HTTP endpoints behind payments.
9
+ # x402 paywall for service providers - gate HTTP endpoints behind payments.
10
10
  #
11
11
  # Providers use this class to:
12
12
  # - Return HTTP 402 responses with properly formatted PAYMENT-REQUIRED headers
@@ -29,9 +29,9 @@ module Remitmd
29
29
  # @param facilitator_url [String] base URL of the remit.md facilitator
30
30
  # @param facilitator_token [String] bearer JWT for authenticating calls to /api/v1/x402/verify
31
31
  # @param max_timeout_seconds [Integer] how long the payment authorization remains valid
32
- # @param resource [String, nil] V2 URL or path of the resource being protected
33
- # @param description [String, nil] V2 human-readable description
34
- # @param mime_type [String, nil] V2 MIME type of the resource
32
+ # @param resource [String, nil] V2 - URL or path of the resource being protected
33
+ # @param description [String, nil] V2 - human-readable description
34
+ # @param mime_type [String, nil] V2 - MIME type of the resource
35
35
  def initialize( # rubocop:disable Metrics/ParameterLists
36
36
  wallet_address:,
37
37
  amount_usdc:,
data/lib/remitmd.rb CHANGED
@@ -4,7 +4,7 @@ require_relative "remitmd/errors"
4
4
  require_relative "remitmd/models"
5
5
  require_relative "remitmd/keccak"
6
6
  require_relative "remitmd/signer"
7
- require_relative "remitmd/http_signer"
7
+ require_relative "remitmd/cli_signer"
8
8
  require_relative "remitmd/http"
9
9
  require_relative "remitmd/wallet"
10
10
  require_relative "remitmd/mock"
@@ -12,7 +12,7 @@ require_relative "remitmd/a2a"
12
12
  require_relative "remitmd/x402_client"
13
13
  require_relative "remitmd/x402_paywall"
14
14
 
15
- # remit.md Ruby SDK universal payment protocol for AI agents.
15
+ # remit.md Ruby SDK - universal payment protocol for AI agents.
16
16
  #
17
17
  # @example Direct payment
18
18
  # wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_PRIVATE_KEY"])
@@ -26,5 +26,5 @@ require_relative "remitmd/x402_paywall"
26
26
  # mock.was_paid?("0x0000000000000000000000000000000000000001", 1.00) # => true
27
27
  #
28
28
  module Remitmd
29
- VERSION = "0.1.8"
29
+ VERSION = "0.2.1"
30
30
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: remitmd
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.8
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - remit.md
@@ -51,9 +51,9 @@ files:
51
51
  - README.md
52
52
  - lib/remitmd.rb
53
53
  - lib/remitmd/a2a.rb
54
+ - lib/remitmd/cli_signer.rb
54
55
  - lib/remitmd/errors.rb
55
56
  - lib/remitmd/http.rb
56
- - lib/remitmd/http_signer.rb
57
57
  - lib/remitmd/keccak.rb
58
58
  - lib/remitmd/mock.rb
59
59
  - lib/remitmd/models.rb
@@ -1,200 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "net/http"
4
- require "uri"
5
- require "json"
6
-
7
- module Remitmd
8
- # Signer backed by a local HTTP signing server.
9
- #
10
- # Delegates digest signing to an HTTP server (typically
11
- # `http://127.0.0.1:7402`). The signer server holds the encrypted key;
12
- # this adapter only needs a bearer token and URL.
13
- #
14
- # @example
15
- # signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
16
- # wallet = Remitmd::RemitWallet.new(signer: signer, chain: "base")
17
- #
18
- class HttpSigner
19
- include Signer
20
-
21
- # Create an HttpSigner, fetching and caching the wallet address.
22
- #
23
- # @param url [String] signer server URL (e.g. "http://127.0.0.1:7402")
24
- # @param token [String] bearer token for authentication
25
- # @raise [RemitError] if the server is unreachable, returns an error, or returns no address
26
- def initialize(url:, token:)
27
- @url = url.chomp("/")
28
- @token = token
29
- @address = fetch_address
30
- end
31
-
32
- # Sign a 32-byte digest (raw binary bytes).
33
- # Posts to /sign/digest with the hex-encoded digest.
34
- # Returns a 0x-prefixed 65-byte hex signature.
35
- #
36
- # @param digest_bytes [String] 32-byte binary digest
37
- # @return [String] 0x-prefixed 65-byte hex signature
38
- # @raise [RemitError] on network, auth, policy, or server errors
39
- def sign(digest_bytes)
40
- hex = "0x#{digest_bytes.unpack1("H*")}"
41
- uri = URI("#{@url}/sign/digest")
42
- http = build_http(uri)
43
-
44
- req = Net::HTTP::Post.new(uri.path)
45
- req["Content-Type"] = "application/json"
46
- req["Authorization"] = "Bearer #{@token}"
47
- req.body = { digest: hex }.to_json
48
-
49
- resp = begin
50
- http.request(req)
51
- rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
52
- raise RemitError.new(
53
- RemitError::NETWORK_ERROR,
54
- "HttpSigner: cannot reach signer server at #{@url}: #{e.message}"
55
- )
56
- end
57
-
58
- handle_sign_response(resp)
59
- end
60
-
61
- # The cached Ethereum address (0x-prefixed).
62
- # @return [String]
63
- attr_reader :address
64
-
65
- # Never expose the bearer token in inspect/to_s output.
66
- def inspect
67
- "#<Remitmd::HttpSigner address=#{@address}>"
68
- end
69
-
70
- alias to_s inspect
71
-
72
- private
73
-
74
- # Fetch the wallet address from GET /address during construction.
75
- # @return [String] the 0x-prefixed Ethereum address
76
- # @raise [RemitError] on any failure
77
- def fetch_address
78
- uri = URI("#{@url}/address")
79
- http = build_http(uri)
80
-
81
- req = Net::HTTP::Get.new(uri.path)
82
- req["Authorization"] = "Bearer #{@token}"
83
-
84
- resp = begin
85
- http.request(req)
86
- rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, SocketError => e
87
- raise RemitError.new(
88
- RemitError::NETWORK_ERROR,
89
- "HttpSigner: cannot reach signer server at #{@url}: #{e.message}"
90
- )
91
- end
92
-
93
- status = resp.code.to_i
94
-
95
- if status == 401
96
- raise RemitError.new(
97
- RemitError::UNAUTHORIZED,
98
- "HttpSigner: unauthorized -- check your REMIT_SIGNER_TOKEN"
99
- )
100
- end
101
-
102
- unless (200..299).cover?(status)
103
- raise RemitError.new(
104
- RemitError::SERVER_ERROR,
105
- "HttpSigner: GET /address failed (#{status})"
106
- )
107
- end
108
-
109
- body = begin
110
- JSON.parse(resp.body.to_s)
111
- rescue JSON::ParserError
112
- raise RemitError.new(
113
- RemitError::SERVER_ERROR,
114
- "HttpSigner: GET /address returned malformed JSON"
115
- )
116
- end
117
-
118
- addr = body["address"]
119
- if addr.nil? || addr.to_s.empty?
120
- raise RemitError.new(
121
- RemitError::SERVER_ERROR,
122
- "HttpSigner: GET /address returned no address"
123
- )
124
- end
125
-
126
- addr.to_s
127
- end
128
-
129
- # Handle the response from POST /sign/digest.
130
- # @param resp [Net::HTTPResponse]
131
- # @return [String] the 0x-prefixed hex signature
132
- # @raise [RemitError] on any error
133
- def handle_sign_response(resp)
134
- status = resp.code.to_i
135
-
136
- if status == 401
137
- raise RemitError.new(
138
- RemitError::UNAUTHORIZED,
139
- "HttpSigner: unauthorized -- check your REMIT_SIGNER_TOKEN"
140
- )
141
- end
142
-
143
- if status == 403
144
- reason = begin
145
- data = JSON.parse(resp.body.to_s)
146
- data["reason"] || "unknown"
147
- rescue JSON::ParserError
148
- "unknown"
149
- end
150
- raise RemitError.new(
151
- RemitError::UNAUTHORIZED,
152
- "HttpSigner: policy denied -- #{reason}"
153
- )
154
- end
155
-
156
- unless (200..299).cover?(status)
157
- detail = begin
158
- data = JSON.parse(resp.body.to_s)
159
- data["reason"] || data["error"] || "server error"
160
- rescue JSON::ParserError
161
- "server error"
162
- end
163
- raise RemitError.new(
164
- RemitError::SERVER_ERROR,
165
- "HttpSigner: sign failed (#{status}): #{detail}"
166
- )
167
- end
168
-
169
- body = begin
170
- JSON.parse(resp.body.to_s)
171
- rescue JSON::ParserError
172
- raise RemitError.new(
173
- RemitError::SERVER_ERROR,
174
- "HttpSigner: POST /sign/digest returned malformed JSON"
175
- )
176
- end
177
-
178
- sig = body["signature"]
179
- if sig.nil? || sig.to_s.empty?
180
- raise RemitError.new(
181
- RemitError::SERVER_ERROR,
182
- "HttpSigner: server returned no signature"
183
- )
184
- end
185
-
186
- sig.to_s
187
- end
188
-
189
- # Build a Net::HTTP client for the given URI.
190
- # @param uri [URI] the target URI
191
- # @return [Net::HTTP]
192
- def build_http(uri)
193
- http = Net::HTTP.new(uri.host, uri.port)
194
- http.use_ssl = uri.scheme == "https"
195
- http.open_timeout = 5
196
- http.read_timeout = 10
197
- http
198
- end
199
- end
200
- end