Skip to main content

nautilus_blockchain/execution/
client.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{collections::HashSet, sync::Arc};
17
18use alloy::primitives::Address;
19use async_trait::async_trait;
20use nautilus_common::{
21    clients::ExecutionClient,
22    messages::execution::{
23        BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
24        GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
25        ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
26    },
27};
28use nautilus_core::UnixNanos;
29use nautilus_live::ExecutionClientCore;
30use nautilus_model::{
31    accounts::AccountAny,
32    defi::{
33        SharedChain, Token,
34        validation::validate_address,
35        wallet::{TokenBalance, WalletBalance},
36    },
37    enums::OmsType,
38    identifiers::{AccountId, ClientId, Venue},
39    reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
40    types::{AccountBalance, MarginBalance, Money},
41};
42
43use crate::{
44    cache::BlockchainCache, config::BlockchainExecutionClientConfig,
45    contracts::erc20::Erc20Contract, rpc::http::BlockchainHttpRpcClient,
46};
47
48/// Execution client for blockchain interactions including balance tracking and order execution.
49#[derive(Debug)]
50pub struct BlockchainExecutionClient {
51    /// Core execution client providing base functionality.
52    core: ExecutionClientCore,
53    /// Cache for storing token metadata and other blockchain data.
54    cache: BlockchainCache,
55    /// The blockchain network configuration.
56    chain: SharedChain,
57    /// The wallet address used for transactions and balance queries.
58    wallet_address: Address,
59    /// Tracks native currency and ERC-20 token balances.
60    wallet_balance: WalletBalance,
61    /// Contract interface for ERC-20 token interactions.
62    erc20_contract: Erc20Contract,
63    /// HTTP RPC client for blockchain queries.
64    http_rpc_client: Arc<BlockchainHttpRpcClient>,
65}
66
67impl BlockchainExecutionClient {
68    /// Creates a new [`BlockchainExecutionClient`] instance for the specified configuration.
69    ///
70    /// # Errors
71    ///
72    /// Returns an error if the wallet address or any token address in the config is invalid.
73    pub fn new(
74        core_client: ExecutionClientCore,
75        config: BlockchainExecutionClientConfig,
76    ) -> anyhow::Result<Self> {
77        let chain = Arc::new(config.chain);
78        let cache = BlockchainCache::new(chain.clone());
79        let http_rpc_client = Arc::new(BlockchainHttpRpcClient::new(
80            config.http_rpc_url.clone(),
81            config.rpc_requests_per_second,
82        ));
83        let wallet_address = validate_address(config.wallet_address.as_str())?;
84        let erc20_contract = Erc20Contract::new(http_rpc_client.clone(), true);
85
86        // Initialize token universe, so we can fetch them from the blockchain later.
87        let mut token_universe = HashSet::new();
88        if let Some(specified_tokens) = config.tokens {
89            for token in specified_tokens {
90                let token_address = validate_address(token.as_str())?;
91                token_universe.insert(token_address);
92            }
93        }
94        let wallet_balance = WalletBalance::new(token_universe);
95
96        Ok(Self {
97            core: core_client,
98            wallet_balance,
99            chain,
100            cache,
101            erc20_contract,
102            http_rpc_client,
103            wallet_address,
104        })
105    }
106
107    /// Fetches the native currency balance (e.g., ETH) for the wallet from the blockchain.
108    async fn fetch_native_currency_balance(&self) -> anyhow::Result<Money> {
109        let balance_u256 = self
110            .http_rpc_client
111            .get_balance(&self.wallet_address, None)
112            .await?;
113
114        let native_currency = self.chain.native_currency();
115
116        // Convert from wei (18 decimals on-chain) to Money
117        let balance = Money::from_wei(balance_u256, native_currency);
118
119        Ok(balance)
120    }
121
122    /// Fetches the balance of a specific ERC-20 token for the wallet.
123    async fn fetch_token_balance(
124        &mut self,
125        token_address: &Address,
126    ) -> anyhow::Result<TokenBalance> {
127        // Get the cached token or fetch it from the blockchain and cache it.
128        let token = if let Some(token) = self.cache.get_token(token_address) {
129            token.to_owned()
130        } else {
131            let token_info = self.erc20_contract.fetch_token_info(token_address).await?;
132            let token = Token::new(
133                self.chain.clone(),
134                *token_address,
135                token_info.name,
136                token_info.symbol,
137                token_info.decimals,
138            );
139            self.cache.add_token(token.clone()).await?;
140            token
141        };
142
143        let amount = self
144            .erc20_contract
145            .balance_of(token_address, &self.wallet_address)
146            .await?;
147        let token_balance = TokenBalance::new(amount, token);
148
149        // TODO: Use price oracle here and cache, to get the latest price then convert to USD
150        // then use token_balance.set_amount_usd(amount_usd) to set the amount_usd value.
151
152        Ok(token_balance)
153    }
154
155    /// Refreshes all wallet balances including native currency and tracked ERC-20 tokens.
156    async fn refresh_wallet_balances(&mut self) -> anyhow::Result<()> {
157        let native_currency_balance = self.fetch_native_currency_balance().await?;
158        log::info!(
159            "Initializing wallet balance with native currency balance: {} {}",
160            native_currency_balance.as_decimal(),
161            native_currency_balance.currency
162        );
163        self.wallet_balance
164            .set_native_currency_balance(native_currency_balance);
165
166        // Fetch token balances from the blockchain.
167        if self.wallet_balance.is_token_universe_initialized() {
168            let tokens: Vec<Address> = self
169                .wallet_balance
170                .token_universe
171                .clone()
172                .into_iter()
173                .collect();
174            for token in tokens {
175                if let Ok(token_balance) = self.fetch_token_balance(&token).await {
176                    log::info!("Adding token balance to the wallet: {token_balance}");
177                    self.wallet_balance.add_token_balance(token_balance);
178                }
179            }
180        } else {
181            // TODO sync from transfer events for tokens that wallet interacted with.
182        }
183
184        Ok(())
185    }
186}
187
188#[async_trait(?Send)]
189impl ExecutionClient for BlockchainExecutionClient {
190    fn is_connected(&self) -> bool {
191        self.core.is_connected()
192    }
193
194    fn client_id(&self) -> ClientId {
195        self.core.client_id
196    }
197
198    fn account_id(&self) -> AccountId {
199        self.core.account_id
200    }
201
202    fn venue(&self) -> Venue {
203        self.core.venue
204    }
205
206    fn oms_type(&self) -> OmsType {
207        self.core.oms_type
208    }
209
210    fn get_account(&self) -> Option<AccountAny> {
211        todo!("implement get_account")
212    }
213
214    fn generate_account_state(
215        &self,
216        _balances: Vec<AccountBalance>,
217        _margins: Vec<MarginBalance>,
218        _reported: bool,
219        _ts_event: UnixNanos,
220    ) -> anyhow::Result<()> {
221        todo!("implement generate_account_state")
222    }
223
224    fn start(&mut self) -> anyhow::Result<()> {
225        todo!("implement start")
226    }
227
228    fn stop(&mut self) -> anyhow::Result<()> {
229        todo!("implement stop")
230    }
231
232    fn submit_order(&self, _cmd: &SubmitOrder) -> anyhow::Result<()> {
233        todo!("implement submit_order")
234    }
235
236    fn submit_order_list(&self, _cmd: &SubmitOrderList) -> anyhow::Result<()> {
237        todo!("implement submit_order_list")
238    }
239
240    fn modify_order(&self, _cmd: &ModifyOrder) -> anyhow::Result<()> {
241        todo!("implement modify_order")
242    }
243
244    fn cancel_order(&self, _cmd: &CancelOrder) -> anyhow::Result<()> {
245        todo!("implement cancel_order")
246    }
247
248    fn cancel_all_orders(&self, _cmd: &CancelAllOrders) -> anyhow::Result<()> {
249        todo!("implement cancel_all_orders")
250    }
251
252    fn batch_cancel_orders(&self, _cmd: &BatchCancelOrders) -> anyhow::Result<()> {
253        todo!("implement batch_cancel_orders")
254    }
255
256    fn query_account(&self, _cmd: &QueryAccount) -> anyhow::Result<()> {
257        todo!("implement query_account")
258    }
259
260    fn query_order(&self, _cmd: &QueryOrder) -> anyhow::Result<()> {
261        todo!("implement query_order")
262    }
263
264    async fn connect(&mut self) -> anyhow::Result<()> {
265        if self.core.is_connected() {
266            log::warn!("Blockchain execution client already connected");
267            return Ok(());
268        }
269
270        log::info!(
271            "Connecting to blockchain execution client on chain {}",
272            self.chain.name
273        );
274
275        self.refresh_wallet_balances().await?;
276
277        self.core.set_connected();
278        log::info!(
279            "Blockchain execution client connected on chain {}",
280            self.chain.name
281        );
282        Ok(())
283    }
284
285    async fn disconnect(&mut self) -> anyhow::Result<()> {
286        self.core.set_disconnected();
287        Ok(())
288    }
289
290    async fn generate_order_status_report(
291        &self,
292        _cmd: &GenerateOrderStatusReport,
293    ) -> anyhow::Result<Option<OrderStatusReport>> {
294        todo!("implement generate_order_status_report")
295    }
296
297    async fn generate_order_status_reports(
298        &self,
299        _cmd: &GenerateOrderStatusReports,
300    ) -> anyhow::Result<Vec<OrderStatusReport>> {
301        todo!("implement generate_order_status_reports")
302    }
303
304    async fn generate_fill_reports(
305        &self,
306        _cmd: GenerateFillReports,
307    ) -> anyhow::Result<Vec<FillReport>> {
308        todo!("implement generate_fill_reports")
309    }
310
311    async fn generate_position_status_reports(
312        &self,
313        _cmd: &GeneratePositionStatusReports,
314    ) -> anyhow::Result<Vec<PositionStatusReport>> {
315        todo!("implement generate_position_status_reports")
316    }
317
318    async fn generate_mass_status(
319        &self,
320        _lookback_mins: Option<u64>,
321    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
322        todo!("implement generate_mass_status")
323    }
324}