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