Skip to main content

winterwallet_client/
instruction.rs

1use solana_address::Address;
2use solana_instruction::{AccountMeta, Instruction};
3use winterwallet_common::{
4    ID, MAX_CPI_INSTRUCTION_ACCOUNTS, MAX_PASSTHROUGH_ACCOUNTS, SIGNATURE_LEN, WINTERNITZ_SCALARS,
5    discriminator,
6};
7
8use crate::Error;
9
10// Compile-time check that SIGNATURE_LEN matches the core type size.
11const _: () = assert!(
12    SIGNATURE_LEN == (WINTERNITZ_SCALARS + 2) * 32,
13    "SIGNATURE_LEN must equal (WINTERNITZ_SCALARS + 2) * 32"
14);
15
16// ── Instruction builders ─────────────────────────────────────────────
17
18/// Build an Initialize instruction.
19///
20/// Accounts: `[payer (signer, writable), wallet_pda (writable), system_program]`.
21pub fn initialize(
22    payer: &Address,
23    wallet_pda: &Address,
24    signature_bytes: &[u8; SIGNATURE_LEN],
25    next_root: &[u8; 32],
26) -> Instruction {
27    let mut data = Vec::with_capacity(1 + SIGNATURE_LEN + 32);
28    data.push(discriminator::INITIALIZE);
29    data.extend_from_slice(signature_bytes);
30    data.extend_from_slice(next_root);
31
32    Instruction {
33        program_id: ID,
34        accounts: vec![
35            AccountMeta::new(*payer, true),
36            AccountMeta::new(*wallet_pda, false),
37            AccountMeta::new_readonly(solana_system_interface::program::id(), false),
38        ],
39        data,
40    }
41}
42
43/// Build an Advance instruction from pre-encoded payload and account list.
44///
45/// Use [`encode_advance`] to produce `payload` and `passthrough_accounts`
46/// atomically from inner instructions. Passing hand-crafted values risks
47/// ordering mismatches.
48pub fn advance(
49    wallet_pda: &Address,
50    passthrough_accounts: &[AccountMeta],
51    signature_bytes: &[u8; SIGNATURE_LEN],
52    new_root: &[u8; 32],
53    payload: &[u8],
54) -> Instruction {
55    let mut data = Vec::with_capacity(1 + SIGNATURE_LEN + 32 + payload.len());
56    data.push(discriminator::ADVANCE);
57    data.extend_from_slice(signature_bytes);
58    data.extend_from_slice(new_root);
59    data.extend_from_slice(payload);
60
61    let mut accounts = Vec::with_capacity(1 + passthrough_accounts.len());
62    accounts.push(AccountMeta::new(*wallet_pda, false));
63    accounts.extend_from_slice(passthrough_accounts);
64
65    Instruction {
66        program_id: ID,
67        accounts,
68        data,
69    }
70}
71
72/// Build a Withdraw instruction (for use as an inner CPI inside Advance).
73///
74/// This returns an [`Instruction`] targeting the WinterWallet program itself.
75/// It is intended to be passed to [`encode_advance`], NOT submitted as a
76/// top-level instruction. The wallet PDA is marked `is_signer = false`
77/// here — the on-chain Advance handler promotes it to signer via
78/// `invoke_signed`.
79pub fn withdraw(wallet_pda: &Address, receiver: &Address, lamports: u64) -> Instruction {
80    let mut data = Vec::with_capacity(1 + 8);
81    data.push(discriminator::WITHDRAW);
82    data.extend_from_slice(&lamports.to_le_bytes());
83
84    Instruction {
85        program_id: ID,
86        accounts: vec![
87            AccountMeta::new(*wallet_pda, false),
88            AccountMeta::new(*receiver, false),
89        ],
90        data,
91    }
92}
93
94/// Build a Close instruction (for use as an inner CPI inside Advance).
95///
96/// Sweeps all lamports from the wallet PDA to `receiver` and tears down the
97/// account: zeros data length, lamports, and reassigns owner to System.
98/// As with [`withdraw`], pass the result to [`encode_advance`] — do NOT submit
99/// top-level. The wallet PDA is promoted to signer on-chain via `invoke_signed`.
100pub fn close(wallet_pda: &Address, receiver: &Address) -> Instruction {
101    Instruction {
102        program_id: ID,
103        accounts: vec![
104            AccountMeta::new(*wallet_pda, false),
105            AccountMeta::new(*receiver, false),
106        ],
107        data: vec![discriminator::CLOSE],
108    }
109}
110
111// ── CPI payload encoding ─────────────────────────────────────────────
112
113/// The result of encoding inner CPI instructions for the Advance payload.
114///
115/// Both `data` and `accounts` are produced atomically — they MUST stay in
116/// sync. The `accounts` are ordered to match how the on-chain program
117/// consumes them: for each inner instruction, the program account comes
118/// first, then the instruction's account metas.
119pub struct AdvancePayload {
120    /// The encoded CPI payload bytes to append to Advance instruction data.
121    pub data: Vec<u8>,
122    /// The passthrough accounts to include in the Advance instruction,
123    /// ordered to match the payload.
124    pub accounts: Vec<AccountMeta>,
125}
126
127/// Encode inner CPI instructions into an Advance payload + ordered account list.
128///
129/// # Wire format
130///
131/// ```text
132/// num_instructions: u8
133/// per instruction:
134///   num_accounts: u8
135///   data_len: u16 LE
136///   data: [u8; data_len]
137/// ```
138///
139/// Accounts are NOT encoded in the payload bytes — they are passed
140/// positionally in the transaction's account list. This function produces
141/// both the payload and the correctly-ordered account metas as a single
142/// atomic return value, making it impossible to get them out of sync.
143pub fn encode_advance(inner_instructions: &[Instruction]) -> Result<AdvancePayload, Error> {
144    if inner_instructions.len() > 255 {
145        return Err(Error::PayloadTooLarge("more than 255 inner instructions"));
146    }
147
148    let total_accounts: usize = inner_instructions
149        .iter()
150        .map(|ix| 1 + ix.accounts.len())
151        .sum();
152    if total_accounts > MAX_PASSTHROUGH_ACCOUNTS {
153        return Err(Error::PayloadTooLarge(
154            "total passthrough accounts exceeds MAX_PASSTHROUGH_ACCOUNTS (128)",
155        ));
156    }
157
158    let payload_len: usize = 1 + inner_instructions
159        .iter()
160        .map(|ix| 1 + 2 + ix.data.len())
161        .sum::<usize>();
162
163    let mut data = Vec::with_capacity(payload_len);
164    let mut accounts = Vec::with_capacity(total_accounts);
165
166    data.push(inner_instructions.len() as u8);
167
168    for ix in inner_instructions {
169        if ix.accounts.len() > MAX_CPI_INSTRUCTION_ACCOUNTS {
170            return Err(Error::PayloadTooLarge(
171                "inner instruction exceeds MAX_CPI_INSTRUCTION_ACCOUNTS (16)",
172            ));
173        }
174        if ix.data.len() > u16::MAX as usize {
175            return Err(Error::PayloadTooLarge(
176                "inner instruction data exceeds u16::MAX",
177            ));
178        }
179
180        data.push(ix.accounts.len() as u8);
181        data.extend_from_slice(&(ix.data.len() as u16).to_le_bytes());
182        data.extend_from_slice(&ix.data);
183
184        accounts.push(AccountMeta::new_readonly(ix.program_id, false));
185        // Inner instruction accounts: signer flags are always false in
186        // the outer transaction. The on-chain Advance handler promotes
187        // the wallet PDA to signer via invoke_signed.
188        for meta in &ix.accounts {
189            if meta.is_writable {
190                accounts.push(AccountMeta::new(meta.pubkey, false));
191            } else {
192                accounts.push(AccountMeta::new_readonly(meta.pubkey, false));
193            }
194        }
195    }
196
197    Ok(AdvancePayload { data, accounts })
198}