kinode_process_lib 1.0.1

A library for writing Kinode processes in Rust.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
use crate::{Message, Request as KiRequest};
pub use alloy::rpc::json_rpc::ErrorPayload;
pub use alloy::rpc::types::eth::pubsub::SubscriptionResult;
pub use alloy::rpc::types::pubsub::Params;
pub use alloy::rpc::types::{
    request::{TransactionInput, TransactionRequest},
    Block, BlockId, BlockNumberOrTag, FeeHistory, Filter, FilterBlockOption, Log, Transaction,
    TransactionReceipt,
};
pub use alloy::transports::Authorization as AlloyAuthorization;
pub use alloy_primitives::{Address, BlockHash, BlockNumber, Bytes, TxHash, U128, U256, U64, U8};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::error::Error;
use std::fmt;

/// Subscription kind. Pulled directly from alloy (https://github.com/alloy-rs/alloy).
/// Why? Because alloy is not yet 1.0 and the types in this interface must be stable.
/// If alloy SubscriptionKind changes, we can implement a transition function in runtime
/// for this type.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
#[serde(rename_all = "camelCase")]
pub enum SubscriptionKind {
    /// New block headers subscription.
    ///
    /// Fires a notification each time a new header is appended to the chain, including chain
    /// reorganizations. In case of a chain reorganization the subscription will emit all new
    /// headers for the new chain. Therefore the subscription can emit multiple headers on the same
    /// height.
    NewHeads,
    /// Logs subscription.
    ///
    /// Returns logs that are included in new imported blocks and match the given filter criteria.
    /// In case of a chain reorganization previous sent logs that are on the old chain will be
    /// resent with the removed property set to true. Logs from transactions that ended up in the
    /// new chain are emitted. Therefore, a subscription can emit logs for the same transaction
    /// multiple times.
    Logs,
    /// New Pending Transactions subscription.
    ///
    /// Returns the hash or full tx for all transactions that are added to the pending state and
    /// are signed with a key that is available in the node. When a transaction that was
    /// previously part of the canonical chain isn't part of the new canonical chain after a
    /// reorganization its again emitted.
    NewPendingTransactions,
    /// Node syncing status subscription.
    ///
    /// Indicates when the node starts or stops synchronizing. The result can either be a boolean
    /// indicating that the synchronization has started (true), finished (false) or an object with
    /// various progress indicators.
    Syncing,
}

/// The Action and Request type that can be made to eth:distro:sys. Any process with messaging
/// capabilities can send this action to the eth provider.
///
/// Will be serialized and deserialized using `serde_json::to_vec` and `serde_json::from_slice`.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EthAction {
    /// Subscribe to logs with a custom filter. ID is to be used to unsubscribe.
    /// Logs come in as JSON value which can be parsed to [`alloy::rpc::types::eth::pubsub::SubscriptionResult`]
    SubscribeLogs {
        sub_id: u64,
        chain_id: u64,
        kind: SubscriptionKind,
        params: serde_json::Value,
    },
    /// Kill a SubscribeLogs subscription of a given ID, to stop getting updates.
    UnsubscribeLogs(u64),
    /// Raw request. Used by kinode_process_lib.
    Request {
        chain_id: u64,
        method: String,
        params: serde_json::Value,
    },
}

/// Incoming [`crate::Request`] containing subscription updates or errors that processes will receive.
/// Can deserialize all incoming requests from eth:distro:sys to this type.
///
/// Will be serialized and deserialized using `serde_json::to_vec` and `serde_json::from_slice`.
pub type EthSubResult = Result<EthSub, EthSubError>;

/// Incoming type for successful subscription updates.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EthSub {
    pub id: u64,
    /// can be parsed to [`alloy::rpc::types::eth::pubsub::SubscriptionResult`]
    pub result: serde_json::Value,
}

/// If your subscription is closed unexpectedly, you will receive this.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct EthSubError {
    pub id: u64,
    pub error: String,
}

/// The [`crate::Response`] body type which a process will get from requesting
/// with an [`EthAction`] will be of this type, serialized and deserialized
/// using [`serde_json::to_vec`] and [`serde_json::from_slice`].
///
/// In the case of an [`EthAction::SubscribeLogs`] request, the response will indicate if
/// the subscription was successfully created or not.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EthResponse {
    Ok,
    Response(serde_json::Value),
    Err(EthError),
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EthError {
    /// RPC provider returned an error.
    /// Can be parsed to [`alloy::rpc::json_rpc::ErrorPayload`]
    RpcError(serde_json::Value),
    /// provider module cannot parse message
    MalformedRequest,
    /// No RPC provider for the chain
    NoRpcForChain,
    /// Subscription closed
    SubscriptionClosed(u64),
    /// Invalid method
    InvalidMethod(String),
    /// Invalid parameters
    InvalidParams,
    /// Permission denied
    PermissionDenied,
    /// RPC timed out
    RpcTimeout,
    /// RPC gave garbage back
    RpcMalformedResponse,
}

impl fmt::Display for EthError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            EthError::RpcError(e) => write!(f, "RPC error: {:?}", e),
            EthError::MalformedRequest => write!(f, "Malformed request"),
            EthError::NoRpcForChain => write!(f, "No RPC provider for chain"),
            EthError::SubscriptionClosed(id) => write!(f, "Subscription {} closed", id),
            EthError::InvalidMethod(m) => write!(f, "Invalid method: {}", m),
            EthError::InvalidParams => write!(f, "Invalid parameters"),
            EthError::PermissionDenied => write!(f, "Permission denied"),
            EthError::RpcTimeout => write!(f, "RPC request timed out"),
            EthError::RpcMalformedResponse => write!(f, "RPC returned malformed response"),
        }
    }
}

impl Error for EthError {}

/// The action type used for configuring eth:distro:sys. Only processes which have the "root"
/// [`crate::Capability`] from eth:distro:sys can successfully send this action.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EthConfigAction {
    /// Add a new provider to the list of providers.
    AddProvider(ProviderConfig),
    /// Remove a provider from the list of providers.
    /// The tuple is (chain_id, node_id/rpc_url).
    RemoveProvider((u64, String)),
    /// make our provider public
    SetPublic,
    /// make our provider not-public
    SetPrivate,
    /// add node to whitelist on a provider
    AllowNode(String),
    /// remove node from whitelist on a provider
    UnallowNode(String),
    /// add node to blacklist on a provider
    DenyNode(String),
    /// remove node from blacklist on a provider
    UndenyNode(String),
    /// Set the list of providers to a new list.
    /// Replaces all existing saved provider configs.
    SetProviders(SavedConfigs),
    /// Get the list of current providers as a [`SavedConfigs`] object.
    GetProviders,
    /// Get the current access settings.
    GetAccessSettings,
    /// Get the state of calls and subscriptions. Used for debugging.
    GetState,
}

/// Response type from an [`EthConfigAction`] request.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum EthConfigResponse {
    Ok,
    /// Response from a GetProviders request.
    /// Note the [`crate::net::KnsUpdate`] will only have the correct `name` field.
    /// The rest of the Update is not saved in this module.
    Providers(SavedConfigs),
    /// Response from a GetAccessSettings request.
    AccessSettings(AccessSettings),
    /// Permission denied due to missing [`crate::Capability`]
    PermissionDenied,
    /// Response from a GetState request
    State {
        active_subscriptions: HashMap<crate::Address, HashMap<u64, Option<String>>>, // None if local, Some(node_provider_name) if remote
        outstanding_requests: HashSet<u64>,
    },
}

/// Settings for our ETH provider
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccessSettings {
    pub public: bool,           // whether or not other nodes can access through us
    pub allow: HashSet<String>, // whitelist for access (only used if public == false)
    pub deny: HashSet<String>,  // blacklist for access (always used)
}

pub type SavedConfigs = HashSet<ProviderConfig>;

/// Provider config. Can currently be a node or a ws provider instance.
#[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)]
pub struct ProviderConfig {
    pub chain_id: u64,
    pub trusted: bool,
    pub provider: NodeOrRpcUrl,
}

#[derive(Clone, Debug, Deserialize, Serialize, Hash, Eq, PartialEq)]
pub enum Authorization {
    Basic(String),
    Bearer(String),
    Raw(String),
}

impl From<Authorization> for AlloyAuthorization {
    fn from(auth: Authorization) -> AlloyAuthorization {
        match auth {
            Authorization::Basic(value) => AlloyAuthorization::Basic(value),
            Authorization::Bearer(value) => AlloyAuthorization::Bearer(value),
            Authorization::Raw(value) => AlloyAuthorization::Raw(value),
        }
    }
}

#[derive(Clone, Debug, Serialize, Hash, Eq, PartialEq)]
pub enum NodeOrRpcUrl {
    Node {
        kns_update: crate::net::KnsUpdate,
        use_as_provider: bool, // false for just-routers inside saved config
    },
    RpcUrl {
        url: String,
        auth: Option<Authorization>,
    },
}

impl std::cmp::PartialEq<str> for NodeOrRpcUrl {
    fn eq(&self, other: &str) -> bool {
        match self {
            NodeOrRpcUrl::Node { kns_update, .. } => kns_update.name == other,
            NodeOrRpcUrl::RpcUrl { url, .. } => url == other,
        }
    }
}

impl<'de> Deserialize<'de> for NodeOrRpcUrl {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        #[derive(Deserialize)]
        #[serde(untagged)]
        enum RpcUrlHelper {
            String(String),
            Struct {
                url: String,
                auth: Option<Authorization>,
            },
        }

        #[derive(Deserialize)]
        enum Helper {
            Node {
                kns_update: crate::net::KnsUpdate,
                use_as_provider: bool,
            },
            RpcUrl(RpcUrlHelper),
        }

        let helper = Helper::deserialize(deserializer)?;

        Ok(match helper {
            Helper::Node {
                kns_update,
                use_as_provider,
            } => NodeOrRpcUrl::Node {
                kns_update,
                use_as_provider,
            },
            Helper::RpcUrl(url_helper) => match url_helper {
                RpcUrlHelper::String(url) => NodeOrRpcUrl::RpcUrl { url, auth: None },
                RpcUrlHelper::Struct { url, auth } => NodeOrRpcUrl::RpcUrl { url, auth },
            },
        })
    }
}

/// An EVM chain provider. Create this object to start making RPC calls.
/// Set the chain_id to determine which chain to call: requests will fail
/// unless the node this process is running on has access to a provider
/// for that chain.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Provider {
    chain_id: u64,
    request_timeout: u64,
}

impl Provider {
    /// Instantiate a new provider.
    pub fn new(chain_id: u64, request_timeout: u64) -> Self {
        Self {
            chain_id,
            request_timeout,
        }
    }
    /// Sends a request based on the specified [`EthAction`] and parses the response.
    ///
    /// This function constructs a request targeting the Ethereum distribution system, serializes the provided [`EthAction`],
    /// and sends it. It awaits a response with a specified timeout, then attempts to parse the response into the expected
    /// type `T`. This method is generic and can be used for various Ethereum actions by specifying the appropriate [`EthAction`]
    /// and return type `T`.
    pub fn send_request_and_parse_response<T: serde::de::DeserializeOwned>(
        &self,
        action: EthAction,
    ) -> Result<T, EthError> {
        let resp = KiRequest::new()
            .target(("our", "eth", "distro", "sys"))
            .body(serde_json::to_vec(&action).unwrap())
            .send_and_await_response(self.request_timeout)
            .unwrap()
            .map_err(|_| EthError::RpcTimeout)?;

        match resp {
            Message::Response { body, .. } => match serde_json::from_slice::<EthResponse>(&body) {
                Ok(EthResponse::Response(value)) => {
                    serde_json::from_value::<T>(value).map_err(|_| EthError::RpcMalformedResponse)
                }
                Ok(EthResponse::Err(e)) => Err(e),
                _ => Err(EthError::RpcMalformedResponse),
            },
            _ => Err(EthError::RpcMalformedResponse),
        }
    }

    /// Retrieves the current block number.
    ///
    /// # Returns
    /// A `Result<u64, EthError>` representing the current block number.
    pub fn get_block_number(&self) -> Result<u64, EthError> {
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_blockNumber".to_string(),
            params: ().into(),
        };

        let res = self.send_request_and_parse_response::<U64>(action)?;
        Ok(res.to::<u64>())
    }

    /// Retrieves the balance of the given address at the specified block.
    ///
    /// # Parameters
    /// - `address`: The address to query the balance for.
    /// - `tag`: Optional block ID to specify the block at which the balance is queried.
    ///
    /// # Returns
    /// A `Result<U256, EthError>` representing the balance of the address.
    pub fn get_balance(&self, address: Address, tag: Option<BlockId>) -> Result<U256, EthError> {
        let params = serde_json::to_value((
            address,
            tag.unwrap_or(BlockId::Number(BlockNumberOrTag::Latest)),
        ))
        .unwrap();
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getBalance".to_string(),
            params,
        };

        self.send_request_and_parse_response::<U256>(action)
    }

    /// Retrieves logs based on a filter.
    ///
    /// # Parameters
    /// - `filter`: The filter criteria for the logs.
    ///
    /// # Returns
    /// A `Result<Vec<Log>, EthError>` containing the logs that match the filter.
    pub fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>, EthError> {
        // NOTE: filter must be encased by a tuple to be serialized correctly
        let Ok(params) = serde_json::to_value((filter,)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getLogs".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Vec<Log>>(action)
    }

    /// Retrieves the current gas price.
    ///
    /// # Returns
    /// A `Result<U256, EthError>` representing the current gas price.
    pub fn get_gas_price(&self) -> Result<U256, EthError> {
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_gasPrice".to_string(),
            params: ().into(),
        };

        self.send_request_and_parse_response::<U256>(action)
    }

    /// Retrieves the number of transactions sent from the given address.
    ///
    /// # Parameters
    /// - `address`: The address to query the transaction count for.
    /// - `tag`: Optional block ID to specify the block at which the count is queried.
    ///
    /// # Returns
    /// A `Result<U256, EthError>` representing the number of transactions sent from the address.
    pub fn get_transaction_count(
        &self,
        address: Address,
        tag: Option<BlockId>,
    ) -> Result<U256, EthError> {
        let Ok(params) = serde_json::to_value((address, tag.unwrap_or_default())) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getTransactionCount".to_string(),
            params,
        };

        self.send_request_and_parse_response::<U256>(action)
    }

    /// Retrieves a block by its hash.
    ///
    /// # Parameters
    /// - `hash`: The hash of the block to retrieve.
    /// - `full_tx`: Whether to return full transaction objects or just their hashes.
    ///
    /// # Returns
    /// A `Result<Option<Block>, EthError>` representing the block, if found.
    pub fn get_block_by_hash(
        &self,
        hash: BlockHash,
        full_tx: bool,
    ) -> Result<Option<Block>, EthError> {
        let Ok(params) = serde_json::to_value((hash, full_tx)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getBlockByHash".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Option<Block>>(action)
    }
    /// Retrieves a block by its number or tag.
    ///
    /// # Parameters
    /// - `number`: The number or tag of the block to retrieve.
    /// - `full_tx`: Whether to return full transaction objects or just their hashes.
    ///
    /// # Returns
    /// A `Result<Option<Block>, EthError>` representing the block, if found.
    pub fn get_block_by_number(
        &self,
        number: BlockNumberOrTag,
        full_tx: bool,
    ) -> Result<Option<Block>, EthError> {
        let Ok(params) = serde_json::to_value((number, full_tx)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getBlockByNumber".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Option<Block>>(action)
    }

    /// Retrieves the storage at a given address and key.
    ///
    /// # Parameters
    /// - `address`: The address of the storage to query.
    /// - `key`: The key of the storage slot to retrieve.
    /// - `tag`: Optional block ID to specify the block at which the storage is queried.
    ///
    /// # Returns
    /// A `Result<Bytes, EthError>` representing the data stored at the given address and key.
    pub fn get_storage_at(
        &self,
        address: Address,
        key: U256,
        tag: Option<BlockId>,
    ) -> Result<Bytes, EthError> {
        let Ok(params) = serde_json::to_value((address, key, tag.unwrap_or_default())) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getStorageAt".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Bytes>(action)
    }

    /// Retrieves the code at a given address.
    ///
    /// # Parameters
    /// - `address`: The address of the code to query.
    /// - `tag`: The block ID to specify the block at which the code is queried.
    ///
    /// # Returns
    /// A `Result<Bytes, EthError>` representing the code stored at the given address.
    pub fn get_code_at(&self, address: Address, tag: BlockId) -> Result<Bytes, EthError> {
        let Ok(params) = serde_json::to_value((address, tag)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getCode".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Bytes>(action)
    }

    /// Retrieves a transaction by its hash.
    ///
    /// # Parameters
    /// - `hash`: The hash of the transaction to retrieve.
    ///
    /// # Returns
    /// A `Result<Option<Transaction>, EthError>` representing the transaction, if found.
    pub fn get_transaction_by_hash(&self, hash: TxHash) -> Result<Option<Transaction>, EthError> {
        // NOTE: hash must be encased by a tuple to be serialized correctly
        let Ok(params) = serde_json::to_value((hash,)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getTransactionByHash".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Option<Transaction>>(action)
    }

    /// Retrieves the receipt of a transaction by its hash.
    ///
    /// # Parameters
    /// - `hash`: The hash of the transaction for which the receipt is requested.
    ///
    /// # Returns
    /// A `Result<Option<TransactionReceipt>, EthError>` representing the transaction receipt, if found.
    pub fn get_transaction_receipt(
        &self,
        hash: TxHash,
    ) -> Result<Option<TransactionReceipt>, EthError> {
        // NOTE: hash must be encased by a tuple to be serialized correctly
        let Ok(params) = serde_json::to_value((hash,)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_getTransactionReceipt".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Option<TransactionReceipt>>(action)
    }

    /// Estimates the amount of gas that a transaction will consume.
    ///
    /// # Parameters
    /// - `tx`: The transaction request object containing the details of the transaction to estimate gas for.
    /// - `block`: Optional block ID to specify the block at which the gas estimate should be made.
    ///
    /// # Returns
    /// A `Result<U256, EthError>` representing the estimated gas amount.
    pub fn estimate_gas(
        &self,
        tx: TransactionRequest,
        block: Option<BlockId>,
    ) -> Result<U256, EthError> {
        let Ok(params) = serde_json::to_value((tx, block.unwrap_or_default())) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_estimateGas".to_string(),
            params,
        };

        self.send_request_and_parse_response::<U256>(action)
    }

    /// Retrieves the list of accounts controlled by the node.
    ///
    /// # Returns
    /// A `Result<Vec<Address>, EthError>` representing the list of accounts.
    /// Note: This function may return an empty list depending on the node's configuration and capabilities.
    pub fn get_accounts(&self) -> Result<Vec<Address>, EthError> {
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_accounts".to_string(),
            params: serde_json::Value::Array(vec![]),
        };

        self.send_request_and_parse_response::<Vec<Address>>(action)
    }

    /// Retrieves the fee history for a given range of blocks.
    ///
    /// # Parameters
    /// - `block_count`: The number of blocks to include in the history.
    /// - `last_block`: The ending block number or tag for the history range.
    /// - `reward_percentiles`: A list of percentiles to report fee rewards for.
    ///
    /// # Returns
    /// A `Result<FeeHistory, EthError>` representing the fee history for the specified range.
    pub fn get_fee_history(
        &self,
        block_count: U256,
        last_block: BlockNumberOrTag,
        reward_percentiles: Vec<f64>,
    ) -> Result<FeeHistory, EthError> {
        let Ok(params) = serde_json::to_value((block_count, last_block, reward_percentiles)) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_feeHistory".to_string(),
            params,
        };

        self.send_request_and_parse_response::<FeeHistory>(action)
    }

    /// Executes a call transaction, which is directly executed in the VM of the node, but never mined into the blockchain.
    ///
    /// # Parameters
    /// - `tx`: The transaction request object containing the details of the call.
    /// - `block`: Optional block ID to specify the block at which the call should be made.
    ///
    /// # Returns
    /// A `Result<Bytes, EthError>` representing the result of the call.
    pub fn call(&self, tx: TransactionRequest, block: Option<BlockId>) -> Result<Bytes, EthError> {
        let Ok(params) = serde_json::to_value((tx, block.unwrap_or_default())) else {
            return Err(EthError::InvalidParams);
        };
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_call".to_string(),
            params,
        };

        self.send_request_and_parse_response::<Bytes>(action)
    }

    /// Returns a Kimap instance with the default address using this provider.
    pub fn kimap(&self) -> crate::kimap::Kimap {
        crate::kimap::Kimap::default(self.request_timeout)
    }

    /// Returns a Kimap instance with a custom address using this provider.
    pub fn kimap_with_address(self, address: Address) -> crate::kimap::Kimap {
        crate::kimap::Kimap::new(self, address)
    }

    /// Sends a raw transaction to the network.
    ///
    /// # Parameters
    /// - `tx`: The raw transaction data.
    ///
    /// # Returns
    /// A `Result<TxHash, EthError>` representing the hash of the transaction once it has been sent.
    pub fn send_raw_transaction(&self, tx: Bytes) -> Result<TxHash, EthError> {
        let action = EthAction::Request {
            chain_id: self.chain_id,
            method: "eth_sendRawTransaction".to_string(),
            // NOTE: tx must be encased by a tuple to be serialized correctly
            params: serde_json::to_value((tx,)).unwrap(),
        };

        self.send_request_and_parse_response::<TxHash>(action)
    }

    /// Subscribes to logs without waiting for a confirmation.
    ///
    /// WARNING: some RPC providers will throw an error if a subscription filter
    /// has the `from_block` parameter. Make sure to avoid this parameter for subscriptions
    /// even while using it for getLogs.
    ///
    /// # Parameters
    /// - `sub_id`: The subscription ID to be used for unsubscribing.
    /// - `filter`: The filter criteria for the logs.
    ///
    /// # Returns
    /// A `Result<(), EthError>` indicating whether the subscription was created.
    pub fn subscribe(&self, sub_id: u64, filter: Filter) -> Result<(), EthError> {
        let action = EthAction::SubscribeLogs {
            sub_id,
            chain_id: self.chain_id,
            kind: SubscriptionKind::Logs,
            params: serde_json::to_value(Params::Logs(Box::new(filter)))
                .map_err(|_| EthError::InvalidParams)?,
        };

        let Ok(body) = serde_json::to_vec(&action) else {
            return Err(EthError::InvalidParams);
        };

        let resp = KiRequest::new()
            .target(("our", "eth", "distro", "sys"))
            .body(body)
            .send_and_await_response(self.request_timeout)
            .unwrap()
            .map_err(|_| EthError::RpcTimeout)?;

        match resp {
            Message::Response { body, .. } => {
                let response = serde_json::from_slice::<EthResponse>(&body);
                match response {
                    Ok(EthResponse::Ok) => Ok(()),
                    Ok(EthResponse::Err(e)) => Err(e),
                    _ => Err(EthError::RpcMalformedResponse),
                }
            }
            _ => Err(EthError::RpcMalformedResponse),
        }
    }

    /// Subscribe in a loop until successful
    pub fn subscribe_loop(
        &self,
        sub_id: u64,
        filter: Filter,
        print_verbosity_success: u8,
        print_verbosity_error: u8,
    ) {
        loop {
            match self.subscribe(sub_id, filter.clone()) {
                Ok(()) => break,
                Err(_) => {
                    crate::print_to_terminal(
                        print_verbosity_error,
                        "failed to subscribe to chain! trying again in 5s...",
                    );
                    std::thread::sleep(std::time::Duration::from_secs(5));
                    continue;
                }
            }
        }
        crate::print_to_terminal(print_verbosity_success, "subscribed to logs successfully");
    }

    /// Unsubscribes from a previously created subscription.
    ///
    /// # Parameters
    /// - `sub_id`: The subscription ID to unsubscribe from.
    ///
    /// # Returns
    /// A `Result<(), EthError>` indicating whether the subscription was cancelled.
    pub fn unsubscribe(&self, sub_id: u64) -> Result<(), EthError> {
        let action = EthAction::UnsubscribeLogs(sub_id);

        let resp = KiRequest::new()
            .target(("our", "eth", "distro", "sys"))
            .body(serde_json::to_vec(&action).map_err(|_| EthError::MalformedRequest)?)
            .send_and_await_response(self.request_timeout)
            .unwrap()
            .map_err(|_| EthError::RpcTimeout)?;

        match resp {
            Message::Response { body, .. } => match serde_json::from_slice::<EthResponse>(&body) {
                Ok(EthResponse::Ok) => Ok(()),
                _ => Err(EthError::RpcMalformedResponse),
            },
            _ => Err(EthError::RpcMalformedResponse),
        }
    }
}