Skip to main content

winterwallet_client/
transaction.rs

1use solana_address::Address;
2use solana_instruction::Instruction;
3
4use crate::Error;
5
6/// Solana legacy transaction wire-size limit.
7pub const LEGACY_TRANSACTION_SIZE_LIMIT: usize = 1232;
8
9/// Default compute unit limit used for dry-run previews where simulation is not
10/// available. Live transactions should use simulation-based CU estimation instead.
11pub const DEFAULT_ADVANCE_COMPUTE_UNIT_LIMIT: u32 = 800_000;
12
13/// Build a ComputeBudget `SetComputeUnitLimit` instruction.
14pub fn set_compute_unit_limit(units: u32) -> Instruction {
15    let mut data = Vec::with_capacity(5);
16    data.push(0x02);
17    data.extend_from_slice(&units.to_le_bytes());
18    Instruction {
19        program_id: compute_budget_program_id(),
20        accounts: vec![],
21        data,
22    }
23}
24
25/// Build a ComputeBudget `SetComputeUnitPrice` instruction.
26pub fn set_compute_unit_price(micro_lamports: u64) -> Instruction {
27    let mut data = Vec::with_capacity(9);
28    data.push(0x03);
29    data.extend_from_slice(&micro_lamports.to_le_bytes());
30    Instruction {
31        program_id: compute_budget_program_id(),
32        accounts: vec![],
33        data,
34    }
35}
36
37/// Prefix an instruction set with compute-budget instructions.
38pub fn with_compute_budget(
39    instructions: &[Instruction],
40    unit_limit: u32,
41    unit_price_micro_lamports: u64,
42) -> Vec<Instruction> {
43    let mut out = Vec::with_capacity(instructions.len() + 2);
44    out.push(set_compute_unit_limit(unit_limit));
45    out.push(set_compute_unit_price(unit_price_micro_lamports));
46    out.extend_from_slice(instructions);
47    out
48}
49
50/// Estimate the serialized size of a legacy transaction with the given payer.
51///
52/// The estimate is exact for transactions with at most 256 account keys, which
53/// is comfortably above WinterWallet's current account limits.
54pub fn estimate_legacy_transaction_size(
55    payer: &Address,
56    instructions: &[Instruction],
57) -> Result<usize, Error> {
58    let accounts = collect_accounts(payer, instructions)?;
59    let required_signatures = accounts.iter().filter(|a| a.is_signer).count();
60    let message_size = legacy_message_size(&accounts, instructions)?;
61    Ok(compact_u16_len(required_signatures) + required_signatures * 64 + message_size)
62}
63
64/// Validate legacy transaction size against Solana's 1232-byte wire limit.
65pub fn validate_legacy_transaction_size(
66    payer: &Address,
67    instructions: &[Instruction],
68) -> Result<usize, Error> {
69    let estimated = estimate_legacy_transaction_size(payer, instructions)?;
70    if estimated > LEGACY_TRANSACTION_SIZE_LIMIT {
71        return Err(Error::TransactionTooLarge {
72            estimated,
73            limit: LEGACY_TRANSACTION_SIZE_LIMIT,
74        });
75    }
76    Ok(estimated)
77}
78
79/// Ensure a transaction only requires the payer's signature.
80pub fn validate_payer_only_signers(
81    payer: &Address,
82    instructions: &[Instruction],
83) -> Result<(), Error> {
84    for ix in instructions {
85        for meta in &ix.accounts {
86            if meta.is_signer && meta.pubkey != *payer {
87                return Err(Error::UnsupportedTransaction(
88                    "transaction requires a non-payer signature",
89                ));
90            }
91        }
92    }
93    Ok(())
94}
95
96fn compute_budget_program_id() -> Address {
97    solana_address::address!("ComputeBudget111111111111111111111111111111")
98}
99
100#[derive(Clone)]
101pub struct AccountEntry {
102    pub pubkey: Address,
103    pub is_signer: bool,
104    pub is_writable: bool,
105}
106
107fn collect_accounts(
108    payer: &Address,
109    instructions: &[Instruction],
110) -> Result<Vec<AccountEntry>, Error> {
111    let mut accounts = Vec::new();
112    upsert(&mut accounts, payer, true, true);
113
114    for ix in instructions {
115        upsert(&mut accounts, &ix.program_id, false, false);
116        for meta in &ix.accounts {
117            upsert(
118                &mut accounts,
119                &meta.pubkey,
120                meta.is_signer,
121                meta.is_writable,
122            );
123        }
124    }
125
126    accounts.sort_by_key(|entry| match (entry.is_signer, entry.is_writable) {
127        (true, true) => 0,
128        (true, false) => 1,
129        (false, true) => 2,
130        (false, false) => 3,
131    });
132
133    if accounts.len() > u8::MAX as usize + 1 {
134        return Err(Error::UnsupportedTransaction(
135            "legacy message account indexes exceed u8 range",
136        ));
137    }
138
139    Ok(accounts)
140}
141
142fn legacy_message_size(
143    accounts: &[AccountEntry],
144    instructions: &[Instruction],
145) -> Result<usize, Error> {
146    let mut size = 3; // header
147    size += compact_u16_len(accounts.len());
148    size += accounts.len() * 32;
149    size += 32; // recent blockhash
150    size += compact_u16_len(instructions.len());
151
152    for ix in instructions {
153        size += 1; // program id index
154        size += compact_u16_len(ix.accounts.len());
155        size += ix.accounts.len(); // account indexes
156        size += compact_u16_len(ix.data.len());
157        size += ix.data.len();
158
159        if accounts.iter().all(|a| a.pubkey != ix.program_id) {
160            return Err(Error::UnsupportedTransaction(
161                "instruction program id missing from account list",
162            ));
163        }
164        for meta in &ix.accounts {
165            if accounts.iter().all(|a| a.pubkey != meta.pubkey) {
166                return Err(Error::UnsupportedTransaction(
167                    "instruction account missing from account list",
168                ));
169            }
170        }
171    }
172
173    Ok(size)
174}
175
176pub fn upsert(
177    accounts: &mut Vec<AccountEntry>,
178    pubkey: &Address,
179    is_signer: bool,
180    is_writable: bool,
181) {
182    if let Some(existing) = accounts.iter_mut().find(|a| a.pubkey == *pubkey) {
183        existing.is_signer |= is_signer;
184        existing.is_writable |= is_writable;
185    } else {
186        accounts.push(AccountEntry {
187            pubkey: *pubkey,
188            is_signer,
189            is_writable,
190        });
191    }
192}
193
194fn compact_u16_len(value: usize) -> usize {
195    let mut value = value;
196    let mut len = 1;
197    while value >= 0x80 {
198        value >>= 7;
199        len += 1;
200    }
201    len
202}