rail0-sdk 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1304e38f4530a6bfdbfe8f3196ed562b4869b9e6ab7d455cae744afb21ad21ed
4
- data.tar.gz: 87090219ed6aef2133815ca4a704a5c7a8e926cead00e59688a42aac25f29297
3
+ metadata.gz: 5432df0776e08a77c56678e4e977d87c81ddf2be6176fef59a95051e287a6ce9
4
+ data.tar.gz: 7747a265cc8d84f243a09a8b1ab66e3cba545a3817554bc321dc5aa9eaed34be
5
5
  SHA512:
6
- metadata.gz: 77f22e52615cd7cac1b1535084a388111e20850e01a89a39628322bf825f8ea023c1db7e9d185bbb9c0616fa1827add2ebbdb2d8749cbf60fbfe099f6f0719f8
7
- data.tar.gz: ae3d34f5cceffb1ea59f33114b53c5308eb115b929bfa581717e7e3357fedbb38a527ef0a35136943eadbc4fa0e93a589ebbacff4f13c880f885c95869644a4a
6
+ metadata.gz: 440edc898e25a83ad3c76615777b4074f7daf003904db75129df01c4fcb223aebad7e5a1e425212207842aa334cdab1bccfe79a0023fcf08578d5bc09da8462a
7
+ data.tar.gz: 7482c21a09309b6dabe1f26c4b19b29890d8b8dfc679d4fc1b15d8b1de76a39b2f5998414e2e28ab3ee840edc2e31e7531ebe3b215c0a308f92068d0754ac46a
data/README.md CHANGED
@@ -378,6 +378,10 @@ client.payments.create(params, idempotency_key: nil) # or keyword fields
378
378
  # Reusing a key for DIFFERENT terms raises Rail0::ApiError with code
379
379
  # "idempotency_key_reused" (422) instead of returning the first payment.
380
380
  client.payments.get(id)
381
+ client.payments.update(id, description: "Order #42") # → the payment, same shape as get
382
+ client.payments.update(id, description: nil) # nil (or "") clears it
383
+ # The description is the only editable field: off-chain, so any status, settled included.
384
+ # Payer or payee only (a non-participant gets 404); over 255 characters is a 422.
381
385
  client.payments.list(status: "authorized", disputed: false, chain_id: 84532, sort: "-created_at")
382
386
  client.payments.list(status: %w[authorized expired]) # any of several states → ?status=authorized,expired
383
387
  client.payments.list(operation: "refund") # payments with at least one refund transaction
@@ -770,6 +774,17 @@ RAIL0 itself:
770
774
  A failed transaction carries the same triple as `error_code`, `error_title` and
771
775
  `error_detail`, whether it reverted on-chain or was refused before broadcast.
772
776
 
777
+ ### Ids in request paths
778
+
779
+ Every id (and operation name) a method interpolates into a request path is
780
+ percent-encoded as ONE path segment, so a value carrying `/`, `?`, `#`, a space or
781
+ non-ASCII characters can never address a different route than the method names
782
+ (`payments.get("../webhooks/x")` requests `/payments/..%2Fwebhooks%2Fx`, not
783
+ `/webhooks/x`). The ids the gateway hands out — UUIDs, `0x` hex — are unchanged by it.
784
+ A value that is `nil`, empty, `"."` or `".."` is never a valid id, and would still
785
+ re-target the request once encoded, so the method raises `ArgumentError` before
786
+ sending anything.
787
+
773
788
  ## Configuration
774
789
 
775
790
  ```ruby
@@ -854,7 +869,7 @@ lib/rail0/
854
869
  accounts.rb the caller's own account profile, read and update (JWT)
855
870
  webhooks.rb webhook subscription management (JWT)
856
871
  analytics.rb account-scoped payment analytics (JWT)
857
- query.rb shared query-string helper
872
+ query.rb shared query-string and path-segment helpers
858
873
  ```
859
874
 
860
875
  ## Development
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "query"
4
+
3
5
  module Rail0
4
6
  module Resources
5
7
  # The merchant account itself (requires JWT).
@@ -17,6 +19,8 @@ module Rail0
17
19
  # {Resources::Wallets}; buyer-facing discovery of what a merchant accepts lives on
18
20
  # {Resources::PaymentMethods}.
19
21
  class Accounts
22
+ include Query
23
+
20
24
  attr_reader :http
21
25
 
22
26
  def initialize(http)
@@ -29,7 +33,7 @@ module Rail0
29
33
  # @return [Hash] `id`, `name`, `email`, `created_at`, `updated_at`. `email` is part of
30
34
  # the response because only the account's owner, or an operator, can read it.
31
35
  def get(account_id)
32
- http.get("/accounts/#{account_id}")
36
+ http.get("/accounts/#{segment(account_id)}")
33
37
  end
34
38
 
35
39
  # Update the account's own profile (PATCH /accounts/:account_id).
@@ -55,7 +59,7 @@ module Rail0
55
59
  body[:email] = email unless email.nil?
56
60
  raise ArgumentError, "at least one of name or email is required" if body.empty?
57
61
 
58
- http.patch("/accounts/#{account_id}", body)
62
+ http.patch("/accounts/#{segment(account_id)}", body)
59
63
  end
60
64
  end
61
65
  end
@@ -99,7 +99,25 @@ module Rail0
99
99
  # (Integer or nil — divide `amount`, `capturable_amount` and `refundable_amount` by
100
100
  # 10**decimals to render them) and `in_flight` (Boolean — see {list}).
101
101
  def get(id)
102
- http.get("/payments/#{id}")
102
+ http.get("/payments/#{segment(id)}")
103
+ end
104
+
105
+ # Set or clear a payment's description (PATCH /payments/{id}).
106
+ #
107
+ # The description is the only field this route edits: it is off-chain metadata, so
108
+ # it can change at any status, settled payments included. Only a participant (the
109
+ # payer or the payee) may edit it — a non-participant gets the same 404 as an unknown
110
+ # id, and a deactivated caller wallet a 403 `wallet_deactivated`.
111
+ #
112
+ # +description:+ is required, and is always sent: passing +nil+ sends JSON +null+,
113
+ # which clears it (as does +""+). Over 255 characters is a 422.
114
+ # (rail0-gateway#373)
115
+ #
116
+ # @param id [String] Payment UUID or rail0_id.
117
+ # @param description [String, nil] The new description; +nil+ or +""+ clears it.
118
+ # @return [Hash] The updated payment, the same shape {get} returns.
119
+ def update(id, description:)
120
+ http.patch("/payments/#{segment(id)}", { description: description })
103
121
  end
104
122
 
105
123
  # List on-chain transactions for a payment.
@@ -114,7 +132,7 @@ module Rail0
114
132
  # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total: } }
115
133
  def transactions(id, operation: nil, status: nil, sort: nil, page: nil, per_page: nil)
116
134
  query = build_query(operation: operation, status: status, sort: sort, page: page, per_page: per_page)
117
- http.get_list("/payments/#{id}/transactions#{query}")
135
+ http.get_list("/payments/#{segment(id)}/transactions#{query}")
118
136
  end
119
137
 
120
138
  # Fetch ONE of a payment's transactions
@@ -130,7 +148,7 @@ module Rail0
130
148
  # @param transaction_id [String] Transaction UUID.
131
149
  # @return [Hash]
132
150
  def get_transaction(id, transaction_id)
133
- http.get("/payments/#{id}/transactions/#{transaction_id}")
151
+ http.get("/payments/#{segment(id)}/transactions/#{segment(transaction_id)}")
134
152
  end
135
153
 
136
154
  # Re-enqueue a stuck broadcast
@@ -153,7 +171,7 @@ module Rail0
153
171
  # @param transaction_id [String] The transaction row to redrive.
154
172
  # @return [Hash] The transaction, re-enqueued.
155
173
  def redrive(id, transaction_id)
156
- http.post("/payments/#{id}/transactions/#{transaction_id}/redrive", {})
174
+ http.post("/payments/#{segment(id)}/transactions/#{segment(transaction_id)}/redrive", {})
157
175
  end
158
176
 
159
177
  # Submit the payer's EIP-712 signature (PUT /payments/{id}/sign).
@@ -161,7 +179,7 @@ module Rail0
161
179
  # @param params [Hash] { signature: "0x…" } (65-byte 0x-prefixed hex).
162
180
  # @return [Hash]
163
181
  def sign(id, params)
164
- http.put("/payments/#{id}/sign", params)
182
+ http.put("/payments/#{segment(id)}/sign", params)
165
183
  end
166
184
 
167
185
  # List a payment's dispute open/close history.
@@ -173,7 +191,7 @@ module Rail0
173
191
  # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total: } }
174
192
  def disputes(id, status: nil, sort: nil, page: nil, per_page: nil)
175
193
  query = build_query(status: status, sort: sort, page: page, per_page: per_page)
176
- http.get_list("/payments/#{id}/disputes#{query}")
194
+ http.get_list("/payments/#{segment(id)}/disputes#{query}")
177
195
  end
178
196
 
179
197
  # Build the unsigned transaction for an operation
@@ -193,7 +211,7 @@ module Rail0
193
211
  # with different terms is refused 422 +idempotency_key_reused+. Scoped to this
194
212
  # payment. (rail0-gateway#331)
195
213
  def prepare(id, operation, body = nil, idempotency_key: nil)
196
- http.post("/payments/#{id}/#{operation}/prepare", body,
214
+ http.post("/payments/#{segment(id)}/#{segment(operation)}/prepare", body,
197
215
  headers: idempotency_headers(idempotency_key))
198
216
  end
199
217
 
@@ -203,7 +221,7 @@ module Rail0
203
221
  # @param params [Hash] { signed_transaction: "0x…" }.
204
222
  # @return [Hash]
205
223
  def submit(id, operation, params)
206
- http.post("/payments/#{id}/#{operation}", params)
224
+ http.post("/payments/#{segment(id)}/#{segment(operation)}", params)
207
225
  end
208
226
 
209
227
  # Record a transaction the caller broadcast themselves (MetaMask/wallet flow)
@@ -213,7 +231,7 @@ module Rail0
213
231
  # @param params [Hash] { transaction_hash: "0x…" }.
214
232
  # @return [Hash]
215
233
  def submit_by_hash(id, operation, params)
216
- http.post("/payments/#{id}/#{operation}/submitted", params)
234
+ http.post("/payments/#{segment(id)}/#{segment(operation)}/submitted", params)
217
235
  end
218
236
 
219
237
  # Phase 1 — build the unsigned authorize() transaction (escrow hold).
@@ -302,7 +320,7 @@ module Rail0
302
320
  # @param params [Hash] { signed_transaction: "0x…" }.
303
321
  # @return [Hash]
304
322
  def dispute(id, params)
305
- http.post("/payments/#{id}/dispute", params)
323
+ http.post("/payments/#{segment(id)}/dispute", params)
306
324
  end
307
325
 
308
326
  # Phase 1 — build the unsigned closeDispute() transaction (payer only).
@@ -318,7 +336,7 @@ module Rail0
318
336
  # @param params [Hash] { signed_transaction: "0x…" }.
319
337
  # @return [Hash]
320
338
  def close_dispute(id, params)
321
- http.post("/payments/#{id}/dispute/close", params)
339
+ http.post("/payments/#{segment(id)}/dispute/close", params)
322
340
  end
323
341
 
324
342
  # The payer's counterpart to {#submit_by_hash}, which covers only the operations
@@ -336,7 +354,7 @@ module Rail0
336
354
  # @param params [Hash] { transaction_hash: "0x…" }.
337
355
  # @return [Hash]
338
356
  def dispute_submit_by_hash(id, params)
339
- http.post("/payments/#{id}/dispute/submitted", params)
357
+ http.post("/payments/#{segment(id)}/dispute/submitted", params)
340
358
  end
341
359
 
342
360
  # Report an already-broadcast close-dispute transaction by hash (payer only); HTTP 202.
@@ -344,14 +362,17 @@ module Rail0
344
362
  # @param params [Hash] { transaction_hash: "0x…" }.
345
363
  # @return [Hash]
346
364
  def close_dispute_submit_by_hash(id, params)
347
- http.post("/payments/#{id}/dispute/close/submitted", params)
365
+ http.post("/payments/#{segment(id)}/dispute/close/submitted", params)
348
366
  end
349
367
 
350
368
  private
351
369
 
370
+ # +path+ is one of this class's own literals ("dispute/prepare",
371
+ # "dispute/close/prepare"), never caller input, so it is interpolated as-is:
372
+ # its `/` is meant to separate segments. Only +id+ goes through {#segment}.
352
373
  def prepare_dispute(path, id, reason, idempotency_key: nil)
353
374
  body = reason ? { reason: reason } : {}
354
- http.post("/payments/#{id}/#{path}", body,
375
+ http.post("/payments/#{segment(id)}/#{path}", body,
355
376
  headers: idempotency_headers(idempotency_key))
356
377
  end
357
378
 
@@ -1,15 +1,59 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "cgi"
4
+ require "erb"
4
5
 
5
6
  module Rail0
6
7
  module Resources
7
- # Shared query-string builder for resource classes. Included so every
8
- # resource turns keyword filters into a URL query the same way: nil values
9
- # are dropped, and booleans/integers are stringified.
8
+ # Shared URL builders for resource classes. Included so every resource turns
9
+ # keyword filters into a URL query the same way (nil values are dropped,
10
+ # booleans/integers are stringified) and escapes every caller-supplied path
11
+ # segment the same way ({#segment}).
10
12
  module Query
13
+ # Segments that would not stay put once interpolated: "." and ".." are dot
14
+ # segments that a URL normaliser (a proxy, a CDN, the server's router) may
15
+ # resolve away, and an empty one collapses the path onto its parent.
16
+ UNSAFE_SEGMENTS = ["", ".", ".."].freeze
17
+ private_constant :UNSAFE_SEGMENTS
18
+
11
19
  private
12
20
 
21
+ # Escapes ONE caller-supplied value for interpolation into a request path:
22
+ #
23
+ # http.get("/payments/#{segment(id)}/transactions/#{segment(transaction_id)}")
24
+ #
25
+ # Paths used to be plain interpolation, so an id was pasted in raw: one
26
+ # containing `/`, `?`, `#` or `..` addressed a different route than the method
27
+ # named — `payments.get("../webhooks/x")` was a GET on /webhooks/x with the
28
+ # caller's token. The ids this SDK is handed normally come from the gateway, but
29
+ # a caller that forwards one from a URL (a proxy route, a CLI argument) passes
30
+ # along whatever it was given.
31
+ #
32
+ # ERB::Util.url_encode percent-encodes everything outside the RFC 3986
33
+ # unreserved set (A-Z a-z 0-9 - . _ ~), UTF-8 byte by byte, with a space as
34
+ # %20 — the path-correct form (CGI.escape, used for queries, would turn a
35
+ # space into `+`, which in a path is a literal plus). For the values legitimately
36
+ # passed (UUIDs, 0x hex, operation names, integer ids) it changes nothing. This
37
+ # matches rail0-ts's `path` tag, which uses encodeURIComponent.
38
+ #
39
+ # A value that is exactly "", "." or ".." raises ArgumentError instead: encoding
40
+ # leaves the dots as they are (they are unreserved), so the segment would still
41
+ # be a dot segment, and an empty one silently re-targets the parent route
42
+ # (`payments.get("")` would list payments). None of them is a valid id, so
43
+ # failing before the request is the only honest answer.
44
+ #
45
+ # @param value [#to_s] The id (or other caller-supplied value) to interpolate.
46
+ # @return [String] The percent-encoded segment.
47
+ # @raise [ArgumentError] If the value is nil, empty, "." or "..".
48
+ def segment(value)
49
+ str = value.to_s
50
+ if value.nil? || UNSAFE_SEGMENTS.include?(str)
51
+ raise ArgumentError, "invalid path segment #{value.inspect}: an id must not be nil, empty, \".\" or \"..\""
52
+ end
53
+
54
+ ERB::Util.url_encode(str)
55
+ end
56
+
13
57
  # Builds "?k=v&…" from keyword filters, or "" when every value is nil.
14
58
  #
15
59
  # An Array value becomes ONE comma-separated pair (`status: %w[a b]` →
@@ -38,7 +38,7 @@ module Rail0
38
38
  query = build_query(chain_id: chain_id, token_symbol: token_symbol, active: active,
39
39
  default: default, token_active: token_active,
40
40
  sort: sort, page: page, per_page: per_page)
41
- http.get_list("/accounts/#{account_id}/wallets#{query}")
41
+ http.get_list("/accounts/#{segment(account_id)}/wallets#{query}")
42
42
  end
43
43
 
44
44
  # Fetch a single wallet by its id or 0x address.
@@ -46,7 +46,7 @@ module Rail0
46
46
  # @param id_or_address [String] Wallet UUID or 0x address.
47
47
  # @return [Hash] id, address, label, active
48
48
  def get(account_id, id_or_address)
49
- http.get("/accounts/#{account_id}/wallets/#{id_or_address}")
49
+ http.get(wallet_path(account_id, id_or_address))
50
50
  end
51
51
 
52
52
  # Add a wallet to the account.
@@ -74,7 +74,7 @@ module Rail0
74
74
  def create(account_id, address:, message:, signature:, label: nil)
75
75
  body = { address: address, message: message, signature: signature }
76
76
  body[:label] = label unless label.nil?
77
- http.post("/accounts/#{account_id}/wallets", body)
77
+ http.post("/accounts/#{segment(account_id)}/wallets", body)
78
78
  end
79
79
 
80
80
  # Update a wallet's label and/or active status.
@@ -87,7 +87,7 @@ module Rail0
87
87
  body = {}
88
88
  body[:label] = label unless label.nil?
89
89
  body[:active] = active unless active.nil?
90
- http.patch("/accounts/#{account_id}/wallets/#{id_or_address}", body)
90
+ http.patch(wallet_path(account_id, id_or_address), body)
91
91
  end
92
92
 
93
93
  # Soft-delete (deactivate) a wallet. Returns HTTP 204.
@@ -95,7 +95,7 @@ module Rail0
95
95
  # @param id_or_address [String] Wallet UUID or 0x address.
96
96
  # @return [nil]
97
97
  def delete(account_id, id_or_address)
98
- http.delete("/accounts/#{account_id}/wallets/#{id_or_address}")
98
+ http.delete(wallet_path(account_id, id_or_address))
99
99
  end
100
100
 
101
101
  # Read a wallet's live on-chain balances across the configured chains. Each
@@ -109,7 +109,7 @@ module Rail0
109
109
  # @return [Hash] wallet_id, address, balances
110
110
  def balances(account_id, id_or_address, chain_id: nil, token_symbol: nil)
111
111
  query = build_query(chain_id: chain_id, token_symbol: token_symbol)
112
- http.get("/accounts/#{account_id}/wallets/#{id_or_address}/balances#{query}")
112
+ http.get("#{wallet_path(account_id, id_or_address)}/balances#{query}")
113
113
  end
114
114
 
115
115
  # ── Token holdings ────────────────────────────────────────────────────────
@@ -133,7 +133,7 @@ module Rail0
133
133
  def add_token(account_id, id_or_address, chain_id:, token:, default: nil)
134
134
  body = { chain_id: chain_id, token: token }
135
135
  body[:default] = default unless default.nil?
136
- http.post("/accounts/#{account_id}/wallets/#{id_or_address}/tokens", body)
136
+ http.post("#{wallet_path(account_id, id_or_address)}/tokens", body)
137
137
  end
138
138
 
139
139
  # Stop accepting a token on this wallet (soft delete). Returns HTTP 204.
@@ -150,13 +150,13 @@ module Rail0
150
150
  #
151
151
  # @return [nil]
152
152
  def remove_token(account_id, id_or_address, token_id)
153
- http.delete("/accounts/#{account_id}/wallets/#{id_or_address}/tokens/#{token_id}")
153
+ http.delete("#{wallet_path(account_id, id_or_address)}/tokens/#{segment(token_id)}")
154
154
  end
155
155
 
156
156
  # Re-enable an existing token holding.
157
157
  # @return [Hash] the token holding
158
158
  def enable_token(account_id, id_or_address, token_id)
159
- http.patch("/accounts/#{account_id}/wallets/#{id_or_address}/tokens/#{token_id}/enable")
159
+ http.patch("#{wallet_path(account_id, id_or_address)}/tokens/#{segment(token_id)}/enable")
160
160
  end
161
161
 
162
162
  # Disable an existing token holding without forgetting it — the holding (and
@@ -166,7 +166,15 @@ module Rail0
166
166
  # active default holding — promote another token to default first.
167
167
  # @return [Hash] the token holding
168
168
  def disable_token(account_id, id_or_address, token_id)
169
- http.patch("/accounts/#{account_id}/wallets/#{id_or_address}/tokens/#{token_id}/disable")
169
+ http.patch("#{wallet_path(account_id, id_or_address)}/tokens/#{segment(token_id)}/disable")
170
+ end
171
+
172
+ private
173
+
174
+ # "/accounts/{account_id}/wallets/{id_or_address}" with both ids escaped by
175
+ # {Query#segment} — the prefix every single-wallet route shares.
176
+ def wallet_path(account_id, id_or_address)
177
+ "/accounts/#{segment(account_id)}/wallets/#{segment(id_or_address)}"
170
178
  end
171
179
  end
172
180
  end
@@ -71,7 +71,7 @@ module Rail0
71
71
  # @param id [String] Webhook UUID.
72
72
  # @return [Hash]
73
73
  def get(id)
74
- http.get("/webhooks/#{id}")
74
+ http.get("/webhooks/#{segment(id)}")
75
75
  end
76
76
 
77
77
  # Update a webhook's name, callback_url, and/or topics.
@@ -87,35 +87,35 @@ module Rail0
87
87
  body[:name] = name unless name.nil?
88
88
  body[:callback_url] = callback_url unless callback_url.nil?
89
89
  body[:topics] = Array(topics) unless topics.nil?
90
- http.patch("/webhooks/#{id}", body)
90
+ http.patch("/webhooks/#{segment(id)}", body)
91
91
  end
92
92
 
93
93
  # Re-enable a disabled webhook.
94
94
  # @param id [String] Webhook UUID.
95
95
  # @return [Hash]
96
96
  def enable(id)
97
- http.put("/webhooks/#{id}/enable")
97
+ http.put("/webhooks/#{segment(id)}/enable")
98
98
  end
99
99
 
100
100
  # Disable a webhook (stops deliveries without deleting it).
101
101
  # @param id [String] Webhook UUID.
102
102
  # @return [Hash]
103
103
  def disable(id)
104
- http.put("/webhooks/#{id}/disable")
104
+ http.put("/webhooks/#{segment(id)}/disable")
105
105
  end
106
106
 
107
107
  # Generate a new shared secret, returned once on the response.
108
108
  # @param id [String] Webhook UUID.
109
109
  # @return [Hash] webhook record including the new shared_secret
110
110
  def rotate_secret(id)
111
- http.put("/webhooks/#{id}/rotate_secret")
111
+ http.put("/webhooks/#{segment(id)}/rotate_secret")
112
112
  end
113
113
 
114
114
  # Reset the delivery circuit breaker and re-enable the webhook.
115
115
  # @param id [String] Webhook UUID.
116
116
  # @return [Hash]
117
117
  def reset_circuit(id)
118
- http.put("/webhooks/#{id}/reset_circuit")
118
+ http.put("/webhooks/#{segment(id)}/reset_circuit")
119
119
  end
120
120
 
121
121
  # List delivery attempts for a webhook.
@@ -138,7 +138,7 @@ module Rail0
138
138
  query = build_query(status: status, topic: topic, payment_id: payment_id,
139
139
  response_code: response_code, since: since, until: until_time,
140
140
  sort: sort, page: page, per_page: per_page)
141
- http.get_list("/webhooks/#{id}/event_callbacks#{query}")
141
+ http.get_list("/webhooks/#{segment(id)}/event_callbacks#{query}")
142
142
  end
143
143
 
144
144
  # Re-deliver one recorded delivery's exact payload
@@ -161,14 +161,14 @@ module Rail0
161
161
  # @param callback_id [String] The event callback's UUID (from {event_callbacks}).
162
162
  # @return [Hash] `{ status: "queued" }` (HTTP 202).
163
163
  def redeliver(id, callback_id)
164
- http.post("/webhooks/#{id}/event_callbacks/#{callback_id}/redeliver", {})
164
+ http.post("/webhooks/#{segment(id)}/event_callbacks/#{segment(callback_id)}/redeliver", {})
165
165
  end
166
166
 
167
167
  # Delete a webhook. Returns HTTP 204.
168
168
  # @param id [String] Webhook UUID.
169
169
  # @return [nil]
170
170
  def delete(id)
171
- http.delete("/webhooks/#{id}")
171
+ http.delete("/webhooks/#{segment(id)}")
172
172
  end
173
173
  end
174
174
  end
data/lib/rail0/types.rb CHANGED
@@ -173,16 +173,6 @@ module Rail0
173
173
  keyword_init: true
174
174
  )
175
175
 
176
- # A wallet's token holding, with the wallet, token, and blockchain nested via their own schemas.
177
- WalletToken = Struct.new(
178
- :default, # Boolean
179
- :active, # Boolean
180
- :wallet, # Wallet
181
- :token, # Token
182
- :blockchain, # Blockchain
183
- keyword_init: true
184
- )
185
-
186
176
  # A wallet's token holding as nested under its wallet (GET /accounts/:id/wallets): the token plus
187
177
  # this wallet's per-token flags, without re-nesting the wallet.
188
178
  WalletTokenHolding = Struct.new(
data/lib/rail0/version.rb CHANGED
@@ -5,5 +5,5 @@ module Rail0
5
5
  # consumed by path from a sibling checkout, where the number bought nothing: no registry
6
6
  # ever served a 1.x, no tag was ever cut, and the one consumer resolves the working tree.
7
7
  # Publishing from 1.2.0 would have implied two releases nobody can install.
8
- VERSION = "1.3.0"
8
+ VERSION = "1.4.0"
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rail0-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Commerce Layer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-09-29 00:00:00.000000000 Z
11
+ date: 2026-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger