Skip to main content

interveil_sdk/intent/
sign.rs

1use crate::error::VeilError;
2use crate::intent::intent::Intent;
3use crate::signer::Signer;
4
5/// A signed intent ready for submission to the node.
6#[derive(Debug, Clone)]
7pub struct SignedIntent {
8    pub intent: Intent,
9    pub pubkey: Vec<u8>,
10    pub signature: Vec<u8>,
11}
12
13impl SignedIntent {
14    /// Serialize to JSON for HTTP submission to node.
15    /// Format:
16    /// {
17    ///   "intent": "<base64 encoded intent bytes>",
18    ///   "pubkey": "<hex encoded public key>",
19    ///   "signature": "<hex encoded signature>"
20    /// }
21    pub fn to_json(&self) -> Result<String, VeilError> {
22        let intent_b64 = base64_encode(&self.intent.to_bytes()?);
23        let pubkey_hex = hex_encode(&self.pubkey);
24        let sig_hex = hex_encode(&self.signature);
25
26        // Manual JSON construction to avoid serde_json dependency in main code
27        Ok(format!(
28            r#"{{"intent":"{}","pubkey":"{}","signature":"{}"}}"#,
29            intent_b64, pubkey_hex, sig_hex
30        ))
31    }
32}
33
34impl Intent {
35    /// Sign this intent with the given signer.
36    ///
37    /// Flow:
38    ///   intent → to_bytes() → blake3 hash (32 bytes) → signer.sign(hash) → SignedIntent
39    ///
40    /// We hash before signing because:
41    ///   1. Fixed 32-byte input regardless of intent size
42    ///   2. Deterministic
43    ///   3. Standard practice (Solana itself hashes messages before signing)
44    pub fn sign(&self, signer: &dyn Signer) -> Result<SignedIntent, VeilError> {
45        let intent_bytes = self.to_bytes()?;
46        let hash = blake3::hash(&intent_bytes);
47        let signature = signer.sign(hash.as_bytes())?;
48        let pubkey = signer.public_key();
49
50        Ok(SignedIntent {
51            intent: self.clone(),
52            pubkey,
53            signature,
54        })
55    }
56}
57
58// --- Encoding helpers (no external dependency) ---
59
60fn base64_encode(data: &[u8]) -> String {
61    // Minimal base64 implementation
62    const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
63    let mut result = String::new();
64    for chunk in data.chunks(3) {
65        let b0 = chunk[0] as u32;
66        let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
67        let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
68        let triple = (b0 << 16) | (b1 << 8) | b2;
69
70        result.push(CHARS[((triple >> 18) & 0x3F) as usize] as char);
71        result.push(CHARS[((triple >> 12) & 0x3F) as usize] as char);
72        if chunk.len() > 1 {
73            result.push(CHARS[((triple >> 6) & 0x3F) as usize] as char);
74        } else {
75            result.push('=');
76        }
77        if chunk.len() > 2 {
78            result.push(CHARS[(triple & 0x3F) as usize] as char);
79        } else {
80            result.push('=');
81        }
82    }
83    result
84}
85
86fn hex_encode(data: &[u8]) -> String {
87    data.iter().map(|b| format!("{:02x}", b)).collect()
88}