use std::str::FromStr;
use anyhow::{Result, Context, bail};
use serde::{Deserialize, Serialize};
use tracing::{info, debug, warn};
#[cfg(feature = "solana")]
use solana_sdk::{
commitment_config::CommitmentConfig,
instruction::{AccountMeta, Instruction},
pubkey::Pubkey,
signature::{Keypair, Signature, Signer},
system_program,
transaction::Transaction,
};
pub const FINGERPRINT_PROGRAM_ID: &str = "PSMFprnt1111111111111111111111111111111111";
pub const MAX_METADATA_URI_LEN: usize = 200;
pub const FINGERPRINT_ACCOUNT_SIZE: usize = 32 + 32 + 8 + 1 + 4 + MAX_METADATA_URI_LEN;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OnChainFingerprint {
pub creator: String,
pub content_hash: [u8; 32],
pub timestamp: i64,
pub version: u8,
pub metadata_uri: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerificationResult {
pub found: bool,
pub creator: Option<String>,
pub registered_at: Option<i64>,
pub verified: bool,
pub account_address: Option<String>,
}
#[derive(Debug, Clone)]
pub struct SolanaConfig {
pub rpc_url: String,
pub commitment: String,
pub program_id: String,
}
impl Default for SolanaConfig {
fn default() -> Self {
Self {
rpc_url: "https://api.devnet.solana.com".to_string(),
commitment: "confirmed".to_string(),
program_id: FINGERPRINT_PROGRAM_ID.to_string(),
}
}
}
#[cfg(feature = "solana")]
pub struct SolanaFingerprintClient {
config: SolanaConfig,
payer: Keypair,
program_id: Pubkey,
}
#[cfg(feature = "solana")]
impl SolanaFingerprintClient {
pub fn new(rpc_url: &str, keypair_path: &str) -> Result<Self> {
let config = SolanaConfig {
rpc_url: rpc_url.to_string(),
..Default::default()
};
let keypair_data = std::fs::read_to_string(keypair_path)
.context("Failed to read keypair file")?;
let keypair_bytes: Vec<u8> = serde_json::from_str(&keypair_data)
.context("Failed to parse keypair JSON")?;
let payer = Keypair::from_bytes(&keypair_bytes)
.context("Failed to create keypair")?;
let program_id = Pubkey::from_str(&config.program_id)
.context("Invalid program ID")?;
Ok(Self {
config,
payer,
program_id,
})
}
pub fn with_config(config: SolanaConfig, keypair_path: &str) -> Result<Self> {
let keypair_data = std::fs::read_to_string(keypair_path)
.context("Failed to read keypair file")?;
let keypair_bytes: Vec<u8> = serde_json::from_str(&keypair_data)
.context("Failed to parse keypair JSON")?;
let payer = Keypair::from_bytes(&keypair_bytes)
.context("Failed to create keypair")?;
let program_id = Pubkey::from_str(&config.program_id)
.context("Invalid program ID")?;
Ok(Self {
config,
payer,
program_id,
})
}
pub fn derive_fingerprint_address(&self, content_hash: &[u8; 32]) -> (Pubkey, u8) {
Pubkey::find_program_address(
&[b"fingerprint", content_hash.as_ref()],
&self.program_id,
)
}
pub async fn store_fingerprint(
&self,
content_hash: &[u8; 32],
metadata_uri: Option<&str>,
) -> Result<String> {
info!("Storing fingerprint on Solana: {:?}", hex::encode(content_hash));
let (fingerprint_pda, bump) = self.derive_fingerprint_address(content_hash);
let mut instruction_data = vec![0u8]; instruction_data.extend_from_slice(content_hash);
instruction_data.push(bump);
if let Some(uri) = metadata_uri {
if uri.len() > MAX_METADATA_URI_LEN {
bail!("Metadata URI too long (max {} characters)", MAX_METADATA_URI_LEN);
}
instruction_data.extend_from_slice(&(uri.len() as u32).to_le_bytes());
instruction_data.extend_from_slice(uri.as_bytes());
} else {
instruction_data.extend_from_slice(&0u32.to_le_bytes());
}
let instruction = Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(fingerprint_pda, false),
AccountMeta::new(self.payer.pubkey(), true),
AccountMeta::new_readonly(system_program::id(), false),
],
data: instruction_data,
};
debug!("Instruction created for PDA: {}", fingerprint_pda);
debug!("Creator: {}", self.payer.pubkey());
Ok(format!("simulated_signature_{}", hex::encode(&content_hash[..8])))
}
pub async fn verify_content(&self, content_hash: &[u8; 32]) -> Result<VerificationResult> {
info!("Verifying content hash: {:?}", hex::encode(content_hash));
let (fingerprint_pda, _) = self.derive_fingerprint_address(content_hash);
debug!("Looking up PDA: {}", fingerprint_pda);
Ok(VerificationResult {
found: false, creator: None,
registered_at: None,
verified: false,
account_address: Some(fingerprint_pda.to_string()),
})
}
pub async fn get_fingerprint(&self, content_hash: &[u8; 32]) -> Result<Option<OnChainFingerprint>> {
let (fingerprint_pda, _) = self.derive_fingerprint_address(content_hash);
debug!("Fetching fingerprint account: {}", fingerprint_pda);
Ok(None) }
pub async fn get_creator_fingerprints(&self, creator: &str) -> Result<Vec<OnChainFingerprint>> {
let creator_pubkey = Pubkey::from_str(creator)
.context("Invalid creator address")?;
debug!("Fetching fingerprints for creator: {}", creator_pubkey);
Ok(Vec::new()) }
pub async fn transfer_ownership(
&self,
content_hash: &[u8; 32],
new_owner: &str,
) -> Result<String> {
let new_owner_pubkey = Pubkey::from_str(new_owner)
.context("Invalid new owner address")?;
let (fingerprint_pda, _) = self.derive_fingerprint_address(content_hash);
let mut instruction_data = vec![1u8]; instruction_data.extend_from_slice(&new_owner_pubkey.to_bytes());
let instruction = Instruction {
program_id: self.program_id,
accounts: vec![
AccountMeta::new(fingerprint_pda, false),
AccountMeta::new(self.payer.pubkey(), true),
AccountMeta::new_readonly(new_owner_pubkey, false),
],
data: instruction_data,
};
debug!("Transfer instruction created");
Ok(format!("simulated_transfer_{}", hex::encode(&content_hash[..8])))
}
}
pub async fn verify_fingerprint_hash(
rpc_url: &str,
content_hash: &[u8; 32],
) -> Result<VerificationResult> {
let program_id = Pubkey::from_str(FINGERPRINT_PROGRAM_ID)?;
let (fingerprint_pda, _) = Pubkey::find_program_address(
&[b"fingerprint", content_hash.as_ref()],
&program_id,
);
Ok(VerificationResult {
found: false,
creator: None,
registered_at: None,
verified: false,
account_address: Some(fingerprint_pda.to_string()),
})
}
pub fn parse_fingerprint_hash(hash_str: &str) -> Result<[u8; 32]> {
let bytes = hex_decode(hash_str)?;
if bytes.len() != 32 {
bail!("Fingerprint hash must be 32 bytes (64 hex characters)");
}
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
Ok(arr)
}
mod hex {
pub fn encode(bytes: &[u8]) -> String {
bytes.iter().map(|b| format!("{:02x}", b)).collect()
}
}
fn hex_decode(s: &str) -> Result<Vec<u8>> {
if s.len() % 2 != 0 {
bail!("Hex string must have even length");
}
(0..s.len())
.step_by(2)
.map(|i| {
u8::from_str_radix(&s[i..i + 2], 16)
.context("Invalid hex character")
})
.collect()
}
#[cfg(feature = "solana")]
pub mod instructions {
use super::*;
pub fn store_fingerprint(
program_id: &Pubkey,
fingerprint_pda: &Pubkey,
creator: &Pubkey,
content_hash: &[u8; 32],
metadata_uri: Option<&str>,
bump: u8,
) -> Instruction {
let mut data = vec![0u8]; data.extend_from_slice(content_hash);
data.push(bump);
if let Some(uri) = metadata_uri {
let uri_bytes = uri.as_bytes();
data.extend_from_slice(&(uri_bytes.len() as u32).to_le_bytes());
data.extend_from_slice(uri_bytes);
} else {
data.extend_from_slice(&0u32.to_le_bytes());
}
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*fingerprint_pda, false),
AccountMeta::new(*creator, true),
AccountMeta::new_readonly(system_program::id(), false),
],
data,
}
}
pub fn verify_fingerprint(
program_id: &Pubkey,
fingerprint_pda: &Pubkey,
) -> Instruction {
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new_readonly(*fingerprint_pda, false),
],
data: vec![2u8], }
}
pub fn transfer_ownership(
program_id: &Pubkey,
fingerprint_pda: &Pubkey,
current_owner: &Pubkey,
new_owner: &Pubkey,
) -> Instruction {
let mut data = vec![1u8]; data.extend_from_slice(&new_owner.to_bytes());
Instruction {
program_id: *program_id,
accounts: vec![
AccountMeta::new(*fingerprint_pda, false),
AccountMeta::new(*current_owner, true),
AccountMeta::new_readonly(*new_owner, false),
],
data,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_fingerprint_hash() {
let hash_str = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
let result = parse_fingerprint_hash(hash_str).unwrap();
assert_eq!(result.len(), 32);
assert_eq!(result[0], 0x01);
assert_eq!(result[1], 0x23);
}
#[test]
fn test_invalid_hash_length() {
let short_hash = "0123456789abcdef";
let result = parse_fingerprint_hash(short_hash);
assert!(result.is_err());
}
#[test]
fn test_hex_encode() {
let bytes = [0x01, 0x23, 0x45, 0x67];
let encoded = hex::encode(&bytes);
assert_eq!(encoded, "01234567");
}
}