thru_base/
txn_tools.rs

1use crate::{
2    StateProof,
3    tn_public_address::tn_pubkey_to_address_string,
4    txn_lib::{TnPubkey, Transaction},
5};
6use anyhow::Result;
7use hex;
8use std::collections::HashMap;
9
10/// No-op program identifier (32-byte array with 0x03 in the last byte)
11pub const NOOP_PROGRAM: [u8; 32] = {
12    let mut arr = [0u8; 32];
13    arr[31] = 0x03;
14    arr
15};
16pub const SYSTEM_PROGRAM: [u8; 32] = {
17    let mut arr = [0u8; 32];
18    arr[31] = 0x01;
19    arr
20};
21pub const EOA_PROGRAM: [u8; 32] = {
22    let arr = [0u8; 32];
23    arr
24};
25
26pub const UPLOADER_PROGRAM: [u8; 32] = {
27    let mut arr = [0u8; 32];
28    arr[31] = 0x02;
29    arr
30};
31pub const FAUCET_PROGRAM: [u8; 32] = {
32    let mut arr = [0u8; 32];
33    arr[31] = 0xFA;
34    arr
35};
36
37#[derive(Debug, Clone)]
38pub struct TransactionBuilder {
39    // fee_payer: FdPubkey,
40    // program: FdPubkey,
41    // fee: u64,
42    // nonce: u64,
43}
44
45impl TransactionBuilder {
46    /// Build balance transfer transaction
47    pub fn build_create_with_fee_payer_proof(
48        fee_payer: TnPubkey,
49        start_slot: u64,
50        fee_payer_state_proof: &StateProof,
51    ) -> Result<Transaction> {
52        let tx = Transaction::new(fee_payer, NOOP_PROGRAM, 0, 0)
53            .with_fee_payer_state_proof(fee_payer_state_proof)
54            .with_start_slot(start_slot)
55            .with_expiry_after(100)
56            .with_compute_units(10_000)
57            .with_memory_units(10_000)
58            .with_state_units(10_000);
59        Ok(tx)
60    }
61
62    /// Build balance transfer transaction for EOA program (tn_eoa_program.c)
63    ///
64    /// This creates a transaction that calls the TRANSFER instruction of the EOA program,
65    /// which transfers balance from one account to another.
66    ///
67    /// # Arguments
68    /// * `fee_payer` - The account paying the transaction fee (also the from_account for the transfer)
69    /// * `program` - The EOA program pubkey (typically EOA_PROGRAM constant = all zeros)
70    /// * `to_account` - The destination account receiving the transfer
71    /// * `amount` - The amount to transfer
72    /// * `fee` - Transaction fee
73    /// * `nonce` - Account nonce
74    /// * `start_slot` - Starting slot for transaction validity
75    pub fn build_transfer(
76        fee_payer: TnPubkey,
77        program: TnPubkey,
78        to_account: TnPubkey,
79        amount: u64,
80        fee: u64,
81        nonce: u64,
82        start_slot: u64,
83    ) -> Result<Transaction> {
84        // Create transfer instruction data for EOA program
85        // Account layout: [0: fee_payer/from_account, 1: program, 2: to_account]
86        let from_account_idx = 0u16; // fee_payer is also the from_account
87        let to_account_idx = 2u16; // to_account added via add_rw_account
88        let instruction_data =
89            build_transfer_instruction(from_account_idx, to_account_idx, amount)?;
90
91        let tx = Transaction::new(fee_payer, program, fee, nonce)
92            .with_start_slot(start_slot)
93            .add_rw_account(to_account) // Destination account (receives transfer)
94            .with_instructions(instruction_data)
95            .with_expiry_after(100)
96            .with_compute_units(10000)
97            .with_memory_units(10000)
98            .with_state_units(10000);
99
100        Ok(tx)
101    }
102
103    /// Build regular account creation transaction (with optional state proof)
104    pub fn build_create_account(
105        fee_payer: TnPubkey,
106        program: TnPubkey,
107        target_account: TnPubkey,
108        seed: &str,
109        state_proof: Option<&[u8]>,
110        fee: u64,
111        nonce: u64,
112        start_slot: u64,
113    ) -> Result<Transaction> {
114        // Account layout: [0: fee_payer, 1: program, 2: target_account]
115        let target_account_idx = 2u16; // target_account added via add_rw_account
116        let instruction_data =
117            build_create_account_instruction(target_account_idx, seed, state_proof)?;
118
119        let tx = Transaction::new(fee_payer, program, fee, nonce)
120            .with_start_slot(start_slot)
121            .add_rw_account(target_account)
122            .with_instructions(instruction_data)
123            .with_expiry_after(100)
124            .with_compute_units(10_000)
125            .with_memory_units(10_000)
126            .with_state_units(10_000);
127
128        Ok(tx)
129    }
130
131    /// Build account creation transaction
132    pub fn build_create_ephemeral_account(
133        fee_payer: TnPubkey,
134        program: TnPubkey,
135        target_account: TnPubkey,
136        seed: &[u8; 32],
137        fee: u64,
138        nonce: u64,
139        start_slot: u64,
140    ) -> Result<Transaction> {
141        // Account layout: [0: fee_payer, 1: program, 2: target_account]
142        let target_account_idx = 2u16; // target_account added via add_rw_account
143        let instruction_data = build_ephemeral_account_instruction(target_account_idx, seed)?;
144
145        let tx = Transaction::new(fee_payer, program, fee, nonce)
146            .with_start_slot(start_slot)
147            .add_rw_account(target_account)
148            .with_instructions(instruction_data)
149            .with_expiry_after(100)
150            .with_compute_units(50_000)
151            .with_memory_units(10_000)
152            .with_state_units(10_000);
153        Ok(tx)
154    }
155
156    /// Build account resize transaction
157    pub fn build_resize_account(
158        fee_payer: TnPubkey,
159        program: TnPubkey,
160        target_account: TnPubkey,
161        new_size: u64,
162        fee: u64,
163        nonce: u64,
164        start_slot: u64,
165    ) -> Result<Transaction> {
166        // Account layout: [0: fee_payer, 1: program, 2: target_account]
167        let target_account_idx = 2u16; // target_account added via add_rw_account
168        let instruction_data = build_resize_instruction(target_account_idx, new_size)?;
169
170        let tx = Transaction::new(fee_payer, program, fee, nonce)
171            .with_start_slot(start_slot)
172            .with_expiry_after(100)
173            .with_compute_units(100032)
174            .with_state_units(1 + new_size.checked_div(4096).unwrap() as u16)
175            .with_memory_units(10000)
176            .add_rw_account(target_account)
177            .with_instructions(instruction_data)
178            .with_expiry_after(100)
179            .with_compute_units(10_000 + 2 * new_size as u32)
180            .with_memory_units(10_000)
181            .with_state_units(10_000);
182
183        Ok(tx)
184    }
185
186    /// Build account compression transaction
187    pub fn build_compress_account(
188        fee_payer: TnPubkey,
189        program: TnPubkey,
190        target_account: TnPubkey,
191        state_proof: &[u8],
192        fee: u64,
193        nonce: u64,
194        start_slot: u64,
195        account_size: u32,
196    ) -> Result<Transaction> {
197        // Account layout: [0: fee_payer, 1: program, 2: target_account]
198        let target_account_idx = 2u16; // target_account added via add_rw_account
199        let instruction_data = build_compress_instruction(target_account_idx, state_proof)?;
200
201        let tx = Transaction::new(fee_payer, program, fee, nonce)
202            .with_start_slot(start_slot)
203            .with_may_compress_account()
204            .add_rw_account(target_account)
205            .with_instructions(instruction_data)
206            .with_expiry_after(100)
207            .with_compute_units(100_300 + account_size * 2)
208            .with_memory_units(10000)
209            .with_state_units(10000);
210
211        Ok(tx)
212    }
213
214    /// Build account decompression transaction
215    pub fn build_decompress_account(
216        fee_payer: TnPubkey,
217        program: TnPubkey,
218        target_account: TnPubkey,
219        account_data: &[u8],
220        state_proof: &[u8],
221        fee: u64,
222        nonce: u64,
223        start_slot: u64,
224    ) -> Result<Transaction> {
225        // Account layout: [0: fee_payer, 1: program, 2: target_account]
226        let target_account_idx = 2u16; // target_account added via add_rw_account
227        let instruction_data =
228            build_decompress_instruction(target_account_idx, account_data, state_proof)?;
229
230        let tx = Transaction::new(fee_payer, program, fee, nonce)
231            .with_start_slot(start_slot)
232            .add_rw_account(target_account)
233            .with_instructions(instruction_data)
234            .with_compute_units(100_300 + account_data.len() as u32 * 2)
235            .with_state_units(10_000)
236            .with_memory_units(10_000)
237            .with_expiry_after(100);
238        Ok(tx)
239    }
240
241    /// Build data write transaction
242    pub fn build_write_data(
243        fee_payer: TnPubkey,
244        program: TnPubkey,
245        target_account: TnPubkey,
246        offset: u16,
247        data: &[u8],
248        fee: u64,
249        nonce: u64,
250        start_slot: u64,
251    ) -> Result<Transaction> {
252        // Account layout: [0: fee_payer, 1: program, 2: target_account]
253        let target_account_idx = 2u16; // target_account added via add_rw_account
254        let instruction_data = build_write_instruction(target_account_idx, offset, data)?;
255
256        let tx = Transaction::new(fee_payer, program, fee, nonce)
257            .with_start_slot(start_slot)
258            .with_expiry_after(100)
259            .with_compute_units(100045)
260            .with_state_units(10000)
261            .with_memory_units(10000)
262            .add_rw_account(target_account)
263            .with_instructions(instruction_data);
264
265        Ok(tx)
266    }
267}
268
269/// Build transfer instruction for EOA program (tn_eoa_program.c)
270///
271/// Instruction format (matching tn_eoa_instruction_t and tn_eoa_transfer_args_t):
272/// - Discriminant: u32 (4 bytes) = TN_EOA_INSTRUCTION_TRANSFER (1)
273/// - Amount: u64 (8 bytes)
274/// - From account index: u16 (2 bytes)
275/// - To account index: u16 (2 bytes)
276/// Total: 16 bytes
277fn build_transfer_instruction(
278    from_account_idx: u16,
279    to_account_idx: u16,
280    amount: u64,
281) -> Result<Vec<u8>> {
282    let mut instruction = Vec::new();
283
284    // TN_EOA_INSTRUCTION_TRANSFER = 1 (u32, 4 bytes little-endian)
285    instruction.extend_from_slice(&1u32.to_le_bytes());
286
287    // tn_eoa_transfer_args_t structure:
288    // - amount (u64, 8 bytes little-endian)
289    instruction.extend_from_slice(&amount.to_le_bytes());
290
291    // - from_account_idx (u16, 2 bytes little-endian)
292    instruction.extend_from_slice(&from_account_idx.to_le_bytes());
293
294    // - to_account_idx (u16, 2 bytes little-endian)
295    instruction.extend_from_slice(&to_account_idx.to_le_bytes());
296
297    Ok(instruction)
298}
299
300/// Build regular account creation instruction (TN_SYS_PROG_DISCRIMINANT_ACCOUNT_CREATE = 0x00)
301fn build_create_account_instruction(
302    target_account_idx: u16,
303    seed: &str,
304    state_proof: Option<&[u8]>,
305) -> Result<Vec<u8>> {
306    let mut instruction = Vec::new();
307
308    // TN_SYS_PROG_DISCRIMINANT_ACCOUNT_CREATE = 0x00
309    instruction.push(0x00);
310
311    // Target account index (little-endian u16) - matching tn_system_program_account_create_args_t
312    instruction.extend_from_slice(&target_account_idx.to_le_bytes());
313
314    // Seed should be hex-decoded (to match addrtool behavior)
315    let seed_bytes =
316        hex::decode(seed).map_err(|e| anyhow::anyhow!("Failed to decode hex seed: {}", e))?;
317
318    // Seed length (little-endian u64) - matching tn_system_program_account_create_args_t.seed_len
319    instruction.extend_from_slice(&(seed_bytes.len() as u64).to_le_bytes());
320
321    // has_proof flag (1 byte) - matching tn_system_program_account_create_args_t.has_proof
322    let has_proof = state_proof.is_some();
323    instruction.push(if has_proof { 1u8 } else { 0u8 });
324
325    // Seed data (seed_len bytes follow)
326    instruction.extend_from_slice(&seed_bytes);
327
328    // Proof data (if present, proof follows seed)
329    if let Some(proof) = state_proof {
330        instruction.extend_from_slice(proof);
331    }
332
333    Ok(instruction)
334}
335
336/// Build ephemeral account creation instruction
337fn build_ephemeral_account_instruction(
338    target_account_idx: u16,
339    seed: &[u8; 32],
340) -> Result<Vec<u8>> {
341    let mut instruction = Vec::new();
342
343    // TN_SYS_PROG_DISCRIMINANT_ACCOUNT_CREATE_EPHEMERAL = 01
344    instruction.push(0x01);
345
346    // Target account index (little-endian u16)
347    instruction.extend_from_slice(&target_account_idx.to_le_bytes());
348
349    // Seed length (little-endian u64)
350    instruction.extend_from_slice(&(seed.len() as u64).to_le_bytes());
351
352    // Seed data
353    instruction.extend_from_slice(seed);
354
355    Ok(instruction)
356}
357
358/// Build account resize instruction
359fn build_resize_instruction(target_account_idx: u16, new_size: u64) -> Result<Vec<u8>> {
360    let mut instruction = Vec::new();
361
362    // TN_SYS_PROG_DISCRIMINANT_ACCOUNT_RESIZE = 04
363    instruction.push(0x04);
364
365    // Target account index (little-endian u16)
366    instruction.extend_from_slice(&target_account_idx.to_le_bytes());
367
368    // New size (little-endian u64)
369    instruction.extend_from_slice(&new_size.to_le_bytes());
370
371    Ok(instruction)
372}
373
374/// Build data write instruction
375fn build_write_instruction(target_account_idx: u16, offset: u16, data: &[u8]) -> Result<Vec<u8>> {
376    let mut instruction = Vec::new();
377
378    // TN_SYS_PROG_DISCRIMINANT_WRITE = C8
379    instruction.push(0xC8);
380
381    // Target account index (little-endian u16)
382    instruction.extend_from_slice(&target_account_idx.to_le_bytes());
383
384    // Offset (little-endian u16)
385    instruction.extend_from_slice(&offset.to_le_bytes());
386
387    // Data length (little-endian u16)
388    instruction.extend_from_slice(&(data.len() as u16).to_le_bytes());
389
390    // Data
391    instruction.extend_from_slice(data);
392
393    Ok(instruction)
394}
395
396/// Build account compression instruction
397fn build_compress_instruction(target_account_idx: u16, state_proof: &[u8]) -> Result<Vec<u8>> {
398    let mut instruction = Vec::new();
399
400    // TN_SYS_PROG_DISCRIMINANT_ACCOUNT_COMPRESS - based on C test, this appears to be different from other discriminants
401    // Looking at the C test pattern and other system discriminants, compression is likely 0x05
402    instruction.push(0x05);
403
404    // Target account index (little-endian u16)
405    instruction.extend_from_slice(&target_account_idx.to_le_bytes());
406
407    // State proof bytes
408    instruction.extend_from_slice(state_proof);
409
410    Ok(instruction)
411}
412
413fn build_decompress_instruction(
414    target_account_idx: u16,
415    account_data: &[u8],
416    state_proof: &[u8],
417) -> Result<Vec<u8>> {
418    let mut instruction = Vec::new();
419
420    // TN_SYS_PROG_DISCRIMINANT_ACCOUNT_DECOMPRESS = 0x06
421    instruction.push(0x06);
422
423    // tn_system_program_account_decompress_args_t: account_idx (u16) + data_len (u64)
424    instruction.extend_from_slice(&target_account_idx.to_le_bytes());
425    instruction.extend_from_slice(&(account_data.len() as u64).to_le_bytes());
426
427    // Account data
428    instruction.extend_from_slice(account_data);
429
430    // State proof bytes
431    instruction.extend_from_slice(state_proof);
432
433    Ok(instruction)
434}
435
436/// Generate ephemeral account address from seed
437/// This replaces the `addrtool --ephemeral` functionality
438/// Based on create_program_defined_account_address from tn_vm_syscalls.c
439/// Note: For ephemeral accounts, the owner is always the system program (all zeros)
440pub fn generate_ephemeral_address(seed: &str) -> Result<String> {
441    // Owner is always system program (all zeros) for ephemeral accounts
442    let owner_pubkey = [0u8; 32];
443
444    // Convert seed string to hex bytes (addrtool expects hex-encoded seed)
445    let seed_bytes =
446        hex::decode(seed).map_err(|e| anyhow::anyhow!("Failed to decode hex seed: {}", e))?;
447
448    // Pad or truncate to exactly 32 bytes (matching C implementation)
449    let mut seed_32 = [0u8; 32];
450    let copy_len = std::cmp::min(seed_bytes.len(), 32);
451    seed_32[..copy_len].copy_from_slice(&seed_bytes[..copy_len]);
452
453    // Use the new implementation from tn_public_address
454    Ok(
455        crate::tn_public_address::create_program_defined_account_address_string(
456            &owner_pubkey,
457            true, // is_ephemeral = true
458            &seed_32,
459        ),
460    )
461}
462
463pub fn generate_system_derived_address(seed: &str, is_ephemeral: bool) -> Result<String> {
464    // Convert seed string to hex bytes (addrtool expects hex-encoded seed)
465    let seed_bytes =
466        hex::decode(seed).map_err(|e| anyhow::anyhow!("Failed to decode hex seed: {}", e))?;
467
468    let pubkey = generate_derived_address(&seed_bytes, &[0u8; 32], is_ephemeral)?;
469
470    Ok(tn_pubkey_to_address_string(&pubkey))
471}
472
473pub fn generate_derived_address(
474    seed: &[u8],
475    owner_pubkey: &[u8; 32],
476    is_ephemeral: bool,
477) -> Result<[u8; 32]> {
478    use sha2::{Digest, Sha256};
479
480    // Create SHA256 hasher
481    let mut hasher = Sha256::new();
482
483    // Hash owner pubkey (32 bytes) - system program
484    hasher.update(&owner_pubkey);
485
486    // Hash is_ephemeral flag (1 byte)
487    hasher.update(&[is_ephemeral as u8]);
488
489    // Hash seed bytes
490    hasher.update(&seed);
491
492    // Finalize hash to get 32-byte result
493    Ok(hasher.finalize().into())
494}
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    #[test]
501    fn test_ephemeral_address_generation() {
502        // Test with hex-encoded seeds (as addrtool expects)
503        let hex_seed1 = hex::encode("test_seed_123");
504        let hex_seed2 = hex::encode("test_seed_123");
505        let hex_seed3 = hex::encode("different_seed");
506
507        let addr1 = generate_ephemeral_address(&hex_seed1).unwrap();
508        let addr2 = generate_ephemeral_address(&hex_seed2).unwrap();
509        let addr3 = generate_ephemeral_address(&hex_seed3).unwrap();
510
511        // Same inputs should produce same address
512        assert_eq!(addr1, addr2);
513
514        // Different seeds should produce different addresses
515        assert_ne!(addr1, addr3);
516
517        // All addresses should be ta... format
518        assert!(addr1.starts_with("ta"));
519        assert!(addr2.starts_with("ta"));
520        assert!(addr3.starts_with("ta"));
521
522        // All addresses should be 46 characters
523        assert_eq!(addr1.len(), 46);
524        assert_eq!(addr2.len(), 46);
525        assert_eq!(addr3.len(), 46);
526    }
527
528    #[test]
529    fn test_eoa_transfer_instruction_format() {
530        // Test that the transfer instruction matches the EOA program's expected format
531        let from_idx = 0u16;
532        let to_idx = 2u16;
533        let amount = 1000u64;
534
535        let instruction = build_transfer_instruction(from_idx, to_idx, amount).unwrap();
536
537        // Expected format (matching tn_eoa_program.c):
538        // - Discriminant: u32 (4 bytes) = 1
539        // - Amount: u64 (8 bytes)
540        // - From account index: u16 (2 bytes)
541        // - To account index: u16 (2 bytes)
542        // Total: 16 bytes
543
544        assert_eq!(instruction.len(), 16, "Instruction should be 16 bytes");
545
546        // Check discriminant (TN_EOA_INSTRUCTION_TRANSFER = 1)
547        let discriminant = u32::from_le_bytes([
548            instruction[0],
549            instruction[1],
550            instruction[2],
551            instruction[3],
552        ]);
553        assert_eq!(discriminant, 1, "Discriminant should be 1 for TRANSFER");
554
555        // Check amount
556        let parsed_amount = u64::from_le_bytes([
557            instruction[4],
558            instruction[5],
559            instruction[6],
560            instruction[7],
561            instruction[8],
562            instruction[9],
563            instruction[10],
564            instruction[11],
565        ]);
566        assert_eq!(parsed_amount, amount, "Amount should match input");
567
568        // Check from_account_idx
569        let parsed_from = u16::from_le_bytes([instruction[12], instruction[13]]);
570        assert_eq!(parsed_from, from_idx, "From index should match input");
571
572        // Check to_account_idx
573        let parsed_to = u16::from_le_bytes([instruction[14], instruction[15]]);
574        assert_eq!(parsed_to, to_idx, "To index should match input");
575    }
576
577    #[test]
578    fn test_faucet_deposit_instruction_layout_with_fee_payer_depositor() {
579        let fee_payer = [1u8; 32];
580        let faucet_program = FAUCET_PROGRAM;
581        let faucet_account = [2u8; 32];
582        let depositor_account = fee_payer;
583        let amount = 500u64;
584
585        let tx = TransactionBuilder::build_faucet_deposit(
586            fee_payer,
587            faucet_program,
588            faucet_account,
589            depositor_account,
590            EOA_PROGRAM,
591            amount,
592            0,
593            42,
594            100,
595        )
596        .expect("build faucet deposit");
597
598        let rw_accs = tx.rw_accs.expect("rw accounts must exist");
599        assert_eq!(rw_accs.len(), 1);
600        assert_eq!(rw_accs[0], faucet_account);
601
602        let ro_accs = tx.r_accs.expect("ro accounts must exist");
603        assert_eq!(ro_accs.len(), 1);
604        assert_eq!(ro_accs[0], EOA_PROGRAM);
605
606        let instruction = tx.instructions.expect("instruction bytes must exist");
607        assert_eq!(instruction.len(), 18, "Deposit instruction must be 18 bytes");
608
609        let discriminant =
610            u32::from_le_bytes([instruction[0], instruction[1], instruction[2], instruction[3]]);
611        assert_eq!(discriminant, 0, "Deposit discriminant should be 0");
612
613        let faucet_idx = u16::from_le_bytes([instruction[4], instruction[5]]);
614        let depositor_idx = u16::from_le_bytes([instruction[6], instruction[7]]);
615        let eoa_idx = u16::from_le_bytes([instruction[8], instruction[9]]);
616        let parsed_amount = u64::from_le_bytes([
617            instruction[10],
618            instruction[11],
619            instruction[12],
620            instruction[13],
621            instruction[14],
622            instruction[15],
623            instruction[16],
624            instruction[17],
625        ]);
626
627        assert_eq!(faucet_idx, 2, "Faucet account should be first RW account");
628        assert_eq!(depositor_idx, 0, "Depositor shares the fee payer index");
629        assert_eq!(eoa_idx, 3, "EOA program should follow RW accounts");
630        assert_eq!(parsed_amount, amount, "Amount should match input");
631    }
632
633    #[test]
634    fn test_build_token_initialize_mint() {
635        // Create test keypairs and addresses
636        let fee_payer = [1u8; 32];
637        let token_program = [2u8; 32];
638        let mint_account = [3u8; 32];
639        let creator = [4u8; 32];
640        let mint_authority = [5u8; 32];
641        let freeze_authority = [6u8; 32];
642
643        let decimals = 9u8;
644        let ticker = "TEST";
645        let seed = [7u8; 32];
646        let state_proof = vec![8u8; 64];
647
648        // Test with freeze authority
649        let result = TransactionBuilder::build_token_initialize_mint(
650            fee_payer,
651            token_program,
652            mint_account,
653            creator,
654            mint_authority,
655            Some(freeze_authority),
656            decimals,
657            ticker,
658            seed,
659            state_proof.clone(),
660            1000, // fee
661            1,    // nonce
662            100,  // start_slot
663        );
664
665        assert!(result.is_ok(), "Should build valid transaction with freeze authority");
666        let tx = result.unwrap();
667        assert!(tx.instructions.is_some(), "Transaction should have instructions");
668
669        // Test without freeze authority
670        let result_no_freeze = TransactionBuilder::build_token_initialize_mint(
671            fee_payer,
672            token_program,
673            mint_account,
674            creator,
675            mint_authority,
676            None,
677            decimals,
678            ticker,
679            seed,
680            state_proof,
681            1000,
682            1,
683            100,
684        );
685
686        assert!(result_no_freeze.is_ok(), "Should build valid transaction without freeze authority");
687    }
688
689    #[test]
690    fn test_build_token_initialize_mint_instruction_format() {
691        let mint_account_idx = 2u16;
692        let decimals = 9u8;
693        let creator = [1u8; 32];
694        let mint_authority = [2u8; 32];
695        let freeze_authority = [3u8; 32];
696        let ticker = "TST";
697        let seed = [4u8; 32];
698        let state_proof = vec![5u8; 10];
699
700        let instruction = build_token_initialize_mint_instruction(
701            mint_account_idx,
702            decimals,
703            creator,
704            mint_authority,
705            Some(freeze_authority),
706            ticker,
707            seed,
708            state_proof.clone(),
709        )
710        .unwrap();
711
712        // Verify instruction structure
713        // Tag (1) + mint_account_idx (2) + decimals (1) + creator (32) + mint_authority (32) + 
714        // freeze_authority (32) + has_freeze_authority (1) + ticker_len (1) + ticker_bytes (8) + seed (32) + proof
715        let expected_min_size = 1 + 2 + 1 + 32 + 32 + 32 + 1 + 1 + 8 + 32 + state_proof.len();
716        assert_eq!(instruction.len(), expected_min_size);
717
718        // Verify tag
719        assert_eq!(instruction[0], 0, "First byte should be InitializeMint tag (0)");
720
721        // Verify mint account index
722        let parsed_idx = u16::from_le_bytes([instruction[1], instruction[2]]);
723        assert_eq!(parsed_idx, mint_account_idx);
724
725        // Verify decimals
726        assert_eq!(instruction[3], decimals);
727
728        // Verify creator is at correct position (bytes 4-35)
729        assert_eq!(&instruction[4..36], &creator);
730
731        // Verify mint_authority is at correct position (bytes 36-67)
732        assert_eq!(&instruction[36..68], &mint_authority);
733
734        // Verify freeze_authority is at correct position (bytes 68-99)
735        assert_eq!(&instruction[68..100], &freeze_authority);
736
737        // Verify has_freeze_authority flag
738        assert_eq!(instruction[100], 1);
739    }
740
741    #[test]
742    fn test_token_initialize_mint_creator_vs_mint_authority() {
743        // Test that creator and mint_authority can be different
744        let fee_payer = [1u8; 32];
745        let token_program = [2u8; 32];
746        let mint_account = [3u8; 32];
747        let creator = [4u8; 32];
748        let mint_authority = [5u8; 32]; // Different from creator
749        let seed = [6u8; 32];
750        let state_proof = vec![7u8; 32];
751
752        let result = TransactionBuilder::build_token_initialize_mint(
753            fee_payer,
754            token_program,
755            mint_account,
756            creator,
757            mint_authority,
758            None,
759            9,
760            "TEST",
761            seed,
762            state_proof,
763            1000,
764            1,
765            100,
766        );
767
768        assert!(result.is_ok(), "Should allow different creator and mint_authority");
769
770        // Test that creator and mint_authority can be the same
771        let result_same = TransactionBuilder::build_token_initialize_mint(
772            fee_payer,
773            token_program,
774            mint_account,
775            creator,
776            creator, // Same as creator
777            None,
778            9,
779            "TEST",
780            seed,
781            vec![7u8; 32],
782            1000,
783            1,
784            100,
785        );
786
787        assert!(result_same.is_ok(), "Should allow same creator and mint_authority");
788    }
789}
790
791/// Uploader program instruction discriminants
792pub const TN_UPLOADER_PROGRAM_INSTRUCTION_CREATE: u32 = 0x00;
793pub const TN_UPLOADER_PROGRAM_INSTRUCTION_WRITE: u32 = 0x01;
794pub const TN_UPLOADER_PROGRAM_INSTRUCTION_DESTROY: u32 = 0x02;
795pub const TN_UPLOADER_PROGRAM_INSTRUCTION_FINALIZE: u32 = 0x03;
796
797/// Uploader program CREATE instruction arguments (matches C struct)
798#[repr(C, packed)]
799#[derive(Debug, Clone, Copy)]
800pub struct UploaderCreateArgs {
801    pub buffer_account_idx: u16,
802    pub meta_account_idx: u16,
803    pub authority_account_idx: u16,
804    pub buffer_account_sz: u32,
805    pub expected_account_hash: [u8; 32],
806    pub seed_len: u32,
807    // seed bytes follow
808}
809
810/// Uploader program WRITE instruction arguments (matches C struct)
811#[repr(C, packed)]
812#[derive(Debug, Clone, Copy)]
813pub struct UploaderWriteArgs {
814    pub buffer_account_idx: u16,
815    pub meta_account_idx: u16,
816    pub data_len: u32,
817    pub data_offset: u32,
818    // data bytes follow
819}
820
821/// Uploader program FINALIZE instruction arguments (matches C struct)
822#[repr(C, packed)]
823#[derive(Debug, Clone, Copy)]
824pub struct UploaderFinalizeArgs {
825    pub buffer_account_idx: u16,
826    pub meta_account_idx: u16,
827    pub expected_account_hash: [u8; 32],
828}
829
830/// Uploader program DESTROY instruction arguments (matches C struct)
831#[repr(C, packed)]
832#[derive(Debug, Clone, Copy)]
833pub struct UploaderDestroyArgs {
834    pub buffer_account_idx: u16,
835    pub meta_account_idx: u16,
836}
837
838/// Manager program instruction discriminants (matches C defines)
839pub const MANAGER_INSTRUCTION_CREATE_PERMANENT: u8 = 0x00;
840pub const MANAGER_INSTRUCTION_CREATE_EPHEMERAL: u8 = 0x01;
841pub const MANAGER_INSTRUCTION_UPGRADE: u8 = 0x02;
842pub const MANAGER_INSTRUCTION_SET_PAUSE: u8 = 0x03;
843pub const MANAGER_INSTRUCTION_DESTROY: u8 = 0x04;
844pub const MANAGER_INSTRUCTION_FINALIZE: u8 = 0x05;
845pub const MANAGER_INSTRUCTION_SET_AUTHORITY: u8 = 0x06;
846pub const MANAGER_INSTRUCTION_CLAIM_AUTHORITY: u8 = 0x07;
847
848/// Manager program header arguments (matches C struct)
849#[repr(C, packed)]
850#[derive(Debug, Clone, Copy)]
851pub struct ManagerHeaderArgs {
852    pub discriminant: u8,
853    pub meta_account_idx: u16,
854    pub program_account_idx: u16,
855}
856
857/// Manager program CREATE instruction arguments (matches C struct)
858#[repr(C, packed)]
859#[derive(Debug, Clone, Copy)]
860pub struct ManagerCreateArgs {
861    pub discriminant: u8,
862    pub meta_account_idx: u16,
863    pub program_account_idx: u16,
864    pub srcbuf_account_idx: u16,
865    pub srcbuf_offset: u32,
866    pub srcbuf_size: u32,
867    pub authority_account_idx: u16,
868    pub seed_len: u32,
869    // seed bytes and proof bytes follow
870}
871
872/// Manager program UPGRADE instruction arguments (matches C struct)
873#[repr(C, packed)]
874#[derive(Debug, Clone, Copy)]
875pub struct ManagerUpgradeArgs {
876    pub discriminant: u8,
877    pub meta_account_idx: u16,
878    pub program_account_idx: u16,
879    pub srcbuf_account_idx: u16,
880    pub srcbuf_offset: u32,
881    pub srcbuf_size: u32,
882}
883
884/// Manager program SET_PAUSE instruction arguments (matches C struct)
885#[repr(C, packed)]
886#[derive(Debug, Clone, Copy)]
887pub struct ManagerSetPauseArgs {
888    pub discriminant: u8,
889    pub meta_account_idx: u16,
890    pub program_account_idx: u16,
891    pub is_paused: u8,
892}
893
894/// Manager program SET_AUTHORITY instruction arguments (matches C struct)
895#[repr(C, packed)]
896#[derive(Debug, Clone, Copy)]
897pub struct ManagerSetAuthorityArgs {
898    pub discriminant: u8,
899    pub meta_account_idx: u16,
900    pub program_account_idx: u16,
901    pub authority_candidate: [u8; 32],
902}
903
904/// Test uploader program instruction discriminants (matches C defines)
905pub const TN_TEST_UPLOADER_PROGRAM_DISCRIMINANT_CREATE: u8 = 0x00;
906pub const TN_TEST_UPLOADER_PROGRAM_DISCRIMINANT_WRITE: u8 = 0x01;
907
908/// Test uploader program CREATE instruction arguments (matches C struct)
909#[repr(C, packed)]
910#[derive(Debug, Clone, Copy)]
911pub struct TestUploaderCreateArgs {
912    pub account_idx: u16,
913    pub is_ephemeral: u8,
914    pub account_sz: u32,
915    pub seed_len: u32,
916    // seed bytes follow, then optional state proof
917}
918
919/// Test uploader program WRITE instruction arguments (matches C struct)
920#[repr(C, packed)]
921#[derive(Debug, Clone, Copy)]
922pub struct TestUploaderWriteArgs {
923    pub target_account_idx: u16,
924    pub target_offset: u32,
925    pub data_len: u32,
926    // data bytes follow
927}
928
929/// System program DECOMPRESS2 instruction arguments (matches C struct)
930#[repr(C, packed)]
931#[derive(Debug, Clone, Copy)]
932pub struct SystemProgramDecompress2Args {
933    pub target_account_idx: u16,
934    pub meta_account_idx: u16,
935    pub data_account_idx: u16,
936    pub data_offset: u32,
937}
938
939impl TransactionBuilder {
940    /// Build uploader program CREATE transaction
941    pub fn build_uploader_create(
942        fee_payer: TnPubkey,
943        uploader_program: TnPubkey,
944        meta_account: TnPubkey,
945        buffer_account: TnPubkey,
946        buffer_size: u32,
947        expected_hash: [u8; 32],
948        seed: &[u8],
949        fee: u64,
950        nonce: u64,
951        start_slot: u64,
952    ) -> Result<Transaction> {
953        // Account layout: [0: fee_payer, 1: uploader_program, 2: meta_account, 3: buffer_account]
954        let authority_account_idx = 0u16;
955
956        let mut tx = Transaction::new(fee_payer, uploader_program, fee, nonce)
957            .with_start_slot(start_slot)
958            .with_expiry_after(10)
959            .with_compute_units(50_000 + 2 * buffer_size as u32)
960            .with_memory_units(10_000)
961            .with_state_units(10_000);
962
963        let mut meta_account_idx = 2u16;
964        let mut buffer_account_idx = 3u16;
965        if meta_account > buffer_account {
966            meta_account_idx = 3u16;
967            buffer_account_idx = 2u16;
968            tx = tx
969                .add_rw_account(buffer_account)
970                .add_rw_account(meta_account)
971        } else {
972            tx = tx
973                .add_rw_account(meta_account)
974                .add_rw_account(buffer_account)
975        }
976
977        let instruction_data = build_uploader_create_instruction(
978            buffer_account_idx,
979            meta_account_idx,
980            authority_account_idx,
981            buffer_size,
982            expected_hash,
983            seed,
984        )?;
985
986        tx = tx.with_instructions(instruction_data);
987
988        Ok(tx)
989    }
990
991    /// Build uploader program WRITE transaction
992    pub fn build_uploader_write(
993        fee_payer: TnPubkey,
994        uploader_program: TnPubkey,
995        meta_account: TnPubkey,
996        buffer_account: TnPubkey,
997        data: &[u8],
998        offset: u32,
999        fee: u64,
1000        nonce: u64,
1001        start_slot: u64,
1002    ) -> Result<Transaction> {
1003        // Account layout: [0: fee_payer, 1: uploader_program, 2: meta_account, 3: buffer_account]
1004        let mut tx = Transaction::new(fee_payer, uploader_program, fee, nonce)
1005            .with_start_slot(start_slot)
1006            .with_expiry_after(10000)
1007            .with_compute_units(500_000_000)
1008            .with_memory_units(5000)
1009            .with_state_units(5000);
1010
1011        let mut meta_account_idx = 2u16;
1012        let mut buffer_account_idx = 3u16;
1013        if meta_account > buffer_account {
1014            meta_account_idx = 3u16;
1015            buffer_account_idx = 2u16;
1016            tx = tx
1017                .add_rw_account(buffer_account)
1018                .add_rw_account(meta_account)
1019        } else {
1020            tx = tx
1021                .add_rw_account(meta_account)
1022                .add_rw_account(buffer_account)
1023        }
1024
1025        let instruction_data =
1026            build_uploader_write_instruction(buffer_account_idx, meta_account_idx, data, offset)?;
1027
1028        tx = tx.with_instructions(instruction_data);
1029
1030        Ok(tx)
1031    }
1032
1033    /// Build uploader program FINALIZE transaction
1034    pub fn build_uploader_finalize(
1035        fee_payer: TnPubkey,
1036        uploader_program: TnPubkey,
1037        meta_account: TnPubkey,
1038        buffer_account: TnPubkey,
1039        buffer_size: u32,
1040        expected_hash: [u8; 32],
1041        fee: u64,
1042        nonce: u64,
1043        start_slot: u64,
1044    ) -> Result<Transaction> {
1045        let mut tx = Transaction::new(fee_payer, uploader_program, fee, nonce)
1046            .with_start_slot(start_slot)
1047            .with_expiry_after(10000)
1048            .with_compute_units(50_000 + 200 * buffer_size as u32)
1049            .with_memory_units(5000)
1050            .with_state_units(5000);
1051
1052        // Account layout: [0: fee_payer, 1: uploader_program, 2: meta_account, 3: buffer_account]
1053        let mut meta_account_idx = 2u16;
1054        let mut buffer_account_idx = 3u16;
1055        if meta_account > buffer_account {
1056            meta_account_idx = 3u16;
1057            buffer_account_idx = 2u16;
1058            tx = tx
1059                .add_rw_account(buffer_account)
1060                .add_rw_account(meta_account)
1061        } else {
1062            tx = tx
1063                .add_rw_account(meta_account)
1064                .add_rw_account(buffer_account)
1065        }
1066
1067        let instruction_data = build_uploader_finalize_instruction(
1068            buffer_account_idx,
1069            meta_account_idx,
1070            expected_hash,
1071        )?;
1072
1073        tx = tx.with_instructions(instruction_data);
1074
1075        Ok(tx)
1076    }
1077
1078    /// Build uploader program DESTROY transaction
1079    pub fn build_uploader_destroy(
1080        fee_payer: TnPubkey,
1081        uploader_program: TnPubkey,
1082        meta_account: TnPubkey,
1083        buffer_account: TnPubkey,
1084        fee: u64,
1085        nonce: u64,
1086        start_slot: u64,
1087    ) -> Result<Transaction> {
1088        let mut tx = Transaction::new(fee_payer, uploader_program, fee, nonce)
1089            .with_start_slot(start_slot)
1090            .with_expiry_after(10000)
1091            .with_compute_units(50000)
1092            .with_memory_units(5000)
1093            .with_state_units(5000);
1094
1095        // Account layout: [0: fee_payer, 1: uploader_program, 2: meta_account, 3: buffer_account]
1096        let mut meta_account_idx = 2u16;
1097        let mut buffer_account_idx = 3u16;
1098        if meta_account > buffer_account {
1099            meta_account_idx = 3u16;
1100            buffer_account_idx = 2u16;
1101            tx = tx
1102                .add_rw_account(buffer_account)
1103                .add_rw_account(meta_account)
1104        } else {
1105            tx = tx
1106                .add_rw_account(meta_account)
1107                .add_rw_account(buffer_account)
1108        }
1109
1110        let instruction_data =
1111            build_uploader_destroy_instruction(buffer_account_idx, meta_account_idx)?;
1112
1113        tx = tx.with_instructions(instruction_data);
1114        Ok(tx)
1115    }
1116
1117    /// Build manager program CREATE transaction
1118    pub fn build_manager_create(
1119        fee_payer: TnPubkey,
1120        manager_program: TnPubkey,
1121        meta_account: TnPubkey,
1122        program_account: TnPubkey,
1123        srcbuf_account: TnPubkey,
1124        authority_account: TnPubkey,
1125        srcbuf_offset: u32,
1126        srcbuf_size: u32,
1127        seed: &[u8],
1128        is_ephemeral: bool,
1129        meta_proof: Option<&[u8]>,
1130        program_proof: Option<&[u8]>,
1131        fee: u64,
1132        nonce: u64,
1133        start_slot: u64,
1134    ) -> Result<Transaction> {
1135        let mut tx = Transaction::new(fee_payer, manager_program, fee, nonce)
1136            .with_start_slot(start_slot)
1137            .with_expiry_after(10000)
1138            .with_compute_units(500_000_000)
1139            .with_memory_units(5000)
1140            .with_state_units(5000);
1141
1142        // Check if authority_account is the same as fee_payer
1143        let authority_is_fee_payer = authority_account == fee_payer;
1144
1145        // Separate accounts by access type and sort each group by pubkey
1146        let mut rw_accounts = vec![(meta_account, "meta"), (program_account, "program")];
1147
1148        let mut r_accounts = vec![(srcbuf_account, "srcbuf")];
1149
1150        // Only add authority_account if it's different from fee_payer
1151        if !authority_is_fee_payer {
1152            r_accounts.push((authority_account, "authority"));
1153        }
1154
1155        // Sort read-write accounts by pubkey
1156        rw_accounts.sort_by(|a, b| a.0.cmp(&b.0));
1157
1158        // Sort read-only accounts by pubkey
1159        r_accounts.sort_by(|a, b| a.0.cmp(&b.0));
1160
1161        // Combine sorted accounts: read-write first, then read-only
1162        let mut accounts = rw_accounts;
1163        accounts.extend(r_accounts);
1164
1165        let mut meta_account_idx = 0u16;
1166        let mut program_account_idx = 0u16;
1167        let mut srcbuf_account_idx = 0u16;
1168        let mut authority_account_idx = if authority_is_fee_payer {
1169            0u16 // Use fee_payer index (0) when authority is the same as fee_payer
1170        } else {
1171            0u16 // Will be set in the loop below
1172        };
1173
1174        for (i, (account, account_type)) in accounts.iter().enumerate() {
1175            let idx = (i + 2) as u16; // Skip fee_payer (0) and program (1)
1176            match *account_type {
1177                "meta" => {
1178                    meta_account_idx = idx;
1179                    tx = tx.add_rw_account(*account);
1180                }
1181                "program" => {
1182                    program_account_idx = idx;
1183                    tx = tx.add_rw_account(*account);
1184                }
1185                "srcbuf" => {
1186                    srcbuf_account_idx = idx;
1187                    tx = tx.add_r_account(*account);
1188                }
1189                "authority" => {
1190                    authority_account_idx = idx;
1191                    tx = tx.add_r_account(*account);
1192                }
1193                _ => unreachable!(),
1194            }
1195        }
1196
1197        let discriminant = if is_ephemeral {
1198            MANAGER_INSTRUCTION_CREATE_EPHEMERAL
1199        } else {
1200            MANAGER_INSTRUCTION_CREATE_PERMANENT
1201        };
1202
1203        // Concatenate proofs if both are provided (for permanent programs)
1204        let combined_proof = if let (Some(meta), Some(program)) = (meta_proof, program_proof) {
1205            let mut combined = Vec::with_capacity(meta.len() + program.len());
1206            combined.extend_from_slice(meta);
1207            combined.extend_from_slice(program);
1208            Some(combined)
1209        } else {
1210            None
1211        };
1212
1213        let instruction_data = build_manager_create_instruction(
1214            discriminant,
1215            meta_account_idx,
1216            program_account_idx,
1217            srcbuf_account_idx,
1218            authority_account_idx,
1219            srcbuf_offset,
1220            srcbuf_size,
1221            seed,
1222            combined_proof.as_deref(),
1223        )?;
1224
1225        tx = tx.with_instructions(instruction_data);
1226        Ok(tx)
1227    }
1228
1229    /// Build manager program UPGRADE transaction
1230    pub fn build_manager_upgrade(
1231        fee_payer: TnPubkey,
1232        manager_program: TnPubkey,
1233        meta_account: TnPubkey,
1234        program_account: TnPubkey,
1235        srcbuf_account: TnPubkey,
1236        srcbuf_offset: u32,
1237        srcbuf_size: u32,
1238        fee: u64,
1239        nonce: u64,
1240        start_slot: u64,
1241    ) -> Result<Transaction> {
1242        let mut tx = Transaction::new(fee_payer, manager_program, fee, nonce)
1243            .with_start_slot(start_slot)
1244            .with_expiry_after(10000)
1245            .with_compute_units(500_000_000)
1246            .with_memory_units(5000)
1247            .with_state_units(5000);
1248
1249        // Separate accounts by access type and sort each group by pubkey
1250        let mut rw_accounts = vec![(meta_account, "meta"), (program_account, "program")];
1251
1252        let mut r_accounts = vec![(srcbuf_account, "srcbuf")];
1253
1254        // Sort read-write accounts by pubkey
1255        rw_accounts.sort_by(|a, b| a.0.cmp(&b.0));
1256
1257        // Sort read-only accounts by pubkey
1258        r_accounts.sort_by(|a, b| a.0.cmp(&b.0));
1259
1260        // Combine sorted accounts: read-write first, then read-only
1261        let mut accounts = rw_accounts;
1262        accounts.extend(r_accounts);
1263
1264        let mut meta_account_idx = 0u16;
1265        let mut program_account_idx = 0u16;
1266        let mut srcbuf_account_idx = 0u16;
1267
1268        for (i, (account, account_type)) in accounts.iter().enumerate() {
1269            let idx = (i + 2) as u16; // Skip fee_payer (0) and program (1)
1270            match *account_type {
1271                "meta" => {
1272                    meta_account_idx = idx;
1273                    tx = tx.add_rw_account(*account);
1274                }
1275                "program" => {
1276                    program_account_idx = idx;
1277                    tx = tx.add_rw_account(*account);
1278                }
1279                "srcbuf" => {
1280                    srcbuf_account_idx = idx;
1281                    tx = tx.add_r_account(*account);
1282                }
1283                _ => unreachable!(),
1284            }
1285        }
1286
1287        let instruction_data = build_manager_upgrade_instruction(
1288            meta_account_idx,
1289            program_account_idx,
1290            srcbuf_account_idx,
1291            srcbuf_offset,
1292            srcbuf_size,
1293        )?;
1294
1295        tx = tx.with_instructions(instruction_data);
1296        Ok(tx)
1297    }
1298
1299    /// Build manager program SET_PAUSE transaction
1300    pub fn build_manager_set_pause(
1301        fee_payer: TnPubkey,
1302        manager_program: TnPubkey,
1303        meta_account: TnPubkey,
1304        program_account: TnPubkey,
1305        is_paused: bool,
1306        fee: u64,
1307        nonce: u64,
1308        start_slot: u64,
1309    ) -> Result<Transaction> {
1310        let mut tx = Transaction::new(fee_payer, manager_program, fee, nonce)
1311            .with_start_slot(start_slot)
1312            .with_expiry_after(10000)
1313            .with_compute_units(100_000_000)
1314            .with_memory_units(5000)
1315            .with_state_units(5000);
1316
1317        // Add accounts in sorted order
1318        let mut accounts = vec![(meta_account, "meta"), (program_account, "program")];
1319        accounts.sort_by(|a, b| a.0.cmp(&b.0));
1320
1321        let mut meta_account_idx = 0u16;
1322        let mut program_account_idx = 0u16;
1323
1324        for (i, (account, account_type)) in accounts.iter().enumerate() {
1325            let idx = (i + 2) as u16;
1326            match *account_type {
1327                "meta" => {
1328                    meta_account_idx = idx;
1329                    tx = tx.add_rw_account(*account);
1330                }
1331                "program" => {
1332                    program_account_idx = idx;
1333                    tx = tx.add_rw_account(*account);
1334                }
1335                _ => unreachable!(),
1336            }
1337        }
1338
1339        let instruction_data =
1340            build_manager_set_pause_instruction(meta_account_idx, program_account_idx, is_paused)?;
1341
1342        tx = tx.with_instructions(instruction_data);
1343        Ok(tx)
1344    }
1345
1346    /// Build manager program simple transactions (DESTROY, FINALIZE, CLAIM_AUTHORITY)
1347    pub fn build_manager_simple(
1348        fee_payer: TnPubkey,
1349        manager_program: TnPubkey,
1350        meta_account: TnPubkey,
1351        program_account: TnPubkey,
1352        instruction_type: u8,
1353        fee: u64,
1354        nonce: u64,
1355        start_slot: u64,
1356    ) -> Result<Transaction> {
1357        let mut tx = Transaction::new(fee_payer, manager_program, fee, nonce)
1358            .with_start_slot(start_slot)
1359            .with_expiry_after(10000)
1360            .with_compute_units(100_000_000)
1361            .with_memory_units(5000)
1362            .with_state_units(5000);
1363
1364        // Add accounts in sorted order
1365        let mut accounts = vec![(meta_account, "meta"), (program_account, "program")];
1366        accounts.sort_by(|a, b| a.0.cmp(&b.0));
1367
1368        let mut meta_account_idx = 0u16;
1369        let mut program_account_idx = 0u16;
1370
1371        for (i, (account, account_type)) in accounts.iter().enumerate() {
1372            let idx = (i + 2) as u16;
1373            match *account_type {
1374                "meta" => {
1375                    meta_account_idx = idx;
1376                    tx = tx.add_rw_account(*account);
1377                }
1378                "program" => {
1379                    program_account_idx = idx;
1380                    tx = tx.add_rw_account(*account);
1381                }
1382                _ => unreachable!(),
1383            }
1384        }
1385
1386        let instruction_data = build_manager_header_instruction(
1387            instruction_type,
1388            meta_account_idx,
1389            program_account_idx,
1390        )?;
1391
1392        tx = tx.with_instructions(instruction_data);
1393        Ok(tx)
1394    }
1395
1396    /// Build manager program SET_AUTHORITY transaction
1397    pub fn build_manager_set_authority(
1398        fee_payer: TnPubkey,
1399        manager_program: TnPubkey,
1400        meta_account: TnPubkey,
1401        program_account: TnPubkey,
1402        authority_candidate: [u8; 32],
1403        fee: u64,
1404        nonce: u64,
1405        start_slot: u64,
1406    ) -> Result<Transaction> {
1407        let mut tx = Transaction::new(fee_payer, manager_program, fee, nonce)
1408            .with_start_slot(start_slot)
1409            .with_expiry_after(10000)
1410            .with_compute_units(100_000_000)
1411            .with_memory_units(5000)
1412            .with_state_units(5000);
1413
1414        // Add accounts in sorted order
1415        let mut accounts = vec![(meta_account, "meta"), (program_account, "program")];
1416        accounts.sort_by(|a, b| a.0.cmp(&b.0));
1417
1418        let mut meta_account_idx = 0u16;
1419        let mut program_account_idx = 0u16;
1420
1421        for (i, (account, account_type)) in accounts.iter().enumerate() {
1422            let idx = (i + 2) as u16;
1423            match *account_type {
1424                "meta" => {
1425                    meta_account_idx = idx;
1426                    tx = tx.add_rw_account(*account);
1427                }
1428                "program" => {
1429                    program_account_idx = idx;
1430                    tx = tx.add_rw_account(*account);
1431                }
1432                _ => unreachable!(),
1433            }
1434        }
1435
1436        let instruction_data = build_manager_set_authority_instruction(
1437            meta_account_idx,
1438            program_account_idx,
1439            authority_candidate,
1440        )?;
1441
1442        tx = tx.with_instructions(instruction_data);
1443        Ok(tx)
1444    }
1445
1446    /// Build test uploader program CREATE transaction
1447    pub fn build_test_uploader_create(
1448        fee_payer: TnPubkey,
1449        test_uploader_program: TnPubkey,
1450        target_account: TnPubkey,
1451        account_sz: u32,
1452        seed: &[u8],
1453        is_ephemeral: bool,
1454        state_proof: Option<&[u8]>,
1455        fee: u64,
1456        nonce: u64,
1457        start_slot: u64,
1458    ) -> Result<Transaction> {
1459        // Account layout: [0: fee_payer, 1: test_uploader_program, 2: target_account]
1460        let target_account_idx = 2u16;
1461
1462        let tx = Transaction::new(fee_payer, test_uploader_program, fee, nonce)
1463            .with_start_slot(start_slot)
1464            .with_expiry_after(100)
1465            .with_compute_units(100_000 + account_sz)
1466            .with_memory_units(10_000)
1467            .with_state_units(10_000)
1468            .add_rw_account(target_account);
1469
1470        let instruction_data = build_test_uploader_create_instruction(
1471            target_account_idx,
1472            account_sz,
1473            seed,
1474            is_ephemeral,
1475            state_proof,
1476        )?;
1477
1478        let tx = tx.with_instructions(instruction_data);
1479        Ok(tx)
1480    }
1481
1482    /// Build test uploader program WRITE transaction
1483    pub fn build_test_uploader_write(
1484        fee_payer: TnPubkey,
1485        test_uploader_program: TnPubkey,
1486        target_account: TnPubkey,
1487        offset: u32,
1488        data: &[u8],
1489        fee: u64,
1490        nonce: u64,
1491        start_slot: u64,
1492    ) -> Result<Transaction> {
1493        // Account layout: [0: fee_payer, 1: test_uploader_program, 2: target_account]
1494        let target_account_idx = 2u16;
1495
1496        let tx = Transaction::new(fee_payer, test_uploader_program, fee, nonce)
1497            .with_start_slot(start_slot)
1498            .with_expiry_after(10_000)
1499            .with_compute_units(100_000 + 18 * data.len() as u32)
1500            .with_memory_units(10_000)
1501            .with_state_units(10_000)
1502            .add_rw_account(target_account);
1503
1504        let instruction_data =
1505            build_test_uploader_write_instruction(target_account_idx, offset, data)?;
1506
1507        let tx = tx.with_instructions(instruction_data);
1508        Ok(tx)
1509    }
1510
1511    /// Build account decompression transaction using DECOMPRESS2 (separate meta and data accounts)
1512    pub fn build_decompress2(
1513        fee_payer: TnPubkey,
1514        program: TnPubkey,
1515        target_account: TnPubkey,
1516        meta_account: TnPubkey,
1517        data_account: TnPubkey,
1518        data_offset: u32,
1519        state_proof: &[u8],
1520        fee: u64,
1521        nonce: u64,
1522        start_slot: u64,
1523        data_sz: u32,
1524    ) -> Result<Transaction> {
1525        // Account layout: [0: fee_payer, 1: program, 2: target_account, 3+: meta/data accounts]
1526        let mut tx = Transaction::new(fee_payer, program, fee, nonce)
1527            .with_start_slot(start_slot)
1528            .with_expiry_after(100)
1529            .with_compute_units(10_000 + 2 * data_sz)
1530            .with_memory_units(10_000)
1531            .with_state_units(10);
1532
1533        // Add target account (read-write)
1534        let target_account_idx = 2u16;
1535        tx = tx.add_rw_account(target_account);
1536
1537        let mut meta_account_idx = 0u16;
1538        let mut data_account_idx = 0u16;
1539
1540        // Handle meta and data accounts - if they're the same, add only once; if different, add both and sort
1541        if meta_account == data_account {
1542            // Same account for both meta and data
1543            let account_idx = 3u16;
1544            meta_account_idx = account_idx;
1545            data_account_idx = account_idx;
1546            tx = tx.add_r_account(meta_account);
1547        } else {
1548            // Different accounts - add both and sort by pubkey
1549            let mut read_accounts = vec![(meta_account, "meta"), (data_account, "data")];
1550            read_accounts.sort_by(|a, b| a.0.cmp(&b.0));
1551
1552            for (i, (account, account_type)) in read_accounts.iter().enumerate() {
1553                let idx = (3 + i) as u16; // Start from index 3
1554                match *account_type {
1555                    "meta" => {
1556                        meta_account_idx = idx;
1557                        tx = tx.add_r_account(*account);
1558                    }
1559                    "data" => {
1560                        data_account_idx = idx;
1561                        tx = tx.add_r_account(*account);
1562                    }
1563                    _ => unreachable!(),
1564                }
1565            }
1566        }
1567
1568        let instruction_data = build_decompress2_instruction(
1569            target_account_idx,
1570            meta_account_idx,
1571            data_account_idx,
1572            data_offset,
1573            state_proof,
1574        )?;
1575
1576        tx = tx.with_instructions(instruction_data);
1577        Ok(tx)
1578    }
1579}
1580
1581/// Build uploader CREATE instruction data
1582fn build_uploader_create_instruction(
1583    buffer_account_idx: u16,
1584    meta_account_idx: u16,
1585    authority_account_idx: u16,
1586    buffer_size: u32,
1587    expected_hash: [u8; 32],
1588    seed: &[u8],
1589) -> Result<Vec<u8>> {
1590    let mut instruction = Vec::new();
1591
1592    // Discriminant (4 bytes, little-endian)
1593    instruction.extend_from_slice(&TN_UPLOADER_PROGRAM_INSTRUCTION_CREATE.to_le_bytes());
1594
1595    // Create args struct
1596    let args = UploaderCreateArgs {
1597        buffer_account_idx,
1598        meta_account_idx,
1599        authority_account_idx,
1600        buffer_account_sz: buffer_size,
1601        expected_account_hash: expected_hash,
1602        seed_len: seed.len() as u32,
1603    };
1604
1605    // Serialize args (unsafe due to packed struct)
1606    let args_bytes = unsafe {
1607        std::slice::from_raw_parts(
1608            &args as *const _ as *const u8,
1609            std::mem::size_of::<UploaderCreateArgs>(),
1610        )
1611    };
1612    instruction.extend_from_slice(args_bytes);
1613
1614    // Append seed bytes
1615    instruction.extend_from_slice(seed);
1616
1617    Ok(instruction)
1618}
1619
1620/// Build uploader WRITE instruction data
1621fn build_uploader_write_instruction(
1622    buffer_account_idx: u16,
1623    meta_account_idx: u16,
1624    data: &[u8],
1625    offset: u32,
1626) -> Result<Vec<u8>> {
1627    let mut instruction = Vec::new();
1628
1629    // Discriminant (4 bytes, little-endian)
1630    instruction.extend_from_slice(&TN_UPLOADER_PROGRAM_INSTRUCTION_WRITE.to_le_bytes());
1631
1632    // Write args
1633    let args = UploaderWriteArgs {
1634        buffer_account_idx,
1635        meta_account_idx,
1636        data_len: data.len() as u32,
1637        data_offset: offset,
1638    };
1639
1640    // Serialize args
1641    let args_bytes = unsafe {
1642        std::slice::from_raw_parts(
1643            &args as *const _ as *const u8,
1644            std::mem::size_of::<UploaderWriteArgs>(),
1645        )
1646    };
1647    instruction.extend_from_slice(args_bytes);
1648
1649    // Append data
1650    instruction.extend_from_slice(data);
1651
1652    Ok(instruction)
1653}
1654
1655/// Build uploader FINALIZE instruction data
1656fn build_uploader_finalize_instruction(
1657    buffer_account_idx: u16,
1658    meta_account_idx: u16,
1659    expected_hash: [u8; 32],
1660) -> Result<Vec<u8>> {
1661    let mut instruction = Vec::new();
1662
1663    // Discriminant (4 bytes, little-endian)
1664    instruction.extend_from_slice(&TN_UPLOADER_PROGRAM_INSTRUCTION_FINALIZE.to_le_bytes());
1665
1666    // Finalize args
1667    let args = UploaderFinalizeArgs {
1668        buffer_account_idx,
1669        meta_account_idx,
1670        expected_account_hash: expected_hash,
1671    };
1672
1673    // Serialize args
1674    let args_bytes = unsafe {
1675        std::slice::from_raw_parts(
1676            &args as *const _ as *const u8,
1677            std::mem::size_of::<UploaderFinalizeArgs>(),
1678        )
1679    };
1680    instruction.extend_from_slice(args_bytes);
1681
1682    Ok(instruction)
1683}
1684
1685/// Build uploader DESTROY instruction data
1686fn build_uploader_destroy_instruction(
1687    buffer_account_idx: u16,
1688    meta_account_idx: u16,
1689) -> Result<Vec<u8>> {
1690    let mut instruction = Vec::new();
1691
1692    // Discriminant (4 bytes, little-endian)
1693    instruction.extend_from_slice(&TN_UPLOADER_PROGRAM_INSTRUCTION_DESTROY.to_le_bytes());
1694
1695    // Destroy args
1696    let args = UploaderDestroyArgs {
1697        buffer_account_idx,
1698        meta_account_idx,
1699    };
1700
1701    // Serialize args
1702    let args_bytes = unsafe {
1703        std::slice::from_raw_parts(
1704            &args as *const _ as *const u8,
1705            std::mem::size_of::<UploaderDestroyArgs>(),
1706        )
1707    };
1708    instruction.extend_from_slice(args_bytes);
1709
1710    Ok(instruction)
1711}
1712
1713/// Build manager CREATE instruction data
1714fn build_manager_create_instruction(
1715    discriminant: u8,
1716    meta_account_idx: u16,
1717    program_account_idx: u16,
1718    srcbuf_account_idx: u16,
1719    authority_account_idx: u16,
1720    srcbuf_offset: u32,
1721    srcbuf_size: u32,
1722    seed: &[u8],
1723    proof: Option<&[u8]>,
1724) -> Result<Vec<u8>> {
1725    let mut instruction = Vec::new();
1726
1727    // Create args
1728    let args = ManagerCreateArgs {
1729        discriminant,
1730        meta_account_idx,
1731        program_account_idx,
1732        srcbuf_account_idx,
1733        srcbuf_offset,
1734        srcbuf_size,
1735        authority_account_idx,
1736        seed_len: seed.len() as u32,
1737    };
1738
1739    // Serialize args
1740    let args_bytes = unsafe {
1741        std::slice::from_raw_parts(
1742            &args as *const ManagerCreateArgs as *const u8,
1743            std::mem::size_of::<ManagerCreateArgs>(),
1744        )
1745    };
1746    instruction.extend_from_slice(args_bytes);
1747
1748    // Add seed bytes
1749    instruction.extend_from_slice(seed);
1750
1751    // Add proof bytes (only for permanent accounts)
1752    if let Some(proof_bytes) = proof {
1753        instruction.extend_from_slice(proof_bytes);
1754    }
1755
1756    Ok(instruction)
1757}
1758
1759/// Build manager UPGRADE instruction data
1760fn build_manager_upgrade_instruction(
1761    meta_account_idx: u16,
1762    program_account_idx: u16,
1763    srcbuf_account_idx: u16,
1764    srcbuf_offset: u32,
1765    srcbuf_size: u32,
1766) -> Result<Vec<u8>> {
1767    let mut instruction = Vec::new();
1768
1769    let args = ManagerUpgradeArgs {
1770        discriminant: MANAGER_INSTRUCTION_UPGRADE,
1771        meta_account_idx,
1772        program_account_idx,
1773        srcbuf_account_idx,
1774        srcbuf_offset,
1775        srcbuf_size,
1776    };
1777
1778    let args_bytes = unsafe {
1779        std::slice::from_raw_parts(
1780            &args as *const ManagerUpgradeArgs as *const u8,
1781            std::mem::size_of::<ManagerUpgradeArgs>(),
1782        )
1783    };
1784    instruction.extend_from_slice(args_bytes);
1785
1786    Ok(instruction)
1787}
1788
1789/// Build manager SET_PAUSE instruction data
1790fn build_manager_set_pause_instruction(
1791    meta_account_idx: u16,
1792    program_account_idx: u16,
1793    is_paused: bool,
1794) -> Result<Vec<u8>> {
1795    let mut instruction = Vec::new();
1796
1797    let args = ManagerSetPauseArgs {
1798        discriminant: MANAGER_INSTRUCTION_SET_PAUSE,
1799        meta_account_idx,
1800        program_account_idx,
1801        is_paused: if is_paused { 1 } else { 0 },
1802    };
1803
1804    let args_bytes = unsafe {
1805        std::slice::from_raw_parts(
1806            &args as *const ManagerSetPauseArgs as *const u8,
1807            std::mem::size_of::<ManagerSetPauseArgs>(),
1808        )
1809    };
1810    instruction.extend_from_slice(args_bytes);
1811
1812    Ok(instruction)
1813}
1814
1815/// Build manager header-only instruction data (DESTROY, FINALIZE, CLAIM_AUTHORITY)
1816fn build_manager_header_instruction(
1817    discriminant: u8,
1818    meta_account_idx: u16,
1819    program_account_idx: u16,
1820) -> Result<Vec<u8>> {
1821    let mut instruction = Vec::new();
1822
1823    let args = ManagerHeaderArgs {
1824        discriminant,
1825        meta_account_idx,
1826        program_account_idx,
1827    };
1828
1829    let args_bytes = unsafe {
1830        std::slice::from_raw_parts(
1831            &args as *const ManagerHeaderArgs as *const u8,
1832            std::mem::size_of::<ManagerHeaderArgs>(),
1833        )
1834    };
1835    instruction.extend_from_slice(args_bytes);
1836
1837    Ok(instruction)
1838}
1839
1840/// Build manager SET_AUTHORITY instruction data
1841fn build_manager_set_authority_instruction(
1842    meta_account_idx: u16,
1843    program_account_idx: u16,
1844    authority_candidate: [u8; 32],
1845) -> Result<Vec<u8>> {
1846    let mut instruction = Vec::new();
1847
1848    let args = ManagerSetAuthorityArgs {
1849        discriminant: MANAGER_INSTRUCTION_SET_AUTHORITY,
1850        meta_account_idx,
1851        program_account_idx,
1852        authority_candidate,
1853    };
1854
1855    let args_bytes = unsafe {
1856        std::slice::from_raw_parts(
1857            &args as *const ManagerSetAuthorityArgs as *const u8,
1858            std::mem::size_of::<ManagerSetAuthorityArgs>(),
1859        )
1860    };
1861    instruction.extend_from_slice(args_bytes);
1862
1863    Ok(instruction)
1864}
1865
1866/// Build test uploader CREATE instruction data
1867fn build_test_uploader_create_instruction(
1868    account_idx: u16,
1869    account_sz: u32,
1870    seed: &[u8],
1871    is_ephemeral: bool,
1872    state_proof: Option<&[u8]>,
1873) -> Result<Vec<u8>> {
1874    let mut instruction = Vec::new();
1875
1876    // Discriminant (1 byte)
1877    instruction.push(TN_TEST_UPLOADER_PROGRAM_DISCRIMINANT_CREATE);
1878
1879    // Create args struct
1880    let args = TestUploaderCreateArgs {
1881        account_idx,
1882        is_ephemeral: if is_ephemeral { 1u8 } else { 0u8 },
1883        account_sz,
1884        seed_len: seed.len() as u32,
1885    };
1886
1887    // Serialize args (unsafe due to packed struct)
1888    let args_bytes = unsafe {
1889        std::slice::from_raw_parts(
1890            &args as *const _ as *const u8,
1891            std::mem::size_of::<TestUploaderCreateArgs>(),
1892        )
1893    };
1894    instruction.extend_from_slice(args_bytes);
1895
1896    // Append seed bytes
1897    instruction.extend_from_slice(seed);
1898
1899    // Append state proof if provided (for non-ephemeral accounts)
1900    if let Some(proof) = state_proof {
1901        instruction.extend_from_slice(proof);
1902    }
1903
1904    Ok(instruction)
1905}
1906
1907/// Build test uploader WRITE instruction data
1908fn build_test_uploader_write_instruction(
1909    target_account_idx: u16,
1910    target_offset: u32,
1911    data: &[u8],
1912) -> Result<Vec<u8>> {
1913    let mut instruction = Vec::new();
1914
1915    // Discriminant (1 byte)
1916    instruction.push(TN_TEST_UPLOADER_PROGRAM_DISCRIMINANT_WRITE);
1917
1918    // Write args
1919    let args = TestUploaderWriteArgs {
1920        target_account_idx,
1921        target_offset,
1922        data_len: data.len() as u32,
1923    };
1924
1925    // Serialize args
1926    let args_bytes = unsafe {
1927        std::slice::from_raw_parts(
1928            &args as *const _ as *const u8,
1929            std::mem::size_of::<TestUploaderWriteArgs>(),
1930        )
1931    };
1932    instruction.extend_from_slice(args_bytes);
1933
1934    // Append data
1935    instruction.extend_from_slice(data);
1936
1937    Ok(instruction)
1938}
1939
1940/// Build system program DECOMPRESS2 instruction data
1941pub fn build_decompress2_instruction(
1942    target_account_idx: u16,
1943    meta_account_idx: u16,
1944    data_account_idx: u16,
1945    data_offset: u32,
1946    state_proof: &[u8],
1947) -> Result<Vec<u8>> {
1948    let mut instruction = Vec::new();
1949
1950    // Discriminant (1 byte) - TN_SYS_PROG_DISCRIMINANT_ACCOUNT_DECOMPRESS2 = 0x08
1951    instruction.push(0x08);
1952
1953    // DECOMPRESS2 args
1954    let args = SystemProgramDecompress2Args {
1955        target_account_idx,
1956        meta_account_idx,
1957        data_account_idx,
1958        data_offset,
1959    };
1960
1961    // Serialize args
1962    let args_bytes = unsafe {
1963        std::slice::from_raw_parts(
1964            &args as *const _ as *const u8,
1965            std::mem::size_of::<SystemProgramDecompress2Args>(),
1966        )
1967    };
1968    instruction.extend_from_slice(args_bytes);
1969
1970    // Append state proof bytes
1971    instruction.extend_from_slice(state_proof);
1972
1973    Ok(instruction)
1974}
1975
1976/// Token program instruction discriminants
1977pub const TOKEN_INSTRUCTION_INITIALIZE_MINT: u8 = 0x00;
1978pub const TOKEN_INSTRUCTION_INITIALIZE_ACCOUNT: u8 = 0x01;
1979pub const TOKEN_INSTRUCTION_TRANSFER: u8 = 0x02;
1980pub const TOKEN_INSTRUCTION_MINT_TO: u8 = 0x03;
1981pub const TOKEN_INSTRUCTION_BURN: u8 = 0x04;
1982pub const TOKEN_INSTRUCTION_CLOSE_ACCOUNT: u8 = 0x05;
1983pub const TOKEN_INSTRUCTION_FREEZE_ACCOUNT: u8 = 0x06;
1984pub const TOKEN_INSTRUCTION_THAW_ACCOUNT: u8 = 0x07;
1985
1986/* WTHRU instruction discriminants */
1987pub const TN_WTHRU_INSTRUCTION_INITIALIZE_MINT: u32 = 0;
1988pub const TN_WTHRU_INSTRUCTION_DEPOSIT: u32 = 1;
1989pub const TN_WTHRU_INSTRUCTION_WITHDRAW: u32 = 2;
1990// lease, resolve (lil library), show lease info, 
1991// cost to lease, renew, record management, listing records for domain, 
1992// subdomain management 
1993
1994/* Name service instruction discriminants */
1995pub const TN_NAME_SERVICE_INSTRUCTION_INITIALIZE_ROOT: u32 = 0;
1996pub const TN_NAME_SERVICE_INSTRUCTION_REGISTER_SUBDOMAIN: u32 = 1;
1997pub const TN_NAME_SERVICE_INSTRUCTION_APPEND_RECORD: u32 = 2;
1998pub const TN_NAME_SERVICE_INSTRUCTION_DELETE_RECORD: u32 = 3;
1999pub const TN_NAME_SERVICE_INSTRUCTION_UNREGISTER: u32 = 4;
2000
2001/* Name service proof discriminants */
2002pub const TN_NAME_SERVICE_PROOF_INLINE: u32 = 0;
2003
2004/* Name service limits */
2005pub const TN_NAME_SERVICE_MAX_DOMAIN_LENGTH: usize = 64;
2006pub const TN_NAME_SERVICE_MAX_KEY_LENGTH: usize = 32;
2007pub const TN_NAME_SERVICE_MAX_VALUE_LENGTH: usize = 256;
2008
2009// Thru registrar instruction types (u32 discriminants)
2010pub const TN_THRU_REGISTRAR_INSTRUCTION_INITIALIZE_REGISTRY: u32 = 0;
2011pub const TN_THRU_REGISTRAR_INSTRUCTION_PURCHASE_DOMAIN: u32 = 1;
2012pub const TN_THRU_REGISTRAR_INSTRUCTION_RENEW_LEASE: u32 = 2;
2013pub const TN_THRU_REGISTRAR_INSTRUCTION_CLAIM_EXPIRED_DOMAIN: u32 = 3;
2014
2015/// Helper function to add sorted accounts and return their indices
2016fn add_sorted_accounts(tx: Transaction, accounts: &[(TnPubkey, bool)]) -> (Transaction, Vec<u16>) {
2017    // Separate RW and RO accounts, sort each group separately
2018    let mut rw_accounts: Vec<_> = accounts.iter().enumerate()
2019        .filter(|(_, (_, writable))| *writable)
2020        .collect();
2021    let mut ro_accounts: Vec<_> = accounts.iter().enumerate()
2022        .filter(|(_, (_, writable))| !*writable)
2023        .collect();
2024    
2025    // Sort each group by pubkey
2026    rw_accounts.sort_by(|a, b| a.1.0.cmp(&b.1.0));
2027    ro_accounts.sort_by(|a, b| a.1.0.cmp(&b.1.0));
2028
2029    let mut updated_tx = tx;
2030    let mut indices = vec![0u16; accounts.len()];
2031    let mut seen: HashMap<TnPubkey, u16> = HashMap::new();
2032    seen.insert(updated_tx.fee_payer, 0u16);
2033    seen.insert(updated_tx.program, 1u16);
2034
2035    let mut next_idx = 2u16;
2036
2037    // Process RW accounts first (in sorted order)
2038    for (i, (account, _)) in rw_accounts.iter() {
2039        if let Some(idx) = seen.get(account) {
2040            indices[*i] = *idx;
2041            continue;
2042        }
2043
2044        let account_idx = next_idx;
2045        next_idx = next_idx.saturating_add(1);
2046        seen.insert(*account, account_idx);
2047        indices[*i] = account_idx;
2048
2049        updated_tx = updated_tx.add_rw_account(*account);
2050    }
2051
2052    // Then process RO accounts (in sorted order)
2053    for (i, (account, _)) in ro_accounts.iter() {
2054        if let Some(idx) = seen.get(account) {
2055            indices[*i] = *idx;
2056            continue;
2057        }
2058
2059        let account_idx = next_idx;
2060        next_idx = next_idx.saturating_add(1);
2061        seen.insert(*account, account_idx);
2062        indices[*i] = account_idx;
2063
2064        updated_tx = updated_tx.add_r_account(*account);
2065    }
2066
2067    (updated_tx, indices)
2068}
2069
2070/// Helper function to get authority index (0 if fee_payer, else add as readonly)
2071fn add_sorted_rw_accounts(mut tx: Transaction, accounts: &[TnPubkey]) -> (Transaction, Vec<u16>) {
2072    if accounts.is_empty() {
2073        return (tx, Vec::new());
2074    }
2075
2076    let mut sorted: Vec<(usize, TnPubkey)> = accounts.iter().cloned().enumerate().collect();
2077    sorted.sort_by(|a, b| a.1.cmp(&b.1));
2078
2079    let mut indices = vec![0u16; accounts.len()];
2080    for (pos, (orig_idx, account)) in sorted.into_iter().enumerate() {
2081        let idx = (2 + pos) as u16;
2082        indices[orig_idx] = idx;
2083        tx = tx.add_rw_account(account);
2084    }
2085
2086    (tx, indices)
2087}
2088
2089fn add_sorted_ro_accounts(
2090    mut tx: Transaction,
2091    base_idx: u16,
2092    accounts: &[TnPubkey],
2093) -> (Transaction, Vec<u16>) {
2094    if accounts.is_empty() {
2095        return (tx, Vec::new());
2096    }
2097
2098    let mut sorted: Vec<(usize, TnPubkey)> = accounts.iter().cloned().enumerate().collect();
2099    sorted.sort_by(|a, b| a.1.cmp(&b.1));
2100
2101    let mut indices = vec![0u16; accounts.len()];
2102    for (pos, (orig_idx, account)) in sorted.into_iter().enumerate() {
2103        let idx = base_idx + pos as u16;
2104        indices[orig_idx] = idx;
2105        tx = tx.add_r_account(account);
2106    }
2107
2108    (tx, indices)
2109}
2110
2111impl TransactionBuilder {
2112    /// Build token program InitializeMint transaction
2113    pub fn build_token_initialize_mint(
2114        fee_payer: TnPubkey,
2115        token_program: TnPubkey,
2116        mint_account: TnPubkey,
2117        creator: TnPubkey,
2118        mint_authority: TnPubkey,
2119        freeze_authority: Option<TnPubkey>,
2120        decimals: u8,
2121        ticker: &str,
2122        seed: [u8; 32],
2123        state_proof: Vec<u8>,
2124        fee: u64,
2125        nonce: u64,
2126        start_slot: u64,
2127    ) -> Result<Transaction> {
2128        let base_tx =
2129            Transaction::new(fee_payer, token_program, fee, nonce).with_start_slot(start_slot);
2130        let (tx, indices) = add_sorted_rw_accounts(base_tx, &[mint_account]);
2131        let mint_account_idx = indices[0];
2132
2133        let instruction_data = build_token_initialize_mint_instruction(
2134            mint_account_idx,
2135            decimals,
2136            creator,
2137            mint_authority,
2138            freeze_authority,
2139            ticker,
2140            seed,
2141            state_proof,
2142        )?;
2143
2144        let tx = tx
2145            .with_instructions(instruction_data)
2146            .with_expiry_after(100)
2147            .with_compute_units(300_000)
2148            .with_state_units(10_000)
2149            .with_memory_units(10_000);
2150
2151        Ok(tx)
2152    }
2153
2154    /// Build token program InitializeAccount transaction
2155    pub fn build_token_initialize_account(
2156        fee_payer: TnPubkey,
2157        token_program: TnPubkey,
2158        token_account: TnPubkey,
2159        mint_account: TnPubkey,
2160        owner: TnPubkey,
2161        seed: [u8; 32],
2162        state_proof: Vec<u8>,
2163        fee: u64,
2164        nonce: u64,
2165        start_slot: u64,
2166    ) -> Result<Transaction> {
2167        let owner_is_fee_payer = owner == fee_payer;
2168
2169        let mut rw_accounts = vec![token_account];
2170        rw_accounts.sort();
2171
2172        let mut ro_accounts = vec![mint_account];
2173        if !owner_is_fee_payer {
2174            ro_accounts.push(owner);
2175            ro_accounts.sort();
2176        }
2177
2178        let mut tx =
2179            Transaction::new(fee_payer, token_program, fee, nonce).with_start_slot(start_slot);
2180
2181        let mut token_account_idx = 0u16;
2182        for (i, account) in rw_accounts.iter().enumerate() {
2183            let idx = (2 + i) as u16;
2184            if *account == token_account {
2185                token_account_idx = idx;
2186            }
2187            tx = tx.add_rw_account(*account);
2188        }
2189
2190        let base_ro_idx = 2 + rw_accounts.len() as u16;
2191        let mut mint_account_idx = 0u16;
2192        let mut owner_account_idx = if owner_is_fee_payer { 0u16 } else { 0u16 };
2193        for (i, account) in ro_accounts.iter().enumerate() {
2194            let idx = base_ro_idx + i as u16;
2195            if *account == mint_account {
2196                mint_account_idx = idx;
2197            } else if !owner_is_fee_payer && *account == owner {
2198                owner_account_idx = idx;
2199            }
2200            tx = tx.add_r_account(*account);
2201        }
2202
2203        let instruction_data = build_token_initialize_account_instruction(
2204            token_account_idx,
2205            mint_account_idx,
2206            owner_account_idx,
2207            seed,
2208            state_proof,
2209        )?;
2210
2211        let tx = tx
2212            .with_instructions(instruction_data)
2213            .with_expiry_after(100)
2214            .with_compute_units(300_000)
2215            .with_state_units(10_000)
2216            .with_memory_units(10_000);
2217
2218        Ok(tx)
2219    }
2220
2221    /// Build token program Transfer transaction
2222    pub fn build_token_transfer(
2223        fee_payer: TnPubkey,
2224        token_program: TnPubkey,
2225        source_account: TnPubkey,
2226        dest_account: TnPubkey,
2227        _authority: TnPubkey,
2228        amount: u64,
2229        fee: u64,
2230        nonce: u64,
2231        start_slot: u64,
2232    ) -> Result<Transaction> {
2233        let mut tx = Transaction::new(fee_payer, token_program, fee, nonce)
2234            .with_start_slot(start_slot)
2235            .with_expiry_after(100)
2236            .with_compute_units(300_000)
2237            .with_state_units(10_000)
2238            .with_memory_units(10_000);
2239
2240        let is_self_transfer = source_account == dest_account;
2241        let (source_account_idx, dest_account_idx) = if is_self_transfer {
2242            // For self-transfers, add the account only once and use same index
2243            tx = tx.add_rw_account(source_account);
2244            (2u16, 2u16)
2245        } else {
2246            // Add source and dest accounts in sorted order
2247            let accounts = &[(source_account, true), (dest_account, true)];
2248            let (updated_tx, indices) = add_sorted_accounts(tx, accounts);
2249            tx = updated_tx;
2250            (indices[0], indices[1])
2251        };
2252
2253        // Note: For transfers, the authority (source account owner) must sign the transaction
2254        // The token program will verify the signature matches the source account owner
2255
2256        let instruction_data =
2257            build_token_transfer_instruction(source_account_idx, dest_account_idx, amount)?;
2258
2259        Ok(tx.with_instructions(instruction_data))
2260    }
2261
2262    /// Build token program MintTo transaction
2263    pub fn build_token_mint_to(
2264        fee_payer: TnPubkey,
2265        token_program: TnPubkey,
2266        mint_account: TnPubkey,
2267        dest_account: TnPubkey,
2268        authority: TnPubkey,
2269        amount: u64,
2270        fee: u64,
2271        nonce: u64,
2272        start_slot: u64,
2273    ) -> Result<Transaction> {
2274        let base_tx = Transaction::new(fee_payer, token_program, fee, nonce)
2275            .with_start_slot(start_slot)
2276            .with_expiry_after(100)
2277            .with_compute_units(300_000)
2278            .with_state_units(10_000)
2279            .with_memory_units(10_000);
2280
2281        let (tx_after_rw, rw_indices) =
2282            add_sorted_rw_accounts(base_tx, &[mint_account, dest_account]);
2283        let mint_account_idx = rw_indices[0];
2284        let dest_account_idx = rw_indices[1];
2285
2286        let mut tx = tx_after_rw;
2287        let authority_account_idx = if authority == fee_payer {
2288            0u16
2289        } else {
2290            let base_ro_idx = 2 + rw_indices.len() as u16;
2291            let (tx_after_ro, ro_indices) = add_sorted_ro_accounts(tx, base_ro_idx, &[authority]);
2292            tx = tx_after_ro;
2293            ro_indices[0]
2294        };
2295
2296        let instruction_data = build_token_mint_to_instruction(
2297            mint_account_idx,
2298            dest_account_idx,
2299            authority_account_idx,
2300            amount,
2301        )?;
2302
2303        Ok(tx.with_instructions(instruction_data))
2304    }
2305
2306    /// Build token program Burn transaction
2307    pub fn build_token_burn(
2308        fee_payer: TnPubkey,
2309        token_program: TnPubkey,
2310        token_account: TnPubkey,
2311        mint_account: TnPubkey,
2312        authority: TnPubkey,
2313        amount: u64,
2314        fee: u64,
2315        nonce: u64,
2316        start_slot: u64,
2317    ) -> Result<Transaction> {
2318        let base_tx = Transaction::new(fee_payer, token_program, fee, nonce)
2319            .with_start_slot(start_slot)
2320            .with_expiry_after(100)
2321            .with_compute_units(300_000)
2322            .with_state_units(10_000)
2323            .with_memory_units(10_000);
2324
2325        let (tx_after_rw, rw_indices) =
2326            add_sorted_rw_accounts(base_tx, &[token_account, mint_account]);
2327        let token_account_idx = rw_indices[0];
2328        let mint_account_idx = rw_indices[1];
2329
2330        let mut tx = tx_after_rw;
2331        let authority_account_idx = if authority == fee_payer {
2332            0u16
2333        } else {
2334            let base_ro_idx = 2 + rw_indices.len() as u16;
2335            let (tx_after_ro, ro_indices) = add_sorted_ro_accounts(tx, base_ro_idx, &[authority]);
2336            tx = tx_after_ro;
2337            ro_indices[0]
2338        };
2339
2340        let instruction_data = build_token_burn_instruction(
2341            token_account_idx,
2342            mint_account_idx,
2343            authority_account_idx,
2344            amount,
2345        )?;
2346
2347        Ok(tx.with_instructions(instruction_data))
2348    }
2349
2350    /// Build token program FreezeAccount transaction
2351    pub fn build_token_freeze_account(
2352        fee_payer: TnPubkey,
2353        token_program: TnPubkey,
2354        token_account: TnPubkey,
2355        mint_account: TnPubkey,
2356        authority: TnPubkey,
2357        fee: u64,
2358        nonce: u64,
2359        start_slot: u64,
2360    ) -> Result<Transaction> {
2361        let base_tx = Transaction::new(fee_payer, token_program, fee, nonce)
2362            .with_start_slot(start_slot)
2363            .with_expiry_after(100)
2364            .with_compute_units(300_000)
2365            .with_state_units(10_000)
2366            .with_memory_units(10_000);
2367
2368        let (tx_after_rw, rw_indices) =
2369            add_sorted_rw_accounts(base_tx, &[token_account, mint_account]);
2370        let token_account_idx = rw_indices[0];
2371        let mint_account_idx = rw_indices[1];
2372
2373        let mut tx = tx_after_rw;
2374        let authority_account_idx = if authority == fee_payer {
2375            0u16
2376        } else {
2377            let base_ro_idx = 2 + rw_indices.len() as u16;
2378            let (tx_after_ro, ro_indices) = add_sorted_ro_accounts(tx, base_ro_idx, &[authority]);
2379            tx = tx_after_ro;
2380            ro_indices[0]
2381        };
2382
2383        let instruction_data = build_token_freeze_account_instruction(
2384            token_account_idx,
2385            mint_account_idx,
2386            authority_account_idx,
2387        )?;
2388
2389        Ok(tx.with_instructions(instruction_data))
2390    }
2391
2392    /// Build token program ThawAccount transaction
2393    pub fn build_token_thaw_account(
2394        fee_payer: TnPubkey,
2395        token_program: TnPubkey,
2396        token_account: TnPubkey,
2397        mint_account: TnPubkey,
2398        authority: TnPubkey,
2399        fee: u64,
2400        nonce: u64,
2401        start_slot: u64,
2402    ) -> Result<Transaction> {
2403        let base_tx = Transaction::new(fee_payer, token_program, fee, nonce)
2404            .with_start_slot(start_slot)
2405            .with_expiry_after(100)
2406            .with_compute_units(300_000)
2407            .with_state_units(10_000)
2408            .with_memory_units(10_000);
2409
2410        let (tx_after_rw, rw_indices) =
2411            add_sorted_rw_accounts(base_tx, &[token_account, mint_account]);
2412        let token_account_idx = rw_indices[0];
2413        let mint_account_idx = rw_indices[1];
2414
2415        let mut tx = tx_after_rw;
2416        let authority_account_idx = if authority == fee_payer {
2417            0u16
2418        } else {
2419            let base_ro_idx = 2 + rw_indices.len() as u16;
2420            let (tx_after_ro, ro_indices) = add_sorted_ro_accounts(tx, base_ro_idx, &[authority]);
2421            tx = tx_after_ro;
2422            ro_indices[0]
2423        };
2424
2425        let instruction_data = build_token_thaw_account_instruction(
2426            token_account_idx,
2427            mint_account_idx,
2428            authority_account_idx,
2429        )?;
2430
2431        Ok(tx.with_instructions(instruction_data))
2432    }
2433
2434    /// Build token program CloseAccount transaction
2435    pub fn build_token_close_account(
2436        fee_payer: TnPubkey,
2437        token_program: TnPubkey,
2438        token_account: TnPubkey,
2439        destination: TnPubkey,
2440        authority: TnPubkey,
2441        fee: u64,
2442        nonce: u64,
2443        start_slot: u64,
2444    ) -> Result<Transaction> {
2445        let base_tx = Transaction::new(fee_payer, token_program, fee, nonce)
2446            .with_start_slot(start_slot)
2447            .with_expiry_after(100)
2448            .with_compute_units(300_000)
2449            .with_state_units(10_000)
2450            .with_memory_units(10_000);
2451
2452        let mut rw_accounts = vec![token_account];
2453        let destination_in_accounts = destination != fee_payer;
2454        if destination_in_accounts {
2455            rw_accounts.push(destination);
2456        }
2457
2458        let (tx_after_rw, rw_indices) = add_sorted_rw_accounts(base_tx, &rw_accounts);
2459        let token_account_idx = rw_indices[0];
2460        let destination_idx = if destination_in_accounts {
2461            rw_indices[1]
2462        } else {
2463            0u16
2464        };
2465
2466        let mut tx = tx_after_rw;
2467        let authority_account_idx = if authority == fee_payer {
2468            0u16
2469        } else {
2470            let base_ro_idx = 2 + rw_indices.len() as u16;
2471            let (tx_after_ro, ro_indices) = add_sorted_ro_accounts(tx, base_ro_idx, &[authority]);
2472            tx = tx_after_ro;
2473            ro_indices[0]
2474        };
2475
2476        let instruction_data = build_token_close_account_instruction(
2477            token_account_idx,
2478            destination_idx,
2479            authority_account_idx,
2480        )?;
2481
2482        Ok(tx.with_instructions(instruction_data))
2483    }
2484
2485    /// Build WTHRU initialize transaction
2486    pub fn build_wthru_initialize_mint(
2487        fee_payer: TnPubkey,
2488        wthru_program: TnPubkey,
2489        token_program: TnPubkey,
2490        mint_account: TnPubkey,
2491        vault_account: TnPubkey,
2492        decimals: u8,
2493        mint_seed: [u8; 32],
2494        mint_proof: Vec<u8>,
2495        vault_proof: Vec<u8>,
2496        fee: u64,
2497        nonce: u64,
2498        start_slot: u64,
2499    ) -> Result<Transaction> {
2500        let mut tx = Transaction::new(fee_payer, wthru_program, fee, nonce)
2501            .with_start_slot(start_slot)
2502            .with_expiry_after(100)
2503            .with_compute_units(500_000)
2504            .with_state_units(10_000)
2505            .with_memory_units(10_000);
2506
2507        let accounts = [
2508            (mint_account, true),
2509            (vault_account, true),
2510            (token_program, false),
2511        ];
2512
2513        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2514        tx = tx_with_accounts;
2515
2516        let mint_account_idx = indices[0];
2517        let vault_account_idx = indices[1];
2518        let token_program_idx = indices[2];
2519
2520        let instruction_data = build_wthru_initialize_mint_instruction(
2521            token_program_idx,
2522            mint_account_idx,
2523            vault_account_idx,
2524            decimals,
2525            mint_seed,
2526            mint_proof,
2527            vault_proof,
2528        )?;
2529
2530        Ok(tx.with_instructions(instruction_data))
2531    }
2532
2533    /// Build WTHRU deposit transaction
2534    pub fn build_wthru_deposit(
2535        fee_payer: TnPubkey,
2536        wthru_program: TnPubkey,
2537        token_program: TnPubkey,
2538        mint_account: TnPubkey,
2539        vault_account: TnPubkey,
2540        dest_token_account: TnPubkey,
2541        fee: u64,
2542        nonce: u64,
2543        start_slot: u64,
2544    ) -> Result<Transaction> {
2545        let mut tx = Transaction::new(fee_payer, wthru_program, fee, nonce)
2546            .with_start_slot(start_slot)
2547            .with_expiry_after(100)
2548            .with_compute_units(400_000)
2549            .with_state_units(10_000)
2550            .with_memory_units(10_000);
2551
2552        let accounts = [
2553            (mint_account, true),
2554            (vault_account, true),
2555            (dest_token_account, true),
2556            (token_program, false),
2557        ];
2558
2559        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2560        tx = tx_with_accounts;
2561
2562        let mint_account_idx = indices[0];
2563        let vault_account_idx = indices[1];
2564        let dest_account_idx = indices[2];
2565        let token_program_idx = indices[3];
2566
2567        let instruction_data = build_wthru_deposit_instruction(
2568            token_program_idx,
2569            vault_account_idx,
2570            mint_account_idx,
2571            dest_account_idx,
2572        )?;
2573
2574        Ok(tx.with_instructions(instruction_data))
2575    }
2576
2577    /// Build WTHRU withdraw transaction
2578    pub fn build_wthru_withdraw(
2579        fee_payer: TnPubkey,
2580        wthru_program: TnPubkey,
2581        token_program: TnPubkey,
2582        mint_account: TnPubkey,
2583        vault_account: TnPubkey,
2584        wthru_token_account: TnPubkey,
2585        recipient_account: TnPubkey,
2586        amount: u64,
2587        fee: u64,
2588        nonce: u64,
2589        start_slot: u64,
2590    ) -> Result<Transaction> {
2591        let mut tx = Transaction::new(fee_payer, wthru_program, fee, nonce)
2592            .with_start_slot(start_slot)
2593            .with_expiry_after(100)
2594            .with_compute_units(400_000)
2595            .with_state_units(10_000)
2596            .with_memory_units(10_000);
2597
2598        let accounts = [
2599            (mint_account, true),
2600            (vault_account, true),
2601            (wthru_token_account, true),
2602            (recipient_account, true),
2603            (token_program, false),
2604        ];
2605
2606        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2607        tx = tx_with_accounts;
2608
2609        let mint_account_idx = indices[0];
2610        let vault_account_idx = indices[1];
2611        let token_account_idx = indices[2];
2612        let recipient_account_idx = indices[3];
2613        let token_program_idx = indices[4];
2614
2615        let owner_account_idx = 0u16; // fee payer/owner
2616
2617        let instruction_data = build_wthru_withdraw_instruction(
2618            token_program_idx,
2619            vault_account_idx,
2620            mint_account_idx,
2621            token_account_idx,
2622            owner_account_idx,
2623            recipient_account_idx,
2624            amount,
2625        )?;
2626
2627        Ok(tx.with_instructions(instruction_data))
2628    }
2629
2630    /// Build faucet program Deposit transaction
2631    /// The faucet program will invoke the EOA program to transfer from depositor to faucet account
2632    pub fn build_faucet_deposit(
2633        fee_payer: TnPubkey,
2634        faucet_program: TnPubkey,
2635        faucet_account: TnPubkey,
2636        depositor_account: TnPubkey,
2637        eoa_program: TnPubkey,
2638        amount: u64,
2639        fee: u64,
2640        nonce: u64,
2641        start_slot: u64,
2642    ) -> Result<Transaction> {
2643        let tx = Transaction::new(fee_payer, faucet_program, fee, nonce)
2644            .with_start_slot(start_slot)
2645            .with_expiry_after(100)
2646            .with_compute_units(300_000)
2647            .with_state_units(10_000)
2648            .with_memory_units(10_000);
2649
2650        let (tx, depositor_account_idx) = Self::ensure_rw_account(tx, depositor_account);
2651        let (tx, faucet_account_idx) = Self::ensure_rw_account(tx, faucet_account);
2652        let (tx, eoa_program_idx) = Self::ensure_ro_account(tx, eoa_program);
2653
2654        let instruction_data = build_faucet_deposit_instruction(
2655            faucet_account_idx,
2656            depositor_account_idx,
2657            eoa_program_idx,
2658            amount,
2659        )?;
2660
2661        Ok(tx.with_instructions(instruction_data))
2662    }
2663
2664    fn resolve_account_index(tx: &Transaction, target: &TnPubkey) -> Option<u16> {
2665        if *target == tx.fee_payer {
2666            return Some(0u16);
2667        }
2668
2669        if *target == tx.program {
2670            return Some(1u16);
2671        }
2672
2673        if let Some(ref rw) = tx.rw_accs {
2674            if let Some(pos) = rw.iter().position(|acc| acc == target) {
2675                return Some(2u16 + pos as u16);
2676            }
2677        }
2678
2679        if let Some(ref ro) = tx.r_accs {
2680            let base = 2u16 + tx.rw_accs.as_ref().map_or(0u16, |v| v.len() as u16);
2681            if let Some(pos) = ro.iter().position(|acc| acc == target) {
2682                return Some(base + pos as u16);
2683            }
2684        }
2685
2686        None
2687    }
2688
2689    fn ensure_rw_account(mut tx: Transaction, account: TnPubkey) -> (Transaction, u16) {
2690        if let Some(idx) = Self::resolve_account_index(&tx, &account) {
2691            return (tx, idx);
2692        }
2693
2694        tx = tx.add_rw_account(account);
2695        let idx = Self::resolve_account_index(&tx, &account)
2696            .expect("read-write account index should exist after insertion");
2697
2698        (tx, idx)
2699    }
2700
2701    fn ensure_ro_account(mut tx: Transaction, account: TnPubkey) -> (Transaction, u16) {
2702        if let Some(idx) = Self::resolve_account_index(&tx, &account) {
2703            return (tx, idx);
2704        }
2705
2706        tx = tx.add_r_account(account);
2707        let idx = Self::resolve_account_index(&tx, &account)
2708            .expect("read-only account index should exist after insertion");
2709
2710        (tx, idx)
2711    }
2712
2713    /// Build faucet program Withdraw transaction
2714    pub fn build_faucet_withdraw(
2715        fee_payer: TnPubkey,
2716        faucet_program: TnPubkey,
2717        faucet_account: TnPubkey,
2718        recipient_account: TnPubkey,
2719        amount: u64,
2720        fee: u64,
2721        nonce: u64,
2722        start_slot: u64,
2723    ) -> Result<Transaction> {
2724        let mut tx = Transaction::new(fee_payer, faucet_program, fee, nonce)
2725            .with_start_slot(start_slot)
2726            .with_expiry_after(100)
2727            .with_compute_units(300_000)
2728            .with_state_units(10_000)
2729            .with_memory_units(10_000);
2730
2731        // Determine account indices, handling duplicates with fee_payer and between accounts
2732        // Check for duplicates first
2733        let faucet_is_fee_payer = faucet_account == fee_payer;
2734        let recipient_is_fee_payer = recipient_account == fee_payer;
2735        let recipient_is_faucet = recipient_account == faucet_account;
2736
2737        let (faucet_account_idx, recipient_account_idx) = if faucet_is_fee_payer && recipient_is_fee_payer {
2738            // Both are fee_payer (same account)
2739            (0u16, 0u16)
2740        } else if faucet_is_fee_payer {
2741            // Faucet is fee_payer, recipient is different
2742            tx = tx.add_rw_account(recipient_account);
2743            (0u16, 2u16)
2744        } else if recipient_is_fee_payer {
2745            // Recipient is fee_payer, faucet is different
2746            tx = tx.add_rw_account(faucet_account);
2747            (2u16, 0u16)
2748        } else if recipient_is_faucet {
2749            // Both are same account (but not fee_payer)
2750            tx = tx.add_rw_account(faucet_account);
2751            (2u16, 2u16)
2752        } else {
2753            // Both are different accounts, add in sorted order
2754            if faucet_account < recipient_account {
2755                tx = tx.add_rw_account(faucet_account);
2756                tx = tx.add_rw_account(recipient_account);
2757                (2u16, 3u16)
2758            } else {
2759                tx = tx.add_rw_account(recipient_account);
2760                tx = tx.add_rw_account(faucet_account);
2761                (3u16, 2u16)
2762            }
2763        };
2764
2765        let instruction_data = build_faucet_withdraw_instruction(
2766            faucet_account_idx,
2767            recipient_account_idx,
2768            amount,
2769        )?;
2770
2771        Ok(tx.with_instructions(instruction_data))
2772    }
2773
2774    /// Build name service InitializeRoot transaction
2775    pub fn build_name_service_initialize_root(
2776        fee_payer: TnPubkey,
2777        name_service_program: TnPubkey,
2778        registrar_account: TnPubkey,
2779        authority_account: TnPubkey,
2780        root_name: &str,
2781        state_proof: Vec<u8>,
2782        fee: u64,
2783        nonce: u64,
2784        start_slot: u64,
2785    ) -> Result<Transaction> {
2786        let mut tx = Transaction::new(fee_payer, name_service_program, fee, nonce)
2787            .with_start_slot(start_slot)
2788            .with_expiry_after(100)
2789            .with_compute_units(500_000)
2790            .with_state_units(10_000)
2791            .with_memory_units(10_000);
2792
2793        let accounts = [(registrar_account, true), (authority_account, false)];
2794        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2795        tx = tx_with_accounts;
2796
2797        let registrar_account_idx = indices[0];
2798        let authority_account_idx = indices[1];
2799
2800        let instruction_data = build_name_service_initialize_root_instruction(
2801            registrar_account_idx,
2802            authority_account_idx,
2803            root_name,
2804            state_proof,
2805        )?;
2806
2807        Ok(tx.with_instructions(instruction_data))
2808    }
2809
2810    /// Build name service RegisterSubdomain transaction
2811    pub fn build_name_service_register_subdomain(
2812        fee_payer: TnPubkey,
2813        name_service_program: TnPubkey,
2814        domain_account: TnPubkey,
2815        parent_account: TnPubkey,
2816        owner_account: TnPubkey,
2817        authority_account: TnPubkey,
2818        domain_name: &str,
2819        state_proof: Vec<u8>,
2820        fee: u64,
2821        nonce: u64,
2822        start_slot: u64,
2823    ) -> Result<Transaction> {
2824        let mut tx = Transaction::new(fee_payer, name_service_program, fee, nonce)
2825            .with_start_slot(start_slot)
2826            .with_expiry_after(100)
2827            .with_compute_units(500_000)
2828            .with_state_units(10_000)
2829            .with_memory_units(10_000);
2830
2831        let accounts = [
2832            (domain_account, true),
2833            (parent_account, true),
2834            (owner_account, false),
2835            (authority_account, false),
2836        ];
2837        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2838        tx = tx_with_accounts;
2839
2840        let domain_account_idx = indices[0];
2841        let parent_account_idx = indices[1];
2842        let owner_account_idx = indices[2];
2843        let authority_account_idx = indices[3];
2844
2845        let instruction_data = build_name_service_register_subdomain_instruction(
2846            domain_account_idx,
2847            parent_account_idx,
2848            owner_account_idx,
2849            authority_account_idx,
2850            domain_name,
2851            state_proof,
2852        )?;
2853
2854        Ok(tx.with_instructions(instruction_data))
2855    }
2856
2857    /// Build name service AppendRecord transaction
2858    pub fn build_name_service_append_record(
2859        fee_payer: TnPubkey,
2860        name_service_program: TnPubkey,
2861        domain_account: TnPubkey,
2862        owner_account: TnPubkey,
2863        key: &[u8],
2864        value: &[u8],
2865        fee: u64,
2866        nonce: u64,
2867        start_slot: u64,
2868    ) -> Result<Transaction> {
2869        let mut tx = Transaction::new(fee_payer, name_service_program, fee, nonce)
2870            .with_start_slot(start_slot)
2871            .with_expiry_after(100)
2872            .with_compute_units(250_000)
2873            .with_state_units(10_000)
2874            .with_memory_units(10_000);
2875
2876        let accounts = [(domain_account, true), (owner_account, false)];
2877        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2878        tx = tx_with_accounts;
2879
2880        let domain_account_idx = indices[0];
2881        let owner_account_idx = indices[1];
2882
2883        let instruction_data = build_name_service_append_record_instruction(
2884            domain_account_idx,
2885            owner_account_idx,
2886            key,
2887            value,
2888        )?;
2889
2890        Ok(tx.with_instructions(instruction_data))
2891    }
2892
2893    /// Build name service DeleteRecord transaction
2894    pub fn build_name_service_delete_record(
2895        fee_payer: TnPubkey,
2896        name_service_program: TnPubkey,
2897        domain_account: TnPubkey,
2898        owner_account: TnPubkey,
2899        key: &[u8],
2900        fee: u64,
2901        nonce: u64,
2902        start_slot: u64,
2903    ) -> Result<Transaction> {
2904        let mut tx = Transaction::new(fee_payer, name_service_program, fee, nonce)
2905            .with_start_slot(start_slot)
2906            .with_expiry_after(100)
2907            .with_compute_units(200_000)
2908            .with_state_units(10_000)
2909            .with_memory_units(10_000);
2910
2911        let accounts = [(domain_account, true), (owner_account, false)];
2912        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2913        tx = tx_with_accounts;
2914
2915        let domain_account_idx = indices[0];
2916        let owner_account_idx = indices[1];
2917
2918        let instruction_data = build_name_service_delete_record_instruction(
2919            domain_account_idx,
2920            owner_account_idx,
2921            key,
2922        )?;
2923
2924        Ok(tx.with_instructions(instruction_data))
2925    }
2926
2927    /// Build name service UnregisterSubdomain transaction
2928    pub fn build_name_service_unregister_subdomain(
2929        fee_payer: TnPubkey,
2930        name_service_program: TnPubkey,
2931        domain_account: TnPubkey,
2932        owner_account: TnPubkey,
2933        fee: u64,
2934        nonce: u64,
2935        start_slot: u64,
2936    ) -> Result<Transaction> {
2937        let mut tx = Transaction::new(fee_payer, name_service_program, fee, nonce)
2938            .with_start_slot(start_slot)
2939            .with_expiry_after(100)
2940            .with_compute_units(200_000)
2941            .with_state_units(10_000)
2942            .with_memory_units(10_000);
2943
2944        let accounts = [(domain_account, true), (owner_account, false)];
2945        let (tx_with_accounts, indices) = add_sorted_accounts(tx, &accounts);
2946        tx = tx_with_accounts;
2947
2948        let domain_account_idx = indices[0];
2949        let owner_account_idx = indices[1];
2950
2951        let instruction_data = build_name_service_unregister_subdomain_instruction(
2952            domain_account_idx,
2953            owner_account_idx,
2954        )?;
2955
2956        Ok(tx.with_instructions(instruction_data))
2957    }
2958
2959    /// Build thru registrar InitializeRegistry transaction
2960    pub fn build_thru_registrar_initialize_registry(
2961        fee_payer: TnPubkey,
2962        thru_registrar_program: TnPubkey,
2963        config_account: TnPubkey,
2964        name_service_program: TnPubkey,
2965        root_registrar_account: TnPubkey,
2966        treasurer_account: TnPubkey,
2967        token_mint_account: TnPubkey,
2968        token_program: TnPubkey,
2969        root_domain_name: &str,
2970        price_per_year: u64,
2971        config_proof: Vec<u8>,
2972        registrar_proof: Vec<u8>,
2973        fee: u64,
2974        nonce: u64,
2975        start_slot: u64,
2976    ) -> Result<Transaction> {
2977        let mut tx = Transaction::new(fee_payer, thru_registrar_program, fee, nonce)
2978            .with_start_slot(start_slot)
2979            .with_expiry_after(100)
2980            .with_compute_units(500_000)
2981            .with_state_units(10_000)
2982            .with_memory_units(10_000);
2983
2984        // Add accounts in sorted order (read-write first, then read-only)
2985        // Registrar must be writable because the base name service CPI creates/resizes it.
2986        let mut rw_accounts = vec![config_account, root_registrar_account];
2987        rw_accounts.sort();
2988
2989        let mut ro_accounts = vec![
2990            name_service_program,
2991            treasurer_account,
2992            token_mint_account,
2993            token_program,
2994        ];
2995        ro_accounts.sort();
2996
2997        // Add RW accounts
2998        let mut config_account_idx = 0u16;
2999        let mut root_registrar_account_idx = 0u16;
3000        for (i, account) in rw_accounts.iter().enumerate() {
3001            let idx = (2 + i) as u16;
3002            if *account == config_account {
3003                config_account_idx = idx;
3004            } else if *account == root_registrar_account {
3005                root_registrar_account_idx = idx;
3006            }
3007            tx = tx.add_rw_account(*account);
3008        }
3009
3010        // Add RO accounts
3011        let base_ro_idx = 2 + rw_accounts.len() as u16;
3012        let mut name_service_program_idx = 0u16;
3013        let mut treasurer_account_idx = 0u16;
3014        let mut token_mint_account_idx = 0u16;
3015        let mut token_program_idx = 0u16;
3016
3017        for (i, account) in ro_accounts.iter().enumerate() {
3018            let idx = base_ro_idx + i as u16;
3019            if *account == name_service_program {
3020                name_service_program_idx = idx;
3021            } else if *account == root_registrar_account {
3022                root_registrar_account_idx = idx;
3023            } else if *account == treasurer_account {
3024                treasurer_account_idx = idx;
3025            } else if *account == token_mint_account {
3026                token_mint_account_idx = idx;
3027            } else if *account == token_program {
3028                token_program_idx = idx;
3029            }
3030            tx = tx.add_r_account(*account);
3031        }
3032
3033        let instruction_data = build_thru_registrar_initialize_registry_instruction(
3034            config_account_idx,
3035            name_service_program_idx,
3036            root_registrar_account_idx,
3037            treasurer_account_idx,
3038            token_mint_account_idx,
3039            token_program_idx,
3040            root_domain_name,
3041            price_per_year,
3042            config_proof,
3043            registrar_proof,
3044        )?;
3045
3046        Ok(tx.with_instructions(instruction_data))
3047    }
3048
3049    /// Build thru registrar PurchaseDomain transaction
3050    pub fn build_thru_registrar_purchase_domain(
3051        fee_payer: TnPubkey,
3052        thru_registrar_program: TnPubkey,
3053        config_account: TnPubkey,
3054        lease_account: TnPubkey,
3055        domain_account: TnPubkey,
3056        name_service_program: TnPubkey,
3057        root_registrar_account: TnPubkey,
3058        treasurer_account: TnPubkey,
3059        payer_token_account: TnPubkey,
3060        token_mint_account: TnPubkey,
3061        token_program: TnPubkey,
3062        domain_name: &str,
3063        years: u8,
3064        lease_proof: Vec<u8>,
3065        domain_proof: Vec<u8>,
3066        fee: u64,
3067        nonce: u64,
3068        start_slot: u64,
3069    ) -> Result<Transaction> {
3070        let mut tx = Transaction::new(fee_payer, thru_registrar_program, fee, nonce)
3071            .with_start_slot(start_slot)
3072            .with_expiry_after(100)
3073            .with_compute_units(500_000)
3074            .with_state_units(10_000)
3075            .with_memory_units(10_000);
3076
3077        // Add accounts in sorted order
3078        // Token accounts must be writable for transfer; lease/domain are created; root registrar and config are updated via CPI.
3079        let mut rw_accounts = vec![
3080            config_account,
3081            lease_account,
3082            domain_account,
3083            treasurer_account,
3084            payer_token_account,
3085            root_registrar_account,
3086        ];
3087        rw_accounts.sort();
3088
3089        let mut ro_accounts = vec![
3090            name_service_program,
3091            token_mint_account,
3092            token_program,
3093        ];
3094        ro_accounts.sort();
3095
3096        // Add RW accounts
3097        let mut config_account_idx = 0u16;
3098        let mut lease_account_idx = 0u16;
3099        let mut domain_account_idx = 0u16;
3100        let mut treasurer_account_idx = 0u16;
3101        let mut payer_token_account_idx = 0u16;
3102        let mut root_registrar_account_idx = 0u16;
3103        for (i, account) in rw_accounts.iter().enumerate() {
3104            let idx = (2 + i) as u16;
3105            if *account == config_account {
3106                config_account_idx = idx;
3107            } else if *account == lease_account {
3108                lease_account_idx = idx;
3109            } else if *account == domain_account {
3110                domain_account_idx = idx;
3111            } else if *account == treasurer_account {
3112                treasurer_account_idx = idx;
3113            } else if *account == payer_token_account {
3114                payer_token_account_idx = idx;
3115            } else if *account == root_registrar_account {
3116                root_registrar_account_idx = idx;
3117            }
3118            tx = tx.add_rw_account(*account);
3119        }
3120
3121        // Add RO accounts
3122        let base_ro_idx = 2 + rw_accounts.len() as u16;
3123        let mut name_service_program_idx = 0u16;
3124        let mut token_mint_account_idx = 0u16;
3125        let mut token_program_idx = 0u16;
3126
3127        for (i, account) in ro_accounts.iter().enumerate() {
3128            let idx = base_ro_idx + i as u16;
3129            if *account == config_account {
3130                config_account_idx = idx; // Should remain zero; config moved to RW set
3131            } else if *account == name_service_program {
3132                name_service_program_idx = idx;
3133            } else if *account == root_registrar_account {
3134                root_registrar_account_idx = idx;
3135            } else if *account == treasurer_account {
3136                treasurer_account_idx = idx;
3137            } else if *account == payer_token_account {
3138                payer_token_account_idx = idx;
3139            } else if *account == token_mint_account {
3140                token_mint_account_idx = idx;
3141            } else if *account == token_program {
3142                token_program_idx = idx;
3143            }
3144            tx = tx.add_r_account(*account);
3145        }
3146
3147        let instruction_data = build_thru_registrar_purchase_domain_instruction(
3148            config_account_idx,
3149            lease_account_idx,
3150            domain_account_idx,
3151            name_service_program_idx,
3152            root_registrar_account_idx,
3153            treasurer_account_idx,
3154            payer_token_account_idx,
3155            token_mint_account_idx,
3156            token_program_idx,
3157            domain_name,
3158            years,
3159            lease_proof,
3160            domain_proof,
3161        )?;
3162
3163        Ok(tx.with_instructions(instruction_data))
3164    }
3165
3166    /// Build thru registrar RenewLease transaction
3167    pub fn build_thru_registrar_renew_lease(
3168        fee_payer: TnPubkey,
3169        thru_registrar_program: TnPubkey,
3170        config_account: TnPubkey,
3171        lease_account: TnPubkey,
3172        treasurer_account: TnPubkey,
3173        payer_token_account: TnPubkey,
3174        token_mint_account: TnPubkey,
3175        token_program: TnPubkey,
3176        years: u8,
3177        fee: u64,
3178        nonce: u64,
3179        start_slot: u64,
3180    ) -> Result<Transaction> {
3181        let mut tx = Transaction::new(fee_payer, thru_registrar_program, fee, nonce)
3182            .with_start_slot(start_slot)
3183            .with_expiry_after(100)
3184            .with_compute_units(300_000)
3185            .with_state_units(10_000)
3186            .with_memory_units(10_000);
3187
3188        // Add accounts in sorted order
3189        // Token accounts must be writable for transfer.
3190        let mut rw_accounts = vec![lease_account, treasurer_account, payer_token_account];
3191        rw_accounts.sort();
3192
3193        let mut ro_accounts = vec![
3194            config_account,
3195            token_mint_account,
3196            token_program,
3197        ];
3198        ro_accounts.sort();
3199
3200        // Add RW accounts
3201        let mut lease_account_idx = 0u16;
3202        let mut treasurer_account_idx = 0u16;
3203        let mut payer_token_account_idx = 0u16;
3204        for (i, account) in rw_accounts.iter().enumerate() {
3205            let idx = (2 + i) as u16;
3206            if *account == lease_account {
3207                lease_account_idx = idx;
3208            } else if *account == treasurer_account {
3209                treasurer_account_idx = idx;
3210            } else if *account == payer_token_account {
3211                payer_token_account_idx = idx;
3212            }
3213            tx = tx.add_rw_account(*account);
3214        }
3215
3216        // Add RO accounts
3217        let base_ro_idx = 2 + rw_accounts.len() as u16;
3218        let mut config_account_idx = 0u16;
3219        let mut token_mint_account_idx = 0u16;
3220        let mut token_program_idx = 0u16;
3221
3222        for (i, account) in ro_accounts.iter().enumerate() {
3223            let idx = base_ro_idx + i as u16;
3224            if *account == config_account {
3225                config_account_idx = idx;
3226            } else if *account == token_mint_account {
3227                token_mint_account_idx = idx;
3228            } else if *account == token_program {
3229                token_program_idx = idx;
3230            }
3231            tx = tx.add_r_account(*account);
3232        }
3233
3234        let instruction_data = build_thru_registrar_renew_lease_instruction(
3235            config_account_idx,
3236            lease_account_idx,
3237            treasurer_account_idx,
3238            payer_token_account_idx,
3239            token_mint_account_idx,
3240            token_program_idx,
3241            years,
3242        )?;
3243
3244        Ok(tx.with_instructions(instruction_data))
3245    }
3246
3247    /// Build thru registrar ClaimExpiredDomain transaction
3248    pub fn build_thru_registrar_claim_expired_domain(
3249        fee_payer: TnPubkey,
3250        thru_registrar_program: TnPubkey,
3251        config_account: TnPubkey,
3252        lease_account: TnPubkey,
3253        treasurer_account: TnPubkey,
3254        payer_token_account: TnPubkey,
3255        token_mint_account: TnPubkey,
3256        token_program: TnPubkey,
3257        years: u8,
3258        fee: u64,
3259        nonce: u64,
3260        start_slot: u64,
3261    ) -> Result<Transaction> {
3262        let mut tx = Transaction::new(fee_payer, thru_registrar_program, fee, nonce)
3263            .with_start_slot(start_slot)
3264            .with_expiry_after(100)
3265            .with_compute_units(300_000)
3266            .with_state_units(10_000)
3267            .with_memory_units(10_000);
3268
3269        // Add accounts in sorted order
3270        // Token accounts must be writable for transfer.
3271        let mut rw_accounts = vec![lease_account, treasurer_account, payer_token_account];
3272        rw_accounts.sort();
3273
3274        let mut ro_accounts = vec![
3275            config_account,
3276            token_mint_account,
3277            token_program,
3278        ];
3279        ro_accounts.sort();
3280
3281        // Add RW accounts
3282        let mut lease_account_idx = 0u16;
3283        let mut treasurer_account_idx = 0u16;
3284        let mut payer_token_account_idx = 0u16;
3285        for (i, account) in rw_accounts.iter().enumerate() {
3286            let idx = (2 + i) as u16;
3287            if *account == lease_account {
3288                lease_account_idx = idx;
3289            } else if *account == treasurer_account {
3290                treasurer_account_idx = idx;
3291            } else if *account == payer_token_account {
3292                payer_token_account_idx = idx;
3293            }
3294            tx = tx.add_rw_account(*account);
3295        }
3296
3297        // Add RO accounts
3298        let base_ro_idx = 2 + rw_accounts.len() as u16;
3299        let mut config_account_idx = 0u16;
3300        let mut token_mint_account_idx = 0u16;
3301        let mut token_program_idx = 0u16;
3302
3303        for (i, account) in ro_accounts.iter().enumerate() {
3304            let idx = base_ro_idx + i as u16;
3305            if *account == config_account {
3306                config_account_idx = idx;
3307            } else if *account == token_mint_account {
3308                token_mint_account_idx = idx;
3309            } else if *account == token_program {
3310                token_program_idx = idx;
3311            }
3312            tx = tx.add_r_account(*account);
3313        }
3314
3315        let instruction_data = build_thru_registrar_claim_expired_domain_instruction(
3316            config_account_idx,
3317            lease_account_idx,
3318            treasurer_account_idx,
3319            payer_token_account_idx,
3320            token_mint_account_idx,
3321            token_program_idx,
3322            years,
3323        )?;
3324
3325        Ok(tx.with_instructions(instruction_data))
3326    }
3327}
3328
3329/// Build token InitializeMint instruction data
3330fn build_token_initialize_mint_instruction(
3331    mint_account_idx: u16,
3332    decimals: u8,
3333    creator: TnPubkey,
3334    mint_authority: TnPubkey,
3335    freeze_authority: Option<TnPubkey>,
3336    ticker: &str,
3337    seed: [u8; 32],
3338    state_proof: Vec<u8>,
3339) -> Result<Vec<u8>> {
3340    let mut instruction_data = Vec::new();
3341
3342    // Instruction tag
3343    instruction_data.push(TOKEN_INSTRUCTION_INITIALIZE_MINT);
3344
3345    // mint_account_index (u16)
3346    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3347
3348    // decimals (u8)
3349    instruction_data.push(decimals);
3350
3351    // creator (32 bytes)
3352    instruction_data.extend_from_slice(&creator);
3353
3354    // mint_authority (32 bytes)
3355    instruction_data.extend_from_slice(&mint_authority);
3356
3357    // freeze_authority (32 bytes) and has_freeze_authority flag
3358    let (freeze_auth, has_freeze_auth) = match freeze_authority {
3359        Some(auth) => (auth, 1u8),
3360        None => ([0u8; 32], 0u8),
3361    };
3362    instruction_data.extend_from_slice(&freeze_auth);
3363    instruction_data.push(has_freeze_auth);
3364
3365    // ticker_len and ticker_bytes (max 8 bytes)
3366    let ticker_bytes = ticker.as_bytes();
3367    if ticker_bytes.len() > 8 {
3368        return Err(anyhow::anyhow!("Ticker must be 8 characters or less"));
3369    }
3370
3371    instruction_data.push(ticker_bytes.len() as u8);
3372    let mut ticker_padded = [0u8; 8];
3373    ticker_padded[..ticker_bytes.len()].copy_from_slice(ticker_bytes);
3374    instruction_data.extend_from_slice(&ticker_padded);
3375
3376    // seed (32 bytes)
3377    instruction_data.extend_from_slice(&seed);
3378
3379    // state proof (variable length)
3380    instruction_data.extend_from_slice(&state_proof);
3381
3382    Ok(instruction_data)
3383}
3384
3385/// Build token InitializeAccount instruction data
3386fn build_token_initialize_account_instruction(
3387    token_account_idx: u16,
3388    mint_account_idx: u16,
3389    owner_account_idx: u16,
3390    seed: [u8; 32],
3391    state_proof: Vec<u8>,
3392) -> Result<Vec<u8>> {
3393    let mut instruction_data = Vec::new();
3394
3395    // Instruction tag
3396    instruction_data.push(TOKEN_INSTRUCTION_INITIALIZE_ACCOUNT);
3397
3398    // token_account_index (u16)
3399    instruction_data.extend_from_slice(&token_account_idx.to_le_bytes());
3400
3401    // mint_account_index (u16)
3402    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3403
3404    // owner_account_index (u16)
3405    instruction_data.extend_from_slice(&owner_account_idx.to_le_bytes());
3406
3407    // seed (32 bytes)
3408    instruction_data.extend_from_slice(&seed);
3409
3410    // state proof (variable length)
3411    instruction_data.extend_from_slice(&state_proof);
3412
3413    Ok(instruction_data)
3414}
3415
3416/// Build token Transfer instruction data
3417fn build_token_transfer_instruction(
3418    source_account_idx: u16,
3419    dest_account_idx: u16,
3420    amount: u64,
3421) -> Result<Vec<u8>> {
3422    let mut instruction_data = Vec::new();
3423
3424    // Instruction tag
3425    instruction_data.push(TOKEN_INSTRUCTION_TRANSFER);
3426
3427    // source_account_index (u16)
3428    instruction_data.extend_from_slice(&source_account_idx.to_le_bytes());
3429
3430    // dest_account_index (u16)
3431    instruction_data.extend_from_slice(&dest_account_idx.to_le_bytes());
3432
3433    // amount (u64)
3434    instruction_data.extend_from_slice(&amount.to_le_bytes());
3435
3436    Ok(instruction_data)
3437}
3438
3439/// Build token MintTo instruction data
3440fn build_token_mint_to_instruction(
3441    mint_account_idx: u16,
3442    dest_account_idx: u16,
3443    authority_idx: u16,
3444    amount: u64,
3445) -> Result<Vec<u8>> {
3446    let mut instruction_data = Vec::new();
3447
3448    // Instruction tag
3449    instruction_data.push(TOKEN_INSTRUCTION_MINT_TO);
3450
3451    // mint_account_index (u16)
3452    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3453
3454    // dest_account_index (u16)
3455    instruction_data.extend_from_slice(&dest_account_idx.to_le_bytes());
3456
3457    // authority_index (u16)
3458    instruction_data.extend_from_slice(&authority_idx.to_le_bytes());
3459
3460    // amount (u64)
3461    instruction_data.extend_from_slice(&amount.to_le_bytes());
3462
3463    Ok(instruction_data)
3464}
3465
3466/// Build token Burn instruction data
3467fn build_token_burn_instruction(
3468    token_account_idx: u16,
3469    mint_account_idx: u16,
3470    authority_idx: u16,
3471    amount: u64,
3472) -> Result<Vec<u8>> {
3473    let mut instruction_data = Vec::new();
3474
3475    // Instruction tag
3476    instruction_data.push(TOKEN_INSTRUCTION_BURN);
3477
3478    // token_account_index (u16)
3479    instruction_data.extend_from_slice(&token_account_idx.to_le_bytes());
3480
3481    // mint_account_index (u16)
3482    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3483
3484    // authority_index (u16)
3485    instruction_data.extend_from_slice(&authority_idx.to_le_bytes());
3486
3487    // amount (u64)
3488    instruction_data.extend_from_slice(&amount.to_le_bytes());
3489
3490    Ok(instruction_data)
3491}
3492
3493/// Build token FreezeAccount instruction data
3494fn build_token_freeze_account_instruction(
3495    token_account_idx: u16,
3496    mint_account_idx: u16,
3497    authority_idx: u16,
3498) -> Result<Vec<u8>> {
3499    let mut instruction_data = Vec::new();
3500
3501    // Instruction tag
3502    instruction_data.push(TOKEN_INSTRUCTION_FREEZE_ACCOUNT);
3503
3504    // token_account_index (u16)
3505    instruction_data.extend_from_slice(&token_account_idx.to_le_bytes());
3506
3507    // mint_account_index (u16)
3508    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3509
3510    // authority_index (u16)
3511    instruction_data.extend_from_slice(&authority_idx.to_le_bytes());
3512
3513    Ok(instruction_data)
3514}
3515
3516/// Build token ThawAccount instruction data
3517fn build_token_thaw_account_instruction(
3518    token_account_idx: u16,
3519    mint_account_idx: u16,
3520    authority_idx: u16,
3521) -> Result<Vec<u8>> {
3522    let mut instruction_data = Vec::new();
3523
3524    // Instruction tag
3525    instruction_data.push(TOKEN_INSTRUCTION_THAW_ACCOUNT);
3526
3527    // token_account_index (u16)
3528    instruction_data.extend_from_slice(&token_account_idx.to_le_bytes());
3529
3530    // mint_account_index (u16)
3531    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3532
3533    // authority_index (u16)
3534    instruction_data.extend_from_slice(&authority_idx.to_le_bytes());
3535
3536    Ok(instruction_data)
3537}
3538
3539/// Build token CloseAccount instruction data
3540fn build_token_close_account_instruction(
3541    token_account_idx: u16,
3542    destination_idx: u16,
3543    authority_idx: u16,
3544) -> Result<Vec<u8>> {
3545    let mut instruction_data = Vec::new();
3546
3547    // Instruction tag
3548    instruction_data.push(TOKEN_INSTRUCTION_CLOSE_ACCOUNT);
3549
3550    // token_account_index (u16)
3551    instruction_data.extend_from_slice(&token_account_idx.to_le_bytes());
3552
3553    // destination_index (u16)
3554    instruction_data.extend_from_slice(&destination_idx.to_le_bytes());
3555
3556    // authority_index (u16)
3557    instruction_data.extend_from_slice(&authority_idx.to_le_bytes());
3558
3559    Ok(instruction_data)
3560}
3561
3562fn build_wthru_initialize_mint_instruction(
3563    token_program_idx: u16,
3564    mint_account_idx: u16,
3565    vault_account_idx: u16,
3566    decimals: u8,
3567    mint_seed: [u8; 32],
3568    mint_proof: Vec<u8>,
3569    vault_proof: Vec<u8>,
3570) -> Result<Vec<u8>> {
3571    let mint_proof_len =
3572        u64::try_from(mint_proof.len()).map_err(|_| anyhow::anyhow!("mint proof too large"))?;
3573    let vault_proof_len =
3574        u64::try_from(vault_proof.len()).map_err(|_| anyhow::anyhow!("vault proof too large"))?;
3575
3576    let mut instruction_data = Vec::new();
3577    instruction_data.extend_from_slice(&TN_WTHRU_INSTRUCTION_INITIALIZE_MINT.to_le_bytes());
3578    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
3579    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3580    instruction_data.extend_from_slice(&vault_account_idx.to_le_bytes());
3581    instruction_data.push(decimals);
3582    instruction_data.extend_from_slice(&mint_seed);
3583    instruction_data.extend_from_slice(&mint_proof_len.to_le_bytes());
3584    instruction_data.extend_from_slice(&vault_proof_len.to_le_bytes());
3585    instruction_data.extend_from_slice(&mint_proof);
3586    instruction_data.extend_from_slice(&vault_proof);
3587
3588    Ok(instruction_data)
3589}
3590
3591fn build_wthru_deposit_instruction(
3592    token_program_idx: u16,
3593    vault_account_idx: u16,
3594    mint_account_idx: u16,
3595    dest_account_idx: u16,
3596) -> Result<Vec<u8>> {
3597    let mut instruction_data = Vec::new();
3598    instruction_data.extend_from_slice(&TN_WTHRU_INSTRUCTION_DEPOSIT.to_le_bytes());
3599    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
3600    instruction_data.extend_from_slice(&vault_account_idx.to_le_bytes());
3601    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3602    instruction_data.extend_from_slice(&dest_account_idx.to_le_bytes());
3603
3604    Ok(instruction_data)
3605}
3606
3607fn build_wthru_withdraw_instruction(
3608    token_program_idx: u16,
3609    vault_account_idx: u16,
3610    mint_account_idx: u16,
3611    wthru_token_account_idx: u16,
3612    owner_account_idx: u16,
3613    recipient_account_idx: u16,
3614    amount: u64,
3615) -> Result<Vec<u8>> {
3616    let mut instruction_data = Vec::new();
3617    instruction_data.extend_from_slice(&TN_WTHRU_INSTRUCTION_WITHDRAW.to_le_bytes());
3618    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
3619    instruction_data.extend_from_slice(&vault_account_idx.to_le_bytes());
3620    instruction_data.extend_from_slice(&mint_account_idx.to_le_bytes());
3621    instruction_data.extend_from_slice(&wthru_token_account_idx.to_le_bytes());
3622    instruction_data.extend_from_slice(&owner_account_idx.to_le_bytes());
3623    instruction_data.extend_from_slice(&recipient_account_idx.to_le_bytes());
3624    instruction_data.extend_from_slice(&amount.to_le_bytes());
3625
3626    Ok(instruction_data)
3627}
3628
3629/// Build faucet Deposit instruction data
3630fn build_faucet_deposit_instruction(
3631    faucet_account_idx: u16,
3632    depositor_account_idx: u16,
3633    eoa_program_idx: u16,
3634    amount: u64,
3635) -> Result<Vec<u8>> {
3636    let mut instruction_data = Vec::new();
3637
3638    // Discriminant: TN_FAUCET_INSTRUCTION_DEPOSIT = 0 (u32, 4 bytes little-endian)
3639    instruction_data.extend_from_slice(&0u32.to_le_bytes());
3640
3641    // tn_faucet_deposit_args_t structure:
3642    // - faucet_account_idx (u16, 2 bytes little-endian)
3643    instruction_data.extend_from_slice(&faucet_account_idx.to_le_bytes());
3644
3645    // - depositor_account_idx (u16, 2 bytes little-endian)
3646    instruction_data.extend_from_slice(&depositor_account_idx.to_le_bytes());
3647
3648    // - eoa_program_idx (u16, 2 bytes little-endian)
3649    instruction_data.extend_from_slice(&eoa_program_idx.to_le_bytes());
3650
3651    // - amount (u64, 8 bytes little-endian)
3652    instruction_data.extend_from_slice(&amount.to_le_bytes());
3653
3654    Ok(instruction_data)
3655}
3656
3657/// Build faucet Withdraw instruction data
3658fn build_faucet_withdraw_instruction(
3659    faucet_account_idx: u16,
3660    recipient_account_idx: u16,
3661    amount: u64,
3662) -> Result<Vec<u8>> {
3663    let mut instruction_data = Vec::new();
3664
3665    // Discriminant: TN_FAUCET_INSTRUCTION_WITHDRAW = 1 (u32, 4 bytes little-endian)
3666    instruction_data.extend_from_slice(&1u32.to_le_bytes());
3667
3668    // tn_faucet_withdraw_args_t structure:
3669    // - faucet_account_idx (u16, 2 bytes little-endian)
3670    instruction_data.extend_from_slice(&faucet_account_idx.to_le_bytes());
3671
3672    // - recipient_account_idx (u16, 2 bytes little-endian)
3673    instruction_data.extend_from_slice(&recipient_account_idx.to_le_bytes());
3674
3675    // - amount (u64, 8 bytes little-endian)
3676    instruction_data.extend_from_slice(&amount.to_le_bytes());
3677
3678    Ok(instruction_data)
3679}
3680
3681#[repr(C, packed)]
3682struct NameServiceInitializeRootArgs {
3683    registrar_account_idx: u16,
3684    authority_account_idx: u16,
3685    root_name: [u8; TN_NAME_SERVICE_MAX_DOMAIN_LENGTH],
3686    root_name_length: u32,
3687}
3688
3689#[repr(C, packed)]
3690struct NameServiceRegisterSubdomainArgs {
3691    domain_account_idx: u16,
3692    parent_account_idx: u16,
3693    owner_account_idx: u16,
3694    authority_account_idx: u16,
3695    name: [u8; TN_NAME_SERVICE_MAX_DOMAIN_LENGTH],
3696    name_length: u32,
3697}
3698
3699#[repr(C, packed)]
3700struct NameServiceAppendRecordArgs {
3701    domain_account_idx: u16,
3702    owner_account_idx: u16,
3703    key_length: u32,
3704    key: [u8; TN_NAME_SERVICE_MAX_KEY_LENGTH],
3705    value_length: u32,
3706    value: [u8; TN_NAME_SERVICE_MAX_VALUE_LENGTH],
3707}
3708
3709#[repr(C, packed)]
3710struct NameServiceDeleteRecordArgs {
3711    domain_account_idx: u16,
3712    owner_account_idx: u16,
3713    key_length: u32,
3714    key: [u8; TN_NAME_SERVICE_MAX_KEY_LENGTH],
3715}
3716
3717#[repr(C, packed)]
3718struct NameServiceUnregisterSubdomainArgs {
3719    domain_account_idx: u16,
3720    owner_account_idx: u16,
3721}
3722
3723/// Build name service InitializeRoot instruction data
3724fn build_name_service_initialize_root_instruction(
3725    registrar_account_idx: u16,
3726    authority_account_idx: u16,
3727    root_name: &str,
3728    state_proof: Vec<u8>,
3729) -> Result<Vec<u8>> {
3730    let root_name_bytes = root_name.as_bytes();
3731    if root_name_bytes.is_empty()
3732        || root_name_bytes.len() > TN_NAME_SERVICE_MAX_DOMAIN_LENGTH
3733    {
3734        return Err(anyhow::anyhow!(
3735            "Root name length must be between 1 and {}",
3736            TN_NAME_SERVICE_MAX_DOMAIN_LENGTH
3737        ));
3738    }
3739
3740    let mut args = NameServiceInitializeRootArgs {
3741        registrar_account_idx,
3742        authority_account_idx,
3743        root_name: [0u8; TN_NAME_SERVICE_MAX_DOMAIN_LENGTH],
3744        root_name_length: root_name_bytes.len() as u32,
3745    };
3746    args.root_name[..root_name_bytes.len()].copy_from_slice(root_name_bytes);
3747
3748    let mut instruction_data = Vec::new();
3749    instruction_data.extend_from_slice(&TN_NAME_SERVICE_INSTRUCTION_INITIALIZE_ROOT.to_le_bytes());
3750
3751    let args_bytes = unsafe {
3752        std::slice::from_raw_parts(
3753            &args as *const _ as *const u8,
3754            std::mem::size_of::<NameServiceInitializeRootArgs>(),
3755        )
3756    };
3757    instruction_data.extend_from_slice(args_bytes);
3758
3759    instruction_data.extend_from_slice(&TN_NAME_SERVICE_PROOF_INLINE.to_le_bytes());
3760    instruction_data.extend_from_slice(&state_proof);
3761
3762    Ok(instruction_data)
3763}
3764
3765/// Build name service RegisterSubdomain instruction data
3766fn build_name_service_register_subdomain_instruction(
3767    domain_account_idx: u16,
3768    parent_account_idx: u16,
3769    owner_account_idx: u16,
3770    authority_account_idx: u16,
3771    domain_name: &str,
3772    state_proof: Vec<u8>,
3773) -> Result<Vec<u8>> {
3774    let domain_bytes = domain_name.as_bytes();
3775    if domain_bytes.is_empty()
3776        || domain_bytes.len() > TN_NAME_SERVICE_MAX_DOMAIN_LENGTH
3777    {
3778        return Err(anyhow::anyhow!(
3779            "Domain name length must be between 1 and {}",
3780            TN_NAME_SERVICE_MAX_DOMAIN_LENGTH
3781        ));
3782    }
3783
3784    let mut args = NameServiceRegisterSubdomainArgs {
3785        domain_account_idx,
3786        parent_account_idx,
3787        owner_account_idx,
3788        authority_account_idx,
3789        name: [0u8; TN_NAME_SERVICE_MAX_DOMAIN_LENGTH],
3790        name_length: domain_bytes.len() as u32,
3791    };
3792    args.name[..domain_bytes.len()].copy_from_slice(domain_bytes);
3793
3794    let mut instruction_data = Vec::new();
3795    instruction_data.extend_from_slice(&TN_NAME_SERVICE_INSTRUCTION_REGISTER_SUBDOMAIN.to_le_bytes());
3796
3797    let args_bytes = unsafe {
3798        std::slice::from_raw_parts(
3799            &args as *const _ as *const u8,
3800            std::mem::size_of::<NameServiceRegisterSubdomainArgs>(),
3801        )
3802    };
3803    instruction_data.extend_from_slice(args_bytes);
3804
3805    instruction_data.extend_from_slice(&TN_NAME_SERVICE_PROOF_INLINE.to_le_bytes());
3806    instruction_data.extend_from_slice(&state_proof);
3807
3808    Ok(instruction_data)
3809}
3810
3811/// Build name service AppendRecord instruction data
3812fn build_name_service_append_record_instruction(
3813    domain_account_idx: u16,
3814    owner_account_idx: u16,
3815    key: &[u8],
3816    value: &[u8],
3817) -> Result<Vec<u8>> {
3818    if key.is_empty() || key.len() > TN_NAME_SERVICE_MAX_KEY_LENGTH {
3819        return Err(anyhow::anyhow!(
3820            "Key length must be between 1 and {} bytes",
3821            TN_NAME_SERVICE_MAX_KEY_LENGTH
3822        ));
3823    }
3824    if value.len() > TN_NAME_SERVICE_MAX_VALUE_LENGTH {
3825        return Err(anyhow::anyhow!(
3826            "Value length must be <= {} bytes",
3827            TN_NAME_SERVICE_MAX_VALUE_LENGTH
3828        ));
3829    }
3830
3831    let mut args = NameServiceAppendRecordArgs {
3832        domain_account_idx,
3833        owner_account_idx,
3834        key_length: key.len() as u32,
3835        key: [0u8; TN_NAME_SERVICE_MAX_KEY_LENGTH],
3836        value_length: value.len() as u32,
3837        value: [0u8; TN_NAME_SERVICE_MAX_VALUE_LENGTH],
3838    };
3839    args.key[..key.len()].copy_from_slice(key);
3840    args.value[..value.len()].copy_from_slice(value);
3841
3842    let mut instruction_data = Vec::new();
3843    instruction_data.extend_from_slice(&TN_NAME_SERVICE_INSTRUCTION_APPEND_RECORD.to_le_bytes());
3844
3845    let args_bytes = unsafe {
3846        std::slice::from_raw_parts(
3847            &args as *const _ as *const u8,
3848            std::mem::size_of::<NameServiceAppendRecordArgs>(),
3849        )
3850    };
3851    instruction_data.extend_from_slice(args_bytes);
3852
3853    Ok(instruction_data)
3854}
3855
3856/// Build name service DeleteRecord instruction data
3857fn build_name_service_delete_record_instruction(
3858    domain_account_idx: u16,
3859    owner_account_idx: u16,
3860    key: &[u8],
3861) -> Result<Vec<u8>> {
3862    if key.is_empty() || key.len() > TN_NAME_SERVICE_MAX_KEY_LENGTH {
3863        return Err(anyhow::anyhow!(
3864            "Key length must be between 1 and {} bytes",
3865            TN_NAME_SERVICE_MAX_KEY_LENGTH
3866        ));
3867    }
3868
3869    let mut args = NameServiceDeleteRecordArgs {
3870        domain_account_idx,
3871        owner_account_idx,
3872        key_length: key.len() as u32,
3873        key: [0u8; TN_NAME_SERVICE_MAX_KEY_LENGTH],
3874    };
3875    args.key[..key.len()].copy_from_slice(key);
3876
3877    let mut instruction_data = Vec::new();
3878    instruction_data.extend_from_slice(&TN_NAME_SERVICE_INSTRUCTION_DELETE_RECORD.to_le_bytes());
3879
3880    let args_bytes = unsafe {
3881        std::slice::from_raw_parts(
3882            &args as *const _ as *const u8,
3883            std::mem::size_of::<NameServiceDeleteRecordArgs>(),
3884        )
3885    };
3886    instruction_data.extend_from_slice(args_bytes);
3887
3888    Ok(instruction_data)
3889}
3890
3891/// Build name service UnregisterSubdomain instruction data
3892fn build_name_service_unregister_subdomain_instruction(
3893    domain_account_idx: u16,
3894    owner_account_idx: u16,
3895) -> Result<Vec<u8>> {
3896    let args = NameServiceUnregisterSubdomainArgs {
3897        domain_account_idx,
3898        owner_account_idx,
3899    };
3900
3901    let mut instruction_data = Vec::new();
3902    instruction_data.extend_from_slice(&TN_NAME_SERVICE_INSTRUCTION_UNREGISTER.to_le_bytes());
3903
3904    let args_bytes = unsafe {
3905        std::slice::from_raw_parts(
3906            &args as *const _ as *const u8,
3907            std::mem::size_of::<NameServiceUnregisterSubdomainArgs>(),
3908        )
3909    };
3910    instruction_data.extend_from_slice(args_bytes);
3911
3912    Ok(instruction_data)
3913}
3914
3915/// Build thru registrar InitializeRegistry instruction data
3916fn build_thru_registrar_initialize_registry_instruction(
3917    config_account_idx: u16,
3918    name_service_program_idx: u16,
3919    root_registrar_account_idx: u16,
3920    treasurer_account_idx: u16,
3921    token_mint_account_idx: u16,
3922    token_program_idx: u16,
3923    root_domain_name: &str,
3924    price_per_year: u64,
3925    config_proof: Vec<u8>,
3926    registrar_proof: Vec<u8>,
3927) -> Result<Vec<u8>> {
3928    let mut instruction_data = Vec::new();
3929
3930    // Discriminant: TN_THRU_REGISTRAR_INSTRUCTION_INITIALIZE_REGISTRY = 0 (u32, 4 bytes little-endian)
3931    instruction_data.extend_from_slice(&TN_THRU_REGISTRAR_INSTRUCTION_INITIALIZE_REGISTRY.to_le_bytes());
3932
3933    // tn_thru_registrar_initialize_registry_args_t structure:
3934    // - config_account_idx (u16, 2 bytes little-endian)
3935    instruction_data.extend_from_slice(&config_account_idx.to_le_bytes());
3936    // - name_service_program_account_idx (u16, 2 bytes little-endian)
3937    instruction_data.extend_from_slice(&name_service_program_idx.to_le_bytes());
3938    // - root_registrar_account_idx (u16, 2 bytes little-endian)
3939    instruction_data.extend_from_slice(&root_registrar_account_idx.to_le_bytes());
3940    // - treasurer_account_idx (u16, 2 bytes little-endian)
3941    instruction_data.extend_from_slice(&treasurer_account_idx.to_le_bytes());
3942    // - token_mint_account_idx (u16, 2 bytes little-endian)
3943    instruction_data.extend_from_slice(&token_mint_account_idx.to_le_bytes());
3944    // - token_program_account_idx (u16, 2 bytes little-endian)
3945    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
3946    // - root_domain_name (64 bytes, padded with zeros)
3947    let domain_bytes = root_domain_name.as_bytes();
3948    if domain_bytes.len() > 64 {
3949        return Err(anyhow::anyhow!("Root domain name must be 64 characters or less"));
3950    }
3951    let mut domain_padded = [0u8; 64];
3952    domain_padded[..domain_bytes.len()].copy_from_slice(domain_bytes);
3953    instruction_data.extend_from_slice(&domain_padded);
3954    // - root_domain_name_length (u32, 4 bytes little-endian)
3955    instruction_data.extend_from_slice(&(domain_bytes.len() as u32).to_le_bytes());
3956    // - price_per_year (u64, 8 bytes little-endian)
3957    instruction_data.extend_from_slice(&price_per_year.to_le_bytes());
3958
3959    // Variable-length proofs follow:
3960    // - config_proof (variable length)
3961    instruction_data.extend_from_slice(&config_proof);
3962    // - registrar_proof (variable length)
3963    instruction_data.extend_from_slice(&registrar_proof);
3964
3965    Ok(instruction_data)
3966}
3967
3968/// Build thru registrar PurchaseDomain instruction data
3969fn build_thru_registrar_purchase_domain_instruction(
3970    config_account_idx: u16,
3971    lease_account_idx: u16,
3972    domain_account_idx: u16,
3973    name_service_program_idx: u16,
3974    root_registrar_account_idx: u16,
3975    treasurer_account_idx: u16,
3976    payer_token_account_idx: u16,
3977    token_mint_account_idx: u16,
3978    token_program_idx: u16,
3979    domain_name: &str,
3980    years: u8,
3981    lease_proof: Vec<u8>,
3982    domain_proof: Vec<u8>,
3983) -> Result<Vec<u8>> {
3984    let mut instruction_data = Vec::new();
3985
3986    // Discriminant: TN_THRU_REGISTRAR_INSTRUCTION_PURCHASE_DOMAIN = 1 (u32, 4 bytes little-endian)
3987    instruction_data.extend_from_slice(&TN_THRU_REGISTRAR_INSTRUCTION_PURCHASE_DOMAIN.to_le_bytes());
3988
3989    // tn_thru_registrar_purchase_domain_args_t structure:
3990    // - config_account_idx (u16, 2 bytes little-endian)
3991    instruction_data.extend_from_slice(&config_account_idx.to_le_bytes());
3992    // - lease_account_idx (u16, 2 bytes little-endian)
3993    instruction_data.extend_from_slice(&lease_account_idx.to_le_bytes());
3994    // - domain_account_idx (u16, 2 bytes little-endian)
3995    instruction_data.extend_from_slice(&domain_account_idx.to_le_bytes());
3996    // - name_service_program_account_idx (u16, 2 bytes little-endian)
3997    instruction_data.extend_from_slice(&name_service_program_idx.to_le_bytes());
3998    // - root_registrar_account_idx (u16, 2 bytes little-endian)
3999    instruction_data.extend_from_slice(&root_registrar_account_idx.to_le_bytes());
4000    // - treasurer_account_idx (u16, 2 bytes little-endian)
4001    instruction_data.extend_from_slice(&treasurer_account_idx.to_le_bytes());
4002    // - payer_token_account_idx (u16, 2 bytes little-endian)
4003    instruction_data.extend_from_slice(&payer_token_account_idx.to_le_bytes());
4004    // - token_mint_account_idx (u16, 2 bytes little-endian)
4005    instruction_data.extend_from_slice(&token_mint_account_idx.to_le_bytes());
4006    // - token_program_account_idx (u16, 2 bytes little-endian)
4007    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
4008    // - domain_name (64 bytes, padded with zeros)
4009    let domain_bytes = domain_name.as_bytes();
4010    if domain_bytes.len() > 64 {
4011        return Err(anyhow::anyhow!("Domain name must be 64 characters or less"));
4012    }
4013    let mut domain_padded = [0u8; 64];
4014    domain_padded[..domain_bytes.len()].copy_from_slice(domain_bytes);
4015    instruction_data.extend_from_slice(&domain_padded);
4016    // - domain_name_length (u32, 4 bytes little-endian)
4017    instruction_data.extend_from_slice(&(domain_bytes.len() as u32).to_le_bytes());
4018    // - years (u8, 1 byte)
4019    instruction_data.push(years);
4020
4021    // Variable-length proofs follow:
4022    // - lease_proof (variable length)
4023    instruction_data.extend_from_slice(&lease_proof);
4024    // - domain_proof (variable length)
4025    instruction_data.extend_from_slice(&domain_proof);
4026
4027    Ok(instruction_data)
4028}
4029
4030/// Build thru registrar RenewLease instruction data
4031fn build_thru_registrar_renew_lease_instruction(
4032    config_account_idx: u16,
4033    lease_account_idx: u16,
4034    treasurer_account_idx: u16,
4035    payer_token_account_idx: u16,
4036    token_mint_account_idx: u16,
4037    token_program_idx: u16,
4038    years: u8,
4039) -> Result<Vec<u8>> {
4040    let mut instruction_data = Vec::new();
4041
4042    // Discriminant: TN_THRU_REGISTRAR_INSTRUCTION_RENEW_LEASE = 2 (u32, 4 bytes little-endian)
4043    instruction_data.extend_from_slice(&TN_THRU_REGISTRAR_INSTRUCTION_RENEW_LEASE.to_le_bytes());
4044
4045    // tn_thru_registrar_renew_lease_args_t structure:
4046    // - config_account_idx (u16, 2 bytes little-endian)
4047    instruction_data.extend_from_slice(&config_account_idx.to_le_bytes());
4048    // - lease_account_idx (u16, 2 bytes little-endian)
4049    instruction_data.extend_from_slice(&lease_account_idx.to_le_bytes());
4050    // - treasurer_account_idx (u16, 2 bytes little-endian)
4051    instruction_data.extend_from_slice(&treasurer_account_idx.to_le_bytes());
4052    // - payer_token_account_idx (u16, 2 bytes little-endian)
4053    instruction_data.extend_from_slice(&payer_token_account_idx.to_le_bytes());
4054    // - token_mint_account_idx (u16, 2 bytes little-endian)
4055    instruction_data.extend_from_slice(&token_mint_account_idx.to_le_bytes());
4056    // - token_program_account_idx (u16, 2 bytes little-endian)
4057    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
4058    // - years (u8, 1 byte)
4059    instruction_data.push(years);
4060
4061    Ok(instruction_data)
4062}
4063
4064/// Build thru registrar ClaimExpiredDomain instruction data
4065fn build_thru_registrar_claim_expired_domain_instruction(
4066    config_account_idx: u16,
4067    lease_account_idx: u16,
4068    treasurer_account_idx: u16,
4069    payer_token_account_idx: u16,
4070    token_mint_account_idx: u16,
4071    token_program_idx: u16,
4072    years: u8,
4073) -> Result<Vec<u8>> {
4074    let mut instruction_data = Vec::new();
4075
4076    // Discriminant: TN_THRU_REGISTRAR_INSTRUCTION_CLAIM_EXPIRED_DOMAIN = 3 (u32, 4 bytes little-endian)
4077    instruction_data.extend_from_slice(&TN_THRU_REGISTRAR_INSTRUCTION_CLAIM_EXPIRED_DOMAIN.to_le_bytes());
4078
4079    // tn_thru_registrar_claim_expired_domain_args_t structure:
4080    // - config_account_idx (u16, 2 bytes little-endian)
4081    instruction_data.extend_from_slice(&config_account_idx.to_le_bytes());
4082    // - lease_account_idx (u16, 2 bytes little-endian)
4083    instruction_data.extend_from_slice(&lease_account_idx.to_le_bytes());
4084    // - treasurer_account_idx (u16, 2 bytes little-endian)
4085    instruction_data.extend_from_slice(&treasurer_account_idx.to_le_bytes());
4086    // - payer_token_account_idx (u16, 2 bytes little-endian)
4087    instruction_data.extend_from_slice(&payer_token_account_idx.to_le_bytes());
4088    // - token_mint_account_idx (u16, 2 bytes little-endian)
4089    instruction_data.extend_from_slice(&token_mint_account_idx.to_le_bytes());
4090    // - token_program_account_idx (u16, 2 bytes little-endian)
4091    instruction_data.extend_from_slice(&token_program_idx.to_le_bytes());
4092    // - years (u8, 1 byte)
4093    instruction_data.push(years);
4094
4095    Ok(instruction_data)
4096}