rail0-sdk 1.7.0 → 1.7.2

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: 861d8ebab53accbd5a962ed1cb040a91461841b63722077b93374291f8de66bd
4
- data.tar.gz: e7d22a5a5d2e9ad6a22149427195b1d7d341e15da84e1d858f453d343859b771
3
+ metadata.gz: 3c69ef678235fc14acf0f3bcaebc302bd3b63d6dd1b41a1358381b49fd6a9738
4
+ data.tar.gz: 1d42362b54cf2118b2150087211bd7f5df49a8248278af49603fd745d32be08b
5
5
  SHA512:
6
- metadata.gz: b926413fd2b987d43b60b78216506baa56eb8954c3d51c7ba2e61a977e597a250c2b961772a27fba93bca1a2f08b036250c15111ecac692823e3ab84548f2004
7
- data.tar.gz: a43b612c63ea9114034c4e80eeccb9670758bfecae1267efe5568f322475adfc8dcdf2f935b35f40d9cfcf8559b94b1e65a00b36093c937bf5ac6a385920ba5c
6
+ metadata.gz: 061516a40ea00e3ebf114f9ae841bfac830682f443caaa8f82fbe4789de91d1eab760896c6a0447a371092cd01f5bbb9f30200e8e17f0897bce60390ba541e82
7
+ data.tar.gz: 5f0e8c80653d3234d24f2816f607a84ecfb99c743afa2dd58fa64d3c184510458796ab54efc8cad00c7b478f35a26bf68d6339e1c592dff8d2712d087e3aae03
data/README.md CHANGED
@@ -150,8 +150,16 @@ signs after `create` (and the payee signs in refund phase 1) is the more dangero
150
150
  two artifacts: the **token** honours it on its own, with no RAIL0 involvement, for up to
151
151
  its `value` — so an endpoint that answered with `{from: you, to: attacker, value: your
152
152
  whole balance}` used to get exactly that signed. `sign_signing_payload(signing_payload,
153
- key, payment:, amount: nil)` checks it first, against data you already hold:
154
- `message.from` is your key's address, `domain.chainId` the payment's `chain_id`,
153
+ key, payment:, amount: nil)` checks it first. The typed-data envelope: `primaryType` is
154
+ `ReceiveWithAuthorization` (authorize/charge on v1.4.0+ deployments, and every refund) or
155
+ `TransferWithAuthorization` (authorize/charge on v1.1.0–v1.3.x); `types[primaryType]` is
156
+ **exactly** the EIP-3009 struct — `address from, address to, uint256 value, uint256
157
+ validAfter, uint256 validBefore, bytes32 nonce`, same names, types and order;
158
+ `types.EIP712Domain`, when present, is exactly `string name, string version, uint256
159
+ chainId, address verifyingContract`; and `types` defines nothing else (a local key hashes
160
+ with fixed typehashes, but a browser wallet hashes whatever `types` says, so this is what
161
+ lets `PayloadVerify.verify` guard an `eth_signTypedData_v4` call). Then, against data you
162
+ already hold: `message.from` is your key's address, `domain.chainId` the payment's `chain_id`,
155
163
  `domain.verifyingContract` the payment's `token`, `message.to` its `rail0_contract`, and
156
164
  `message.value` **exactly** the amount — `amount:` scaled by the payment's `decimals` when
157
165
  you pass one (a refund), otherwise the payment's own base-unit `amount` (a created
@@ -374,6 +382,7 @@ client.wallets.list(account_id, token_active: true)
374
382
 
375
383
  client.wallets.get(account_id, id_or_address)
376
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)
377
386
  client.wallets.delete(account_id, id_or_address) # 204
378
387
  client.wallets.balances(account_id, id_or_address, chain_id: 84532) # live on-chain balances
379
388
  ```
@@ -384,6 +393,17 @@ refused with 422 `last_active_wallet` — login resolves the account from a wall
384
393
  so an account with none could neither act nor add one back. Another wallet must
385
394
  be active first.
386
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
+
387
407
  **A session is bound to the wallet it signed in with.** Reads (`list`, `get`,
388
408
  `balances`) are account-wide, but `update`, `delete` and the token-holding methods
389
409
  act only on the session's own wallet: another wallet of the same account is
@@ -827,7 +847,7 @@ Requires the `eth` gem. No private key ever leaves your process.
827
847
  |--------|-----|
828
848
  | `sign_signing_payload(signing_payload, signer, payment:, amount: nil)` | Verify the EIP-3009 payload from a create/refund response against the payment, then sign it verbatim — the recommended path, and the only one that follows the gateway across contract versions |
829
849
  | `sign_payload(signer, signing_payload, payment: nil, amount: nil)` | The same signer; with `payment:` it is `sign_signing_payload`, without it signs blindly (**deprecated**) |
830
- | `Rail0::PayloadVerify.verify(signing_payload, payment:, signer:, amount: nil)` | The payload pre-sign check on its own |
850
+ | `Rail0::PayloadVerify.verify(signing_payload, payment:, signer:, amount: nil)` | The payload pre-sign check on its own — run it before handing the payload to a browser wallet |
831
851
  | `sign_transaction(unsigned_transaction, key, payment:, operation:, amount: nil)` | Verify a prepare step's unsigned EIP-1559 transaction against the payment, then sign it; returns the 0x raw tx. Without `payment:` it signs blindly (**deprecated**) |
832
852
  | `Rail0::TxVerify.verify(unsigned_transaction, payment:, operation:, amount: nil, signer: nil)` | The pre-sign check on its own — for a signer that is not a local key |
833
853
  | `sign_transfer_with_authorization(signer, domain, params)` | Raw EIP-3009 `TransferWithAuthorization` signer, for talking to a token directly |
@@ -846,7 +866,8 @@ client.payments.sign(payment[:rail0_id], { signature: sig.to_hex })
846
866
  **The gateway builds the payload; you sign it verbatim.** `sign_signing_payload` takes the
847
867
  typehash from the payload's `primaryType` and every field from its `message` — only
848
868
  the gateway knows which contract version a payment lives on, and the version selects
849
- the typehash, the EIP-712 domain and the field layout. An unrecognised `primaryType`
869
+ the typehash and the EIP-712 domain (the field layout is EIP-3009's own, the same on
870
+ every version, which is why the check pins it). An unrecognised `primaryType`
850
871
  raises rather than falling back, because a guessed typehash yields a *valid*
851
872
  signature over the wrong digest, which fails only on-chain, after gas.
852
873
 
@@ -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",
@@ -27,10 +27,17 @@ module Rail0
27
27
  # 5. +message.value+ is EXACTLY the amount asked for, in base units — not a ceiling:
28
28
  # the gateway asks for the full amount of the operation.
29
29
  #
30
+ # Before those, the typed-data envelope: +primaryType+ is one of the two EIP-3009
31
+ # authorization types, +types[primaryType]+ is EXACTLY the EIP-3009 field list (names,
32
+ # types, order), +types.EIP712Domain+ (when present) is exactly the token domain's
33
+ # field list, and +types+ defines nothing else. {Signing.sign_payload} hashes with
34
+ # fixed typehashes and would not notice, but a browser wallet hashes whatever +types+
35
+ # says — so a caller that verifies here and then hands the payload to
36
+ # +eth_signTypedData_v4+ needs it.
37
+ #
30
38
  # The payload is still signed VERBATIM: the typehash comes from its own +primaryType+
31
39
  # and the digest from its own message. This only refuses a message whose fields
32
- # contradict what the caller asked for. Like the CLI, it does not check +primaryType+
33
- # against the contract version (the signer already refuses an unknown one), the
40
+ # contradict what the caller asked for. Like the CLI, it does not check the
34
41
  # validity window, or +message.nonce+ — recomputing the nonce needs the deployment's
35
42
  # VERSION(), which no payment response carries. The bounded residual is an
36
43
  # authorization for the RIGHT token, on the RIGHT chain, from the RIGHT signer, for
@@ -62,6 +69,7 @@ module Rail0
62
69
  pay = payment_fields(payment)
63
70
  refuse("cannot check the authorization without the address of the signing key") if blank?(signer)
64
71
 
72
+ check_typed_data(sp[:primaryType], sp[:types])
65
73
  check_from(message[:from], signer.to_s)
66
74
  check_chain(domain[:chainId], pay[:chain_id])
67
75
  check_address(domain[:verifyingContract], pay[:token], "token") do |got, want|
@@ -91,6 +99,73 @@ module Rail0
91
99
 
92
100
  private_class_method :parse, :payment_fields
93
101
 
102
+ # The EIP-3009 authorization types the gateway emits. Which one a payment needs is the
103
+ # deployment's contract version, which no payment response carries: 1.4.0 and later
104
+ # use ReceiveWithAuthorization for authorize/charge, 1.1.0–1.3.x used
105
+ # TransferWithAuthorization, and refunds are ReceiveWithAuthorization on every
106
+ # version. So both are accepted and nothing else is.
107
+ PRIMARY_TYPES = %w[TransferWithAuthorization ReceiveWithAuthorization].freeze
108
+
109
+ # The EIP-3009 struct shared by both types: names, types AND order are the typehash.
110
+ AUTHORIZATION_FIELDS = [
111
+ %w[from address], %w[to address], %w[value uint256],
112
+ %w[validAfter uint256], %w[validBefore uint256], %w[nonce bytes32]
113
+ ].freeze
114
+
115
+ # The token's EIP-712 domain, as the gateway declares it and the token hashes it.
116
+ DOMAIN_FIELDS = [
117
+ %w[name string], %w[version string], %w[chainId uint256], %w[verifyingContract address]
118
+ ].freeze
119
+
120
+ private_constant :PRIMARY_TYPES, :AUTHORIZATION_FIELDS, :DOMAIN_FIELDS
121
+
122
+ # The typed-data envelope: refuses an unknown +primaryType+, a +types[primaryType]+
123
+ # that differs from the EIP-3009 struct in any name, type or position, an
124
+ # +EIP712Domain+ that differs from the token domain, or any further type.
125
+ def self.check_typed_data(primary_type, types)
126
+ unless PRIMARY_TYPES.include?(primary_type)
127
+ refuse("the authorization's primaryType #{primary_type.inspect} is not an EIP-3009 authorization type " \
128
+ "(TransferWithAuthorization or ReceiveWithAuthorization)")
129
+ end
130
+ refuse("the signing payload has no types") unless types.is_a?(Hash)
131
+
132
+ types = types.transform_keys(&:to_s)
133
+ extra = types.keys.find { |name| name != primary_type && name != "EIP712Domain" }
134
+ if extra
135
+ refuse("the signing payload's types define #{extra.inspect}, which an EIP-3009 authorization does not use")
136
+ end
137
+ check_type_fields("types.#{primary_type}", types[primary_type], AUTHORIZATION_FIELDS)
138
+ check_type_fields("types.EIP712Domain", types["EIP712Domain"], DOMAIN_FIELDS) if types.key?("EIP712Domain")
139
+ end
140
+
141
+ def self.check_type_fields(label, fields, want)
142
+ return if exact_fields?(fields, want)
143
+
144
+ expected = want.map { |name, type| "#{type} #{name}" }.join(",")
145
+ refuse("#{label} is (#{describe_fields(fields)}) but must be exactly (#{expected})")
146
+ end
147
+
148
+ def self.describe_fields(fields)
149
+ return (fields.nil? ? "null" : fields.inspect) unless fields.is_a?(Array)
150
+
151
+ fields.map { |f| f.is_a?(Hash) ? "#{field_of(f, 'type')} #{field_of(f, 'name')}" : f.inspect }.join(",")
152
+ end
153
+
154
+ def self.exact_fields?(fields, want)
155
+ fields.is_a?(Array) && fields.size == want.size &&
156
+ fields.each_with_index.all? do |f, i|
157
+ f.is_a?(Hash) && f.size == 2 && field_of(f, "name") == want[i][0] && field_of(f, "type") == want[i][1]
158
+ end
159
+ end
160
+
161
+ # A field entry's +name+ / +type+, whether the payload arrived string- or
162
+ # symbol-keyed.
163
+ def self.field_of(field, key)
164
+ field.key?(key) ? field[key] : field[key.to_sym]
165
+ end
166
+
167
+ private_class_method :check_typed_data, :check_type_fields, :describe_fields, :exact_fields?, :field_of
168
+
94
169
  # Check 1: from = the key's own address. An absent +from+ is a mismatch, not a skip.
95
170
  def self.check_from(from, signer)
96
171
  return if same_address?(from, signer)
@@ -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?
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
 
@@ -457,24 +459,25 @@ module Rail0
457
459
  keyword_init: true
458
460
  )
459
461
 
460
- # One reported policy value.
461
- PolicySetting = Struct.new(
462
+ # One reported configuration value.
463
+ ConfigSetting = Struct.new(
462
464
  :value, # The effective value (number, boolean, string, list or map).
463
465
  :env, # String — The environment variable that sets it; null for a constant.
464
466
  :overridden, # Boolean — Whether that variable is set in this process.
465
- :description, # String — Plain-text explanation of the entry: what it controls, which part of the gateway reads it and the effect of raising or lowering it, and its unit (from config/policy_descriptions.yml). Every entry has one; older gateways omit the field.
467
+ :description, # String — Plain-text explanation of the entry: what it controls, which part of the gateway reads it and the effect of raising or lowering it, and its unit (from config/config_descriptions.yml). Every entry has one; older gateways omit the field.
466
468
  keyword_init: true
467
469
  )
468
470
 
469
- # GET /admin/policy: the effective policy, grouped. Every leaf is a PolicySetting.
470
- PolicyReport = Struct.new(
471
+ # GET /admin/config: the effective configuration, grouped. Every leaf is a ConfigSetting.
472
+ ConfigReport = Struct.new(
471
473
  :payments, # Hash
472
474
  :recovery, # Hash
473
475
  :retention, # Hash
474
476
  :webhooks, # Hash
475
477
  :settlement, # Hash
476
478
  :rate_limit, # Hash
477
- :siwe, # Hash
479
+ :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.
480
+ :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
481
  :fees, # Hash
479
482
  :rpc, # Hash
480
483
  :onboarding, # Hash
@@ -532,18 +535,5 @@ module Rail0
532
535
  :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
536
  keyword_init: true
534
537
  )
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
538
  end
549
539
  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.0"
8
+ VERSION = "1.7.2"
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.0
4
+ version: 1.7.2
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-19 00:00:00.000000000 Z
11
+ date: 2026-10-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger