Skip to main content

tronz_contract/trc20/
mod.rs

1//! TRC20 ABI bindings and (optionally) the provider-bound [`Trc20Instance`].
2//!
3//! TRC20 is byte-for-byte compatible with the EVM ERC20 ABI, so the interface
4//! and all call/return codecs are generated directly by `alloy`'s
5//! [`sol!`](alloy_sol_macro::sol) macro. No bespoke ABI codec is needed.
6
7#[cfg(feature = "provider")]
8pub mod instance;
9
10use alloy_sol_macro::sol;
11use alloy_sol_types::SolCall;
12#[cfg(feature = "provider")]
13pub use instance::{Trc20Error, Trc20Ext, Trc20Instance};
14use tronz_primitives::{Address, Bytes, U256};
15
16sol! {
17    #[derive(Debug, PartialEq, Eq)]
18    interface ITRC20 {
19        function name()                                                  external view returns (string);
20        function symbol()                                                external view returns (string);
21        function decimals()                                              external view returns (uint8);
22        function totalSupply()                                           external view returns (uint256);
23        function balanceOf(address account)                              external view returns (uint256);
24        function transfer(address to, uint256 amount)                    external returns (bool);
25        function approve(address spender, uint256 amount)                external returns (bool);
26        function allowance(address owner, address spender)               external view returns (uint256);
27        function transferFrom(address from, address to, uint256 amount)  external returns (bool);
28
29        event Transfer(address indexed from, address indexed to, uint256 value);
30        event Approval(address indexed owner, address indexed spender, uint256 value);
31    }
32}
33
34/// ABI-encode the `transfer(to, amount)` call into the calldata bytes used by a
35/// `TriggerSmartContract`.
36pub fn encode_transfer(to: Address, amount: U256) -> Bytes {
37    ITRC20::transferCall { to: to.into(), amount }.abi_encode().into()
38}
39
40/// ABI-encode the `approve(spender, amount)` call.
41pub fn encode_approve(spender: Address, amount: U256) -> Bytes {
42    ITRC20::approveCall { spender: spender.into(), amount }.abi_encode().into()
43}
44
45/// ABI-encode the `transferFrom(from, to, amount)` call.
46pub fn encode_transfer_from(from: Address, to: Address, amount: U256) -> Bytes {
47    ITRC20::transferFromCall { from: from.into(), to: to.into(), amount }.abi_encode().into()
48}
49
50/// ABI-encode the `balanceOf(account)` constant call.
51pub fn encode_balance_of(account: Address) -> Bytes {
52    ITRC20::balanceOfCall { account: account.into() }.abi_encode().into()
53}
54
55/// ABI-encode the `allowance(owner, spender)` constant call.
56pub fn encode_allowance(owner: Address, spender: Address) -> Bytes {
57    ITRC20::allowanceCall { owner: owner.into(), spender: spender.into() }.abi_encode().into()
58}
59
60/// Decode the `uint256` returned by `balanceOf` / `allowance` / `totalSupply`.
61pub fn decode_uint256_return(output: &[u8]) -> Result<U256, alloy_sol_types::Error> {
62    ITRC20::balanceOfCall::abi_decode_returns(output)
63}
64
65/// Decode the `uint8` returned by `decimals`.
66pub fn decode_decimals_return(output: &[u8]) -> Result<u8, alloy_sol_types::Error> {
67    ITRC20::decimalsCall::abi_decode_returns(output)
68}
69
70/// Decode the `string` returned by `name` / `symbol`.
71pub fn decode_string_return(output: &[u8]) -> Result<String, alloy_sol_types::Error> {
72    ITRC20::nameCall::abi_decode_returns(output)
73}
74
75/// The four-byte selector for a generated call type.
76pub fn selector<C: SolCall>() -> [u8; 4] {
77    C::SELECTOR
78}
79
80#[cfg(test)]
81mod tests {
82    use alloy_sol_types::SolValue;
83
84    use super::*;
85
86    const ADDR: &str = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
87
88    fn addr() -> Address {
89        ADDR.parse().unwrap()
90    }
91
92    #[test]
93    fn transfer_selector_matches_erc20() {
94        // keccak256("transfer(address,uint256)")[..4] == 0xa9059cbb
95        let data = encode_transfer(addr(), U256::from(1u64));
96        assert_eq!(&data[..4], &[0xa9, 0x05, 0x9c, 0xbb]);
97        assert_eq!(ITRC20::transferCall::SELECTOR, [0xa9, 0x05, 0x9c, 0xbb]);
98    }
99
100    #[test]
101    fn balance_of_selector() {
102        // keccak256("balanceOf(address)")[..4] == 0x70a08231
103        let data = encode_balance_of(addr());
104        assert_eq!(&data[..4], &[0x70, 0xa0, 0x82, 0x31]);
105    }
106
107    #[test]
108    fn transfer_encodes_evm_address_not_tron() {
109        let data = encode_transfer(addr(), U256::from(0u64));
110        // The address argument occupies bytes [4..36]; the low 20 bytes must be
111        // the EVM body (0x41 prefix stripped), left-padded to 32 bytes.
112        let arg = &data[4..36];
113        assert_eq!(&arg[..12], &[0u8; 12], "address must be left-padded");
114        assert_eq!(&arg[12..], addr().as_evm_bytes());
115    }
116
117    #[test]
118    fn uint256_return_roundtrip() {
119        let value = U256::from(123_456_789u64);
120        let encoded = value.abi_encode();
121        assert_eq!(decode_uint256_return(&encoded).unwrap(), value);
122    }
123
124    #[test]
125    fn string_return_roundtrip() {
126        let encoded = "Tether USD".to_string().abi_encode();
127        assert_eq!(decode_string_return(&encoded).unwrap(), "Tether USD");
128    }
129
130    #[test]
131    fn decimals_return_roundtrip() {
132        // abi-encoded uint8 is a left-padded 32-byte word.
133        let mut encoded = [0u8; 32];
134        encoded[31] = 6;
135        assert_eq!(decode_decimals_return(&encoded).unwrap(), 6u8);
136    }
137}