renegade_sdk/renegade_wallet_client/actions/
withdraw.rs

1//! Withdraw funds from an account balance
2
3use std::time::Duration;
4
5use alloy::primitives::Address;
6use renegade_circuit_types::Amount;
7use renegade_crypto::fields::scalar_to_u256;
8use renegade_darkpool_types::balance::DarkpoolStateBalance;
9use renegade_external_api::http::balance::{
10    WITHDRAW_BALANCE_ROUTE, WithdrawBalanceRequest, WithdrawBalanceResponse,
11};
12use renegade_solidity_abi::v2::{
13    IDarkpoolV2::WithdrawalAuth, transfer_auth::withdrawal::create_withdrawal_auth,
14};
15
16use crate::{
17    RenegadeClientError,
18    actions::{NON_BLOCKING_PARAM, construct_http_path},
19    client::RenegadeClient,
20    websocket::TaskWaiter,
21};
22
23/// The timeout for a withdrawal action to complete.
24///
25/// This is longer than the default since any enqueued fee payment tasks must
26/// complete first.
27const TASK_WAITER_TIMEOUT: Duration = Duration::from_secs(120);
28
29// --- Public Actions --- //
30impl RenegadeClient {
31    /// Withdraw funds from an account balance. Waits for the withdrawal task to
32    /// complete before returning.
33    pub async fn withdraw(&self, mint: Address, amount: Amount) -> Result<(), RenegadeClientError> {
34        let request = self.build_withdrawal_request(mint, amount).await?;
35
36        let path = self.build_withdrawal_request_path(mint, false)?;
37
38        self.relayer_client.post::<_, WithdrawBalanceResponse>(&path, request).await?;
39
40        Ok(())
41    }
42
43    /// Enqueues a withdrawal task in the relayer. Returns a `TaskWaiter` that
44    /// can be used to await task completion.
45    pub async fn enqueue_withdrawal(
46        &self,
47        mint: Address,
48        amount: Amount,
49    ) -> Result<TaskWaiter, RenegadeClientError> {
50        let request = self.build_withdrawal_request(mint, amount).await?;
51
52        let path = self.build_withdrawal_request_path(mint, true)?;
53
54        let WithdrawBalanceResponse { task_id, .. } =
55            self.relayer_client.post(&path, request).await?;
56
57        let task_waiter = self.watch_task(task_id, TASK_WAITER_TIMEOUT).await?;
58
59        Ok(task_waiter)
60    }
61}
62
63// --- Private Helpers --- //
64impl RenegadeClient {
65    /// Builds the request to withdraw from a balance
66    async fn build_withdrawal_request(
67        &self,
68        mint: Address,
69        amount: Amount,
70    ) -> Result<WithdrawBalanceRequest, RenegadeClientError> {
71        let signature = self.build_withdrawal_auth(mint, amount).await?;
72        Ok(WithdrawBalanceRequest { amount, signature })
73    }
74
75    /// Builds the signature over the balance commitment which authorizes the
76    /// withdrawal
77    async fn build_withdrawal_auth(
78        &self,
79        mint: Address,
80        amount: Amount,
81    ) -> Result<Vec<u8>, RenegadeClientError> {
82        let balance = self.get_balance_by_mint(mint).await?;
83        let mut state_balance: DarkpoolStateBalance =
84            crate::renegade_wallet_client::conversions::api_balance_to_state_balance(balance)?;
85
86        // First, we simulate fee payments on the balance.
87        // This is necessary because the withdrawal API handler will execute fee
88        // payments before executing the withdrawal.
89        // We need to ensure that the balance whose commitment we sign to authorize the
90        // withdrawal is correctly updated to reflect this.
91        simulate_fee_payments(&mut state_balance);
92
93        // Next, we update the balance's amount, progressing its cryptographic state
94        // accordingly.
95        state_balance.inner.amount -= amount;
96        let new_amount = state_balance.inner.amount;
97        let new_amount_public_share = state_balance.stream_cipher_encrypt(&new_amount);
98        state_balance.public_share.amount = new_amount_public_share;
99        state_balance.compute_recovery_id();
100
101        // Finally, we compute the commitment to the balance & sign it to authorize the
102        // withdrawal
103        let commitment = scalar_to_u256(&state_balance.compute_commitment());
104        let chain_id = self.get_chain_id();
105
106        let WithdrawalAuth { signature } =
107            create_withdrawal_auth(commitment, chain_id, self.get_account_signer())
108                .map_err(RenegadeClientError::signing)?;
109
110        Ok(signature.to_vec())
111    }
112
113    /// Builds the request path for the withdrawal balance endpoint
114    fn build_withdrawal_request_path(
115        &self,
116        mint: Address,
117        non_blocking: bool,
118    ) -> Result<String, RenegadeClientError> {
119        let path = construct_http_path!(WITHDRAW_BALANCE_ROUTE, "account_id" => self.get_account_id(), "mint" => mint);
120        let query_string =
121            serde_urlencoded::to_string(&[(NON_BLOCKING_PARAM, non_blocking.to_string())])
122                .map_err(RenegadeClientError::serde)?;
123
124        Ok(format!("{path}?{query_string}"))
125    }
126}
127
128// ----------------------
129// | Non-Member Helpers |
130// ----------------------
131
132/// Simulates fee payments on the balance
133///
134/// Only apply fee simulation if the balance has outstanding fees.
135fn simulate_fee_payments(state_balance: &mut DarkpoolStateBalance) {
136    // First, we simulate the relayer fee payment
137    if state_balance.inner.relayer_fee_balance > 0 {
138        state_balance.pay_relayer_fee();
139        state_balance.reencrypt_relayer_fee();
140        state_balance.compute_recovery_id();
141    }
142
143    // Then, we simulate the protocol fee payment.
144    // We use a dummy address for the protocol fee receiver since we don't actually
145    // need a valid fee note.
146    if state_balance.inner.protocol_fee_balance > 0 {
147        state_balance.pay_protocol_fee(Address::ZERO);
148        state_balance.reencrypt_protocol_fee();
149        state_balance.compute_recovery_id();
150    }
151}