Skip to main content

hiero_sdk/token/
token_wipe_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/// Wipes the provided amount of tokens from the specified Account. Must be signed by the Token's
30/// Wipe key.
31///
32/// On success, tokens are removed from the account and the total supply of the token is decreased by
33/// the wiped amount.
34///
35/// The amount provided is in the lowest denomination possible. Example:
36/// Token A has 2 decimals. In order to wipe 100 tokens from account, one must provide amount of 10000.
37/// In order to wipe 100.55 tokens, one must provide amount of 10055.
38///
39/// - If the provided account is not found, the transaction will resolve to `INVALID_ACCOUNT_ID`.
40/// - If the provided account has been deleted, the transaction will resolve to `ACCOUNT_DELETED`.
41/// - If the provided token is not found, the transaction will resolve to `INVALID_TOKEN_ID`.
42/// - If the provided token has been deleted, the transaction will resolve to `TOKEN_WAS_DELETED`.
43/// - If an Association between the provided token and account is not found, the transaction will
44/// resolve to `TOKEN_NOT_ASSOCIATED_TO_ACCOUNT`.
45/// - If Wipe Key is not present in the Token, transaction results in `TOKEN_HAS_NO_WIPE_KEY`.
46/// - If the provided account is the Token's Treasury Account, transaction results in
47/// `CANNOT_WIPE_TOKEN_TREASURY_ACCOUNT`
48/// - If both amount and serialNumbers get filled, a `INVALID_TRANSACTION_BODY` response code will be
49/// returned.
50/// - If neither the amount nor the serialNumbers get filled, a `INVALID_WIPING_AMOUNT` response code
51/// will be returned.
52/// - If the serialNumbers list contains a non-positive integer as a serial number, a `INVALID_NFT_ID`
53/// response code will be returned.
54/// - If the serialNumbers' list count is greater than the batch size limit global dynamic property, a
55/// `BATCH_SIZE_LIMIT_EXCEEDED` response code will be returned.
56///
57pub type TokenWipeTransaction = Transaction<TokenWipeTransactionData>;
58
59#[derive(Debug, Clone, Default)]
60pub struct TokenWipeTransactionData {
61    /// The account to be wiped.
62    account_id: Option<AccountId>,
63
64    /// The token for which the account will be wiped.
65    token_id: Option<TokenId>,
66
67    // TODO change type of `amount` from `Option<u64>` to `u64`
68    /// The amount of a fungible token to wipe from the specified account.
69    amount: Option<u64>,
70
71    /// The serial numbers of a non-fungible token to wipe from the specified account.
72    serials: Vec<u64>,
73}
74
75impl TokenWipeTransaction {
76    /// Returns the account to be wiped.
77    #[must_use]
78    pub fn get_account_id(&self) -> Option<AccountId> {
79        self.data().account_id
80    }
81
82    /// Sets the account to be wiped.
83    pub fn account_id(&mut self, account_id: AccountId) -> &mut Self {
84        self.data_mut().account_id = Some(account_id);
85        self
86    }
87
88    /// Returns the token for which the account will be wiped.
89    #[must_use]
90    pub fn get_token_id(&self) -> Option<TokenId> {
91        self.data().token_id
92    }
93
94    /// Sets the token for which the account will be wiped.
95    pub fn token_id(&mut self, token_id: impl Into<TokenId>) -> &mut Self {
96        self.data_mut().token_id = Some(token_id.into());
97        self
98    }
99
100    /// Returns the amount of a fungible token to wipe from the specified account.
101    #[must_use]
102    pub fn get_amount(&self) -> Option<u64> {
103        self.data().amount
104    }
105
106    // TODO remove `impl Into<_>`
107    /// Sets the amount of a fungible token to wipe from the specified account.
108    pub fn amount(&mut self, amount: impl Into<u64>) -> &mut Self {
109        self.data_mut().amount = Some(amount.into());
110        self
111    }
112
113    /// Returns the serial numbers of a non-fungible token to wipe from the specified account.
114    #[must_use]
115    pub fn get_serials(&self) -> &[u64] {
116        &self.data().serials
117    }
118
119    /// Sets the serial numbers of a non-fungible token to wipe from the specified account.
120    pub fn serials(&mut self, serials: impl IntoIterator<Item = u64>) -> &mut Self {
121        self.data_mut().serials = serials.into_iter().collect();
122        self
123    }
124}
125
126impl TransactionData for TokenWipeTransactionData {}
127
128impl TransactionExecute for TokenWipeTransactionData {
129    fn execute(
130        &self,
131        channel: Channel,
132        request: services::Transaction,
133    ) -> BoxGrpcFuture<'_, services::TransactionResponse> {
134        Box::pin(async { TokenServiceClient::new(channel).wipe_token_account(request).await })
135    }
136}
137
138impl ValidateChecksums for TokenWipeTransactionData {
139    fn validate_checksums(&self, ledger_id: &RefLedgerId) -> Result<(), Error> {
140        self.account_id.validate_checksums(ledger_id)?;
141        self.token_id.validate_checksums(ledger_id)
142    }
143}
144
145impl ToTransactionDataProtobuf for TokenWipeTransactionData {
146    fn to_transaction_data_protobuf(
147        &self,
148        chunk_info: &ChunkInfo,
149    ) -> services::transaction_body::Data {
150        let _ = chunk_info.assert_single_transaction();
151
152        services::transaction_body::Data::TokenWipe(self.to_protobuf())
153    }
154}
155
156impl ToSchedulableTransactionDataProtobuf for TokenWipeTransactionData {
157    fn to_schedulable_transaction_data_protobuf(
158        &self,
159    ) -> services::schedulable_transaction_body::Data {
160        services::schedulable_transaction_body::Data::TokenWipe(self.to_protobuf())
161    }
162}
163
164impl From<TokenWipeTransactionData> for AnyTransactionData {
165    fn from(transaction: TokenWipeTransactionData) -> Self {
166        Self::TokenWipe(transaction)
167    }
168}
169
170impl FromProtobuf<services::TokenWipeAccountTransactionBody> for TokenWipeTransactionData {
171    fn from_protobuf(pb: services::TokenWipeAccountTransactionBody) -> crate::Result<Self> {
172        Ok(Self {
173            account_id: Option::from_protobuf(pb.account)?,
174            token_id: Option::from_protobuf(pb.token)?,
175            amount: Some(pb.amount),
176            serials: pb.serial_numbers.into_iter().map(|it| it as u64).collect(),
177        })
178    }
179}
180impl ToProtobuf for TokenWipeTransactionData {
181    type Protobuf = services::TokenWipeAccountTransactionBody;
182
183    fn to_protobuf(&self) -> Self::Protobuf {
184        services::TokenWipeAccountTransactionBody {
185            token: self.token_id.to_protobuf(),
186            account: self.account_id.to_protobuf(),
187            amount: self.amount.unwrap_or_default(),
188            serial_numbers: self.serials.iter().map(|num| *num as i64).collect(),
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use expect_test::expect;
196    use hiero_sdk_proto::services;
197
198    use crate::protobuf::{
199        FromProtobuf,
200        ToProtobuf,
201    };
202    use crate::transaction::test_helpers::{
203        check_body,
204        transaction_body,
205    };
206    use crate::{
207        AccountId,
208        AnyTransaction,
209        TokenId,
210        TokenWipeTransaction,
211    };
212
213    const TEST_ACCOUNT_ID: AccountId = AccountId::new(0, 6, 9);
214    const TEST_TOKEN_ID: TokenId = TokenId::new(4, 2, 0);
215    const TEST_AMOUNT: u64 = 4;
216    const TEST_SERIALS: [u64; 3] = [8, 9, 10];
217    fn make_transaction() -> TokenWipeTransaction {
218        let mut tx = TokenWipeTransaction::new_for_tests();
219
220        tx.token_id(TEST_TOKEN_ID)
221            .account_id(TEST_ACCOUNT_ID)
222            .amount(TEST_AMOUNT)
223            .freeze()
224            .unwrap();
225
226        tx
227    }
228
229    fn make_transaction_nft() -> TokenWipeTransaction {
230        let mut tx = TokenWipeTransaction::new_for_tests();
231
232        tx.token_id(TEST_TOKEN_ID)
233            .account_id(TEST_ACCOUNT_ID)
234            .serials(TEST_SERIALS)
235            .freeze()
236            .unwrap();
237
238        tx
239    }
240
241    #[test]
242    fn serialize_fungible() {
243        let tx = make_transaction();
244
245        let tx = transaction_body(tx);
246
247        let tx = check_body(tx);
248
249        expect![[r#"
250            TokenWipe(
251                TokenWipeAccountTransactionBody {
252                    token: Some(
253                        TokenId {
254                            shard_num: 4,
255                            realm_num: 2,
256                            token_num: 0,
257                        },
258                    ),
259                    account: Some(
260                        AccountId {
261                            shard_num: 0,
262                            realm_num: 6,
263                            account: Some(
264                                AccountNum(
265                                    9,
266                                ),
267                            ),
268                        },
269                    ),
270                    amount: 4,
271                    serial_numbers: [],
272                },
273            )
274        "#]]
275        .assert_debug_eq(&tx);
276    }
277
278    #[test]
279    fn to_from_bytes_fungible() {
280        let tx = make_transaction();
281
282        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
283
284        let tx = transaction_body(tx);
285        let tx2 = transaction_body(tx2);
286
287        assert_eq!(tx, tx2);
288    }
289
290    #[test]
291    fn serialize_nft() {
292        let tx = make_transaction_nft();
293
294        let tx = transaction_body(tx);
295
296        let tx = check_body(tx);
297
298        expect![[r#"
299            TokenWipe(
300                TokenWipeAccountTransactionBody {
301                    token: Some(
302                        TokenId {
303                            shard_num: 4,
304                            realm_num: 2,
305                            token_num: 0,
306                        },
307                    ),
308                    account: Some(
309                        AccountId {
310                            shard_num: 0,
311                            realm_num: 6,
312                            account: Some(
313                                AccountNum(
314                                    9,
315                                ),
316                            ),
317                        },
318                    ),
319                    amount: 0,
320                    serial_numbers: [
321                        8,
322                        9,
323                        10,
324                    ],
325                },
326            )
327        "#]]
328        .assert_debug_eq(&tx);
329    }
330
331    #[test]
332    fn to_from_bytes_nft() {
333        let tx = make_transaction_nft();
334
335        let tx2 = AnyTransaction::from_bytes(&tx.to_bytes().unwrap()).unwrap();
336
337        let tx = transaction_body(tx);
338        let tx2 = transaction_body(tx2);
339
340        assert_eq!(tx, tx2);
341    }
342
343    #[test]
344    fn from_proto_body() {
345        let tx = services::TokenWipeAccountTransactionBody {
346            token: Some(TEST_TOKEN_ID.to_protobuf()),
347            account: Some(TEST_ACCOUNT_ID.to_protobuf()),
348            amount: TEST_AMOUNT,
349            serial_numbers: TEST_SERIALS.into_iter().map(|it| it as i64).collect(),
350        };
351
352        let tx = super::TokenWipeTransactionData::from_protobuf(tx).unwrap();
353
354        assert_eq!(tx.token_id, Some(TEST_TOKEN_ID));
355        assert_eq!(tx.account_id, Some(TEST_ACCOUNT_ID));
356        assert_eq!(tx.amount, Some(TEST_AMOUNT));
357        assert_eq!(tx.serials, TEST_SERIALS);
358    }
359
360    #[test]
361    fn get_set_token_id() {
362        let mut tx = TokenWipeTransaction::new();
363        tx.token_id(TEST_TOKEN_ID);
364
365        assert_eq!(tx.get_token_id(), Some(TEST_TOKEN_ID));
366    }
367
368    #[test]
369    #[should_panic]
370    fn get_set_token_id_frozen_panic() {
371        let mut tx = make_transaction();
372        tx.token_id(TEST_TOKEN_ID);
373    }
374
375    #[test]
376    fn get_set_account_id() {
377        let mut tx = TokenWipeTransaction::new();
378        tx.account_id(TEST_ACCOUNT_ID);
379
380        assert_eq!(tx.get_account_id(), Some(TEST_ACCOUNT_ID));
381    }
382
383    #[test]
384    #[should_panic]
385    fn get_set_account_id_frozen_panic() {
386        let mut tx = make_transaction();
387        tx.account_id(TEST_ACCOUNT_ID);
388    }
389
390    #[test]
391    fn get_set_amount() {
392        let mut tx = TokenWipeTransaction::new();
393        tx.amount(TEST_AMOUNT);
394
395        assert_eq!(tx.get_amount(), Some(TEST_AMOUNT));
396    }
397
398    #[test]
399    #[should_panic]
400    fn get_set_amount_frozen_panic() {
401        let mut tx = make_transaction();
402        tx.amount(TEST_AMOUNT);
403    }
404
405    #[test]
406    fn get_set_serial_numbers() {
407        let mut tx = TokenWipeTransaction::new();
408        tx.serials(TEST_SERIALS);
409
410        assert_eq!(tx.get_serials(), TEST_SERIALS);
411    }
412
413    #[test]
414    #[should_panic]
415    fn get_set_serial_numbers_frozen_panic() {
416        let mut tx = make_transaction_nft();
417        tx.serials(TEST_SERIALS);
418    }
419}