Skip to main content

dpo2u_sdk/
lib.rs

1//! # dpo2u-sdk
2//!
3//! Rust client SDK for the DPO2U on-chain compliance programs on Solana.
4//! Provides canonical program IDs, PDA derivers, and purpose-hash helpers
5//! for integrators building Anchor programs that CPI into DPO2U or off-chain
6//! clients that build DPO2U transactions directly.
7//!
8//! ## Programs covered
9//!
10//! | Program | Program ID (devnet) | Purpose |
11//! |---|---|---|
12//! | `compliance_registry` | `7q19zbMMFCPSDhJhh3cfUVJstin6r1Q4dgmeDAuQERyK` | DPIA/audit attestation with ZK binding |
13//! | `sp1_verifier` | `5xrWphWXoFnXJh7jYt3tyWZAwX1itbyyxJQs8uumiRTW` | Groth16 pairing CPI target |
14//! | `consent_manager` | `D5mLHU4uUQAkoMvtviAzBe1ugpdxfdqQ7VuGoKLaTjfB` | DPDP India consent events |
15//! | `art_vault` | `C7sGZFeWPxEkaGHACwqdzCcy4QkacqPLYEwEarVpidna` | MiCAR ART safeguards (PoR + liquidity + buffer + velocity) |
16//! | `aiverify_attestation` | `DSCVxsdJd5wVJan5WqQfpKkqxazWJR7D7cjd3r65s6cm` | AI Verify Singapore attestation |
17//! | `agent_registry` | `5qeuUAaJi9kTzsfmiphQ89PNrpqy7xW7sCvhBZQ6mya7` | DPO/auditor DIDs with capability bits |
18//! | `payment_gateway` | `4Qj6GziMjUfh4TszuSnasnEqnASqQBS6SHw6YAu9U23Q` | MCP invoicing (SPL Token CPI) |
19//! | `fee_distributor` | `88eKEEMMnugv8AFWRvqa4i7LEiL7tM9bEuPTVkRbD76x` | Atomic 70/20/10 split |
20//! | `agent_wallet_factory` | `AjRqmxyieQieov2qsNefdYpa6HbPhzciED7s5TfZi1in` | Deterministic PDA wallets |
21//!
22//! ## Quick start — derive a consent PDA
23//!
24//! ```
25//! use dpo2u_sdk::{programs, pdas};
26//! use solana_program::pubkey::Pubkey;
27//! use std::str::FromStr;
28//!
29//! let user = Pubkey::from_str("7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU").unwrap();
30//! let fiduciary = Pubkey::from_str("5qeuUAaJi9kTzsfmiphQ89PNrpqy7xW7sCvhBZQ6mya7").unwrap();
31//! let purpose_hash = pdas::purpose_hash(b"marketing_communications");
32//!
33//! let (consent_pda, _bump) = pdas::consent_pda(&user, &fiduciary, &purpose_hash);
34//! assert_ne!(consent_pda, Pubkey::default());
35//! ```
36
37#![cfg_attr(docsrs, feature(doc_cfg))]
38
39#[cfg(feature = "mcp-client")]
40#[cfg_attr(docsrs, doc(cfg(feature = "mcp-client")))]
41pub mod mcp;
42
43pub mod programs {
44    //! Canonical program IDs (devnet === mainnet addresses — upgrades keep IDs).
45    use solana_program::{pubkey, pubkey::Pubkey};
46
47    pub const COMPLIANCE_REGISTRY: Pubkey = pubkey!("7q19zbMMFCPSDhJhh3cfUVJstin6r1Q4dgmeDAuQERyK");
48    pub const SP1_VERIFIER: Pubkey = pubkey!("5xrWphWXoFnXJh7jYt3tyWZAwX1itbyyxJQs8uumiRTW");
49    pub const CONSENT_MANAGER: Pubkey = pubkey!("D5mLHU4uUQAkoMvtviAzBe1ugpdxfdqQ7VuGoKLaTjfB");
50    pub const ART_VAULT: Pubkey = pubkey!("C7sGZFeWPxEkaGHACwqdzCcy4QkacqPLYEwEarVpidna");
51    pub const AIVERIFY_ATTESTATION: Pubkey =
52        pubkey!("DSCVxsdJd5wVJan5WqQfpKkqxazWJR7D7cjd3r65s6cm");
53    pub const AGENT_REGISTRY: Pubkey = pubkey!("5qeuUAaJi9kTzsfmiphQ89PNrpqy7xW7sCvhBZQ6mya7");
54    pub const PAYMENT_GATEWAY: Pubkey = pubkey!("4Qj6GziMjUfh4TszuSnasnEqnASqQBS6SHw6YAu9U23Q");
55    pub const FEE_DISTRIBUTOR: Pubkey = pubkey!("88eKEEMMnugv8AFWRvqa4i7LEiL7tM9bEuPTVkRbD76x");
56    pub const AGENT_WALLET_FACTORY: Pubkey =
57        pubkey!("AjRqmxyieQieov2qsNefdYpa6HbPhzciED7s5TfZi1in");
58}
59
60pub mod pdas {
61    //! PDA derivers — all match the `#[account(seeds = ...)]` declarations in
62    //! the on-chain programs. Use these exact functions client-side to avoid
63    //! seed drift.
64    use super::programs;
65    use sha2::{Digest, Sha256};
66    use solana_program::pubkey::Pubkey;
67
68    /// `[b"attestation", subject, commitment]` under `compliance_registry`.
69    pub fn attestation_pda(subject: &Pubkey, commitment: &[u8; 32]) -> (Pubkey, u8) {
70        Pubkey::find_program_address(
71            &[b"attestation", subject.as_ref(), commitment],
72            &programs::COMPLIANCE_REGISTRY,
73        )
74    }
75
76    /// `[b"consent", user, data_fiduciary, purpose_hash]` under `consent_manager`.
77    pub fn consent_pda(
78        user: &Pubkey,
79        data_fiduciary: &Pubkey,
80        purpose_hash: &[u8; 32],
81    ) -> (Pubkey, u8) {
82        Pubkey::find_program_address(
83            &[
84                b"consent",
85                user.as_ref(),
86                data_fiduciary.as_ref(),
87                purpose_hash,
88            ],
89            &programs::CONSENT_MANAGER,
90        )
91    }
92
93    /// `[b"art_vault", authority]` under `art_vault`.
94    pub fn art_vault_pda(authority: &Pubkey) -> (Pubkey, u8) {
95        Pubkey::find_program_address(&[b"art_vault", authority.as_ref()], &programs::ART_VAULT)
96    }
97
98    /// `[b"aiverify", model_hash]` under `aiverify_attestation`.
99    pub fn aiverify_pda(model_hash: &[u8; 32]) -> (Pubkey, u8) {
100        Pubkey::find_program_address(
101            &[b"aiverify", model_hash],
102            &programs::AIVERIFY_ATTESTATION,
103        )
104    }
105
106    /// `[b"agent", authority, name_bytes]` under `agent_registry`.
107    pub fn agent_pda(authority: &Pubkey, name: &str) -> (Pubkey, u8) {
108        Pubkey::find_program_address(
109            &[b"agent", authority.as_ref(), name.as_bytes()],
110            &programs::AGENT_REGISTRY,
111        )
112    }
113
114    /// SHA-256 hash of a purpose's human-readable text. Clients that want the
115    /// same on-chain PDA MUST call this function with the exact same UTF-8
116    /// bytes.
117    pub fn purpose_hash(purpose_text_bytes: &[u8]) -> [u8; 32] {
118        let mut h = Sha256::new();
119        h.update(purpose_text_bytes);
120        let out = h.finalize();
121        let mut arr = [0u8; 32];
122        arr.copy_from_slice(&out);
123        arr
124    }
125
126    /// Commitment derivation compatible with the `compliance-registry` SP1
127    /// flow: `sha256(subject_did_text)`. Used for the `create_attestation`
128    /// legacy path and for local verification of the verified path's
129    /// `subject_commitment` in the proof public values.
130    pub fn commitment_from_subject(subject_text: &str) -> [u8; 32] {
131        purpose_hash(subject_text.as_bytes())
132    }
133}
134
135pub mod seeds {
136    //! Raw seed constants (for callers building transactions that need to
137    //! mirror the Rust program's `#[account(seeds = ...)]` exactly).
138
139    pub const ATTESTATION: &[u8] = b"attestation";
140    pub const CONSENT: &[u8] = b"consent";
141    pub const ART_VAULT: &[u8] = b"art_vault";
142    pub const AIVERIFY: &[u8] = b"aiverify";
143    pub const AGENT: &[u8] = b"agent";
144}
145
146pub mod public_values {
147    //! Helpers for parsing the 96-byte `PublicValuesStruct` ABI layout used
148    //! by both `compliance-registry::create_verified_attestation` and
149    //! `consent-manager::record_verified_consent`.
150    //!
151    //! Layout:
152    //!   - bytes `[0..32]`: `uint32 threshold` (last 4 bytes big-endian)
153    //!   - bytes `[32..64]`: `bytes32 subject_commitment` / `purpose_hash`
154    //!   - bytes `[64..96]`: `bool meets_threshold` (last byte)
155
156    use thiserror::Error;
157
158    #[derive(Debug, Error)]
159    pub enum PublicValuesError {
160        #[error("public_inputs must be exactly 96 bytes, got {0}")]
161        WrongLength(usize),
162    }
163
164    pub struct PublicValues {
165        pub threshold: u32,
166        pub subject_commitment: [u8; 32],
167        pub meets_threshold: bool,
168    }
169
170    pub fn parse(public_inputs: &[u8]) -> Result<PublicValues, PublicValuesError> {
171        if public_inputs.len() != 96 {
172            return Err(PublicValuesError::WrongLength(public_inputs.len()));
173        }
174        let mut threshold_bytes = [0u8; 4];
175        threshold_bytes.copy_from_slice(&public_inputs[28..32]);
176        let threshold = u32::from_be_bytes(threshold_bytes);
177
178        let mut subject_commitment = [0u8; 32];
179        subject_commitment.copy_from_slice(&public_inputs[32..64]);
180
181        let meets_threshold = public_inputs[95] != 0;
182
183        Ok(PublicValues {
184            threshold,
185            subject_commitment,
186            meets_threshold,
187        })
188    }
189}
190
191#[cfg(test)]
192mod tests {
193    use super::*;
194    use solana_program::pubkey::Pubkey;
195
196    #[test]
197    fn consent_pda_is_deterministic() {
198        let user = Pubkey::new_unique();
199        let fid = Pubkey::new_unique();
200        let h = pdas::purpose_hash(b"marketing_communications");
201        let (a, _) = pdas::consent_pda(&user, &fid, &h);
202        let (b, _) = pdas::consent_pda(&user, &fid, &h);
203        assert_eq!(a, b);
204    }
205
206    #[test]
207    fn consent_pda_differs_across_purposes() {
208        let user = Pubkey::new_unique();
209        let fid = Pubkey::new_unique();
210        let h1 = pdas::purpose_hash(b"marketing");
211        let h2 = pdas::purpose_hash(b"analytics");
212        let (a, _) = pdas::consent_pda(&user, &fid, &h1);
213        let (b, _) = pdas::consent_pda(&user, &fid, &h2);
214        assert_ne!(a, b);
215    }
216
217    #[test]
218    fn purpose_hash_is_sha256() {
219        let h = pdas::purpose_hash(b"");
220        // sha256("") hex
221        assert_eq!(
222            hex_lower(&h),
223            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
224        );
225    }
226
227    #[test]
228    fn aiverify_pda_has_global_uniqueness_per_model() {
229        let h = [0u8; 32];
230        let (pda1, _) = pdas::aiverify_pda(&h);
231        let (pda2, _) = pdas::aiverify_pda(&h);
232        assert_eq!(pda1, pda2);
233    }
234
235    #[test]
236    fn parse_public_values_rejects_wrong_length() {
237        assert!(public_values::parse(&[0u8; 95]).is_err());
238        assert!(public_values::parse(&[0u8; 97]).is_err());
239    }
240
241    #[test]
242    fn parse_public_values_extracts_fields() {
243        let mut pi = [0u8; 96];
244        // threshold = 70 (u32 big-endian in [28..32])
245        pi[28..32].copy_from_slice(&70u32.to_be_bytes());
246        // subject_commitment = 0xaa * 32
247        for b in pi.iter_mut().take(64).skip(32) {
248            *b = 0xaa;
249        }
250        // meets_threshold = true (last byte)
251        pi[95] = 1;
252
253        let pv = public_values::parse(&pi).unwrap();
254        assert_eq!(pv.threshold, 70);
255        assert_eq!(pv.subject_commitment, [0xaau8; 32]);
256        assert!(pv.meets_threshold);
257    }
258
259    fn hex_lower(b: &[u8]) -> String {
260        let mut s = String::with_capacity(b.len() * 2);
261        for byte in b {
262            s.push_str(&format!("{:02x}", byte));
263        }
264        s
265    }
266}
267
268// Re-export for convenience so callers don't need to go through modules.
269pub use programs::*;