rail0-sdk 1.9.0 → 1.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7e9c4a0434db81266c43b414c1f3f2f4b6f2466ef7aa9f1f4f1e1406542fabdf
4
- data.tar.gz: a01332eba373c295821bd20edaa16076f4ef20b9fdb204064a0c9c378a5b8f1b
3
+ metadata.gz: db1685625a4eb7aebc1cbf222a83b595a28fa04aacf1bf0f4ce05cdf3a59129d
4
+ data.tar.gz: 27c405e53fdd5a59825294b4def8b99aabb6d782b8feacf88db559cd0a078d0b
5
5
  SHA512:
6
- metadata.gz: abc0ca11b0940752160e3503087fd89d62c1e8ba04b5c6412cdef41751b598b25acd988fb52e4b04aa5a2c2e64c6fafdf0a72625eceb98238e0e2abd6a955e55
7
- data.tar.gz: 9267bc67421f3aa0867c5c9b24a959679d1c648ea5e9598df149743a06efa76db088ea9f1b21a39a46c0f2974376ceadf783e34a65b59f1d846a5ae603c08464
6
+ metadata.gz: 4cd815e13c30acaf0acd54a12b938a91cc6bbfe5545234fdcc701b047cc58c2a0e2cf74cf1598e1c62459e9c865c6dc637443b6569175b118f5d5bbba96353f1
7
+ data.tar.gz: d3b87f6b4716dfea60c888cd6fc3ef9eb09003d2ab0557228f5a21d695a16c5ba76fef19e3cedeaa0e7573545feac63e5efaeba585f3edfd97c00cf5491c73b9
data/README.md CHANGED
@@ -652,13 +652,17 @@ client.payments.submit_by_hash(id, "capture", { transaction_hash: "0x…" })
652
652
  A payout moves the merchant wallet's balance to a **payout destination** — a deposit
653
653
  address at an exchange, Circle Mint, an off-ramp provider or your own custody. It is a
654
654
  plain ERC-20 `transfer(address,uint256)` from the wallet, signed by the destination's own
655
- wallet; rail0 never holds the key. Destination amounts (`reserve_amount`, `min_amount`)
656
- are **base-unit** strings; the `*_formatted` fields beside them carry the human decimals.
655
+ wallet; rail0 never holds the key. Destination amounts (`min_amount`) are **base-unit**
656
+ strings; the `*_formatted` fields beside them carry the human decimals.
657
657
 
658
658
  ### 1. Register a destination (with the confirmation)
659
659
 
660
660
  Generate the deposit address at the venue **with the network set to the payout's chain**,
661
- note the venue's minimum deposit, then register it. `confirm_details:` is required and has
661
+ note the venue's minimum deposit, then register it. The minimum is required, as exactly one
662
+ of `min_amount:` (the venue's figure in base units, from its deposit page) or
663
+ `no_minimum: true` (the merchant states the venue has none). Neither, a blank figure, or both
664
+ raise `ArgumentError` before anything is sent, and nothing is defaulted: a minimum nobody
665
+ looked up is how a deposit gets lost. `confirm_details:` is required and has
662
666
  no default: `true` is the merchant's acceptance of the gateway's `details_v1` text
663
667
  (`Rail0::Resources::PayoutDestinations::CONFIRM_DETAILS_TEXT`), recorded with who and when:
664
668
 
@@ -669,17 +673,24 @@ no default: `true` is the merchant's acceptance of the gateway's `details_v1` te
669
673
  Show that text and pass the merchant's answer; anything but `true` is 422
670
674
  `confirmation_required` and nothing is saved.
671
675
 
676
+ `label:` is required too: the name the merchant knows the destination by. The gateway trims
677
+ it, and it must be unique among the account's live destinations, compared without regard to
678
+ case (409 `payout_destination_label_taken`; deleting a destination frees its label). A blank
679
+ or whitespace-only label raises `ArgumentError` before anything is sent (the gateway's 422
680
+ `label_required`).
681
+
672
682
  ```ruby
673
683
  dest = client.payout_destinations.create(
674
684
  wallet_id: WALLET_ID, # the session's own wallet (uuid or address)
675
685
  chain_id: 5042002, token: USDC, address: VENUE_DEPOSIT_ADDRESS,
676
- label: "Kraken", kind: "exchange", # exchange | circle_mint | self_custody | provider
677
- min_amount: "5000000", # the venue's minimum, base units (5 USDC); omit for none
686
+ label: "Kraken", # required, unique per account (case-insensitive)
687
+ min_amount: "5000000", # the venue's minimum, base units (5 USDC) — or no_minimum: true
678
688
  confirm_details: merchant_accepted # true only once the merchant accepted the text above
679
689
  )
680
690
  dest[:status] # => "unverified"
681
691
 
682
692
  client.payout_destinations.list(status: "verified") # paged like payments
693
+ client.payout_destinations.list(include_deleted: true) # deleted ones too (deleted_at set)
683
694
  client.payout_destinations.get(dest[:id])
684
695
  client.payout_destinations.events(dest[:id]) # audit trail, newest first
685
696
  ```
@@ -699,7 +710,7 @@ client.payout_destinations.confirm(dest[:id]) # => status "verified
699
710
 
700
711
  ### 3. Pay out: prepare → verify → sign → submit
701
712
 
702
- `amount` is a **human decimal** or `"all"` (the balance minus the reserve, below). A payout
713
+ `amount` is a **human decimal** or `"all"` (the balance minus what is kept back, below). A payout
703
714
  below the destination's `min_amount` is refused 422 `payout_below_minimum`.
704
715
 
705
716
  ```ruby
@@ -744,25 +755,65 @@ cancelled (409 `payout_already_signed`): `resend` it and let it settle.
744
755
  A runnable end-to-end flow (register → proof → confirm → payout → poll) is in
745
756
  [`examples/06_payouts.rb`](examples/06_payouts.rb).
746
757
 
747
- ### Minimum and reserve
758
+ ### Minimum, reserves, disable and delete
748
759
 
749
760
  - **Minimum** (`min_amount`): the venue's minimum deposit. Below it the venue may never
750
- credit a transfer, so the gateway refuses it. Changing it needs the same confirmation, and
751
- raising it above the latest proof takes the verification away (prove it again):
761
+ credit a transfer, so the gateway refuses it. It is required and explicit: a figure
762
+ (`"0"` included), or `no_minimum: true` when the venue has none — the destination then
763
+ reads `no_minimum: true` with `min_amount` nil (`no_minimum: false` with a nil `min_amount`
764
+ is a destination registered before the minimum was asked: never stated). A blank figure,
765
+ or a figure beside `no_minimum: true`, raises `ArgumentError` (the gateway's
766
+ `minimum_required` / `minimum_conflict`). Changing either needs the same confirmation, and
767
+ raising the minimum above the latest proof takes the verification away (prove it again):
752
768
 
753
769
  ```ruby
754
770
  client.payout_destinations.update(dest[:id], min_amount: "10000000", confirm_details: merchant_accepted)
755
- client.payout_destinations.update(dest[:id], min_amount: "", confirm_details: merchant_accepted) # none
771
+ client.payout_destinations.update(dest[:id], no_minimum: true, confirm_details: merchant_accepted) # none
772
+ ```
773
+
774
+ - **Reserves**: what a payout keeps back in the wallet is decided at payout time, not stored
775
+ on the destination. The **refund reserve** is the refundable balance of the wallet's
776
+ payments in this token still inside their refund window (a refund is pulled from the payee
777
+ wallet, so an emptied wallet fails it). The **gas reserve**, only on a chain whose gas is
778
+ paid in this very token (Arc's USDC, `native_gas`), is the gas for those refunds and for
779
+ capturing the open authorizations plus a base of new captures; `"0"` elsewhere. On such a
780
+ chain the payout's own fee (`gas_allowance`) is always kept back too. `reserves` shows the
781
+ figures right now, with `available`: what `"all"` moves with both reserves kept:
782
+
783
+ ```ruby
784
+ r = client.payout_destinations.reserves(dest[:id])
785
+ r[:refund_reserve_formatted] # => "40.00" (r[:refundable_payments] payments)
786
+ r[:gas_reserve_formatted] # => "0.12"
787
+ r[:available_formatted] # => "960.00" what amount: "all" moves
788
+ ```
789
+
790
+ Both reserves are kept by default. A payout can opt out of either (a refund or capture after
791
+ it may then fail for lack of funds); the payout records what it kept back in
792
+ `refund_reserve` / `gas_reserve` (base units, nil for a proof):
793
+
794
+ ```ruby
795
+ client.payouts.prepare(destination_id: dest[:id], amount: "all", keep_refund_reserve: false)
796
+ client.payouts.prepare(destination_id: dest[:id], amount: "all", keep_gas_reserve: false)
797
+ ```
798
+
799
+ - **Rename**: `label:` renames it, under the same rules as on create: never blank
800
+ (`ArgumentError` here, 422 `label_required` at the gateway) and unique per account,
801
+ case-insensitive (409 `payout_destination_label_taken`):
802
+
803
+ ```ruby
804
+ client.payout_destinations.update(dest[:id], label: "Kraken main")
756
805
  ```
757
806
 
758
- - **Reserve** (`reserve_amount`): what an `"all"` payout leaves in the wallet so refunds can
759
- still be paid. Unset, the gateway's suggestion applies (`suggested_reserve`: the refundable
760
- balance of payments still inside their refund window).
807
+ - **Disable and delete**: `active: false` disables a destination (`true` re-enables it with
808
+ the verification it had). `delete` removes it from the lists and frees its payment method
809
+ (one destination per token and chain, so another address can be registered there) and its
810
+ label; the row is kept (`get` and `events` still read it, with `deleted_at` set
811
+ and `status` `"disabled"`). Either settles a pending payout first: an unsigned one is
812
+ cancelled, a signed one refuses with 409 `payout_already_signed`.
761
813
 
762
814
  ```ruby
763
- client.payout_destinations.update(dest[:id], reserve_amount: "100000000")
764
- client.payout_destinations.update(dest[:id], reserve_amount: "") # back to the suggestion
765
- client.payout_destinations.update(dest[:id], active: false) # disable (true re-enables)
815
+ client.payout_destinations.update(dest[:id], active: false) # disable (true re-enables)
816
+ client.payout_destinations.delete(dest[:id]) # soft delete, own wallet only
766
817
  ```
767
818
 
768
819
  ## Webhooks (JWT)
@@ -1220,7 +1271,7 @@ lib/rail0/
1220
1271
  accounts.rb the caller's own account profile, read and update (JWT)
1221
1272
  webhooks.rb webhook subscription management (JWT)
1222
1273
  analytics.rb account-scoped payment analytics (JWT)
1223
- payout_destinations.rb payout destinations: register, confirm, update (JWT)
1274
+ payout_destinations.rb payout destinations: register, confirm, update, delete, reserves (JWT)
1224
1275
  payouts.rb merchant payouts: prepare, sign, submit (JWT)
1225
1276
  query.rb shared query-string and path-segment helpers
1226
1277
  ```
@@ -114,23 +114,27 @@ module Rail0
114
114
  "onboarding_link_expired" => "the link has expired — sign up again, or ask your RAIL0 contact for a new link",
115
115
  "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",
116
116
  # Payouts (gateway#455): the merchant's transfer of its balance to a payout destination.
117
- "confirmation_required" => "registering a payout destination, or changing its min_amount, needs confirm_details: true — the merchant's confirmation that they checked the address, network, token and minimum deposit at the venue (PayoutDestinations::CONFIRM_DETAILS_TEXT)",
118
- "payout_destination_exists" => "this wallet already has a destination with this address for this token and chain — use it (payout_destinations.list)",
117
+ "confirmation_required" => "registering a payout destination, or changing its min_amount or no_minimum, needs confirm_details: true — the merchant's confirmation that they checked the address, network, token and minimum deposit at the venue (PayoutDestinations::CONFIRM_DETAILS_TEXT)",
118
+ "payout_destination_exists" => "this payment method (token and chain) already has a destination — one per method; delete it to register another address (payout_destinations.list)",
119
+ "payout_destination_label_taken" => "another destination of this account already has this label (compared without regard to case) — pick another name, or delete that destination to free it (payout_destinations.list)",
120
+ "label_required" => "a payout destination needs a label — a name, not blank or whitespace (payout_destinations.create / update)",
119
121
  "payout_destination_zero_address" => "the zero address cannot receive a payout — register the venue's deposit address",
120
122
  "payout_destination_own_wallet" => "the destination is the paying wallet itself — register the deposit address of the exchange, Circle Mint account or another wallet you control",
121
123
  "payout_destination_is_contract" => "the address is a RAIL0 contract or a token contract on this chain, where funds cannot be recovered — register the venue's deposit address",
122
124
  "payout_destination_disabled" => "the destination is disabled — re-enable it (payout_destinations.update(id, active: true)) or choose another one",
123
125
  "payout_destination_unverified" => "the destination is not verified: send the proof (payouts.prepare(purpose: \"proof\")), wait for it to confirm, check the venue credited it, then payout_destinations.confirm — until then only the proof amount can be sent",
124
126
  "payout_destination_proof_missing" => "the proof transfer has not confirmed on-chain yet — prepare, sign and submit it (purpose: \"proof\"), wait for payouts.get to say confirmed, then confirm the destination",
125
- "payout_below_minimum" => "the amount is below the destination's min_amount (the venue may never credit it) — send at least the minimum; for \"all\", the balance after the reserve is below it",
126
- "insufficient_balance" => "the wallet does not hold enough of the token for this amount after the refund reserve — the detail names what is available",
127
+ "minimum_required" => "a payout destination needs its venue's minimum deposit: min_amount (base units, from the venue's deposit page), or no_minimum: true when the venue has none — a blank min_amount is not one (to remove a minimum, send no_minimum: true)",
128
+ "minimum_conflict" => "min_amount and no_minimum: true contradict each other — send the venue's minimum, or no_minimum: true when it has none, not both",
129
+ "payout_below_minimum" => "the amount is below the destination's min_amount (the venue may never credit it) — send at least the minimum; for \"all\", the balance after what is kept back (reserves, gas) is below it",
130
+ "insufficient_balance" => "the wallet does not hold enough of the token for this amount after what the payout keeps back (refund reserve, gas reserve, gas allowance) — the detail names what is available; payout_destinations.reserves(id) shows the figures",
127
131
  "payout_limit_reached" => "this wallet reached its hourly payout limit — retry later",
128
132
  "payout_already_signed" => "the payout already holds a signed transaction that may still be mined — resend it (payouts.resend) and let it settle; it cannot be re-prepared or cancelled",
129
133
  "payout_not_pending" => "the payout is already submitted or settled — prepare a new one",
130
134
  "payout_not_signed" => "the payout holds no signed transaction to resend — submit the signed transfer first",
131
135
  "payout_hash_in_use" => "another payout already records this transaction hash — one transaction settles one payout",
132
136
  "foreign_transfer_hash" => "the reported transaction does not move this payout's amount of its token to its destination — report the hash of the transfer you broadcast for this payout",
133
- "invalid_amount" => "the amount is not valid: base units (digits only, below 2^120) for reserve_amount/min_amount, a positive decimal within the token's decimals elsewhere",
137
+ "invalid_amount" => "the amount is not valid: base units (digits only, below 2^120) for min_amount, a positive decimal within the token's decimals elsewhere",
134
138
  "invalid_transaction" => "the signed transaction could not be decoded, or its sender is not allowed here — sign the prepare's unsigned_transaction unmodified with the right wallet",
135
139
  "unknown_chain" => "that chain id is not configured on this gateway — see chains.list",
136
140
  # A BARE forbidden is not a party mismatch: the gateway split those into codes of
@@ -13,8 +13,10 @@ module Rail0
13
13
  # merchant has checked the venue credited it ({#confirm}). Until then it can receive the
14
14
  # proof amount at most. See {Payouts} for the transfer itself.
15
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.
16
+ # Amounts on a destination (+min_amount+) are BASE-UNIT strings (digits only, below
17
+ # 2^120); the +*_formatted+ fields beside them carry the human decimals. What a payout
18
+ # keeps back in the wallet (the refund and gas reserves) is decided at payout time, not
19
+ # stored on the destination: see {#reserves} and {Payouts#prepare}.
18
20
  class PayoutDestinations
19
21
  # The default of {update}'s keywords: "not passed", told apart from an explicit value.
20
22
  UNSET = Object.new.freeze
@@ -23,16 +25,13 @@ module Rail0
23
25
  include Query
24
26
 
25
27
  # 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
28
+ # stands for on {create} and on an {update} that changes +min_amount+ or +no_minimum+. Show it to the
27
29
  # merchant and send +confirm_details: true+ only once they have accepted it.
28
30
  CONFIRM_DETAILS_TEXT =
29
31
  "I have checked the address, network, token and minimum deposit on the venue's deposit " \
30
32
  "page and confirm they are correct. Funds sent to a wrong address or network, or below " \
31
33
  "the venue's minimum, may be permanently lost and cannot be recovered by rail0."
32
34
 
33
- # The venue kinds a destination can be registered as.
34
- KINDS = %w[exchange circle_mint self_custody provider].freeze
35
-
36
35
  # A destination's statuses — the values {list}'s +status+ filter accepts.
37
36
  STATUSES = %w[unverified verified disabled].freeze
38
37
 
@@ -49,9 +48,12 @@ module Rail0
49
48
  # @param sort [String, nil] Comma-separated sort fields; prefix with - for desc.
50
49
  # @param page [Integer, nil] Page number (1-based).
51
50
  # @param per_page [Integer, nil] Items per page (max 100).
51
+ # @param include_deleted [Boolean, nil] +true+ lists deleted destinations too
52
+ # (+deleted_at+ set); left out by default.
52
53
  # @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)
54
+ def list(wallet_id: nil, status: nil, sort: nil, page: nil, per_page: nil, include_deleted: nil)
55
+ query = build_query(wallet_id: wallet_id, status: status, sort: sort, page: page, per_page: per_page,
56
+ include_deleted: include_deleted)
55
57
  http.get_list("/payout_destinations#{query}")
56
58
  end
57
59
 
@@ -70,26 +72,45 @@ module Rail0
70
72
  # refused 422 +confirmation_required+ and nothing is saved. Pass the merchant's answer,
71
73
  # never a hard-coded +true+.
72
74
  #
75
+ # The venue's minimum deposit is REQUIRED too, as EXACTLY ONE of: +min_amount+, the
76
+ # figure the merchant read on the venue's deposit page, or +no_minimum: true+, the
77
+ # merchant's statement that the venue has none. Neither, a blank figure, or both raise
78
+ # ArgumentError here and nothing is sent (the gateway would answer 422
79
+ # +minimum_required+ / +minimum_conflict+). Nothing is defaulted: a minimum nobody
80
+ # looked up is how a deposit gets lost, so pass what the merchant answered.
81
+ #
82
+ # +label+ is REQUIRED: the name the merchant knows the destination by. The gateway
83
+ # trims it; it must be unique among the account's live destinations, compared without
84
+ # regard to case (409 +payout_destination_label_taken+ otherwise; deleting a destination
85
+ # frees its label). A blank or whitespace-only label raises ArgumentError here and
86
+ # nothing is sent (the gateway would answer 422 +label_required+).
87
+ #
73
88
  # @param wallet_id [String] The wallet (uuid or address) that will sign transfers to it;
74
89
  # must be the session's own.
75
90
  # @param chain_id [Integer] The chain the deposit address was issued for.
76
91
  # @param token [String] The token's address on that chain.
77
92
  # @param address [String] The deposit address, generated at the venue WITH THE NETWORK
78
93
  # SET TO THIS CHAIN.
94
+ # @param label [String] The destination's name, unique per account (case-insensitive).
79
95
  # @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.
96
+ # @param min_amount [String, nil] The venue's deposit minimum in base units. A payout
97
+ # below it is refused (+payout_below_minimum+) and the proof is raised to it.
98
+ # @param no_minimum [Boolean] +true+: the merchant states the venue has no minimum.
87
99
  # @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)
100
+ # @raise [ArgumentError] when +label+ is blank, when neither or both of +min_amount+ /
101
+ # +no_minimum: true+ are given, or +min_amount+ is blank.
102
+ def create(wallet_id:, chain_id:, token:, address:, label:, confirm_details:, min_amount: nil,
103
+ no_minimum: false)
104
+ check_label!(label)
105
+ figure = !min_amount.nil? && !min_amount.to_s.strip.empty?
106
+ if figure == (no_minimum == true)
107
+ raise ArgumentError, "exactly one of min_amount (the venue's minimum, base units) or no_minimum: true " \
108
+ "is required"
109
+ end
110
+
90
111
  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
112
+ confirm_details: confirm_details, label: label, min_amount: min_amount,
113
+ no_minimum: (true if no_minimum == true) }.compact
93
114
  http.post("/payout_destinations", body)
94
115
  end
95
116
 
@@ -103,9 +124,15 @@ module Rail0
103
124
  # Update a destination. Only the keywords you PASS are sent, and at least one is
104
125
  # required (ArgumentError otherwise).
105
126
  #
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
127
+ # - +label+: renames it. Never blank (ArgumentError here, 422 +label_required+ at the
128
+ # gateway) and unique among the account's live destinations, case-insensitive (409
129
+ # +payout_destination_label_taken+).
130
+ # - +min_amount+: the venue's minimum, base units (+"0"+ is a figure like any other); it
131
+ # also clears +no_minimum+. Never blank: an empty string raises ArgumentError here.
132
+ # - +no_minimum: true+: the venue has no minimum — how a minimum is removed (+min_amount+
133
+ # becomes nil). Never together with +min_amount+ (ArgumentError here, 422
134
+ # +minimum_conflict+ at the gateway).
135
+ # - A request that CHANGES +min_amount+ or +no_minimum+ also needs +confirm_details: true+ — the same
109
136
  # attestation as {create} ({CONFIRM_DETAILS_TEXT}), else 422 +confirmation_required+
110
137
  # and nothing is written. Raising the minimum above the destination's latest proof
111
138
  # takes its verification away: prove it again with a new proof.
@@ -115,17 +142,53 @@ module Rail0
115
142
  #
116
143
  # @param id [String] Destination uuid.
117
144
  # @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,
145
+ def update(id, label: UNSET, min_amount: UNSET, no_minimum: UNSET, active: UNSET, confirm_details: UNSET)
146
+ body = { label: label, min_amount: min_amount, no_minimum: no_minimum, active: active,
121
147
  confirm_details: confirm_details }.reject { |_, v| v.equal?(UNSET) }
122
148
  if body.except(:confirm_details).empty?
123
- raise ArgumentError, "at least one of label, reserve_amount, min_amount or active is required"
149
+ raise ArgumentError, "at least one of label, min_amount, no_minimum or active is required"
124
150
  end
125
151
 
152
+ check_label!(body[:label]) if body.key?(:label)
153
+ check_minimum!(body)
154
+
126
155
  http.patch("/payout_destinations/#{segment(id)}", body)
127
156
  end
128
157
 
158
+ # Delete a destination (soft): it leaves the lists ({list} shows it again with
159
+ # +include_deleted: true+), and its payment method and its label are free to be
160
+ # registered again. The row is
161
+ # kept — its payouts and audit trail still name it, and {get} / {events} still read it
162
+ # with +deleted_at+ set and +status+ "disabled". Its pending payout is settled first, as
163
+ # on a switch-off: an unsigned one is cancelled, a signed one refuses the delete 409
164
+ # +payout_already_signed+. Only the destination's own wallet may delete it; a deleted
165
+ # destination takes no further write (404). Recorded as +destination.deleted+.
166
+ # @param id [String] Destination uuid.
167
+ # @return [Hash] The destination as it now stands.
168
+ def delete(id)
169
+ http.delete("/payout_destinations/#{segment(id)}")
170
+ end
171
+
172
+ # What a payout to the destination would keep back in the wallet right now, computed
173
+ # from the wallet's payments and the chain's current fees (nothing is stored):
174
+ #
175
+ # - +refund_reserve+: the refundable balance of the wallet's payments in this token
176
+ # still inside their refund window (+refundable_payments+ of them).
177
+ # - +gas_reserve+: on a chain whose gas IS this token (+native_gas+, Arc's USDC), the gas
178
+ # for those refunds and for capturing the open authorizations plus +base_captures+
179
+ # more; "0" elsewhere.
180
+ # - +gas_allowance+: the payout transfer's own fee, always kept back on a native-gas chain.
181
+ # - +available+: what +amount: "all"+ moves with both reserves kept (the default).
182
+ #
183
+ # Every amount is base units with its +*_formatted+ twin. Both reserves can be opted
184
+ # out of per payout ({Payouts#prepare}'s +keep_refund_reserve:+ / +keep_gas_reserve:+).
185
+ # Any wallet of the account may read it; a deleted destination still reads.
186
+ # @param id [String] Destination uuid.
187
+ # @return [Hash] The reserves (see Rail0::Types::PayoutReserves).
188
+ def reserves(id)
189
+ http.get("/payout_destinations/#{segment(id)}/reserves")
190
+ end
191
+
129
192
  # The merchant's attestation that the venue CREDITED the proof transfer: marks the
130
193
  # destination +verified+. The proof payout must be +confirmed+ on-chain first (422
131
194
  # +payout_destination_proof_missing+ otherwise).
@@ -144,6 +207,28 @@ module Rail0
144
207
  def events(id, page: nil, per_page: nil)
145
208
  http.get_list("/payout_destinations/#{segment(id)}/events#{build_query(page: page, per_page: per_page)}")
146
209
  end
210
+
211
+ private
212
+
213
+ # A destination's label is required and never blank (the gateway's +label_required+);
214
+ # refused here so nothing is sent.
215
+ def check_label!(label)
216
+ return unless label.to_s.strip.empty?
217
+
218
+ raise ArgumentError, "label is required and cannot be blank: name the destination"
219
+ end
220
+
221
+ # {update}'s local minimum checks, before anything is sent: a blank +min_amount+ is
222
+ # never a minimum (the gateway's +minimum_required+; +no_minimum: true+ removes one),
223
+ # and a figure beside +no_minimum: true+ contradicts itself (+minimum_conflict+).
224
+ def check_minimum!(body)
225
+ if body.key?(:min_amount) && body[:min_amount].to_s.strip.empty?
226
+ raise ArgumentError, "min_amount cannot be blank: pass the venue's minimum, or no_minimum: true to remove it"
227
+ end
228
+ return unless body.key?(:min_amount) && body[:no_minimum] == true
229
+
230
+ raise ArgumentError, "pass min_amount or no_minimum: true, not both"
231
+ end
147
232
  end
148
233
  end
149
234
  end
@@ -20,8 +20,9 @@ module Rail0
20
20
  # +failed+, or subscribe to the +payouts.confirmed+ / +payouts.failed+ webhooks.
21
21
  #
22
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.
23
+ # minus what the payout keeps back); the payout record's +amount+ is base units, with
24
+ # +amount_formatted+ beside it, and +refund_reserve+ / +gas_reserve+ record what it kept
25
+ # back (base units; nil for a proof).
25
26
  class Payouts
26
27
  include Query
27
28
 
@@ -70,17 +71,29 @@ module Rail0
70
71
  # +min_amount+ when that is higher; +amount+ is ignored. An unverified destination
71
72
  # may receive the proof at most.
72
73
  # - +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+.
74
+ # the balance minus what is kept back. Below the destination's +min_amount+ it is
75
+ # refused 422 +payout_below_minimum+.
76
+ # - What a payout keeps back in the wallet is decided here, at payout time: the refund
77
+ # reserve (the refundable balance of payments still inside their refund window) and,
78
+ # on a chain whose gas is this very token (Arc's USDC), the gas reserve (gas for the
79
+ # refunds and captures the wallet still owes). Both are kept by default;
80
+ # +keep_refund_reserve: false+ / +keep_gas_reserve: false+ opts this payout out of one
81
+ # (a refund or capture after it may then fail for lack of funds). Payout purpose only;
82
+ # sent only when given. {PayoutDestinations#reserves} shows the figures beforehand.
75
83
  # - A payout that already holds a signed transaction cannot be re-prepared (409
76
84
  # +payout_already_signed+): {resend} it, or let it settle.
77
85
  #
78
86
  # @param destination_id [String] Destination uuid.
79
87
  # @param purpose [String, nil] One of {PURPOSES}; the gateway defaults to "payout".
80
88
  # @param amount [String, nil] Human decimal or "all"; required for a payout.
89
+ # @param keep_refund_reserve [Boolean, nil] +false+ opts out of keeping the refund
90
+ # reserve; the gateway defaults to +true+.
91
+ # @param keep_gas_reserve [Boolean, nil] +false+ opts out of keeping the gas reserve;
92
+ # the gateway defaults to +true+.
81
93
  # @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
94
+ def prepare(destination_id:, purpose: nil, amount: nil, keep_refund_reserve: nil, keep_gas_reserve: nil)
95
+ body = { destination_id: destination_id, purpose: purpose, amount: amount,
96
+ keep_refund_reserve: keep_refund_reserve, keep_gas_reserve: keep_gas_reserve }.compact
84
97
  http.post("/payouts/prepare", body)
85
98
  end
86
99
 
data/lib/rail0/types.rb CHANGED
@@ -47,6 +47,20 @@ module Rail0
47
47
  :decimals, # Integer
48
48
  :raw, # String — Balance in base units (exact).
49
49
  :amount, # String — Human-decimal balance.
50
+ :payout, # What a payout in this token keeps back for gas — on every ERC-20 line; null on the native balance.
51
+ keyword_init: true
52
+ )
53
+
54
+ # The gas figures of a payout in this token on this chain: whether the chain's gas IS this very
55
+ # token (`native_gas`; Arc's USDC) and, then, the allowance POST /payouts/prepare keeps back from
56
+ # `all`, from a typed amount's ceiling and from the proof (`gas_allowance` = gas_limit ×
57
+ # max_fee_per_gas in base units, the charge the EVM takes before a transfer runs; `0` on any other
58
+ # chain). Null allowance when the fee could not be read. The reserves a payout keeps back are
59
+ # decided at payout time: GET /payout_destinations/{id}/reserves.
60
+ BalancePayout = Struct.new(
61
+ :native_gas, # Boolean
62
+ :gas_allowance, # String | nil
63
+ :gas_allowance_formatted, # String | nil
50
64
  keyword_init: true
51
65
  )
52
66
 
@@ -600,27 +614,51 @@ module Rail0
600
614
  )
601
615
 
602
616
  PayoutDestination = Struct.new(
603
- :id, # String
604
- :wallet_id, # String
605
- :chain_id, # Integer
606
- :token, # String — ERC-20 address on that chain, lowercased.
607
- :token_symbol, # String | nil
608
- :address, # String — The deposit address, lowercased.
609
- :label, # String | nil
610
- :kind, # String
611
- :status, # String
612
- :has_code, # Boolean | nil — Whether the address held code on this chain when registered (eth_getCode, best effort). An EOA and an undeployed contract look the same.
613
- :reserve_amount, # String | nil — Base units kept in the wallet on a full-balance payout; null = the suggestion.
614
- :reserve_amount_formatted, # String | nil — `reserve_amount` in human decimals (the token's decimals); null while the suggestion applies, or when the token left the catalog.
615
- :suggested_reserve, # String — Base units: the refundable balance of the wallet's payments in this token still inside their refund window.
616
- :suggested_reserve_formatted, # String | nil — `suggested_reserve` in human decimals; null when the token left the catalog.
617
- :min_amount, # String | nil — The venue's deposit minimum, base units; null = no minimum. A payout below it is refused (`payout_below_minimum`); the proof transfer is raised to it.
618
- :min_amount_formatted, # String | nil — `min_amount` in human decimals (the token's decimals); null when there is no minimum, or when the token left the catalog.
619
- :proof_payout_id, # String | nil
620
- :verified_at, # String | nil
621
- :disabled_at, # String | nil
622
- :created_at, # String
623
- :updated_at, # String
617
+ :id, # String
618
+ :wallet_id, # String
619
+ :chain_id, # Integer
620
+ :token, # String — ERC-20 address on that chain, lowercased.
621
+ :token_symbol, # String | nil
622
+ :address, # String — The deposit address, lowercased.
623
+ :label, # String — The merchant's name for it: never blank, unique (case-insensitive) among the account's live destinations.
624
+ :status, # String
625
+ :has_code, # Boolean | nil — Whether the address held code on this chain when registered (eth_getCode, best effort). An EOA and an undeployed contract look the same.
626
+ :min_amount, # String | nil — The venue's deposit minimum, base units; null = no minimum. A payout below it is refused (`payout_below_minimum`); the proof transfer is raised to it.
627
+ :no_minimum, # Boolean — true when the merchant stated the venue has no minimum deposit (`min_amount` is then null). false with a null `min_amount`: never stated (a destination registered before it was asked).
628
+ :min_amount_formatted, # String | nil — `min_amount` in human decimals (the token's decimals); null when there is no minimum, or when the token left the catalog.
629
+ :proof_payout_id, # String | nil
630
+ :verified_at, # String | nil
631
+ :disabled_at, # String | nil
632
+ :deleted_at, # String | nil — When the merchant deleted it (DELETE /payout_destinations/{id}); null for a live destination.
633
+ :created_at, # String
634
+ :updated_at, # String
635
+ keyword_init: true
636
+ )
637
+
638
+ # What a payout to the destination would keep back right now (GET
639
+ # /payout_destinations/{id}/reserves). Every amount is base units as a string, with its
640
+ # human-decimals twin (`*_formatted`, the token's decimals).
641
+ PayoutReserves = Struct.new(
642
+ :destination_id, # String
643
+ :chain_id, # Integer
644
+ :token, # String — ERC-20 address on that chain, lowercased.
645
+ :token_symbol, # String
646
+ :decimals, # Integer
647
+ :balance, # String — The wallet's balance of the token, read now.
648
+ :balance_formatted, # String
649
+ :refund_reserve, # String — The refundable balance of the wallet's payments in this token still inside their refund window.
650
+ :refund_reserve_formatted, # String
651
+ :refundable_payments, # Integer — How many payments `refund_reserve` covers: the refunds the wallet may still owe.
652
+ :native_gas, # Boolean — Whether the chain's gas is paid in this very token (Arc's USDC). The gas figures are `0` when it is not.
653
+ :gas_reserve, # String — Gas for `gas_refunds` refunds and for `open_authorizations` + `base_captures` captures, at their gas limits and the fee ceiling a prepare would set now; `0` off a native-gas chain. Counted over every token of the chain: on Arc a EURC payment's capture or refund is paid in USDC gas too.
654
+ :gas_reserve_formatted, # String
655
+ :gas_refunds, # Integer — The wallet's payments on this chain, in any token, still inside their refund window with something left to refund: the refunds the gas reserve pays for.
656
+ :open_authorizations, # Integer — The wallet's payments on this chain, in any token, still capturable (a capture's gas is the merchant wallet's).
657
+ :base_captures, # Integer — Captures the gas reserve pays for on top of the open authorizations, so the wallet can capture the payments it takes next.
658
+ :gas_allowance, # String — The payout transfer's own fee (gas_limit × max_fee_per_gas), always kept back on a native-gas chain; `0` elsewhere.
659
+ :gas_allowance_formatted, # String
660
+ :available, # String — balance − refund_reserve − gas_reserve − gas_allowance, floored at 0: what `amount: all` moves with both reserves kept (the default).
661
+ :available_formatted, # String
624
662
  keyword_init: true
625
663
  )
626
664
 
@@ -634,13 +672,16 @@ module Rail0
634
672
  :address, # String — The recipient.
635
673
  :amount, # String — Base units.
636
674
  :amount_formatted, # String — Human decimals.
675
+ :refund_reserve, # String | nil — Base units this payout kept back in the wallet for refunds, decided at prepare; `0` when the merchant opted out (`keep_refund_reserve: false`); null for a proof.
676
+ :gas_reserve, # String | nil — Base units this payout kept back for the gas the wallet still owes (native-gas chains only, `0` elsewhere or when opted out with `keep_gas_reserve: false`); null for a proof.
637
677
  :purpose, # String
638
- :status, # String
678
+ :status, # String — `pending` (prepared, awaiting the signature) → `submitted` (hash known; still `submitted` once mined, until its block is final) → `confirmed` | `failed`. Both outcomes are decided only once the receipt's block is at or below the chain's finality bound (its `finality_tag` block, or head − `required_confirmations` for `depth` or a node that serves no tag) and still canonical; `payouts.confirmed` / `payouts.failed` fire then.
639
679
  :unsigned_transaction, # String | nil — JSON of the EIP-1559 transaction to sign (`to` = the token, `data` = transfer(address,uint256)). Check it against the decoded fields beside it before signing.
640
680
  :signed, # Boolean — Whether the gateway stores the wallet's signed transaction (the payload itself is never exposed). A `pending` payout with `signed: true` already has its `transaction_hash` and may be in a mempool: resend it (POST /payouts/{id}/resend); it cannot be re-prepared.
641
681
  :transaction_hash, # String | nil — Keccak of the signed transaction, stored with it on submit (so a signed `pending` row has it too), or the hash the wallet reported.
642
682
  :sender, # String | nil
643
- :block_number, # Integer | nil
683
+ :block_number, # Integer | nil — The receipt's block. Set when the gateway first sees the receipt (the payout stays `submitted` until that block is final) moved to the new block if a reorg re-includes the transaction elsewhere, and cleared only while no block holds it.
684
+ :mined_at, # String | nil — When the gateway first saw the receipt (its own clock, not the block's timestamp). Non-null on a `submitted` payout: mined, waiting for finality.
644
685
  :gas_used, # String | nil
645
686
  :error_code, # String | nil — When failed: `reverted` (mined, status 0), `foreign_transfer` (mined, but the receipt has no Transfer of this amount of this token from the wallet to this destination), `dropped` (not mined, and the wallet's confirmed nonce has passed the signed transaction's), `submission_timeout` (a reported hash no node has ever known, past SUBMITTED_TX_TTL), `cancelled` (unsigned, cancelled by the merchant with POST /payouts/{id}/cancel or by disabling its destination). `submission_timeout` and `dropped` are not final: POST /payouts/{id}/submitted with a late hash puts the payout back to `submitted`.
646
687
  :error_title, # String | nil
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.9.0"
8
+ VERSION = "1.10.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.9.0
4
+ version: 1.10.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-30 00:00:00.000000000 Z
11
+ date: 2026-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: logger