tronz-contract 0.4.0

ABI bindings and contract instances for the tronz TRON SDK.
Documentation
//! [`TronCallBuilder`] — a typed contract call produced by [`tron_sol!`].
//!
//! Wraps a raw [`CallBuilder`] together with the originating [`SolCall`] type so
//! that [`call`](TronCallBuilder::call) can decode the return value into the
//! function's Rust return type, mirroring alloy's `#[sol(rpc)]` ergonomics.
//!
//! [`tron_sol!`]: tronz_sol_macro::tron_sol

use core::marker::PhantomData;

use alloy_sol_types::SolCall;
use tronz_primitives::Trx;
use tronz_provider::{PendingTransaction, TronProvider};

use crate::{
    call::CallBuilder,
    error::{ContractError, Result},
};

/// A type-safe, provider-bound contract call.
///
/// Generated by the typed methods of a [`tron_sol!`]-produced instance. Choose
/// [`call`](Self::call) to simulate (decoding into the function's return type),
/// or [`send`](Self::send) to broadcast a state-changing transaction.
///
/// [`tron_sol!`]: tronz_sol_macro::tron_sol
#[must_use = "a TronCallBuilder does nothing until `.call()` or `.send()` is awaited"]
pub struct TronCallBuilder<P, C> {
    inner: CallBuilder<P>,
    _call: PhantomData<fn() -> C>,
}

impl<P: TronProvider, C: SolCall> TronCallBuilder<P, C> {
    /// Wrap a raw [`CallBuilder`].
    #[inline]
    pub fn new(inner: CallBuilder<P>) -> Self {
        Self { inner, _call: PhantomData }
    }

    /// Attach a TRX amount to the call (for payable functions).
    #[inline]
    pub fn value(self, trx: Trx) -> Self {
        Self { inner: self.inner.value(trx), _call: PhantomData }
    }

    /// Attach a TRC10 token to the call.
    #[inline]
    pub fn token(self, token_id: i64, value: Trx) -> Self {
        Self { inner: self.inner.token(token_id, value), _call: PhantomData }
    }

    /// Estimate the energy this call would consume.
    pub async fn estimate_energy(&self) -> Result<i64> {
        self.inner.estimate_energy().await
    }

    /// Simulate the call (`trigger_constant_contract`) and decode the result
    /// into the function's return type.
    pub async fn call(self) -> Result<C::Return> {
        let output = self.inner.call().await?;
        let fn_name = C::SIGNATURE.split('(').next().unwrap_or(C::SIGNATURE);
        C::abi_decode_returns_validate(&output)
            .map_err(|e| ContractError::decode_err(fn_name, &output, e.into()))
    }

    /// Broadcast the call as a state-changing transaction (`trigger_smart_contract`).
    ///
    /// Requires a signer attached to the provider.
    pub async fn send(self) -> Result<PendingTransaction<P>> {
        self.inner.send().await
    }
}

#[cfg(test)]
mod tests {
    use alloy_primitives::U256;
    use alloy_sol_types::sol;
    use tronz_primitives::{Address, Bytes};
    use tronz_provider::{RootProvider, transport::mock::MockTransport, types::ConstantCallResult};

    use super::*;
    use crate::{call::CallBuilder, error::ContractError};

    sol! {
        function balanceOf(address) external view returns (uint256);
    }

    fn addr() -> Address {
        Address::from_evm_bytes([1u8; 20])
    }

    fn make_builder(
        provider: RootProvider<MockTransport>,
    ) -> TronCallBuilder<RootProvider<MockTransport>, balanceOfCall> {
        TronCallBuilder::new(CallBuilder::new(provider, addr(), Bytes::new()))
    }

    fn canned(output: Vec<u8>) -> ConstantCallResult {
        ConstantCallResult { output: output.into(), energy_used: 0, revert_reason: None }
    }

    #[tokio::test]
    async fn empty_output_yields_zero_data_error() {
        let provider = RootProvider::new(MockTransport::new());
        provider.transport().push_ok("trigger_constant_contract", canned(vec![]));
        let err = make_builder(provider).call().await.unwrap_err();
        assert!(
            matches!(&err, ContractError::ZeroData(name, _) if name == "balanceOf"),
            "expected ZeroData(\"balanceOf\", _), got {err:?}"
        );
    }

    #[tokio::test]
    async fn valid_output_decodes_correctly() {
        let provider = RootProvider::new(MockTransport::new());
        let mut out = [0u8; 32];
        out[31] = 99;
        provider.transport().push_ok("trigger_constant_contract", canned(out.to_vec()));
        let value = make_builder(provider).call().await.unwrap();
        assert_eq!(value, U256::from(99u64));
    }

    #[tokio::test]
    async fn truncated_output_yields_abi_error() {
        let provider = RootProvider::new(MockTransport::new());
        provider.transport().push_ok("trigger_constant_contract", canned(vec![0xde, 0xad]));
        let err = make_builder(provider).call().await.unwrap_err();
        assert!(matches!(err, ContractError::Abi(_)), "expected Abi(_), got {err:?}");
    }
}