rail0-sdk 1.2.0 → 1.3.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: 60c25c2843d57ee49df3b17267e56a612f56701d61e48430786b1c6435987eb9
4
- data.tar.gz: d8ef2d01d59b1a775f116753608bba6ab90d0d868a360040c0bc8fdc64f64de0
3
+ metadata.gz: 1304e38f4530a6bfdbfe8f3196ed562b4869b9e6ab7d455cae744afb21ad21ed
4
+ data.tar.gz: 87090219ed6aef2133815ca4a704a5c7a8e926cead00e59688a42aac25f29297
5
5
  SHA512:
6
- metadata.gz: e7e6441337f7daf05f4e55ba22d94a380755f5775fd203b0699e8f16029fc493c88e9863a81e2d1559e71c383db7b79330503cd724c2219dd0a1b169badfac81
7
- data.tar.gz: 943a28ce8740139e30e4966ce1021b9a85271789418a2274d505a6457733a09e8cc84c3c8f435ff34962dec3ed7e5beded3fc0f7cec131de626d8e5d6535fc5d
6
+ metadata.gz: 77f22e52615cd7cac1b1535084a388111e20850e01a89a39628322bf825f8ea023c1db7e9d185bbb9c0616fa1827add2ebbdb2d8749cbf60fbfe099f6f0719f8
7
+ data.tar.gz: ae3d34f5cceffb1ea59f33114b53c5308eb115b929bfa581717e7e3357fedbb38a527ef0a35136943eadbc4fa0e93a589ebbacff4f13c880f885c95869644a4a
data/README.md CHANGED
@@ -237,13 +237,30 @@ logging — it says exactly which sessions died, which `revoked_all: true` canno
237
237
  ## Catalog (public)
238
238
 
239
239
  ```ruby
240
- client.chains.list # GET /blockchains (each chain's :contract is the active RAIL0 deployment)
240
+ client.chains.list # GET /blockchains (each chain's :contract is the active RAIL0 deployment, :settlement its measured confirm time)
241
241
  client.chains.list(network_type: "testnet", symbol: "ETH")
242
242
  client.tokens.list # GET /tokens — every token, retired ones included
243
243
  client.tokens.list(chain_id: 84532, symbol: "USDC")
244
244
  client.tokens.list(active: true) # what a NEW payment may use
245
245
  ```
246
246
 
247
+ Each chain's `settlement` is how long an operation on it has taken to confirm **on this
248
+ gateway**, end to end (the chain's finality lag plus the gateway/indexer pipeline):
249
+ `{ p50_seconds:, p90_seconds:, sample_size:, window_days: }`, percentiles of broadcast →
250
+ confirmation over the trailing `window_days`, in whole seconds rounded up. It is always
251
+ present, but the percentiles are `nil` below the gateway's minimum sample (20), so size a
252
+ polling deadline from it with a fixed fallback:
253
+
254
+ ```ruby
255
+ FALLBACK = 15 * 60 # seconds
256
+ chain = client.chains.list.find { |c| c[:chain_id] == 84532 }
257
+ p90 = chain.dig(:settlement, :p90_seconds)
258
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + (p90 ? [3 * p90, FALLBACK].max : FALLBACK)
259
+ ```
260
+
261
+ It is a trailing-window measurement (a capacity figure, cached by the gateway for 10
262
+ minutes), not a promise, so treat it as a hint and never as a timeout that fails a payment.
263
+
247
264
  `tokens.list` without `active:` is the **historical** catalogue — a payment references its
248
265
  token forever, so a retired one must still resolve. A checkout picker should pass
249
266
  `active: true`: `payments.create` answers 422 `unknown_token` for a retired token.
@@ -362,8 +379,10 @@ client.payments.create(params, idempotency_key: nil) # or keyword fields
362
379
  # "idempotency_key_reused" (422) instead of returning the first payment.
363
380
  client.payments.get(id)
364
381
  client.payments.list(status: "authorized", disputed: false, chain_id: 84532, sort: "-created_at")
382
+ client.payments.list(status: %w[authorized expired]) # any of several states → ?status=authorized,expired
365
383
  client.payments.list(operation: "refund") # payments with at least one refund transaction
366
384
  client.payments.transactions(id, operation: "capture")
385
+ client.payments.transactions(id, status: %w[submitting submitted]) # on its way to the chain
367
386
  # `operation` on both takes Payments::TRANSACTION_OPERATIONS — dispute and close_dispute included.
368
387
  client.payments.redrive(id, transaction_id) # re-enqueue a stuck broadcast
369
388
  # Offer `redrive` on the row's `redrivable` flag — the same predicate the gateway guards
@@ -387,10 +406,29 @@ client.disputes.list(status: "closed", sort: "-opened_at")
387
406
  `payments.list`/`transactions`/`disputes` and `disputes.list` return a paginated
388
407
  `{ data:, meta: { page:, per_page:, total: } }` envelope.
389
408
 
390
- Every payment row carries `chain_id`, list rows included. You need it to display
391
- an amount: `amount` is in base units and the token's decimals resolve from
392
- `token` **plus** its chain — an address alone identifies a token only within one
393
- chain — so a listing never needs a `get(id)` per row to render totals.
409
+ `status:` on `payments.list` and `payments.transactions` takes one value or an Array; an
410
+ Array is sent comma-separated and matches **any** of the listed states. An empty Array is
411
+ dropped like `nil` (no filter). An unknown value raises `Rail0::ApiError` 400
412
+ `validation_failed`, naming it. Any query filter given an Array is sent the same way.
413
+
414
+ Every payment row carries `chain_id` and `decimals`, list rows included (and the payment
415
+ embedded in `disputes.list`). Amounts (`amount`, `capturable_amount`, `refundable_amount`)
416
+ are in base units: divide by `10**decimals` to render them, with no `tokens.list` join and no
417
+ `get(id)` per row. `decimals` is `nil` only when the gateway cannot resolve the token (a
418
+ contract it has not loaded, a token deleted outright). Fall back to `tokens.list` then,
419
+ matching on `token` **plus** `chain_id`, since an address alone identifies a token only
420
+ within one chain.
421
+
422
+ ```ruby
423
+ BigDecimal(row[:amount]) / (10**row[:decimals]) # "100000000", 6 → 100.0
424
+ ```
425
+
426
+ Every payment row also carries `in_flight`: `true` while one of its transactions is on its
427
+ way to the chain, meaning `submitting`, `submitted`, or `pending` holding its signed
428
+ transaction (the `redrivable` rows). A `pending` row still waiting for its signature does not
429
+ count, nor do `confirmed`/`failed` rows. While it is true the balances are about to move, so
430
+ disable further actions on the payment. It comes on list rows too, which embed no
431
+ transactions, so a bulk action can skip busy rows without a `get(id)` each.
394
432
 
395
433
  ### Refund (two-phase EIP-3009)
396
434
 
@@ -462,6 +500,25 @@ client.webhooks.redeliver(id, callback_id) # 202 { status: "queued" } — repla
462
500
  client.webhooks.delete(id) # 204
463
501
  ```
464
502
 
503
+ Every delivery's `payment` object carries `capturable_amount` (base units) and
504
+ `authorization_expiry` (epoch seconds), whatever the topic. That is what makes
505
+ `payments.authorization_expiring` actionable. The gateway sends it **once per payment**, a
506
+ configurable notice (24h by default) before `authorization_expiry`, for a payment that can
507
+ still be captured (`authorized`, `partially_captured`, `partially_refunded`) and still holds
508
+ escrow. It is the last chance to capture; `payments.expired` says the chance has gone. Its
509
+ `transaction` is `null`, and the time left is `authorization_expiry` minus the event's
510
+ `emitted_at` (ISO-8601). There is no separate field for it because it would be stale on
511
+ every retry:
512
+
513
+ ```ruby
514
+ event = JSON.parse(raw, symbolize_names: true)
515
+ if event[:topic] == "payments.authorization_expiring"
516
+ payment = event[:payment]
517
+ secs_left = payment[:authorization_expiry] - Time.iso8601(event[:emitted_at]).to_i
518
+ # capture payment[:capturable_amount] (base units) within secs_left, or let it expire
519
+ end
520
+ ```
521
+
465
522
  `redeliver` replays a recorded delivery's exact payload — the same event id, so a receiver
466
523
  that already processed it deduplicates — under a fresh signature. It is the way back for
467
524
  events lost while the circuit breaker was open. Delivery is async and the dispatcher drops
@@ -22,7 +22,13 @@ module Rail0
22
22
  # required_confirmations, finality_tag (the settlement rule: the tag where the
23
23
  # chain serves one, the count only where it does not), and contract — the chain's
24
24
  # active RAIL0 deployment as `{ address:, version:, deployed_at: }` (what holds
25
- # the money; nullable in principle, present in practice)
25
+ # the money; nullable in principle, present in practice), and settlement — how long
26
+ # an operation on the chain has taken to confirm ON THIS GATEWAY, as
27
+ # `{ p50_seconds:, p90_seconds:, sample_size:, window_days: }`: broadcast-to-confirmation
28
+ # percentiles (whole seconds, rounded up) over the trailing `window_days`. Always
29
+ # present; the percentiles are nil below the gateway's minimum sample (20), so check
30
+ # `sample_size` and keep a fixed fallback. A wait-deadline hint (e.g. 3 × p90_seconds,
31
+ # floored at your fallback), not a guarantee.
26
32
  def list(network_type: nil, symbol: nil)
27
33
  http.get("/blockchains#{build_query(network_type: network_type, symbol: symbol)}")
28
34
  end
@@ -32,7 +32,9 @@ module Rail0
32
32
 
33
33
  # List payments for the authenticated wallet (requires JWT). Returns
34
34
  # payments where the caller is the payer or payee.
35
- # @param status [String, nil] Filter by payment status.
35
+ # @param status [String, Array<String>, nil] Filter by payment status: one state, or
36
+ # an Array matching ANY of them (sent comma-separated, `status=authorized,expired`).
37
+ # An unknown value is 400 `validation_failed` naming it.
36
38
  # @param mode [String, nil] Filter by mode ("authorize" or "charge").
37
39
  # @param payer [String, nil] Filter by payer address.
38
40
  # @param payee [String, nil] Filter by payee address.
@@ -52,7 +54,12 @@ module Rail0
52
54
  # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
53
55
  # @param page [Integer, nil] Page number (1-based; 1..1,000,000, 400 outside).
54
56
  # @param per_page [Integer, nil] Items per page (max 100).
55
- # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total: } }
57
+ # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total: } }. Each row
58
+ # carries, besides the payment fields, `chain_id`, `decimals` (Integer, or nil when
59
+ # the gateway can't resolve the token — fall back to {Tokens#list} then) to render the
60
+ # base-unit amounts without a token lookup, and `in_flight` (Boolean: a transaction is
61
+ # `submitting`, `submitted`, or `pending` holding its signed transaction — disable
62
+ # further actions on the row while true).
56
63
  def list(status: nil, mode: nil, payer: nil, payee: nil, token: nil, rail0_id: nil,
57
64
  chain_id: nil, disputed: nil, operation: nil, min_amount: nil, max_amount: nil,
58
65
  created_from: nil, created_to: nil, sort: nil, page: nil, per_page: nil)
@@ -88,7 +95,9 @@ module Rail0
88
95
 
89
96
  # Fetch current payment state (DB status + live on-chain amounts + transactions).
90
97
  # @param id [String] Payment UUID or rail0_id.
91
- # @return [Hash]
98
+ # @return [Hash] The payment with its embedded transactions, including `decimals`
99
+ # (Integer or nil — divide `amount`, `capturable_amount` and `refundable_amount` by
100
+ # 10**decimals to render them) and `in_flight` (Boolean — see {list}).
92
101
  def get(id)
93
102
  http.get("/payments/#{id}")
94
103
  end
@@ -97,7 +106,8 @@ module Rail0
97
106
  # @param id [String] Payment UUID or rail0_id.
98
107
  # @param operation [String, nil] Filter by operation (see {TRANSACTION_OPERATIONS} —
99
108
  # dispute and close_dispute included).
100
- # @param status [String, nil] Filter by transaction status.
109
+ # @param status [String, Array<String>, nil] Filter by transaction status: one, or an
110
+ # Array matching any of them (`%w[submitting submitted]` = on its way to the chain).
101
111
  # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
102
112
  # @param page [Integer, nil] Page number (1-based; 1..1,000,000, 400 outside).
103
113
  # @param per_page [Integer, nil] Items per page (max 100).
@@ -10,10 +10,30 @@ module Rail0
10
10
  module Query
11
11
  private
12
12
 
13
+ # Builds "?k=v&…" from keyword filters, or "" when every value is nil.
14
+ #
15
+ # An Array value becomes ONE comma-separated pair (`status: %w[a b]` →
16
+ # `status=a,b`): the gateway's multi-value filters (e.g. `status` on
17
+ # GET /payments, #367) take the comma form. Each element is escaped on its
18
+ # own and the separating comma is left literal, so a comma can never leak
19
+ # in from an element. nil elements are dropped; an Array left empty drops
20
+ # the whole pair, the same as nil, rather than sending a blank `status=`
21
+ # the gateway would reject with 400.
13
22
  def build_query(**params)
14
- pairs = params.compact.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }
23
+ pairs = params.filter_map do |k, v|
24
+ value = query_value(v)
25
+ "#{k}=#{value}" unless value.nil?
26
+ end
15
27
  pairs.empty? ? "" : "?#{pairs.join('&')}"
16
28
  end
29
+
30
+ def query_value(value)
31
+ return nil if value.nil?
32
+ return CGI.escape(value.to_s) unless value.is_a?(Array)
33
+
34
+ items = value.compact
35
+ items.map { |item| CGI.escape(item.to_s) }.join(",") unless items.empty?
36
+ end
17
37
  end
18
38
  end
19
39
  end
@@ -25,6 +25,7 @@ module Rail0
25
25
  payments.voided
26
26
  payments.released
27
27
  payments.refunded
28
+ payments.authorization_expiring
28
29
  payments.expired
29
30
  payments.failed
30
31
  payments.disputed
data/lib/rail0/types.rb CHANGED
@@ -133,6 +133,22 @@ module Rail0
133
133
  :required_confirmations, # Integer — Confirmations this gateway waits for before treating a transaction as settled. The FALLBACK rule: where the chain serves a finality tag (see finality_tag) that tag governs instead, so a client showing this number on such a chain is describing a rule that is not in force.
134
134
  :finality_tag, # String — The block tag the chain calls settled (`safe`, `finalized`), when it serves one — and what the gateway actually gates on. Null where the chain serves none, in which case required_confirmations is counted.
135
135
  :contract, # ChainContract
136
+ :settlement, # BlockchainSettlement
137
+ keyword_init: true
138
+ )
139
+
140
+ # How long an operation on this chain has taken to settle ON THIS GATEWAY: percentiles of broadcast
141
+ # (`submitted_at`) to confirmation (`confirmed_at`) over the confirmed transactions of the trailing
142
+ # `window_days`. End-to-end — the chain's finality lag plus the gateway/indexer pipeline — so it is
143
+ # what a client actually waits for, not the chain's finality. A measurement, not a guarantee: use it
144
+ # as a wait-deadline hint (e.g. a small multiple of `p90_seconds`) and keep a fixed fallback for
145
+ # when it is null. Deliberately a long-window capacity figure, not a live alarm (#289). Always
146
+ # present; the percentiles are null when `sample_size` is below the gateway's minimum (20).
147
+ BlockchainSettlement = Struct.new(
148
+ :p50_seconds, # Integer — Median settlement time, whole seconds rounded up. Null below the minimum sample.
149
+ :p90_seconds, # Integer — 90th-percentile settlement time, whole seconds rounded up — the figure to size a deadline from. Null below the minimum sample.
150
+ :sample_size, # Integer — Confirmed transactions measured in the window (0 when none).
151
+ :window_days, # Integer — The trailing window the figures cover, in days.
136
152
  keyword_init: true
137
153
  )
138
154
 
@@ -188,7 +204,8 @@ module Rail0
188
204
  keyword_init: true
189
205
  )
190
206
 
191
- # Base persisted payment fields, plus the `chain_id` of the payment's deployment.
207
+ # Base persisted payment fields, plus the `chain_id` of the payment's deployment, its token's
208
+ # `decimals`, and whether a transaction is `in_flight`.
192
209
  Payment = Struct.new(
193
210
  :id, # String
194
211
  :contract_id, # String
@@ -203,11 +220,13 @@ module Rail0
203
220
  :payer, # String
204
221
  :payee, # String
205
222
  :token, # String
223
+ :decimals, # Decimals of the payment's token: divide `amount`, `capturable_amount` and `refundable_amount` (base units) by 10^decimals to render them, with no GET /tokens join on `token` + `chain_id`. Resolved from the gateway's in-memory chain catalogue, retired tokens and archived contract versions included. Null only when the gateway cannot resolve the token (a contract it has not loaded, a token deleted outright) — fall back to GET /tokens then.
206
224
  :authorization_expiry, # Integer
207
225
  :refund_expiry, # Integer
208
226
  :escrow_stranded, # Boolean — True exactly inside the stranded-escrow window (#233): a partial capture has permanently ruled void out, and release only opens at authorization_expiry — so no verb can return the buyer's uncaptured escrow until then. Mirrors RAIL0.sol; the gateway names the window, it cannot shorten it.
209
227
  :escrow_returnable_at, # String — When the stranded escrow becomes returnable (release opens) — the authorization expiry as ISO-8601. Null whenever nothing is stranded, so presence alone is the signal.
210
228
  :disputed, # Boolean — True while an open dispute exists.
229
+ :in_flight, # Boolean — True while one of the payment's transactions is on its way to the chain and not yet settled: status `submitting`, `submitted`, or `pending` holding its signed transaction (the `redrivable` rows — signed and handed over, broadcast not yet sent). A `pending` row still awaiting its signature (an unfinished prepare) does not count, nor do `confirmed`/`failed`. While true the mirrored balances are about to move, so a client should disable further actions on the payment. Carried by list rows too, which embed no transactions; computed for a whole page in one query.
211
230
  :last_error_code, # Decoded reason of the last failed on-chain attempt; null once the payment makes forward progress. Non-null means the latest attempt failed.
212
231
  :last_error_message, # Human-readable form of last_error_code.
213
232
  :description,
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.2.0"
8
+ VERSION = "1.3.0"
9
9
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rail0-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.0
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Commerce Layer