Skip to main content

smplx_sdk/provider/rpc/
elements.rs

1use std::str::FromStr;
2
3use electrsd::bitcoind::bitcoincore_rpc::{Auth, Client, RpcApi};
4
5use serde_json::Value;
6
7use simplicityhl::elements::{Address, AssetId, Txid};
8use simplicityhl::simplicity::bitcoin;
9
10use super::error::RpcError;
11
12use crate::utils::sat2btc;
13
14/// A lightweight wrapper around the standard `bitcoincore_rpc` `Client` providing Elements-specific functionality.
15#[derive(Debug)]
16pub struct ElementsRpc {
17    /// The underlying JSON-RPC client connected to the Elements node.
18    pub inner: Client,
19    /// The authentication credentials used.
20    pub auth: Auth,
21    /// The URL endpoint of the node.
22    pub url: String,
23}
24
25impl ElementsRpc {
26    /// Creates a new `ElementsRpc` client.
27    ///
28    /// # Errors
29    /// Returns an `RpcError` if it fails to initialize the connection or if a liveness `ping` fails.
30    pub fn new(url: String, auth: Auth) -> Result<Self, RpcError> {
31        let inner = Client::new(url.as_str(), auth.clone())?;
32        inner.ping()?;
33
34        Ok(Self { inner, auth, url })
35    }
36
37    /// Requests a new wallet address from the node, mapped to the provided label.
38    ///
39    /// # Errors
40    /// Returns an `RpcError` if the node fails to generate an address or yields an invalid string payload.
41    ///
42    /// # Panics
43    /// Panics if the returned JSON value is not a string, or if the string cannot be parsed into a valid `Address`.
44    pub fn get_new_address(&self, label: &str) -> Result<Address, RpcError> {
45        const METHOD: &str = "getnewaddress";
46
47        let addr: Value = self.inner.call(METHOD, &[label.into(), "bech32".to_string().into()])?;
48
49        Ok(Address::from_str(addr.as_str().unwrap()).unwrap())
50    }
51
52    /// Instructs the node to transfer funds directly to the target address.
53    ///
54    /// # Errors
55    /// Returns an `RpcError` if the node returns an error or returns an invalid `Txid` payload.
56    ///
57    /// # Panics
58    /// Panics if the satoshi amount cannot be converted to a valid BTC amount, if the returned
59    ///  JSON value is not a string, or if the string cannot be parsed into a valid `Txid`.
60    pub fn send_to_address(&self, address: &Address, satoshi: u64, asset: Option<AssetId>) -> Result<Txid, RpcError> {
61        const METHOD: &str = "sendtoaddress";
62
63        let btc = sat2btc(satoshi);
64        let btc = bitcoin::amount::Amount::from_btc(btc)
65            .unwrap()
66            .to_string_in(bitcoin::amount::Denomination::Bitcoin);
67
68        let r = match asset {
69            Some(asset) => self.inner.call::<Value>(
70                METHOD,
71                &[
72                    address.to_string().into(),
73                    btc.into(),
74                    "".into(),
75                    "".into(),
76                    false.into(),
77                    false.into(),
78                    1.into(),
79                    "UNSET".into(),
80                    false.into(),
81                    asset.to_string().into(),
82                ],
83            )?,
84            None => self
85                .inner
86                .call::<Value>(METHOD, &[address.to_string().into(), btc.into()])?,
87        };
88
89        Ok(Txid::from_str(r.as_str().unwrap()).unwrap())
90    }
91
92    /// Instructs the node to rescan the block chain for missed wallet transactions.
93    ///
94    /// # Errors
95    /// Returns an `RpcError` if the node fails the RPC call.
96    pub fn rescan_blockchain(&self, start: Option<u64>, stop: Option<u64>) -> Result<(), RpcError> {
97        const METHOD: &str = "rescanblockchain";
98
99        let mut args = Vec::with_capacity(2);
100
101        if start.is_some() {
102            args.push(start.into());
103        }
104
105        if stop.is_some() {
106            args.push(stop.into());
107        }
108
109        self.inner.call::<Value>(METHOD, &args)?;
110
111        Ok(())
112    }
113
114    /// Mines a specified number of new blocks mapping generated rewards to a newly generated wallet address.
115    ///
116    /// # Errors
117    /// Returns an `RpcError` if generating the address or calling the `generatetoaddress` RPC command fails.
118    pub fn generate_blocks(&self, block_num: u64) -> Result<(), RpcError> {
119        const METHOD: &str = "generatetoaddress";
120
121        let address = self.get_new_address("")?.to_string();
122        self.inner.call::<Value>(METHOD, &[block_num.into(), address.into()])?;
123
124        Ok(())
125    }
126
127    /// Instructs the node to sweep the `initialfreecoins` balance generated by standard regtest genesis blocks.
128    ///
129    /// # Errors
130    /// Returns an `RpcError` if the `getnewaddress` or `sendtoaddress` RPC commands fail.
131    pub fn sweep_initialfreecoins(&self) -> Result<(), RpcError> {
132        const METHOD: &str = "sendtoaddress";
133
134        let address = self.get_new_address("")?;
135        self.inner.call::<Value>(
136            METHOD,
137            &[
138                address.to_string().into(),
139                "21".into(),
140                "".into(),
141                "".into(),
142                true.into(),
143            ],
144        )?;
145
146        Ok(())
147    }
148
149    /// Retrieves the current block chain tip height.
150    ///
151    /// # Errors
152    /// Returns an `RpcError` if the node call fails or yields a JSON structure that does not map successfully to a `u64`.
153    pub fn height(&self) -> Result<u64, RpcError> {
154        const METHOD: &str = "getblockcount";
155
156        self.inner
157            .call::<serde_json::Value>(METHOD, &[])?
158            .as_u64()
159            .ok_or_else(|| RpcError::ElementsRpcUnexpectedReturn(METHOD.into()))
160    }
161}