switchboard_solana/attestation_program/client/
function.rs

1use crate::*;
2
3use kv_log_macro::{debug, info};
4use sha2::{Digest, Sha256};
5use solana_client::rpc_config::{RpcAccountInfoConfig, RpcProgramAccountsConfig};
6use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel};
7use solana_sdk::signer::Signer;
8
9use std::sync::Arc;
10
11pub const DEFAULT_FUNCTION_MR_ENCLAVE: [u8; 32] = [
12    23, 255, 152, 240, 7, 140, 229, 105, 131, 187, 62, 160, 17, 200, 177, 156, 238, 213, 242, 156,
13    153, 94, 5, 77, 91, 218, 183, 146, 181, 206, 173, 108,
14];
15
16#[derive(Default, Debug, Clone)]
17pub struct FunctionFilters {
18    pub attestation_queue: Option<Pubkey>,
19    pub authority: Option<Pubkey>,
20    pub metadata: Option<Vec<u8>>,
21    pub permissions: Option<u32>,
22    // Note: This will not catch functions for which the verifier fetching
23    // is the secondary oracle. This is because the verifier is not stored
24    // on the function account but on the queue account.
25    pub queue_idx: Option<u32>,
26}
27impl FunctionFilters {
28    pub fn to_vec(&self) -> Vec<solana_client::rpc_filter::RpcFilterType> {
29        let mut filters = vec![FunctionAccountData::get_discriminator_filter()];
30
31        // AttestationQueue & Authority Filters
32        // Combine these two filters for efficiency
33        if let Some(attestation_queue) = &self.attestation_queue {
34            if let Some(authority) = &self.authority {
35                filters.push(FunctionAccountData::get_queue_and_authority_filter(
36                    attestation_queue,
37                    authority,
38                ));
39            } else {
40                filters.push(FunctionAccountData::get_queue_filter(attestation_queue));
41            }
42        } else if let Some(authority) = &self.authority {
43            filters.push(FunctionAccountData::get_authority_filter(authority));
44        }
45
46        // Metadata Filter
47        if let Some(metadata) = &self.metadata {
48            filters.push(FunctionAccountData::get_metadata_filter(metadata.clone()));
49        }
50
51        // Permissions Filter
52        if let Some(permissions) = &self.permissions {
53            filters.push(FunctionAccountData::get_permissions_filter(permissions));
54        }
55
56        // Queue Idx Filter
57        if let Some(queue_idx) = &self.queue_idx {
58            filters.push(FunctionAccountData::get_queue_idx_filter(queue_idx));
59        }
60
61        filters
62    }
63}
64
65impl FunctionAccountData {
66    pub async fn get_program_accounts(
67        rpc: &solana_client::nonblocking::rpc_client::RpcClient,
68        filters: FunctionFilters,
69        commitment: Option<CommitmentLevel>,
70    ) -> Result<Vec<(Pubkey, FunctionAccountData)>, SbError> {
71        let mut functions = vec![];
72
73        let accounts = rpc
74            .get_program_accounts_with_config(
75                &SWITCHBOARD_ATTESTATION_PROGRAM_ID,
76                RpcProgramAccountsConfig {
77                    filters: Some(filters.to_vec()),
78                    account_config: RpcAccountInfoConfig {
79                        encoding: Some(solana_account_decoder::UiAccountEncoding::Base64Zstd),
80                        commitment: Some(CommitmentConfig {
81                            commitment: commitment.unwrap_or(CommitmentLevel::Processed),
82                        }),
83                        ..Default::default()
84                    },
85                    ..Default::default()
86                },
87            )
88            .await
89            .map_err(|e| SbError::CustomError {
90                message: "Failed to get program accounts".to_string(),
91                source: Arc::new(e),
92            })?;
93
94        for (pubkey, account) in accounts {
95            if let Ok(function_data) = FunctionAccountData::try_deserialize(&mut &account.data[..])
96            {
97                functions.push((pubkey, function_data));
98            }
99        }
100
101        Ok(functions)
102    }
103
104    // meh this is a PDA so we can deterministically derive this
105    // we could set the name to some sha256 hash of the params
106    // then do a gPA call to filter by name
107    pub async fn get_or_create_from_seed(
108        rpc: &solana_client::nonblocking::rpc_client::RpcClient,
109        payer: std::sync::Arc<Keypair>,
110        attestation_queue: Pubkey,
111        seed: Option<&str>,
112        params: Option<FunctionInitParams>,
113    ) -> Result<Pubkey, SbError> {
114        let mut params = params.unwrap_or_default();
115
116        let mut seed = format!(
117            "function-{}-{}",
118            attestation_queue,
119            seed.unwrap_or("default")
120        )
121        .as_str()
122        .as_bytes()
123        .to_vec();
124        seed.extend_from_slice(&payer.secret().to_bytes());
125
126        // were injecting the mrenclave and metadata into the params so cant use params in keypair schema
127
128        // Sha256 hash of the seeds = the metadata of the function
129        let mut hash = [0u8; 32];
130        hash.copy_from_slice(&Sha256::digest(&seed).as_slice()[..32]);
131        params.metadata = hash.to_vec();
132
133        // default to some mrenclave value for testing purposes
134        if let Some(mrenclave) = &params.mr_enclave {
135            if mrenclave == &[0u8; 32] {
136                params.mr_enclave = Some(DEFAULT_FUNCTION_MR_ENCLAVE);
137            }
138        } else {
139            params.mr_enclave = Some(DEFAULT_FUNCTION_MR_ENCLAVE);
140        }
141
142        let functions = FunctionAccountData::get_program_accounts(
143            rpc,
144            FunctionFilters {
145                attestation_queue: Some(attestation_queue),
146                authority: Some(payer.pubkey()),
147                metadata: Some(hash.to_vec()),
148                ..Default::default()
149            },
150            None,
151        )
152        .await?;
153
154        if functions.is_empty() {
155            // Create a new function
156
157            let slot = rpc.get_slot().await.unwrap();
158            let creator_seed = payer.pubkey().to_bytes().to_vec();
159
160            let (function_pubkey, _bump) = Pubkey::find_program_address(
161                &[b"FunctionAccountData", &creator_seed, &slot.to_le_bytes()],
162                &SWITCHBOARD_ATTESTATION_PROGRAM_ID,
163            );
164
165            info!(
166                "[Function] creating new function account {} ...",
167                function_pubkey
168            );
169
170            let (address_lookup_table, _) = Pubkey::find_program_address(
171                &[&function_pubkey.to_bytes(), &slot.to_le_bytes()],
172                &solana_address_lookup_table_program::ID,
173            );
174
175            let name_seed = SwitchboardWallet::parse_name(b"default");
176
177            // get or create the default switchboard wallet
178            let switchboard_wallet = SwitchboardWallet::get_or_create_from_seed(
179                rpc,
180                payer.clone(),
181                attestation_queue,
182                Some(name_seed.to_vec()),
183            )
184            .await
185            .unwrap();
186
187            let function_init_ixn = Self::build_ix(
188                &FunctionInitAccounts {
189                    function: function_pubkey,
190                    address_lookup_table,
191                    authority: payer.pubkey(),
192                    attestation_queue,
193                    payer: payer.pubkey(),
194                    escrow_wallet: switchboard_wallet,
195                    escrow_wallet_authority: None,
196                },
197                &FunctionInitParams {
198                    recent_slot: slot,
199                    creator_seed: None,
200                    metadata: hash.to_vec(),
201                    ..params
202                },
203            )
204            .unwrap();
205
206            let tx = crate::ix_to_tx(
207                &[function_init_ixn],
208                &[&*payer],
209                rpc.get_latest_blockhash().await.unwrap_or_default(),
210            )
211            .unwrap();
212
213            let signature = rpc.send_and_confirm_transaction(&tx).await.unwrap();
214
215            info!(
216                "[Function] switchboard function {} initialized. Tx Signature: {}",
217                function_pubkey, signature
218            );
219
220            Ok(function_pubkey)
221        } else if functions.len() == 1 {
222            let function_pubkey = functions[0].0;
223
224            println!("[Function] Found function {}", function_pubkey);
225
226            Ok(function_pubkey)
227        } else {
228            let function_pubkey = functions[0].0;
229
230            println!(
231                "[Function] Found {} functions - {}",
232                functions.len(),
233                function_pubkey
234            );
235            debug!("Warning: Too many functions yielded from the getProgramAccounts filter");
236
237            Ok(function_pubkey)
238        }
239    }
240
241    pub fn get_discriminator_filter() -> solana_client::rpc_filter::RpcFilterType {
242        solana_client::rpc_filter::RpcFilterType::Memcmp(
243            solana_client::rpc_filter::Memcmp::new_raw_bytes(
244                0,
245                FunctionAccountData::discriminator().to_vec(),
246            ),
247        )
248    }
249
250    pub fn get_permissions_filter(permissions: &u32) -> solana_client::rpc_filter::RpcFilterType {
251        solana_client::rpc_filter::RpcFilterType::Memcmp(
252            solana_client::rpc_filter::Memcmp::new_raw_bytes(
253                10,
254                permissions.to_le_bytes().to_vec(),
255            ),
256        )
257    }
258
259    #[deprecated(
260        since = "0.28.35",
261        note = "please use a `FunctionRequestAccountData` for all on-demand executions"
262    )]
263    pub fn get_is_triggered_filter() -> solana_client::rpc_filter::RpcFilterType {
264        solana_client::rpc_filter::RpcFilterType::Memcmp(
265            solana_client::rpc_filter::Memcmp::new_raw_bytes(9, vec![1u8]),
266        )
267    }
268
269    #[deprecated(
270        since = "0.28.35",
271        note = "please use a `FunctionRoutineAccountData` for all scheduled executions"
272    )]
273    pub fn get_is_scheduled_filter() -> solana_client::rpc_filter::RpcFilterType {
274        solana_client::rpc_filter::RpcFilterType::Memcmp(
275            solana_client::rpc_filter::Memcmp::new_raw_bytes(8, vec![1u8]),
276        )
277    }
278
279    pub fn get_is_active_filter() -> solana_client::rpc_filter::RpcFilterType {
280        solana_client::rpc_filter::RpcFilterType::Memcmp(
281            solana_client::rpc_filter::Memcmp::new_raw_bytes(
282                14,
283                vec![FunctionStatus::Active as u8],
284            ),
285        )
286    }
287
288    pub fn get_queue_filter(queue_pubkey: &Pubkey) -> solana_client::rpc_filter::RpcFilterType {
289        solana_client::rpc_filter::RpcFilterType::Memcmp(
290            solana_client::rpc_filter::Memcmp::new_raw_bytes(2553, queue_pubkey.to_bytes().into()),
291        )
292    }
293
294    pub fn get_authority_filter(
295        authority_pubkey: &Pubkey,
296    ) -> solana_client::rpc_filter::RpcFilterType {
297        solana_client::rpc_filter::RpcFilterType::Memcmp(
298            solana_client::rpc_filter::Memcmp::new_raw_bytes(
299                2521,
300                authority_pubkey.to_bytes().into(),
301            ),
302        )
303    }
304
305    pub fn get_queue_and_authority_filter(
306        queue_pubkey: &Pubkey,
307        authority_pubkey: &Pubkey,
308    ) -> solana_client::rpc_filter::RpcFilterType {
309        let bytes = vec![authority_pubkey.to_bytes(), queue_pubkey.to_bytes()].concat();
310        solana_client::rpc_filter::RpcFilterType::Memcmp(
311            solana_client::rpc_filter::Memcmp::new_raw_bytes(2521, bytes),
312        )
313    }
314
315    pub fn get_queue_idx_filter(queue_idx: &u32) -> solana_client::rpc_filter::RpcFilterType {
316        solana_client::rpc_filter::RpcFilterType::Memcmp(
317            solana_client::rpc_filter::Memcmp::new_raw_bytes(10, queue_idx.to_le_bytes().to_vec()),
318        )
319    }
320
321    pub fn get_metadata_filter(metadata: Vec<u8>) -> solana_client::rpc_filter::RpcFilterType {
322        solana_client::rpc_filter::RpcFilterType::Memcmp(
323            solana_client::rpc_filter::Memcmp::new_raw_bytes(112, metadata),
324        )
325    }
326
327    #[deprecated(
328        since = "0.28.35",
329        note = "please use a `FunctionRoutineAccountData` or `FunctionRequestAccountData` for all function executions."
330    )]
331    pub fn get_is_ready_filters(
332        queue_pubkey: &Pubkey,
333    ) -> Vec<solana_client::rpc_filter::RpcFilterType> {
334        vec![
335            FunctionAccountData::get_discriminator_filter(),
336            FunctionAccountData::get_is_triggered_filter(),
337            FunctionAccountData::get_is_scheduled_filter(),
338            FunctionAccountData::get_is_active_filter(),
339            FunctionAccountData::get_queue_filter(queue_pubkey),
340        ]
341    }
342
343    pub fn get_schedule(&self) -> Option<cron::Schedule> {
344        if self.schedule[0] == 0 {
345            return None;
346        }
347        let every_second = cron::Schedule::try_from("* * * * * *").unwrap();
348        let schedule = std::str::from_utf8(&self.schedule)
349            .unwrap_or("* * * * * *")
350            .trim_end_matches('\0');
351        let schedule = cron::Schedule::try_from(schedule);
352        Some(schedule.unwrap_or(every_second))
353    }
354
355    pub fn get_last_execution_datetime(&self) -> chrono::DateTime<chrono::Utc> {
356        chrono::NaiveDateTime::from_timestamp_opt(self.last_execution_timestamp, 0)
357            .unwrap()
358            .and_utc()
359    }
360
361    pub fn get_next_execution_datetime(&self) -> Option<chrono::DateTime<chrono::Utc>> {
362        let schedule = self.get_schedule()?;
363
364        // If we havent ever executed, use the current timestamp
365        let last_execution_timestamp = if self.last_execution_timestamp > 0 {
366            self.last_execution_timestamp
367        } else {
368            unix_timestamp()
369        };
370        let last_execution_datetime =
371            chrono::NaiveDateTime::from_timestamp_opt(last_execution_timestamp, 0)
372                .unwrap()
373                .and_utc();
374
375        schedule.after(&last_execution_datetime).next()
376    }
377
378    pub fn should_execute(&self, now: chrono::DateTime<chrono::Utc>) -> bool {
379        if self.is_triggered > 0 {
380            return true;
381        }
382        let schedule = self.get_schedule();
383        if schedule.is_none() {
384            return false;
385        }
386        if self.last_execution_timestamp == 0 {
387            return true;
388        }
389        let dt = self.get_last_execution_datetime();
390        let next_trigger_time = schedule.unwrap().after(&dt).next();
391        if next_trigger_time.is_none() {
392            return false;
393        }
394        let next_trigger_time = next_trigger_time.unwrap();
395        if next_trigger_time > now {
396            return false;
397        }
398        true
399    }
400
401    #[deprecated(
402        since = "0.28.35",
403        note = "please use a `FunctionRoutineAccountData` for all scheduled executions"
404    )]
405    pub fn is_scheduled(&self) -> bool {
406        self.schedule[0] == 0
407    }
408
409    pub fn fetch(
410        client: &solana_client::rpc_client::RpcClient,
411        pubkey: Pubkey,
412    ) -> std::result::Result<Self, switchboard_common::SbError> {
413        crate::client::fetch_zerocopy_account(client, pubkey)
414    }
415
416    pub async fn fetch_async(
417        client: &solana_client::nonblocking::rpc_client::RpcClient,
418        pubkey: Pubkey,
419    ) -> std::result::Result<Self, switchboard_common::SbError> {
420        crate::client::fetch_zerocopy_account_async(client, pubkey).await
421    }
422
423    pub fn fetch_sync<T: solana_sdk::client::SyncClient>(
424        client: &T,
425        pubkey: Pubkey,
426    ) -> std::result::Result<Self, switchboard_common::SbError> {
427        crate::client::fetch_zerocopy_account_sync(client, pubkey)
428    }
429
430    pub fn build_ix(
431        accounts: &FunctionInitAccounts,
432        params: &FunctionInitParams,
433    ) -> Result<Instruction, SbError> {
434        Ok(crate::utils::build_ix(
435            &SWITCHBOARD_ATTESTATION_PROGRAM_ID,
436            accounts,
437            params,
438        ))
439    }
440}
441
442pub struct FunctionInitAccounts {
443    pub function: Pubkey,
444    pub address_lookup_table: Pubkey,
445    pub authority: Pubkey,
446    pub attestation_queue: Pubkey,
447    pub payer: Pubkey,
448    pub escrow_wallet: Pubkey,
449    pub escrow_wallet_authority: Option<Pubkey>,
450}
451impl ToAccountMetas for FunctionInitAccounts {
452    fn to_account_metas(&self, _: Option<bool>) -> Vec<AccountMeta> {
453        let mut account_metas = Vec::new();
454
455        account_metas.push(AccountMeta::new(self.function, false));
456        account_metas.push(AccountMeta::new(self.address_lookup_table, false));
457        account_metas.push(AccountMeta::new_readonly(self.authority, false));
458        account_metas.push(AccountMeta::new_readonly(self.attestation_queue, false));
459        account_metas.push(AccountMeta::new(self.payer, true));
460        account_metas.push(AccountMeta::new(self.escrow_wallet, false));
461
462        if let Some(escrow_wallet_authority) = &self.escrow_wallet_authority {
463            account_metas.push(AccountMeta::new_readonly(*escrow_wallet_authority, true));
464        } else {
465            account_metas.push(AccountMeta::new_readonly(
466                SWITCHBOARD_ATTESTATION_PROGRAM_ID,
467                false,
468            ));
469        }
470
471        account_metas.push(AccountMeta::new(
472            find_associated_token_address(&self.escrow_wallet, &NativeMint::id()),
473            false,
474        ));
475        account_metas.push(AccountMeta::new_readonly(NativeMint::ID, false));
476        account_metas.push(AccountMeta::new_readonly(anchor_spl::token::ID, false));
477        account_metas.push(AccountMeta::new_readonly(
478            anchor_spl::associated_token::ID,
479            false,
480        ));
481        account_metas.push(AccountMeta::new_readonly(
482            anchor_lang::system_program::ID,
483            false,
484        ));
485        account_metas.push(AccountMeta::new_readonly(
486            solana_address_lookup_table_program::ID,
487            false,
488        ));
489        account_metas
490    }
491}