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 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 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 if let Some(metadata) = &self.metadata {
48 filters.push(FunctionAccountData::get_metadata_filter(metadata.clone()));
49 }
50
51 if let Some(permissions) = &self.permissions {
53 filters.push(FunctionAccountData::get_permissions_filter(permissions));
54 }
55
56 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 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 let mut hash = [0u8; 32];
130 hash.copy_from_slice(&Sha256::digest(&seed).as_slice()[..32]);
131 params.metadata = hash.to_vec();
132
133 if let Some(mrenclave) = ¶ms.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 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 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 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}