rail0-sdk 1.8.0 → 1.9.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.
@@ -74,7 +74,12 @@ module Rail0
74
74
  # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
75
75
  # @param page [Integer, nil] Page number (1-based; 1..1,000,000, 400 outside).
76
76
  # @param per_page [Integer, nil] Items per page (max 100).
77
- # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total: } }. Each row
77
+ # @param after [String, nil] Cursor paging: the previous page's `meta[:next_cursor]`.
78
+ # Walks newest first at a constant cost per page, with no total (`total` reads 0);
79
+ # `page:` and any `sort:` other than "-created_at" beside it are 422 `invalid_cursor`.
80
+ # Omit it on the first page (pass `per_page:` alone) and follow `next_cursor` until nil.
81
+ # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total:, total_pages:, links:,
82
+ # next_cursor: } }. Each row
78
83
  # carries, besides the payment fields, `chain_id`, `decimals` (Integer, or nil when
79
84
  # the gateway can't resolve the token — fall back to {Tokens#list} then) to render the
80
85
  # base-unit amounts without a token lookup, and `in_flight` (Boolean: a transaction is
@@ -84,7 +89,7 @@ module Rail0
84
89
  chain_id: nil, disputed: nil, operation: nil, min_amount: nil, max_amount: nil,
85
90
  created_from: nil, created_to: nil, scope: nil, account_id: nil, has_error: nil,
86
91
  expiring_within: nil, reference: nil, reference_origin: nil, sort: nil, page: nil,
87
- per_page: nil)
92
+ per_page: nil, after: nil)
88
93
  query = build_query(status: status, mode: mode, payer: payer, payee: payee, token: token,
89
94
  rail0_id: rail0_id, reference: reference, reference_origin: reference_origin,
90
95
  chain_id: chain_id, disputed: disputed,
@@ -93,7 +98,7 @@ module Rail0
93
98
  created_from: created_from, created_to: created_to,
94
99
  scope: scope, account_id: account_id, has_error: has_error,
95
100
  expiring_within: expiring_within,
96
- sort: sort, page: page, per_page: per_page)
101
+ sort: sort, page: page, per_page: per_page, after: after)
97
102
  http.get_list("/payments#{query}")
98
103
  end
99
104
 
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "query"
4
+
5
+ module Rail0
6
+ module Resources
7
+ # Payout destinations: where a merchant wallet sends its balance — a deposit address at
8
+ # an exchange, Circle Mint, an off-ramp provider or the merchant's own custody (JWT,
9
+ # merchant session).
10
+ #
11
+ # A destination is registered +unverified+. It becomes +verified+ after a small PROOF
12
+ # transfer ({Payouts#prepare} with +purpose: "proof"+) has confirmed on-chain AND the
13
+ # merchant has checked the venue credited it ({#confirm}). Until then it can receive the
14
+ # proof amount at most. See {Payouts} for the transfer itself.
15
+ #
16
+ # Amounts on a destination (+reserve_amount+, +min_amount+) are BASE-UNIT strings (digits
17
+ # only, below 2^120); the +*_formatted+ fields beside them carry the human decimals.
18
+ class PayoutDestinations
19
+ # The default of {update}'s keywords: "not passed", told apart from an explicit value.
20
+ UNSET = Object.new.freeze
21
+ private_constant :UNSET
22
+
23
+ include Query
24
+
25
+ # The text of the gateway's +details_v1+ attestation, which +confirm_details: true+
26
+ # stands for on {create} and on an {update} that changes +min_amount+. Show it to the
27
+ # merchant and send +confirm_details: true+ only once they have accepted it.
28
+ CONFIRM_DETAILS_TEXT =
29
+ "I have checked the address, network, token and minimum deposit on the venue's deposit " \
30
+ "page and confirm they are correct. Funds sent to a wrong address or network, or below " \
31
+ "the venue's minimum, may be permanently lost and cannot be recovered by rail0."
32
+
33
+ # The venue kinds a destination can be registered as.
34
+ KINDS = %w[exchange circle_mint self_custody provider].freeze
35
+
36
+ # A destination's statuses — the values {list}'s +status+ filter accepts.
37
+ STATUSES = %w[unverified verified disabled].freeze
38
+
39
+ attr_reader :http
40
+
41
+ def initialize(http)
42
+ @http = http
43
+ freeze
44
+ end
45
+
46
+ # List the account's payout destinations (every wallet of the account).
47
+ # @param wallet_id [String, nil] Only this wallet's destinations (uuid).
48
+ # @param status [String, nil] One of {STATUSES}.
49
+ # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
50
+ # @param page [Integer, nil] Page number (1-based).
51
+ # @param per_page [Integer, nil] Items per page (max 100).
52
+ # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total:, total_pages:, links: } }
53
+ def list(wallet_id: nil, status: nil, sort: nil, page: nil, per_page: nil)
54
+ query = build_query(wallet_id: wallet_id, status: status, sort: sort, page: page, per_page: per_page)
55
+ http.get_list("/payout_destinations#{query}")
56
+ end
57
+
58
+ # Register a payout destination for the session's own wallet. Answers 201 with the
59
+ # destination, +unverified+.
60
+ #
61
+ # +confirm_details+ is REQUIRED and has no default on purpose: +true+ is the merchant's
62
+ # attestation of {CONFIRM_DETAILS_TEXT} (the gateway's +details_v1+) —
63
+ #
64
+ # "I have checked the address, network, token and minimum deposit on the venue's
65
+ # deposit page and confirm they are correct. Funds sent to a wrong address or network,
66
+ # or below the venue's minimum, may be permanently lost and cannot be recovered by
67
+ # rail0."
68
+ #
69
+ # — recorded with who and when in the +destination.created+ event. Anything but +true+ is
70
+ # refused 422 +confirmation_required+ and nothing is saved. Pass the merchant's answer,
71
+ # never a hard-coded +true+.
72
+ #
73
+ # @param wallet_id [String] The wallet (uuid or address) that will sign transfers to it;
74
+ # must be the session's own.
75
+ # @param chain_id [Integer] The chain the deposit address was issued for.
76
+ # @param token [String] The token's address on that chain.
77
+ # @param address [String] The deposit address, generated at the venue WITH THE NETWORK
78
+ # SET TO THIS CHAIN.
79
+ # @param confirm_details [Boolean] The merchant's acceptance of {CONFIRM_DETAILS_TEXT}.
80
+ # @param label [String, nil] A name for the destination.
81
+ # @param kind [String, nil] One of {KINDS}; the gateway defaults to "exchange".
82
+ # @param reserve_amount [String, nil] Base units to keep in the wallet on a full-balance
83
+ # ("all") payout; omitted, the gateway's suggestion (the refundable balance still
84
+ # inside its refund window) applies.
85
+ # @param min_amount [String, nil] The venue's deposit minimum in base units; omitted, none.
86
+ # A payout below it is refused (+payout_below_minimum+) and the proof is raised to it.
87
+ # @return [Hash] The destination.
88
+ def create(wallet_id:, chain_id:, token:, address:, confirm_details:, label: nil, kind: nil,
89
+ reserve_amount: nil, min_amount: nil)
90
+ body = { wallet_id: wallet_id, chain_id: chain_id, token: token, address: address,
91
+ confirm_details: confirm_details, label: label, kind: kind,
92
+ reserve_amount: reserve_amount, min_amount: min_amount }.compact
93
+ http.post("/payout_destinations", body)
94
+ end
95
+
96
+ # Read one destination (any wallet of the account may).
97
+ # @param id [String] Destination uuid.
98
+ # @return [Hash]
99
+ def get(id)
100
+ http.get("/payout_destinations/#{segment(id)}")
101
+ end
102
+
103
+ # Update a destination. Only the keywords you PASS are sent, and at least one is
104
+ # required (ArgumentError otherwise).
105
+ #
106
+ # - +reserve_amount+ / +min_amount+: base units; an empty string restores the suggestion
107
+ # (reserve) or removes the minimum.
108
+ # - A request that CHANGES +min_amount+ also needs +confirm_details: true+ — the same
109
+ # attestation as {create} ({CONFIRM_DETAILS_TEXT}), else 422 +confirmation_required+
110
+ # and nothing is written. Raising the minimum above the destination's latest proof
111
+ # takes its verification away: prove it again with a new proof.
112
+ # - +active: false+ disables it (any wallet of the account may); its unsigned pending
113
+ # payout is cancelled, a signed one refuses the switch-off 409 +payout_already_signed+.
114
+ # +active: true+ re-enables it with the verification it had.
115
+ #
116
+ # @param id [String] Destination uuid.
117
+ # @return [Hash] The destination.
118
+ def update(id, label: UNSET, reserve_amount: UNSET, min_amount: UNSET, active: UNSET,
119
+ confirm_details: UNSET)
120
+ body = { label: label, reserve_amount: reserve_amount, min_amount: min_amount, active: active,
121
+ confirm_details: confirm_details }.reject { |_, v| v.equal?(UNSET) }
122
+ if body.except(:confirm_details).empty?
123
+ raise ArgumentError, "at least one of label, reserve_amount, min_amount or active is required"
124
+ end
125
+
126
+ http.patch("/payout_destinations/#{segment(id)}", body)
127
+ end
128
+
129
+ # The merchant's attestation that the venue CREDITED the proof transfer: marks the
130
+ # destination +verified+. The proof payout must be +confirmed+ on-chain first (422
131
+ # +payout_destination_proof_missing+ otherwise).
132
+ # @param id [String] Destination uuid.
133
+ # @return [Hash] The destination, now verified.
134
+ def confirm(id)
135
+ http.post("/payout_destinations/#{segment(id)}/confirm")
136
+ end
137
+
138
+ # The destination's audit trail, newest first: its own events and those of every
139
+ # payout made to it (+destination.created+, +payout.prepared+, +payout.confirmed+, …).
140
+ # @param id [String] Destination uuid.
141
+ # @param page [Integer, nil] Page number (1-based).
142
+ # @param per_page [Integer, nil] Items per page (default 25, max 100).
143
+ # @return [Hash] { data: Array<Hash>, meta: { ... } }
144
+ def events(id, page: nil, per_page: nil)
145
+ http.get_list("/payout_destinations/#{segment(id)}/events#{build_query(page: page, per_page: per_page)}")
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "query"
4
+
5
+ module Rail0
6
+ module Resources
7
+ # Merchant payouts: an ERC-20 transfer of the wallet's balance to one of its
8
+ # {PayoutDestinations} (JWT, merchant session; the destination's own wallet signs).
9
+ #
10
+ # Two-phase like the payment operations. {prepare} builds the unsigned EIP-1559
11
+ # transaction — a +transfer(address,uint256)+ on the TOKEN contract — and answers the
12
+ # pending payout with it in +unsigned_transaction+. Then either:
13
+ #
14
+ # - sign it offline and {submit} the raw bytes (the gateway broadcasts them) — or do
15
+ # both with {sign_and_submit}, which VERIFIES the transaction before signing; or
16
+ # - broadcast it yourself (a wallet that signs and sends in one step) and report the
17
+ # hash with {submit_by_hash}.
18
+ #
19
+ # The payout then settles asynchronously: poll {get} until +status+ is +confirmed+ or
20
+ # +failed+, or subscribe to the +payouts.confirmed+ / +payouts.failed+ webhooks.
21
+ #
22
+ # Amounts: +prepare+'s +amount+ is a HUMAN decimal ("250.00") or "all" (the balance
23
+ # minus the destination's reserve); the payout record's +amount+ is base units, with
24
+ # +amount_formatted+ beside it.
25
+ class Payouts
26
+ include Query
27
+
28
+ # What a payout is for: "proof" (the small test transfer that verifies a
29
+ # destination) or "payout" (the default).
30
+ PURPOSES = %w[payout proof].freeze
31
+
32
+ # A payout's statuses — the values {list}'s +status+ filter accepts.
33
+ STATUSES = %w[pending submitted confirmed failed].freeze
34
+
35
+ attr_reader :http
36
+
37
+ def initialize(http)
38
+ @http = http
39
+ freeze
40
+ end
41
+
42
+ # List the account's payouts, newest first by default.
43
+ # @param destination_id [String, nil] Only payouts to this destination (uuid).
44
+ # @param wallet_id [String, nil] Only this wallet's payouts (uuid).
45
+ # @param status [String, Array<String>, nil] One of {STATUSES}, or an Array matching
46
+ # ANY of them (sent comma-separated).
47
+ # @param purpose [String, nil] One of {PURPOSES}.
48
+ # @param created_from [String, nil] Only payouts created at/after this ISO-8601 time.
49
+ # @param created_to [String, nil] Only payouts created at/before this ISO-8601 time.
50
+ # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
51
+ # @param page [Integer, nil] Page number (1-based).
52
+ # @param per_page [Integer, nil] Items per page (default 25, max 100).
53
+ # @param after [String, nil] Cursor paging, as on {Payments#list}: the previous page's
54
+ # `meta[:next_cursor]`; no total, and not combinable with `page:` or another sort.
55
+ # @return [Hash] { data: Array<Hash>, meta: { page:, per_page:, total:, total_pages:, links:,
56
+ # next_cursor: } }
57
+ def list(destination_id: nil, wallet_id: nil, status: nil, purpose: nil, created_from: nil,
58
+ created_to: nil, sort: nil, page: nil, per_page: nil, after: nil)
59
+ query = build_query(destination_id: destination_id, wallet_id: wallet_id, status: status,
60
+ purpose: purpose, created_from: created_from, created_to: created_to,
61
+ sort: sort, page: page, per_page: per_page, after: after)
62
+ http.get_list("/payouts#{query}")
63
+ end
64
+
65
+ # Prepare a transfer to a destination (POST /payouts/prepare): 201 with a new pending
66
+ # payout, or 200 re-preparing the destination's unsigned pending one (one pending
67
+ # payout per destination). Its +unsigned_transaction+ is what to sign.
68
+ #
69
+ # - +purpose: "proof"+ sends the gateway's proof amount, raised to the destination's
70
+ # +min_amount+ when that is higher; +amount+ is ignored. An unverified destination
71
+ # may receive the proof at most.
72
+ # - +purpose: "payout"+ (the default) sends +amount+: a human decimal, or "all" for
73
+ # the balance minus the reserve. Below the destination's +min_amount+ it is refused
74
+ # 422 +payout_below_minimum+.
75
+ # - A payout that already holds a signed transaction cannot be re-prepared (409
76
+ # +payout_already_signed+): {resend} it, or let it settle.
77
+ #
78
+ # @param destination_id [String] Destination uuid.
79
+ # @param purpose [String, nil] One of {PURPOSES}; the gateway defaults to "payout".
80
+ # @param amount [String, nil] Human decimal or "all"; required for a payout.
81
+ # @return [Hash] The pending payout.
82
+ def prepare(destination_id:, purpose: nil, amount: nil)
83
+ body = { destination_id: destination_id, purpose: purpose, amount: amount }.compact
84
+ http.post("/payouts/prepare", body)
85
+ end
86
+
87
+ # Read one payout.
88
+ # @param id [String] Payout uuid.
89
+ # @return [Hash]
90
+ def get(id)
91
+ http.get("/payouts/#{segment(id)}")
92
+ end
93
+
94
+ # The payout's audit trail, newest first.
95
+ # @param id [String] Payout uuid.
96
+ # @param page [Integer, nil] Page number (1-based).
97
+ # @param per_page [Integer, nil] Items per page (default 25, max 100).
98
+ # @return [Hash] { data: Array<Hash>, meta: { ... } }
99
+ def events(id, page: nil, per_page: nil)
100
+ http.get_list("/payouts/#{segment(id)}/events#{build_query(page: page, per_page: per_page)}")
101
+ end
102
+
103
+ # Submit the signed transfer; the gateway stores it, then broadcasts it (HTTP 202,
104
+ # now +submitted+). A node refusal answers 502 and leaves the payout +pending+ with
105
+ # its signature: {resend} it. Prefer {sign_and_submit}, which verifies before signing.
106
+ # @param id [String] Payout uuid.
107
+ # @param params [Hash] { signed_transaction: "0x…" }.
108
+ # @return [Hash] The payout.
109
+ def submit(id, params)
110
+ http.post("/payouts/#{segment(id)}/submit", params)
111
+ end
112
+
113
+ # Verify, sign and submit a prepared payout in one call.
114
+ #
115
+ # {Rail0::Signing.sign_payout} first holds the +unsigned_transaction+ to the payout —
116
+ # +to+ the token, value 0, the payout's chain, calldata exactly
117
+ # +transfer(destination address, payout amount)+, nothing trailing — and, when given,
118
+ # to the registered +destination:+ and to the +amount:+ you asked prepare for (scaled
119
+ # by +decimals:+). Anything else raises {Rail0::TxVerify::RefusedSignature} and
120
+ # nothing is signed or sent. Then {submit}.
121
+ #
122
+ # destination = client.payout_destinations.get(destination_id)
123
+ # payout = client.payouts.prepare(destination_id: destination_id, amount: "250.00")
124
+ # client.payouts.sign_and_submit(payout, MERCHANT_PRIVATE_KEY, destination: destination,
125
+ # amount: "250.00", decimals: 6)
126
+ #
127
+ # Needs the optional +eth+ gem (loaded here, not by +require "rail0"+).
128
+ #
129
+ # @param payout [Hash] The prepared payout (from {prepare} or {get}).
130
+ # @param private_key [String, Eth::Key] The destination wallet's key.
131
+ # @param destination [Hash, nil] The destination the payout must be for.
132
+ # @param amount [String, nil] The human decimal passed to prepare (not "all").
133
+ # @param decimals [Integer, nil] The token's decimals, required with +amount:+.
134
+ # @return [Hash] The payout, now +submitted+.
135
+ def sign_and_submit(payout, private_key, destination: nil, amount: nil, decimals: nil)
136
+ require_relative "../signing"
137
+
138
+ raw = Rail0::Signing.sign_payout(payout, private_key, destination: destination,
139
+ amount: amount, decimals: decimals)
140
+ submit(payout[:id] || payout["id"], { signed_transaction: raw })
141
+ end
142
+
143
+ # Re-broadcast the signed transaction a {submit} stored (no body): the same bytes, so
144
+ # the same nonce and hash. For a submit whose broadcast was refused or went
145
+ # unanswered (422 +payout_not_signed+ when there is nothing to resend).
146
+ # @param id [String] Payout uuid.
147
+ # @return [Hash] The payout.
148
+ def resend(id)
149
+ http.post("/payouts/#{segment(id)}/resend")
150
+ end
151
+
152
+ # Cancel an UNSIGNED pending payout: it becomes +failed+ with +error_code+ "cancelled"
153
+ # and the destination is free for a new prepare. A signed one is refused 409
154
+ # +payout_already_signed+ — its transaction may still be mined.
155
+ # @param id [String] Payout uuid.
156
+ # @return [Hash] The payout.
157
+ def cancel(id)
158
+ http.post("/payouts/#{segment(id)}/cancel")
159
+ end
160
+
161
+ # Report a transfer the wallet broadcast itself, by hash (POST /payouts/{id}/submitted,
162
+ # HTTP 202). The hash must be THIS transfer — from the wallet, on the token, this amount
163
+ # to this destination — or 422 +foreign_transfer_hash+. Also accepts a LATE hash on a
164
+ # payout that failed +submission_timeout+ or +dropped+.
165
+ # @param id [String] Payout uuid.
166
+ # @param params [Hash] { transaction_hash: "0x…" }.
167
+ # @return [Hash] The payout, now +submitted+.
168
+ def submit_by_hash(id, params)
169
+ http.post("/payouts/#{segment(id)}/submitted", params)
170
+ end
171
+ end
172
+ end
173
+ end
@@ -11,7 +11,7 @@ module Rail0
11
11
  # segment the same way ({#segment}).
12
12
  module Query
13
13
  # Segments that would not stay put once interpolated: "." and ".." are dot
14
- # segments that a URL normaliser (a proxy, a CDN, the server's router) may
14
+ # segments that a URL normalizer (a proxy, a CDN, the server's router) may
15
15
  # resolve away, and an empty one collapses the path onto its parent.
16
16
  UNSAFE_SEGMENTS = ["", ".", ".."].freeze
17
17
  private_constant :UNSAFE_SEGMENTS
@@ -16,6 +16,10 @@ module Rail0
16
16
  include Query
17
17
 
18
18
  # Event topics a subscription can carry. It may carry any non-empty subset.
19
+ # The payout topics deliver the same envelope ({id, emitted_at, topic}) with the
20
+ # subject under its own key: +payout+ for payouts.confirmed / payouts.failed (a
21
+ # failure's reason is its +error_code+), +payout_destination+ for
22
+ # payout_destinations.verified.
19
23
  TOPICS = %w[
20
24
  payments.created
21
25
  payments.signed
@@ -30,6 +34,9 @@ module Rail0
30
34
  payments.failed
31
35
  payments.disputed
32
36
  payments.dispute_closed
37
+ payouts.confirmed
38
+ payouts.failed
39
+ payout_destinations.verified
33
40
  ].freeze
34
41
 
35
42
  attr_reader :http
data/lib/rail0/signing.rb CHANGED
@@ -15,6 +15,7 @@ end
15
15
 
16
16
  require_relative "tx_verify"
17
17
  require_relative "payload_verify"
18
+ require_relative "payout_verify"
18
19
 
19
20
  module Rail0
20
21
  # EIP-712 and EIP-3009 signing utilities for RAIL0 payments.
@@ -277,6 +278,49 @@ module Rail0
277
278
  "so it is verified first", uplevel: 1, category: :deprecated)
278
279
  end
279
280
 
281
+ assemble_and_sign(unsigned_transaction, key)
282
+ end
283
+
284
+ # Verify, then sign, a prepared payout's +unsigned_transaction+ — the ERC-20
285
+ # +transfer(address,uint256)+ that moves the merchant's balance to a payout
286
+ # destination — and return the signed raw transaction for {Resources::Payouts#submit}.
287
+ #
288
+ # Before the key touches it, {Rail0::PayoutVerify.verify} holds the transaction to the
289
+ # payout: value 0, the payout's chain, +to+ = the payout's token, the key's own address
290
+ # as +from+, and calldata EXACTLY +transfer(payout.address, payout.amount)+ with nothing
291
+ # trailing. With +destination:+ the payout must also be for that registered destination
292
+ # (chain, token, address); with +amount:+ (the human decimal you asked prepare for) and
293
+ # +decimals:+ its amount must be exactly that. A mismatch raises
294
+ # {Rail0::TxVerify::RefusedSignature} naming the field, and nothing is signed. There is
295
+ # no blind form.
296
+ #
297
+ # destination = client.payout_destinations.get(destination_id)
298
+ # payout = client.payouts.prepare(destination_id: destination_id, amount: "250.00")
299
+ # raw = Rail0::Signing.sign_payout(payout, MERCHANT_PRIVATE_KEY, destination: destination,
300
+ # amount: "250.00", decimals: 6)
301
+ # client.payouts.submit(payout[:id], signed_transaction: raw)
302
+ #
303
+ # {Resources::Payouts#sign_and_submit} does the last two steps in one call.
304
+ #
305
+ # @param payout [Hash] the prepared payout (from +payouts.prepare+ or +payouts.get+)
306
+ # @param private_key [String, Eth::Key] the destination wallet's key
307
+ # @param destination [Hash, nil] the registered destination the payout must be for
308
+ # @param amount [String, nil] the human decimal passed to prepare (not "all")
309
+ # @param decimals [Integer, nil] the token's decimals, required with +amount:+
310
+ # @return [String] 0x-prefixed RLP-encoded signed transaction
311
+ def self.sign_payout(payout, private_key, destination: nil, amount: nil, decimals: nil)
312
+ key = private_key.is_a?(Eth::Key) ? private_key : Eth::Key.new(priv: private_key.delete_prefix("0x"))
313
+ unsigned = payout.is_a?(Hash) ? (payout[:unsigned_transaction] || payout["unsigned_transaction"]) : nil
314
+ tx = PayoutVerify.verify(unsigned, payout: payout, destination: destination, amount: amount,
315
+ decimals: decimals, signer: key.address.to_s)
316
+ assemble_and_sign(tx, key)
317
+ end
318
+
319
+ # Assembles the EIP-1559 transaction from the gateway's field-set and signs it with
320
+ # +key+ (an Eth::Key). Shared by {sign_transaction} and {sign_payout}, AFTER their
321
+ # checks: the field-set has already been verified, or deliberately not (the deprecated
322
+ # blind form).
323
+ def self.assemble_and_sign(unsigned_transaction, key)
280
324
  f = unsigned_transaction.is_a?(String) ? JSON.parse(unsigned_transaction) : unsigned_transaction
281
325
  f = f.transform_keys(&:to_s)
282
326
 
@@ -309,6 +353,8 @@ module Rail0
309
353
  "0x#{tx.hex}"
310
354
  end
311
355
 
356
+ private_class_method :assemble_and_sign
357
+
312
358
  # Verify, then sign, the EIP-3009 +signing_payload+ of a create, get or refund
313
359
  # prepare response. The checked form — prefer it over the bare {sign_payload}.
314
360
  #
@@ -185,7 +185,8 @@ module Rail0
185
185
 
186
186
  # The encodings a zero native value legitimately arrives in — absent, nil, the
187
187
  # number 0, or the string "0" / "0x0". A whitelist, deliberately: anything
188
- # unrecognised must refuse the signature rather than be guessed at.
188
+ # unrecognised must refuse the signature rather than be guessed at. Public because
189
+ # {Rail0::PayoutVerify} holds a payout transfer to the same rule.
189
190
  def self.zero_native_value?(value)
190
191
  case value
191
192
  when nil then true
@@ -201,7 +202,7 @@ module Rail0
201
202
  left.to_s.casecmp(right.to_s).zero?
202
203
  end
203
204
 
204
- private_class_method :refuse, :zero_native_value?, :same_address?
205
+ private_class_method :refuse, :same_address?
205
206
 
206
207
  # Scales the human amount the CALLER typed by the payment's token decimals, for
207
208
  # comparison against the calldata. nil when the operation takes no amount. For one