rail0-sdk 1.7.1 → 1.8.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: 5d88d3da009d4c04853e8fb794eba655913763a1391c6e0bd6915bda6cb68d36
4
- data.tar.gz: 5cc37022ce71f950c6b11dcf421902518b63caebfd54a13f78eaf909a7eef39c
3
+ metadata.gz: '078c5043f2867777ccba9da57fe022e3db252cff6010f50dbad7c9dd37b7c8a7'
4
+ data.tar.gz: e64f79000eeb6b94b78367c223eb33f48ac5ffc23218ac89dffb3b99e23504d1
5
5
  SHA512:
6
- metadata.gz: 2512e0a842d7d76749dc4028a0c1d2590b7fe1dd4e95676572a39aa8c41fa9a882c2e4a534e85080f0fe8089c8001c6f2101ff4b9fb7ab9f4e2cc9d7e6d35d62
7
- data.tar.gz: 7b569639525815722a586f308d45ebc67c964cbd4f6f1ae4c5bac4a8c5a731589b03dffebcfcdb375e3a265f9f1143b52162d5e8685de591e39a364de3257cf8
6
+ metadata.gz: 4030fedd465183a1e4dba618ad1c3d7ff75d667032e3ebce6e7da87aeb4e1c75c9ab1205378d89fd2329a005ceb7f8a1b22ab3504b466e0c06b387117a5736a3
7
+ data.tar.gz: d894eea1f54c0c8514be643b3fdb7300cdc8c2158dd4c4db0de29b0b8fb09c7226d2910e981c921735a5b5589310484243cc9ddc9b01b92b75692ae68e5c43f0
data/README.md CHANGED
@@ -382,6 +382,7 @@ client.wallets.list(account_id, token_active: true)
382
382
 
383
383
  client.wallets.get(account_id, id_or_address)
384
384
  client.wallets.update(account_id, id_or_address, label: "Renamed", active: false)
385
+ client.wallets.update(account_id, id_or_address, active: true) # back on (see below)
385
386
  client.wallets.delete(account_id, id_or_address) # 204
386
387
  client.wallets.balances(account_id, id_or_address, chain_id: 84532) # live on-chain balances
387
388
  ```
@@ -392,6 +393,17 @@ refused with 422 `last_active_wallet` — login resolves the account from a wall
392
393
  so an account with none could neither act nor add one back. Another wallet must
393
394
  be active first.
394
395
 
396
+ **Switching a wallet back on.** Every switch-off records who did it in the wallet's
397
+ `deactivated_by`: `"owner"` (a session signed in with that wallet) or `"operator"`
398
+ (an operator session); `nil` while active, and on a switch-off older than the record.
399
+ A deactivated wallet's session is read-only (403 `wallet_deactivated`) except for one
400
+ write: `update(account_id, id, active: true)` on its **own** wallet, which switches it
401
+ back on when its owner switched it off. A wallet an operator switched off (or with
402
+ `deactivated_by: nil`) raises 403 `wallet_deactivated_by_operator`: an operator's
403
+ switch-off revokes the key, so only an operator lifts it. The flip side: a wallet its
404
+ owner switched off can be switched back on by whoever holds its key, so a definitive
405
+ revocation (a leaked key) must come from an operator.
406
+
395
407
  **A session is bound to the wallet it signed in with.** Reads (`list`, `get`,
396
408
  `balances`) are account-wide, but `update`, `delete` and the token-holding methods
397
409
  act only on the session's own wallet: another wallet of the same account is
@@ -459,6 +471,11 @@ client.payments.create(params, idempotency_key: nil) # or keyword fields
459
471
  client.payments.get(id)
460
472
  client.payments.update(id, description: "Order #42") # → the payment, same shape as get
461
473
  client.payments.update(id, description: nil) # nil (or "") clears it
474
+ # The payee's own fields (the payer gets 403 not_the_payee): an internal note never shown
475
+ # to the payer, and the payment's identifier in the merchant's system. Only what you pass
476
+ # is sent.
477
+ client.payments.update(id, merchant_note: "refund agreed by email")
478
+ client.payments.update(id, reference: "ORD-1042", reference_origin: "commercelayer")
462
479
  # The description is the only editable field: off-chain, so any status, settled included.
463
480
  # Payer or payee only (a non-participant gets 404); over 255 characters is a 422.
464
481
  client.payments.list(status: "authorized", disputed: false, chain_id: 84532, sort: "-created_at")
@@ -632,6 +649,7 @@ client.webhooks.enable(id)
632
649
  client.webhooks.disable(id)
633
650
  client.webhooks.rotate_secret(id) # returns a fresh shared_secret
634
651
  client.webhooks.reset_circuit(id)
652
+ client.webhooks.test(id) # signed webhooks.test event now → { delivered:, response_code:, … }; not logged
635
653
  # status is "delivered" or "failed" only; payment_id takes a UUID or a rail0_id.
636
654
  client.webhooks.event_callbacks(id, status: "failed", response_code: "500")
637
655
  client.webhooks.redeliver(id, callback_id) # 202 { status: "queued" } — replay one delivery
@@ -728,11 +746,13 @@ client.accounts.get(account_id)
728
746
  client.accounts.update(account_id, email: "ops@merchant.example")
729
747
  client.accounts.update(account_id, name: "Merchant Ltd", email: "ops@merchant.example")
730
748
  client.accounts.update(account_id, priority_tip: false) # minimum tip on the account's own transactions
749
+ # Payment windows of NEW payments: seconds, or :default (sent as null) for the gateway default.
750
+ client.accounts.update(account_id, authorization_ttl: 2 * 86_400, refund_ttl: :default)
731
751
  ```
732
752
 
733
753
  `email` is in the response because the holder is this endpoint's only possible caller.
734
754
 
735
- `update` needs at least one of `name`/`email`/`priority_tip` (it raises `ArgumentError`
755
+ `update` needs at least one of `name`/`email`/`priority_tip`/a payment window (it raises `ArgumentError`
736
756
  before sending an empty PATCH, which the gateway would answer with 400). Name and email are
737
757
  unique across accounts, so a value another account holds answers **409**. It is a write on
738
758
  the account, so a deactivated wallet (403 `wallet_deactivated`) or account (403
@@ -743,6 +763,15 @@ exposed here.
743
763
  capture, void, release, refund): `true`, the default, tips the network's suggested priority
744
764
  fee; `false` uses the chain's minimum tip (0 on most chains, 25 gwei on Polygon PoS/Amoy) —
745
765
  cheaper, slower to confirm under load. The buyer's dispute/close-dispute never read it.
766
+
767
+ **Payment windows** — `authorization_ttl` (how long an authorization can be captured) and
768
+ `refund_ttl` (how long a payment can be refunded or disputed), in seconds — are the deadlines
769
+ the account's **new** payments are created with: its own value, or the gateway default. The
770
+ profile's `payment_windows` says which, and the bounds; outside them, or with the refund
771
+ window less than `min_refund_window` (one day) past the authorization, the gateway answers
772
+ **422** `invalid_payment_windows`. A payment's deadlines are signed into it, so a change moves
773
+ only the payments created afterwards.
774
+
746
775
  The account's wallets are a collection under the same path (`client.wallets`), and
747
776
  buyer-facing discovery is `client.payment_methods`.
748
777
 
@@ -1070,25 +1099,52 @@ uses them everywhere, table-aligned hashes because some literals are tables, and
1070
1099
  (`retry_on_429`, `eip712`, `secp256k1`) that each spell themselves their own way. A linter
1071
1100
  that argues with the codebase teaches people to ignore it.
1072
1101
 
1073
- ## Publishing
1074
-
1075
- Releases publish from GitHub Actions (`.github/workflows/release.yml`) through RubyGems
1076
- **Trusted Publishing**: RubyGems.org trusts that workflow in this repo, so there is no API
1077
- key to store or rotate and no OTP prompt.
1078
-
1079
- 1. Merge a PR that bumps `Rail0::VERSION` in `lib/rail0/version.rb`.
1080
- 2. Publish a GitHub release on `main` tagged `v<version>` (e.g. `v1.2.0`).
1081
-
1082
- The workflow checks the tag against `Rail0::VERSION`, runs `rake build` (rubocop and the
1083
- specs first, then the gem) and pushes it. A version already on RubyGems.org is skipped, so
1084
- a re-run (Actions → release → Run workflow, from `main`, with the tag as input) is always
1085
- safe — that is also how a release cut before the workflow existed gets published.
1086
-
1087
- One-time setup, by an owner of the gem on rubygems.org: rail0-sdk → Trusted publishers →
1088
- GitHub Actions, repository `commercelayer/rail0-ruby`, workflow `release.yml`.
1089
-
1090
- Publishing by hand still works as a fallback (`gem build rail0-sdk.gemspec && gem push
1091
- rail0-sdk-<version>.gem`, with the OTP the gem's `rubygems_mfa_required` asks for).
1102
+ ## Releasing
1103
+
1104
+ A release is an approved pull request: merging it is what publishes.
1105
+
1106
+ 1. `bin/release-prepare`, on an up-to-date, clean `main`. It computes the version from the titles of the pull requests merged since the last tag
1107
+ (Conventional Commits): a breaking change — `!` after the type, as in `feat(api)!:`, or a
1108
+ `BREAKING CHANGE:` footer — makes it a major, any `feat` a minor, anything else a patch,
1109
+ and it refuses when nothing merged. `bin/release-prepare minor` (or `patch`, `major`,
1110
+ `X.Y.Z`) sets it by hand instead. The release PR says which pull requests decided it.
1111
+ It then bumps `Rail0::VERSION` and `Gemfile.lock`, writes the release
1112
+ notes from the pull requests merged since the last tag, and opens
1113
+ `chore(release): vX.Y.Z`. `--dry-run` prints all of it and writes nothing.
1114
+
1115
+ **Or on a feature branch**, to release a change in the same review: run it on a clean
1116
+ branch that is pushed, contains `origin/main` and has an open pull request into `main`.
1117
+ The bump is committed and pushed on that branch (`chore(release): vX.Y.Z`), the pull
1118
+ request's own title counts toward the version and gets a line in the notes, and the
1119
+ notes go into its description between `<!-- release:start -->` markers — the text above
1120
+ them is kept. Re-run it after new commits or a merge of `main`: it refreshes the version
1121
+ and the notes instead of bumping twice. One approval then covers the change and its
1122
+ release.
1123
+ 2. Review the pull request (edit the notes in its body if needed), approve and merge it.
1124
+ The approval must come from a reviewer **other than the pull request's author**, on its
1125
+ final commit: release.yml checks it before publishing anything and stops otherwise —
1126
+ an admin merge that bypassed the review, a direct push to `main` or a resume of an
1127
+ unapproved release publishes nothing.
1128
+ 3. `.github/workflows/release.yml` runs on that push to `main`: CI again on the release
1129
+ commit, `gem push` through RubyGems **Trusted Publishing**, then the tag `vX.Y.Z` and
1130
+ the GitHub release (notes from the pull request), then a Slack message.
1131
+
1132
+ A merge that does not change the version releases nothing. If a run stops halfway, Actions
1133
+ → release → Run workflow (on `main`) resumes it: what is already on RubyGems.org or already
1134
+ tagged is skipped. `rake release` is disabled (it would tag and push from a laptop), and no
1135
+ API key exists anywhere: RubyGems.org trusts only `release.yml` in this repo.
1136
+
1137
+ One-time settings (repository admins):
1138
+
1139
+ - environment `release` (Settings → Environments), deployment limited to `main`;
1140
+ - on rubygems.org, rail0-sdk's trusted publisher (repository `commercelayer/rail0-ruby`,
1141
+ workflow `release.yml`) bound to environment `release` — only once the new
1142
+ `release.yml` is on `main`;
1143
+ - a tag ruleset on `v*`: block updates and deletions, restrict creation to GitHub Actions
1144
+ and admins;
1145
+ - branch protection on `main`: required `ci` check (`spec`), "Include administrators";
1146
+ - the Slack channel: `release.yml` posts through the `PIPELINE_SLACK_CHANNEL_WEBHOOK_URL`
1147
+ secret, named in one place (the `announce` job).
1092
1148
 
1093
1149
  ## License
1094
1150
 
@@ -55,7 +55,10 @@ module Rail0
55
55
  "not_payer_or_payee" => "only the payer or the payee may do this",
56
56
  "release_submitter_not_a_party" => "a release must be sent by the payment's payer or payee — omit `from` to build it for the signed-in wallet",
57
57
  "payer_must_be_caller" => "the payer must be the signed-in wallet — sign in as the buyer to create its payment",
58
- "wallet_deactivated" => "this wallet is deactivated: it can still read, but not act on payments or change the account — reactivate it or sign in with another of the account's wallets",
58
+ # gateway#439: a deactivated session may switch its OWN wallet back on when its owner
59
+ # switched it off; an operator's switch-off is the operator's to lift.
60
+ "wallet_deactivated" => "this wallet is deactivated: it can still read, but not act on payments or change the account — if you switched it off, its own session can switch it back on (wallets.update(..., active: true)), or sign in with another of the account's wallets",
61
+ "wallet_deactivated_by_operator" => "an operator deactivated this wallet, so only an operator can reactivate it — ask an operator to reactivate it",
59
62
  "account_deactivated" => "the merchant account is deactivated: void, release and refund still work, but authorize, charge, capture and account changes do not — ask an operator to reactivate it",
60
63
  # A session is bound to the one wallet it signed in with: the account's other wallets
61
64
  # are readable, but not this session's to change (operators are exempt).
@@ -65,8 +68,9 @@ module Rail0
65
68
  "wallet_add_unavailable" => "adding a wallet to an account is not available yet — it returns with the merchant wallet-management section",
66
69
  # Login resolves the account FROM a wallet, so an account left with no active one
67
70
  # could neither act nor add a wallet back. Adding a wallet is suspended and a session
68
- # changes only its own wallet, so the other wallet has to be reactivated by an operator.
69
- "last_active_wallet" => "this is the account's only active wallet — another wallet must be active before this one can be switched off (adding a wallet is not available yet; an operator can reactivate one)",
71
+ # changes only its own wallet, so the other wallet comes back on from its own session
72
+ # (if its owner switched it off) or from an operator (gateway#439).
73
+ "last_active_wallet" => "this is the account's only active wallet — another wallet must be active before this one can be switched off: sign in with one you switched off and reactivate it, or ask an operator (adding a wallet is not available yet)",
70
74
  "last_admin" => "the change would leave no active operator — grant the operator role to another active account first",
71
75
  "not_redrivable" => "only a pending transaction that already holds its signed bytes can be redriven — check the transaction's `redrivable` flag, and submit the signature if it has none",
72
76
  "refund_expired" => "the refund window has closed (refundExpiry passed) — refund/dispute is no longer possible",
@@ -30,12 +30,16 @@ module Rail0
30
30
 
31
31
  # The account's own profile.
32
32
  # @param account_id [String] The account UUID — must be the one this JWT belongs to.
33
- # @return [Hash] `id`, `name`, `email`, `priority_tip`, `created_at`, `updated_at`.
33
+ # @return [Hash] `id`, `name`, `email`, `priority_tip`, `authorization_ttl`, `refund_ttl`,
34
+ # `payment_windows`, `created_at`, `updated_at`.
34
35
  # `email` is part of the response because only the account's owner, or an operator,
35
36
  # can read it. `priority_tip` is the fee setting of the account's OWN transactions
36
37
  # (authorize, capture, void, release, refund): true, the default, tips the network's
37
38
  # suggested priority fee; false uses the chain's minimum tip — 0 on most chains,
38
39
  # 25 gwei on Polygon PoS/Amoy. The buyer's dispute/close_dispute never read it.
40
+ # `authorization_ttl` / `refund_ttl` are the payment windows the account's NEW
41
+ # payments are created with, in seconds — its own or the gateway default;
42
+ # `payment_windows` says which, and the bounds a new value must fit.
39
43
  def get(account_id)
40
44
  http.get("/accounts/#{segment(account_id)}")
41
45
  end
@@ -48,6 +52,10 @@ module Rail0
48
52
  # are UNIQUE across accounts, so a value another account already holds answers 409.
49
53
  # `priority_tip: false` is a value, not an absence: it is sent as false and switches
50
54
  # the account's own transactions to the chain's minimum tip; nil leaves it alone.
55
+ # The payment windows take a number of seconds, or `:default` to put one back on the
56
+ # gateway default (sent as null); nil leaves it alone. Outside the bounds in
57
+ # `payment_windows`, or closer than its `min_refund_window`, the gateway answers 422
58
+ # invalid_payment_windows. A change moves only the payments created afterwards.
51
59
  #
52
60
  # This is a write on the account itself, so a deactivated wallet (403
53
61
  # wallet_deactivated) or a deactivated account (403 account_deactivated) cannot
@@ -60,17 +68,35 @@ module Rail0
60
68
  # @param priority_tip [Boolean, nil] Tip the network's suggested priority fee on the
61
69
  # account's own transactions (true, the gateway default) or the chain's minimum tip
62
70
  # (false). Owner or admin.
71
+ # @param authorization_ttl [Integer, :default, nil] Authorization (capture) window of
72
+ # new payments, in seconds, or :default. Owner or admin.
73
+ # @param refund_ttl [Integer, :default, nil] Refund and dispute window of new
74
+ # payments, in seconds, or :default. Owner or admin.
63
75
  # @return [Hash] the updated profile, same shape as {get}.
64
- # @raise [ArgumentError] when none of name, email or priority_tip is given.
65
- def update(account_id, name: nil, email: nil, priority_tip: nil)
76
+ # @raise [ArgumentError] when no field is given, or a window is neither an Integer
77
+ # nor :default.
78
+ def update(account_id, name: nil, email: nil, priority_tip: nil, authorization_ttl: nil, refund_ttl: nil)
66
79
  body = {}
67
80
  body[:name] = name unless name.nil?
68
81
  body[:email] = email unless email.nil?
69
82
  body[:priority_tip] = priority_tip unless priority_tip.nil?
70
- raise ArgumentError, "at least one of name, email or priority_tip is required" if body.empty?
83
+ { authorization_ttl: authorization_ttl, refund_ttl: refund_ttl }.each do |key, value|
84
+ body[key] = window(key, value) unless value.nil?
85
+ end
86
+ raise ArgumentError, "at least one of name, email, priority_tip or a payment window is required" if body.empty?
71
87
 
72
88
  http.patch("/accounts/#{segment(account_id)}", body)
73
89
  end
90
+
91
+ private
92
+
93
+ # A payment window as the gateway takes it: whole seconds, or null for the default.
94
+ def window(key, value)
95
+ return if value == :default
96
+ return value if value.is_a?(Integer)
97
+
98
+ raise ArgumentError, "#{key} must be an Integer number of seconds or :default"
99
+ end
74
100
  end
75
101
  end
76
102
  end
@@ -13,6 +13,11 @@ module Rail0
13
13
  # step (e.g. MetaMask) skip the local signer and report the hash via
14
14
  # {submit_by_hash} instead.
15
15
  class Payments
16
+ # The default of {update}'s keywords: "not passed", told apart from an explicit +nil+
17
+ # (which is sent, as JSON null, and clears the field).
18
+ UNSET = Object.new.freeze
19
+ private_constant :UNSET
20
+
16
21
  include Query
17
22
 
18
23
  # Operations accepted by the prepare/submit endpoints.
@@ -62,6 +67,10 @@ module Rail0
62
67
  # `last_error_code`, false = none.
63
68
  # @param expiring_within [Integer, nil] scope "all" only: `authorized` payments whose
64
69
  # `authorization_expiry` falls within this many SECONDS from now (1..90 days).
70
+ # @param reference [String, nil] Only payments with this reference — the payment's
71
+ # identifier in the merchant's own system (exact match).
72
+ # @param reference_origin [String, nil] Only payments whose reference comes from this
73
+ # system (case-insensitive).
65
74
  # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
66
75
  # @param page [Integer, nil] Page number (1-based; 1..1,000,000, 400 outside).
67
76
  # @param per_page [Integer, nil] Items per page (max 100).
@@ -74,9 +83,11 @@ module Rail0
74
83
  def list(status: nil, mode: nil, payer: nil, payee: nil, token: nil, rail0_id: nil,
75
84
  chain_id: nil, disputed: nil, operation: nil, min_amount: nil, max_amount: nil,
76
85
  created_from: nil, created_to: nil, scope: nil, account_id: nil, has_error: nil,
77
- expiring_within: nil, sort: nil, page: nil, per_page: nil)
86
+ expiring_within: nil, reference: nil, reference_origin: nil, sort: nil, page: nil,
87
+ per_page: nil)
78
88
  query = build_query(status: status, mode: mode, payer: payer, payee: payee, token: token,
79
- rail0_id: rail0_id, chain_id: chain_id, disputed: disputed,
89
+ rail0_id: rail0_id, reference: reference, reference_origin: reference_origin,
90
+ chain_id: chain_id, disputed: disputed,
80
91
  operation: operation,
81
92
  min_amount: min_amount, max_amount: max_amount,
82
93
  created_from: created_from, created_to: created_to,
@@ -97,7 +108,10 @@ module Rail0
97
108
  # create(chain_id: 84532, mode: "authorize", amount: "100.00", token: "0x…", payer: "0x…", payee: "0x…")
98
109
  # create({ chain_id: 84532, ... }, idempotency_key: "order-42")
99
110
  #
100
- # @param params [Hash, nil] chain_id, mode, amount, token, payer, payee, description (opt), metadata (opt).
111
+ # @param params [Hash, nil] chain_id, mode, amount, token, payer, payee, description (opt),
112
+ # metadata (opt), reference (opt — the payment's identifier in the merchant's own
113
+ # system, max 128 characters, not unique, an idempotency term), reference_origin (opt
114
+ # — which system: a lowercase slug, e.g. "commercelayer"; needs a reference).
101
115
  # @param idempotency_key [String, nil] Optional Idempotency-Key header value.
102
116
  # @param fields [Hash] Field keywords, used when +params+ is omitted.
103
117
  # @return [Hash]
@@ -116,22 +130,36 @@ module Rail0
116
130
  http.get("/payments/#{segment(id)}")
117
131
  end
118
132
 
119
- # Set or clear a payment's description (PATCH /payments/{id}).
133
+ # Set or clear a payment's off-chain fields (PATCH /payments/{id}).
134
+ #
135
+ # Off-chain metadata, so it can change at any status, settled payments included. Only
136
+ # a participant (the payer or the payee) may call it — a non-participant gets the same
137
+ # 404 as an unknown id, and a deactivated caller wallet a 403 `wallet_deactivated`.
138
+ #
139
+ # Only the keywords you PASS are sent, and at least one is required (ArgumentError
140
+ # first, rather than the gateway's 400). +nil+ or +""+ clears a field.
120
141
  #
121
- # The description is the only field this route edits: it is off-chain metadata, so
122
- # it can change at any status, settled payments included. Only a participant (the
123
- # payer or the payee) may edit it — a non-participant gets the same 404 as an unknown
124
- # id, and a deactivated caller wallet a 403 `wallet_deactivated`.
142
+ # - +description:+ either party, max 255 characters (422 over).
143
+ # - +merchant_note:+ the PAYEE's internal note, max 2000 characters — never shown to
144
+ # the payer, not in webhooks.
145
+ # - +reference:+ / +reference_origin:+ the payment's identifier in the merchant's own
146
+ # system and which system; the payee corrects them, and clearing the reference
147
+ # clears its origin.
125
148
  #
126
- # +description:+ is required, and is always sent: passing +nil+ sends JSON +null+,
127
- # which clears it (as does +""+). Over 255 characters is a 422.
128
- # (rail0-gateway#373)
149
+ # The last three are the payee's: the payer gets 403 `not_the_payee`.
129
150
  #
130
151
  # @param id [String] Payment UUID or rail0_id.
131
- # @param description [String, nil] The new description; +nil+ or +""+ clears it.
132
152
  # @return [Hash] The updated payment, the same shape {get} returns.
133
- def update(id, description:)
134
- http.patch("/payments/#{segment(id)}", { description: description })
153
+ # @raise [ArgumentError] when no field is given.
154
+ def update(id, description: UNSET, merchant_note: UNSET, reference: UNSET, reference_origin: UNSET)
155
+ body = { description: description, merchant_note: merchant_note, reference: reference,
156
+ reference_origin: reference_origin }.reject { |_, v| v.equal?(UNSET) }
157
+ if body.empty?
158
+ raise ArgumentError,
159
+ "at least one of description, merchant_note, reference or reference_origin is required"
160
+ end
161
+
162
+ http.patch("/payments/#{segment(id)}", body)
135
163
  end
136
164
 
137
165
  # List on-chain transactions for a payment.
@@ -92,11 +92,16 @@ module Rail0
92
92
  # operator may change any). +active: false+ on the account's only active wallet
93
93
  # raises ApiError 422 +last_active_wallet+: login resolves the account from a
94
94
  # wallet, so an account with none could neither act nor add one back.
95
+ #
96
+ # +active: true+ is the one write a deactivated wallet's own session may make: it
97
+ # switches the wallet back on when its owner switched it off. A wallet an operator
98
+ # switched off (+deactivated_by+ "operator", or nil on an older switch-off) raises
99
+ # ApiError 403 +wallet_deactivated_by_operator+: only an operator lifts it.
95
100
  # @param account_id [String] Account UUID.
96
101
  # @param id_or_address [String] Wallet UUID or 0x address.
97
102
  # @param label [String, nil] New label.
98
103
  # @param active [Boolean, nil] New active status.
99
- # @return [Hash] id, address, label, active
104
+ # @return [Hash] id, address, label, active, deactivated_by
100
105
  def update(account_id, id_or_address, label: nil, active: nil)
101
106
  body = {}
102
107
  body[:label] = label unless label.nil?
@@ -127,6 +127,18 @@ module Rail0
127
127
  http.put("/webhooks/#{segment(id)}/reset_circuit")
128
128
  end
129
129
 
130
+ # Send a test event to the webhook's callback now and report what it did
131
+ # (POST /webhooks/{id}/test). The event has topic "webhooks.test", +test: true+ and
132
+ # no payment, and is signed and delivered like a real one. Synchronous, one attempt,
133
+ # not logged in {event_callbacks} and not counted by the circuit breaker; it works on
134
+ # a disabled webhook. A refusal by the endpoint is a result, not an error.
135
+ # @param id [String] Webhook UUID.
136
+ # @return [Hash] +delivered+, +response_code+, +response_message+, +error_reason+,
137
+ # +duration_ms+.
138
+ def test(id)
139
+ http.post("/webhooks/#{segment(id)}/test", {})
140
+ end
141
+
130
142
  # List delivery attempts for a webhook.
131
143
  # @param id [String] Webhook UUID.
132
144
  # @param status [String, nil] Filter by delivery status: "delivered" or "failed" only
data/lib/rail0/types.rb CHANGED
@@ -209,10 +209,11 @@ module Rail0
209
209
 
210
210
  # Public-safe wallet view (the reduced set a buyer needs to discover a merchant's payment methods).
211
211
  Wallet = Struct.new(
212
- :id, # String
213
- :address, # String
212
+ :id, # String
213
+ :address, # String
214
214
  :label,
215
- :active, # Boolean
215
+ :active, # Boolean
216
+ :deactivated_by, # Who switched the wallet off: `owner` (a session signed in with this wallet) or `operator` (an admin session). Null while active, and on a switch-off that predates the record — treated as `operator`. A wallet an operator switched off can be switched back on only by an operator (403 `wallet_deactivated_by_operator`). Always null on GET /payment_methods, which serves active wallets only.
216
217
  keyword_init: true
217
218
  )
218
219
 
@@ -229,11 +230,12 @@ module Rail0
229
230
  # A wallet with its token holdings nested inline. `tokens` is empty when the wallet has none — the
230
231
  # wallet is still returned, never omitted.
231
232
  WalletWithTokens = Struct.new(
232
- :id, # String
233
- :address, # String
233
+ :id, # String
234
+ :address, # String
234
235
  :label,
235
- :active, # Boolean
236
- :tokens, # Array
236
+ :active, # Boolean
237
+ :deactivated_by, # Who switched the wallet off: `owner` (a session signed in with this wallet) or `operator` (an admin session). Null while active, and on a switch-off that predates the record — treated as `operator`. A wallet an operator switched off can be switched back on only by an operator (403 `wallet_deactivated_by_operator`). Always null on GET /payment_methods, which serves active wallets only.
238
+ :tokens, # Array
237
239
  keyword_init: true
238
240
  )
239
241
 
@@ -263,6 +265,9 @@ module Rail0
263
265
  :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.
264
266
  :last_error_message, # Human-readable form of last_error_code.
265
267
  :description,
268
+ :reference, # This payment's identifier in the merchant's own system; set at create, correctable by the payee.
269
+ :reference_origin, # The system the reference comes from, a lowercase slug (e.g. commercelayer).
270
+ :merchant_note, # The payee's internal note (PATCH /payments/{id}). Present only when the session is the payment's PAYEE, and on the operator's Full view; absent for the payer.
266
271
  :metadata,
267
272
  :signed_at, # When the payer signature was stored (null while unsigned).
268
273
  :created_at, # String
@@ -289,6 +294,16 @@ module Rail0
289
294
  keyword_init: true
290
295
  )
291
296
 
297
+ # What the callback did with a test event (POST /webhooks/{id}/test).
298
+ WebhookTestResult = Struct.new(
299
+ :delivered, # Boolean — The endpoint answered 2xx.
300
+ :response_code, # The HTTP status it answered; null when no response came (see error_reason).
301
+ :response_message, # Up to 1000 printable characters of its response body.
302
+ :error_reason, # Why no response came: a refused address, a TLS or connection error, the deadline.
303
+ :duration_ms, # Integer — How long the attempt took.
304
+ keyword_init: true
305
+ )
306
+
292
307
  Transaction = Struct.new(
293
308
  :id, # String
294
309
  :payment_id, # String
@@ -334,32 +349,48 @@ module Rail0
334
349
  )
335
350
 
336
351
  # The account's own profile as the holder reads it (GET) and as a PATCH returns it — id, name,
337
- # email, priority_tip, timestamps. Deliberately no admin/role field: the operator grant lives in a
338
- # separate table, so a standard account's profile carries no trace of that axis. An ADMIN reading
339
- # any account gets the whole record plus `admin` instead, which is a different shape and not this
340
- # one.
352
+ # email, priority_tip, payment windows, timestamps. Deliberately no admin/role field: the operator
353
+ # grant lives in a separate table, so a standard account's profile carries no trace of that axis. An
354
+ # ADMIN reading any account gets the whole record plus `admin` instead, which is a different shape
355
+ # and not this one.
341
356
  Account = Struct.new(
342
- :id, # String
343
- :name, # String
357
+ :id, # String
358
+ :name, # String
344
359
  :email,
345
- :priority_tip, # Boolean — Whether the merchant's own transactions tip the network's suggested priority fee (true, the default) or the chain's minimum tip (false: 0 on most chains, 25 gwei on Polygon PoS/Amoy). The buyer's dispute/close_dispute never read it.
346
- :created_at, # String
347
- :updated_at, # String
360
+ :priority_tip, # Boolean — Whether the merchant's own transactions tip the network's suggested priority fee (true, the default) or the chain's minimum tip (false: 0 on most chains, 25 gwei on Polygon PoS/Amoy). The buyer's dispute/close_dispute never read it.
361
+ :authorization_ttl, # Integer — Seconds an authorization of the account's new payments stays capturable after the payment is created: the account's own value, or the gateway default (AUTHORIZATION_TTL).
362
+ :refund_ttl, # Integer — Seconds the account's new payments stay refundable, and open to a dispute, after they are created: the account's own value, or the gateway default (REFUND_TTL).
363
+ :payment_windows, # PaymentWindows
364
+ :created_at, # String
365
+ :updated_at, # String
366
+ keyword_init: true
367
+ )
368
+
369
+ # How the account's windows are set and what a new value must meet: per window, whether it is the
370
+ # account's own (custom) or the default, the default, and the bounds. The refund window has no floor
371
+ # of its own: the pair must keep refund_ttl - authorization_ttl >= min_refund_window.
372
+ PaymentWindows = Struct.new(
373
+ :authorization_ttl, # Hash
374
+ :refund_ttl, # Hash
375
+ :min_refund_window, # Integer
348
376
  keyword_init: true
349
377
  )
350
378
 
351
379
  # The whole account record, as an operator reads it (Full entity: GET /accounts, and GET/PATCH
352
380
  # /accounts/{account_id} for an admin session): every column plus `admin` and the derived `status`.
353
381
  AccountFull = Struct.new(
354
- :id, # String
355
- :name, # String
356
- :email, # String — Stored lowercase.
357
- :active, # Boolean — The operator's switch. false = deactivated.
358
- :priority_tip, # Boolean — The merchant's fee setting (see Account.priority_tip).
359
- :created_at, # String
360
- :updated_at, # String
361
- :admin, # Boolean — Whether the account holds the operator grant (a row in administrators).
362
- :status, # String — One-word reading of active: active, or deactivated (switched off by an operator).
382
+ :id, # String
383
+ :name, # String
384
+ :email, # String — Stored lowercase.
385
+ :active, # Boolean — The operator's switch. false = deactivated.
386
+ :priority_tip, # Boolean — The merchant's fee setting (see Account.priority_tip).
387
+ :authorization_ttl, # Integer — Seconds an authorization of the account's new payments stays capturable after the payment is created: the account's own value, or the gateway default (AUTHORIZATION_TTL).
388
+ :refund_ttl, # Integer — Seconds the account's new payments stay refundable, and open to a dispute, after they are created: the account's own value, or the gateway default (REFUND_TTL).
389
+ :payment_windows, # PaymentWindows
390
+ :created_at, # String
391
+ :updated_at, # String
392
+ :admin, # Boolean — Whether the account holds the operator grant (a row in administrators).
393
+ :status, # String — One-word reading of active: active, or deactivated (switched off by an operator).
363
394
  keyword_init: true
364
395
  )
365
396
 
@@ -474,7 +505,8 @@ module Rail0
474
505
  :webhooks, # Hash
475
506
  :settlement, # Hash
476
507
  :rate_limit, # Hash
477
- :siwe, # Hash
508
+ :siwe, # Hash — domains (SIWE_DOMAIN), accepted_chain_ids (1 plus the ids of the active catalog chains: the chainId values a login may carry) and nonce_ttl.
509
+ :session, # Hash — environment (RAIL0_ENVIRONMENT, default RACK_ENV: the scope every issued token is bound to), jwt_ttl and jwt_audience: the session token's lifetime (a constant) and audience (`rail0-gateway:<environment>`).
478
510
  :fees, # Hash
479
511
  :rpc, # Hash
480
512
  :onboarding, # Hash
@@ -532,18 +564,5 @@ module Rail0
532
564
  :contract_address, # String — Address (lowercase) of the RAIL0 deployment the payment is bound to, archived versions included. The sweeper accepts a receipt's event only when this contract emitted it.
533
565
  keyword_init: true
534
566
  )
535
-
536
- # Per-chain indexer config.
537
- SyncBlockchain = Struct.new(
538
- :chain_id, # Integer
539
- :start_block, # Integer
540
- :required_confirmations, # Integer — Fallback burial depth, used where the chain serves no finality tag.
541
- :finality_tag, # String — Which block the chain calls settled; the indexer gates every notify on it.
542
- :explorer_url, # Block explorer base URL; null when the chain has none.
543
- :network_type, # String — "testnet" or "mainnet"; selects which chains a deployment indexes.
544
- :rpc_urls, # Array — Ordered list of public RPC endpoints tried in turn (serial fallback).
545
- :contracts, # Array — Active RAIL0 contract addresses on the chain (all watched versions).
546
- keyword_init: true
547
- )
548
567
  end
549
568
  end
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.7.1"
8
+ VERSION = "1.8.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.7.1
4
+ version: 1.8.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-10-20 00:00:00.000000000 Z
11
+ date: 2026-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger