1use crate::errors::ProvaError;
31use crate::types::{ActionType, AgentAccount, AttestParams, AttestResult, ProvaConfig, RegisterAgentResult};
32use sha2::{Digest, Sha256};
33use solana_client::rpc_client::RpcClient;
34use solana_sdk::{
35 commitment_config::CommitmentConfig,
36 compute_budget::ComputeBudgetInstruction,
37 ed25519_instruction,
38 instruction::{AccountMeta, Instruction},
39 pubkey::Pubkey,
40 signature::Keypair,
41 signer::Signer,
42 system_program,
43 sysvar,
44 transaction::Transaction,
45};
46use std::str::FromStr;
47use std::sync::Arc;
48
49pub const PROVA_PROGRAM_ID: &str = "G11dBAzLQaADtHHM2AZNz3ThCDnkY5nhX3Ujddu1CMM1";
51
52pub const AGENT_SEED: &[u8] = b"prova_agent";
54
55pub const MAX_BATCH_ATTESTATIONS: usize = 100;
57
58const DISC_REGISTER_AGENT: [u8; 8] = [135, 157, 66, 195, 2, 113, 175, 30];
60const DISC_RECORD_ATTESTATIONS: [u8; 8] = [228, 78, 80, 123, 83, 203, 69, 235];
61const DISC_REVOKE_AGENT: [u8; 8] = [227, 60, 209, 125, 240, 117, 163, 73];
62const DISC_UPDATE_POLICY_ROOT: [u8; 8] = [204, 72, 225, 189, 180, 164, 143, 74];
63
64const ACCOUNT_DISCRIMINATOR: [u8; 8] = [241, 119, 69, 140, 233, 9, 112, 50];
66
67pub struct ProvaClient {
68 rpc: RpcClient,
69 agent_keypair: Arc<Keypair>,
70 program_id: Pubkey,
71 network: String,
72}
73
74impl ProvaClient {
75 pub fn new(agent_keypair: Keypair, config: ProvaConfig) -> Self {
76 let network = if config.rpc_url.contains("mainnet") {
77 "mainnet"
78 } else {
79 "devnet"
80 }
81 .to_string();
82
83 let program_id = config
84 .program_id
85 .as_deref()
86 .and_then(|s| Pubkey::from_str(s).ok())
87 .unwrap_or_else(|| Pubkey::from_str(PROVA_PROGRAM_ID).unwrap());
88
89 let rpc = RpcClient::new_with_commitment(config.rpc_url, CommitmentConfig::confirmed());
90
91 Self {
92 rpc,
93 agent_keypair: Arc::new(agent_keypair),
94 program_id,
95 network,
96 }
97 }
98
99 pub fn derive_agent_pda(&self, operator: &Pubkey) -> (Pubkey, u8) {
103 Pubkey::find_program_address(&[AGENT_SEED, operator.as_ref()], &self.program_id)
104 }
105
106 pub fn hash_action(action: &str) -> [u8; 32] {
108 let mut h = Sha256::new();
109 h.update(action.as_bytes());
110 h.finalize().into()
111 }
112
113 pub fn explorer_url(&self, signature: &str) -> String {
115 format!(
116 "https://explorer.solana.com/tx/{}?cluster={}",
117 signature, self.network
118 )
119 }
120
121 pub async fn register_agent(
128 &self,
129 operator_keypair: &Keypair,
130 policy_root: Option<[u8; 32]>,
131 ) -> Result<RegisterAgentResult, ProvaError> {
132 let agent_id: [u8; 32] = self.agent_keypair.pubkey().to_bytes();
133 let policy = policy_root.unwrap_or([0u8; 32]);
134 let (agent_pda, _bump) = self.derive_agent_pda(&operator_keypair.pubkey());
135
136 let mut data = Vec::with_capacity(8 + 32 + 32);
138 data.extend_from_slice(&DISC_REGISTER_AGENT);
139 data.extend_from_slice(&agent_id);
140 data.extend_from_slice(&policy);
141
142 let ix = Instruction {
143 program_id: self.program_id,
144 accounts: vec![
145 AccountMeta::new(agent_pda, false),
146 AccountMeta::new(operator_keypair.pubkey(), true),
147 AccountMeta::new_readonly(system_program::id(), false),
148 ],
149 data,
150 };
151
152 let sig = self
153 .send_with_priority(&[ix], operator_keypair, 100_000)
154 .await?;
155
156 Ok(RegisterAgentResult {
157 explorer_url: self.explorer_url(&sig),
158 tx_signature: sig,
159 agent_pda,
160 })
161 }
162
163 pub async fn attest(
165 &self,
166 operator_keypair: &Keypair,
167 action_hash: [u8; 32],
168 action_type: ActionType,
169 privacy_mode: bool,
170 ) -> Result<AttestResult, ProvaError> {
171 self.send_attestations(
172 operator_keypair,
173 &[AttestParams {
174 action_hash,
175 action_type,
176 privacy_mode,
177 }],
178 )
179 .await
180 }
181
182 pub async fn batch_attest(
184 &self,
185 operator_keypair: &Keypair,
186 attestations: &[AttestParams],
187 ) -> Result<AttestResult, ProvaError> {
188 if attestations.is_empty() {
189 return Err(ProvaError::InvalidInput(
190 "attestations array cannot be empty".into(),
191 ));
192 }
193 if attestations.len() > MAX_BATCH_ATTESTATIONS {
194 return Err(ProvaError::BatchLimitExceeded(MAX_BATCH_ATTESTATIONS));
195 }
196 self.send_attestations(operator_keypair, attestations).await
197 }
198
199 pub async fn revoke_agent(
201 &self,
202 operator_keypair: &Keypair,
203 ) -> Result<AttestResult, ProvaError> {
204 let (agent_pda, _) = self.derive_agent_pda(&operator_keypair.pubkey());
205
206 let ix = Instruction {
207 program_id: self.program_id,
208 accounts: vec![
209 AccountMeta::new(agent_pda, false),
210 AccountMeta::new(operator_keypair.pubkey(), true),
211 ],
212 data: DISC_REVOKE_AGENT.to_vec(),
213 };
214
215 let sig = self
216 .send_with_priority(&[ix], operator_keypair, 50_000)
217 .await?;
218
219 Ok(AttestResult {
220 explorer_url: self.explorer_url(&sig),
221 tx_signature: sig,
222 })
223 }
224
225 pub async fn update_policy_root(
227 &self,
228 operator_keypair: &Keypair,
229 new_root: [u8; 32],
230 ) -> Result<AttestResult, ProvaError> {
231 let (agent_pda, _) = self.derive_agent_pda(&operator_keypair.pubkey());
232
233 let mut data = Vec::with_capacity(8 + 32);
234 data.extend_from_slice(&DISC_UPDATE_POLICY_ROOT);
235 data.extend_from_slice(&new_root);
236
237 let ix = Instruction {
238 program_id: self.program_id,
239 accounts: vec![
240 AccountMeta::new(agent_pda, false),
241 AccountMeta::new(operator_keypair.pubkey(), true),
242 ],
243 data,
244 };
245
246 let sig = self
247 .send_with_priority(&[ix], operator_keypair, 50_000)
248 .await?;
249
250 Ok(AttestResult {
251 explorer_url: self.explorer_url(&sig),
252 tx_signature: sig,
253 })
254 }
255
256 pub async fn get_agent_account(
260 &self,
261 operator: &Pubkey,
262 ) -> Result<AgentAccount, ProvaError> {
263 let (pda, _) = self.derive_agent_pda(operator);
264 let account_data = self
265 .rpc
266 .get_account_data(&pda)
267 .map_err(|e| ProvaError::AgentNotFound(format!("{}: {}", pda, e)))?;
268
269 Self::deserialize_agent_account(&pda, &account_data)
270 }
271
272 pub async fn is_agent_active(&self, operator: &Pubkey) -> bool {
274 match self.get_agent_account(operator).await {
275 Ok(acc) => !acc.revoked,
276 Err(_) => false,
277 }
278 }
279
280 fn deserialize_agent_account(
283 pda: &Pubkey,
284 data: &[u8],
285 ) -> Result<AgentAccount, ProvaError> {
286 const MIN_LEN: usize = 8 + 32 + 32 + 32 + 8 + 8 + 1 + 1;
289 if data.len() < MIN_LEN {
290 return Err(ProvaError::AccountError(format!(
291 "Account data too short: {} < {}",
292 data.len(),
293 MIN_LEN
294 )));
295 }
296
297 if data[..8] != ACCOUNT_DISCRIMINATOR {
299 return Err(ProvaError::AccountError(
300 "Invalid account discriminator".into(),
301 ));
302 }
303
304 let d = &data[8..];
305 let operator = Pubkey::try_from(&d[0..32])
306 .map_err(|_| ProvaError::AccountError("Invalid operator pubkey".into()))?;
307
308 let mut agent_id = [0u8; 32];
309 agent_id.copy_from_slice(&d[32..64]);
310
311 let mut policy_root = [0u8; 32];
312 policy_root.copy_from_slice(&d[64..96]);
313
314 let attestation_count = u64::from_le_bytes(d[96..104].try_into().unwrap());
315 let created_at = i64::from_le_bytes(d[104..112].try_into().unwrap());
316 let revoked = d[112] != 0;
317 let bump = d[113];
318
319 Ok(AgentAccount {
320 address: *pda,
321 operator,
322 agent_id,
323 policy_root,
324 attestation_count,
325 created_at,
326 revoked,
327 bump,
328 })
329 }
330
331 async fn send_attestations(
333 &self,
334 operator_keypair: &Keypair,
335 entries: &[AttestParams],
336 ) -> Result<AttestResult, ProvaError> {
337 let (agent_pda, _) = self.derive_agent_pda(&operator_keypair.pubkey());
338
339 let mut ed25519_ixs = Vec::with_capacity(entries.len());
341 let mut attestation_inputs_data = Vec::new();
342
343 let len_bytes = (entries.len() as u32).to_le_bytes();
345 attestation_inputs_data.extend_from_slice(&len_bytes);
346
347 for entry in entries {
348 let sig = self.agent_keypair.sign_message(&entry.action_hash);
350 let sig_bytes: [u8; 64] = sig.into();
351
352 let ed25519_ix = ed25519_instruction::new_ed25519_instruction_with_signature(
354 &entry.action_hash,
355 &sig_bytes,
356 &self.agent_keypair.pubkey().to_bytes(),
357 );
358 ed25519_ixs.push(ed25519_ix);
359
360 attestation_inputs_data.push(entry.action_type as u8);
366 attestation_inputs_data.extend_from_slice(&entry.action_hash);
367 attestation_inputs_data.push(entry.privacy_mode as u8);
368 attestation_inputs_data.extend_from_slice(&sig_bytes);
369 }
370
371 let mut ix_data = Vec::with_capacity(8 + attestation_inputs_data.len());
373 ix_data.extend_from_slice(&DISC_RECORD_ATTESTATIONS);
374 ix_data.extend_from_slice(&attestation_inputs_data);
375
376 let record_ix = Instruction {
377 program_id: self.program_id,
378 accounts: vec![
379 AccountMeta::new(agent_pda, false),
380 AccountMeta::new(operator_keypair.pubkey(), true),
381 AccountMeta::new_readonly(sysvar::instructions::id(), false),
382 ],
383 data: ix_data,
384 };
385
386 let compute_units = 50_000 + (entries.len() as u32 * 15_000);
388 let mut all_ixs = Vec::with_capacity(2 + entries.len() + 1);
389 all_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(compute_units));
390 all_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(100_000));
391 all_ixs.extend(ed25519_ixs);
392 all_ixs.push(record_ix);
393
394 let sig = self
395 .send_tx(&all_ixs, operator_keypair)
396 .await?;
397
398 Ok(AttestResult {
399 explorer_url: self.explorer_url(&sig),
400 tx_signature: sig,
401 })
402 }
403
404 async fn send_with_priority(
406 &self,
407 ixs: &[Instruction],
408 payer: &Keypair,
409 compute_units: u32,
410 ) -> Result<String, ProvaError> {
411 let mut all_ixs = Vec::with_capacity(2 + ixs.len());
412 all_ixs.push(ComputeBudgetInstruction::set_compute_unit_limit(compute_units));
413 all_ixs.push(ComputeBudgetInstruction::set_compute_unit_price(100_000));
414 all_ixs.extend_from_slice(ixs);
415 self.send_tx(&all_ixs, payer).await
416 }
417
418 async fn send_tx(
420 &self,
421 ixs: &[Instruction],
422 payer: &Keypair,
423 ) -> Result<String, ProvaError> {
424 let blockhash = self.rpc.get_latest_blockhash()?;
425
426 let tx = Transaction::new_signed_with_payer(
427 ixs,
428 Some(&payer.pubkey()),
429 &[payer],
430 blockhash,
431 );
432
433 let sig = self
434 .rpc
435 .send_and_confirm_transaction(&tx)
436 .map_err(|e| ProvaError::TransactionError(e.to_string()))?;
437
438 Ok(sig.to_string())
439 }
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn hash_action_is_deterministic() {
448 let h1 = ProvaClient::hash_action("swap 100 USDC");
449 let h2 = ProvaClient::hash_action("swap 100 USDC");
450 assert_eq!(h1, h2);
451 assert_ne!(h1, [0u8; 32]);
452 }
453
454 #[test]
455 fn derive_pda_is_consistent() {
456 let agent = Keypair::new();
457 let config = ProvaConfig::default();
458 let client = ProvaClient::new(agent, config);
459 let operator = Keypair::new();
460
461 let (pda1, bump1) = client.derive_agent_pda(&operator.pubkey());
462 let (pda2, bump2) = client.derive_agent_pda(&operator.pubkey());
463 assert_eq!(pda1, pda2);
464 assert_eq!(bump1, bump2);
465 }
466
467 #[test]
468 fn explorer_url_format() {
469 let agent = Keypair::new();
470 let config = ProvaConfig::default();
471 let client = ProvaClient::new(agent, config);
472 let url = client.explorer_url("abc123");
473 assert!(url.contains("abc123"));
474 assert!(url.contains("devnet"));
475 }
476
477 #[test]
478 fn batch_limit_enforced() {
479 let agent = Keypair::new();
480 let config = ProvaConfig::default();
481 let client = ProvaClient::new(agent, config);
482 let entries: Vec<AttestParams> = (0..101)
483 .map(|_| AttestParams {
484 action_hash: [0u8; 32],
485 action_type: ActionType::Transaction,
486 privacy_mode: false,
487 })
488 .collect();
489 let operator = Keypair::new();
490
491 let result = tokio::runtime::Runtime::new()
492 .unwrap()
493 .block_on(client.batch_attest(&operator, &entries));
494
495 assert!(result.is_err());
496 assert!(result.unwrap_err().to_string().contains("100"));
497 }
498
499 #[test]
500 fn deserialize_agent_account_rejects_short_data() {
501 let pda = Pubkey::new_unique();
502 let data = vec![0u8; 10];
503 assert!(ProvaClient::deserialize_agent_account(&pda, &data).is_err());
504 }
505
506 #[test]
507 fn deserialize_agent_account_rejects_bad_discriminator() {
508 let pda = Pubkey::new_unique();
509 let data = vec![0u8; 122]; assert!(ProvaClient::deserialize_agent_account(&pda, &data).is_err());
511 }
512}