1#[cfg(feature = "chain-rpc")]
2use solana_rpc_client::nonblocking::rpc_client::RpcClient;
3#[cfg(feature = "chain-rpc")]
4use solana_sdk::account::Account;
5use solana_sdk::{
6 instruction::{AccountMeta, Instruction},
7 pubkey::Pubkey,
8};
9
10#[cfg(feature = "chain-rpc")]
11use super::ChainError;
12
13pub const DEFAULT_SOL_RESERVE_LAMPORTS: u64 = 1_200_000;
15pub const NATIVE_MINT: Pubkey = solana_sdk::pubkey!("So11111111111111111111111111111111111111112");
16
17pub(crate) const TOKEN_PROGRAM_ID: Pubkey =
18 solana_sdk::pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA");
19const SYSTEM_PROGRAM_ID: Pubkey = solana_sdk::pubkey!("11111111111111111111111111111111");
20const SPL_TOKEN_SYNC_NATIVE: u8 = 17;
21const SPL_TOKEN_CLOSE_ACCOUNT: u8 = 9;
22const SYSTEM_INSTRUCTION_TRANSFER: u32 = 2;
23const SYSTEM_INSTRUCTION_CREATE_ACCOUNT_WITH_SEED: u32 = 3;
24const SPL_TOKEN_TRANSFER: u8 = 3;
25const SPL_TOKEN_INITIALIZE_ACCOUNT3: u8 = 18;
26const TOKEN_ACCOUNT_RENT_LAMPORTS: u64 = 2_039_280;
28const UNWRAP_SEED: &str = "acta-unwrap";
30#[cfg(feature = "chain-rpc")]
31const TOKEN_ACCOUNT_MINT_OFFSET: usize = 0;
32#[cfg(feature = "chain-rpc")]
33const TOKEN_ACCOUNT_OWNER_OFFSET: usize = 32;
34#[cfg(feature = "chain-rpc")]
35const TOKEN_ACCOUNT_AMOUNT_OFFSET: usize = 64;
36#[cfg(feature = "chain-rpc")]
37const TOKEN_ACCOUNT_BASE_LEN: usize = 72;
38#[cfg(feature = "chain-rpc")]
39const TOKEN_ACCOUNT_RENT_LEN: usize = 165;
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
42pub enum NativeSolFunding {
43 #[default]
44 Disabled,
45 WrapIfNeeded {
46 reserve_lamports: u64,
47 },
48}
49
50impl NativeSolFunding {
51 #[must_use]
52 pub const fn with_default_reserve() -> Self {
53 Self::WrapIfNeeded {
54 reserve_lamports: DEFAULT_SOL_RESERVE_LAMPORTS,
55 }
56 }
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60#[cfg(feature = "chain-rpc")]
61pub(crate) enum WrapDecision {
62 NotNeeded,
63 Wrap {
64 wrap_lamports: u64,
65 },
66 InsufficientNativeSol {
67 needed_lamports: u64,
68 available_lamports: u64,
69 },
70}
71
72#[cfg(feature = "chain-rpc")]
73pub(crate) enum WrapPlan {
74 NotNeeded,
75 Wrap {
76 instructions: Vec<Instruction>,
77 budget: NativeSolBudget,
78 },
79}
80
81#[cfg(feature = "chain-rpc")]
82pub(crate) struct WsolWrapRequest {
83 pub(crate) owner: Pubkey,
84 pub(crate) funding_ata: Pubkey,
85 pub(crate) token_program: Pubkey,
86 pub(crate) expected_settlement: u64,
87 pub(crate) reserve_lamports: u64,
88 pub(crate) allow_account_creation: bool,
89 pub(crate) fee_payer: Pubkey,
90 pub(crate) additional_ata_rent_lamports: u64,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94#[cfg(feature = "chain-rpc")]
95pub(crate) struct NativeSolBudget {
96 pub(crate) owner: Pubkey,
97 wrap_lamports: u64,
98 ata_rent_lamports: u64,
99 reserve_lamports: u64,
100}
101
102#[cfg(feature = "chain-rpc")]
103impl NativeSolBudget {
104 fn required_lamports(self, fee_lamports: u64) -> Result<u64, ChainError> {
105 self.wrap_lamports
106 .checked_add(self.ata_rent_lamports)
107 .and_then(|value| value.checked_add(fee_lamports))
108 .and_then(|value| value.checked_add(self.reserve_lamports))
109 .ok_or(ChainError::NativeSolBudgetOverflow)
110 }
111
112 pub(crate) fn validate(
113 self,
114 available_lamports: u64,
115 fee_lamports: u64,
116 ) -> Result<(), ChainError> {
117 let needed_lamports = self.required_lamports(fee_lamports)?;
118 if available_lamports < needed_lamports {
119 return Err(ChainError::InsufficientNativeSol {
120 needed_lamports,
121 available_lamports,
122 });
123 }
124 Ok(())
125 }
126}
127
128#[cfg(feature = "chain-rpc")]
129pub(crate) fn is_native_mint(mint: &Pubkey) -> bool {
130 *mint == NATIVE_MINT
131}
132
133#[cfg(feature = "chain-rpc")]
134pub(crate) fn decide_wrap(
135 existing_wsol: u64,
136 expected_settlement: u64,
137 sol_balance: u64,
138 reserve_lamports: u64,
139 ata_rent_lamports: u64,
140) -> WrapDecision {
141 if existing_wsol >= expected_settlement {
142 return WrapDecision::NotNeeded;
143 }
144 let wrap_lamports = expected_settlement - existing_wsol;
145 let needed_lamports = wrap_lamports
146 .saturating_add(ata_rent_lamports)
147 .saturating_add(reserve_lamports);
148 if sol_balance < needed_lamports {
149 return WrapDecision::InsufficientNativeSol {
150 needed_lamports,
151 available_lamports: sol_balance,
152 };
153 }
154 WrapDecision::Wrap { wrap_lamports }
155}
156
157#[cfg(feature = "chain-rpc")]
158pub(crate) async fn plan_wsol_wrap(
159 rpc: &RpcClient,
160 account: Option<Account>,
161 request: WsolWrapRequest,
162) -> Result<WrapPlan, ChainError> {
163 let WsolWrapRequest {
164 owner,
165 funding_ata,
166 token_program,
167 expected_settlement,
168 reserve_lamports,
169 allow_account_creation,
170 fee_payer,
171 additional_ata_rent_lamports,
172 } = request;
173 let (existing_wsol, ata_rent_lamports) = match account {
174 Some(account) => (parse_wsol_balance(&account, &owner, &token_program)?, 0),
175 None if allow_account_creation => {
176 let rent = if fee_payer == owner {
177 rpc.get_minimum_balance_for_rent_exemption(TOKEN_ACCOUNT_RENT_LEN)
178 .await?
179 } else {
180 0
181 };
182 (0, rent)
183 }
184 None => return Err(ChainError::MissingFundingAccount(funding_ata)),
185 };
186 if existing_wsol >= expected_settlement {
187 return Ok(WrapPlan::NotNeeded);
188 }
189 let sol_balance = rpc.get_balance(&owner).await?;
190
191 match decide_wrap(
192 existing_wsol,
193 expected_settlement,
194 sol_balance,
195 reserve_lamports,
196 ata_rent_lamports.saturating_add(additional_ata_rent_lamports),
197 ) {
198 WrapDecision::NotNeeded => Ok(WrapPlan::NotNeeded),
199 WrapDecision::InsufficientNativeSol {
200 needed_lamports,
201 available_lamports,
202 } => Err(ChainError::InsufficientNativeSol {
203 needed_lamports,
204 available_lamports,
205 }),
206 WrapDecision::Wrap { wrap_lamports } => Ok(WrapPlan::Wrap {
207 instructions: vec![
208 build_system_transfer_ix(&owner, &funding_ata, wrap_lamports),
209 build_sync_native_ix(&token_program, &funding_ata),
210 ],
211 budget: NativeSolBudget {
212 owner,
213 wrap_lamports,
214 ata_rent_lamports: ata_rent_lamports
215 .checked_add(additional_ata_rent_lamports)
216 .ok_or(ChainError::NativeSolBudgetOverflow)?,
217 reserve_lamports,
218 },
219 }),
220 }
221}
222
223#[cfg(feature = "chain-rpc")]
224pub(crate) async fn token_account_rent_lamports(rpc: &RpcClient) -> Result<u64, ChainError> {
225 rpc.get_minimum_balance_for_rent_exemption(TOKEN_ACCOUNT_RENT_LEN)
226 .await
227 .map_err(ChainError::from)
228}
229
230#[cfg(feature = "chain-rpc")]
231fn parse_wsol_balance(
232 account: &Account,
233 expected_owner: &Pubkey,
234 token_program: &Pubkey,
235) -> Result<u64, ChainError> {
236 if account.owner != *token_program || account.data.len() < TOKEN_ACCOUNT_BASE_LEN {
237 return Err(ChainError::InvalidAccountData);
238 }
239 if read_pubkey(&account.data, TOKEN_ACCOUNT_MINT_OFFSET)? != NATIVE_MINT
240 || read_pubkey(&account.data, TOKEN_ACCOUNT_OWNER_OFFSET)? != *expected_owner
241 {
242 return Err(ChainError::InvalidAccountData);
243 }
244 read_u64(&account.data, TOKEN_ACCOUNT_AMOUNT_OFFSET)
245}
246
247#[cfg(feature = "chain-rpc")]
248fn read_pubkey(data: &[u8], offset: usize) -> Result<Pubkey, ChainError> {
249 let bytes: [u8; 32] = data
250 .get(offset..offset + 32)
251 .ok_or(ChainError::InvalidAccountData)?
252 .try_into()
253 .map_err(|_| ChainError::InvalidAccountData)?;
254 Ok(Pubkey::new_from_array(bytes))
255}
256
257#[cfg(feature = "chain-rpc")]
258fn read_u64(data: &[u8], offset: usize) -> Result<u64, ChainError> {
259 let bytes: [u8; 8] = data
260 .get(offset..offset + 8)
261 .ok_or(ChainError::InvalidAccountData)?
262 .try_into()
263 .map_err(|_| ChainError::InvalidAccountData)?;
264 Ok(u64::from_le_bytes(bytes))
265}
266
267pub fn build_wrap_sol_ixs(owner: Pubkey, fee_payer: Pubkey, lamports: u64) -> Vec<Instruction> {
269 let ata = super::ix::derive_associated_token_address(&owner, &NATIVE_MINT, &TOKEN_PROGRAM_ID);
270 vec![
271 super::ix::create_associated_token_account_idempotent_ix(
272 &fee_payer,
273 &ata,
274 &owner,
275 &NATIVE_MINT,
276 &TOKEN_PROGRAM_ID,
277 ),
278 build_system_transfer_ix(&owner, &ata, lamports),
279 build_sync_native_ix(&TOKEN_PROGRAM_ID, &ata),
280 ]
281}
282
283pub fn build_maker_deposit_native_sol_ixs(
285 program_id: &Pubkey,
286 maker_owner: Pubkey,
287 fee_payer: Pubkey,
288 lamports: u64,
289) -> Result<Vec<Instruction>, super::ChainIxError> {
290 let args = super::MakerDepositIxArgs {
291 maker_owner,
292 amount: lamports,
293 premium_mint: NATIVE_MINT,
294 token_program: TOKEN_PROGRAM_ID,
295 create_atas: true,
296 };
297 let mut ixs = super::build_maker_deposit_ixs(program_id, &args, fee_payer)?;
298 let Some(deposit) = ixs.pop() else {
299 return Ok(ixs);
300 };
301 let ata =
302 super::ix::derive_associated_token_address(&maker_owner, &NATIVE_MINT, &TOKEN_PROGRAM_ID);
303 ixs.push(build_system_transfer_ix(&maker_owner, &ata, lamports));
304 ixs.push(build_sync_native_ix(&TOKEN_PROGRAM_ID, &ata));
305 ixs.push(deposit);
306 Ok(ixs)
307}
308
309pub fn build_maker_withdraw_native_sol_ixs(
314 program_id: &Pubkey,
315 maker_owner: Pubkey,
316 fee_payer: Pubkey,
317 lamports: u64,
318) -> Result<Vec<Instruction>, super::ChainIxError> {
319 let args = super::MakerWithdrawIxArgs {
320 maker_owner,
321 amount: lamports,
322 premium_mint: NATIVE_MINT,
323 token_program: TOKEN_PROGRAM_ID,
324 create_atas: true,
325 };
326 let mut ixs = super::build_maker_withdraw_ixs(program_id, &args, fee_payer)?;
327 let ata =
328 super::ix::derive_associated_token_address(&maker_owner, &NATIVE_MINT, &TOKEN_PROGRAM_ID);
329 let temporary = unwrap_account(&maker_owner);
330 ixs.extend([
331 build_create_account_with_seed_ix(&maker_owner, &temporary),
332 build_initialize_account3_ix(&temporary, &maker_owner),
333 build_token_transfer_ix(&ata, &temporary, &maker_owner, lamports),
334 build_close_account_ix(&TOKEN_PROGRAM_ID, &temporary, &maker_owner),
335 ]);
336 Ok(ixs)
337}
338
339pub fn unwrap_account(owner: &Pubkey) -> Pubkey {
342 use sha2::{Digest, Sha256};
343 let hash = Sha256::new()
344 .chain_update(owner.as_ref())
345 .chain_update(UNWRAP_SEED.as_bytes())
346 .chain_update(TOKEN_PROGRAM_ID.as_ref())
347 .finalize();
348 Pubkey::new_from_array(hash.into())
349}
350
351fn build_create_account_with_seed_ix(owner: &Pubkey, account: &Pubkey) -> Instruction {
352 let mut data = Vec::with_capacity(4 + 32 + 8 + UNWRAP_SEED.len() + 8 + 8 + 32);
353 data.extend_from_slice(&SYSTEM_INSTRUCTION_CREATE_ACCOUNT_WITH_SEED.to_le_bytes());
354 data.extend_from_slice(owner.as_ref());
355 data.extend_from_slice(&(UNWRAP_SEED.len() as u64).to_le_bytes());
356 data.extend_from_slice(UNWRAP_SEED.as_bytes());
357 data.extend_from_slice(&TOKEN_ACCOUNT_RENT_LAMPORTS.to_le_bytes());
358 data.extend_from_slice(&(TOKEN_ACCOUNT_RENT_LEN as u64).to_le_bytes());
359 data.extend_from_slice(TOKEN_PROGRAM_ID.as_ref());
360 Instruction {
361 program_id: SYSTEM_PROGRAM_ID,
362 accounts: vec![
363 AccountMeta::new(*owner, true),
364 AccountMeta::new(*account, false),
365 AccountMeta::new_readonly(*owner, true),
366 ],
367 data,
368 }
369}
370
371fn build_initialize_account3_ix(account: &Pubkey, owner: &Pubkey) -> Instruction {
372 let mut data = Vec::with_capacity(33);
373 data.push(SPL_TOKEN_INITIALIZE_ACCOUNT3);
374 data.extend_from_slice(owner.as_ref());
375 Instruction {
376 program_id: TOKEN_PROGRAM_ID,
377 accounts: vec![
378 AccountMeta::new(*account, false),
379 AccountMeta::new_readonly(NATIVE_MINT, false),
380 ],
381 data,
382 }
383}
384
385fn build_token_transfer_ix(from: &Pubkey, to: &Pubkey, owner: &Pubkey, amount: u64) -> Instruction {
386 let mut data = Vec::with_capacity(9);
387 data.push(SPL_TOKEN_TRANSFER);
388 data.extend_from_slice(&amount.to_le_bytes());
389 Instruction {
390 program_id: TOKEN_PROGRAM_ID,
391 accounts: vec![
392 AccountMeta::new(*from, false),
393 AccountMeta::new(*to, false),
394 AccountMeta::new_readonly(*owner, true),
395 ],
396 data,
397 }
398}
399
400fn build_close_account_ix(token_program: &Pubkey, account: &Pubkey, owner: &Pubkey) -> Instruction {
401 Instruction {
402 program_id: *token_program,
403 accounts: vec![
404 AccountMeta::new(*account, false),
405 AccountMeta::new(*owner, false),
406 AccountMeta::new_readonly(*owner, true),
407 ],
408 data: vec![SPL_TOKEN_CLOSE_ACCOUNT],
409 }
410}
411
412fn build_system_transfer_ix(from: &Pubkey, to: &Pubkey, lamports: u64) -> Instruction {
413 let mut data = Vec::with_capacity(12);
414 data.extend_from_slice(&SYSTEM_INSTRUCTION_TRANSFER.to_le_bytes());
415 data.extend_from_slice(&lamports.to_le_bytes());
416 Instruction {
417 program_id: SYSTEM_PROGRAM_ID,
418 accounts: vec![AccountMeta::new(*from, true), AccountMeta::new(*to, false)],
419 data,
420 }
421}
422
423fn build_sync_native_ix(token_program: &Pubkey, native_account: &Pubkey) -> Instruction {
424 Instruction {
425 program_id: *token_program,
426 accounts: vec![AccountMeta::new(*native_account, false)],
427 data: vec![SPL_TOKEN_SYNC_NATIVE],
428 }
429}
430
431#[cfg(all(test, feature = "chain-rpc"))]
432#[path = "wsol_tests.rs"]
433mod tests;