rail0-sdk 1.5.0 → 1.6.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 +4 -4
- data/README.md +52 -11
- data/lib/rail0/error_hints.rb +25 -3
- data/lib/rail0/resources/wallets.rb +20 -4
- data/lib/rail0/signing.rb +40 -6
- data/lib/rail0/tx_verify.rb +280 -0
- data/lib/rail0/types.rb +87 -4
- data/lib/rail0/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e046261654c0f0d93915dd0ac7003e78f224115748e697581cac6020c4ea4003
|
|
4
|
+
data.tar.gz: f9fea94c35a6b996732e0bbdc538361a229134edaa4f52bc6a010e1dfd55b871
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dcb35d580188aef4a5e6b36878e6e8b3da40dddd671a792eb3275f4baad286636ec6f00e6284d1a0263c6ec764d539e17d8c85ea1904b77f3f1326e8929174fd
|
|
7
|
+
data.tar.gz: 592f3db30a8c9ecdc755f5b64e9dc0a69166ca73f6b51efcae2c348fb28e60d39b148b41b3cd54015012837c7a8987583d3bc6621f6ef82a4bca3e8b14381526
|
data/README.md
CHANGED
|
@@ -82,8 +82,12 @@ sig = Rail0::Signing.sign_payload(BUYER_PRIVATE_KEY, payment[:signing_payload])
|
|
|
82
82
|
client.payments.sign(rail0_id, { signature: sig.to_hex })
|
|
83
83
|
|
|
84
84
|
# 3. Payee prepares + broadcasts the on-chain authorize tx (signs it locally).
|
|
85
|
-
|
|
86
|
-
|
|
85
|
+
# payment:/operation: are what the gateway's transaction is CHECKED against before
|
|
86
|
+
# the key signs it (see "Payment lifecycle"); `payment` is the record read from the gateway.
|
|
87
|
+
payment = client.payments.get(rail0_id)
|
|
88
|
+
prep = client.payments.authorize_prepare(rail0_id)
|
|
89
|
+
raw = Rail0::Signing.sign_transaction(prep[:unsigned_transaction], MERCHANT_PRIVATE_KEY,
|
|
90
|
+
payment: payment, operation: "authorize")
|
|
87
91
|
client.payments.authorize(rail0_id, { signed_transaction: raw }) # HTTP 202 (async)
|
|
88
92
|
|
|
89
93
|
# 4. Poll until the authorization confirms.
|
|
@@ -95,7 +99,8 @@ end
|
|
|
95
99
|
|
|
96
100
|
# 5. Payee captures the escrowed funds (partial capture is supported).
|
|
97
101
|
cap = client.payments.capture_prepare(rail0_id, "50.00")
|
|
98
|
-
raw = Rail0::Signing.sign_transaction(cap[:unsigned_transaction], MERCHANT_PRIVATE_KEY
|
|
102
|
+
raw = Rail0::Signing.sign_transaction(cap[:unsigned_transaction], MERCHANT_PRIVATE_KEY,
|
|
103
|
+
payment: payment, operation: "capture", amount: "50.00")
|
|
99
104
|
client.payments.capture(rail0_id, { signed_transaction: raw })
|
|
100
105
|
```
|
|
101
106
|
|
|
@@ -108,12 +113,34 @@ Each on-chain operation is a `prepare` → `submit` pair:
|
|
|
108
113
|
|
|
109
114
|
1. **prepare** — `client.payments.<op>_prepare(...)` returns a transaction whose
|
|
110
115
|
`unsigned_transaction` is the EIP-1559 field-set to sign.
|
|
111
|
-
2. **sign** — `Rail0::Signing.sign_transaction(unsigned, private_key)`
|
|
112
|
-
signed raw tx.
|
|
116
|
+
2. **sign** — `Rail0::Signing.sign_transaction(unsigned, private_key, payment:, operation:, amount:)`
|
|
117
|
+
verifies the transaction against the payment record, then returns the signed raw tx.
|
|
113
118
|
3. **submit** — `client.payments.<op>(id, { signed_transaction: raw })` broadcasts
|
|
114
119
|
it. The gateway acknowledges with HTTP 202 and confirms asynchronously — poll
|
|
115
120
|
`client.payments.get(id)` until the status settles.
|
|
116
121
|
|
|
122
|
+
**What `sign_transaction` verifies, and why.** The gateway builds the EIP-1559
|
|
123
|
+
transaction and your key signs it, so a compromised or misconfigured endpoint could
|
|
124
|
+
otherwise have that key sign anything — an `approve(spender, MAX)` on the token, a
|
|
125
|
+
`capture` answered to a `void` prepare, a capture for more than you asked. Given
|
|
126
|
+
`payment:` (the record from `payments.get`) and `operation:`, `sign_transaction` checks
|
|
127
|
+
the transaction **before** the key touches it, against data you already hold (no extra
|
|
128
|
+
request): `value` is zero (every RAIL0 operation is value-0), `chain_id` is the
|
|
129
|
+
payment's chain, `from` is your key's address, `to` is the payment's `rail0_contract`,
|
|
130
|
+
the 4-byte selector is the RAIL0 function for `operation:` (the selectors are derived
|
|
131
|
+
from the contract's function signatures, `Rail0::TxVerify::SELECTORS`, and pinned
|
|
132
|
+
against its ABI in the spec suite), the first calldata word is the payment's `rail0_id`,
|
|
133
|
+
and — for `capture` and `refund` — the amount word equals `amount:` scaled by the
|
|
134
|
+
payment's `decimals`. The first mismatch raises `Rail0::TxVerify::RefusedSignature`
|
|
135
|
+
("refusing to sign: …", naming the field) and no signature is produced. It fails closed:
|
|
136
|
+
a capture/refund with no `amount:`, or a payment with unknown `decimals`, is refused
|
|
137
|
+
rather than skipped. `to` is a consistency check only — the address comes from the same
|
|
138
|
+
gateway — which is why the calldata binding to this payment's id and operation is what
|
|
139
|
+
closes a swapped transaction. Calling `sign_transaction` without `payment:` still signs
|
|
140
|
+
blindly and is **deprecated** (a `:deprecated`-category warning; shown with
|
|
141
|
+
`-W:deprecated`). `Rail0::TxVerify.verify` runs the check on its own, for a signer that
|
|
142
|
+
is not a local key.
|
|
143
|
+
|
|
117
144
|
Wallets that sign **and** broadcast in one step (e.g. MetaMask via
|
|
118
145
|
`eth_sendTransaction`) skip steps 2–3 and report the hash instead:
|
|
119
146
|
|
|
@@ -327,8 +354,17 @@ client.wallets.balances(account_id, id_or_address, chain_id: 84532) # live on-ch
|
|
|
327
354
|
`delete` only deactivates (the same as `update(active: false)`; payments name the
|
|
328
355
|
address, so the row stays). Either way the account's **only active wallet** is
|
|
329
356
|
refused with 422 `last_active_wallet` — login resolves the account from a wallet,
|
|
330
|
-
so an account with none could neither act nor add one back.
|
|
331
|
-
|
|
357
|
+
so an account with none could neither act nor add one back. Another wallet must
|
|
358
|
+
be active first.
|
|
359
|
+
|
|
360
|
+
**A session is bound to the wallet it signed in with.** Reads (`list`, `get`,
|
|
361
|
+
`balances`) are account-wide, but `update`, `delete` and the token-holding methods
|
|
362
|
+
act only on the session's own wallet: another wallet of the same account is
|
|
363
|
+
refused with 403 `not_your_wallet`. An operator session is exempt.
|
|
364
|
+
|
|
365
|
+
> **Adding a wallet is currently unavailable.** The gateway answers every
|
|
366
|
+
> `wallets.create` with 403 `wallet_add_unavailable`. It returns with the merchant
|
|
367
|
+
> wallet-management section, which keeps the request shape and the proof below.
|
|
332
368
|
|
|
333
369
|
**Registering a wallet needs a SIWE proof-of-ownership of the address being
|
|
334
370
|
added**, not merely the session JWT. `auth.prove_address` runs that handshake and
|
|
@@ -457,7 +493,8 @@ sig = Rail0::Signing.sign_payload(MERCHANT_PRIVATE_KEY, p1[:signing_payload])
|
|
|
457
493
|
|
|
458
494
|
# Phase 2 — amount + signature → returns the unsigned on-chain tx.
|
|
459
495
|
p2 = client.payments.refund_prepare(rail0_id, amount: "20.00", signature: sig.to_hex)
|
|
460
|
-
raw = Rail0::Signing.sign_transaction(p2[:unsigned_transaction], MERCHANT_PRIVATE_KEY
|
|
496
|
+
raw = Rail0::Signing.sign_transaction(p2[:unsigned_transaction], MERCHANT_PRIVATE_KEY,
|
|
497
|
+
payment: payment, operation: "refund", amount: "20.00")
|
|
461
498
|
client.payments.refund(rail0_id, { signed_transaction: raw })
|
|
462
499
|
```
|
|
463
500
|
|
|
@@ -470,12 +507,15 @@ pattern. Like the rest of `/payments` they need a session, but the payer's
|
|
|
470
507
|
account-less SIWE login is enough:
|
|
471
508
|
|
|
472
509
|
```ruby
|
|
510
|
+
payment = client.payments.get(rail0_id)
|
|
473
511
|
prep = client.payments.dispute_prepare(rail0_id, reason: "not_received") # reason optional
|
|
474
|
-
raw = Rail0::Signing.sign_transaction(prep[:unsigned_transaction], BUYER_PRIVATE_KEY
|
|
512
|
+
raw = Rail0::Signing.sign_transaction(prep[:unsigned_transaction], BUYER_PRIVATE_KEY,
|
|
513
|
+
payment: payment, operation: "dispute")
|
|
475
514
|
client.payments.dispute(rail0_id, { signed_transaction: raw })
|
|
476
515
|
# … later …
|
|
477
516
|
prep = client.payments.close_dispute_prepare(rail0_id, reason: "withdrawn") # or omit reason:
|
|
478
|
-
raw = Rail0::Signing.sign_transaction(prep[:unsigned_transaction], BUYER_PRIVATE_KEY
|
|
517
|
+
raw = Rail0::Signing.sign_transaction(prep[:unsigned_transaction], BUYER_PRIVATE_KEY,
|
|
518
|
+
payment: payment, operation: "close_dispute")
|
|
479
519
|
client.payments.close_dispute(rail0_id, { signed_transaction: raw })
|
|
480
520
|
```
|
|
481
521
|
|
|
@@ -756,7 +796,8 @@ Requires the `eth` gem. No private key ever leaves your process.
|
|
|
756
796
|
| Method | Use |
|
|
757
797
|
|--------|-----|
|
|
758
798
|
| `sign_payload(signer, signing_payload)` | Sign the EIP-3009 payload from a create/refund response — the recommended path, and the only one that follows the gateway across contract versions |
|
|
759
|
-
| `sign_transaction(unsigned_transaction, key)` |
|
|
799
|
+
| `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**) |
|
|
800
|
+
| `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 |
|
|
760
801
|
| `sign_transfer_with_authorization(signer, domain, params)` | Raw EIP-3009 `TransferWithAuthorization` signer, for talking to a token directly |
|
|
761
802
|
|
|
762
803
|
```ruby
|
data/lib/rail0/error_hints.rb
CHANGED
|
@@ -20,7 +20,7 @@ module Rail0
|
|
|
20
20
|
"nothing_to_dispute" => "a dispute needs a merchant-held (refundable) balance",
|
|
21
21
|
"unknown_dispute_reason" => "reason must be a dispute-reason code (Rail0::DisputeReasons.codes(:open) / codes(:close)) or exactly its bytes32 — full_refund is system-only",
|
|
22
22
|
"transaction_not_overwritable" => "a transaction for this operation is already in flight — wait for it to settle",
|
|
23
|
-
"signer_mismatch" => "the
|
|
23
|
+
"signer_mismatch" => "the signature recovers to a different wallet than the one that must sign — the payment's payer/payee, or at POST /claim the wallet the onboarding request names",
|
|
24
24
|
# The SIWE BINDING failures, split out of signer_mismatch so a failed login says
|
|
25
25
|
# WHICH part of the proof did not bind (#216). None names the server's own
|
|
26
26
|
# expectation: that endpoint is unauthenticated, so echoing the allow-list or the
|
|
@@ -56,9 +56,16 @@ module Rail0
|
|
|
56
56
|
"payer_must_be_caller" => "the payer must be the signed-in wallet — sign in as the buyer to create its payment",
|
|
57
57
|
"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
58
|
"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",
|
|
59
|
+
# A session is bound to the one wallet it signed in with: the account's other wallets
|
|
60
|
+
# are readable, but not this session's to change (operators are exempt).
|
|
61
|
+
"not_your_wallet" => "a session can change only the wallet it signed in with — sign in with that wallet to update it, deactivate it or manage its tokens",
|
|
62
|
+
# POST /accounts/:id/wallets is suspended for every session until the merchant
|
|
63
|
+
# wallet-management section lands.
|
|
64
|
+
"wallet_add_unavailable" => "adding a wallet to an account is not available yet — it returns with the merchant wallet-management section",
|
|
59
65
|
# Login resolves the account FROM a wallet, so an account left with no active one
|
|
60
|
-
# could neither act nor add a wallet back.
|
|
61
|
-
|
|
66
|
+
# could neither act nor add a wallet back. Adding a wallet is suspended and a session
|
|
67
|
+
# changes only its own wallet, so the other wallet has to be reactivated by an operator.
|
|
68
|
+
"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)",
|
|
62
69
|
"last_admin" => "the change would leave no active operator — grant the operator role to another active account first",
|
|
63
70
|
"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",
|
|
64
71
|
"refund_expired" => "the refund window has closed (refundExpiry passed) — refund/dispute is no longer possible",
|
|
@@ -72,6 +79,21 @@ module Rail0
|
|
|
72
79
|
"unknown_token" => "the token isn't configured on this chain",
|
|
73
80
|
"no_active_contract" => "no active RAIL0 contract on that chain",
|
|
74
81
|
"missing_param" => "a required parameter is missing from the request",
|
|
82
|
+
# Onboarding: POST /signup and POST /claim (the gateway's onboarding flow).
|
|
83
|
+
# The /admin/onboardings codes (onboarding_not_found, onboarding_completed) are left
|
|
84
|
+
# out: those routes are the console's, not the SDK's. The link codes never say which
|
|
85
|
+
# term failed — a stale link has one remedy: the newest email, or a new link.
|
|
86
|
+
"signup_unavailable" => "self-service sign-up is switched off on this gateway for now — try again later, or ask your RAIL0 contact for an invite",
|
|
87
|
+
"onboarding_unavailable" => "this gateway has no onboarding secret configured, so it can neither issue nor verify sign-up links — ask the operator",
|
|
88
|
+
"email_provider_not_configured" => "the gateway has no email provider that delivers, so no link was sent and nothing was saved — ask the operator to configure one",
|
|
89
|
+
"email_domain_not_allowed" => "this environment emails only its allowed domains (the detail lists them) — use an address on one of them; nothing was saved",
|
|
90
|
+
"account_email_taken" => "an account with this email already exists — sign in with its wallet, or use another email",
|
|
91
|
+
"account_wallet_taken" => "this wallet is already registered to an account — sign in with it, or use another wallet",
|
|
92
|
+
"account_name_taken" => "an account with this name already exists — choose another name",
|
|
93
|
+
"terms_required" => "the claim needs terms_version: send the version of the terms the merchant accepted",
|
|
94
|
+
"onboarding_link_invalid" => "the link was altered or its request no longer exists — use the link in the most recent email, or ask for a new one",
|
|
95
|
+
"onboarding_link_expired" => "the link has expired — sign up again, or ask your RAIL0 contact for a new link",
|
|
96
|
+
"onboarding_link_superseded" => "a newer link was sent for this request and this one no longer works — use the link in the most recent email",
|
|
75
97
|
# A BARE forbidden is not a party mismatch: the gateway split those into codes of
|
|
76
98
|
# their own (not_the_payee, not_the_payer, wallet_deactivated, account_deactivated,
|
|
77
99
|
# not_your_account) because they need different fixes. This entry kept describing
|
|
@@ -11,6 +11,12 @@ module Rail0
|
|
|
11
11
|
# with its token holdings nested under +tokens+ (a wallet with no matching
|
|
12
12
|
# tokens is still returned with an empty list). +id_or_address+ accepts either
|
|
13
13
|
# the wallet UUID or its 0x address (unique per account).
|
|
14
|
+
#
|
|
15
|
+
# A session is bound to the wallet it signed in with. Reads (list, get, balances)
|
|
16
|
+
# are account-wide, but update, delete and the token-holding methods act only on
|
|
17
|
+
# the session's own wallet: another wallet of the same account raises ApiError 403
|
|
18
|
+
# +not_your_wallet+ (an operator session is exempt). +create+ is suspended for
|
|
19
|
+
# every session (403 +wallet_add_unavailable+).
|
|
14
20
|
class Wallets
|
|
15
21
|
include Query
|
|
16
22
|
|
|
@@ -51,6 +57,10 @@ module Rail0
|
|
|
51
57
|
|
|
52
58
|
# Add a wallet to the account.
|
|
53
59
|
#
|
|
60
|
+
# CURRENTLY UNAVAILABLE: the gateway answers every session ApiError 403
|
|
61
|
+
# +wallet_add_unavailable+ until the merchant wallet-management section lands.
|
|
62
|
+
# The method and its proof shape are kept because that section reuses them.
|
|
63
|
+
#
|
|
54
64
|
# Requires a SIWE PROOF-OF-OWNERSHIP of the address being added, not merely
|
|
55
65
|
# the session JWT: the gateway verifies that +signature+ recovers to
|
|
56
66
|
# +address+ (422 otherwise), consumes the nonce carried in +message+, and
|
|
@@ -77,9 +87,11 @@ module Rail0
|
|
|
77
87
|
http.post("/accounts/#{segment(account_id)}/wallets", body)
|
|
78
88
|
end
|
|
79
89
|
|
|
80
|
-
# Update a wallet's label and/or active status.
|
|
81
|
-
#
|
|
82
|
-
#
|
|
90
|
+
# Update a wallet's label and/or active status. Only the wallet the session signed
|
|
91
|
+
# in with: another wallet of the account raises ApiError 403 +not_your_wallet+ (an
|
|
92
|
+
# operator may change any). +active: false+ on the account's only active wallet
|
|
93
|
+
# raises ApiError 422 +last_active_wallet+: login resolves the account from a
|
|
94
|
+
# wallet, so an account with none could neither act nor add one back.
|
|
83
95
|
# @param account_id [String] Account UUID.
|
|
84
96
|
# @param id_or_address [String] Wallet UUID or 0x address.
|
|
85
97
|
# @param label [String, nil] New label.
|
|
@@ -93,7 +105,8 @@ module Rail0
|
|
|
93
105
|
end
|
|
94
106
|
|
|
95
107
|
# Soft-delete (deactivate) a wallet — the same as +update(active: false)+, so the
|
|
96
|
-
# account's only active wallet raises ApiError 422 +last_active_wallet
|
|
108
|
+
# account's only active wallet raises ApiError 422 +last_active_wallet+, and another
|
|
109
|
+
# wallet than the session's own 403 +not_your_wallet+. Returns HTTP 204.
|
|
97
110
|
# @param account_id [String] Account UUID.
|
|
98
111
|
# @param id_or_address [String] Wallet UUID or 0x address.
|
|
99
112
|
# @return [nil]
|
|
@@ -119,6 +132,9 @@ module Rail0
|
|
|
119
132
|
# Which tokens a wallet accepts, on which chains. This is what GET
|
|
120
133
|
# /payment_methods then exposes to buyers, so a merchant onboarding flow that
|
|
121
134
|
# can create a wallet but not configure its tokens cannot finish. (#12)
|
|
135
|
+
#
|
|
136
|
+
# Owner only, and only on the wallet the session signed in with: another wallet
|
|
137
|
+
# of the same account raises ApiError 403 +not_your_wallet+.
|
|
122
138
|
|
|
123
139
|
# Accept a token (on a chain) on this wallet.
|
|
124
140
|
#
|
data/lib/rail0/signing.rb
CHANGED
|
@@ -13,6 +13,8 @@ ensure
|
|
|
13
13
|
$VERBOSE = original_verbose
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
+
require_relative "tx_verify"
|
|
17
|
+
|
|
16
18
|
module Rail0
|
|
17
19
|
# EIP-712 and EIP-3009 signing utilities for RAIL0 payments.
|
|
18
20
|
#
|
|
@@ -233,15 +235,47 @@ module Rail0
|
|
|
233
235
|
# (chain id, nonce, to, value, data, gas, fees) and the client assembles and
|
|
234
236
|
# signs the transaction locally.
|
|
235
237
|
#
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
#
|
|
238
|
+
# With +payment:+ and +operation:+ the transaction is VERIFIED before the key
|
|
239
|
+
# touches it ({Rail0::TxVerify.verify}): zero value, the payment's chain and RAIL0
|
|
240
|
+
# contract, the key's own address as +from+, the RAIL0 function for +operation+,
|
|
241
|
+
# this payment's id in the calldata and — for capture and refund — the +amount:+ you
|
|
242
|
+
# asked for, scaled by the payment's +decimals+. A mismatch raises
|
|
243
|
+
# {Rail0::TxVerify::RefusedSignature} naming the field, and nothing is signed.
|
|
244
|
+
# Everything compared is already in hand, so this adds no gateway call.
|
|
245
|
+
#
|
|
246
|
+
# payment = client.payments.get(rail0_id) # chain_id, rail0_contract, rail0_id, decimals
|
|
247
|
+
# tx = client.payments.capture_prepare(rail0_id, "50.00")
|
|
248
|
+
# raw = Rail0::Signing.sign_transaction(tx[:unsigned_transaction], PAYEE_PRIVATE_KEY,
|
|
249
|
+
# payment: payment, operation: "capture", amount: "50.00")
|
|
250
|
+
# client.payments.capture(rail0_id, { signed_transaction: raw })
|
|
251
|
+
#
|
|
252
|
+
# Without +payment:+ the transaction is signed BLINDLY — a compromised or
|
|
253
|
+
# misconfigured endpoint could have this key sign an arbitrary transaction. That
|
|
254
|
+
# form is DEPRECATED (a +:deprecated+ warning is emitted; enable them with
|
|
255
|
+
# +Warning[:deprecated] = true+ or +-W:deprecated+) and will be removed in the next
|
|
256
|
+
# major version.
|
|
239
257
|
#
|
|
240
258
|
# @param unsigned_transaction [String, Hash] The +unsigned_transaction+ JSON
|
|
241
259
|
# string from a prepare response (a pre-parsed Hash is also accepted).
|
|
242
|
-
# @param private_key [String] Signer's private key (0x-prefixed or raw hex).
|
|
260
|
+
# @param private_key [String, Eth::Key] Signer's private key (0x-prefixed or raw hex).
|
|
261
|
+
# @param payment [Hash, nil] The payment record the transaction is for.
|
|
262
|
+
# @param operation [String, Symbol, nil] The operation the prepare was called for.
|
|
263
|
+
# @param amount [String, nil] The human decimal passed to the prepare (capture/refund).
|
|
243
264
|
# @return [String] 0x-prefixed RLP-encoded signed transaction.
|
|
244
|
-
def self.sign_transaction(unsigned_transaction, private_key)
|
|
265
|
+
def self.sign_transaction(unsigned_transaction, private_key, payment: nil, operation: nil, amount: nil)
|
|
266
|
+
key = private_key.is_a?(Eth::Key) ? private_key : Eth::Key.new(priv: private_key.delete_prefix("0x"))
|
|
267
|
+
|
|
268
|
+
if payment || operation
|
|
269
|
+
# Before the transaction is even assembled: one that fails the check must
|
|
270
|
+
# leave no signature behind.
|
|
271
|
+
TxVerify.verify(unsigned_transaction, payment: payment, operation: operation,
|
|
272
|
+
amount: amount, signer: key.address.to_s)
|
|
273
|
+
else
|
|
274
|
+
warn("Rail0::Signing.sign_transaction without payment:/operation: signs the gateway's " \
|
|
275
|
+
"transaction blindly and is deprecated; pass the payment record and the operation " \
|
|
276
|
+
"so it is verified first", uplevel: 1, category: :deprecated)
|
|
277
|
+
end
|
|
278
|
+
|
|
245
279
|
f = unsigned_transaction.is_a?(String) ? JSON.parse(unsigned_transaction) : unsigned_transaction
|
|
246
280
|
f = f.transform_keys(&:to_s)
|
|
247
281
|
|
|
@@ -261,7 +295,7 @@ module Rail0
|
|
|
261
295
|
# digest signature), so an arbitrary #sign(digest) object cannot serve here.
|
|
262
296
|
# An Eth::Key is accepted so a caller holding one doesn't have to export it
|
|
263
297
|
# back to a String.
|
|
264
|
-
tx.sign(
|
|
298
|
+
tx.sign(key)
|
|
265
299
|
"0x#{tx.hex}"
|
|
266
300
|
end
|
|
267
301
|
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Rail0
|
|
6
|
+
# Pre-sign verification of a gateway-built unsigned transaction.
|
|
7
|
+
#
|
|
8
|
+
# +Rail0::Signing.sign_transaction+ used to sign whatever +unsigned_transaction+ a
|
|
9
|
+
# prepare step returned. A hostile or merely misconfigured gateway endpoint could
|
|
10
|
+
# therefore have the merchant's own key sign an arbitrary transaction — an
|
|
11
|
+
# +approve(spender, MAX)+ on the token, a capture answered to a void prepare, a
|
|
12
|
+
# capture for a larger amount than asked. The checks here run on data the caller
|
|
13
|
+
# already holds (the payment record, the operation it asked for, the amount it typed)
|
|
14
|
+
# BEFORE the key touches the transaction, so they add no gateway round-trip. A
|
|
15
|
+
# mismatch raises {RefusedSignature}, whose message starts with "refusing to sign:"
|
|
16
|
+
# and names the field; no signature is produced.
|
|
17
|
+
#
|
|
18
|
+
# Ported from the rail0 CLI's cmd/tx_verify.go (#63), which is the reference for the
|
|
19
|
+
# check order and the fail-closed rules. Loaded by +rail0/signing+ (it needs the
|
|
20
|
+
# +eth+ gem for keccak256), never by +require "rail0"+ alone.
|
|
21
|
+
module TxVerify
|
|
22
|
+
# Raised instead of signing. Deliberately not an ArgumentError: the caller's
|
|
23
|
+
# arguments are fine, it is the GATEWAY's transaction that is not.
|
|
24
|
+
class RefusedSignature < StandardError; end
|
|
25
|
+
|
|
26
|
+
# RAIL0.sol's +Payment+ struct as the ABI encodes it: payer, payee, token, amount
|
|
27
|
+
# (uint120), authorizationExpiry and refundExpiry (uint48). All static types, so the
|
|
28
|
+
# tuple is encoded INLINE — six 32-byte words right after the paymentId — which is
|
|
29
|
+
# what fixes the position of the amount argument below.
|
|
30
|
+
PAYMENT_TUPLE = "(address,address,address,uint120,uint48,uint48)"
|
|
31
|
+
private_constant :PAYMENT_TUPLE
|
|
32
|
+
|
|
33
|
+
# The eight RAIL0 entry points, keyed by the gateway's wire name for the operation.
|
|
34
|
+
# Copied from RAIL0.sol; the spec pins the derived selectors against the contract's
|
|
35
|
+
# compiled +methodIdentifiers+, so a typo here cannot agree with itself.
|
|
36
|
+
FUNCTION_SIGNATURES = {
|
|
37
|
+
"authorize" => "authorize(bytes32,#{PAYMENT_TUPLE},uint8,bytes32,bytes32)",
|
|
38
|
+
"charge" => "charge(bytes32,#{PAYMENT_TUPLE},uint8,bytes32,bytes32)",
|
|
39
|
+
"capture" => "capture(bytes32,#{PAYMENT_TUPLE},uint256)",
|
|
40
|
+
"void" => "void(bytes32,#{PAYMENT_TUPLE})",
|
|
41
|
+
"release" => "release(bytes32,#{PAYMENT_TUPLE})",
|
|
42
|
+
"refund" => "refund(bytes32,#{PAYMENT_TUPLE},uint256,uint8,bytes32,bytes32)",
|
|
43
|
+
"dispute" => "dispute(bytes32,#{PAYMENT_TUPLE},bytes32)",
|
|
44
|
+
"close_dispute" => "closeDispute(bytes32,#{PAYMENT_TUPLE},bytes32)"
|
|
45
|
+
}.freeze
|
|
46
|
+
|
|
47
|
+
# Operation → the function's 4-byte selector as 8 lowercase hex chars: the first
|
|
48
|
+
# four bytes of keccak256(signature), computed from the table above rather than
|
|
49
|
+
# hand-copied.
|
|
50
|
+
SELECTORS = FUNCTION_SIGNATURES.transform_values do |sig|
|
|
51
|
+
Eth::Util.keccak256(sig)[0, 4].unpack1("H*")
|
|
52
|
+
end.freeze
|
|
53
|
+
|
|
54
|
+
# Calldata position (in 32-byte words after the selector) of the +uint256 amount+
|
|
55
|
+
# argument of capture and refund: word 0 is the paymentId, words 1-6 the inline
|
|
56
|
+
# Payment tuple, so the amount is word 7. Only these two operations take an amount.
|
|
57
|
+
AMOUNT_WORD = { "capture" => 7, "refund" => 7 }.freeze
|
|
58
|
+
private_constant :AMOUNT_WORD
|
|
59
|
+
|
|
60
|
+
SELECTOR_HEX = 8 # 4-byte selector
|
|
61
|
+
WORD_HEX = 64 # 32-byte ABI word
|
|
62
|
+
private_constant :SELECTOR_HEX, :WORD_HEX
|
|
63
|
+
|
|
64
|
+
# Folds the vocabularies an operation arrives in onto the gateway's wire name:
|
|
65
|
+
# "closeDispute", "close dispute", :close_dispute are one operation.
|
|
66
|
+
def self.canonical_operation(operation)
|
|
67
|
+
operation.to_s.strip
|
|
68
|
+
.gsub(/([a-z0-9])([A-Z])/, '\1_\2')
|
|
69
|
+
.gsub(/[\s-]+/, "_")
|
|
70
|
+
.downcase
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Verifies a gateway-built +unsigned_transaction+ against what the caller asked for,
|
|
74
|
+
# BEFORE any key touches it. Raises {RefusedSignature} naming the first field that
|
|
75
|
+
# disagrees; returns the parsed, string-keyed transaction otherwise.
|
|
76
|
+
#
|
|
77
|
+
# In order:
|
|
78
|
+
# 1. the JSON parses;
|
|
79
|
+
# 2. +value+ is zero — every RAIL0 operation is value-0, funds move as token
|
|
80
|
+
# transfers the contract performs, so a non-zero value is the gateway asking for ETH;
|
|
81
|
+
# 3. +chain_id+ is the payment's chain;
|
|
82
|
+
# 4. +from+, when present, is +signer+ (the signing key's address);
|
|
83
|
+
# 5. +to+ is the payment's +rail0_contract+ — a consistency check (the address comes
|
|
84
|
+
# from the same gateway), but it catches a response stitched from two payments;
|
|
85
|
+
# 6. the 4-byte selector is the RAIL0 function for +operation+, and for capture and
|
|
86
|
+
# refund the amount word equals +amount+ scaled by the payment's +decimals+;
|
|
87
|
+
# 7. the paymentId word is the payment's +rail0_id+.
|
|
88
|
+
#
|
|
89
|
+
# Every right-hand side is something the caller supplied or derived locally — never
|
|
90
|
+
# the prepare response itself. FAIL-CLOSED on what cannot be compared: an unknown
|
|
91
|
+
# operation, a capture/refund without an +amount+, or a payment with unknown
|
|
92
|
+
# +decimals+ refuses rather than skips. Only the paymentId comparison is skipped,
|
|
93
|
+
# when the payment carries no usable +rail0_id+.
|
|
94
|
+
#
|
|
95
|
+
# @param unsigned_transaction [String, Hash] the prepare response's field-set
|
|
96
|
+
# @param payment [Hash] the payment record (from +payments.create+ / +payments.get+)
|
|
97
|
+
# @param operation [String, Symbol] the operation the prepare was called for
|
|
98
|
+
# @param amount [String, nil] the human decimal passed to the prepare (capture/refund)
|
|
99
|
+
# @param signer [String, nil] the address of the key about to sign, or nil to skip
|
|
100
|
+
# @return [Hash] the transaction, string-keyed
|
|
101
|
+
def self.verify(unsigned_transaction, payment:, operation:, amount: nil, signer: nil)
|
|
102
|
+
tx = parse(unsigned_transaction)
|
|
103
|
+
pay = payment_fields(payment)
|
|
104
|
+
check_header(tx, pay, signer)
|
|
105
|
+
|
|
106
|
+
data = tx["data"].to_s
|
|
107
|
+
# The operation before the payment id, so a swapped operation is named as such.
|
|
108
|
+
check_call(data, operation, expected_base_units(pay, operation, amount))
|
|
109
|
+
check_payment_id(data, pay[:rail0_id])
|
|
110
|
+
tx
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Checks 2-5: the fields outside the calldata.
|
|
114
|
+
def self.check_header(fields, pay, signer)
|
|
115
|
+
unless zero_native_value?(fields["value"])
|
|
116
|
+
refuse("transaction attaches native currency (value=#{fields['value'].inspect}); " \
|
|
117
|
+
"every RAIL0 operation is value-0")
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
if pay[:chain_id] && !pay[:chain_id].to_i.zero? && fields["chain_id"].to_s != pay[:chain_id].to_s
|
|
121
|
+
refuse("transaction targets chain #{fields['chain_id']} but this payment is on chain #{pay[:chain_id]}")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
from = fields["from"].to_s
|
|
125
|
+
if signer && !from.empty? && !same_address?(from, signer)
|
|
126
|
+
refuse("transaction is prepared from #{from} but the signing key is #{signer}")
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
to = fields["to"].to_s
|
|
130
|
+
return unless pay[:rail0_contract] && !to.empty? && !same_address?(to, pay[:rail0_contract])
|
|
131
|
+
|
|
132
|
+
refuse("transaction is addressed to #{to} but this payment's RAIL0 contract is #{pay[:rail0_contract]}")
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.parse(unsigned_transaction)
|
|
136
|
+
tx = unsigned_transaction.is_a?(String) ? JSON.parse(unsigned_transaction) : unsigned_transaction
|
|
137
|
+
refuse("the unsigned transaction is not an object") unless tx.is_a?(Hash)
|
|
138
|
+
tx.transform_keys(&:to_s)
|
|
139
|
+
rescue JSON::ParserError => e
|
|
140
|
+
refuse("the gateway's unsigned transaction is not valid JSON: #{e.message}")
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.payment_fields(payment)
|
|
144
|
+
refuse("cannot check the transaction without the payment it is for") unless payment.is_a?(Hash)
|
|
145
|
+
p = payment.transform_keys { |k| k.respond_to?(:to_sym) ? k.to_sym : k }
|
|
146
|
+
{ chain_id: p[:chain_id], rail0_id: p[:rail0_id], rail0_contract: presence(p[:rail0_contract]),
|
|
147
|
+
decimals: p[:decimals] }
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def self.presence(value)
|
|
151
|
+
s = value.to_s
|
|
152
|
+
s.empty? ? nil : s
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
private_class_method :check_header, :parse, :payment_fields, :presence
|
|
156
|
+
|
|
157
|
+
def self.refuse(message)
|
|
158
|
+
raise RefusedSignature, "refusing to sign: #{message}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# The encodings a zero native value legitimately arrives in — absent, nil, the
|
|
162
|
+
# number 0, or the string "0" / "0x0". A whitelist, deliberately: anything
|
|
163
|
+
# unrecognised must refuse the signature rather than be guessed at.
|
|
164
|
+
def self.zero_native_value?(value)
|
|
165
|
+
case value
|
|
166
|
+
when nil then true
|
|
167
|
+
when Integer then value.zero?
|
|
168
|
+
when String
|
|
169
|
+
s = value.strip
|
|
170
|
+
s.empty? || s == "0" || s.casecmp("0x0").zero?
|
|
171
|
+
else false
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def self.same_address?(left, right)
|
|
176
|
+
left.to_s.casecmp(right.to_s).zero?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
private_class_method :refuse, :zero_native_value?, :same_address?
|
|
180
|
+
|
|
181
|
+
# Scales the human amount the CALLER typed by the payment's token decimals, for
|
|
182
|
+
# comparison against the calldata. nil when the operation takes no amount. For one
|
|
183
|
+
# that does, a missing amount or unknown decimals is a refusal — the whole guard
|
|
184
|
+
# exists for that comparison, so it must not be skipped.
|
|
185
|
+
def self.expected_base_units(pay, operation, amount)
|
|
186
|
+
key = canonical_operation(operation)
|
|
187
|
+
return nil unless AMOUNT_WORD.key?(key)
|
|
188
|
+
|
|
189
|
+
refuse("cannot check the #{key} amount without the amount you asked for") if amount.to_s.strip.empty?
|
|
190
|
+
if pay[:decimals].nil?
|
|
191
|
+
refuse("the payment's token decimals are unknown, so the #{key} amount cannot be checked " \
|
|
192
|
+
"(fetch the payment with payments.get first)")
|
|
193
|
+
end
|
|
194
|
+
to_base_units(amount.to_s, Integer(pay[:decimals]))
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# String math, no Float: "1.5" at 6 decimals is "1500000". More fractional digits
|
|
198
|
+
# than the token has is a refusal, not a truncation — the gateway 422s such an
|
|
199
|
+
# amount, so dropping digits would compare against a figure never asked for.
|
|
200
|
+
def self.to_base_units(human, decimals)
|
|
201
|
+
s = human.strip
|
|
202
|
+
refuse("amount #{human.inspect} is not a decimal number") unless s.match?(/\A\d+(\.\d+)?\z/)
|
|
203
|
+
whole, frac = s.split(".", 2)
|
|
204
|
+
frac ||= ""
|
|
205
|
+
if frac.length > decimals
|
|
206
|
+
refuse("amount #{human.inspect} has #{frac.length} decimal places, but this token has #{decimals}")
|
|
207
|
+
end
|
|
208
|
+
(whole + frac.ljust(decimals, "0")).sub(/\A0+(?=\d)/, "")
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
private_class_method :expected_base_units, :to_base_units
|
|
212
|
+
|
|
213
|
+
# Asserts the call is the RAIL0 function for +operation+ and, for capture and refund,
|
|
214
|
+
# that it moves exactly +want_units+ base units.
|
|
215
|
+
#
|
|
216
|
+
# Checking the paymentId alone binds the transaction to the right PAYMENT but not to
|
|
217
|
+
# the right OPERATION: every RAIL0 function takes the paymentId first, so a hostile
|
|
218
|
+
# endpoint could answer a void prepare with capture calldata (settling the escrow
|
|
219
|
+
# to the merchant instead of returning it). The selector closes that; the amount
|
|
220
|
+
# word closes the remaining capture/refund swap — the right function for a
|
|
221
|
+
# different quantity than the caller typed. Fail-closed: no known selector, or an
|
|
222
|
+
# amount-taking operation with nothing local to compare, refuses rather than skips.
|
|
223
|
+
def self.check_call(data, operation, want_units)
|
|
224
|
+
key = canonical_operation(operation)
|
|
225
|
+
want = SELECTORS[key] || refuse("no known RAIL0 function for operation #{operation.to_s.inspect}")
|
|
226
|
+
raw = strip_hex(data)
|
|
227
|
+
refuse("calldata is #{raw.length} hex chars, too short to carry a function selector") if raw.length < SELECTOR_HEX
|
|
228
|
+
got = raw[0, SELECTOR_HEX]
|
|
229
|
+
if got != want
|
|
230
|
+
refuse("the call is #{describe_selector(got)} but you asked to #{key} (#{FUNCTION_SIGNATURES[key]})")
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
word = AMOUNT_WORD[key]
|
|
234
|
+
return if word.nil?
|
|
235
|
+
|
|
236
|
+
refuse("cannot check the #{key} amount without the amount you asked for") if want_units.nil?
|
|
237
|
+
start = SELECTOR_HEX + (word * WORD_HEX)
|
|
238
|
+
if raw.length < start + WORD_HEX
|
|
239
|
+
refuse("calldata is #{raw.length} hex chars, too short to carry the #{key} amount")
|
|
240
|
+
end
|
|
241
|
+
got_int = raw[start, WORD_HEX].to_i(16)
|
|
242
|
+
want_int = Integer(want_units, 10)
|
|
243
|
+
return if got_int == want_int
|
|
244
|
+
|
|
245
|
+
refuse("the call would #{key} #{got_int} base units but you asked for #{want_int}")
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Names a selector for an error message: the RAIL0 function it belongs to when it
|
|
249
|
+
# is one of ours (the likely shape of a swapped operation), and the bare hex otherwise.
|
|
250
|
+
def self.describe_selector(selector)
|
|
251
|
+
op = SELECTORS.key(selector)
|
|
252
|
+
op ? "#{FUNCTION_SIGNATURES[op].split('(', 2).first}()" : "selector 0x#{selector}"
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
# Asserts the call's first argument is THIS payment's id.
|
|
256
|
+
#
|
|
257
|
+
# All eight RAIL0 operations take +bytes32 paymentId+ first, so under ABI encoding
|
|
258
|
+
# the id always occupies bytes 4..36 of the calldata. A hostile gateway's most
|
|
259
|
+
# profitable swap is +approve(spender, MAX)+ on the token contract, which this
|
|
260
|
+
# rejects: approve's first argument is a left-padded address, so it cannot be a
|
|
261
|
+
# rail0_id. Skipped only when there is no usable rail0_id to compare against.
|
|
262
|
+
def self.check_payment_id(data, rail0_id)
|
|
263
|
+
want = strip_hex(rail0_id.to_s)
|
|
264
|
+
return if want.length != WORD_HEX
|
|
265
|
+
|
|
266
|
+
raw = strip_hex(data)
|
|
267
|
+
if raw.length < SELECTOR_HEX + WORD_HEX
|
|
268
|
+
refuse("calldata is #{raw.length} hex chars, too short to carry a paymentId")
|
|
269
|
+
end
|
|
270
|
+
got = raw[SELECTOR_HEX, WORD_HEX]
|
|
271
|
+
refuse("the call acts on payment 0x#{got} but you asked for 0x#{want}") if got != want
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def self.strip_hex(hex)
|
|
275
|
+
hex.to_s.strip.downcase.delete_prefix("0x")
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
private_class_method :check_call, :describe_selector, :check_payment_id, :strip_hex
|
|
279
|
+
end
|
|
280
|
+
end
|
data/lib/rail0/types.rb
CHANGED
|
@@ -106,7 +106,7 @@ module Rail0
|
|
|
106
106
|
)
|
|
107
107
|
|
|
108
108
|
AdminRpcHealthEndpoint = Struct.new(
|
|
109
|
-
:url, # String —
|
|
109
|
+
:url, # String — The endpoint, as configured.
|
|
110
110
|
:ok, # Boolean
|
|
111
111
|
:status, # String
|
|
112
112
|
:error, # String — Short `Class: first line`; never an upstream body.
|
|
@@ -348,6 +348,86 @@ module Rail0
|
|
|
348
348
|
keyword_init: true
|
|
349
349
|
)
|
|
350
350
|
|
|
351
|
+
# The whole account record, as an operator reads it (Full entity: GET /accounts, and GET/PATCH
|
|
352
|
+
# /accounts/{account_id} for an admin session): every column plus `admin` and the derived `status`.
|
|
353
|
+
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).
|
|
363
|
+
keyword_init: true
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
# An onboarding request (Full entity): every column plus the derived `status` and the `creator`.
|
|
367
|
+
# Never a token or a link: the token is not stored.
|
|
368
|
+
Onboarding = Struct.new(
|
|
369
|
+
:id, # String — Also the id of the account the claim creates.
|
|
370
|
+
:origin, # String
|
|
371
|
+
:name, # String
|
|
372
|
+
:email, # String — Lowercase.
|
|
373
|
+
:address, # String — Lowercase 0x + 40 hex.
|
|
374
|
+
:created_by, # The operator account of an invite; null for a sign-up.
|
|
375
|
+
:link_version, # Integer — The version of the last link minted; a link for any lower one answers onboarding_link_superseded.
|
|
376
|
+
:last_sent_at, # String — When the last send was queued.
|
|
377
|
+
:send_count, # Integer — Sends queued in all (0 for a request migrated from a pending account, which has no link in the current format yet).
|
|
378
|
+
:last_delivered_at, # When the provider accepted the last send.
|
|
379
|
+
:last_send_error, # A short fixed code for why the last send failed (provider_rejected, timeout, email_domain_not_allowed, …); null when it was delivered or is in flight.
|
|
380
|
+
:expires_at, # String — When the last link, and the open request, expire.
|
|
381
|
+
:completed_at, # When the claim created the account.
|
|
382
|
+
:terms_version, # The terms version accepted at the claim.
|
|
383
|
+
:created_at, # String
|
|
384
|
+
:updated_at, # String
|
|
385
|
+
:status, # String
|
|
386
|
+
:creator, # The operator who created an invite; null for a sign-up.
|
|
387
|
+
keyword_init: true
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
# A clash of a request with an account (the value already belongs to it) or with another request not
|
|
391
|
+
# yet completed.
|
|
392
|
+
OnboardingConflict = Struct.new(
|
|
393
|
+
:field, # String
|
|
394
|
+
:with, # String
|
|
395
|
+
:id, # String — The account's or the other request's id.
|
|
396
|
+
keyword_init: true
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# The mail queued for the request — never its link, which exists only in the mail.
|
|
400
|
+
QueuedEmail = Struct.new(
|
|
401
|
+
:kind, # String
|
|
402
|
+
:to, # String
|
|
403
|
+
:queued, # Boolean
|
|
404
|
+
keyword_init: true
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# An operator's create, reuse or correction.
|
|
408
|
+
OnboardingOutcome = Struct.new(
|
|
409
|
+
:onboarding, # Onboarding
|
|
410
|
+
:conflicts, # Array
|
|
411
|
+
:email, # QueuedEmail
|
|
412
|
+
keyword_init: true
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
# A resend or reminder.
|
|
416
|
+
OnboardingSent = Struct.new(
|
|
417
|
+
:onboarding, # Onboarding
|
|
418
|
+
:email, # QueuedEmail
|
|
419
|
+
keyword_init: true
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
# The mail as it would have been sent, rendered by the gateway's template with `<claim link
|
|
423
|
+
# generated after the request is created>` in place of the link.
|
|
424
|
+
EmailPreview = Struct.new(
|
|
425
|
+
:to, # String
|
|
426
|
+
:subject, # String
|
|
427
|
+
:text, # String
|
|
428
|
+
keyword_init: true
|
|
429
|
+
)
|
|
430
|
+
|
|
351
431
|
# An operator grant with the people named.
|
|
352
432
|
AdministratorFull = Struct.new(
|
|
353
433
|
:id, # String
|
|
@@ -379,9 +459,10 @@ module Rail0
|
|
|
379
459
|
|
|
380
460
|
# One reported policy value.
|
|
381
461
|
PolicySetting = Struct.new(
|
|
382
|
-
:value,
|
|
383
|
-
:env,
|
|
384
|
-
:overridden,
|
|
462
|
+
:value, # The effective value (number, boolean, string, list or map).
|
|
463
|
+
:env, # String — The environment variable that sets it; null for a constant.
|
|
464
|
+
: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.
|
|
385
466
|
keyword_init: true
|
|
386
467
|
)
|
|
387
468
|
|
|
@@ -396,6 +477,8 @@ module Rail0
|
|
|
396
477
|
:siwe, # Hash
|
|
397
478
|
:fees, # Hash
|
|
398
479
|
:rpc, # Hash
|
|
480
|
+
:onboarding, # Hash
|
|
481
|
+
:email, # Hash
|
|
399
482
|
:generated_at, # String
|
|
400
483
|
keyword_init: true
|
|
401
484
|
)
|
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.
|
|
8
|
+
VERSION = "1.6.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.
|
|
4
|
+
version: 1.6.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-
|
|
11
|
+
date: 2026-10-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|
|
@@ -59,6 +59,7 @@ files:
|
|
|
59
59
|
- lib/rail0/resources/webhooks.rb
|
|
60
60
|
- lib/rail0/signing.rb
|
|
61
61
|
- lib/rail0/stablecoins.rb
|
|
62
|
+
- lib/rail0/tx_verify.rb
|
|
62
63
|
- lib/rail0/types.rb
|
|
63
64
|
- lib/rail0/version.rb
|
|
64
65
|
- lib/rail0/webhook_signature.rb
|