1mod utils;
2
3use std::borrow::Borrow;
4use std::{collections::HashMap, future::Future, ops::Deref};
5
6use anchor_lang::{AnchorSerialize, InstructionData, ToAccountMetas};
7use gmsol_solana_utils::instruction_group::{ComputeBudgetOptions, GetInstructionsOptions};
8use gmsol_solana_utils::make_bundle_builder::MakeBundleBuilder;
9use gmsol_solana_utils::solana_client::nonblocking::rpc_client::RpcClient;
10use gmsol_solana_utils::{
11 bundle_builder::{BundleBuilder, BundleOptions},
12 transaction_builder::TransactionBuilder,
13};
14use solana_sdk::{
15 address_lookup_table::{state::AddressLookupTable, AddressLookupTableAccount},
16 hash::Hash,
17 instruction::{AccountMeta, CompiledInstruction, Instruction},
18 message::{
19 v0::{Message, MessageAddressTableLookup},
20 MessageHeader, VersionedMessage,
21 },
22 pubkey::Pubkey,
23 signer::Signer,
24};
25
26use crate::squads::{
27 pda::{get_ephemeral_signer_pda, get_proposal_pda, get_transaction_pda, get_vault_pda},
28 squads_multisig_v4::{
29 accounts::{Proposal, VaultTransaction},
30 client::{accounts, args},
31 types::{
32 ProposalCreateArgs, ProposalVoteArgs, VaultTransactionCreateArgs,
33 VaultTransactionMessage,
34 },
35 ID,
36 },
37};
38
39use utils::{get_multisig, versioned_message_to_transaction_message};
40
41pub struct SquadsVaultTransaction(VaultTransaction);
43
44impl Deref for SquadsVaultTransaction {
45 type Target = VaultTransaction;
46
47 fn deref(&self) -> &Self::Target {
48 &self.0
49 }
50}
51
52impl anchor_lang::Discriminator for SquadsVaultTransaction {
53 const DISCRIMINATOR: &'static [u8] = VaultTransaction::DISCRIMINATOR;
54}
55
56impl anchor_lang::AccountDeserialize for SquadsVaultTransaction {
57 fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
58 let inner = VaultTransaction::try_deserialize_unchecked(buf)
59 .map_err(|_err| anchor_lang::error::ErrorCode::AccountDidNotDeserialize)?;
60
61 Ok(Self(inner))
62 }
63}
64
65impl SquadsVaultTransaction {
66 pub fn to_message(&self) -> Message {
68 let message = &self.0.message;
69 let instructions = message
70 .instructions
71 .iter()
72 .map(|ix| CompiledInstruction {
73 program_id_index: ix.program_id_index,
74 accounts: ix.account_indexes.clone(),
75 data: ix.data.clone(),
76 })
77 .collect();
78 let address_table_lookups = message
79 .address_table_lookups
80 .iter()
81 .map(|atl| MessageAddressTableLookup {
82 account_key: atl.account_key,
83 writable_indexes: atl.writable_indexes.clone(),
84 readonly_indexes: atl.readonly_indexes.clone(),
85 })
86 .collect();
87 let num_non_signers = message.account_keys.len() as u8 - message.num_signers;
88 Message {
89 header: MessageHeader {
90 num_required_signatures: message.num_signers,
91 num_readonly_signed_accounts: message.num_signers - message.num_writable_signers,
92 num_readonly_unsigned_accounts: num_non_signers - message.num_writable_non_signers,
93 },
94 account_keys: message.account_keys.clone(),
95 recent_blockhash: Hash::default(),
96 instructions,
97 address_table_lookups,
98 }
99 }
100}
101
102pub struct SquadsProposal(Proposal);
104
105impl Deref for SquadsProposal {
106 type Target = Proposal;
107
108 fn deref(&self) -> &Self::Target {
109 &self.0
110 }
111}
112
113impl anchor_lang::Discriminator for SquadsProposal {
114 const DISCRIMINATOR: &'static [u8] = Proposal::DISCRIMINATOR;
115}
116
117impl anchor_lang::AccountDeserialize for SquadsProposal {
118 fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result<Self> {
119 let inner = Proposal::try_deserialize_unchecked(buf)?;
120
121 Ok(Self(inner))
122 }
123}
124
125#[derive(Debug, Clone, Default)]
127pub struct VaultTransactionOptions {
128 pub memo: Option<String>,
130 pub draft: bool,
132 pub ephemeral_signers: u8,
134}
135
136pub trait SquadsOps<C> {
138 fn squads_create_vault_transaction_and_return_data<M: Borrow<VersionedMessage>>(
140 &self,
141 multisig: &Pubkey,
142 vault_index: u8,
143 transaction_index: u64,
144 message_builder: impl FnOnce(&[Pubkey]) -> crate::Result<M>,
145 options: VaultTransactionOptions,
146 ) -> crate::Result<TransactionBuilder<C, (Pubkey, VaultTransaction)>>;
147
148 fn squads_create_vault_transaction_with_index<M: Borrow<VersionedMessage>>(
150 &self,
151 multisig: &Pubkey,
152 vault_index: u8,
153 transaction_index: u64,
154 message_builder: impl FnOnce(&[Pubkey]) -> crate::Result<M>,
155 options: VaultTransactionOptions,
156 ) -> crate::Result<TransactionBuilder<C, Pubkey>>;
157
158 fn squads_create_vault_transaction<M: Borrow<VersionedMessage>>(
160 &self,
161 multisig: &Pubkey,
162 vault_index: u8,
163 message_builder: impl FnOnce(&[Pubkey]) -> crate::Result<M>,
164 options: VaultTransactionOptions,
165 offset: Option<u64>,
166 ) -> impl Future<Output = crate::Result<TransactionBuilder<C, Pubkey>>>;
167
168 fn squads_create_vault_transaction_with_message(
170 &self,
171 multisig: &Pubkey,
172 vault_index: u8,
173 message: &VersionedMessage,
174 options: VaultTransactionOptions,
175 offset: Option<u64>,
176 ) -> impl Future<Output = crate::Result<TransactionBuilder<C, Pubkey>>> {
177 self.squads_create_vault_transaction(
178 multisig,
179 vault_index,
180 move |_| Ok(message),
181 options,
182 offset,
183 )
184 }
185
186 fn squads_approve_proposal(
188 &self,
189 multisig: &Pubkey,
190 proposal: &Pubkey,
191 memo: Option<String>,
192 ) -> crate::Result<TransactionBuilder<C>>;
193
194 fn squads_execute_vault_transaction(
196 &self,
197 multisig: &Pubkey,
198 data: VaultTransaction,
199 luts_cache: Option<&HashMap<Pubkey, AddressLookupTableAccount>>,
200 ) -> impl Future<Output = crate::Result<TransactionBuilder<C>>>;
201
202 fn squads_from_bundle<'a, T>(
204 &'a self,
205 multisig: &Pubkey,
206 vault_index: u8,
207 bundle: T,
208 ) -> Squads<'a, C, T>;
209}
210
211impl<C: Deref<Target = impl Signer> + Clone> SquadsOps<C> for crate::Client<C> {
212 fn squads_create_vault_transaction_and_return_data<M: Borrow<VersionedMessage>>(
213 &self,
214 multisig: &Pubkey,
215 vault_index: u8,
216 transaction_index: u64,
217 message_builder: impl FnOnce(&[Pubkey]) -> crate::Result<M>,
218 options: VaultTransactionOptions,
219 ) -> crate::Result<TransactionBuilder<C, (Pubkey, VaultTransaction)>> {
220 let payer = self.payer();
221 let transaction_pda = get_transaction_pda(multisig, transaction_index, Some(&ID));
222 let proposal_pda = get_proposal_pda(multisig, transaction_index, Some(&ID)).0;
223 let vault_pda = get_vault_pda(multisig, vault_index, Some(&ID));
224
225 let VaultTransactionOptions {
226 memo,
227 draft,
228 ephemeral_signers,
229 } = options;
230
231 let ephemeral_signer_accounts = (0..ephemeral_signers)
232 .map(|index| get_ephemeral_signer_pda(&transaction_pda.0, index, Some(&ID)).0)
233 .collect::<Vec<_>>();
234
235 let message = message_builder(&ephemeral_signer_accounts)?;
236 let transaction_message = versioned_message_to_transaction_message(message.borrow());
237 let rpc = self.store_transaction().pre_instructions(
238 vec![
239 Instruction {
240 program_id: ID,
241 accounts: accounts::VaultTransactionCreate {
242 creator: payer,
243 rent_payer: payer,
244 transaction: transaction_pda.0,
245 multisig: *multisig,
246 system_program: solana_sdk::system_program::id(),
247 }
248 .to_account_metas(Some(false)),
249 data: args::VaultTransactionCreate {
250 args: VaultTransactionCreateArgs {
251 ephemeral_signers,
252 vault_index,
253 memo,
254 transaction_message: transaction_message
255 .try_to_vec()
256 .map_err(crate::Error::custom)?,
257 },
258 }
259 .data(),
260 },
261 Instruction {
262 program_id: ID,
263 accounts: accounts::ProposalCreate {
264 creator: payer,
265 rent_payer: payer,
266 proposal: proposal_pda,
267 multisig: *multisig,
268 system_program: solana_sdk::system_program::id(),
269 }
270 .to_account_metas(Some(false)),
271 data: args::ProposalCreate {
272 args: ProposalCreateArgs {
273 draft,
274 transaction_index,
275 },
276 }
277 .data(),
278 },
279 ],
280 false,
281 );
282
283 let data = VaultTransaction {
284 multisig: *multisig,
285 creator: payer,
286 index: transaction_index,
287 bump: transaction_pda.1,
288 vault_index,
289 vault_bump: vault_pda.1,
290 ephemeral_signer_bumps: vec![],
291 message: transaction_message.try_into()?,
292 };
293
294 Ok(rpc.output((transaction_pda.0, data)))
295 }
296
297 fn squads_create_vault_transaction_with_index<M: Borrow<VersionedMessage>>(
298 &self,
299 multisig: &Pubkey,
300 vault_index: u8,
301 transaction_index: u64,
302 message_builder: impl FnOnce(&[Pubkey]) -> crate::Result<M>,
303 options: VaultTransactionOptions,
304 ) -> crate::Result<TransactionBuilder<C, Pubkey>> {
305 let (txn, (transaction, _)) = self
306 .squads_create_vault_transaction_and_return_data(
307 multisig,
308 vault_index,
309 transaction_index,
310 message_builder,
311 options,
312 )?
313 .swap_output(());
314
315 Ok(txn.output(transaction))
316 }
317
318 async fn squads_create_vault_transaction<M: Borrow<VersionedMessage>>(
319 &self,
320 multisig: &Pubkey,
321 vault_index: u8,
322 message_builder: impl FnOnce(&[Pubkey]) -> crate::Result<M>,
323 options: VaultTransactionOptions,
324 offset: Option<u64>,
325 ) -> crate::Result<TransactionBuilder<C, Pubkey>> {
326 let multisig_data = get_multisig(&self.store_program().rpc(), multisig)
327 .await
328 .map_err(crate::Error::custom)?;
329
330 self.squads_create_vault_transaction_with_index(
331 multisig,
332 vault_index,
333 multisig_data.transaction_index + 1 + offset.unwrap_or(0),
334 message_builder,
335 options,
336 )
337 }
338
339 fn squads_approve_proposal(
340 &self,
341 multisig: &Pubkey,
342 proposal: &Pubkey,
343 memo: Option<String>,
344 ) -> crate::Result<TransactionBuilder<C>> {
345 let txn = self
346 .store_transaction()
347 .program(ID)
348 .args(
349 args::ProposalApprove {
350 args: ProposalVoteArgs { memo },
351 }
352 .data(),
353 )
354 .accounts(
355 accounts::ProposalApprove {
356 multisig: *multisig,
357 member: self.payer(),
358 proposal: *proposal,
359 }
360 .to_account_metas(Some(false)),
361 );
362
363 Ok(txn)
364 }
365
366 async fn squads_execute_vault_transaction(
367 &self,
368 multisig: &Pubkey,
369 data: VaultTransaction,
370 luts_cache: Option<&HashMap<Pubkey, AddressLookupTableAccount>>,
371 ) -> crate::Result<TransactionBuilder<C>> {
372 let program_id = ID;
373
374 let vault_transaction = data;
375 let vault = get_vault_pda(multisig, vault_transaction.vault_index, Some(&program_id)).0;
376 let transaction =
377 get_transaction_pda(multisig, vault_transaction.index, Some(&program_id)).0;
378 let proposal = get_proposal_pda(multisig, vault_transaction.index, Some(&program_id)).0;
379
380 let (remaining_accounts, luts) = message_to_execute_account_metas(
381 &self.store_program().rpc(),
382 vault_transaction.message,
383 vault_transaction.ephemeral_signer_bumps,
384 &vault,
385 &transaction,
386 Some(&program_id),
387 luts_cache,
388 )
389 .await;
390
391 let txn = self
392 .store_transaction()
393 .program(ID)
394 .args(args::VaultTransactionExecute {}.data())
395 .accounts(
396 accounts::VaultTransactionExecute {
397 multisig: *multisig,
398 proposal,
399 transaction,
400 member: self.payer(),
401 }
402 .to_account_metas(Some(false)),
403 )
404 .accounts(remaining_accounts)
405 .lookup_tables(luts.into_iter().map(|lut| (lut.key, lut.addresses)));
406
407 Ok(txn)
408 }
409
410 fn squads_from_bundle<'a, T>(
411 &'a self,
412 multisig: &Pubkey,
413 vault_index: u8,
414 bundle: T,
415 ) -> Squads<'a, C, T> {
416 Squads {
417 client: self,
418 multisig: *multisig,
419 vault_index,
420 builder: bundle,
421 approve: false,
422 execute: false,
423 }
424 }
425}
426
427#[derive(Clone)]
429pub struct Squads<'a, C, T> {
430 client: &'a crate::Client<C>,
431 multisig: Pubkey,
432 vault_index: u8,
433 builder: T,
434 approve: bool,
435 execute: bool,
436}
437
438impl<C, T> Squads<'_, C, T> {
439 pub fn approve(&mut self, approve: bool) -> &mut Self {
441 self.approve = approve;
442 self
443 }
444
445 pub fn execute(&mut self, execute: bool) -> &mut Self {
447 self.execute = execute;
448 self
449 }
450}
451
452impl<'a, C: Deref<Target = impl Signer> + Clone, T> MakeBundleBuilder<'a, C> for Squads<'a, C, T>
453where
454 T: MakeBundleBuilder<'a, C>,
455{
456 async fn build_with_options(
457 &mut self,
458 options: BundleOptions,
459 ) -> gmsol_solana_utils::Result<BundleBuilder<'a, C>> {
460 let inner = self.builder.build_with_options(options).await?;
461
462 let mut luts_cache = HashMap::<_, _>::default();
463
464 let multisig_data = get_multisig(&self.client.store_program().rpc(), &self.multisig)
465 .await
466 .map_err(gmsol_solana_utils::Error::custom)?;
467 let mut txn_idx = multisig_data.transaction_index;
468
469 let mut bundle = inner.try_clone_empty()?;
470
471 let mut transactions = vec![];
472 let mut transaction_indexes = vec![];
473 let mut transaction_datas = vec![];
474 let mut compute_budgets = vec![];
475
476 let tg = inner.build()?.into_group();
477
478 let luts = tg.luts();
479
480 for (key, addresses) in luts.iter() {
481 luts_cache.entry(*key).or_insert(AddressLookupTableAccount {
482 key: *key,
483 addresses: addresses.clone(),
484 });
485 }
486
487 for ag in tg.groups().iter().flat_map(|pg| pg.iter()) {
488 txn_idx += 1;
489 let message = ag.message_with_blockhash_and_options(
490 Default::default(),
491 GetInstructionsOptions {
492 compute_budget: ComputeBudgetOptions {
493 without_compute_budget: true,
494 ..Default::default()
495 },
496 ..Default::default()
497 },
498 Some(luts),
499 )?;
500 let (rpc, (transaction, data)) = self
501 .client
502 .squads_create_vault_transaction_and_return_data(
503 &self.multisig,
504 self.vault_index,
505 txn_idx,
506 |_| Ok(&message),
507 Default::default(),
508 )
509 .map_err(gmsol_solana_utils::Error::custom)?
510 .swap_output(());
511 bundle.push(rpc)?;
512 transactions.push(transaction);
513 transaction_indexes.push(txn_idx);
514 transaction_datas.push(data);
515 compute_budgets.push(*ag.compute_budget());
516 }
517
518 if !transactions.is_empty() {
519 tracing::info!(
520 start_index = multisig_data.transaction_index + 1,
521 end_index = txn_idx,
522 "Creating vault transactions: {transactions:#?}"
523 );
524
525 if self.approve {
526 for idx in transaction_indexes.iter() {
527 let proposal = get_proposal_pda(&self.multisig, *idx, None).0;
528 bundle.push(
529 self.client
530 .squads_approve_proposal(&self.multisig, &proposal, None)
531 .map_err(gmsol_solana_utils::Error::custom)?,
532 )?;
533 }
534 }
535
536 if self.execute {
537 for (idx, data) in transaction_datas.into_iter().enumerate() {
538 let compute_budget = compute_budgets[idx];
539 let mut txn = self
540 .client
541 .squads_execute_vault_transaction(&self.multisig, data, Some(&luts_cache))
542 .await
543 .map_err(gmsol_solana_utils::Error::custom)?;
544 *txn.compute_budget_mut() += compute_budget;
545 bundle.push(txn)?;
546 }
547 }
548 }
549
550 Ok(bundle)
551 }
552}
553
554pub async fn message_to_execute_account_metas(
558 rpc_client: &RpcClient,
559 message: VaultTransactionMessage,
560 ephemeral_signer_bumps: Vec<u8>,
561 vault_pda: &Pubkey,
562 transaction_pda: &Pubkey,
563 program_id: Option<&Pubkey>,
564 luts_cache: Option<&HashMap<Pubkey, AddressLookupTableAccount>>,
565) -> (Vec<AccountMeta>, Vec<AddressLookupTableAccount>) {
566 let mut account_metas = Vec::with_capacity(message.account_keys.len());
567
568 let mut address_lookup_table_accounts: Vec<AddressLookupTableAccount> = Vec::new();
569
570 let ephemeral_signer_pdas: Vec<Pubkey> = (0..ephemeral_signer_bumps.len())
571 .map(|additional_signer_index| {
572 let (pda, _bump_seed) = get_ephemeral_signer_pda(
573 transaction_pda,
574 additional_signer_index as u8,
575 program_id,
576 );
577 pda
578 })
579 .collect();
580
581 let address_lookup_table_keys = message
582 .address_table_lookups
583 .iter()
584 .map(|lookup| lookup.account_key)
585 .collect::<Vec<_>>();
586
587 for key in address_lookup_table_keys {
588 let address_lookup_table_account = match luts_cache.as_ref().and_then(|map| map.get(&key)) {
589 Some(lut) => lut.clone(),
590 None => {
591 let account_data = rpc_client.get_account(&key).await.unwrap().data;
592 let lookup_table = AddressLookupTable::deserialize(&account_data).unwrap();
593
594 AddressLookupTableAccount {
595 addresses: lookup_table.addresses.to_vec(),
596 key,
597 }
598 }
599 };
600
601 address_lookup_table_accounts.push(address_lookup_table_account);
602 account_metas.push(AccountMeta::new(key, false));
603 }
604
605 for (account_index, account_key) in message.account_keys.iter().enumerate() {
606 let is_writable =
607 VaultTransactionMessage::is_static_writable_index(&message, account_index);
608 let is_signer = VaultTransactionMessage::is_signer_index(&message, account_index)
609 && !account_key.eq(vault_pda)
610 && !ephemeral_signer_pdas.contains(account_key);
611
612 account_metas.push(AccountMeta {
613 pubkey: *account_key,
614 is_signer,
615 is_writable,
616 });
617 }
618
619 for lookup in &message.address_table_lookups {
620 let lookup_table_account = address_lookup_table_accounts
621 .iter()
622 .find(|account| account.key == lookup.account_key)
623 .unwrap();
624
625 for &account_index in &lookup.writable_indexes {
626 let account_index_usize = account_index as usize;
627
628 let pubkey = lookup_table_account
629 .addresses
630 .get(account_index_usize)
631 .unwrap();
632
633 account_metas.push(AccountMeta::new(*pubkey, false));
634 }
635
636 for &account_index in &lookup.readonly_indexes {
637 let account_index_usize = account_index as usize;
638
639 let pubkey = lookup_table_account
640 .addresses
641 .get(account_index_usize)
642 .unwrap();
643
644 account_metas.push(AccountMeta::new_readonly(*pubkey, false));
645 }
646 }
647
648 (account_metas, address_lookup_table_accounts)
649}