cpchain_rust_sdk/
cpc_web3.rs

1use std::time::{Duration, Instant};
2
3use web3::{
4    types::{Block, BlockId, SignedTransaction, Transaction, TransactionReceipt, H160, H256, U256, BlockNumber},
5    Error, Web3,
6};
7
8use crate::{
9    accounts::Account, address::Address, transport::CPCHttp, types::TransactionParameters,
10};
11
12#[derive(Debug, Clone)]
13pub struct CPCWeb3 {
14    pub web3: Web3<CPCHttp>,
15}
16
17impl CPCWeb3 {
18    pub fn new(url: &str) -> Result<Self, Error> {
19        let transport = CPCHttp::new(url)?;
20        let web3 = web3::Web3::new(transport);
21        Ok(Self { web3 })
22    }
23    pub async fn block_number(&self) -> Result<u64, Error> {
24        let current_block = self.web3.eth().block_number().await?;
25        Ok(current_block.as_u64())
26    }
27
28    pub async fn block(&self, number: Option<u32>) -> Result<Option<Block<H256>>, Error> {
29        let number = match number {
30            Some(n) => n.into(),
31            None => BlockNumber::Latest,
32        };
33        self.web3.eth().block(BlockId::Number(number)).await
34    }
35
36    pub async fn block_with_txs(&self, number: Option<u32>) -> Result<Option<Block<Transaction>>, Error> {
37        let number = match number {
38            Some(n) => n.into(),
39            None => BlockNumber::Latest,
40        };
41        self.web3
42            .eth()
43            .block_with_txs(BlockId::Number(number))
44            .await
45    }
46
47    pub async fn balance(&self, address: &Address) -> Result<U256, Error> {
48        let balance = self.web3.eth().balance(address.h160, None).await?;
49        Ok(balance)
50    }
51
52    pub async fn sign_transaction(
53        &self,
54        account: &Account,
55        tx: &TransactionParameters,
56    ) -> Result<SignedTransaction, Error> {
57        let signed = tx.sign(&account.secret_key);
58        Ok(signed)
59    }
60
61    pub async fn gas_price(&self) -> Result<U256, Error> {
62        Ok(self.web3.eth().gas_price().await?)
63    }
64
65    pub async fn transaction_count(&self, address: &Address) -> Result<U256, Error> {
66        self.web3.eth().transaction_count(address.h160, None).await
67    }
68
69    pub async fn submit_signed_raw_tx(&self, signed: &SignedTransaction) -> Result<H256, Error> {
70        self.web3
71            .eth()
72            .send_raw_transaction(signed.raw_transaction.clone())
73            .await
74    }
75
76    pub async fn wait_tx(
77        &self,
78        tx_hash: &H256,
79    ) -> Result<TransactionReceipt, Box<dyn std::error::Error>> {
80        let start = Instant::now();
81        let timeout = Duration::from_secs(20);
82        loop {
83            let receipt = self.web3.eth().transaction_receipt(*tx_hash).await?;
84            if receipt.is_some() {
85                return Ok(receipt.unwrap());
86            }
87            if start.elapsed() >= timeout {
88                return Err("Waiting for transaction receipt timed out".into());
89            }
90        }
91    }
92
93    pub async fn estimate_gas(&self, req: &TransactionParameters) -> Result<U256, Error> {
94        self.web3
95            .eth()
96            .estimate_gas(req.to_call_request(), None)
97            .await
98    }
99
100    pub async fn transaction_receipt(
101        &self,
102        tx_hash: &H256,
103    ) -> Result<Option<TransactionReceipt>, Error> {
104        self.web3.eth().transaction_receipt(*tx_hash).await
105    }
106
107    pub async fn is_contract(&self, addr: H160) -> Result<bool, Error> {
108        let code = self.web3.eth().code(addr, None).await?;
109        Ok(code.0.len() > 0)
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use crate::types::Bytes;
116
117    use super::*;
118
119    #[tokio::test]
120    async fn test_block_number() {
121        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
122        let number = web3.block_number().await.unwrap();
123        println!("{:?}", number);
124        assert!(number > 0);
125    }
126
127    #[tokio::test]
128    async fn test_get_block_with_txs() {
129        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
130        let block = web3.block_with_txs(Some(100)).await.unwrap();
131        assert!(block.is_some());
132        let block = block.unwrap();
133        assert!(block.number.unwrap().as_u32() == 100);
134        assert!(block.transactions.len() == 0);
135        assert!(block.size.unwrap().as_u32() == 1263);
136        assert!(block.hash.unwrap().to_string().to_lowercase() == "0x1b91…4aef");
137        let block = web3.block_with_txs(Some(10504047)).await.unwrap();
138        assert!(block.is_some());
139        let block = block.unwrap();
140        assert!(block.number.unwrap().as_u32() == 10504047);
141        assert!(block.transactions.len() == 24);
142        assert!(block.size.unwrap().as_u32() == 8619);
143        assert!(block.hash.unwrap().to_string().to_lowercase() == "0x37e1…0e7b");
144        // Check transaction
145        let tx = &block.transactions[0];
146        assert!(tx.hash.to_string() == "0x89cd…6ada");
147        assert!(tx.from.unwrap().to_string() == "0x5e17…b6f0");
148        assert!(tx.to.unwrap().to_string() == "0x2a18…fcd0");
149        assert!(tx.value.to_string() == "0");
150        assert!(tx.gas.to_string() == "2300000");
151        assert!(tx.gas_price.unwrap().to_string() == "18000000000");
152        let tx = &block.transactions.last().unwrap();
153        assert!(tx.hash.to_string() == "0xc5b0…5e62");
154
155        // Get unexists block
156        let block = web3.block_with_txs(Some(100000000)).await.unwrap();
157        assert!(block.is_none());
158    }
159
160    #[tokio::test]
161    async fn test_get_block() {
162        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
163        let block = web3.block(Some(100)).await.unwrap().unwrap();
164        assert!(block.number.unwrap().as_u32() == 100);
165    }
166
167    #[tokio::test]
168    async fn test_get_latest_block() {
169        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
170        let block = web3.block(None).await.unwrap().unwrap();
171        assert!(block.number.unwrap().as_u32() > 100);
172    }
173
174    #[tokio::test]
175    async fn test_get_balance() {
176        let web3 = CPCWeb3::new("http://192.168.0.164:8501").unwrap();
177        let balance = web3
178            .balance(&Address::from_str("0x7D491C482eBa270700b584888f864177205c5159").unwrap())
179            .await
180            .unwrap();
181        println!("{:?}", balance.as_u128());
182    }
183
184    #[tokio::test]
185    async fn test_gas_price() {
186        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
187        let gas_price = web3.gas_price().await.unwrap();
188        assert_eq!(gas_price, U256::from(18) * U256::exp10(9))
189    }
190
191    #[tokio::test]
192    async fn test_get_transactions_cnt() {
193        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
194        let cnt = web3
195            .transaction_count(
196                &Address::from_str("0x1455D180E3adE94ebD9cC324D22a9065d1F5F575").unwrap(),
197            )
198            .await
199            .unwrap();
200        assert!(cnt >= U256::from(1065));
201        let cnt = web3
202            .transaction_count(
203                &Address::from_str("0x2455D180E3adE94ebD9cC324D22a9065d1F5F575").unwrap(),
204            )
205            .await
206            .unwrap();
207        assert!(cnt == U256::from(0))
208    }
209
210    #[tokio::test]
211    async fn test_sign_transaction() {
212        let web3 = CPCWeb3::new("http://192.168.0.164:8501").unwrap();
213        let account = Account::from_phrase(
214            "length much pull abstract almost spin hair chest ankle harbor dizzy life",
215            None,
216        )
217        .unwrap();
218        println!("{}", account.address);
219        let gas_price = web3.gas_price().await.unwrap();
220        let nonce = web3.transaction_count(&account.address).await.unwrap();
221        let tx_object = TransactionParameters::new(
222            41,
223            nonce,
224            Some(Address::from_str("0x1455D180E3adE94ebD9cC324D22a9065d1F5F575").unwrap().h160),
225            300000.into(),
226            gas_price,
227            U256::exp10(17), //0.1 cpc
228            Bytes::default(),
229        );
230
231        let estimated_gas = web3.estimate_gas(&tx_object).await.unwrap();
232        assert_eq!(estimated_gas, U256::from(21000));
233
234        let signed = web3.sign_transaction(&account, &tx_object).await.unwrap();
235        let tx_hash = web3.submit_signed_raw_tx(&signed).await.unwrap();
236        println!("{:?} {:?}", tx_hash, signed.transaction_hash);
237        assert_eq!(tx_hash, signed.transaction_hash);
238        // wait for transaction
239        let receipt = web3.wait_tx(&tx_hash).await.unwrap();
240        println!("{:?}", receipt);
241    }
242
243    #[tokio::test]
244    async fn test_is_contract() {
245        let web3 = CPCWeb3::new("https://civilian.cpchain.io").unwrap();
246        assert_eq!(
247            web3.is_contract(
248                Address::from_str("0xcf3174cd4dc7c4834d8932f7c3800ab98afc437a")
249                    .unwrap()
250                    .h160
251            )
252            .await
253            .unwrap(),
254            true
255        );
256        assert_eq!(
257            web3.is_contract(
258                Address::from_str("0x1455D180E3adE94ebD9cC324D22a9065d1F5F575")
259                    .unwrap()
260                    .h160
261            )
262            .await
263            .unwrap(),
264            false
265        );
266    }
267}