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 {
38        to: to.into(),
39        amount,
40    }
41    .abi_encode()
42    .into()
43}
44
45/// ABI-encode the `approve(spender, amount)` call.
46pub fn encode_approve(spender: Address, amount: U256) -> Bytes {
47    ITRC20::approveCall {
48        spender: spender.into(),
49        amount,
50    }
51    .abi_encode()
52    .into()
53}
54
55/// ABI-encode the `transferFrom(from, to, amount)` call.
56pub fn encode_transfer_from(from: Address, to: Address, amount: U256) -> Bytes {
57    ITRC20::transferFromCall {
58        from: from.into(),
59        to: to.into(),
60        amount,
61    }
62    .abi_encode()
63    .into()
64}
65
66/// ABI-encode the `balanceOf(account)` constant call.
67pub fn encode_balance_of(account: Address) -> Bytes {
68    ITRC20::balanceOfCall {
69        account: account.into(),
70    }
71    .abi_encode()
72    .into()
73}
74
75/// ABI-encode the `allowance(owner, spender)` constant call.
76pub fn encode_allowance(owner: Address, spender: Address) -> Bytes {
77    ITRC20::allowanceCall {
78        owner: owner.into(),
79        spender: spender.into(),
80    }
81    .abi_encode()
82    .into()
83}
84
85/// Decode the `uint256` returned by `balanceOf` / `allowance` / `totalSupply`.
86pub fn decode_uint256_return(output: &[u8]) -> Result<U256, alloy_sol_types::Error> {
87    ITRC20::balanceOfCall::abi_decode_returns(output)
88}
89
90/// Decode the `uint8` returned by `decimals`.
91pub fn decode_decimals_return(output: &[u8]) -> Result<u8, alloy_sol_types::Error> {
92    ITRC20::decimalsCall::abi_decode_returns(output)
93}
94
95/// Decode the `string` returned by `name` / `symbol`.
96pub fn decode_string_return(output: &[u8]) -> Result<String, alloy_sol_types::Error> {
97    ITRC20::nameCall::abi_decode_returns(output)
98}
99
100/// The four-byte selector for a generated call type.
101pub fn selector<C: SolCall>() -> [u8; 4] {
102    C::SELECTOR
103}
104
105#[cfg(test)]
106mod tests {
107    use alloy_sol_types::SolValue;
108
109    use super::*;
110
111    const ADDR: &str = "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t";
112
113    fn addr() -> Address {
114        ADDR.parse().unwrap()
115    }
116
117    #[test]
118    fn transfer_selector_matches_erc20() {
119        // keccak256("transfer(address,uint256)")[..4] == 0xa9059cbb
120        let data = encode_transfer(addr(), U256::from(1u64));
121        assert_eq!(&data[..4], &[0xa9, 0x05, 0x9c, 0xbb]);
122        assert_eq!(ITRC20::transferCall::SELECTOR, [0xa9, 0x05, 0x9c, 0xbb]);
123    }
124
125    #[test]
126    fn balance_of_selector() {
127        // keccak256("balanceOf(address)")[..4] == 0x70a08231
128        let data = encode_balance_of(addr());
129        assert_eq!(&data[..4], &[0x70, 0xa0, 0x82, 0x31]);
130    }
131
132    #[test]
133    fn transfer_encodes_evm_address_not_tron() {
134        let data = encode_transfer(addr(), U256::from(0u64));
135        // The address argument occupies bytes [4..36]; the low 20 bytes must be
136        // the EVM body (0x41 prefix stripped), left-padded to 32 bytes.
137        let arg = &data[4..36];
138        assert_eq!(&arg[..12], &[0u8; 12], "address must be left-padded");
139        assert_eq!(&arg[12..], addr().as_evm_bytes());
140    }
141
142    #[test]
143    fn uint256_return_roundtrip() {
144        let value = U256::from(123_456_789u64);
145        let encoded = value.abi_encode();
146        assert_eq!(decode_uint256_return(&encoded).unwrap(), value);
147    }
148
149    #[test]
150    fn string_return_roundtrip() {
151        let encoded = "Tether USD".to_string().abi_encode();
152        assert_eq!(decode_string_return(&encoded).unwrap(), "Tether USD");
153    }
154
155    #[test]
156    fn decimals_return_roundtrip() {
157        // abi-encoded uint8 is a left-padded 32-byte word.
158        let mut encoded = [0u8; 32];
159        encoded[31] = 6;
160        assert_eq!(decode_decimals_return(&encoded).unwrap(), 6u8);
161    }
162}