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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e07e1a6fd2b134d36dbbdaaa977cc39e8e9176499a29e69bd84e69ddfd6f6979
4
- data.tar.gz: bcca5b159608288c0cdeffc562a76332fe4e831e4dc9813676f66d46c53546ac
3
+ metadata.gz: 1b52d066f91b01da1ccdbbe8229cadc3b40b54d79a1234f0ac96f0f131159a06
4
+ data.tar.gz: 57fc035044049efff0bae7418e48e6ffee589a2cec5d763b056b07d6c23d0b10
5
5
  SHA512:
6
- metadata.gz: fffeb72058c2a328c01bda6e6bfd63718147fc12326385c18c304b9d0e92013e60ba9ad11424af2ee6bd72c69e016494113db5c05ed26e10ee2501bc0873288a
7
- data.tar.gz: 3aa7f43f67c50faaf0139a5f42737a35c5fd59408dc3098144a7a0b3a57bab5dad3eac3fa68fb9819749ba2dd0b7f91460de5e87899279e49b6ab07fcae87c83
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 (transaction) codec
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 reliable "submit and wait"
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.0 or later
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