use crate::error::BitcoinError;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RskNetwork {
Mainnet,
Testnet,
Regtest,
}
impl RskNetwork {
pub fn rpc_url(&self) -> &str {
match self {
RskNetwork::Mainnet => "https://public-node.rsk.co",
RskNetwork::Testnet => "https://public-node.testnet.rsk.co",
RskNetwork::Regtest => "http://localhost:4444",
}
}
pub fn chain_id(&self) -> u64 {
match self {
RskNetwork::Mainnet => 30,
RskNetwork::Testnet => 31,
RskNetwork::Regtest => 33,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RskAddress {
pub address: String,
}
impl RskAddress {
pub fn new(address: String) -> Result<Self, BitcoinError> {
if !address.starts_with("0x") {
return Err(BitcoinError::InvalidAddress(
"RSK address must start with 0x".to_string(),
));
}
if address.len() != 42 {
return Err(BitcoinError::InvalidAddress(
"RSK address must be 42 characters (0x + 40 hex)".to_string(),
));
}
Ok(Self { address })
}
pub fn to_lowercase(&self) -> String {
self.address.to_lowercase()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PegOperation {
PegIn,
PegOut,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum PegStatus {
Pending,
Confirming,
Processing,
Completed,
Failed,
Refunded,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PegInTransaction {
pub id: Uuid,
pub btc_tx_id: String,
pub amount: u64,
pub btc_address: String,
pub rsk_address: RskAddress,
pub rsk_tx_hash: Option<String>,
pub btc_confirmations: u32,
pub status: PegStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PegOutTransaction {
pub id: Uuid,
pub rsk_tx_hash: String,
pub amount: u64,
pub rsk_address: RskAddress,
pub btc_address: String,
pub btc_tx_id: Option<String>,
pub rsk_confirmations: u32,
pub status: PegStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PegConfig {
pub min_amount: u64,
pub max_amount: u64,
pub btc_confirmations_required: u32,
pub rsk_confirmations_required: u32,
pub federation_address: String,
}
impl Default for PegConfig {
fn default() -> Self {
Self {
min_amount: 5_000, max_amount: 1_000_000_000, btc_confirmations_required: 100,
rsk_confirmations_required: 100,
federation_address: String::new(),
}
}
}
pub struct RskClient {
network: RskNetwork,
#[allow(dead_code)]
rpc_url: String,
#[allow(dead_code)]
http_client: reqwest::Client,
}
impl RskClient {
pub fn new(network: RskNetwork) -> Self {
Self {
network,
rpc_url: network.rpc_url().to_string(),
http_client: reqwest::Client::new(),
}
}
pub fn network(&self) -> RskNetwork {
self.network
}
pub fn validate_address(&self, address: &str) -> Result<RskAddress, BitcoinError> {
RskAddress::new(address.to_string())
}
pub async fn get_balance(&self, address: &RskAddress) -> Result<u64, BitcoinError> {
let _ = address;
Ok(0)
}
pub async fn get_transaction_count(&self, address: &RskAddress) -> Result<u64, BitcoinError> {
let _ = address;
Ok(0)
}
pub async fn get_transaction_receipt(
&self,
tx_hash: &str,
) -> Result<RskTransactionReceipt, BitcoinError> {
let _ = tx_hash;
Err(BitcoinError::TransactionNotFound(
"Not implemented".to_string(),
))
}
pub async fn get_block_number(&self) -> Result<u64, BitcoinError> {
Ok(0)
}
pub async fn send_raw_transaction(&self, tx_hex: &str) -> Result<String, BitcoinError> {
let _ = tx_hex;
Err(BitcoinError::BroadcastFailed("Not implemented".to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RskTransactionReceipt {
pub transaction_hash: String,
pub block_number: u64,
pub block_hash: String,
pub from: RskAddress,
pub to: Option<RskAddress>,
pub gas_used: u64,
pub status: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RskContractDeployment {
pub id: Uuid,
pub bytecode: String,
pub constructor_args: Vec<String>,
pub deployer: RskAddress,
pub gas_limit: u64,
pub gas_price: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RskDeploymentResult {
pub deployment_id: Uuid,
pub tx_hash: Option<String>,
pub contract_address: Option<RskAddress>,
pub status: DeploymentStatus,
pub error: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum DeploymentStatus {
Pending,
Broadcast,
Confirmed,
Failed,
}
pub struct PegManager {
config: PegConfig,
rsk_client: RskClient,
peg_ins: HashMap<Uuid, PegInTransaction>,
peg_outs: HashMap<Uuid, PegOutTransaction>,
}
impl PegManager {
pub fn new(config: PegConfig, rsk_client: RskClient) -> Self {
Self {
config,
rsk_client,
peg_ins: HashMap::new(),
peg_outs: HashMap::new(),
}
}
pub fn initiate_peg_in(
&mut self,
btc_tx_id: String,
amount: u64,
btc_address: String,
rsk_address: RskAddress,
) -> Result<PegInTransaction, BitcoinError> {
if amount < self.config.min_amount {
return Err(BitcoinError::Validation(format!(
"Amount {} is below minimum {}",
amount, self.config.min_amount
)));
}
if amount > self.config.max_amount {
return Err(BitcoinError::Validation(format!(
"Amount {} exceeds maximum {}",
amount, self.config.max_amount
)));
}
let tx = PegInTransaction {
id: Uuid::new_v4(),
btc_tx_id,
amount,
btc_address,
rsk_address,
rsk_tx_hash: None,
btc_confirmations: 0,
status: PegStatus::Pending,
};
self.peg_ins.insert(tx.id, tx.clone());
Ok(tx)
}
pub fn initiate_peg_out(
&mut self,
rsk_tx_hash: String,
amount: u64,
rsk_address: RskAddress,
btc_address: String,
) -> Result<PegOutTransaction, BitcoinError> {
if amount < self.config.min_amount {
return Err(BitcoinError::Validation(format!(
"Amount {} is below minimum {}",
amount, self.config.min_amount
)));
}
if amount > self.config.max_amount {
return Err(BitcoinError::Validation(format!(
"Amount {} exceeds maximum {}",
amount, self.config.max_amount
)));
}
let tx = PegOutTransaction {
id: Uuid::new_v4(),
rsk_tx_hash,
amount,
rsk_address,
btc_address,
btc_tx_id: None,
rsk_confirmations: 0,
status: PegStatus::Pending,
};
self.peg_outs.insert(tx.id, tx.clone());
Ok(tx)
}
pub fn update_peg_in(
&mut self,
id: Uuid,
btc_confirmations: u32,
rsk_tx_hash: Option<String>,
) -> Result<(), BitcoinError> {
let tx = self
.peg_ins
.get_mut(&id)
.ok_or_else(|| BitcoinError::TransactionNotFound(id.to_string()))?;
tx.btc_confirmations = btc_confirmations;
if let Some(hash) = rsk_tx_hash {
tx.rsk_tx_hash = Some(hash);
}
if btc_confirmations >= self.config.btc_confirmations_required {
if tx.rsk_tx_hash.is_some() {
tx.status = PegStatus::Completed;
} else {
tx.status = PegStatus::Processing;
}
} else {
tx.status = PegStatus::Confirming;
}
Ok(())
}
pub fn update_peg_out(
&mut self,
id: Uuid,
rsk_confirmations: u32,
btc_tx_id: Option<String>,
) -> Result<(), BitcoinError> {
let tx = self
.peg_outs
.get_mut(&id)
.ok_or_else(|| BitcoinError::TransactionNotFound(id.to_string()))?;
tx.rsk_confirmations = rsk_confirmations;
if let Some(txid) = btc_tx_id {
tx.btc_tx_id = Some(txid);
}
if rsk_confirmations >= self.config.rsk_confirmations_required {
if tx.btc_tx_id.is_some() {
tx.status = PegStatus::Completed;
} else {
tx.status = PegStatus::Processing;
}
} else {
tx.status = PegStatus::Confirming;
}
Ok(())
}
pub fn get_peg_in(&self, id: &Uuid) -> Option<&PegInTransaction> {
self.peg_ins.get(id)
}
pub fn get_peg_out(&self, id: &Uuid) -> Option<&PegOutTransaction> {
self.peg_outs.get(id)
}
pub fn rsk_client(&self) -> &RskClient {
&self.rsk_client
}
pub fn federation_address(&self) -> &str {
&self.config.federation_address
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rsk_address_validation() {
let addr = RskAddress::new("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0".to_string());
assert!(addr.is_ok());
let invalid = RskAddress::new("742d35Cc6634C0532925a3b844Bc9e7595f0bEb0".to_string());
assert!(invalid.is_err());
let too_short = RskAddress::new("0x742d35Cc".to_string());
assert!(too_short.is_err());
}
#[test]
fn test_rsk_network_chain_ids() {
assert_eq!(RskNetwork::Mainnet.chain_id(), 30);
assert_eq!(RskNetwork::Testnet.chain_id(), 31);
assert_eq!(RskNetwork::Regtest.chain_id(), 33);
}
#[test]
fn test_peg_config_defaults() {
let config = PegConfig::default();
assert_eq!(config.min_amount, 5_000);
assert_eq!(config.max_amount, 1_000_000_000);
assert_eq!(config.btc_confirmations_required, 100);
assert_eq!(config.rsk_confirmations_required, 100);
}
#[test]
fn test_peg_manager_peg_in() {
let config = PegConfig::default();
let client = RskClient::new(RskNetwork::Testnet);
let mut manager = PegManager::new(config, client);
let rsk_addr =
RskAddress::new("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0".to_string()).unwrap();
let result = manager.initiate_peg_in(
"btc_txid".to_string(),
100_000,
"bc1qtest".to_string(),
rsk_addr,
);
assert!(result.is_ok());
let tx = result.unwrap();
assert_eq!(tx.amount, 100_000);
assert_eq!(tx.status, PegStatus::Pending);
}
#[test]
fn test_peg_manager_amount_validation() {
let config = PegConfig::default();
let client = RskClient::new(RskNetwork::Testnet);
let mut manager = PegManager::new(config, client);
let rsk_addr =
RskAddress::new("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0".to_string()).unwrap();
let result = manager.initiate_peg_in(
"btc_txid".to_string(),
100,
"bc1qtest".to_string(),
rsk_addr.clone(),
);
assert!(result.is_err());
let result = manager.initiate_peg_in(
"btc_txid".to_string(),
2_000_000_000,
"bc1qtest".to_string(),
rsk_addr,
);
assert!(result.is_err());
}
#[test]
fn test_peg_in_status_updates() {
let config = PegConfig::default();
let client = RskClient::new(RskNetwork::Testnet);
let mut manager = PegManager::new(config, client);
let rsk_addr =
RskAddress::new("0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0".to_string()).unwrap();
let tx = manager
.initiate_peg_in(
"btc_txid".to_string(),
100_000,
"bc1qtest".to_string(),
rsk_addr,
)
.unwrap();
manager.update_peg_in(tx.id, 50, None).unwrap();
let updated = manager.get_peg_in(&tx.id).unwrap();
assert_eq!(updated.status, PegStatus::Confirming);
manager
.update_peg_in(tx.id, 100, Some("rsk_tx_hash".to_string()))
.unwrap();
let completed = manager.get_peg_in(&tx.id).unwrap();
assert_eq!(completed.status, PegStatus::Completed);
}
#[test]
fn test_rsk_client_network() {
let client = RskClient::new(RskNetwork::Mainnet);
assert_eq!(client.network(), RskNetwork::Mainnet);
}
}