gmsol_sdk/client/squads/
mod.rs

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
41/// Squads Vault Transaction.
42pub 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    /// Convert to transaction message.
67    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
102/// Squads Proposal.
103pub 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/// Vault transaction options.
126#[derive(Debug, Clone, Default)]
127pub struct VaultTransactionOptions {
128    /// Memo to create with the vault transcation.
129    pub memo: Option<String>,
130    /// Whether the vault transaction is a draft.
131    pub draft: bool,
132    /// The number of ephemeral signers to use.
133    pub ephemeral_signers: u8,
134}
135
136/// Squads Multisig Ops.
137pub trait SquadsOps<C> {
138    /// Create Vault Transaction with the given transaction index and return the message.
139    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    /// Create Vault Transaction with the given transaction index.
149    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    /// Create Vault Transaction with next transaction index.
159    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    /// Create Vault Transaction with next transaction index with the given message.
169    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    /// Approve a proposal.
187    fn squads_approve_proposal(
188        &self,
189        multisig: &Pubkey,
190        proposal: &Pubkey,
191        memo: Option<String>,
192    ) -> crate::Result<TransactionBuilder<C>>;
193
194    /// Execute a vault transaction.
195    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    /// Create a [`Squads`] from the given [`BundleBuilder`].
203    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/// Squads bundle builder.
428#[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    /// Set whether to approve the proposals.
440    pub fn approve(&mut self, approve: bool) -> &mut Self {
441        self.approve = approve;
442        self
443    }
444
445    /// Set whether to execute the transactions.
446    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
554/// Extracts account metadata from the given message.
555// Adapted from:
556// https://github.com/Squads-Protocol/v4/blob/4f864f8ff1bfabaa0d7367ae33de085e9fe202cf/cli/src/command/vault_transaction_execute.rs#L193
557pub 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}