Skip to main content

chains_sdk/xrp/
advanced.rs

1//! XRP advanced transactions: DEX orders, Escrow, and IOU precision.
2
3// ═══════════════════════════════════════════════════════════════════
4// XRPL Amount Encoding (IOU with Mantissa/Exponent)
5// ═══════════════════════════════════════════════════════════════════
6
7/// Encode an IOU (Issued Currency) amount per XRPL serialization format.
8///
9/// XRPL IOU amounts are 8 bytes:
10/// - Bit 63: Not XRP flag (always 1 for IOU)
11/// - Bit 62: Sign (1 = positive, 0 = negative)
12/// - Bits 54-61: Exponent (biased by 97)
13/// - Bits 0-53: Mantissa (54 bits)
14///
15/// The value = mantissa * 10^(exponent - 97)
16///
17/// # Arguments
18/// - `mantissa` — Significant digits (must be <= 10^16 - 1)
19/// - `exponent` — Power of 10 offset (range: -96 to 80)
20/// - `positive` — Whether the amount is positive
21pub fn encode_iou_amount(mantissa: u64, exponent: i8, positive: bool) -> [u8; 8] {
22    if mantissa == 0 {
23        // Zero amount: special encoding
24        let mut bytes = [0u8; 8];
25        bytes[0] = 0x80; // Not XRP flag, positive, zero mantissa
26        return bytes;
27    }
28
29    // Normalize mantissa to 54 bits max (10^15 <= m < 10^16)
30    let mut m = mantissa;
31    let mut e = exponent as i16;
32
33    // Normalize: mantissa should be in [10^15, 10^16)
34    while m < 1_000_000_000_000_000 && e > -96 {
35        m *= 10;
36        e -= 1;
37    }
38    while m >= 10_000_000_000_000_000 && e < 80 {
39        m /= 10;
40        e += 1;
41    }
42
43    // Bias the exponent: stored = exponent + 97
44    let biased_exp = (e + 97) as u64;
45
46    let mut val: u64 = 0;
47    val |= 1 << 63; // Not XRP flag
48    if positive {
49        val |= 1 << 62; // Positive flag
50    }
51    val |= (biased_exp & 0xFF) << 54; // 8-bit exponent
52    val |= m & 0x003F_FFFF_FFFF_FFFF; // 54-bit mantissa
53
54    val.to_be_bytes()
55}
56
57/// Decode an IOU amount from 8 bytes.
58///
59/// Returns (mantissa, exponent, is_positive).
60pub fn decode_iou_amount(bytes: &[u8; 8]) -> (u64, i8, bool) {
61    let val = u64::from_be_bytes(*bytes);
62
63    // Check for zero
64    if val & 0x003F_FFFF_FFFF_FFFF == 0 {
65        return (0, 0, true);
66    }
67
68    let positive = (val >> 62) & 1 == 1;
69    let biased_exp = ((val >> 54) & 0xFF) as i16;
70    let exponent = (biased_exp - 97) as i8;
71    let mantissa = val & 0x003F_FFFF_FFFF_FFFF;
72
73    (mantissa, exponent, positive)
74}
75
76/// Encode a 3-character currency code for XRPL.
77///
78/// XRPL currency codes are 20 bytes:
79/// - Standard (3-char): 12 zero bytes + 3 ASCII bytes + 5 zero bytes
80/// - Non-standard (40-hex): raw 20 bytes
81pub fn encode_currency_code(code: &str) -> Result<[u8; 20], &'static str> {
82    if code.len() != 3 {
83        return Err("currency code must be 3 characters");
84    }
85    if code == "XRP" {
86        return Err("XRP is not an issued currency");
87    }
88
89    let mut out = [0u8; 20];
90    out[12..15].copy_from_slice(code.as_bytes());
91    Ok(out)
92}
93
94// ═══════════════════════════════════════════════════════════════════
95// OfferCreate / OfferCancel (DEX)
96// ═══════════════════════════════════════════════════════════════════
97
98/// Transaction type code for OfferCreate.
99pub const TT_OFFER_CREATE: u16 = 7;
100/// Transaction type code for OfferCancel.
101pub const TT_OFFER_CANCEL: u16 = 8;
102
103/// Serialize an OfferCreate transaction for signing.
104///
105/// # Arguments
106/// - `account` — 20-byte account address
107/// - `taker_gets_drops` — Amount the taker gets (in drops for XRP, or IOU bytes)
108/// - `taker_pays_drops` — Amount the taker pays (in drops for XRP, or IOU bytes)
109/// - `sequence` — Account sequence number
110/// - `fee_drops` — Fee in drops
111/// - `flags` — Transaction flags (e.g., `tfSell = 0x00080000`)
112pub fn offer_create(
113    account: &[u8; 20],
114    taker_gets_drops: u64,
115    taker_pays_drops: u64,
116    sequence: u32,
117    fee_drops: u64,
118    flags: u32,
119) -> Vec<u8> {
120    let mut buf = Vec::with_capacity(100);
121
122    // TransactionType (field code 0x12 = UInt16)
123    buf.extend_from_slice(&[0x12, (TT_OFFER_CREATE >> 8) as u8, TT_OFFER_CREATE as u8]);
124    // Flags
125    buf.extend_from_slice(&[0x22]);
126    buf.extend_from_slice(&flags.to_be_bytes());
127    // Sequence
128    buf.extend_from_slice(&[0x24]);
129    buf.extend_from_slice(&sequence.to_be_bytes());
130    // Fee (Amount, field code 0x68)
131    buf.push(0x68);
132    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
133    // TakerPays (Amount, field code 0x64)
134    buf.push(0x64);
135    buf.extend_from_slice(&encode_xrp_amount(taker_pays_drops));
136    // TakerGets (Amount, field code 0x65)
137    buf.push(0x65);
138    buf.extend_from_slice(&encode_xrp_amount(taker_gets_drops));
139    // Account (AccountID, field code 0x81 0x14)
140    buf.extend_from_slice(&[0x81, 0x14]);
141    buf.extend_from_slice(account);
142
143    buf
144}
145
146/// Serialize an OfferCancel transaction.
147pub fn offer_cancel(
148    account: &[u8; 20],
149    offer_sequence: u32,
150    sequence: u32,
151    fee_drops: u64,
152) -> Vec<u8> {
153    let mut buf = Vec::with_capacity(60);
154
155    buf.extend_from_slice(&[0x12, (TT_OFFER_CANCEL >> 8) as u8, TT_OFFER_CANCEL as u8]);
156    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]); // flags = 0
157    buf.extend_from_slice(&[0x24]);
158    buf.extend_from_slice(&sequence.to_be_bytes());
159    // OfferSequence (UInt32 field code 0x20 0x19)
160    buf.extend_from_slice(&[0x20, 0x19]);
161    buf.extend_from_slice(&offer_sequence.to_be_bytes());
162    buf.push(0x68);
163    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
164    buf.extend_from_slice(&[0x81, 0x14]);
165    buf.extend_from_slice(account);
166
167    buf
168}
169
170// ═══════════════════════════════════════════════════════════════════
171// Escrow
172// ═══════════════════════════════════════════════════════════════════
173
174/// Transaction type code for EscrowCreate.
175pub const TT_ESCROW_CREATE: u16 = 1;
176/// Transaction type code for EscrowFinish.
177pub const TT_ESCROW_FINISH: u16 = 2;
178/// Transaction type code for EscrowCancel.
179pub const TT_ESCROW_CANCEL: u16 = 4;
180
181/// Serialize an EscrowCreate transaction.
182///
183/// # Arguments
184/// - `account` — Sender address
185/// - `destination` — Recipient address
186/// - `amount_drops` — Amount in drops
187/// - `finish_after` — Unix timestamp after which escrow can be finished
188/// - `cancel_after` — Optional Unix timestamp after which escrow can be cancelled
189/// - `sequence` — Account sequence
190/// - `fee_drops` — Fee in drops
191pub fn escrow_create(
192    account: &[u8; 20],
193    destination: &[u8; 20],
194    amount_drops: u64,
195    finish_after: u32,
196    cancel_after: Option<u32>,
197    sequence: u32,
198    fee_drops: u64,
199) -> Vec<u8> {
200    let mut buf = Vec::with_capacity(80);
201
202    buf.extend_from_slice(&[0x12, (TT_ESCROW_CREATE >> 8) as u8, TT_ESCROW_CREATE as u8]);
203    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]); // flags = 0
204    buf.extend_from_slice(&[0x24]);
205    buf.extend_from_slice(&sequence.to_be_bytes());
206    // FinishAfter (UInt32)
207    buf.extend_from_slice(&[0x20, 0x24]);
208    buf.extend_from_slice(&finish_after.to_be_bytes());
209    // CancelAfter (optional)
210    if let Some(cancel) = cancel_after {
211        buf.extend_from_slice(&[0x20, 0x25]);
212        buf.extend_from_slice(&cancel.to_be_bytes());
213    }
214    // Amount
215    buf.push(0x61);
216    buf.extend_from_slice(&encode_xrp_amount(amount_drops));
217    // Fee
218    buf.push(0x68);
219    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
220    // Account
221    buf.extend_from_slice(&[0x81, 0x14]);
222    buf.extend_from_slice(account);
223    // Destination
224    buf.extend_from_slice(&[0x83, 0x14]);
225    buf.extend_from_slice(destination);
226
227    buf
228}
229
230/// Serialize an EscrowFinish transaction.
231pub fn escrow_finish(
232    account: &[u8; 20],
233    owner: &[u8; 20],
234    offer_sequence: u32,
235    sequence: u32,
236    fee_drops: u64,
237) -> Vec<u8> {
238    let mut buf = Vec::with_capacity(70);
239
240    buf.extend_from_slice(&[0x12, (TT_ESCROW_FINISH >> 8) as u8, TT_ESCROW_FINISH as u8]);
241    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
242    buf.extend_from_slice(&[0x24]);
243    buf.extend_from_slice(&sequence.to_be_bytes());
244    buf.extend_from_slice(&[0x20, 0x19]); // OfferSequence
245    buf.extend_from_slice(&offer_sequence.to_be_bytes());
246    buf.push(0x68);
247    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
248    buf.extend_from_slice(&[0x81, 0x14]);
249    buf.extend_from_slice(account);
250    // Owner
251    buf.extend_from_slice(&[0x82, 0x14]);
252    buf.extend_from_slice(owner);
253
254    buf
255}
256
257/// Serialize an EscrowCancel transaction.
258pub fn escrow_cancel(
259    account: &[u8; 20],
260    owner: &[u8; 20],
261    offer_sequence: u32,
262    sequence: u32,
263    fee_drops: u64,
264) -> Vec<u8> {
265    let mut buf = Vec::with_capacity(70);
266
267    buf.extend_from_slice(&[0x12, (TT_ESCROW_CANCEL >> 8) as u8, TT_ESCROW_CANCEL as u8]);
268    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
269    buf.extend_from_slice(&[0x24]);
270    buf.extend_from_slice(&sequence.to_be_bytes());
271    buf.extend_from_slice(&[0x20, 0x19]);
272    buf.extend_from_slice(&offer_sequence.to_be_bytes());
273    buf.push(0x68);
274    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
275    buf.extend_from_slice(&[0x81, 0x14]);
276    buf.extend_from_slice(account);
277    buf.extend_from_slice(&[0x82, 0x14]);
278    buf.extend_from_slice(owner);
279
280    buf
281}
282
283// ─── Helpers ────────────────────────────────────────────────────────
284
285/// Encode an XRP amount in drops (native currency).
286///
287/// XRP amounts are 8 bytes with bit 63 = 0 (not IOU) and bit 62 = 1 (positive).
288fn encode_xrp_amount(drops: u64) -> [u8; 8] {
289    let val = drops | (0x40 << 56); // Set positive bit
290    val.to_be_bytes()
291}
292
293// ═══════════════════════════════════════════════════════════════════
294// AccountSet
295// ═══════════════════════════════════════════════════════════════════
296
297/// Transaction type code for AccountSet.
298pub const TT_ACCOUNT_SET: u16 = 3;
299
300/// AccountSet flags.
301pub mod account_set_flags {
302    /// Require destination tag on incoming payments.
303    pub const ASF_REQUIRE_DEST: u32 = 1;
304    /// Require authorization for trust lines.
305    pub const ASF_REQUIRE_AUTH: u32 = 2;
306    /// Disallow incoming XRP.
307    pub const ASF_DISALLOW_XRP: u32 = 3;
308    /// Disable the master key.
309    pub const ASF_DISABLE_MASTER: u32 = 4;
310    /// Enable No Freeze on all trust lines.
311    pub const ASF_NO_FREEZE: u32 = 6;
312    /// Enable global freeze.
313    pub const ASF_GLOBAL_FREEZE: u32 = 7;
314    /// Enable deposit authorization.
315    pub const ASF_DEPOSIT_AUTH: u32 = 9;
316    /// Allow trustline clawback.
317    pub const ASF_ALLOW_CLAWBACK: u32 = 16;
318}
319
320/// Serialize an AccountSet transaction.
321pub fn account_set(
322    account: &[u8; 20],
323    set_flag: Option<u32>,
324    clear_flag: Option<u32>,
325    sequence: u32,
326    fee_drops: u64,
327) -> Vec<u8> {
328    let mut buf = Vec::with_capacity(60);
329    buf.extend_from_slice(&[0x12, (TT_ACCOUNT_SET >> 8) as u8, TT_ACCOUNT_SET as u8]);
330    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
331    buf.extend_from_slice(&[0x24]);
332    buf.extend_from_slice(&sequence.to_be_bytes());
333    if let Some(flag) = set_flag {
334        buf.extend_from_slice(&[0x20, 0x21]);
335        buf.extend_from_slice(&flag.to_be_bytes());
336    }
337    if let Some(flag) = clear_flag {
338        buf.extend_from_slice(&[0x20, 0x22]);
339        buf.extend_from_slice(&flag.to_be_bytes());
340    }
341    buf.push(0x68);
342    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
343    buf.extend_from_slice(&[0x81, 0x14]);
344    buf.extend_from_slice(account);
345    buf
346}
347
348// ═══════════════════════════════════════════════════════════════════
349// Payment Channels
350// ═══════════════════════════════════════════════════════════════════
351
352/// Transaction type code for PaymentChannelCreate.
353pub const TT_CHANNEL_CREATE: u16 = 13;
354/// Transaction type code for PaymentChannelFund.
355pub const TT_CHANNEL_FUND: u16 = 14;
356/// Transaction type code for PaymentChannelClaim.
357pub const TT_CHANNEL_CLAIM: u16 = 15;
358
359/// Serialize a PaymentChannelCreate transaction.
360pub fn channel_create(
361    account: &[u8; 20],
362    destination: &[u8; 20],
363    amount_drops: u64,
364    settle_delay: u32,
365    public_key: &[u8; 33],
366    sequence: u32,
367    fee_drops: u64,
368) -> Vec<u8> {
369    let mut buf = Vec::with_capacity(120);
370    buf.extend_from_slice(&[
371        0x12,
372        (TT_CHANNEL_CREATE >> 8) as u8,
373        TT_CHANNEL_CREATE as u8,
374    ]);
375    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
376    buf.extend_from_slice(&[0x24]);
377    buf.extend_from_slice(&sequence.to_be_bytes());
378    buf.extend_from_slice(&[0x20, 0x27]);
379    buf.extend_from_slice(&settle_delay.to_be_bytes());
380    buf.push(0x61);
381    buf.extend_from_slice(&encode_xrp_amount(amount_drops));
382    buf.push(0x68);
383    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
384    buf.extend_from_slice(&[0x71, 0x03]);
385    buf.push(public_key.len() as u8);
386    buf.extend_from_slice(public_key);
387    buf.extend_from_slice(&[0x81, 0x14]);
388    buf.extend_from_slice(account);
389    buf.extend_from_slice(&[0x83, 0x14]);
390    buf.extend_from_slice(destination);
391    buf
392}
393
394/// Serialize a PaymentChannelFund transaction.
395pub fn channel_fund(
396    account: &[u8; 20],
397    channel_id: &[u8; 32],
398    amount_drops: u64,
399    sequence: u32,
400    fee_drops: u64,
401) -> Vec<u8> {
402    let mut buf = Vec::with_capacity(80);
403    buf.extend_from_slice(&[0x12, (TT_CHANNEL_FUND >> 8) as u8, TT_CHANNEL_FUND as u8]);
404    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
405    buf.extend_from_slice(&[0x24]);
406    buf.extend_from_slice(&sequence.to_be_bytes());
407    buf.push(0x61);
408    buf.extend_from_slice(&encode_xrp_amount(amount_drops));
409    buf.push(0x68);
410    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
411    buf.extend_from_slice(&[0x50, 0x16]);
412    buf.extend_from_slice(channel_id);
413    buf.extend_from_slice(&[0x81, 0x14]);
414    buf.extend_from_slice(account);
415    buf
416}
417
418/// Serialize a PaymentChannelClaim transaction.
419pub fn channel_claim(
420    account: &[u8; 20],
421    channel_id: &[u8; 32],
422    balance_drops: Option<u64>,
423    sequence: u32,
424    fee_drops: u64,
425) -> Vec<u8> {
426    let mut buf = Vec::with_capacity(80);
427    buf.extend_from_slice(&[0x12, (TT_CHANNEL_CLAIM >> 8) as u8, TT_CHANNEL_CLAIM as u8]);
428    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
429    buf.extend_from_slice(&[0x24]);
430    buf.extend_from_slice(&sequence.to_be_bytes());
431    if let Some(balance) = balance_drops {
432        buf.push(0x61);
433        buf.extend_from_slice(&encode_xrp_amount(balance));
434    }
435    buf.push(0x68);
436    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
437    buf.extend_from_slice(&[0x50, 0x16]);
438    buf.extend_from_slice(channel_id);
439    buf.extend_from_slice(&[0x81, 0x14]);
440    buf.extend_from_slice(account);
441    buf
442}
443
444// ═══════════════════════════════════════════════════════════════════
445// NFToken (XLS-20)
446// ═══════════════════════════════════════════════════════════════════
447
448/// Transaction type code for NFTokenMint.
449pub const TT_NFTOKEN_MINT: u16 = 25;
450/// Transaction type code for NFTokenCreateOffer.
451pub const TT_NFTOKEN_CREATE_OFFER: u16 = 27;
452/// Transaction type code for NFTokenAcceptOffer.
453pub const TT_NFTOKEN_ACCEPT_OFFER: u16 = 29;
454/// Transaction type code for NFTokenBurn.
455pub const TT_NFTOKEN_BURN: u16 = 26;
456
457/// NFToken mint flags.
458pub mod nftoken_flags {
459    /// NFToken is transferable between accounts.
460    pub const TF_TRANSFERABLE: u32 = 0x0008;
461    /// NFToken can be burned by the issuer.
462    pub const TF_BURNABLE: u32 = 0x0001;
463    /// NFToken offers can only be in XRP.
464    pub const TF_ONLY_XRP: u32 = 0x0002;
465}
466
467/// Serialize an NFTokenMint transaction.
468pub fn nftoken_mint(
469    account: &[u8; 20],
470    nftoken_taxon: u32,
471    flags: u32,
472    uri: Option<&[u8]>,
473    sequence: u32,
474    fee_drops: u64,
475) -> Vec<u8> {
476    let mut buf = Vec::with_capacity(100);
477    buf.extend_from_slice(&[0x12, (TT_NFTOKEN_MINT >> 8) as u8, TT_NFTOKEN_MINT as u8]);
478    buf.extend_from_slice(&[0x22]);
479    buf.extend_from_slice(&flags.to_be_bytes());
480    buf.extend_from_slice(&[0x24]);
481    buf.extend_from_slice(&sequence.to_be_bytes());
482    buf.extend_from_slice(&[0x20, 0x2A]);
483    buf.extend_from_slice(&nftoken_taxon.to_be_bytes());
484    buf.push(0x68);
485    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
486    if let Some(uri_bytes) = uri {
487        buf.extend_from_slice(&[0x75, 0x0D]);
488        buf.push(uri_bytes.len() as u8);
489        buf.extend_from_slice(uri_bytes);
490    }
491    buf.extend_from_slice(&[0x81, 0x14]);
492    buf.extend_from_slice(account);
493    buf
494}
495
496/// Serialize an NFTokenCreateOffer transaction.
497pub fn nftoken_create_offer(
498    account: &[u8; 20],
499    nftoken_id: &[u8; 32],
500    amount_drops: u64,
501    flags: u32,
502    destination: Option<&[u8; 20]>,
503    sequence: u32,
504    fee_drops: u64,
505) -> Vec<u8> {
506    let mut buf = Vec::with_capacity(120);
507    buf.extend_from_slice(&[
508        0x12,
509        (TT_NFTOKEN_CREATE_OFFER >> 8) as u8,
510        TT_NFTOKEN_CREATE_OFFER as u8,
511    ]);
512    buf.extend_from_slice(&[0x22]);
513    buf.extend_from_slice(&flags.to_be_bytes());
514    buf.extend_from_slice(&[0x24]);
515    buf.extend_from_slice(&sequence.to_be_bytes());
516    buf.push(0x61);
517    buf.extend_from_slice(&encode_xrp_amount(amount_drops));
518    buf.push(0x68);
519    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
520    buf.extend_from_slice(&[0x50, 0x2A]);
521    buf.extend_from_slice(nftoken_id);
522    buf.extend_from_slice(&[0x81, 0x14]);
523    buf.extend_from_slice(account);
524    if let Some(dest) = destination {
525        buf.extend_from_slice(&[0x83, 0x14]);
526        buf.extend_from_slice(dest);
527    }
528    buf
529}
530
531/// Serialize an NFTokenAcceptOffer transaction.
532pub fn nftoken_accept_offer(
533    account: &[u8; 20],
534    sell_offer: Option<&[u8; 32]>,
535    buy_offer: Option<&[u8; 32]>,
536    sequence: u32,
537    fee_drops: u64,
538) -> Vec<u8> {
539    let mut buf = Vec::with_capacity(100);
540    buf.extend_from_slice(&[
541        0x12,
542        (TT_NFTOKEN_ACCEPT_OFFER >> 8) as u8,
543        TT_NFTOKEN_ACCEPT_OFFER as u8,
544    ]);
545    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
546    buf.extend_from_slice(&[0x24]);
547    buf.extend_from_slice(&sequence.to_be_bytes());
548    buf.push(0x68);
549    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
550    if let Some(offer) = sell_offer {
551        buf.extend_from_slice(&[0x50, 0x29]);
552        buf.extend_from_slice(offer);
553    }
554    if let Some(offer) = buy_offer {
555        buf.extend_from_slice(&[0x50, 0x28]);
556        buf.extend_from_slice(offer);
557    }
558    buf.extend_from_slice(&[0x81, 0x14]);
559    buf.extend_from_slice(account);
560    buf
561}
562
563/// Serialize an NFTokenBurn transaction.
564pub fn nftoken_burn(
565    account: &[u8; 20],
566    nftoken_id: &[u8; 32],
567    sequence: u32,
568    fee_drops: u64,
569) -> Vec<u8> {
570    let mut buf = Vec::with_capacity(80);
571    buf.extend_from_slice(&[0x12, (TT_NFTOKEN_BURN >> 8) as u8, TT_NFTOKEN_BURN as u8]);
572    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
573    buf.extend_from_slice(&[0x24]);
574    buf.extend_from_slice(&sequence.to_be_bytes());
575    buf.push(0x68);
576    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
577    buf.extend_from_slice(&[0x50, 0x2A]);
578    buf.extend_from_slice(nftoken_id);
579    buf.extend_from_slice(&[0x81, 0x14]);
580    buf.extend_from_slice(account);
581    buf
582}
583
584// ═══════════════════════════════════════════════════════════════════
585// Checks
586// ═══════════════════════════════════════════════════════════════════
587
588/// Transaction type code for CheckCreate.
589pub const TT_CHECK_CREATE: u16 = 16;
590/// Transaction type code for CheckCash.
591pub const TT_CHECK_CASH: u16 = 17;
592/// Transaction type code for CheckCancel.
593pub const TT_CHECK_CANCEL: u16 = 18;
594
595/// Serialize a CheckCreate transaction.
596pub fn check_create(
597    account: &[u8; 20],
598    destination: &[u8; 20],
599    send_max_drops: u64,
600    sequence: u32,
601    fee_drops: u64,
602) -> Vec<u8> {
603    let mut buf = Vec::with_capacity(80);
604    buf.extend_from_slice(&[0x12, (TT_CHECK_CREATE >> 8) as u8, TT_CHECK_CREATE as u8]);
605    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
606    buf.extend_from_slice(&[0x24]);
607    buf.extend_from_slice(&sequence.to_be_bytes());
608    buf.push(0x69);
609    buf.extend_from_slice(&encode_xrp_amount(send_max_drops));
610    buf.push(0x68);
611    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
612    buf.extend_from_slice(&[0x81, 0x14]);
613    buf.extend_from_slice(account);
614    buf.extend_from_slice(&[0x83, 0x14]);
615    buf.extend_from_slice(destination);
616    buf
617}
618
619/// Serialize a CheckCash transaction.
620pub fn check_cash(
621    account: &[u8; 20],
622    check_id: &[u8; 32],
623    amount_drops: u64,
624    sequence: u32,
625    fee_drops: u64,
626) -> Vec<u8> {
627    let mut buf = Vec::with_capacity(80);
628    buf.extend_from_slice(&[0x12, (TT_CHECK_CASH >> 8) as u8, TT_CHECK_CASH as u8]);
629    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
630    buf.extend_from_slice(&[0x24]);
631    buf.extend_from_slice(&sequence.to_be_bytes());
632    buf.push(0x61);
633    buf.extend_from_slice(&encode_xrp_amount(amount_drops));
634    buf.push(0x68);
635    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
636    buf.extend_from_slice(&[0x50, 0x18]);
637    buf.extend_from_slice(check_id);
638    buf.extend_from_slice(&[0x81, 0x14]);
639    buf.extend_from_slice(account);
640    buf
641}
642
643/// Serialize a CheckCancel transaction.
644pub fn check_cancel(
645    account: &[u8; 20],
646    check_id: &[u8; 32],
647    sequence: u32,
648    fee_drops: u64,
649) -> Vec<u8> {
650    let mut buf = Vec::with_capacity(70);
651    buf.extend_from_slice(&[0x12, (TT_CHECK_CANCEL >> 8) as u8, TT_CHECK_CANCEL as u8]);
652    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
653    buf.extend_from_slice(&[0x24]);
654    buf.extend_from_slice(&sequence.to_be_bytes());
655    buf.push(0x68);
656    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
657    buf.extend_from_slice(&[0x50, 0x18]);
658    buf.extend_from_slice(check_id);
659    buf.extend_from_slice(&[0x81, 0x14]);
660    buf.extend_from_slice(account);
661    buf
662}
663
664// ═══════════════════════════════════════════════════════════════════
665// Hooks (SetHook)
666// ═══════════════════════════════════════════════════════════════════
667
668/// Transaction type code for SetHook.
669pub const TT_SET_HOOK: u16 = 22;
670
671/// Serialize a SetHook transaction (basic form).
672pub fn set_hook(
673    account: &[u8; 20],
674    hook_hash: &[u8; 32],
675    sequence: u32,
676    fee_drops: u64,
677) -> Vec<u8> {
678    let mut buf = Vec::with_capacity(100);
679    buf.extend_from_slice(&[0x12, (TT_SET_HOOK >> 8) as u8, TT_SET_HOOK as u8]);
680    buf.extend_from_slice(&[0x22, 0x00, 0x00, 0x00, 0x00]);
681    buf.extend_from_slice(&[0x24]);
682    buf.extend_from_slice(&sequence.to_be_bytes());
683    buf.push(0x68);
684    buf.extend_from_slice(&encode_xrp_amount(fee_drops));
685    buf.extend_from_slice(&[0x50, 0x20]);
686    buf.extend_from_slice(hook_hash);
687    buf.extend_from_slice(&[0x81, 0x14]);
688    buf.extend_from_slice(account);
689    buf
690}
691
692// ═══════════════════════════════════════════════════════════════════
693// Tests
694// ═══════════════════════════════════════════════════════════════════
695
696#[cfg(test)]
697#[allow(clippy::unwrap_used, clippy::expect_used)]
698mod tests {
699    use super::*;
700
701    const ACCOUNT: [u8; 20] = [0x01; 20];
702    const DEST: [u8; 20] = [0x02; 20];
703
704    // ─── IOU Amount Tests ───────────────────────────────────────
705
706    #[test]
707    fn test_iou_encode_decode_roundtrip() {
708        let encoded = encode_iou_amount(1_000_000_000_000_000, 0, true);
709        let (m, e, pos) = decode_iou_amount(&encoded);
710        assert!(pos);
711        // After normalization, mantissa * 10^exponent should represent same value
712        let original = 1_000_000_000_000_000u128 * 10u128.pow(0);
713        let decoded = m as u128 * 10u128.pow((e + 97 - 97) as u32);
714        // Values should be in the same order of magnitude
715        assert!(m > 0);
716    }
717
718    #[test]
719    fn test_iou_zero() {
720        let encoded = encode_iou_amount(0, 0, true);
721        assert_eq!(encoded[0] & 0x80, 0x80); // Not XRP flag
722        let (m, _, _) = decode_iou_amount(&encoded);
723        assert_eq!(m, 0);
724    }
725
726    #[test]
727    fn test_iou_negative() {
728        let encoded = encode_iou_amount(1_000_000_000_000_000, 0, false);
729        let (_, _, pos) = decode_iou_amount(&encoded);
730        assert!(!pos);
731    }
732
733    // ─── Currency Code Tests ────────────────────────────────────
734
735    #[test]
736    fn test_currency_code_usd() {
737        let cc = encode_currency_code("USD").unwrap();
738        assert_eq!(&cc[12..15], b"USD");
739        assert_eq!(&cc[..12], &[0u8; 12]);
740    }
741
742    #[test]
743    fn test_currency_code_xrp_rejected() {
744        assert!(encode_currency_code("XRP").is_err());
745    }
746
747    #[test]
748    fn test_currency_code_wrong_length() {
749        assert!(encode_currency_code("US").is_err());
750        assert!(encode_currency_code("USDC").is_err());
751    }
752
753    // ─── OfferCreate Tests ──────────────────────────────────────
754
755    #[test]
756    fn test_offer_create_serialization() {
757        let tx = offer_create(&ACCOUNT, 1_000_000, 500_000, 42, 12, 0);
758        assert!(!tx.is_empty());
759        // Transaction type should be 7
760        assert_eq!(tx[0], 0x12);
761        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_OFFER_CREATE);
762    }
763
764    #[test]
765    fn test_offer_cancel_serialization() {
766        let tx = offer_cancel(&ACCOUNT, 10, 43, 12);
767        assert!(!tx.is_empty());
768        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_OFFER_CANCEL);
769    }
770
771    // ─── Escrow Tests ───────────────────────────────────────────
772
773    #[test]
774    fn test_escrow_create_serialization() {
775        let tx = escrow_create(
776            &ACCOUNT,
777            &DEST,
778            1_000_000,
779            1700000000,
780            Some(1700100000),
781            44,
782            12,
783        );
784        assert!(!tx.is_empty());
785        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_ESCROW_CREATE);
786    }
787
788    #[test]
789    fn test_escrow_create_no_cancel() {
790        let tx1 = escrow_create(&ACCOUNT, &DEST, 1_000_000, 1700000000, None, 44, 12);
791        let tx2 = escrow_create(
792            &ACCOUNT,
793            &DEST,
794            1_000_000,
795            1700000000,
796            Some(1700100000),
797            44,
798            12,
799        );
800        // With cancel_after should be longer
801        assert!(tx2.len() > tx1.len());
802    }
803
804    #[test]
805    fn test_escrow_finish_serialization() {
806        let tx = escrow_finish(&ACCOUNT, &DEST, 44, 45, 12);
807        assert!(!tx.is_empty());
808        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_ESCROW_FINISH);
809    }
810
811    #[test]
812    fn test_escrow_cancel_serialization() {
813        let tx = escrow_cancel(&ACCOUNT, &DEST, 44, 46, 12);
814        assert!(!tx.is_empty());
815        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_ESCROW_CANCEL);
816    }
817
818    // ─── XRP Amount Encoding ────────────────────────────────────
819
820    #[test]
821    fn test_xrp_amount_encoding() {
822        let amt = encode_xrp_amount(1_000_000);
823        assert_eq!(amt[0] & 0x40, 0x40);
824        assert_eq!(amt[0] & 0x80, 0x00);
825    }
826
827    // ─── AccountSet Tests ───────────────────────────────────────
828
829    #[test]
830    fn test_account_set_with_flag() {
831        let tx = account_set(
832            &ACCOUNT,
833            Some(account_set_flags::ASF_REQUIRE_DEST),
834            None,
835            1,
836            12,
837        );
838        assert!(!tx.is_empty());
839        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_ACCOUNT_SET);
840    }
841
842    #[test]
843    fn test_account_set_clear_flag() {
844        let tx = account_set(
845            &ACCOUNT,
846            None,
847            Some(account_set_flags::ASF_DISALLOW_XRP),
848            2,
849            12,
850        );
851        assert!(!tx.is_empty());
852    }
853
854    #[test]
855    fn test_account_set_no_flags() {
856        let tx = account_set(&ACCOUNT, None, None, 3, 12);
857        assert!(!tx.is_empty());
858    }
859
860    // ─── Payment Channel Tests ──────────────────────────────────
861
862    #[test]
863    fn test_channel_create() {
864        let pk = [0x02; 33];
865        let tx = channel_create(&ACCOUNT, &DEST, 10_000_000, 3600, &pk, 1, 12);
866        assert!(!tx.is_empty());
867        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_CHANNEL_CREATE);
868    }
869
870    #[test]
871    fn test_channel_fund() {
872        let channel = [0xAA; 32];
873        let tx = channel_fund(&ACCOUNT, &channel, 5_000_000, 2, 12);
874        assert!(!tx.is_empty());
875        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_CHANNEL_FUND);
876    }
877
878    #[test]
879    fn test_channel_claim() {
880        let channel = [0xBB; 32];
881        let tx = channel_claim(&ACCOUNT, &channel, Some(1_000_000), 3, 12);
882        assert!(!tx.is_empty());
883        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_CHANNEL_CLAIM);
884    }
885
886    // ─── NFToken (XLS-20) Tests ─────────────────────────────────
887
888    #[test]
889    fn test_nftoken_mint() {
890        let tx = nftoken_mint(
891            &ACCOUNT,
892            0,
893            nftoken_flags::TF_TRANSFERABLE,
894            Some(b"ipfs://QmTest"),
895            1,
896            12,
897        );
898        assert!(!tx.is_empty());
899        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_NFTOKEN_MINT);
900    }
901
902    #[test]
903    fn test_nftoken_mint_no_uri() {
904        let tx1 = nftoken_mint(&ACCOUNT, 1, 0, None, 1, 12);
905        let tx2 = nftoken_mint(&ACCOUNT, 1, 0, Some(b"test"), 1, 12);
906        assert!(tx2.len() > tx1.len());
907    }
908
909    #[test]
910    fn test_nftoken_create_offer() {
911        let nft_id = [0xAA; 32];
912        let tx = nftoken_create_offer(&ACCOUNT, &nft_id, 1_000_000, 0, Some(&DEST), 1, 12);
913        assert!(!tx.is_empty());
914        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_NFTOKEN_CREATE_OFFER);
915    }
916
917    #[test]
918    fn test_nftoken_accept_offer() {
919        let offer = [0xBB; 32];
920        let tx = nftoken_accept_offer(&ACCOUNT, Some(&offer), None, 1, 12);
921        assert!(!tx.is_empty());
922        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_NFTOKEN_ACCEPT_OFFER);
923    }
924
925    #[test]
926    fn test_nftoken_burn() {
927        let nft_id = [0xCC; 32];
928        let tx = nftoken_burn(&ACCOUNT, &nft_id, 1, 12);
929        assert!(!tx.is_empty());
930        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_NFTOKEN_BURN);
931    }
932
933    // ─── Check Tests ────────────────────────────────────────────
934
935    #[test]
936    fn test_check_create() {
937        let tx = check_create(&ACCOUNT, &DEST, 5_000_000, 1, 12);
938        assert!(!tx.is_empty());
939        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_CHECK_CREATE);
940    }
941
942    #[test]
943    fn test_check_cash() {
944        let check_id = [0xDD; 32];
945        let tx = check_cash(&ACCOUNT, &check_id, 5_000_000, 2, 12);
946        assert!(!tx.is_empty());
947        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_CHECK_CASH);
948    }
949
950    #[test]
951    fn test_check_cancel() {
952        let check_id = [0xEE; 32];
953        let tx = check_cancel(&ACCOUNT, &check_id, 3, 12);
954        assert!(!tx.is_empty());
955        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_CHECK_CANCEL);
956    }
957
958    // ─── Hook Tests ─────────────────────────────────────────────
959
960    #[test]
961    fn test_set_hook() {
962        let hook_hash = [0xFF; 32];
963        let tx = set_hook(&ACCOUNT, &hook_hash, 1, 12);
964        assert!(!tx.is_empty());
965        assert_eq!(u16::from_be_bytes([tx[1], tx[2]]), TT_SET_HOOK);
966    }
967}