rail0-sdk 1.9.0 → 1.10.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 +68 -17
- data/lib/rail0/error_hints.rb +9 -5
- data/lib/rail0/resources/payout_destinations.rb +111 -26
- data/lib/rail0/resources/payouts.rb +19 -6
- data/lib/rail0/types.rb +64 -23
- 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: ef5875508f67b6ca5ba08aa397f5afa33ad2fb1d3c53d956c1ea5c1f6f0c066c
|
|
4
|
+
data.tar.gz: 251404c40cce8af924a7b0ffded8052adb3a0fdea8c0f91345a3a9f7dceae3ba
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d3bcef6de15169f95b428d03e16336373f1e234b3c4a7e3aa987ddd72a9e34cdeddb7bb0359e0b03534c60209e89dc50e7a0014d45ff2b4e0c676b2e34df98d4
|
|
7
|
+
data.tar.gz: 8d2d98affb4443432e676d9008aa0a73486946884fc5905e2a44b99fdbad75eec123217b10d4818de8df29d2aad98e410c621552ce435697a5bb0988fcb70bd9
|
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 (`
|
|
656
|
-
|
|
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.
|
|
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",
|
|
677
|
-
min_amount: "5000000", # the venue's minimum, base units (5 USDC)
|
|
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
|
|
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
|
|
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.
|
|
751
|
-
|
|
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],
|
|
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
|
-
- **
|
|
759
|
-
|
|
760
|
-
|
|
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],
|
|
764
|
-
client.payout_destinations.
|
|
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
|
```
|
data/lib/rail0/error_hints.rb
CHANGED
|
@@ -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
|
|
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
|
-
"
|
|
126
|
-
"
|
|
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
|
|
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 (+
|
|
17
|
-
#
|
|
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
|
|
81
|
-
#
|
|
82
|
-
# @param
|
|
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
|
-
|
|
89
|
-
|
|
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,
|
|
92
|
-
|
|
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
|
-
# - +
|
|
107
|
-
#
|
|
108
|
-
#
|
|
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,
|
|
119
|
-
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
604
|
-
:wallet_id,
|
|
605
|
-
:chain_id,
|
|
606
|
-
:token,
|
|
607
|
-
:token_symbol,
|
|
608
|
-
:address,
|
|
609
|
-
:label,
|
|
610
|
-
:
|
|
611
|
-
:
|
|
612
|
-
:
|
|
613
|
-
:
|
|
614
|
-
:
|
|
615
|
-
:
|
|
616
|
-
:
|
|
617
|
-
:
|
|
618
|
-
:
|
|
619
|
-
:
|
|
620
|
-
:
|
|
621
|
-
:
|
|
622
|
-
|
|
623
|
-
|
|
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.
|
|
8
|
+
VERSION = "1.10.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.10.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-
|
|
11
|
+
date: 2026-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: logger
|