Skip to main content

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