xrpl-ruby 0.7.0 → 0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +261 -0
- data/README.md +160 -3
- data/lib/binary-codec/binary_codec.rb +122 -1
- data/lib/binary-codec/enums/definitions.json +84 -288
- data/lib/binary-codec/enums/definitions.rb +38 -1
- data/lib/binary-codec/serdes/binary_serializer.rb +7 -1
- data/lib/binary-codec/types/st_array.rb +7 -2
- data/lib/binary-codec/types/st_object.rb +7 -11
- data/lib/binary-codec/types/uint.rb +109 -50
- data/lib/wallet/wallet.rb +180 -6
- data/lib/xrpl/batch.rb +167 -0
- data/lib/xrpl/client.rb +48 -7
- data/lib/xrpl/fee.rb +94 -0
- data/lib/xrpl/ledger_entry.rb +53 -0
- data/lib/xrpl/model.rb +239 -0
- data/lib/xrpl/transaction.rb +14 -163
- data/lib/xrpl/version.rb +1 -1
- data/lib/xrpl-ruby.rb +4 -0
- metadata +6 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1b52d066f91b01da1ccdbbe8229cadc3b40b54d79a1234f0ac96f0f131159a06
|
|
4
|
+
data.tar.gz: 57fc035044049efff0bae7418e48e6ffee589a2cec5d763b056b07d6c23d0b10
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4fd54f7b3c12b2cdf35ceecf7a226526900e31e866f16ad3b138d86ac98571e8cf501904b478fa5fa3ace4a8f49ae0e7ee160a33ba8fbd7031d0563cb59876eb
|
|
7
|
+
data.tar.gz: 5e61d8a4da0bf7e517b22ae77d3b5ea2580ae36f2751fba690051482c449d2778fb3be9886167e0160ad45d08cc1639afd9eb9b39fefa074163d712b06f1f64b
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,267 @@ All notable changes to this project are documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.9.0] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
Batch (XLS-56) and permission delegation (XLS-75), both enabled on Mainnet
|
|
13
|
+
since October, are fully supported now:
|
|
14
|
+
- `autofill` prepares and prices a Batch.
|
|
15
|
+
- Accounts sign their part with `Wallet#sign_batch`.
|
|
16
|
+
- `Wallet.combine_batch_signers` merges the parts.
|
|
17
|
+
- Delegated transactions are proven on the Testnet.
|
|
18
|
+
|
|
19
|
+
All Testnet specs pass on the release commit: a Batch of one account, a swap
|
|
20
|
+
between two accounts, a signer list, tickets, and delegation with and
|
|
21
|
+
without permission.
|
|
22
|
+
|
|
23
|
+
The codec now also reads UInt Strings the way rippled 3.4.1 does, which is
|
|
24
|
+
the one breaking change. It fixes a serialisation bug that could make a
|
|
25
|
+
multisigned transaction invalid: about one `Signers` entry in 256 lost its
|
|
26
|
+
end marker. Codec fixtures are unchanged: 39/39 transactions and 263/263
|
|
27
|
+
ledger entries, both directions.
|
|
28
|
+
|
|
29
|
+
### Breaking
|
|
30
|
+
|
|
31
|
+
- **UInt Strings are read the way rippled reads them.** rippled 3.4.1
|
|
32
|
+
(`STParsedJSON`) and ripple-binary-codec read these field types like this:
|
|
33
|
+
- **UInt8, UInt16, UInt32:** a whole decimal number.
|
|
34
|
+
- **UInt64:** hex of any length, except in the base-10 fields
|
|
35
|
+
(`MPTAmount`, `MaximumAmount`, `OutstandingAmount`, `LockedAmount`,
|
|
36
|
+
`ConfidentialOutstandingAmount`), which are decimal.
|
|
37
|
+
- **Int32, Int64:** a signed decimal number.
|
|
38
|
+
|
|
39
|
+
A leading `+` is accepted where rippled accepts it (UInt32, Int32,
|
|
40
|
+
Int64), not for UInt8, UInt16 or UInt64.
|
|
41
|
+
|
|
42
|
+
This codec differed in two places, and both now follow rippled:
|
|
43
|
+
- A UInt8/16/32 String in its full width was read as hex. `"00000065"`
|
|
44
|
+
as a `Sequence` was 101 here and is 65 in rippled; `"0000000A"` was 10
|
|
45
|
+
and now raises.
|
|
46
|
+
- An unpadded UInt64 String was read as decimal. `"100"` as `IndexNext`
|
|
47
|
+
was 100 and is `0x100` in rippled; `"36d6"` was 36.
|
|
48
|
+
|
|
49
|
+
Names count only for their own field: `TransactionType` and
|
|
50
|
+
`LedgerEntryType` take type names, `TransactionResult` result codes,
|
|
51
|
+
`PermissionValue` permission names. Before, any field of the same width
|
|
52
|
+
took them, so `TicketSequence: "TrustSet"` was ticket 21 and
|
|
53
|
+
`TransferFee: "Payment"` was 0.
|
|
54
|
+
|
|
55
|
+
Integers and the names in their own fields are unchanged, as is everything
|
|
56
|
+
the codec decodes: it renders UInt8/16/32 as Integers and UInt64 as 16 hex
|
|
57
|
+
digits, which round-trips. Affected is only code that passes
|
|
58
|
+
- UInt8/16/32 values as hex Strings,
|
|
59
|
+
- unpadded UInt64 values meant as decimal, or
|
|
60
|
+
- a name in a field it does not belong to, such as
|
|
61
|
+
`TicketSequence: "TrustSet"` or `TransferFee: "Payment"`, which now
|
|
62
|
+
raises, or
|
|
63
|
+
- an Int64 as a hex String: `Int64.from("DEAD")` was 57005 and now raises.
|
|
64
|
+
Int64 also decodes to a decimal String (`"-9"`) instead of 16 hex digits
|
|
65
|
+
(`"FFFFFFFFFFFFFFF7"`), so it round-trips. rippled 3.4.1 has no Int64
|
|
66
|
+
field, so this concerns direct use of the type only.
|
|
67
|
+
|
|
68
|
+
Where ripple-binary-codec accepts a value,
|
|
69
|
+
`json_to_binary` now produces the same bytes; it also accepts decimal
|
|
70
|
+
UInt8/16 Strings, which rippled reads and ripple-binary-codec refuses.
|
|
71
|
+
|
|
72
|
+
### Added
|
|
73
|
+
|
|
74
|
+
- **Permission delegation is verified on the Testnet** (XLS-75,
|
|
75
|
+
PermissionDelegationV1_1 enabled on Mainnet since 2026-10-08). The library
|
|
76
|
+
needed no change:
|
|
77
|
+
- `Wallet#sign` signs a transaction whose `Account` is another account.
|
|
78
|
+
- `autofill` takes the `Sequence` from `Account`.
|
|
79
|
+
- The codec translates `PermissionValue` names (since 0.8.0).
|
|
80
|
+
|
|
81
|
+
`spec/integration/xrpl/delegation_spec.rb` covers the whole flow:
|
|
82
|
+
- a `DelegateSet`,
|
|
83
|
+
- then a `Payment` with `Delegate`, signed by the delegate,
|
|
84
|
+
- checked against rippled 3.4.1: the account pays the amount and the
|
|
85
|
+
delegate pays the fee,
|
|
86
|
+
- and a delegate without the permission is refused with
|
|
87
|
+
`terNO_DELEGATE_PERMISSION`.
|
|
88
|
+
|
|
89
|
+
The README has a section on it, including the warning from xrpl.org not to
|
|
90
|
+
delegate `PaymentBurn` before `fixCleanup3_4_0` is enabled.
|
|
91
|
+
- **Batch signing across accounts.** `Wallet#sign_batch` produces the
|
|
92
|
+
`BatchSigners` entry for an account with inner transactions in the Batch:
|
|
93
|
+
the wallet's own account, another account it is the regular key of
|
|
94
|
+
(`batch_account:`), or one signer of a signer list (`multisign:`). For a
|
|
95
|
+
delegated inner transaction the `Delegate` signs.
|
|
96
|
+
`Wallet.combine_batch_signers` merges the signed copies, given as Hashes
|
|
97
|
+
or hex. It sorts the entries by numeric AccountID, drops the outer
|
|
98
|
+
account's entry, and merges signatures from one signer list into a single
|
|
99
|
+
entry. It refuses copies of different Batches and an outer transaction that
|
|
100
|
+
is already signed.
|
|
101
|
+
`BinaryCodec.signing_batch_data` builds the payload rippled 3.4.1 verifies:
|
|
102
|
+
`BCH\0`, the outer account, the sequence value (the `TicketSequence` when
|
|
103
|
+
`Sequence` is 0), Flags, the inner transaction IDs and the signer accounts.
|
|
104
|
+
Signatures match the xrpl.js vectors byte for byte, and a swap between two
|
|
105
|
+
accounts validates on the Testnet. `examples/batch.rb` shows both flows.
|
|
106
|
+
- **`BinaryCodec.transaction_id`** gives the ID of any transaction, signed or
|
|
107
|
+
unsigned (SHA512Half of `TXN\0` and its serialisation), and
|
|
108
|
+
`BinaryCodec.inner_transaction_ids` the IDs of a Batch's inner
|
|
109
|
+
transactions. `Wallet#sign` uses it for its `hash`.
|
|
110
|
+
- **`autofill` prepares a Batch** (XLS-56, BatchV1_1 enabled on Mainnet
|
|
111
|
+
since 2026-10-09). Every inner transaction gets `tfInnerBatchTxn`, `Fee`
|
|
112
|
+
`"0"` and an empty `SigningPubKey`; sequences follow `autofillBatchTxn` in
|
|
113
|
+
xrpl.js: the outer account's inner transactions continue after the outer
|
|
114
|
+
`Sequence`, every other account starts at its own, a given `Sequence` or
|
|
115
|
+
`TicketSequence` is kept, and the account's following inner transactions
|
|
116
|
+
continue after an explicit `Sequence` (xrpl.js hands it out a second time). What the ledger would refuse raises
|
|
117
|
+
`ArgumentError` before anything is signed: not exactly one mode flag,
|
|
118
|
+
fewer than two or more than eight inner transactions, an entry that is not
|
|
119
|
+
a `RawTransaction` wrapper, a nested Batch, an inner signature, fee or key.
|
|
120
|
+
The rules live in `XRPL::Batch`, which needs no network.
|
|
121
|
+
|
|
122
|
+
### Fixed
|
|
123
|
+
|
|
124
|
+
- **A Sequence next to a TicketSequence is refused in a Batch.** On a ticket
|
|
125
|
+
`Sequence` is 0, and rippled refuses anything else (`temSEQ_AND_TICKET`).
|
|
126
|
+
`autofill` kept a nonzero one on an inner transaction and counted it, so
|
|
127
|
+
the account's later inner transactions got sequences that were too high.
|
|
128
|
+
It now raises `ArgumentError` before handing out any sequence, for the
|
|
129
|
+
outer transaction as well as the inner ones. Every inner transaction is
|
|
130
|
+
checked before the first sequence is fetched. A `Sequence` given as a
|
|
131
|
+
String is read the way the codec encodes it, also in the BatchSigner
|
|
132
|
+
payload, rather than with `#to_i`; a name or a malformed String raises.
|
|
133
|
+
- **`autofill` gives a transaction on a ticket `Sequence` 0.** With a
|
|
134
|
+
`TicketSequence` and no `Sequence`, it fetched the account's sequence,
|
|
135
|
+
which rippled refuses next to a ticket. This affected every transaction
|
|
136
|
+
type, not only a Batch.
|
|
137
|
+
- **Batch sequences around tickets.** Two cases of `autofill` were wrong
|
|
138
|
+
for a Batch:
|
|
139
|
+
- An inner transaction on a ticket got no `Sequence`. The field is
|
|
140
|
+
required and is 0 on a ticket, and rippled refuses the Batch without it.
|
|
141
|
+
- An inner `TicketCreate` did not reserve the sequences of the tickets it
|
|
142
|
+
creates, so the next inner transaction of that account repeated one of
|
|
143
|
+
them.
|
|
144
|
+
|
|
145
|
+
Both now follow rippled: a transaction on a ticket consumes no sequence,
|
|
146
|
+
and a `TicketCreate` takes one per ticket after its own, or from the
|
|
147
|
+
current sequence when it is itself on a ticket. Found through the Testnet
|
|
148
|
+
tests of xrpl-php, which fixed the first case the same day.
|
|
149
|
+
- **A Batch over several accounts was charged too little.** The fee is
|
|
150
|
+
`(2 + n) × base fee` plus the inner fees (XLS-56 §2.2), with n the
|
|
151
|
+
signatures in `BatchSigners`; 0.8.0 left n out unless it came in as
|
|
152
|
+
`signers_count:`. `Fee.calculate` now counts `BatchSigners` itself, a
|
|
153
|
+
signer-list entry by its nested `Signers`. Because the signatures cover
|
|
154
|
+
the finished Batch, its fee is set before they exist:
|
|
155
|
+
`autofill(batch, batch_signatures: 2)` says how many will come. It is a
|
|
156
|
+
keyword, not a field, so it never reaches the codec. The 2 XRP cap applies
|
|
157
|
+
to a Batch as it does in xrpl.js.
|
|
158
|
+
|
|
159
|
+
- **An STArray element could lose its ObjectEndMarker on encode.** The
|
|
160
|
+
codec closed an element with `0xE1` only when its last byte was not
|
|
161
|
+
already `0xE1`, so the marker went missing whenever an element ended in
|
|
162
|
+
that byte by chance: a `Signer` or `RawTransaction` whose last AccountID
|
|
163
|
+
ends in `0xE1` (about one address in 256), a `Memo` whose data does, or an
|
|
164
|
+
element ending in a nested object (`AffectedNodes`). Such a transaction
|
|
165
|
+
was serialised, and therefore signed, differently from rippled and was
|
|
166
|
+
rejected. Every element now gets its marker; the binaries match
|
|
167
|
+
ripple-binary-codec byte for byte. The codec fixtures contain no such
|
|
168
|
+
element, which is why they never showed it.
|
|
169
|
+
- **Decoding wrote a nested object's end marker twice** when re-serialising
|
|
170
|
+
it, which surfaced as stray `ArrayEndMarker` / `ObjectEndMarker` keys in
|
|
171
|
+
decoded metadata.
|
|
172
|
+
|
|
173
|
+
### Changed
|
|
174
|
+
|
|
175
|
+
- **UInt fields refuse malformed values.** A String that is not a number in
|
|
176
|
+
the field's reading (see Breaking) raises `ArgumentError`. Before, it went
|
|
177
|
+
through `#to_i`:
|
|
178
|
+
- `"101junk"` became 101, `"12.5"` became 12.
|
|
179
|
+
- `"abc"`, `"-1"` and `"0x10"` became 0.
|
|
180
|
+
|
|
181
|
+
A number outside the field's range also raises now. Before, `2**32` as a
|
|
182
|
+
UInt32 came out five bytes long, and a negative number became 0.
|
|
183
|
+
- **`STObject.from_parser` raises on malformed input** instead of ending
|
|
184
|
+
the object silently. The last `rescue => e; break` of the codec is gone;
|
|
185
|
+
a truncated blob used to decode into a partial object.
|
|
186
|
+
|
|
187
|
+
## [0.8.0] - 2026-09-24
|
|
188
|
+
|
|
189
|
+
The codec now targets **rippled 3.4.0** (released 2026-09-17) and passes the
|
|
190
|
+
ripple-binary-codec 2.11.0 fixture set: 39 transactions and 263 ledger
|
|
191
|
+
entries, both directions (302 cases; 287 before). Measured before the change:
|
|
192
|
+
transactions 38/39, accountState 262/263 decode.
|
|
193
|
+
|
|
194
|
+
### Breaking
|
|
195
|
+
|
|
196
|
+
- **`definitions.json` is rippled 3.4.0.** It is what a 3.4.0 node reports
|
|
197
|
+
through `server_definitions` (s1.ripple.com), with the node's own digest in
|
|
198
|
+
`hash`. FIELDS 381 → 357: the 27 `Hook*` fields plus `EmitGeneration` and
|
|
199
|
+
`EmittedTxn` are gone, because rippled dropped Hooks; a blob carrying one
|
|
200
|
+
of them no longer decodes. Added: `VaultKind`, `SubscriptionDate`,
|
|
201
|
+
`RedemptionDate`, `LEVersion`, `ContractResult` (LendingProtocolV1_1,
|
|
202
|
+
closed-ended vaults), and `CredentialIDs` on `VaultWithdraw` and
|
|
203
|
+
`LoanBrokerCoverWithdraw`. ripple-binary-codec's `main` additionally
|
|
204
|
+
carries four `*KeyEpoch` fields from rippled's development branch; they
|
|
205
|
+
are in no release and deliberately not here.
|
|
206
|
+
- **`PermissionValue` decodes to its name.** A `DelegateSet` permission is
|
|
207
|
+
a UInt32 that rippled renders by name: a transaction type as its code plus
|
|
208
|
+
one (`"Payment"` = 1), the twelve granular permissions from 65537
|
|
209
|
+
(`"TrustlineAuthorize"`). Encoding accepts the names; they were silently
|
|
210
|
+
serialised as 0 before.
|
|
211
|
+
|
|
212
|
+
### Added
|
|
213
|
+
|
|
214
|
+
- **Ledger entry models** — `XRPL::LedgerEntry::AccountRoot`,
|
|
215
|
+
`::RippleState`, `::Offer` and the other 28 types, generated from
|
|
216
|
+
`LEDGER_ENTRY_FORMATS` the way the transaction models are generated from
|
|
217
|
+
`TRANSACTION_FORMATS`. `XRPL::LedgerEntry.from(node)` builds the right
|
|
218
|
+
class from anything `ledger_entry`, `account_objects` or `ledger_data`
|
|
219
|
+
returns and keeps rippled's `index` alongside. Every `lsf` flag is a
|
|
220
|
+
constant on its type. All 263 reference ledger entries round-trip through
|
|
221
|
+
their model unchanged.
|
|
222
|
+
- **Flag helpers** on transactions and ledger entries: `#flag?` takes the
|
|
223
|
+
ledger's name, the constant's name or the bit, `#flag_names` lists what
|
|
224
|
+
is set.
|
|
225
|
+
- **Fees by transaction type.** `autofill` now charges what rippled charges:
|
|
226
|
+
`EscrowFinish` pays for its `Fulfillment` (base × (33 + bytes / 16)),
|
|
227
|
+
`AccountDelete`, `AMMCreate` and `VaultCreate` cost the owner reserve
|
|
228
|
+
from `server_state`, a `Batch` pays two base fees plus those of its inner
|
|
229
|
+
transactions, the confidential MPT transactions cost ten base fees, and
|
|
230
|
+
multisigning adds one base fee per signature. Ordinary fees are capped at
|
|
231
|
+
2 XRP (`Client.new(url, max_fee_drops:)`); the reserve-priced types are
|
|
232
|
+
not. The rules are a pure module, `XRPL::Fee.calculate`, and the same
|
|
233
|
+
ones xrpl.js applies. Before, every transaction paid the base fee, so an
|
|
234
|
+
AccountDelete or an EscrowFinish with a fulfillment could not be
|
|
235
|
+
autofilled.
|
|
236
|
+
- **Payment channel claims.** `Wallet#sign_payment_channel_claim(channel,
|
|
237
|
+
drops)` and `#verify_payment_channel_claim`, plus
|
|
238
|
+
`Wallet.verify_payment_channel_claim(..., public_key)` for the receiving
|
|
239
|
+
side; `BinaryCodec.signing_claim_data` underneath. Checked against the
|
|
240
|
+
xrpl.js signature vector byte for byte.
|
|
241
|
+
- The hash prefixes for batch, counterparty and sponsor signatures
|
|
242
|
+
(`fixCleanup3_4_0`) are in `BinaryCodec::HASH_PREFIX`; the signing
|
|
243
|
+
helpers for them are not written yet.
|
|
244
|
+
- `Definitions#delegatable_permissions`, and readers for the transaction
|
|
245
|
+
type, ledger entry type and result tables the codec resolves names
|
|
246
|
+
against.
|
|
247
|
+
|
|
248
|
+
### Changed
|
|
249
|
+
|
|
250
|
+
- `XRPL::Transaction` is built on a new `XRPL::Model` base shared with
|
|
251
|
+
`XRPL::LedgerEntry`. Its public surface is unchanged;
|
|
252
|
+
`Transaction::ValidationError` is now `Model::ValidationError`, reachable
|
|
253
|
+
under both names. A model rejects a hash whose type field contradicts the
|
|
254
|
+
class (`Payment.new('TransactionType' => 'TrustSet')`), where it used to
|
|
255
|
+
keep the foreign value and serialise a TrustSet.
|
|
256
|
+
- The transaction models pick up the 3.4.0 fields without a code change:
|
|
257
|
+
`VaultCreate.new(vault_kind:, subscription_date:, redemption_date:)`.
|
|
258
|
+
- A parity spec that pins `definitions.json` to the 3.4.0 digest and fails
|
|
259
|
+
if a Hook or development-branch field comes back.
|
|
260
|
+
|
|
261
|
+
### Fixed
|
|
262
|
+
|
|
263
|
+
- `ConfidentialOutstandingAmount` renders in base 10 like the other MPToken
|
|
264
|
+
amounts, not as a 16 digit hex string.
|
|
265
|
+
- `Definitions#get_field_instance` returns `nil` for an unknown field name.
|
|
266
|
+
Serialising a hash with a foreign key now fails with "Field x is not
|
|
267
|
+
defined" instead of a `NoMethodError` on `nil`.
|
|
268
|
+
|
|
8
269
|
## [0.7.0] - 2026-08-26
|
|
9
270
|
|
|
10
271
|
The binary codec now passes the full ripple-binary-codec reference fixture set
|
data/README.md
CHANGED
|
@@ -12,14 +12,19 @@ A pure-Ruby library to interact with the [XRP Ledger](https://xrpl.org) (XRPL) b
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
14
|
- Key and wallet management (secp256k1 and ed25519)
|
|
15
|
-
- Address codec and binary
|
|
15
|
+
- Address codec and binary codec, conformant with rippled 3.4.0 and the
|
|
16
|
+
ripple-binary-codec reference fixtures
|
|
17
|
+
- Transaction and ledger entry models generated from the ledger's own
|
|
18
|
+
definitions
|
|
16
19
|
- WebSocket client for the XRP Ledger public API
|
|
17
20
|
- Testnet faucet helper to create and fund wallets
|
|
18
|
-
- Transaction lifecycle: autofill, sign, submit, and
|
|
21
|
+
- Transaction lifecycle: autofill with per-type fees, sign, submit, and
|
|
22
|
+
reliable "submit and wait"
|
|
23
|
+
- Payment channel claims, signed and verified locally
|
|
19
24
|
|
|
20
25
|
## Requirements
|
|
21
26
|
|
|
22
|
-
- Ruby 3.
|
|
27
|
+
- Ruby 3.2 or later
|
|
23
28
|
|
|
24
29
|
## Installation
|
|
25
30
|
|
|
@@ -111,6 +116,158 @@ hash, which is useful for anything read back off the ledger. Note that
|
|
|
111
116
|
`validate!` follows rippled's formats: it checks what the ledger requires for
|
|
112
117
|
serialisation, which is not always what a transaction needs to be meaningful.
|
|
113
118
|
|
|
119
|
+
Flags can be asked about by any of their names:
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
payment.flag?(:tf_partial_payment) # => true
|
|
123
|
+
payment.flag_names # => ["tfPartialPayment"]
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Fees
|
|
127
|
+
|
|
128
|
+
`autofill` sets the fee the transaction type actually needs, following the
|
|
129
|
+
same rules as xrpl.js: an `EscrowFinish` pays for the size of its
|
|
130
|
+
`Fulfillment`, `AccountDelete`, `AMMCreate` and `VaultCreate` cost the owner
|
|
131
|
+
reserve, a `Batch` pays for its signatures and its inner transactions, and multisigning adds one
|
|
132
|
+
base fee per signature. Ordinary fees are capped at 2 XRP; pass
|
|
133
|
+
`max_fee_drops:` to `Client.new` to change that. The rules are available on
|
|
134
|
+
their own as `XRPL::Fee.calculate`.
|
|
135
|
+
|
|
136
|
+
## Ledger entries
|
|
137
|
+
|
|
138
|
+
The objects that make up the ledger's state - `AccountRoot`, `RippleState`,
|
|
139
|
+
`Offer`, `Escrow` and the rest - have models too, generated from
|
|
140
|
+
`LEDGER_ENTRY_FORMATS` the same way:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
node = client.request_with_retry('ledger_entry', account_root: address).dig('result', 'node')
|
|
144
|
+
entry = XRPL::LedgerEntry.from(node)
|
|
145
|
+
|
|
146
|
+
entry.class # => XRPL::LedgerEntry::AccountRoot
|
|
147
|
+
entry.balance # => "370000000"
|
|
148
|
+
entry.flag?(:lsf_default_ripple) # => true
|
|
149
|
+
entry.flag_names # => ["lsfDefaultRipple", "lsfDisableMaster"]
|
|
150
|
+
entry.index # the entry's hash, as rippled reports it
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Every flag rippled defines is a constant on its type, e.g.
|
|
154
|
+
`XRPL::LedgerEntry::AccountRoot::LSF_DEPOSIT_AUTH`.
|
|
155
|
+
|
|
156
|
+
## Batch
|
|
157
|
+
|
|
158
|
+
A `Batch` (XLS-56, enabled on Mainnet since 2026-10-09) applies up to eight
|
|
159
|
+
inner transactions together. `autofill` prepares them: the `tfInnerBatchTxn`
|
|
160
|
+
flag, `Fee "0"`, an empty `SigningPubKey` and their sequences. It also refuses
|
|
161
|
+
a Batch the ledger would refuse. A Batch of one account is signed like any
|
|
162
|
+
other transaction:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
batch = client.autofill({
|
|
166
|
+
'TransactionType' => 'Batch',
|
|
167
|
+
'Account' => alice.classic_address,
|
|
168
|
+
'Flags' => XRPL::Batch::FLAGS['Batch']['tfAllOrNothing'],
|
|
169
|
+
'RawTransactions' => [
|
|
170
|
+
{ 'RawTransaction' => { 'TransactionType' => 'Payment', 'Account' => alice.classic_address,
|
|
171
|
+
'Destination' => bob.classic_address, 'Amount' => '1000000' } },
|
|
172
|
+
{ 'RawTransaction' => { 'TransactionType' => 'Payment', 'Account' => alice.classic_address,
|
|
173
|
+
'Destination' => carol.classic_address, 'Amount' => '1000000' } }
|
|
174
|
+
]
|
|
175
|
+
})
|
|
176
|
+
client.submit_and_wait(batch, wallet: alice, autofill: false)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
When inner transactions come from other accounts, each of those accounts
|
|
180
|
+
signs the finished Batch. The outer account signs it last. The signatures
|
|
181
|
+
cover the inner transaction IDs, so the Batch has to be complete before
|
|
182
|
+
anyone signs, and that includes its fee. `batch_signatures:` tells `autofill`
|
|
183
|
+
how many signatures will be collected, one per other account:
|
|
184
|
+
|
|
185
|
+
```ruby
|
|
186
|
+
batch = client.autofill(swap, batch_signatures: 1) # Alice pays Bob, Bob pays Alice
|
|
187
|
+
signed = Wallet::Wallet.combine_batch_signers([bob.sign_batch(batch)])
|
|
188
|
+
client.submit_and_wait(signed, wallet: alice, autofill: false)
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
- `sign_batch(batch, batch_account:)` signs for another account whose
|
|
192
|
+
regular key the wallet holds.
|
|
193
|
+
- `multisign: true` signs as one member of that account's signer list.
|
|
194
|
+
- `combine_batch_signers` sorts the entries the way rippled requires and
|
|
195
|
+
drops the outer account's entry. It also merges signatures from one signer
|
|
196
|
+
list into a single entry.
|
|
197
|
+
- For an inner transaction with a `Delegate`, the delegate signs.
|
|
198
|
+
|
|
199
|
+
**An outer `tesSUCCESS` does not mean the inner transactions succeeded.**
|
|
200
|
+
Under `tfOnlyOne`, `tfUntilFailure` or `tfIndependent` some of them can fail
|
|
201
|
+
while the Batch itself succeeds. Look up each one by its ID and check its own
|
|
202
|
+
result:
|
|
203
|
+
|
|
204
|
+
```ruby
|
|
205
|
+
BinaryCodec.inner_transaction_ids(signed).each do |id|
|
|
206
|
+
result = client.tx_response(transaction: id).dig('result', 'meta', 'TransactionResult')
|
|
207
|
+
# an inner transaction that was not applied is not found at all
|
|
208
|
+
end
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
`examples/batch.rb` runs both flows against the Testnet.
|
|
212
|
+
|
|
213
|
+
## Delegation
|
|
214
|
+
|
|
215
|
+
An account can let another account send certain transactions on its behalf
|
|
216
|
+
(XLS-75, enabled on Mainnet since 2026-10-08). A `DelegateSet` grants the
|
|
217
|
+
permissions. A permission is either a transaction type, such as `"Payment"`,
|
|
218
|
+
or one of the granular permissions, such as `"TrustlineAuthorize"` or
|
|
219
|
+
`"PaymentMint"`. The codec translates both names.
|
|
220
|
+
|
|
221
|
+
> **Do not delegate `"PaymentBurn"` until the `fixCleanup3_4_0` amendment is
|
|
222
|
+
> enabled on the target network.** Before that fix, a delegate holding
|
|
223
|
+
> `PaymentBurn` can in certain circumstances also mint fungible tokens
|
|
224
|
+
> (trust line tokens or MPTs). The `feature` method of a node reports
|
|
225
|
+
> whether it is enabled there; [Known Amendments](https://xrpl.org/resources/known-amendments)
|
|
226
|
+
> has the Mainnet status. Source:
|
|
227
|
+
> [Permission Values](https://xrpl.org/docs/references/protocol/data-types/permission-values).
|
|
228
|
+
|
|
229
|
+
```ruby
|
|
230
|
+
client.submit_and_wait({
|
|
231
|
+
'TransactionType' => 'DelegateSet',
|
|
232
|
+
'Account' => isaac.classic_address,
|
|
233
|
+
'Authorize' => alice.classic_address,
|
|
234
|
+
'Permissions' => [{ 'Permission' => { 'PermissionValue' => 'Payment' } }]
|
|
235
|
+
}, wallet: isaac)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
The delegate then sends the transaction with `Account` set to the delegating
|
|
239
|
+
account and `Delegate` set to itself, and signs it with its own wallet:
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
client.submit_and_wait({
|
|
243
|
+
'TransactionType' => 'Payment',
|
|
244
|
+
'Account' => isaac.classic_address,
|
|
245
|
+
'Delegate' => alice.classic_address,
|
|
246
|
+
'Destination' => bob.classic_address,
|
|
247
|
+
'Amount' => '1000000'
|
|
248
|
+
}, wallet: alice)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
- **Sequence:** `autofill` takes it from `Account`, as rippled expects.
|
|
252
|
+
- **Signature and fee:** both belong to the delegate. Isaac pays the amount,
|
|
253
|
+
Alice pays the fee.
|
|
254
|
+
- **Without the permission:** the transaction comes back as
|
|
255
|
+
`terNO_DELEGATE_PERMISSION`.
|
|
256
|
+
- **Inside a Batch:** for a delegated inner transaction, the delegate signs
|
|
257
|
+
the `BatchSigners` entry (see above).
|
|
258
|
+
|
|
259
|
+
## Payment channel claims
|
|
260
|
+
|
|
261
|
+
A claim is signed off-ledger and redeemed later with a `PaymentChannelClaim`
|
|
262
|
+
transaction. Amounts are in drops:
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
signature = wallet.sign_payment_channel_claim(channel_id, '1000000')
|
|
266
|
+
|
|
267
|
+
# On the receiving side, against the channel's PublicKey:
|
|
268
|
+
Wallet::Wallet.verify_payment_channel_claim(channel_id, '1000000', signature, public_key)
|
|
269
|
+
```
|
|
270
|
+
|
|
114
271
|
The client is silent by default. To see diagnostic output, pass a logger:
|
|
115
272
|
|
|
116
273
|
```ruby
|
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
3
5
|
module BinaryCodec
|
|
4
6
|
# Hash prefixes for serialization.
|
|
5
7
|
HASH_PREFIX = {
|
|
8
|
+
transaction_id: 0x54584E00, # 'TXN\0'
|
|
6
9
|
transaction_sig: 0x53545800, # 'STX\0'
|
|
7
10
|
transaction_multi_sig: 0x534D5400, # 'SMT\0'
|
|
8
11
|
validation: 0x56414C00, # 'VAL\0'
|
|
9
|
-
proposal: 0x50525000 # 'PRP\0'
|
|
12
|
+
proposal: 0x50525000, # 'PRP\0'
|
|
13
|
+
payment_channel_claim: 0x434C4D00, # 'CLM\0'
|
|
14
|
+
batch: 0x42434800, # 'BCH\0'
|
|
15
|
+
# Role-specific prefixes from fixCleanup3_4_0, so a signature made for
|
|
16
|
+
# one role cannot be replayed as another.
|
|
17
|
+
counterparty_sig: 0x43505400, # 'CPT\0'
|
|
18
|
+
counterparty_multi_sig: 0x43504D00, # 'CPM\0'
|
|
19
|
+
sponsor_sig: 0x53504E00, # 'SPN\0'
|
|
20
|
+
sponsor_multi_sig: 0x53504D00 # 'SPM\0'
|
|
10
21
|
}.freeze
|
|
11
22
|
|
|
12
23
|
# from here: https://github.com/XRPLF/xrpl.js/blob/main/packages/ripple-binary-codec/src/binary.ts
|
|
@@ -38,6 +49,110 @@ module BinaryCodec
|
|
|
38
49
|
st_object.from(json).to_hex
|
|
39
50
|
end
|
|
40
51
|
|
|
52
|
+
# The bytes a payment channel claim is signed over: the 'CLM\0' prefix,
|
|
53
|
+
# the channel ID and the amount in drops. Same as xrpl.js
|
|
54
|
+
# encodeForSigningClaim.
|
|
55
|
+
#
|
|
56
|
+
# @param channel [String] the channel ID, 64 hex characters
|
|
57
|
+
# @param amount [String, Integer] the amount in drops
|
|
58
|
+
# @return [Array<Integer>] the signing data
|
|
59
|
+
def signing_claim_data(channel:, amount:)
|
|
60
|
+
int_to_bytes(HASH_PREFIX[:payment_channel_claim], 4) +
|
|
61
|
+
Hash256.from(channel).to_bytes +
|
|
62
|
+
Uint64.from(Integer(amount)).to_bytes
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# The ID of a transaction: SHA512Half of 'TXN\0' and its serialisation.
|
|
66
|
+
# For a signed transaction that is its hash; for an unsigned one, such as
|
|
67
|
+
# the inner transaction of a Batch, it is the ID rippled gives it.
|
|
68
|
+
#
|
|
69
|
+
# @param transaction [Hash, String] the transaction, or its serialisation as hex
|
|
70
|
+
# @return [String] the ID, 64 uppercase hex characters
|
|
71
|
+
def transaction_id(transaction)
|
|
72
|
+
hex = transaction.is_a?(String) ? transaction : json_to_binary(transaction)
|
|
73
|
+
prefix = int_to_bytes(HASH_PREFIX[:transaction_id], 4).pack('C*')
|
|
74
|
+
Digest::SHA512.digest(prefix + [hex].pack('H*'))[0, 32].unpack1('H*').upcase
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# The IDs of a Batch's inner transactions, in order.
|
|
78
|
+
#
|
|
79
|
+
# @param batch [Hash] the Batch
|
|
80
|
+
# @return [Array<String>] one ID per entry of RawTransactions
|
|
81
|
+
def inner_transaction_ids(batch)
|
|
82
|
+
Array(batch['RawTransactions']).map do |wrapper|
|
|
83
|
+
inner = wrapper.is_a?(::Hash) ? wrapper['RawTransaction'] : nil
|
|
84
|
+
inner = inner.to_h if defined?(XRPL::Transaction) && inner.is_a?(XRPL::Transaction)
|
|
85
|
+
raise ArgumentError, 'Every entry of RawTransactions needs a RawTransaction' unless inner.is_a?(::Hash)
|
|
86
|
+
|
|
87
|
+
transaction_id(inner)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# The bytes a BatchSigner signs over (rippled serializeBatch plus the
|
|
92
|
+
# signer suffix, rippled 3.4.1 STTx::checkBatchSingleSign and
|
|
93
|
+
# checkBatchMultiSign): 'BCH\0', the outer Account, the outer sequence
|
|
94
|
+
# value, Flags, the number of inner transactions and their IDs, then the
|
|
95
|
+
# BatchSigner's Account and, when multisigning, the signing account.
|
|
96
|
+
#
|
|
97
|
+
# Like encodeForSigningBatch in xrpl.js, this encodes what it is given: a
|
|
98
|
+
# Batch without a Sequence encodes the sequence value 0. Wallet#sign_batch
|
|
99
|
+
# refuses to sign one, because rippled requires the Sequence.
|
|
100
|
+
#
|
|
101
|
+
# @param batch [Hash] the Batch, with Account, Flags and its inner
|
|
102
|
+
# transactions complete
|
|
103
|
+
# @param batch_account [String] the account of the BatchSigner entry
|
|
104
|
+
# @param signer_account [String, nil] the account of the signer in the
|
|
105
|
+
# entry's Signers, when the entry is multisigned
|
|
106
|
+
# @return [Array<Integer>] the signing data
|
|
107
|
+
def signing_batch_data(batch, batch_account:, signer_account: nil)
|
|
108
|
+
unless batch['Account'] && batch.key?('Flags')
|
|
109
|
+
raise ArgumentError, 'A Batch needs Account and Flags before it can be signed'
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
ids = inner_transaction_ids(batch)
|
|
113
|
+
bytes = int_to_bytes(HASH_PREFIX[:batch], 4) +
|
|
114
|
+
account_bytes(batch['Account']) +
|
|
115
|
+
int_to_bytes(batch_sequence_value(batch), 4) +
|
|
116
|
+
int_to_bytes(Integer(batch['Flags']), 4) +
|
|
117
|
+
int_to_bytes(ids.size, 4)
|
|
118
|
+
ids.each { |id| bytes += hex_to_bytes(id) }
|
|
119
|
+
bytes += account_bytes(batch_account)
|
|
120
|
+
bytes += account_bytes(signer_account) if signer_account
|
|
121
|
+
bytes
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# rippled's SeqProxy value: the Sequence, or the TicketSequence when the
|
|
125
|
+
# Sequence is 0 because a ticket is used.
|
|
126
|
+
#
|
|
127
|
+
# @param batch [Hash]
|
|
128
|
+
# @return [Integer]
|
|
129
|
+
def batch_sequence_value(batch)
|
|
130
|
+
sequence = uint32_value(batch['Sequence'])
|
|
131
|
+
return sequence if sequence.positive?
|
|
132
|
+
|
|
133
|
+
uint32_value(batch['TicketSequence'])
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# A UInt32 field's value as the codec serialises it, so a check made on
|
|
137
|
+
# it agrees with the bytes that are signed. A String is a decimal
|
|
138
|
+
# number, as rippled reads it, not #to_i: "101junk" raises rather than
|
|
139
|
+
# becoming 101.
|
|
140
|
+
#
|
|
141
|
+
# Only numbers: a UInt32 also takes a permission name ("TrustSet" is
|
|
142
|
+
# 21), which is meant for PermissionValue and would be a wrong ticket or
|
|
143
|
+
# sequence here.
|
|
144
|
+
#
|
|
145
|
+
# @param value [Integer, String, nil]
|
|
146
|
+
# @return [Integer] 0 for nil
|
|
147
|
+
def uint32_value(value)
|
|
148
|
+
return 0 if value.nil?
|
|
149
|
+
if value.is_a?(String) && !value.match?(/\A\+?\d+\z/)
|
|
150
|
+
raise ArgumentError, "A sequence needs a number, got #{value.inspect}"
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
Uint32.from(value).value_of
|
|
154
|
+
end
|
|
155
|
+
|
|
41
156
|
# Generates signing data for a transaction.
|
|
42
157
|
# @param transaction [Hash] The transaction to serialize.
|
|
43
158
|
# @param prefix [Integer] The prefix to add to the serialized data.
|
|
@@ -60,5 +175,11 @@ module BinaryCodec
|
|
|
60
175
|
|
|
61
176
|
prefix_bytes + serialized_bytes
|
|
62
177
|
end
|
|
178
|
+
|
|
179
|
+
private
|
|
180
|
+
|
|
181
|
+
def account_bytes(address)
|
|
182
|
+
AddressCodec::AddressCodec.new.decode_account_id(address).to_a
|
|
183
|
+
end
|
|
63
184
|
end
|
|
64
185
|
end
|