remitmd 0.1.6 → 0.1.8

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: 77a3a0333d3f1afc8315ad1d523bc9adb70e74d2f5af29bc20b608233b0f67b1
4
- data.tar.gz: b04b273b6340c25f8adcc5e755e99a4f32c9e804aec91d2e28058a66f507e9e9
3
+ metadata.gz: 8c2b03f3cb2f1a05e9a33845448154e4d34fda42f65af31823be2f30fb14b6fe
4
+ data.tar.gz: 3d6dc094058152593779b5430899400d7b9d6543693ee3dd35dfd08b88f4c131
5
5
  SHA512:
6
- metadata.gz: 3e343856eb6688f7a5bee1a10fa85ae119e9ac548eeedfb4bb95a21f4a5565d8ad4921efccfcad940f3f8a89fb7a0fd313243c7dd511f132c0540b69e05b2f82
7
- data.tar.gz: 284848e90b0c9c53bc80a07fa7a2bc90ea7f3e1afef282b10308a9fc5805b9c750392d75a6055bf152660d15a1b3948d06fddbf179f5daf2ae3b586f0e18a35a
6
+ metadata.gz: deca4edd8beb6ac2d798102a83838762d746f4508bf5e852bb1b43d74aed1fa24a7b328bc4880560fc5ab60f5bd2c937277123183c3c45cc430bf02407f5c5c8
7
+ data.tar.gz: c8b3bd0dc87584d00abe0b3acb37a52fa16f1f9af337780dd6aecc861f2cd542d2d684d636b28bef6b0227c13002c3c70388809d2e38a2874255bde0dd1c3932
data/README.md CHANGED
@@ -24,7 +24,7 @@ gem install remitmd
24
24
  ```ruby
25
25
  require "remitmd"
26
26
 
27
- wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_PRIVATE_KEY"])
27
+ wallet = Remitmd::RemitWallet.new(private_key: ENV["REMITMD_KEY"])
28
28
 
29
29
  # Direct payment
30
30
  tx = wallet.pay("0xRecipient0000000000000000000000000000001", 1.50)
@@ -39,12 +39,32 @@ Or from environment variables:
39
39
 
40
40
  ```ruby
41
41
  wallet = Remitmd::RemitWallet.from_env
42
- # Requires: REMITMD_PRIVATE_KEY
42
+ # Requires: REMITMD_KEY (or REMIT_SIGNER_URL + REMIT_SIGNER_TOKEN)
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)
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.
51
+
52
+ ```bash
53
+ export REMIT_SIGNER_URL=http://127.0.0.1:7402
54
+ export REMIT_SIGNER_TOKEN=rmit_sk_...
55
+ ```
56
+
57
+ ```ruby
58
+ # Explicit
59
+ signer = Remitmd::HttpSigner.new(url: "http://127.0.0.1:7402", token: "rmit_sk_...")
60
+ wallet = Remitmd::RemitWallet.new(signer: signer)
61
+
62
+ # Or auto-detect from env (recommended)
63
+ wallet = Remitmd::RemitWallet.from_env # detects REMIT_SIGNER_URL automatically
64
+ ```
65
+
66
+ `RemitWallet.from_env` detects signer credentials automatically. Priority: `REMIT_SIGNER_URL` > `REMITMD_KEY`.
67
+
48
68
  ## Payment Models
49
69
 
50
70
  ### Direct Payment
data/lib/remitmd/a2a.rb CHANGED
@@ -4,7 +4,7 @@ require "net/http"
4
4
  require "json"
5
5
  require "uri"
6
6
 
7
- module RemitMd
7
+ module Remitmd
8
8
  # A2A capability extension declared in an agent card.
9
9
  AgentExtension = Data.define(:uri, :description, :required)
10
10
 
@@ -37,7 +37,7 @@ module RemitMd
37
37
 
38
38
  # Fetch and parse the A2A agent card from +base_url/.well-known/agent-card.json+.
39
39
  #
40
- # card = RemitMd::AgentCard.discover("https://remit.md")
40
+ # card = Remitmd::AgentCard.discover("https://remit.md")
41
41
  # puts card.name # => "remit.md"
42
42
  # puts card.url # => "https://remit.md/a2a"
43
43
  #
@@ -55,4 +55,190 @@ module RemitMd
55
55
  new(JSON.parse(response.body))
56
56
  end
57
57
  end
58
+
59
+ # ─── A2A task types ──────────────────────────────────────────────────────────
60
+
61
+ # Status of an A2A task.
62
+ class A2ATaskStatus
63
+ attr_reader :state, :message
64
+
65
+ def initialize(data)
66
+ @state = data["state"].to_s
67
+ @message = data["message"]
68
+ end
69
+ end
70
+
71
+ # An artifact part within an A2A artifact.
72
+ A2AArtifactPart = Data.define(:kind, :data)
73
+
74
+ # An artifact returned by an A2A task.
75
+ class A2AArtifact
76
+ attr_reader :name, :parts
77
+
78
+ def initialize(data)
79
+ @name = data["name"]
80
+ @parts = (data["parts"] || []).map do |p|
81
+ A2AArtifactPart.new(kind: p["kind"].to_s, data: p["data"] || {})
82
+ end
83
+ end
84
+ end
85
+
86
+ # An A2A task returned by message/send, tasks/get, or tasks/cancel.
87
+ class A2ATask
88
+ attr_reader :id, :status, :artifacts
89
+
90
+ def initialize(data)
91
+ @id = data["id"].to_s
92
+ @status = A2ATaskStatus.new(data["status"] || {})
93
+ @artifacts = (data["artifacts"] || []).map { |a| A2AArtifact.new(a) }
94
+ end
95
+
96
+ # Extract txHash from task artifacts, if present.
97
+ # @return [String, nil]
98
+ def tx_hash
99
+ artifacts.each do |artifact|
100
+ artifact.parts.each do |part|
101
+ tx = part.data["txHash"] if part.data.is_a?(Hash)
102
+ return tx if tx.is_a?(String)
103
+ end
104
+ end
105
+ nil
106
+ end
107
+ end
108
+
109
+ # ─── IntentMandate ──────────────────────────────────────────────────────────
110
+
111
+ # A mandate authorizing a payment intent.
112
+ class IntentMandate
113
+ attr_reader :mandate_id, :expires_at, :issuer, :max_amount, :currency
114
+
115
+ def initialize(mandate_id:, expires_at:, issuer:, max_amount:, currency: "USDC")
116
+ @mandate_id = mandate_id
117
+ @expires_at = expires_at
118
+ @issuer = issuer
119
+ @max_amount = max_amount
120
+ @currency = currency
121
+ end
122
+
123
+ def to_h
124
+ {
125
+ mandateId: @mandate_id,
126
+ expiresAt: @expires_at,
127
+ issuer: @issuer,
128
+ allowance: {
129
+ maxAmount: @max_amount,
130
+ currency: @currency,
131
+ },
132
+ }
133
+ end
134
+ end
135
+
136
+ # ─── A2A JSON-RPC client ────────────────────────────────────────────────────
137
+
138
+ # A2A JSON-RPC client — send payments and manage tasks via the A2A protocol.
139
+ #
140
+ # @example
141
+ # card = Remitmd::AgentCard.discover("https://remit.md")
142
+ # signer = Remitmd::PrivateKeySigner.new(ENV["REMITMD_KEY"])
143
+ # client = Remitmd::A2AClient.from_card(card, signer)
144
+ # task = client.send(to: "0xRecipient...", amount: 10)
145
+ # puts task.status.state
146
+ #
147
+ class A2AClient
148
+ CHAIN_IDS = {
149
+ "base" => 8453,
150
+ "base-sepolia" => 84_532,
151
+ }.freeze
152
+
153
+ # @param endpoint [String] full A2A endpoint URL from the agent card
154
+ # @param signer [#sign, #address] a signer for EIP-712 authentication
155
+ # @param chain_id [Integer] chain ID
156
+ # @param verifying_contract [String] verifying contract address
157
+ def initialize(endpoint:, signer:, chain_id:, verifying_contract: "")
158
+ parsed = URI(endpoint)
159
+ @base_url = "#{parsed.scheme}://#{parsed.host}#{parsed.port == parsed.default_port ? "" : ":#{parsed.port}"}"
160
+ @path = parsed.path.empty? ? "/a2a" : parsed.path
161
+ @transport = HttpTransport.new(
162
+ base_url: @base_url,
163
+ signer: signer,
164
+ chain_id: chain_id,
165
+ router_address: verifying_contract
166
+ )
167
+ end
168
+
169
+ # Convenience constructor from an AgentCard and a signer.
170
+ # @param card [AgentCard]
171
+ # @param signer [#sign, #address]
172
+ # @param chain [String] chain name (default: "base")
173
+ # @param verifying_contract [String]
174
+ # @return [A2AClient]
175
+ def self.from_card(card, signer, chain: "base", verifying_contract: "")
176
+ chain_id = CHAIN_IDS[chain] || CHAIN_IDS["base"]
177
+ new(
178
+ endpoint: card.url,
179
+ signer: signer,
180
+ chain_id: chain_id,
181
+ verifying_contract: verifying_contract
182
+ )
183
+ end
184
+
185
+ # Send a direct USDC payment via message/send.
186
+ # @param to [String] recipient 0x address
187
+ # @param amount [Numeric] amount in USDC
188
+ # @param memo [String] optional memo
189
+ # @param mandate [IntentMandate, nil] optional intent mandate
190
+ # @return [A2ATask]
191
+ def send(to:, amount:, memo: "", mandate: nil)
192
+ nonce = SecureRandom.hex(16)
193
+ message_id = SecureRandom.hex(16)
194
+
195
+ message = {
196
+ messageId: message_id,
197
+ role: "user",
198
+ parts: [
199
+ {
200
+ kind: "data",
201
+ data: {
202
+ model: "direct",
203
+ to: to,
204
+ amount: format("%.2f", amount),
205
+ memo: memo,
206
+ nonce: nonce,
207
+ },
208
+ },
209
+ ],
210
+ }
211
+
212
+ message[:metadata] = { mandate: mandate.to_h } if mandate
213
+
214
+ rpc("message/send", { message: message }, message_id)
215
+ end
216
+
217
+ # Fetch the current state of an A2A task by ID.
218
+ # @param task_id [String]
219
+ # @return [A2ATask]
220
+ def get_task(task_id)
221
+ rpc("tasks/get", { id: task_id }, task_id[0, 16])
222
+ end
223
+
224
+ # Cancel an in-progress A2A task.
225
+ # @param task_id [String]
226
+ # @return [A2ATask]
227
+ def cancel_task(task_id)
228
+ rpc("tasks/cancel", { id: task_id }, task_id[0, 16])
229
+ end
230
+
231
+ private
232
+
233
+ def rpc(method, params, call_id)
234
+ body = { jsonrpc: "2.0", id: call_id, method: method, params: params }
235
+ data = @transport.post(@path, body)
236
+ if data.is_a?(Hash) && data["error"]
237
+ err_msg = data["error"]["message"] || JSON.generate(data["error"])
238
+ raise RemitError.new("SERVER_ERROR", "A2A error: #{err_msg}")
239
+ end
240
+ result = data.is_a?(Hash) ? (data["result"] || data) : data
241
+ A2ATask.new(result)
242
+ end
243
+ end
58
244
  end
@@ -15,29 +15,64 @@ 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
19
- INVALID_ADDRESS = "INVALID_ADDRESS"
20
- INVALID_AMOUNT = "INVALID_AMOUNT"
21
- INSUFFICIENT_FUNDS = "INSUFFICIENT_FUNDS"
18
+ # Error code constants — matches TS SDK (28 codes)
19
+ # Auth errors
20
+ INVALID_SIGNATURE = "INVALID_SIGNATURE"
21
+ NONCE_REUSED = "NONCE_REUSED"
22
+ TIMESTAMP_EXPIRED = "TIMESTAMP_EXPIRED"
23
+ UNAUTHORIZED = "UNAUTHORIZED"
24
+
25
+ # Balance / funds
26
+ INSUFFICIENT_BALANCE = "INSUFFICIENT_BALANCE"
27
+ BELOW_MINIMUM = "BELOW_MINIMUM"
28
+
29
+ # Escrow errors
22
30
  ESCROW_NOT_FOUND = "ESCROW_NOT_FOUND"
31
+ ESCROW_ALREADY_FUNDED = "ESCROW_ALREADY_FUNDED"
32
+ ESCROW_EXPIRED = "ESCROW_EXPIRED"
33
+
34
+ # Invoice errors
35
+ INVALID_INVOICE = "INVALID_INVOICE"
36
+ DUPLICATE_INVOICE = "DUPLICATE_INVOICE"
37
+ SELF_PAYMENT = "SELF_PAYMENT"
38
+ INVALID_PAYMENT_TYPE = "INVALID_PAYMENT_TYPE"
39
+
40
+ # Tab errors
41
+ TAB_DEPLETED = "TAB_DEPLETED"
42
+ TAB_EXPIRED = "TAB_EXPIRED"
23
43
  TAB_NOT_FOUND = "TAB_NOT_FOUND"
44
+
45
+ # Stream errors
24
46
  STREAM_NOT_FOUND = "STREAM_NOT_FOUND"
47
+ RATE_EXCEEDS_CAP = "RATE_EXCEEDS_CAP"
48
+
49
+ # Bounty errors
50
+ BOUNTY_EXPIRED = "BOUNTY_EXPIRED"
51
+ BOUNTY_CLAIMED = "BOUNTY_CLAIMED"
52
+ BOUNTY_MAX_ATTEMPTS = "BOUNTY_MAX_ATTEMPTS"
25
53
  BOUNTY_NOT_FOUND = "BOUNTY_NOT_FOUND"
26
- DEPOSIT_NOT_FOUND = "DEPOSIT_NOT_FOUND"
27
- UNAUTHORIZED = "UNAUTHORIZED"
28
- RATE_LIMITED = "RATE_LIMITED"
54
+
55
+ # Chain errors
56
+ CHAIN_MISMATCH = "CHAIN_MISMATCH"
57
+ CHAIN_UNSUPPORTED = "CHAIN_UNSUPPORTED"
58
+
59
+ # Rate limiting
60
+ RATE_LIMITED = "RATE_LIMITED"
61
+
62
+ # Cancellation errors
63
+ CANCEL_BLOCKED_CLAIM_START = "CANCEL_BLOCKED_CLAIM_START"
64
+ CANCEL_BLOCKED_EVIDENCE = "CANCEL_BLOCKED_EVIDENCE"
65
+
66
+ # Protocol errors
67
+ VERSION_MISMATCH = "VERSION_MISMATCH"
29
68
  NETWORK_ERROR = "NETWORK_ERROR"
69
+
70
+ # Legacy aliases (kept for backward compat within the SDK)
71
+ INVALID_ADDRESS = "INVALID_ADDRESS"
72
+ INVALID_AMOUNT = "INVALID_AMOUNT"
73
+ INSUFFICIENT_FUNDS = INSUFFICIENT_BALANCE
30
74
  SERVER_ERROR = "SERVER_ERROR"
31
- NONCE_REUSED = "NONCE_REUSED"
32
- SIGNATURE_INVALID = "SIGNATURE_INVALID"
33
- ESCROW_ALREADY_COMPLETED = "ESCROW_ALREADY_COMPLETED"
34
- ESCROW_EXPIRED = "ESCROW_EXPIRED"
35
- TAB_LIMIT_EXCEEDED = "TAB_LIMIT_EXCEEDED"
36
- BOUNTY_ALREADY_AWARDED = "BOUNTY_ALREADY_AWARDED"
37
- STREAM_NOT_ACTIVE = "STREAM_NOT_ACTIVE"
38
- DEPOSIT_ALREADY_RESOLVED = "DEPOSIT_ALREADY_RESOLVED"
39
- USDC_TRANSFER_FAILED = "USDC_TRANSFER_FAILED"
40
- CHAIN_UNAVAILABLE = "CHAIN_UNAVAILABLE"
75
+ DEPOSIT_NOT_FOUND = "DEPOSIT_NOT_FOUND"
41
76
 
42
77
  attr_reader :code, :doc_url, :context
43
78
 
data/lib/remitmd/http.rb CHANGED
@@ -43,7 +43,7 @@ module Remitmd
43
43
  def request(method, path, body)
44
44
  attempt = 0
45
45
  # Generate idempotency key once per request (stable across retries).
46
- idempotency_key = (method == :post || method == :put || method == :patch) ? SecureRandom.uuid : nil
46
+ idempotency_key = %i[post put patch].include?(method) ? SecureRandom.uuid : nil
47
47
  begin
48
48
  attempt += 1
49
49
  req = build_request(method, path, body, idempotency_key)
@@ -74,9 +74,10 @@ module Remitmd
74
74
  nonce_hex = "0x#{nonce_bytes.unpack1("H*")}"
75
75
  timestamp = Time.now.to_i
76
76
 
77
- # Compute EIP-712 hash and sign it.
77
+ # Strip query string before signing — only the path is included in EIP-712.
78
+ sign_path = full_path.split("?").first
78
79
  http_method = method.to_s.upcase
79
- digest = eip712_hash(http_method, full_path, timestamp, nonce_bytes)
80
+ digest = eip712_hash(http_method, sign_path, timestamp, nonce_bytes)
80
81
  signature = @signer.sign(digest)
81
82
 
82
83
  req["Content-Type"] = "application/json"
@@ -155,8 +156,10 @@ module Remitmd
155
156
  when 200..299
156
157
  parsed
157
158
  when 400
158
- code = parsed["code"] || RemitError::SERVER_ERROR
159
- raise RemitError.new(code, parsed["message"] || "Bad request", context: parsed)
159
+ # Support nested error format: { "error": { "code": "...", "message": "..." } }
160
+ err = parsed.is_a?(Hash) && parsed["error"].is_a?(Hash) ? parsed["error"] : parsed
161
+ code = err["code"] || RemitError::SERVER_ERROR
162
+ raise RemitError.new(code, err["message"] || "Bad request", context: parsed)
160
163
  when 401
161
164
  raise RemitError.new(RemitError::UNAUTHORIZED,
162
165
  "Authentication failed — check your private key and chain ID")
@@ -0,0 +1,200 @@
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
@@ -77,7 +77,7 @@ module Remitmd
77
77
  RC.each { |rc| keccak_round!(state, rc) }
78
78
  end
79
79
 
80
- def keccak_round!(state, rc) # rubocop:disable Metrics/MethodLength
80
+ def keccak_round!(state, rc)
81
81
  # Theta
82
82
  c = Array.new(5) do |x|
83
83
  state[x] ^ state[x + 5] ^ state[x + 10] ^ state[x + 15] ^ state[x + 20]
data/lib/remitmd/mock.rb CHANGED
@@ -180,6 +180,7 @@ module Remitmd
180
180
  invoice_id = fetch!(b, :invoice_id)
181
181
  inv = @state[:pending_invoices].delete(invoice_id)
182
182
  raise not_found(RemitError::ESCROW_NOT_FOUND, invoice_id) unless inv
183
+
183
184
  payee = (inv[:to_agent] || inv["to_agent"]).to_s
184
185
  amount = decimal!(inv, :amount)
185
186
  memo = (inv[:task] || inv["task"]).to_s
@@ -310,7 +311,7 @@ module Remitmd
310
311
  # Bounty submit
311
312
  in ["POST", path] if path.end_with?("/submit") && path.include?("/bounties/")
312
313
  id = extract_id(path, "/bounties/", "/submit")
313
- bnt = @state[:bounties].fetch(id) { raise not_found(RemitError::BOUNTY_NOT_FOUND, id) }
314
+ @state[:bounties].fetch(id) { raise not_found(RemitError::BOUNTY_NOT_FOUND, id) }
314
315
  {
315
316
  "id" => 1,
316
317
  "bounty_id" => id,
@@ -322,9 +323,9 @@ module Remitmd
322
323
 
323
324
  # Bounty award
324
325
  in ["POST", path] if path.end_with?("/award") && path.include?("/bounties/")
325
- id = extract_id(path, "/bounties/", "/award")
326
- submission_id = (b[:submission_id] || b["submission_id"]).to_i
327
- bnt = @state[:bounties].fetch(id) { raise not_found(RemitError::BOUNTY_NOT_FOUND, id) }
326
+ id = extract_id(path, "/bounties/", "/award")
327
+ _submission_id = (b[:submission_id] || b["submission_id"]).to_i
328
+ bnt = @state[:bounties].fetch(id) { raise not_found(RemitError::BOUNTY_NOT_FOUND, id) }
328
329
  new_bnt = update_bounty(bnt, status: BountyStatus::AWARDED)
329
330
  @state[:bounties][id] = new_bnt
330
331
  bounty_hash(new_bnt)
@@ -620,6 +621,7 @@ module Remitmd
620
621
  def check_balance!(amount)
621
622
  bal = @state[:balance]
622
623
  return if bal >= amount
624
+
623
625
  raise RemitError.new(
624
626
  RemitError::INSUFFICIENT_FUNDS,
625
627
  "Insufficient balance: have #{bal.to_s("F")} USDC, need #{amount.to_s("F")} USDC",