rail0-sdk 1.7.0 → 1.7.1
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 +13 -4
- data/lib/rail0/payload_verify.rb +77 -2
- data/lib/rail0/types.rb +5 -5
- data/lib/rail0/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5d88d3da009d4c04853e8fb794eba655913763a1391c6e0bd6915bda6cb68d36
|
|
4
|
+
data.tar.gz: 5cc37022ce71f950c6b11dcf421902518b63caebfd54a13f78eaf909a7eef39c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2512e0a842d7d76749dc4028a0c1d2590b7fe1dd4e95676572a39aa8c41fa9a882c2e4a534e85080f0fe8089c8001c6f2101ff4b9fb7ab9f4e2cc9d7e6d35d62
|
|
7
|
+
data.tar.gz: 7b569639525815722a586f308d45ebc67c964cbd4f6f1ae4c5bac4a8c5a731589b03dffebcfcdb375e3a265f9f1143b52162d5e8685de591e39a364de3257cf8
|
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
|
|
154
|
-
`
|
|
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
|
|
@@ -827,7 +835,7 @@ Requires the `eth` gem. No private key ever leaves your process.
|
|
|
827
835
|
|--------|-----|
|
|
828
836
|
| `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
837
|
| `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 |
|
|
838
|
+
| `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
839
|
| `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
840
|
| `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
841
|
| `sign_transfer_with_authorization(signer, domain, params)` | Raw EIP-3009 `TransferWithAuthorization` signer, for talking to a token directly |
|
|
@@ -846,7 +854,8 @@ client.payments.sign(payment[:rail0_id], { signature: sig.to_hex })
|
|
|
846
854
|
**The gateway builds the payload; you sign it verbatim.** `sign_signing_payload` takes the
|
|
847
855
|
typehash from the payload's `primaryType` and every field from its `message` — only
|
|
848
856
|
the gateway knows which contract version a payment lives on, and the version selects
|
|
849
|
-
the typehash
|
|
857
|
+
the typehash and the EIP-712 domain (the field layout is EIP-3009's own, the same on
|
|
858
|
+
every version, which is why the check pins it). An unrecognised `primaryType`
|
|
850
859
|
raises rather than falling back, because a guessed typehash yields a *valid*
|
|
851
860
|
signature over the wrong digest, which fails only on-chain, after gas.
|
|
852
861
|
|
data/lib/rail0/payload_verify.rb
CHANGED
|
@@ -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
|
|
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)
|
data/lib/rail0/types.rb
CHANGED
|
@@ -457,17 +457,17 @@ module Rail0
|
|
|
457
457
|
keyword_init: true
|
|
458
458
|
)
|
|
459
459
|
|
|
460
|
-
# One reported
|
|
461
|
-
|
|
460
|
+
# One reported configuration value.
|
|
461
|
+
ConfigSetting = Struct.new(
|
|
462
462
|
:value, # The effective value (number, boolean, string, list or map).
|
|
463
463
|
:env, # String — The environment variable that sets it; null for a constant.
|
|
464
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/
|
|
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/config_descriptions.yml). Every entry has one; older gateways omit the field.
|
|
466
466
|
keyword_init: true
|
|
467
467
|
)
|
|
468
468
|
|
|
469
|
-
# GET /admin/
|
|
470
|
-
|
|
469
|
+
# GET /admin/config: the effective configuration, grouped. Every leaf is a ConfigSetting.
|
|
470
|
+
ConfigReport = Struct.new(
|
|
471
471
|
:payments, # Hash
|
|
472
472
|
:recovery, # Hash
|
|
473
473
|
:retention, # Hash
|
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.
|
|
8
|
+
VERSION = "1.7.1"
|
|
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.
|
|
4
|
+
version: 1.7.1
|
|
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-20 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|