pub use serde;
pub use reqwest;
use serde::{Serialize, Deserialize};
#[cfg(feature = "bitcoin")]
pub use bitcoin;
#[cfg(feature = "bitcoin")]
use bitcoin::hashes::hex::FromHex;
pub const DEFAULT_ENDPOINT: &str = "https://btc-v3.chainseeker.info/api";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Status {
pub blocks: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScriptSig {
pub asm: String,
pub hex: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vin {
pub txid: String,
pub vout: u32,
pub script_sig: ScriptSig,
pub txinwitness: Vec<String>,
pub sequence: u32,
pub value: u64,
pub address: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ScriptPubKey {
pub asm: String,
pub hex: String,
pub r#type: String,
pub address: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Utxo {
pub txid: String,
pub vout: u32,
pub script_pub_key: ScriptPubKey,
pub value: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Vout {
pub value: u64,
pub n: usize,
pub script_pub_key: ScriptPubKey,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Transaction {
pub confirmed_height: Option<u32>,
pub hex: String,
pub txid: String,
pub hash: String,
pub size: usize,
pub vsize: usize,
pub weight: usize,
pub version: i32,
pub locktime: u32,
pub vin: Vec<Vin>,
pub vout: Vec<Vout>,
pub fee: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Txid {
pub txid: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockHeader {
pub height: u32,
pub header: String,
pub hash: String,
pub version: i32,
pub previousblockhash: String,
pub merkleroot: String,
pub time: u32,
pub bits: String,
pub difficulty: f64,
pub nonce: u32,
pub size: u32,
pub strippedsize: u32,
pub weight: u32,
pub ntxs: usize,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockWithTxids {
pub height: u32,
pub header: String,
pub hash: String,
pub version: i32,
pub previousblockhash: String,
pub merkleroot: String,
pub time: u32,
pub bits: String,
pub difficulty: f64,
pub nonce: u32,
pub size: u32,
pub strippedsize: u32,
pub weight: u32,
pub txids: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BlockWithTxs {
pub height: u32,
pub header: String,
pub hash: String,
pub version: i32,
pub previousblockhash: String,
pub merkleroot: String,
pub time: u32,
pub bits: String,
pub difficulty: f64,
pub nonce: u32,
pub size: u32,
pub strippedsize: u32,
pub weight: u32,
pub txs: Vec<Transaction>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RichListCount {
pub count: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RichListRank {
pub rank: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RichListEntry {
pub script_pub_key: ScriptPubKey,
pub value: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockSummary {
pub hash : String,
pub time : u32,
pub nonce : u32,
pub size : u32,
pub strippedsize: u32,
pub weight : u32,
pub txcount : usize,
}
#[cfg(feature = "bitcoin")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AddressType {
P2wpkh,
}
#[cfg(feature = "bitcoin")]
#[derive(Debug, Clone)]
pub struct Wallet {
pub address_type: AddressType,
pub private_keys: Vec<bitcoin::PrivateKey>,
}
#[derive(Debug, Clone)]
pub struct Client {
endpoint: String,
reqwest_client: reqwest::Client,
}
impl Client {
pub fn new(endpoint: &str) -> Self {
Self {
endpoint: endpoint.to_string(),
reqwest_client: reqwest::Client::new(),
}
}
pub async fn get<T: for<'de> Deserialize<'de>>(&self, path: &str) -> Result<T, reqwest::Error> {
let url = format!("{}/v1/{}", &self.endpoint, path);
let result = self.reqwest_client.get(url)
.send().await?
.json::<T>().await?;
Ok(result)
}
pub async fn put<T: for<'de> Deserialize<'de>>(&self, path: &str, s: String) -> Result<T, reqwest::Error> {
let url = format!("{}/v1/{}", &self.endpoint, path);
let result = self.reqwest_client.put(url)
.body(s)
.send().await?
.json::<T>().await?;
Ok(result)
}
pub async fn status(&self) -> Result<Status, reqwest::Error> {
self.get("status").await
}
pub async fn tx(&self, txid: &str) -> Result<Transaction, reqwest::Error> {
self.get(&["tx", txid].join("/")).await
}
pub async fn put_tx(&self, hex: String) -> Result<Txid, reqwest::Error> {
self.put("tx/broadcast", hex).await
}
pub async fn block_summary(&self, offset: u32, limit: u32) -> Result<Vec<BlockSummary>, reqwest::Error> {
self.get(&["block_summary", &offset.to_string(), &limit.to_string()].join("/")).await
}
pub async fn block_with_txids<T: ToString>(&self, hash_or_height: T) -> Result<BlockWithTxids, reqwest::Error> {
self.get(&["block_with_txids", &hash_or_height.to_string()].join("/")).await
}
pub async fn block_with_txs<T: ToString>(&self, hash_or_height: T) -> Result<BlockWithTxs, reqwest::Error> {
self.get(&["block_with_txs", &hash_or_height.to_string()].join("/")).await
}
pub async fn block_header<T: ToString>(&self, hash_or_height: T) -> Result<BlockHeader, reqwest::Error> {
self.get(&["block", &hash_or_height.to_string()].join("/")).await
}
pub async fn txids(&self, script_or_address: &str) -> Result<Vec<String>, reqwest::Error> {
self.get(&["txids", script_or_address].join("/")).await
}
pub async fn txs(&self, script_or_address: &str) -> Result<Vec<Transaction>, reqwest::Error> {
self.get(&["txs", script_or_address].join("/")).await
}
pub async fn utxos(&self, script_or_address: &str) -> Result<Vec<Utxo>, reqwest::Error> {
self.get(&["utxos", script_or_address].join("/")).await
}
pub async fn rich_list_count(&self) -> Result<RichListCount, reqwest::Error> {
self.get("rich_list_count").await
}
pub async fn rich_list_addr_rank(&self, script_or_address: &str) -> Result<RichListRank, reqwest::Error> {
self.get(&["rich_list_addr_rank", script_or_address].join("/")).await
}
pub async fn rich_list(&self, offset: u32, limit: u32) -> Result<Vec<Option<RichListEntry>>, reqwest::Error> {
self.get(&["rich_list", &offset.to_string(), &limit.to_string()].join("/")).await
}
#[cfg(feature = "bitcoin")]
pub async fn generate_tx(
&self,
wallet: &Wallet,
txouts: &[bitcoin::TxOut],
change: &bitcoin::Script,
fee_rate: u64,
network: bitcoin::Network,
) -> Result<bitcoin::Transaction, reqwest::Error> {
let secp256k1 = bitcoin::secp256k1::Secp256k1::new();
let public_keys = wallet.private_keys.iter()
.map(|private_key| private_key.public_key(&secp256k1))
.collect::<Vec<bitcoin::PublicKey>>();
let addresses = match wallet.address_type {
AddressType::P2wpkh =>
public_keys.iter()
.map(|public_key| bitcoin::Address::p2wpkh(&public_key, network).unwrap())
.collect::<Vec<bitcoin::Address>>(),
};
let mut utxos_list = Vec::with_capacity(addresses.len());
for address in addresses.iter() {
utxos_list.push(self.utxos(&address.to_string()).await?);
}
let mut input_value: u64 = 0;
let inputs_list = utxos_list.iter().map(|utxos| {
utxos.iter().map(|utxo| {
input_value += utxo.value;
bitcoin::TxIn {
previous_output: bitcoin::OutPoint {
txid: bitcoin::Txid::from_hex(&utxo.txid).unwrap(),
vout: utxo.vout,
},
script_sig: bitcoin::Script::new(),
sequence: 0xFFFFFFFF,
witness: vec![],
}
}).collect::<Vec<bitcoin::TxIn>>()
}).collect::<Vec<Vec<bitcoin::TxIn>>>();
let output_value = txouts.iter().map(|txout| txout.value).sum::<u64>();
let mut outputs = txouts.to_vec();
outputs.push(bitcoin::TxOut {
script_pubkey: (*change).clone(),
value: 0,
});
let mut tx = bitcoin::Transaction {
version: 2,
lock_time: 0,
input: inputs_list.concat(),
output: outputs,
};
let sign = |tx: &mut bitcoin::Transaction| {
let mut sig_hasher = bitcoin::util::bip143::SigHashCache::new(tx);
let mut input_index = 0;
for (utxos, (public_key, private_key)) in utxos_list.iter().zip(public_keys.iter().zip(wallet.private_keys.iter())) {
let script_code = bitcoin::Script::new_p2pkh(&public_key.pubkey_hash());
for utxo in utxos.iter() {
let sighash = sig_hasher.signature_hash(input_index, &script_code, utxo.value, bitcoin::SigHashType::All);
let message = bitcoin::secp256k1::Message::from_slice(&sighash).unwrap();
let signature = secp256k1.sign(&message, &private_key.key);
let mut signature = signature.serialize_der().to_vec();
signature.push(0x01);
let witness = sig_hasher.access_witness(input_index);
witness.clear();
witness.push(signature);
witness.push(public_key.to_bytes());
input_index += 1;
}
}
};
sign(&mut tx);
let weight = tx.get_weight() as u64;
let fee = fee_rate * weight;
tx.output.last_mut().unwrap().value = input_value - output_value - fee;
sign(&mut tx);
Ok(tx)
}
}
pub fn new(endpoint: &str) -> Client {
Client::new(endpoint)
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn status() {
let client = new(DEFAULT_ENDPOINT);
assert!(client.status().await.unwrap().blocks > 0);
}
#[tokio::test]
async fn tx() {
const TXID: &str = "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098";
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.tx(TXID).await.unwrap().txid, TXID);
}
#[cfg(feature = "bitcoin")]
use bitcoin::consensus::Encodable;
#[cfg(feature = "bitcoin")]
#[tokio::test]
async fn put_tx() {
let secp256k1 = bitcoin::secp256k1::Secp256k1::new();
let client = new("https://tbtc-v3.chainseeker.info/api");
let privkey = bitcoin::PrivateKey::from_wif(&std::env::var("CS_PRIVKEY").unwrap()).unwrap();
let pubkey = privkey.public_key(&secp256k1);
let wallet = Wallet {
address_type: AddressType::P2wpkh,
private_keys: vec![privkey],
};
let change = bitcoin::Script::new_v0_wpkh(&pubkey.wpubkey_hash().unwrap());
let tx = client.generate_tx(&wallet, &[], &change, 10, bitcoin::Network::Testnet).await.unwrap();
println!("Txid: {}", tx.txid());
let mut tx_raw = Vec::new();
tx.consensus_encode(&mut tx_raw).unwrap();
let tx_hex = hex::encode(tx_raw);
println!("Raw tx: {}", tx_hex);
assert_eq!(client.put_tx(tx_hex).await.unwrap().txid, tx.txid().to_string());
}
#[tokio::test]
async fn block_summary() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_summary(0, 10).await.unwrap().len(), 10);
}
const BLOCK_HASH: &str = "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048";
const BLOCK_HEIGHT: u32 = 1;
const TXID: &str = "0e3e2357e806b6cdb1f70b54c3a3a17b6714ee1f0e68bebb44a74b1efd512098";
#[tokio::test]
async fn block_with_txids_from_hash() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_with_txids(BLOCK_HASH).await.unwrap().txids, [TXID]);
}
#[tokio::test]
async fn block_with_txids_from_height() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_with_txids(BLOCK_HEIGHT).await.unwrap().txids, [TXID]);
}
#[tokio::test]
async fn block_with_txs_from_hash() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_with_txs(BLOCK_HASH).await.unwrap().txs[0].txid, TXID);
}
#[tokio::test]
async fn block_with_txs_from_height() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_with_txs(BLOCK_HEIGHT).await.unwrap().txs[0].txid, TXID);
}
#[tokio::test]
async fn block_header_from_hash() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_header(BLOCK_HASH).await.unwrap().hash, BLOCK_HASH);
}
#[tokio::test]
async fn block_header_from_height() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.block_header(BLOCK_HEIGHT).await.unwrap().hash, BLOCK_HASH);
}
const ADDRESS: &str = "1CounterpartyXXXXXXXXXXXXXXXUWLpVr";
#[tokio::test]
async fn txids() {
let client = new(DEFAULT_ENDPOINT);
assert!(!client.txids(ADDRESS).await.unwrap().is_empty());
}
#[tokio::test]
async fn txs() {
let client = new(DEFAULT_ENDPOINT);
assert!(!client.txs(ADDRESS).await.unwrap().is_empty());
}
#[tokio::test]
async fn utxos() {
let client = new(DEFAULT_ENDPOINT);
assert!(!client.utxos(ADDRESS).await.unwrap().is_empty());
}
#[tokio::test]
async fn rich_list_count() {
let client = new(DEFAULT_ENDPOINT);
assert!(client.rich_list_count().await.unwrap().count > 0);
}
#[tokio::test]
async fn rich_list_addr_rank() {
let client = new(DEFAULT_ENDPOINT);
assert!(client.rich_list_addr_rank(ADDRESS).await.unwrap().rank > 0);
}
#[tokio::test]
async fn rich_list() {
let client = new(DEFAULT_ENDPOINT);
assert_eq!(client.rich_list(0, 100).await.unwrap().len(), 100);
}
}