rosetta_client/
wallet.rs

1use crate::crypto::address::Address;
2use crate::crypto::bip32::DerivedSecretKey;
3use crate::crypto::bip44::ChildNumber;
4use crate::crypto::SecretKey;
5use crate::signer::{RosettaAccount, RosettaPublicKey, Signer};
6use crate::types::{
7    AccountBalanceRequest, AccountCoinsRequest, AccountFaucetRequest, AccountIdentifier, Amount,
8    BlockIdentifier, BlockTransaction, Coin, ConstructionMetadataRequest,
9    ConstructionSubmitRequest, PublicKey, SearchTransactionsRequest, SearchTransactionsResponse,
10    TransactionIdentifier,
11};
12use crate::{BlockchainConfig, Client, TransactionBuilder};
13use anyhow::{Context as _, Result};
14use futures::{Future, Stream};
15use rosetta_core::types::{
16    Block, BlockRequest, BlockTransactionRequest, BlockTransactionResponse, CallRequest,
17    CallResponse, PartialBlockIdentifier,
18};
19use serde_json::{json, Value};
20use std::pin::Pin;
21use std::task::{Context, Poll};
22use surf::utils::async_trait;
23
24pub enum GenericTransactionBuilder {
25    Ethereum(rosetta_tx_ethereum::EthereumTransactionBuilder),
26    Polkadot(rosetta_tx_polkadot::PolkadotTransactionBuilder),
27}
28
29impl GenericTransactionBuilder {
30    pub fn new(config: &BlockchainConfig) -> Result<Self> {
31        Ok(match config.blockchain {
32            "astar" => Self::Ethereum(Default::default()),
33            "ethereum" => Self::Ethereum(Default::default()),
34            "polkadot" => Self::Polkadot(Default::default()),
35            _ => anyhow::bail!("unsupported blockchain"),
36        })
37    }
38
39    pub fn transfer(&self, address: &Address, amount: u128) -> Result<serde_json::Value> {
40        Ok(match self {
41            Self::Ethereum(tx) => serde_json::to_value(tx.transfer(address, amount)?)?,
42            Self::Polkadot(tx) => serde_json::to_value(tx.transfer(address, amount)?)?,
43        })
44    }
45
46    pub fn method_call(
47        &self,
48        contract: &str,
49        method: &str,
50        params: &[String],
51        amount: u128,
52    ) -> Result<serde_json::Value> {
53        Ok(match self {
54            Self::Ethereum(tx) => {
55                serde_json::to_value(tx.method_call(contract, method, params, amount)?)?
56            }
57            Self::Polkadot(tx) => {
58                serde_json::to_value(tx.method_call(contract, method, params, amount)?)?
59            }
60        })
61    }
62
63    pub fn deploy_contract(&self, contract_binary: Vec<u8>) -> Result<serde_json::Value> {
64        Ok(match self {
65            Self::Ethereum(tx) => serde_json::to_value(tx.deploy_contract(contract_binary)?)?,
66            Self::Polkadot(tx) => serde_json::to_value(tx.deploy_contract(contract_binary)?)?,
67        })
68    }
69
70    pub fn create_and_sign(
71        &self,
72        config: &BlockchainConfig,
73        metadata_params: serde_json::Value,
74        metadata: serde_json::Value,
75        secret_key: &SecretKey,
76    ) -> Vec<u8> {
77        match self {
78            Self::Ethereum(tx) => {
79                let metadata_params = serde_json::from_value(metadata_params).unwrap();
80                let metadata = serde_json::from_value(metadata).unwrap();
81                tx.create_and_sign(config, &metadata_params, &metadata, secret_key)
82            }
83            Self::Polkadot(tx) => {
84                let metadata_params = serde_json::from_value(metadata_params).unwrap();
85                let metadata = serde_json::from_value(metadata).unwrap();
86                tx.create_and_sign(config, &metadata_params, &metadata, secret_key)
87            }
88        }
89    }
90}
91
92/// The wallet provides the main entry point to this crate.
93pub struct Wallet {
94    config: BlockchainConfig,
95    client: Client,
96    account: AccountIdentifier,
97    secret_key: DerivedSecretKey,
98    public_key: PublicKey,
99    tx: GenericTransactionBuilder,
100}
101
102impl Wallet {
103    /// Creates a new wallet from a config, signer and client.
104    pub fn new(config: BlockchainConfig, signer: &Signer, client: Client) -> Result<Self> {
105        let tx = GenericTransactionBuilder::new(&config)?;
106        let secret_key = if config.bip44 {
107            signer
108                .bip44_account(config.algorithm, config.coin, 0)?
109                .derive(ChildNumber::non_hardened_from_u32(0))?
110        } else {
111            signer.master_key(config.algorithm)?.clone()
112        };
113        let public_key = secret_key.public_key();
114        let account = public_key.to_address(config.address_format).to_rosetta();
115        let public_key = public_key.to_rosetta();
116        Ok(Self {
117            config,
118            client,
119            account,
120            secret_key,
121            public_key,
122            tx,
123        })
124    }
125
126    /// Returns the blockchain config.
127    pub fn config(&self) -> &BlockchainConfig {
128        &self.config
129    }
130
131    /// Returns the rosetta client.
132    pub fn client(&self) -> &Client {
133        &self.client
134    }
135
136    /// Returns the public key.
137    pub fn public_key(&self) -> &PublicKey {
138        &self.public_key
139    }
140
141    /// Returns the account identifier.
142    pub fn account(&self) -> &AccountIdentifier {
143        &self.account
144    }
145
146    /// Returns the current block identifier.
147    pub async fn status(&self) -> Result<BlockIdentifier> {
148        let status = self.client.network_status(self.config.network()).await?;
149        Ok(status.current_block_identifier)
150    }
151
152    /// Returns the balance of the wallet.
153    pub async fn balance(&self) -> Result<Amount> {
154        let balance = self
155            .client
156            .account_balance(&AccountBalanceRequest {
157                network_identifier: self.config.network(),
158                account_identifier: self.account.clone(),
159                block_identifier: None,
160                currencies: Some(vec![self.config.currency()]),
161            })
162            .await?;
163        Ok(balance.balances[0].clone())
164    }
165
166    /// Returns block data
167    /// Takes PartialBlockIdentifier
168    pub async fn block(&self, data: PartialBlockIdentifier) -> Result<Block> {
169        let req = BlockRequest {
170            network_identifier: self.config.network(),
171            block_identifier: data,
172        };
173        let block = self.client.block(&req).await?;
174        block.block.context("block not found")
175    }
176
177    /// Returns transactions included in a block
178    /// Parameters:
179    /// 1. block_identifier: BlockIdentifier containing block number and hash
180    /// 2. tx_identifier: TransactionIdentifier containing hash of transaction
181    pub async fn block_transaction(
182        &self,
183        block_identifer: BlockIdentifier,
184        tx_identifier: TransactionIdentifier,
185    ) -> Result<BlockTransactionResponse> {
186        let req = BlockTransactionRequest {
187            network_identifier: self.config.network(),
188            block_identifier: block_identifer,
189            transaction_identifier: tx_identifier,
190        };
191        let block = self.client.block_transaction(&req).await?;
192        Ok(block)
193    }
194
195    /// Extension of rosetta-api does multiple things
196    /// 1. fetching storage
197    /// 2. calling extrinsic/contract
198    pub async fn call(&self, method: String, params: &serde_json::Value) -> Result<CallResponse> {
199        let req = CallRequest {
200            network_identifier: self.config.network(),
201            method,
202            parameters: params.clone(),
203        };
204        let response = self.client.call(&req).await?;
205        Ok(response)
206    }
207
208    /// Returns the coins of the wallet.
209    pub async fn coins(&self) -> Result<Vec<Coin>> {
210        let coins = self
211            .client
212            .account_coins(&AccountCoinsRequest {
213                network_identifier: self.config.network(),
214                account_identifier: self.account.clone(),
215                include_mempool: false,
216                currencies: Some(vec![self.config.currency()]),
217            })
218            .await?;
219        Ok(coins.coins)
220    }
221
222    /// Returns the on chain metadata.
223    /// Parameters:
224    /// - metadata_params: the metadata parameters which we got from transaction builder.
225    pub async fn metadata(&self, metadata_params: serde_json::Value) -> Result<serde_json::Value> {
226        let req = ConstructionMetadataRequest {
227            network_identifier: self.config.network(),
228            options: Some(metadata_params),
229            public_keys: vec![self.public_key.clone()],
230        };
231        let response = self.client.construction_metadata(&req).await?;
232        Ok(response.metadata)
233    }
234
235    /// Submits a transaction and returns the transaction identifier.
236    /// Parameters:
237    /// - transaction: the transaction bytes to submit
238    pub async fn submit(&self, transaction: &[u8]) -> Result<TransactionIdentifier> {
239        let req = ConstructionSubmitRequest {
240            network_identifier: self.config.network(),
241            signed_transaction: hex::encode(transaction),
242        };
243        let submit = self.client.construction_submit(&req).await?;
244        Ok(submit.transaction_identifier)
245    }
246
247    /// Creates, signs and submits a transaction.
248    pub async fn construct(&self, metadata_params: Value) -> Result<TransactionIdentifier> {
249        let metadata = self.metadata(metadata_params.clone()).await?;
250        let transaction = self.tx.create_and_sign(
251            &self.config,
252            metadata_params,
253            metadata,
254            self.secret_key.secret_key(),
255        );
256        self.submit(&transaction).await
257    }
258
259    /// Makes a transfer.
260    /// Parameters:
261    /// - account: the account to transfer to
262    /// - amount: the amount to transfer
263    pub async fn transfer(
264        &self,
265        account: &AccountIdentifier,
266        amount: u128,
267    ) -> Result<TransactionIdentifier> {
268        let address = Address::new(self.config.address_format, account.address.clone());
269        let metadata_params = self.tx.transfer(&address, amount)?;
270        self.construct(metadata_params).await
271    }
272
273    /// Uses the faucet on dev chains to seed the account with funds.
274    /// Parameters:
275    /// - faucet_parameter: the amount to seed the account with
276    pub async fn faucet(&self, faucet_parameter: u128) -> Result<TransactionIdentifier> {
277        let req = AccountFaucetRequest {
278            network_identifier: self.config.network(),
279            account_identifier: self.account.clone(),
280            faucet_parameter,
281        };
282        let resp = self.client.account_faucet(&req).await?;
283        Ok(resp.transaction_identifier)
284    }
285
286    /// Returns the transaction matching the transaction identifier.
287    /// Parameters:
288    /// - tx: the transaction identifier to search for.
289    pub async fn transaction(&self, tx: TransactionIdentifier) -> Result<BlockTransaction> {
290        let req = SearchTransactionsRequest {
291            network_identifier: self.config().network(),
292            operator: None,
293            max_block: None,
294            offset: None,
295            limit: None,
296            transaction_identifier: Some(tx),
297            account_identifier: None,
298            coin_identifier: None,
299            currency: None,
300            status: None,
301            r#type: None,
302            address: None,
303            success: None,
304        };
305        let resp = self.client.search_transactions(&req).await?;
306        anyhow::ensure!(resp.transactions.len() == 1);
307        Ok(resp.transactions[0].clone())
308    }
309
310    /// Returns a stream of transactions associated with the account.
311    pub fn transactions(&self, limit: u16) -> TransactionStream {
312        let req = SearchTransactionsRequest {
313            network_identifier: self.config().network(),
314            operator: None,
315            max_block: None,
316            offset: None,
317            limit: Some(limit as i64),
318            transaction_identifier: None,
319            account_identifier: Some(self.account.clone()),
320            coin_identifier: None,
321            currency: None,
322            status: None,
323            r#type: None,
324            address: None,
325            success: None,
326        };
327        TransactionStream::new(self.client.clone(), req)
328    }
329}
330
331/// Extension trait for the wallet. for ethereum chain
332#[async_trait]
333pub trait EthereumExt {
334    /// deploys contract to chain
335    async fn eth_deploy_contract(&self, bytecode: Vec<u8>) -> Result<TransactionIdentifier>;
336    /// calls a contract view call function
337    async fn eth_view_call(
338        &self,
339        contract_address: &str,
340        method_signature: &str,
341        params: &[String],
342    ) -> Result<CallResponse>;
343    /// calls contract send call function
344    async fn eth_send_call(
345        &self,
346        contract_address: &str,
347        method_signature: &str,
348        params: &[String],
349        amount: u128,
350    ) -> Result<TransactionIdentifier>;
351    /// estimates gas of send call
352    async fn eth_send_call_estimate_gas(
353        &self,
354        contract_address: &str,
355        method_signature: &str,
356        params: &[String],
357        amount: u128,
358    ) -> Result<u128>;
359    /// gets storage from ethereum contract
360    async fn eth_storage(&self, contract_address: &str, storage_slot: &str)
361        -> Result<CallResponse>;
362    /// gets storage proof from ethereum contract
363    async fn eth_storage_proof(
364        &self,
365        contract_address: &str,
366        storage_slot: &str,
367    ) -> Result<CallResponse>;
368    /// gets transaction receipt of specific hash
369    async fn eth_transaction_receipt(&self, tx_hash: &str) -> Result<CallResponse>;
370}
371
372#[async_trait]
373impl EthereumExt for Wallet {
374    async fn eth_deploy_contract(&self, bytecode: Vec<u8>) -> Result<TransactionIdentifier> {
375        let metadata_params = self.tx.deploy_contract(bytecode)?;
376        self.construct(metadata_params).await
377    }
378
379    async fn eth_send_call(
380        &self,
381        contract_address: &str,
382        method_signature: &str,
383        params: &[String],
384        amount: u128,
385    ) -> Result<TransactionIdentifier> {
386        let metadata_params =
387            self.tx
388                .method_call(contract_address, method_signature, params, amount)?;
389        self.construct(metadata_params).await
390    }
391
392    async fn eth_send_call_estimate_gas(
393        &self,
394        contract_address: &str,
395        method_signature: &str,
396        params: &[String],
397        amount: u128,
398    ) -> Result<u128> {
399        let metadata_params =
400            self.tx
401                .method_call(contract_address, method_signature, params, amount)?;
402        let metadata = self.metadata(metadata_params).await?;
403        let metadata: rosetta_config_ethereum::EthereumMetadata = serde_json::from_value(metadata)?;
404        Ok(rosetta_tx_ethereum::U256(metadata.gas_limit).as_u128())
405    }
406
407    async fn eth_view_call(
408        &self,
409        contract_address: &str,
410        method_signature: &str,
411        params: &[String],
412    ) -> Result<CallResponse> {
413        let method = format!("{}-{}-call", contract_address, method_signature);
414        self.call(method, &json!(params)).await
415    }
416
417    async fn eth_storage(
418        &self,
419        contract_address: &str,
420        storage_slot: &str,
421    ) -> Result<CallResponse> {
422        let method = format!("{}-{}-storage", contract_address, storage_slot);
423        self.call(method, &json!({})).await
424    }
425
426    async fn eth_storage_proof(
427        &self,
428        contract_address: &str,
429        storage_slot: &str,
430    ) -> Result<CallResponse> {
431        let method = format!("{}-{}-storage_proof", contract_address, storage_slot);
432        self.call(method, &json!({})).await
433    }
434
435    async fn eth_transaction_receipt(&self, tx_hash: &str) -> Result<CallResponse> {
436        let call_method = format!("{}--transaction_receipt", tx_hash);
437        self.call(call_method, &json!({})).await
438    }
439}
440
441/// A paged transaction stream.
442pub struct TransactionStream {
443    client: Client,
444    request: SearchTransactionsRequest,
445    future: Option<Pin<Box<dyn Future<Output = Result<SearchTransactionsResponse>> + 'static>>>,
446    finished: bool,
447    total_count: Option<i64>,
448}
449
450impl TransactionStream {
451    fn new(client: Client, mut request: SearchTransactionsRequest) -> Self {
452        request.offset = Some(0);
453        Self {
454            client,
455            request,
456            future: None,
457            finished: false,
458            total_count: None,
459        }
460    }
461
462    /// Returns the total number of transactions.
463    pub fn total_count(&self) -> Option<i64> {
464        self.total_count
465    }
466}
467
468impl Stream for TransactionStream {
469    type Item = Result<Vec<BlockTransaction>>;
470
471    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll<Option<Self::Item>> {
472        loop {
473            if self.finished {
474                return Poll::Ready(None);
475            } else if let Some(future) = self.future.as_mut() {
476                futures::pin_mut!(future);
477                match future.poll(cx) {
478                    Poll::Pending => return Poll::Pending,
479                    Poll::Ready(Ok(response)) => {
480                        self.future.take();
481                        self.request.offset = response.next_offset;
482                        self.total_count = Some(response.total_count);
483                        if response.transactions.len() < self.request.limit.unwrap() as _ {
484                            self.finished = true;
485                        }
486                        if response.transactions.is_empty() {
487                            continue;
488                        }
489                        return Poll::Ready(Some(Ok(response.transactions)));
490                    }
491                    Poll::Ready(Err(error)) => {
492                        self.future.take();
493                        return Poll::Ready(Some(Err(error)));
494                    }
495                };
496            } else {
497                let client = self.client.clone();
498                let request = self.request.clone();
499                self.future = Some(Box::pin(async move {
500                    client.search_transactions(&request).await
501                }));
502            }
503        }
504    }
505}