use std::str::FromStr;
use chrono::{SubsecRound, Utc};
use reqwest::Client;
use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
use sha2::{Digest, Sha256};
use sp_core::{
crypto::Ss58Codec,
ed25519::{Public, Signature},
};
#[serde_as]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Transaction {
pub hash: String,
pub input: Input,
pub output: Output,
#[serde_as(as = "DisplayFromStr")]
pub value: Decimal,
#[serde_as(as = "DisplayFromStr")]
pub fee: Decimal,
pub script: Script,
pub signature: Vec<Sign>,
pub date: String,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Sign {
pub signatgure: Signature,
pub key: Public,
}
#[serde_as]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UTXO {
pub block: u64,
pub trx_hash: String,
pub output_hash: String,
pub unspent_hash: String,
#[serde_as(as = "DisplayFromStr")]
pub unspent: Decimal,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub enum Script {
Single,
Multi,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Input {
hash: String,
number: u8,
utxos: Vec<UTXO>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Output {
pub hash: String,
pub number: usize,
pub unspents: Vec<Unspent>,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Unspent {
pub hash: String,
pub data: UnspentData,
}
#[serde_as]
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct UnspentData {
pub wallet: Public,
pub salt: u32,
#[serde_as(as = "DisplayFromStr")]
pub value: Decimal,
}
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct TxRes {
pub hash: String,
pub status: String,
pub description: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ReqBody {
pub public_key: String,
pub request: String,
pub value: String,
}
impl Input {
fn new(response: ResBody) -> Self {
let hash_data = serde_json::to_string(&response.utxo_data)
.expect("Failed to serialize UTXO data");
let hash = HashMaker::generate(&hash_data);
Self {
hash,
number: response.utxo_data.len() as u8,
utxos: response.utxo_data,
}
}
}
impl Output {
pub fn new(unspents: Vec<Unspent>) -> Self {
let str_outputs = serde_json::to_string(&unspents)
.expect("Failed to serialize unspents");
Self {
hash: HashMaker::generate(&str_outputs),
number: unspents.len(),
unspents,
}
}
}
impl Unspent {
pub fn new(wallet: &Public, value: Decimal) -> Self {
let salt: u32 = rand::random();
let data = UnspentData {
wallet: *wallet,
salt,
value,
};
let hash_data = serde_json::to_string(&data)
.expect("Failed to serialize unspent data");
Self {
hash: HashMaker::generate(&hash_data),
data,
}
}
}
pub struct MerkelRoot;
impl MerkelRoot {
pub fn make(transactions: Vec<&String>) -> Vec<String> {
let mut hashs: Vec<String> = transactions.iter().map(|t| t.to_string()).collect();
while hashs.len() > 1 {
let mut new_hashs = Vec::with_capacity((hashs.len() + 1) / 2);
for chunk in hashs.chunks(2) {
let mut hasher = Sha256::new();
hasher.update(&chunk[0]);
if let Some(right) = chunk.get(1) {
hasher.update(right);
}
new_hashs.push(format!("{:x}", hasher.finalize()));
}
hashs = new_hashs;
}
hashs
}
}
pub struct HashMaker;
impl HashMaker {
pub fn generate(data: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(data);
format!("{:x}", hasher.finalize())
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ResBody {
pub public_key: String,
pub utxo_data: Vec<UTXO>,
pub status: String,
pub description: String,
}
impl Transaction {
pub async fn make_and_send(
wallet: String,
private: String,
to: String,
value: String,
) -> Result<String, String> {
let wallet = wallet.trim();
let decimal_value = Decimal::from_str(&value)
.map_err(|e| format!("Invalid value format: {}", e))?;
let fee = decimal_value * Decimal::from_str("0.01")
.expect("Failed to parse fee percentage");
let client = Client::new();
let request = ReqBody {
public_key: wallet.to_string(),
request: "utxo".to_string(),
value,
};
let req_url = "https://centichain.org/jrpc/autxo";
let response = client
.post(req_url)
.json(&request)
.send()
.await
.map_err(|_| "Failed to connect to server. Please check your internet connection.".to_string())?
.json::<ResBody>()
.await
.map_err(|_| "Failed to parse server response".to_string())?;
if response.status != "success" {
return Err(response.description);
}
Transaction::sending(
wallet.to_string(),
private,
decimal_value,
to,
response,
fee,
client,
)
.await
.map(|_| "Transaction Successfully Sent".to_string())
.map_err(|e| e.to_string())
}
async fn sending(
public_key: String,
private_key: String,
value: Decimal,
to: String,
response: ResBody,
fee: Decimal,
client: Client,
) -> Result<(), String> {
let wallet = sp_core::ed25519::Public::from_string(&public_key)
.map_err(|_| "Invalid public key format")?;
let sum_input: Decimal = response.utxo_data.iter()
.map(|unspent| unspent.unspent)
.sum();
let input = Input::new(response);
let to_wallet = to.parse()
.map_err(|_| "Wallet address is incorrect")?;
let change_wallet: Public = public_key.parse()
.map_err(|_| "Invalid change wallet address")?;
let mut unspents = Vec::new();
if sum_input > value + fee {
let change = sum_input - (value + fee);
unspents.push(Unspent::new(&change_wallet, change));
unspents.push(Unspent::new(&to_wallet, value));
} else {
unspents.push(Unspent::new(&to_wallet, value));
}
let output = Output::new(unspents);
let hash = MerkelRoot::make(vec![&input.hash, &output.hash]);
let sign = centichain_keypair::CentichainKey::signing(&private_key, &hash[0])
.map_err(|_| "Failed to sign transaction")?;
let signature = Sign {
signatgure: sign,
key: wallet,
};
let transaction = Transaction {
hash: hash[0].clone(),
input,
output,
value,
fee,
script: Script::Single,
signature: vec![signature],
date: Utc::now().round_subsecs(0).to_string(),
};
let raw_response = client
.post("https://centichain.org/jrpc/trx")
.json(&transaction)
.send()
.await
.map_err(|e| format!("Failed to send transaction: {}", e))?
.text()
.await
.map_err(|e| format!("Failed to get response text: {}", e))?;
println!("Raw response: {}", raw_response);
let response: TxRes = serde_json::from_str(&raw_response)
.map_err(|e| format!("Failed to parse transaction response: {}. Raw response: {}", e, raw_response))?;
if response.status == "success" {
Ok(())
} else {
Err("Server has problem! Please try with another provider.".to_string())
}
}
}