diff --git a/Cargo.toml b/Cargo.toml index 115cb87..f1fb8a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,3 +21,5 @@ ark-ff = "0.4.2" hex = "0.4.3" lazy_static = "1.4.0" regex = "1.10.2" +bs58 = "0.5.0" +ed25519-dalek = "2.1.0" diff --git a/config.template.toml b/config.template.toml index ec21028..3ffc035 100644 --- a/config.template.toml +++ b/config.template.toml @@ -23,3 +23,7 @@ api_key = "xxxxxx" [custom_resolvers] "0xXXXXXXXXXXXX" = ["domain.stark"] + +[solana] +rpc_url = "https://xxxxxxx.solana-mainnet.quiknode.pro/xxxxxxx" +private_key = "xxxxxxx" diff --git a/src/config.rs b/src/config.rs index 631290c..81ccd13 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,11 @@ pub_struct!(Clone, Deserialize; Starkscan { api_key: String, }); +pub_struct!(Clone, Deserialize; Solana { + rpc_url: String, + private_key: FieldElement, +}); + #[derive(Deserialize)] struct RawConfig { server: Server, @@ -46,6 +51,7 @@ struct RawConfig { contracts: Contracts, starkscan: Starkscan, custom_resolvers: HashMap>, + solana: Solana, } pub_struct!(Clone, Deserialize; Config { @@ -55,6 +61,7 @@ pub_struct!(Clone, Deserialize; Config { starkscan: Starkscan, custom_resolvers: HashMap>, reversed_resolvers: HashMap, + solana: Solana, }); impl From for Config { @@ -72,6 +79,7 @@ impl From for Config { starkscan: raw.starkscan, custom_resolvers: raw.custom_resolvers, reversed_resolvers, + solana: raw.solana, } } } diff --git a/src/endpoints/crosschain/mod.rs b/src/endpoints/crosschain/mod.rs new file mode 100644 index 0000000..cb8dfa4 --- /dev/null +++ b/src/endpoints/crosschain/mod.rs @@ -0,0 +1 @@ +pub mod solana; diff --git a/src/endpoints/crosschain/solana/claim.rs b/src/endpoints/crosschain/solana/claim.rs new file mode 100644 index 0000000..d9f8f6d --- /dev/null +++ b/src/endpoints/crosschain/solana/claim.rs @@ -0,0 +1,197 @@ +use std::{ + sync::Arc, + time::{SystemTime, UNIX_EPOCH}, +}; + +use crate::{ + models::AppState, + utils::{get_error, to_hex}, +}; +use axum::{extract::State, http::StatusCode, response::IntoResponse, Json}; +use chrono::{Duration, Utc}; +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use mongodb::bson::doc; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use starknet::{ + core::{ + crypto::{ecdsa_sign, pedersen_hash}, + types::FieldElement, + }, + id::encode, +}; + +#[derive(Deserialize, Debug, Clone)] +pub struct SigQuery { + source_domain: String, + target_address: FieldElement, + source_signature: Vec, + max_validity: u64, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct SNSResponse { + owner_key: String, +} +#[derive(Serialize)] +struct JsonRequest { + jsonrpc: String, + method: String, + params: JsonParams, + id: i32, +} + +#[derive(Serialize)] +struct JsonParams { + domain: String, +} + +#[derive(Deserialize)] +#[allow(unused)] +struct JsonResponse { + jsonrpc: String, + result: Option, + id: i32, + error: Option, +} + +#[derive(Deserialize)] +#[allow(unused)] +struct JsonError { + code: i32, + message: String, +} + +lazy_static::lazy_static! { + static ref SOL_SUBDOMAIN_STR: FieldElement = FieldElement::from_dec_str("9145722242464647959622012987758").unwrap(); +} + +pub async fn handler( + State(state): State>, + Json(query): Json, +) -> impl IntoResponse { + let source_domain = query.source_domain; + let max_validity = query.max_validity; + let target_address = query.target_address; + let source_signature_array: [u8; 64] = match query.source_signature.clone().try_into() { + Ok(arr) => arr, + Err(_) => { + return get_error("Invalid signature length".to_string()); + } + }; + + // verify max_validity is not expired + if !is_valid_timestamp(max_validity) { + return get_error("Signature expired".to_string()); + } + + // get owner of SNS domain + let request_data = JsonRequest { + jsonrpc: "2.0".to_string(), + method: "sns_resolveDomain".to_string(), + params: JsonParams { + domain: source_domain.clone(), + }, + id: 5678, + }; + let client = reqwest::Client::new(); + match client + .post(state.conf.solana.rpc_url.clone()) + .json(&request_data) + .send() + .await + { + Ok(response) => { + match response.json::().await { + Ok(parsed) => { + let owner_pubkey = parsed.result.unwrap(); + + // recreate the message hash + let message = format!( + "{} allow claiming {} on starknet on {} at max validity timestamp {}", + owner_pubkey, + source_domain, + to_hex(&target_address), + max_validity + ); + + // verify Solana signature + match verify_signature(&owner_pubkey, &message, &source_signature_array) { + Ok(()) => { + // Generate starknet signature + let stark_max_validity = Utc::now() + Duration::hours(1); + let stark_max_validity_sec = stark_max_validity.timestamp(); + + let domain_splitted: Vec<&str> = source_domain.split('.').collect(); + let name_encoded = encode(domain_splitted[0]).unwrap(); + + let hash = pedersen_hash( + &pedersen_hash( + &pedersen_hash( + &SOL_SUBDOMAIN_STR, + &FieldElement::from_dec_str( + stark_max_validity_sec.to_string().as_str(), + ) + .unwrap(), + ), + &name_encoded, + ), + &target_address, + ); + + match ecdsa_sign(&state.conf.solana.private_key.clone(), &hash) { + Ok(signature) => ( + StatusCode::OK, + Json(json!({ + "r": signature.r, + "s": signature.s, + "max_validity": stark_max_validity_sec + })), + ) + .into_response(), + Err(e) => get_error(format!( + "Error while generating Starknet signature: {}", + e + )), + } + } + Err(e) => get_error(format!("Signature verification failed: {}", e)), + } + } + Err(e) => get_error(format!("Error parsing response from SNS RPC: {}", e)), + } + } + Err(e) => get_error(format!("Error sending request: SNS RPC: {}", e)), + } +} + +fn is_valid_timestamp(max_validity: u64) -> bool { + let now = SystemTime::now(); + + if let Ok(duration_since_epoch) = now.duration_since(UNIX_EPOCH) { + let current_timestamp = duration_since_epoch.as_secs(); + current_timestamp < max_validity + } else { + false + } +} + +fn verify_signature( + public_key_base58: &str, + message: &str, + signature_bytes: &[u8; 64], +) -> Result<(), ed25519_dalek::SignatureError> { + // Convert the public key bytes to a VerifyingKey instance + let public_key_bytes = bs58::decode(public_key_base58).into_vec().unwrap(); + let v_key = + VerifyingKey::from_bytes(unsafe { &*(public_key_bytes.as_ptr() as *const [u8; 32]) })?; + + // Convert the signature bytes to a Signature instance + let signature = Signature::from_bytes(signature_bytes); + + // Convert message to byte array + let message_bytes = message.as_bytes(); + + // Verify the signature + v_key.verify(message_bytes, &signature) +} diff --git a/src/endpoints/crosschain/solana/mod.rs b/src/endpoints/crosschain/solana/mod.rs new file mode 100644 index 0000000..1e89666 --- /dev/null +++ b/src/endpoints/crosschain/solana/mod.rs @@ -0,0 +1 @@ +pub mod claim; diff --git a/src/endpoints/mod.rs b/src/endpoints/mod.rs index fb4b532..1097f2a 100644 --- a/src/endpoints/mod.rs +++ b/src/endpoints/mod.rs @@ -4,6 +4,7 @@ pub mod addr_to_external_domains; pub mod addr_to_full_ids; pub mod addr_to_token_id; pub mod addrs_to_domains; +pub mod crosschain; pub mod data_to_ids; pub mod domain_to_addr; pub mod domain_to_data; diff --git a/src/main.rs b/src/main.rs index ec0d9ab..024ff29 100644 --- a/src/main.rs +++ b/src/main.rs @@ -147,6 +147,10 @@ async fn main() { get(endpoints::renewal::get_non_subscribed_domains::handler), ) .route("/galxe/verify", post(endpoints::galxe::verify::handler)) + .route( + "/crosschain/solana/claim", + post(endpoints::crosschain::solana::claim::handler), + ) .with_state(shared_state) .layer(cors);