bittensor_rs/extrinsics/
transfer.rs

1//! # Transfer Extrinsics
2//!
3//! Extrinsics for TAO transfers on the Bittensor network:
4//! - `transfer`: Transfer TAO to another account
5//! - `transfer_keep_alive`: Transfer TAO while keeping the sender account alive
6//! - `transfer_all`: Transfer all TAO to another account
7
8use crate::api::api;
9use crate::error::BittensorError;
10use crate::extrinsics::ExtrinsicResponse;
11use crate::types::Balance;
12use crate::AccountId;
13use std::str::FromStr;
14use subxt::OnlineClient;
15use subxt::PolkadotConfig;
16
17/// Parameters for transfer operations
18#[derive(Debug, Clone)]
19pub struct TransferParams {
20    /// Destination account (SS58 address)
21    pub dest: String,
22    /// Amount to transfer in RAO
23    pub amount_rao: u64,
24    /// Keep the sender account alive (minimum balance)
25    pub keep_alive: bool,
26}
27
28impl TransferParams {
29    /// Create new transfer params with amount in TAO
30    ///
31    /// # Example
32    ///
33    /// ```
34    /// use bittensor_rs::extrinsics::TransferParams;
35    ///
36    /// let params = TransferParams::new_tao(
37    ///     "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY",
38    ///     1.5
39    /// );
40    /// assert_eq!(params.amount_rao, 1_500_000_000);
41    /// ```
42    pub fn new_tao(dest: &str, amount_tao: f64) -> Self {
43        Self {
44            dest: dest.to_string(),
45            amount_rao: (amount_tao * 1_000_000_000.0) as u64,
46            keep_alive: true,
47        }
48    }
49
50    /// Create new transfer params with amount in RAO
51    pub fn new_rao(dest: &str, amount_rao: u64) -> Self {
52        Self {
53            dest: dest.to_string(),
54            amount_rao,
55            keep_alive: true,
56        }
57    }
58
59    /// Set whether to keep the sender account alive
60    pub fn keep_alive(mut self, keep_alive: bool) -> Self {
61        self.keep_alive = keep_alive;
62        self
63    }
64}
65
66/// Transfer TAO to another account
67///
68/// # Arguments
69///
70/// * `client` - The subxt client
71/// * `signer` - The account signer (must have sufficient balance)
72/// * `params` - Transfer parameters
73///
74/// # Returns
75///
76/// An `ExtrinsicResponse` with the transfer result
77///
78/// # Example
79///
80/// ```rust,ignore
81/// use bittensor_rs::extrinsics::{transfer, TransferParams};
82///
83/// async fn example(client: &subxt::OnlineClient<subxt::PolkadotConfig>, signer: &impl subxt::tx::Signer<subxt::PolkadotConfig>) -> Result<(), Box<dyn std::error::Error>> {
84///     let params = TransferParams::new_tao(
85///         "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty",
86///         1.0
87///     );
88///     let result = transfer(client, signer, params).await?;
89///     Ok(())
90/// }
91/// ```
92pub async fn transfer<S>(
93    client: &OnlineClient<PolkadotConfig>,
94    signer: &S,
95    params: TransferParams,
96) -> Result<ExtrinsicResponse<Balance>, BittensorError>
97where
98    S: subxt::tx::Signer<PolkadotConfig>,
99{
100    if params.keep_alive {
101        transfer_keep_alive(client, signer, params).await
102    } else {
103        transfer_allow_death(client, signer, params).await
104    }
105}
106
107/// Transfer TAO while keeping the sender account alive
108///
109/// This ensures the sender's account maintains the minimum existential deposit.
110pub async fn transfer_keep_alive<S>(
111    client: &OnlineClient<PolkadotConfig>,
112    signer: &S,
113    params: TransferParams,
114) -> Result<ExtrinsicResponse<Balance>, BittensorError>
115where
116    S: subxt::tx::Signer<PolkadotConfig>,
117{
118    let dest_account =
119        AccountId::from_str(&params.dest).map_err(|_| BittensorError::InvalidHotkey {
120            hotkey: params.dest.clone(),
121        })?;
122
123    let dest_multi: subxt::utils::MultiAddress<AccountId, ()> =
124        subxt::utils::MultiAddress::Id(dest_account);
125
126    let call = api::tx()
127        .balances()
128        .transfer_keep_alive(dest_multi, params.amount_rao);
129
130    let tx_hash = client
131        .tx()
132        .sign_and_submit_default(&call, signer)
133        .await
134        .map_err(|e| BittensorError::TxSubmissionError {
135            message: format!("Failed to submit transfer: {}", e),
136        })?;
137
138    Ok(ExtrinsicResponse::success()
139        .with_message("Transfer completed successfully")
140        .with_extrinsic_hash(&format!("{:?}", tx_hash))
141        .with_data(Balance::from_rao(params.amount_rao)))
142}
143
144/// Transfer TAO allowing the sender account to be reaped
145async fn transfer_allow_death<S>(
146    client: &OnlineClient<PolkadotConfig>,
147    signer: &S,
148    params: TransferParams,
149) -> Result<ExtrinsicResponse<Balance>, BittensorError>
150where
151    S: subxt::tx::Signer<PolkadotConfig>,
152{
153    let dest_account =
154        AccountId::from_str(&params.dest).map_err(|_| BittensorError::InvalidHotkey {
155            hotkey: params.dest.clone(),
156        })?;
157
158    let dest_multi: subxt::utils::MultiAddress<AccountId, ()> =
159        subxt::utils::MultiAddress::Id(dest_account);
160
161    let call = api::tx()
162        .balances()
163        .transfer_allow_death(dest_multi, params.amount_rao);
164
165    let tx_hash = client
166        .tx()
167        .sign_and_submit_default(&call, signer)
168        .await
169        .map_err(|e| BittensorError::TxSubmissionError {
170            message: format!("Failed to submit transfer: {}", e),
171        })?;
172
173    Ok(ExtrinsicResponse::success()
174        .with_message("Transfer completed successfully")
175        .with_extrinsic_hash(&format!("{:?}", tx_hash))
176        .with_data(Balance::from_rao(params.amount_rao)))
177}
178
179/// Transfer all TAO to another account
180///
181/// Transfers the entire balance, optionally keeping the account alive.
182pub async fn transfer_all<S>(
183    client: &OnlineClient<PolkadotConfig>,
184    signer: &S,
185    dest: &str,
186    keep_alive: bool,
187) -> Result<ExtrinsicResponse<()>, BittensorError>
188where
189    S: subxt::tx::Signer<PolkadotConfig>,
190{
191    let dest_account = AccountId::from_str(dest).map_err(|_| BittensorError::InvalidHotkey {
192        hotkey: dest.to_string(),
193    })?;
194
195    let dest_multi: subxt::utils::MultiAddress<AccountId, ()> =
196        subxt::utils::MultiAddress::Id(dest_account);
197
198    let call = api::tx().balances().transfer_all(dest_multi, keep_alive);
199
200    let tx_hash = client
201        .tx()
202        .sign_and_submit_default(&call, signer)
203        .await
204        .map_err(|e| BittensorError::TxSubmissionError {
205            message: format!("Failed to submit transfer_all: {}", e),
206        })?;
207
208    Ok(ExtrinsicResponse::success()
209        .with_message("Transfer all completed")
210        .with_extrinsic_hash(&format!("{:?}", tx_hash))
211        .with_data(()))
212}
213
214#[cfg(test)]
215mod tests {
216    use super::*;
217
218    #[test]
219    fn test_transfer_params_tao() {
220        let params =
221            TransferParams::new_tao("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 1.5);
222        assert_eq!(params.amount_rao, 1_500_000_000);
223        assert!(params.keep_alive);
224    }
225
226    #[test]
227    fn test_transfer_params_rao() {
228        let params =
229            TransferParams::new_rao("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 1000);
230        assert_eq!(params.amount_rao, 1000);
231    }
232
233    #[test]
234    fn test_transfer_params_builder() {
235        let params =
236            TransferParams::new_tao("5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", 1.0)
237                .keep_alive(false);
238
239        assert!(!params.keep_alive);
240    }
241}