Skip to main content

hiero_sdk/token/
token_dissociate_transaction.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use hiero_sdk_proto::services;
4use hiero_sdk_proto::services::token_service_client::TokenServiceClient;
5use tonic::transport::Channel;
6
7use crate::ledger_id::RefLedgerId;
8use crate::protobuf::{
9    FromProtobuf,
10    ToProtobuf,
11};
12use crate::transaction::{
13    AnyTransactionData,
14    ChunkInfo,
15    ToSchedulableTransactionDataProtobuf,
16    ToTransactionDataProtobuf,
17    TransactionData,
18    TransactionExecute,
19};
20use crate::{
21    AccountId,
22    BoxGrpcFuture,
23    Error,
24    TokenId,
25    Transaction,
26    ValidateChecksums,
27};
28
29/// Dissociates the provided account with the provided tokens. Must be signed by the provided
30/// Account's key.
31///
32/// On success, associations between the provided account and tokens are removed.
33///
34/// - If the provided account is not found, the transaction will resolve to `INVALID_ACCOUNT_ID`.
35/// - If the provided account has been deleted, the transaction will resolve to `ACCOUNT_DELETED`.
36/// - If any of the provided tokens is not found, the transaction will resolve to `INVALID_TOKEN_REF`.
37/// - If any of the provided tokens has been deleted, the transaction will resolve to `TOKEN_WAS_DELETED`.
38/// - If an association between the provided account and any of the tokens does not exist, the
39/// transaction will resolve to `TOKEN_NOT_ASSOCIATED_TO_ACCOUNT`.
40/// - If a token has not been deleted and has not expired, and the user has a nonzero balance, the
41/// transaction will resolve to `TRANSACTION_REQUIRES_ZERO_TOKEN_BALANCES`.
42/// - If a <b>fungible token</b> has expired, the user can disassociate even if their token balance is
43/// not zero.
44/// - If a <b>non fungible token</b> has expired, the user can <b>not</b> disassociate if their token
45/// balance is not zero. The transaction will resolve to `TRANSACTION_REQUIRED_ZERO_TOKEN_BALANCES`.
46pub type TokenDissociateTransaction = Transaction<TokenDissociateTransactionData>;
47
48#[derive(Debug, Clone, Default)]
49pub struct TokenDissociateTransactionData {
50    /// The account to be dissociated with the provided tokens.
51    account_id: Option<AccountId>,
52
53    /// The tokens to be dissociated with the provided account.
54    token_ids: Vec<TokenId>,
55}
56
57impl TokenDissociateTransaction {
58    /// Returns the account to be dissociated with the provided tokens.
59    #[must_use]
60    pub fn get_account_id(&self) -> Option<AccountId> {
61        self.data().account_id
62    }
63
64    /// Sets the account to be dissociated with the provided tokens.
65    pub fn account_id(&mut self, account_id: AccountId) -> &mut Self {
66        self.data_mut().account_id = Some(account_id);
67        self
68    }
69
70    /// Returns the tokens to be dissociated with the provided account.
71    #[must_use]
72    pub fn get_token_ids(&self) -> &[TokenId] {
73        &self.data().token_ids
74    }
75
76    /// Sets the tokens to be dissociated with the provided account.
77    pub fn token_ids(&mut self, token_ids: impl IntoIterator<Item = TokenId>) -> &mut Self {
78        self.data_mut().token_ids = token_ids.into_iter().collect();
79        self
80    }
81}
82
83impl TransactionData for TokenDissociateTransactionData {}
84
85impl TransactionExecute for TokenDissociateTransactionData {
86    fn execute(
87        &self,
88        channel: Channel,
89        request: services::Transaction,
90    ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
91        Box::pin(async { TokenServiceClient::new(channel).dissociate_tokens(request).await })
92    }
93}
94
95impl ValidateChecksums for TokenDissociateTransactionData {
96    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
97        self.account_id.validate_checksums(ledger_id)?;
98        for token_id in &self.token_ids {
99            token_id.validate_checksums(ledger_id)?;
100        }
101        Ok(())
102    }
103}
104
105impl ToTransactionDataProtobuf for TokenDissociateTransactionData {
106    fn to_transaction_data_protobuf(
107        &self,
108        chunk_info: &ChunkInfo,
109    ) -> services::transaction_body::Data {
110        let _ = chunk_info.assert_single_transaction();
111
112        services::transaction_body::Data::TokenDissociate(self.to_protobuf())
113    }
114}
115
116impl ToSchedulableTransactionDataProtobuf for TokenDissociateTransactionData {
117    fn to_schedulable_transaction_data_protobuf(
118        &self,
119    ) -> services::schedulable_transaction_body::Data {
120        services::schedulable_transaction_body::Data::TokenDissociate(self.to_protobuf())
121    }
122}
123
124impl From<TokenDissociateTransactionData> for AnyTransactionData {
125    fn from(transaction: TokenDissociateTransactionData) -> Self {
126        Self::TokenDissociate(transaction)
127    }
128}
129
130impl FromProtobuf<services::TokenDissociateTransactionBody> for TokenDissociateTransactionData {
131    fn from_protobuf(pb: services::TokenDissociateTransactionBody) -> crate::Result<Self> {
132        Ok(Self {
133            account_id: Option::from_protobuf(pb.account)?,
134            token_ids: Vec::from_protobuf(pb.tokens)?,
135        })
136    }
137}
138
139impl ToProtobuf for TokenDissociateTransactionData {
140    type Protobuf = services::TokenDissociateTransactionBody;
141
142    fn to_protobuf(&self) -> Self::Protobuf {
143        let account = self.account_id.to_protobuf();
144        let tokens = self.token_ids.to_protobuf();
145
146        services::TokenDissociateTransactionBody { account, tokens }
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use expect_test::expect;
153    use hiero_sdk_proto::services;
154
155    use crate::protobuf::{
156        FromProtobuf,
157        ToProtobuf,
158    };
159    use crate::token::TokenDissociateTransactionData;
160    use crate::transaction::test_helpers::{
161        check_body,
162        transaction_body,
163    };
164    use crate::{
165        AccountId,
166        AnyTransaction,
167        TokenDissociateTransaction,
168        TokenId,
169    };
170
171    const TEST_ACCOUNT_ID: AccountId =
172        AccountId { shard: 6, realm: 9, num: 0, alias: None, evm_address: None, checksum: None };
173
174    const TEST_TOKEN_IDS: [TokenId; 3] =
175        [TokenId::new(4, 2, 0), TokenId::new(4, 2, 1), TokenId::new(4, 2, 2)];
176
177    fn make_transaction() -> TokenDissociateTransaction {
178        let mut tx = TokenDissociateTransaction::new_for_tests();
179
180        tx.account_id(TEST_ACCOUNT_ID).token_ids(TEST_TOKEN_IDS).freeze().unwrap();
181
182        tx
183    }
184
185    #[test]
186    fn serialize() {
187        let tx = make_transaction();
188
189        let tx = transaction_body(tx);
190
191        let tx = check_body(tx);
192
193        expect![[r#"
194            TokenDissociate(
195                TokenDissociateTransactionBody {
196                    account: Some(
197                        AccountId {
198                            shard_num: 6,
199                            realm_num: 9,
200                            account: Some(
201                                AccountNum(
202                                    0,
203                                ),
204                            ),
205                        },
206                    ),
207                    tokens: [
208                        TokenId {
209                            shard_num: 4,
210                            realm_num: 2,
211                            token_num: 0,
212                        },
213                        TokenId {
214                            shard_num: 4,
215                            realm_num: 2,
216                            token_num: 1,
217                        },
218                        TokenId {
219                            shard_num: 4,
220                            realm_num: 2,
221                            token_num: 2,
222                        },
223                    ],
224                },
225            )
226        "#]]
227        .assert_debug_eq(&tx)
228    }
229
230    #[test]
231    fn to_from_bytes() {
232        let tx = make_transaction();
233
234        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
235
236        let tx = transaction_body(tx);
237
238        let tx2 = transaction_body(tx2);
239
240        assert_eq!(tx, tx2);
241    }
242
243    #[test]
244    fn from_proto_body() {
245        let tx = services::TokenDissociateTransactionBody {
246            account: Some(TEST_ACCOUNT_ID.to_protobuf()),
247            tokens: TEST_TOKEN_IDS.iter().map(TokenId::to_protobuf).collect(),
248        };
249
250        let data = TokenDissociateTransactionData::from_protobuf(tx).unwrap();
251
252        assert_eq!(data.account_id, Some(TEST_ACCOUNT_ID));
253        assert_eq!(data.token_ids, TEST_TOKEN_IDS);
254    }
255
256    #[test]
257    fn get_set_account_id() {
258        let mut tx = TokenDissociateTransaction::new();
259        tx.account_id(TEST_ACCOUNT_ID);
260
261        assert_eq!(tx.get_account_id(), Some(TEST_ACCOUNT_ID));
262    }
263
264    #[test]
265    #[should_panic]
266    fn get_set_account_id_frozen_panic() {
267        make_transaction().account_id(TEST_ACCOUNT_ID);
268    }
269
270    #[test]
271    fn get_set_token_ids() {
272        let mut tx = TokenDissociateTransaction::new();
273        tx.token_ids(TEST_TOKEN_IDS);
274
275        assert_eq!(tx.get_token_ids(), &TEST_TOKEN_IDS);
276    }
277
278    #[test]
279    #[should_panic]
280    fn get_set_token_ids_frozen_panic() {
281        make_transaction().token_ids(TEST_TOKEN_IDS);
282    }
283}