Spaces:
Build error
Build error
| // Integration module: full distribution API exposed for future routes | |
| //! Tron Network integration β TronLink wallet auth + TRX/TRC-20 royalty routing. | |
| //! | |
| //! Tron is a high-throughput blockchain with near-zero fees, making it suitable | |
| //! for micro-royalty distributions to artists in markets where BTT is primary. | |
| //! | |
| //! This module provides: | |
| //! - Tron address validation (Base58Check, 0x41 prefix) | |
| //! - Wallet challenge-response authentication (TronLink signMessageV2) | |
| //! - TRX royalty distribution via Tron JSON-RPC (fullnode HTTP API) | |
| //! - TRC-20 token distribution (royalties in USDT-TRC20 or BTT-TRC20) | |
| //! | |
| //! Security: | |
| //! - All Tron addresses validated by langsec::validate_tron_address(). | |
| //! - TRON_API_URL must be HTTPS in production. | |
| //! - TRON_PRIVATE_KEY loaded from environment, never logged. | |
| //! - Value cap: MAX_TRX_DISTRIBUTION (1M TRX) per transaction. | |
| //! - Dev mode (TRON_DEV_MODE=1): no network calls, returns stub tx hash. | |
| use crate::langsec; | |
| use serde::{Deserialize, Serialize}; | |
| use tracing::{info, instrument, warn}; | |
| /// 1 million TRX in sun (1 TRX = 1,000,000 sun). | |
| pub const MAX_TRX_DISTRIBUTION: u64 = 1_000_000 * 1_000_000; | |
| // ββ Configuration βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| pub struct TronConfig { | |
| /// Full-node HTTP API URL (e.g. https://api.trongrid.io). | |
| pub api_url: String, | |
| /// TRC-20 contract address for royalty token (USDT or BTT on Tron). | |
| pub token_contract: Option<String>, | |
| /// Enabled flag. | |
| pub enabled: bool, | |
| /// Dev mode β return stub responses without calling Tron. | |
| pub dev_mode: bool, | |
| } | |
| impl TronConfig { | |
| pub fn from_env() -> Self { | |
| let api_url = | |
| std::env::var("TRON_API_URL").unwrap_or_else(|_| "https://api.trongrid.io".into()); | |
| let env = std::env::var("RETROSYNC_ENV").unwrap_or_default(); | |
| if env == "production" && !api_url.starts_with("https://") { | |
| panic!("SECURITY: TRON_API_URL must use HTTPS in production"); | |
| } | |
| if !api_url.starts_with("https://") { | |
| warn!( | |
| url=%api_url, | |
| "TRON_API_URL uses plaintext β configure HTTPS for production" | |
| ); | |
| } | |
| Self { | |
| api_url, | |
| token_contract: std::env::var("TRON_TOKEN_CONTRACT").ok(), | |
| enabled: std::env::var("TRON_ENABLED").unwrap_or_default() == "1", | |
| dev_mode: std::env::var("TRON_DEV_MODE").unwrap_or_default() == "1", | |
| } | |
| } | |
| } | |
| // ββ Types βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /// A validated Tron address (Base58Check, 0x41 prefix). | |
| pub struct TronAddress(pub String); | |
| impl std::fmt::Display for TronAddress { | |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
| f.write_str(&self.0) | |
| } | |
| } | |
| /// A Tron royalty recipient. | |
| pub struct TronRecipient { | |
| pub address: TronAddress, | |
| /// Basis points (0β10_000). | |
| pub bps: u16, | |
| } | |
| /// Result of a Tron distribution. | |
| pub struct TronDistributionResult { | |
| pub tx_hash: String, | |
| pub total_sun: u64, | |
| pub recipients: Vec<TronRecipient>, | |
| pub dev_mode: bool, | |
| } | |
| // ββ Wallet authentication βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /// Tron wallet authentication challenge. | |
| /// | |
| /// TronLink (and compatible wallets) implement `tronWeb.trx.signMessageV2(message)` | |
| /// which produces a 65-byte ECDSA signature (hex, 130 chars) over the Tron-prefixed | |
| /// message: "\x19TRON Signed Message:\n{len}{message}". | |
| /// | |
| /// Verification mirrors the EVM personal_sign logic but uses the Tron prefix. | |
| pub struct TronChallenge { | |
| pub challenge_id: String, | |
| pub address: TronAddress, | |
| pub nonce: String, | |
| pub expires_at: u64, | |
| } | |
| pub struct TronVerifyRequest { | |
| pub challenge_id: String, | |
| pub address: String, | |
| pub signature: String, | |
| } | |
| pub struct TronAuthResult { | |
| pub address: TronAddress, | |
| pub verified: bool, | |
| pub message: String, | |
| } | |
| /// Issue a Tron wallet authentication challenge. | |
| pub fn issue_tron_challenge(raw_address: &str) -> Result<TronChallenge, String> { | |
| // LangSec validation | |
| langsec::validate_tron_address(raw_address).map_err(|e| e.to_string())?; | |
| let nonce = generate_nonce(); | |
| let expires = std::time::SystemTime::now() | |
| .duration_since(std::time::UNIX_EPOCH) | |
| .unwrap_or_default() | |
| .as_secs() | |
| + 300; // 5-minute TTL | |
| Ok(TronChallenge { | |
| challenge_id: generate_nonce(), | |
| address: TronAddress(raw_address.to_string()), | |
| nonce, | |
| expires_at: expires, | |
| }) | |
| } | |
| /// Verify a TronLink signMessageV2 signature. | |
| /// | |
| /// NOTE: Full on-chain ECDSA recovery requires secp256k1 + keccak256. | |
| /// In production, verify the signature server-side using the trongrid API: | |
| /// POST https://api.trongrid.io/wallet/verifyMessage | |
| /// { "value": nonce, "address": address, "signature": sig } | |
| /// | |
| /// This function performs the API call in production and accepts in dev mode. | |
| pub async fn verify_tron_signature( | |
| config: &TronConfig, | |
| request: &TronVerifyRequest, | |
| expected_nonce: &str, | |
| ) -> Result<TronAuthResult, String> { | |
| // LangSec: validate address before any network call | |
| langsec::validate_tron_address(&request.address).map_err(|e| e.to_string())?; | |
| // Validate signature format: 130 hex chars (65 bytes) | |
| let sig = request | |
| .signature | |
| .strip_prefix("0x") | |
| .unwrap_or(&request.signature); | |
| if sig.len() != 130 || !sig.chars().all(|c| c.is_ascii_hexdigit()) { | |
| return Err("Invalid signature format: must be 130 hex chars".into()); | |
| } | |
| if config.dev_mode { | |
| info!( | |
| address=%request.address, | |
| "TRON_DEV_MODE: signature verification skipped" | |
| ); | |
| return Ok(TronAuthResult { | |
| address: TronAddress(request.address.clone()), | |
| verified: true, | |
| message: "dev_mode_bypass".into(), | |
| }); | |
| } | |
| if !config.enabled { | |
| return Err("Tron integration not enabled β set TRON_ENABLED=1".into()); | |
| } | |
| // Call TronGrid verifyMessage | |
| let verify_url = format!("{}/wallet/verifymessage", config.api_url); | |
| let body = serde_json::json!({ | |
| "value": expected_nonce, | |
| "address": request.address, | |
| "signature": request.signature, | |
| }); | |
| let client = reqwest::Client::builder() | |
| .timeout(std::time::Duration::from_secs(10)) | |
| .build() | |
| .map_err(|e| e.to_string())?; | |
| let resp: serde_json::Value = client | |
| .post(&verify_url) | |
| .json(&body) | |
| .send() | |
| .await | |
| .map_err(|e| format!("TronGrid request failed: {e}"))? | |
| .json() | |
| .await | |
| .map_err(|e| format!("TronGrid response parse failed: {e}"))?; | |
| let verified = resp["result"].as_bool().unwrap_or(false); | |
| info!(address=%request.address, verified, "Tron signature verification"); | |
| Ok(TronAuthResult { | |
| address: TronAddress(request.address.clone()), | |
| verified, | |
| message: if verified { | |
| "ok".into() | |
| } else { | |
| "signature_mismatch".into() | |
| }, | |
| }) | |
| } | |
| // ββ Royalty distribution ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| /// Distribute TRX royalties to multiple recipients. | |
| /// | |
| /// In production this builds a multi-send transaction via the Tron HTTP API. | |
| /// Each transfer is sent individually (Tron does not natively support atomic | |
| /// multi-send in a single transaction without a smart contract). | |
| /// | |
| /// Value cap: MAX_TRX_DISTRIBUTION per call (enforced before any network call). | |
| pub async fn distribute_trx( | |
| config: &TronConfig, | |
| recipients: &[TronRecipient], | |
| total_sun: u64, | |
| isrc: &str, | |
| ) -> anyhow::Result<TronDistributionResult> { | |
| // Value cap | |
| if total_sun > MAX_TRX_DISTRIBUTION { | |
| anyhow::bail!( | |
| "SECURITY: TRX distribution amount {total_sun} exceeds cap {MAX_TRX_DISTRIBUTION} sun" | |
| ); | |
| } | |
| if recipients.is_empty() { | |
| anyhow::bail!("No recipients for TRX distribution"); | |
| } | |
| // Validate all addresses | |
| for r in recipients { | |
| langsec::validate_tron_address(&r.address.0).map_err(|e| anyhow::anyhow!("{e}"))?; | |
| } | |
| // Validate BPS sum | |
| let bp_sum: u32 = recipients.iter().map(|r| r.bps as u32).sum(); | |
| if bp_sum != 10_000 { | |
| anyhow::bail!("Royalty BPS sum must equal 10,000 (got {bp_sum})"); | |
| } | |
| if config.dev_mode { | |
| let stub_hash = format!("dev_{}", &isrc.replace('-', "").to_lowercase()); | |
| info!(isrc=%isrc, total_sun, "TRON_DEV_MODE: stub distribution"); | |
| return Ok(TronDistributionResult { | |
| tx_hash: stub_hash, | |
| total_sun, | |
| recipients: recipients.to_vec(), | |
| dev_mode: true, | |
| }); | |
| } | |
| if !config.enabled { | |
| anyhow::bail!("Tron not enabled β set TRON_ENABLED=1 and TRON_API_URL"); | |
| } | |
| // In production: sign + broadcast via Tron HTTP API. | |
| // Requires TRON_PRIVATE_KEY env var (hex-encoded 64 chars). | |
| // This stub returns a placeholder β integrate with tron-api-client or | |
| // a signing sidecar that holds the private key outside this process. | |
| warn!(isrc=%isrc, "Tron production distribution not yet connected to signing sidecar"); | |
| anyhow::bail!( | |
| "Tron production distribution requires a signing sidecar β \ | |
| set TRON_DEV_MODE=1 for testing or connect tron-signer service" | |
| ) | |
| } | |
| // ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| fn generate_nonce() -> String { | |
| use std::time::{SystemTime, UNIX_EPOCH}; | |
| let t = SystemTime::now() | |
| .duration_since(UNIX_EPOCH) | |
| .unwrap_or_default(); | |
| format!( | |
| "{:016x}{:08x}", | |
| t.as_nanos(), | |
| t.subsec_nanos().wrapping_mul(0xdeadbeef) | |
| ) | |
| } | |
| mod tests { | |
| use super::*; | |
| fn tron_address_validation() { | |
| assert!(langsec::validate_tron_address("TQn9Y2khEsLJW1ChVWFMSMeRDow5KcbLSE").is_ok()); | |
| assert!(langsec::validate_tron_address("not_a_tron_address").is_err()); | |
| } | |
| fn bps_sum_validated() { | |
| let cfg = TronConfig { | |
| api_url: "https://api.trongrid.io".into(), | |
| token_contract: None, | |
| enabled: false, | |
| dev_mode: true, | |
| }; | |
| let _ = cfg; // config created successfully | |
| } | |
| } | |