use crate::rlp::{Bytes, Decodable};
use crate::transactions::Reserved;
use crate::transactions::{Clause, Transaction};
use crate::utils::unhex;
use crate::{Address, U256};
use reqwest::{Client, Url};
use serde::{Deserialize, Serialize};
pub type AResult<T> = std::result::Result<T, Box<dyn std::error::Error + Send + Sync>>;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum ValidationError {
ZeroStorageKey,
BroadcastFailed(String),
}
impl std::error::Error for ValidationError {}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ZeroStorageKey => f.write_str("Account storage key cannot be zero"),
Self::BroadcastFailed(text) => {
f.write_str("Failed to broadcast: ")?;
f.write_str(text.strip_suffix('\n').unwrap_or(text))
}
}
}
}
pub struct ThorNode {
pub base_url: Url,
pub chain_tag: u8,
}
#[serde_with::serde_as]
#[derive(Deserialize)]
struct RawTxResponse {
#[serde_as(as = "unhex::Hex")]
raw: Bytes,
meta: Option<TransactionMeta>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ExtendedTransaction {
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub id: U256,
pub origin: Address,
pub delegator: Option<Address>,
pub size: u32,
#[serde(rename = "chainTag")]
pub chain_tag: u8,
#[serde(rename = "blockRef")]
#[serde_as(as = "unhex::HexNum<8, u64>")]
pub block_ref: u64,
pub expiration: u32,
pub clauses: Vec<Clause>,
#[serde(rename = "gasPriceCoef")]
pub gas_price_coef: u8,
pub gas: u64,
#[serde(rename = "dependsOn")]
#[serde_as(as = "Option<unhex::HexNum<32, U256>>")]
pub depends_on: Option<U256>,
#[serde_as(as = "unhex::HexNum<8, u64>")]
pub nonce: u64,
}
impl ExtendedTransaction {
pub fn as_transaction(self) -> Transaction {
let Self {
chain_tag,
block_ref,
expiration,
clauses,
gas_price_coef,
gas,
depends_on,
nonce,
delegator,
..
} = self;
Transaction {
chain_tag,
block_ref,
expiration,
clauses,
gas_price_coef,
gas,
depends_on,
nonce,
reserved: if delegator.is_some() {
Some(Reserved::new_delegated())
} else {
None
},
signature: None,
}
}
}
#[serde_with::serde_as]
#[derive(Deserialize)]
struct ExtendedTransactionResponse {
#[serde(flatten)]
transaction: ExtendedTransaction,
meta: Option<TransactionMeta>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct TransactionMeta {
#[serde(rename = "blockID")]
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub block_id: U256,
#[serde(rename = "blockNumber")]
pub block_number: u32,
#[serde(rename = "blockTimestamp")]
pub block_timestamp: u32,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Receipt {
#[serde(rename = "gasUsed")]
pub gas_used: u32,
#[serde(rename = "gasPayer")]
pub gas_payer: Address,
pub paid: U256,
pub reward: U256,
pub reverted: bool,
pub outputs: Vec<ReceiptOutput>,
}
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct ReceiptResponse {
#[serde(flatten)]
body: Receipt,
meta: ReceiptMeta,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ReceiptOutput {
#[serde(rename = "contractAddress")]
pub contract_address: Option<Address>,
pub events: Vec<Event>,
pub transfers: Vec<Transfer>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct ReceiptMeta {
#[serde(rename = "blockID")]
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub block_id: U256,
#[serde(rename = "blockNumber")]
pub block_number: u32,
#[serde(rename = "blockTimestamp")]
pub block_timestamp: u32,
#[serde(rename = "txID")]
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub tx_id: U256,
#[serde(rename = "txOrigin")]
pub tx_origin: Address,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Event {
pub address: Address,
#[serde_as(as = "Vec<unhex::HexNum<32, U256>>")]
pub topics: Vec<U256>,
#[serde_as(as = "unhex::Hex")]
pub data: Bytes,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct Transfer {
pub sender: Address,
pub recipient: Address,
pub amount: U256,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BlockInfo {
pub number: u32,
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub id: U256,
pub size: u32,
#[serde_as(as = "unhex::HexNum<32, U256>")]
#[serde(rename = "parentID")]
pub parent_id: U256,
pub timestamp: u32,
#[serde(rename = "gasLimit")]
pub gas_limit: u32,
pub beneficiary: Address,
#[serde(rename = "gasUsed")]
pub gas_used: u32,
#[serde(rename = "totalScore")]
pub total_score: u32,
#[serde_as(as = "unhex::HexNum<32, U256>")]
#[serde(rename = "txsRoot")]
pub txs_root: U256,
#[serde(rename = "txsFeatures")]
pub txs_features: u32,
#[serde_as(as = "unhex::HexNum<32, U256>")]
#[serde(rename = "stateRoot")]
pub state_root: U256,
#[serde_as(as = "unhex::HexNum<32, U256>")]
#[serde(rename = "receiptsRoot")]
pub receipts_root: U256,
#[serde(rename = "isTrunk")]
pub is_trunk: bool,
#[serde(rename = "isFinalized")]
pub is_finalized: bool,
pub com: bool,
pub signer: Address,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct BlockTransaction {
#[serde(flatten)]
pub transaction: ExtendedTransaction,
#[serde(flatten)]
pub receipt: Receipt,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct BlockResponse {
#[serde(flatten)]
base: BlockInfo,
#[serde_as(as = "Vec<unhex::HexNum<32, U256>>")]
transactions: Vec<U256>,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct BlockExtendedResponse {
#[serde(flatten)]
base: BlockInfo,
transactions: Vec<BlockTransaction>,
}
#[derive(Clone, Debug)]
pub enum BlockReference {
Best,
Finalized,
Number(u64),
ID(U256),
}
impl BlockReference {
fn as_query_param(&self) -> String {
match self {
BlockReference::Best => "best".to_string(),
BlockReference::Finalized => "finalized".to_string(),
BlockReference::Number(num) => format!("0x{:02x}", num),
BlockReference::ID(id) => format!("0x{:064x}", id),
}
}
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct AccountInfo {
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub balance: U256,
#[serde_as(as = "unhex::HexNum<32, U256>")]
pub energy: U256,
#[serde(rename = "hasCode")]
pub has_code: bool,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct AccountCodeResponse {
#[serde_as(as = "unhex::Hex")]
code: Bytes,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct AccountStorageResponse {
#[serde_as(as = "unhex::HexNum<32, U256>")]
value: U256,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize)]
struct TransactionBroadcastRequest {
#[serde_as(as = "unhex::Hex")]
raw: Bytes,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Deserialize)]
struct TransactionIdResponse {
#[serde_as(as = "unhex::HexNum<32, U256>")]
id: U256,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SimulateCallRequest {
pub clauses: Vec<Clause>,
pub gas: u64,
#[serde_as(as = "serde_with::DisplayFromStr")]
#[serde(rename = "gasPrice")]
pub gas_price: u64,
pub caller: Address,
#[serde_as(as = "serde_with::DisplayFromStr")]
#[serde(rename = "provedWork")]
pub proved_work: u64,
#[serde(rename = "gasPayer")]
pub gas_payer: Address,
pub expiration: u32,
#[serde_as(as = "unhex::HexNum<8, u64>")]
#[serde(rename = "blockRef")]
pub block_ref: u64,
}
#[serde_with::serde_as]
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct SimulateCallResponse {
#[serde_as(as = "unhex::Hex")]
pub data: Bytes,
pub events: Vec<Event>,
pub transfers: Vec<Transfer>,
#[serde(rename = "gasUsed")]
pub gas_used: u64,
pub reverted: bool,
#[serde(rename = "vmError")]
pub vm_error: String,
}
impl ThorNode {
pub const MAINNET_CHAIN_TAG: u8 = 0x4A;
pub const MAINNET_BASE_URL: &'static str = "https://mainnet.vecha.in/";
pub const TESTNET_CHAIN_TAG: u8 = 0x27;
pub const TESTNET_BASE_URL: &'static str = "https://testnet.vecha.in/";
pub fn mainnet() -> Self {
Self {
base_url: Self::MAINNET_BASE_URL.parse().unwrap(),
chain_tag: Self::MAINNET_CHAIN_TAG,
}
}
pub fn testnet() -> Self {
Self {
base_url: Self::TESTNET_BASE_URL.parse().unwrap(),
chain_tag: Self::TESTNET_CHAIN_TAG,
}
}
pub async fn fetch_transaction(
&self,
transaction_id: U256,
) -> AResult<Option<(Transaction, Option<TransactionMeta>)>> {
let client = Client::new();
let path = format!("/transactions/0x{:064x}", transaction_id);
let response = client
.get(self.base_url.join(&path)?)
.query(&[("raw", "true")])
.send()
.await?
.text()
.await?;
if response.strip_suffix('\n').unwrap_or(&response) == "null" {
Ok(None)
} else {
let decoded: RawTxResponse = serde_json::from_str(&response)?;
let tx = Transaction::decode(&mut &decoded.raw[..])?;
Ok(Some((tx, decoded.meta)))
}
}
pub async fn fetch_extended_transaction(
&self,
transaction_id: U256,
) -> AResult<Option<(ExtendedTransaction, Option<TransactionMeta>)>> {
let client = Client::new();
let path = format!("/transactions/0x{:064x}", transaction_id);
let response = client
.get(self.base_url.join(&path)?)
.send()
.await?
.text()
.await?;
if response.strip_suffix('\n').unwrap_or(&response) == "null" {
Ok(None)
} else {
let decoded: ExtendedTransactionResponse = serde_json::from_str(&response)?;
Ok(Some((decoded.transaction, decoded.meta)))
}
}
pub async fn fetch_transaction_receipt(
&self,
transaction_id: U256,
) -> AResult<Option<(Receipt, ReceiptMeta)>> {
let client = Client::new();
let path = format!("/transactions/0x{:064x}/receipt", transaction_id);
let response = client
.get(self.base_url.join(&path)?)
.send()
.await?
.text()
.await?;
if response.strip_suffix('\n').unwrap_or(&response) == "null" {
Ok(None)
} else {
let decoded: ReceiptResponse = serde_json::from_str(&response)?;
Ok(Some((decoded.body, decoded.meta)))
}
}
pub async fn fetch_block(
&self,
block_ref: BlockReference,
) -> AResult<Option<(BlockInfo, Vec<U256>)>> {
let client = Client::new();
let path = format!("/blocks/{}", block_ref.as_query_param());
let response = client
.get(self.base_url.join(&path)?)
.send()
.await?
.text()
.await?;
if response.strip_suffix('\n').unwrap_or(&response) == "null" {
Ok(None)
} else {
let decoded: BlockResponse = serde_json::from_str(&response)?;
Ok(Some((decoded.base, decoded.transactions)))
}
}
pub async fn fetch_block_expanded(
&self,
block_ref: BlockReference,
) -> AResult<Option<(BlockInfo, Vec<BlockTransaction>)>> {
let client = Client::new();
let path = format!("/blocks/{}", block_ref.as_query_param());
let response = client
.get(self.base_url.join(&path)?)
.query(&[("expanded", "true")])
.send()
.await?
.text()
.await?;
if response.strip_suffix('\n').unwrap_or(&response) == "null" {
Ok(None)
} else {
let decoded: BlockExtendedResponse = serde_json::from_str(&response)?;
Ok(Some((decoded.base, decoded.transactions)))
}
}
pub async fn broadcast_transaction(&self, transaction: &Transaction) -> AResult<U256> {
let client = Client::new();
let response = client
.post(self.base_url.join("/transactions")?)
.json(&TransactionBroadcastRequest {
raw: transaction.to_broadcastable_bytes()?,
})
.send()
.await?
.text()
.await?;
let decoded: TransactionIdResponse = serde_json::from_str(&response)
.map_err(|_| ValidationError::BroadcastFailed(response.to_string()))?;
Ok(decoded.id)
}
pub async fn fetch_account(&self, address: Address) -> AResult<AccountInfo> {
let client = Client::new();
let path = format!("/accounts/{}", address.to_hex());
Ok(client
.get(self.base_url.join(&path)?)
.send()
.await?
.json::<AccountInfo>()
.await?)
}
pub async fn fetch_account_code(&self, address: Address) -> AResult<Option<Bytes>> {
let client = Client::new();
let path = format!("/accounts/{}/code", address.to_hex());
let response = client
.get(self.base_url.join(&path)?)
.send()
.await?
.json::<AccountCodeResponse>()
.await?;
if response.code.is_empty() {
Ok(None)
} else {
Ok(Some(response.code))
}
}
pub async fn fetch_account_storage(&self, address: Address, key: U256) -> AResult<U256> {
if key == 0.into() {
return Err(Box::new(ValidationError::ZeroStorageKey));
}
let client = Client::new();
let path = format!("/accounts/{}/storage/0x{:064x}", address.to_hex(), key);
let response = client
.get(self.base_url.join(&path)?)
.send()
.await?
.json::<AccountStorageResponse>()
.await?;
Ok(response.value)
}
pub async fn simulate_execution(
&self,
request: SimulateCallRequest,
) -> AResult<Vec<SimulateCallResponse>> {
let client = Client::new();
let response = client
.post(self.base_url.join("/accounts/*")?)
.json(&request)
.send()
.await?
.json::<Vec<SimulateCallResponse>>()
.await?;
Ok(response)
}
}