Skip to main content

bulk_client/api/
bulk_http.rs

1//! Bulk Labs HTTP REST API Client
2//!
3//! Provides complete HTTP REST API access to the Bulk Labs exchange:
4//! - Market data endpoints (unsigned)
5//! - Account query endpoints (unsigned)
6//! - Trading endpoints (signed)
7//! - Private endpoints (signed — faucet, etc.)
8//!
9//! # Example
10//!
11//! ```rust,no_run
12//! use bulk_client::*;
13//! use bulk_client::common::side::Side;
14//! use bulk_client::common::tif::TimeInForce;
15//!
16//! #[tokio::main]
17//! async fn main() -> eyre::Result<()> {
18//!     // Read-only client (market data + account queries)
19//!     let client = BulkHttpClient::with_url(
20//!         "https://exchange-api.bulk.trade/api/v1", None,
21//!     )?;
22//!
23//!     let ticker = client.get_ticker("BTC-USD").await?;
24//!     println!("BTC mark: {}", ticker.mark_price);
25//!
26//!     // Authenticated client (trading)
27//!     let client = BulkHttpClient::with_url(
28//!         "https://exchange-api.bulk.trade/api/v1",
29//!         Some("your_base58_private_key"),
30//!     )?;
31//!     let resp = client.place_limit_order(
32//!         "BTC-USD", Side::Buy, 95_000.0, 0.001,
33//!         TimeInForce::GTC, false, None, None,
34//!     ).await?;
35//!     Ok(())
36//! }
37//! ```
38
39use crate::api::parts::{make_nonce, HttpConfig};
40use crate::common::side::Side;
41use crate::common::tif::TimeInForce;
42use crate::msgs::*;
43use crate::transaction::{Action, ActionMeta, Transaction, TransactionSigner};
44use reqwest::{Client, Url};
45use serde::Deserialize;
46use serde_json::{json, Value};
47use solana_hash::Hash;
48use solana_pubkey::Pubkey;
49use std::collections::HashMap;
50use std::str::FromStr;
51use std::sync::Arc;
52use std::time::Duration;
53
54/// HTTP REST API client for Bulk Labs exchange.
55///
56/// Supports both public (unsigned) and private (signed) endpoints.
57/// Construct with `None` for read-only access or provide a private key
58/// for trading operations.
59#[derive(Clone)]
60#[allow(unused)]
61pub struct BulkHttpClient {
62    config: HttpConfig,
63    client: Client,
64    is_localhost: bool,
65}
66
67#[allow(unused)]
68impl BulkHttpClient {
69    /// Create bulk HTTP client
70    ///
71    /// # Arguments
72    /// - `config`: http client config
73    pub fn new(config: &HttpConfig) -> eyre::Result<Self> {
74        let client = Client::builder().timeout(config.default_timeout).build()?;
75
76        let is_localhost = Self::is_localhost(&config.base_url);
77
78        Ok(Self {
79            config: config.clone(),
80            client,
81            is_localhost,
82        })
83    }
84
85    /// Create bulk HTTP client with url, private key
86    ///
87    /// # Arguments
88    /// - `base_url`: http client url
89    /// - `private_key`: optional private key
90    pub fn with_url(base_url: &str, private_key: Option<&str>) -> eyre::Result<Self> {
91        if let Some(private_key) = private_key {
92            let signer = TransactionSigner::from_private_key(private_key)?;
93            let config = HttpConfig {
94                base_url: base_url.to_string(),
95                signer: Some(signer),
96                default_timeout: Duration::from_secs(10),
97            };
98            Self::new(&config)
99        } else {
100            let config = HttpConfig {
101                base_url: base_url.to_string(),
102                signer: None,
103                default_timeout: Duration::from_secs(10),
104            };
105            Self::new(&config)
106        }
107    }
108
109    /// Create bulk HTTP client with a pre-built signer (software key or Ledger).
110    ///
111    /// # Example
112    /// ```text
113    /// let signer = TransactionSigner::from_ledger("usb://ledger", None)?;
114    /// let client = BulkHttpClient::with_signer("https://exchange-api.bulk.trade/api/v1", signer)?;
115    /// let resp = client.request_faucet(None, None, None).await?;
116    /// ```
117    pub fn with_signer(base_url: &str, signer: TransactionSigner) -> eyre::Result<Self> {
118        let config = HttpConfig {
119            base_url: base_url.to_string(),
120            signer: Some(signer),
121            default_timeout: Duration::from_secs(10),
122        };
123        Self::new(&config)
124    }
125
126    /// Channel configuration
127    pub fn config(&self) -> &HttpConfig {
128        &self.config
129    }
130
131    /// Pubkeyt associated with this channel
132    pub fn public_key(&self) -> Option<Pubkey> {
133        self.config.signer.as_ref().map(|x| x.public_key())
134    }
135
136    // =====================================================================
137    // MARKET DATA ENDPOINTS (PUBLIC, UNSIGNED)
138    // =====================================================================
139
140    /// Get exchange information including all available markets.
141    pub async fn get_exchange_info(&self) -> eyre::Result<Vec<MarketInfo>> {
142        let resp = self
143            .client
144            .get(format!("{}/exchangeInfo", self.config.base_url))
145            .send()
146            .await?
147            .error_for_status()?;
148        Ok(resp.json().await?)
149    }
150
151    /// Get market ticker/statistics for a symbol.
152    pub async fn get_ticker(&self, symbol: &str) -> eyre::Result<Ticker> {
153        let resp = self
154            .client
155            .get(format!("{}/ticker/{}", self.config.base_url, symbol))
156            .send()
157            .await?
158            .error_for_status()?;
159        Ok(resp.json().await?)
160    }
161
162    /// Get historical candlestick/OHLCV data.
163    ///
164    /// # Arguments
165    /// - `symbol`: Market symbol (e.g. "BTC-USD")
166    /// - `interval`: Candle interval ("1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w")
167    /// - `start_time`: Optional start timestamp in milliseconds
168    /// - `end_time`: Optional end timestamp in milliseconds
169    /// - `limit`: Maximum candles to return (default 500, max 1000)
170    pub async fn get_klines(
171        &self,
172        symbol: &str,
173        interval: &str,
174        start_time: Option<u64>,
175        end_time: Option<u64>,
176        limit: Option<u32>,
177    ) -> eyre::Result<Vec<Candle>> {
178        let mut params = vec![
179            ("symbol".to_string(), symbol.to_string()),
180            ("interval".to_string(), interval.to_string()),
181            ("limit".to_string(), limit.unwrap_or(500).to_string()),
182        ];
183        if let Some(st) = start_time {
184            params.push(("startTime".to_string(), st.to_string()));
185        }
186        if let Some(et) = end_time {
187            params.push(("endTime".to_string(), et.to_string()));
188        }
189
190        let resp = self
191            .client
192            .get(format!("{}/klines", self.config.base_url))
193            .query(&params)
194            .send()
195            .await?
196            .error_for_status()?;
197        Ok(resp.json().await?)
198    }
199
200    /// Get L2 order book snapshot.
201    ///
202    /// # Arguments
203    /// - `symbol`: Market symbol
204    /// - `nlevels`: Number of price levels per side (default 20, max 1000)
205    /// - `aggregation`: Optional price aggregation/grouping
206    pub async fn get_orderbook(
207        &self,
208        symbol: &str,
209        nlevels: Option<u32>,
210        aggregation: Option<f64>,
211    ) -> eyre::Result<L2Snapshot> {
212        let mut params = vec![
213            ("type".to_string(), "l2Book".to_string()),
214            ("coin".to_string(), symbol.to_string()),
215            ("nlevels".to_string(), nlevels.unwrap_or(20).to_string()),
216        ];
217        if let Some(agg) = aggregation {
218            params.push(("aggregation".to_string(), agg.to_string()));
219        }
220
221        let resp = self
222            .client
223            .get(format!("{}/l2book", self.config.base_url))
224            .query(&params)
225            .send()
226            .await?
227            .error_for_status()?;
228        Ok(resp.json().await?)
229    }
230
231    // =====================================================================
232    // ACCOUNT ENDPOINTS (PUBLIC, UNSIGNED)
233    // =====================================================================
234
235    /// Get complete account state including positions, orders, and margin.
236    ///
237    /// # Arguments
238    /// - `user`: user pubkey to query
239    pub async fn get_account(&self, user: Pubkey) -> eyre::Result<AccountData> {
240        #[derive(Debug, Clone, Deserialize)]
241        #[serde(rename_all = "camelCase")]
242        pub struct FullAccountResponse {
243            pub full_account: AccountData,
244        }
245
246        let user: String = user.to_string();
247
248        let resp = self
249            .client
250            .post(format!("{}/account", self.config.base_url))
251            .json(&json!({ "type": "fullAccount", "user": user }))
252            .send()
253            .await?
254            .error_for_status()?;
255
256        let arr: Vec<FullAccountResponse> = resp.json().await?;
257        arr.into_iter()
258            .next()
259            .map(|r| r.full_account)
260            .ok_or_else(|| eyre::eyre!("empty fullAccount response"))
261    }
262
263    /// Get resting orders for an account.
264    ///
265    /// # Arguments
266    /// - `user`: user pubkey to query
267    pub async fn get_open_orders(&self, user: &str) -> eyre::Result<Vec<OrderState>> {
268        let resp = self
269            .client
270            .post(format!("{}/account", self.config.base_url))
271            .json(&json!({ "type": "openOrders", "user": user }))
272            .send()
273            .await?
274            .error_for_status()?;
275        Ok(resp.json().await?)
276    }
277
278    /// Get trade history (up to 5000 recent fills).
279    ///
280    /// # Arguments
281    /// - `user`: user pubkey to query
282    pub async fn get_fills(&self, user: &str) -> eyre::Result<Vec<Fill>> {
283        let resp = self
284            .client
285            .post(format!("{}/account", self.config.base_url))
286            .json(&json!({ "type": "fills", "user": user }))
287            .send()
288            .await?
289            .error_for_status()?;
290        Ok(resp.json().await?)
291    }
292
293    /// Get closed position history (up to 5000 positions).
294    ///
295    /// # Arguments
296    /// - `user`: user pubkey to query
297    pub async fn get_position_history(&self, user: &str) -> eyre::Result<Vec<PositionInfo>> {
298        let resp = self
299            .client
300            .post(format!("{}/account", self.config.base_url))
301            .json(&json!({ "type": "positions", "user": user }))
302            .send()
303            .await?
304            .error_for_status()?;
305        Ok(resp.json().await?)
306    }
307
308    // =====================================================================
309    // Trading (SIGNED)
310    // =====================================================================
311
312    /// Place multiple order actions in a single signed transaction.
313    ///
314    /// Accepts any mix of limit orders, market orders, cancels, and cancel-alls.
315    ///
316    /// # Example
317    /// ```text
318    /// let resp = client.place_tx(vec![
319    ///     Action::LimitOrder(LimitOrder { .. }),
320    ///     Action::CancelAll(CancelAll { .. }),
321    /// ], None, None).await?;
322    /// ```
323    pub async fn place_tx(
324        &self,
325        actions: Vec<Action>,
326        account: Option<Pubkey>,
327        nonce: Option<u64>,
328    ) -> eyre::Result<Vec<Response>> {
329        let signer = self
330            .config
331            .signer
332            .as_ref()
333            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
334
335        let account = if let Some(account) = account {
336            account
337        } else {
338            signer.public_key()
339        };
340
341        let nonce = nonce.unwrap_or_else(make_nonce);
342        // Build + sign the transaction
343        let mut tx = Transaction {
344            actions,
345            nonce,
346            account: account,
347            signer: signer.public_key(),
348            signature: Default::default(),
349        };
350        tx.sign(signer)?;
351
352        // Build JSON body via tx serialization
353        let body = serde_json::to_string(&tx)?;
354
355        let mut request = self
356            .client
357            .post(format!("{}/order", self.config.base_url))
358            .header("content-type", "application/json");
359        if let Some(mode) = signer.tx_signature_mode_hint_header_value() {
360            request = request.header("X-Bulk-Sig-Mode", mode);
361        }
362        let resp = request.body(body).send().await?;
363        let status = resp.status();
364        if !status.is_success() {
365            let text = resp.text().await.unwrap_or_default();
366            return Err(eyre::eyre!(
367                "HTTP {} from /order: {}",
368                status,
369                text.chars().take(600).collect::<String>()
370            ));
371        }
372
373        let data: Value = resp.json().await?;
374        Ok(Response::parse_responses(&data))
375    }
376
377    /// Place a single limit order.
378    pub async fn place_limit_order(
379        &self,
380        symbol: &str,
381        side: Side,
382        price: f64,
383        size: f64,
384        tif: TimeInForce,
385        reduce_only: bool,
386        account: Option<Pubkey>,
387        nonce: Option<u64>,
388    ) -> eyre::Result<Response> {
389        let signer = self
390            .config
391            .signer
392            .as_ref()
393            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
394
395        let account = if let Some(account) = account {
396            account
397        } else {
398            signer.public_key()
399        };
400
401        let nonce = nonce.unwrap_or_else(make_nonce);
402
403        let order = LimitOrder {
404            symbol: Arc::from(symbol),
405            is_buy: side == Side::Buy,
406            price,
407            size,
408            tif,
409            reduce_only,
410            iso: false,
411            builder_code: None,
412            meta: ActionMeta {
413                account,
414                nonce,
415                seqno: 0,
416                hash: None,
417            },
418        };
419
420        let results = self.place_tx(vec![order.into()], None, None).await?;
421        Ok(results[0].clone())
422    }
423
424    /// Place a single market order.
425    pub async fn place_market_order(
426        &self,
427        symbol: &str,
428        side: Side,
429        size: f64,
430        reduce_only: bool,
431        account: Option<Pubkey>,
432        nonce: Option<u64>,
433    ) -> eyre::Result<Response> {
434        let signer = self
435            .config
436            .signer
437            .as_ref()
438            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
439
440        let account = if let Some(account) = account {
441            account
442        } else {
443            signer.public_key()
444        };
445
446        let nonce = nonce.unwrap_or_else(make_nonce);
447
448        let order = MarketOrder {
449            symbol: Arc::from(symbol),
450            is_buy: side == Side::Buy,
451            size,
452            reduce_only,
453            iso: false,
454            builder_code: None,
455            meta: ActionMeta {
456                account,
457                nonce,
458                seqno: 0,
459                hash: None,
460            },
461        };
462
463        let results = self.place_tx(vec![order.into()], None, None).await?;
464        Ok(results[0].clone())
465    }
466
467    /// Cancel a single order by ID.
468    pub async fn cancel_order(
469        &self,
470        symbol: &str,
471        order_id: &str,
472        account: Option<Pubkey>,
473        nonce: Option<u64>,
474    ) -> eyre::Result<Response> {
475        let signer = self
476            .config
477            .signer
478            .as_ref()
479            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
480
481        let account = if let Some(account) = account {
482            account
483        } else {
484            signer.public_key()
485        };
486
487        let nonce = nonce.unwrap_or_else(make_nonce);
488        let cancel = CancelOrder {
489            symbol: symbol.to_string(),
490            oid: Hash::from_str(&order_id)?,
491            meta: ActionMeta {
492                account,
493                nonce,
494                seqno: 0,
495                hash: None,
496            },
497        };
498
499        let results = self.place_tx(vec![cancel.into()], None, None).await?;
500        Ok(results[0].clone())
501    }
502
503    /// Cancel all orders, optionally filtered by symbols.
504    pub async fn cancel_all(
505        &self,
506        symbols: Vec<String>,
507        account: Option<Pubkey>,
508        nonce: Option<u64>,
509    ) -> eyre::Result<Response> {
510        let signer = self
511            .config
512            .signer
513            .as_ref()
514            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
515
516        let account = if let Some(account) = account {
517            account
518        } else {
519            signer.public_key()
520        };
521
522        let nonce = nonce.unwrap_or_else(make_nonce);
523        let cancel = CancelAll {
524            symbols,
525            meta: ActionMeta {
526                account,
527                nonce,
528                seqno: 0,
529                hash: None,
530            },
531        };
532
533        let results = self.place_tx(vec![cancel.into()], None, None).await?;
534        Ok(results[0].clone())
535    }
536
537    // =====================================================================
538    // Meta (SIGNED)
539    // =====================================================================
540
541    /// Update maximum leverage settings for markets.
542    ///
543    /// # Arguments
544    /// - `settings`: Map of (symbol, max_leverage) pairs
545    pub async fn update_leverage(
546        &self,
547        settings: HashMap<String, f64>,
548        account: Option<Pubkey>,
549        nonce: Option<u64>,
550    ) -> eyre::Result<Response> {
551        let signer = self
552            .config
553            .signer
554            .as_ref()
555            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
556
557        let account = if let Some(account) = account {
558            account
559        } else {
560            signer.public_key()
561        };
562
563        let nonce = nonce.unwrap_or_else(make_nonce);
564        let settings = UpdateUserSettings {
565            max_leverage: settings,
566            meta: ActionMeta {
567                account,
568                nonce,
569                seqno: 0,
570                hash: None,
571            },
572        };
573
574        let results = self.place_tx(vec![settings.into()], None, None).await?;
575        Ok(results[0].clone())
576    }
577
578    /// Create or delete an agent wallet authorization.
579    ///
580    /// # Arguments
581    /// - `agent_pubkey`: Agent's public key (base58)
582    /// - `delete`: `true` to remove the agent, `false` to add
583    pub async fn manage_agent_wallet(
584        &self,
585        agent_pubkey: Pubkey,
586        delete: bool,
587        account: Option<Pubkey>,
588        nonce: Option<u64>,
589    ) -> eyre::Result<Response> {
590        let signer = self
591            .config
592            .signer
593            .as_ref()
594            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
595
596        let account = if let Some(account) = account {
597            account
598        } else {
599            signer.public_key()
600        };
601
602        let nonce = nonce.unwrap_or_else(make_nonce);
603        let settings = AgentWalletCreation {
604            agent: agent_pubkey,
605            delete,
606            meta: ActionMeta {
607                account,
608                nonce,
609                seqno: 0,
610                hash: None,
611            },
612        };
613
614        let results = self
615            .place_tx(vec![Action::AgentWalletCreation(settings)], None, None)
616            .await?;
617        Ok(results[0].clone())
618    }
619
620    /// Approve a builder-code recipient for routed orders.
621    ///
622    /// Builder codes are encoded as builder-code fees on the wire.
623    pub async fn approve_builder_code(
624        &self,
625        to: Pubkey,
626        fee: u8,
627        account: Option<Pubkey>,
628        nonce: Option<u64>,
629    ) -> eyre::Result<Response> {
630        let signer = self
631            .config
632            .signer
633            .as_ref()
634            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
635
636        let account = if let Some(account) = account {
637            account
638        } else {
639            signer.public_key()
640        };
641
642        let nonce = nonce.unwrap_or_else(make_nonce);
643        let action = ApproveCommissionFee {
644            to,
645            max_fee: fee,
646            meta: ActionMeta {
647                account,
648                nonce,
649                seqno: 0,
650                hash: None,
651            },
652        };
653
654        let results = self.place_tx(vec![action.into()], None, None).await?;
655        Ok(results[0].clone())
656    }
657
658    /// Revoke a builder-code recipient approval.
659    pub async fn revoke_builder_code(
660        &self,
661        to: Pubkey,
662        account: Option<Pubkey>,
663        nonce: Option<u64>,
664    ) -> eyre::Result<Response> {
665        let signer = self
666            .config
667            .signer
668            .as_ref()
669            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
670
671        let account = if let Some(account) = account {
672            account
673        } else {
674            signer.public_key()
675        };
676
677        let nonce = nonce.unwrap_or_else(make_nonce);
678        let action = RevokeCommissionFee {
679            to,
680            meta: ActionMeta {
681                account,
682                nonce,
683                seqno: 0,
684                hash: None,
685            },
686        };
687
688        let results = self.place_tx(vec![action.into()], None, None).await?;
689        Ok(results[0].clone())
690    }
691
692    // =====================================================================
693    // Testnet-only (SIGNED)
694    // =====================================================================
695
696    /// Whitelist or unwhitelist an account for testnet faucet access.
697    ///
698    /// **Testnet admin only.**
699    ///
700    /// # Arguments
701    /// - `target_account`: account to be whitelisted
702    /// - `whitelist`: if true is added whitelisted, if false removed
703    /// - `nonce`: tx nonce
704    pub async fn whitelist_faucet(
705        &self,
706        target_account: Pubkey,
707        whitelist: bool,
708        account: Option<Pubkey>,
709        nonce: Option<u64>,
710    ) -> eyre::Result<Response> {
711        let signer = self
712            .config
713            .signer
714            .as_ref()
715            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
716
717        let account = if let Some(account) = account {
718            account
719        } else {
720            signer.public_key()
721        };
722
723        let nonce = nonce.unwrap_or_else(make_nonce);
724        let settings = WhitelistFaucet {
725            target: target_account,
726            whitelist,
727            meta: ActionMeta {
728                account,
729                nonce,
730                seqno: 0,
731                hash: None,
732            },
733        };
734        let results = self
735            .place_tx(vec![Action::WhitelistFaucet(settings)], None, None)
736            .await?;
737        Ok(results[0].clone())
738    }
739
740    /// Request testnet faucet funds.
741    ///
742    /// **Testnet only.**
743    ///
744    /// # Arguments
745    /// - `user`: Optional target user public key (defaults to signer's key)
746    /// - `amount`: Optional specific amount (only for whitelisted accounts)
747    /// - `nonce`: Optional nonce
748    pub async fn request_faucet(
749        &self,
750        user: Option<Pubkey>,
751        amount: Option<f64>,
752        nonce: Option<u64>,
753    ) -> eyre::Result<Response> {
754        let signer = self
755            .config
756            .signer
757            .as_ref()
758            .ok_or_else(|| eyre::eyre!("Private key required for trading operations"))?;
759
760        let user = if let Some(user) = user {
761            user
762        } else {
763            signer.public_key()
764        };
765        let nonce = nonce.unwrap_or_else(make_nonce);
766
767        let req = Faucet {
768            user,
769            amount,
770            meta: ActionMeta {
771                account: user,
772                nonce,
773                seqno: 0,
774                hash: None,
775            },
776        };
777
778        let results = self.place_tx(vec![Action::Faucet(req)], None, None).await?;
779        Ok(results[0].clone())
780    }
781
782    // =====================================================================
783    // Internal helpers
784    // =====================================================================
785
786    /// determine if is localhost URL
787    fn is_localhost(url_str: &str) -> bool {
788        let Ok(url) = Url::parse(url_str) else {
789            return false;
790        };
791        match url.host_str() {
792            Some("localhost" | "127.0.0.1" | "::1") => true,
793            _ => false,
794        }
795    }
796
797    /// Build a signed transaction envelope from a partial JSON body.
798    ///
799    /// Adds `account`, `signer`, and `signature` fields.
800    fn sign_generic_transaction(&self, mut body: Value) -> eyre::Result<Value> {
801        let signer = self
802            .config
803            .signer
804            .as_ref()
805            .ok_or_else(|| eyre::eyre!("Private key required"))?;
806
807        let pk_b58 = signer.public_key_b58();
808        body["account"] = json!(pk_b58);
809        body["signer"] = json!(pk_b58);
810
811        let sig = self.sign_action_payload(&body["action"])?;
812        body["signature"] = json!(sig);
813
814        Ok(body)
815    }
816
817    /// Sign the `action` portion of a transaction and return the base58 signature.
818    ///
819    /// This serializes the action JSON to a canonical string and signs it,
820    /// matching the Python SDK's `sign_transaction` behavior for generic
821    /// (non-order) payloads.
822    fn sign_action_payload(&self, action: &Value) -> eyre::Result<String> {
823        let signer = self
824            .config
825            .signer
826            .as_ref()
827            .ok_or_else(|| eyre::eyre!("Private key required"))?;
828
829        // The Python SDK signs the JSON-serialized action string.
830        // For order transactions we use the binary Signable path instead,
831        // but for generic endpoints (leverage, faucet, agent wallet, etc.)
832        // the exchange expects a signature over the canonical JSON.
833        let message = serde_json::to_string(action)?;
834        let sig = signer.sign_bytes(message.as_bytes())?;
835        Ok(bs58::encode(sig).into_string())
836    }
837}