Skip to main content

awesome_sails_vft_extension/
lib.rs

1// This file is part of Gear.
2
3// Copyright (C) 2025 Gear Technologies Inc.
4// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
5
6// This program is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// This program is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with this program. If not, see <https://www.gnu.org/licenses/>.
18
19//! Awesome VFT-Extension service.
20//!
21//! This service extends the standard VFT functionality with additional features such as:
22//! - Cleaning up expired allowances.
23//! - Transferring the entire balance (`transfer_all`).
24//! - Enumerating allowances and balances.
25//! - Managing storage shards explicitly.
26
27#![no_std]
28
29use awesome_sails_storage::StorageMut;
30use awesome_sails_utils::{
31    ensure,
32    error::{EmitError, Error},
33    math::{Max, NonZero, Zero},
34    ok_if,
35    pause::PausableRef,
36};
37use awesome_sails_vft::{
38    self as vft,
39    utils::{Allowances, Balance, Balances},
40};
41use sails_rs::prelude::*;
42
43/// The VFT Extension service struct.
44pub struct VftExtension<
45    'a,
46    A: StorageMut<Item = Allowances> = PausableRef<'a, Allowances>,
47    B: StorageMut<Item = Balances> = PausableRef<'a, Balances>,
48> {
49    allowances: A,
50    balances: B,
51    vft: vft::VftExposure<vft::Vft<'a, A, B>>,
52}
53
54impl<'a, A: StorageMut<Item = Allowances>, B: StorageMut<Item = Balances>> VftExtension<'a, A, B> {
55    /// Creates a new instance of the VFT Extension service.
56    ///
57    /// # Arguments
58    ///
59    /// * `allowances` - Storage backend for allowances.
60    /// * `balances` - Storage backend for balances.
61    /// * `vft` - Exposure of the base VFT service.
62    pub fn new(allowances: A, balances: B, vft: vft::VftExposure<vft::Vft<'a, A, B>>) -> Self {
63        Self {
64            allowances,
65            balances,
66            vft,
67        }
68    }
69}
70
71#[service]
72impl<A: StorageMut<Item = Allowances>, B: StorageMut<Item = Balances>> VftExtension<'_, A, B> {
73    /// Allocates the next shard for allowances storage.
74    ///
75    /// Useful when the current shard is full.
76    ///
77    /// # Returns
78    ///
79    /// `true` if a new shard was allocated, `false` otherwise.
80    #[export(unwrap_result)]
81    pub fn allocate_next_allowances_shard(&mut self) -> Result<bool, Error> {
82        Ok(self.allowances.get_mut()?.allocate_next_shard())
83    }
84
85    /// Allocates the next shard for balances storage.
86    ///
87    /// Useful when the current shard is full.
88    ///
89    /// # Returns
90    ///
91    /// `true` if a new shard was allocated, `false` otherwise.
92    #[export(unwrap_result)]
93    pub fn allocate_next_balances_shard(&mut self) -> Result<bool, Error> {
94        Ok(self.balances.get_mut()?.allocate_next_shard())
95    }
96
97    /// Removes an expired allowance.
98    ///
99    /// If the allowance from `owner` to `spender` has expired, it is removed to free up storage.
100    ///
101    /// # Arguments
102    ///
103    /// * `owner` - The account that granted the allowance.
104    /// * `spender` - The account that was granted the allowance.
105    ///
106    /// # Returns
107    ///
108    /// `true` if the allowance was removed, `false` otherwise (e.g., if it didn't exist).
109    #[export(unwrap_result)]
110    pub fn remove_expired_allowance(
111        &mut self,
112        owner: ActorId,
113        spender: ActorId,
114    ) -> Result<bool, Error> {
115        ok_if!(owner == spender, false);
116
117        let _owner = owner.try_into()?;
118        let _spender = spender.try_into()?;
119
120        {
121            let mut allowances = self.allowances.get_mut()?;
122
123            let Some((_, (_, expiry))) = (**allowances).get(&(_owner, _spender)) else {
124                return Ok(false);
125            };
126
127            ensure!(*expiry < Syscall::block_height(), AllowanceNotExpiredError);
128
129            allowances.remove(_owner, _spender);
130        }
131
132        // TODO: consider if we need to emit event here.
133        self.vft
134            .emit_event(vft::Event::Approval {
135                owner,
136                spender,
137                value: U256::zero(),
138            })
139            .map_err(|_| EmitError)?;
140
141        Ok(true)
142    }
143
144    /// Transfers the entire balance from the caller to `to`.
145    ///
146    /// # Arguments
147    ///
148    /// * `to` - The recipient of the tokens.
149    ///
150    /// # Returns
151    ///
152    /// `true` if any tokens were transferred.
153    #[export(unwrap_result)]
154    pub fn transfer_all(&mut self, to: ActorId) -> Result<bool, Error> {
155        let from = Syscall::message_source();
156
157        ok_if!(from == to, false);
158
159        let value: U256 = self
160            .balances
161            .get_mut()?
162            .transfer_all(from.try_into()?, to.try_into()?)?
163            .into();
164
165        ok_if!(value.is_zero(), false);
166
167        self.vft
168            .emit_event(vft::Event::Transfer { from, to, value })
169            .map_err(|_| EmitError)?;
170
171        Ok(true)
172    }
173
174    /// Transfers the entire balance from `from` to `to` using the allowance mechanism.
175    ///
176    /// The caller must have sufficient allowance.
177    ///
178    /// # Arguments
179    ///
180    /// * `from` - The account to transfer tokens from.
181    /// * `to` - The recipient of the tokens.
182    ///
183    /// # Returns
184    ///
185    /// `true` if any tokens were transferred.
186    #[export(unwrap_result)]
187    pub fn transfer_all_from(&mut self, from: ActorId, to: ActorId) -> Result<bool, Error> {
188        let spender = Syscall::message_source();
189
190        if spender == from {
191            return self.transfer_all(to);
192        }
193
194        ok_if!(from == to, false);
195
196        let _spender = spender.try_into()?;
197        let _from = from.try_into()?;
198        let _to = to.try_into()?;
199
200        let value = self.balances.get_mut()?.transfer_all(_from, _to)?;
201
202        ok_if!(value.is_zero(), false);
203
204        let _value = <NonZero<Balance>>::try_from(value)?;
205
206        self.allowances.get_mut()?.decrease(
207            _from,
208            _spender,
209            _value.non_zero_cast(),
210            Syscall::block_height(),
211        )?;
212
213        self.vft
214            .emit_event(vft::Event::Transfer {
215                from,
216                to,
217                value: value.into(),
218            })
219            .map_err(|_| EmitError)?;
220
221        Ok(true)
222    }
223
224    /// Returns the allowance detail (amount and expiration block) for a given owner and spender.
225    ///
226    /// # Arguments
227    ///
228    /// * `owner` - The account owning the tokens.
229    /// * `spender` - The account allowed to spend the tokens.
230    ///
231    /// # Returns
232    ///
233    /// An `Option` containing a tuple `(U256, u32)` representing the amount and expiration block height.
234    #[export(unwrap_result)]
235    pub fn allowance_of(
236        &self,
237        owner: ActorId,
238        spender: ActorId,
239    ) -> Result<Option<(U256, u32)>, Error> {
240        Ok((**self.allowances.get()?)
241            .get(&(owner.try_into()?, spender.try_into()?))
242            .map(|(_, &(v, b))| {
243                let approval = if v.is_max() { U256::MAX } else { (*v).into() };
244
245                (approval, b)
246            }))
247    }
248
249    /// Returns a list of all allowances with pagination.
250    ///
251    /// # Arguments
252    ///
253    /// * `cursor` - The index to start from.
254    /// * `len` - The number of items to return.
255    ///
256    /// # Returns
257    ///
258    /// A vector of allowance details.
259    #[allow(clippy::type_complexity)]
260    #[export(unwrap_result)]
261    pub fn allowances(
262        &self,
263        cursor: u32,
264        len: u32,
265    ) -> Result<Vec<((ActorId, ActorId), (U256, u32))>, Error> {
266        Ok(self
267            .allowances
268            .get()?
269            .iter()
270            .skip(cursor as usize)
271            .take(len as usize)
272            .map(|(&(owner, spender), &(allowance, b))| {
273                ((owner.into(), spender.into()), ((*allowance).into(), b))
274            })
275            .collect())
276    }
277
278    /// Returns the balance of an account, if it exists in storage.
279    ///
280    /// Unlike `vft::balance_of` which returns 0 for non-existent accounts, this returns `None`.
281    ///
282    /// # Arguments
283    ///
284    /// * `account` - The account to query.
285    ///
286    /// # Returns
287    ///
288    /// An `Option<U256>` containing the balance.
289    #[export(unwrap_result)]
290    pub fn balance_of(&self, account: ActorId) -> Result<Option<U256>, Error> {
291        Ok((**self.balances.get()?)
292            .get(&account.try_into()?)
293            .map(|(_, &v)| (*v).into()))
294    }
295
296    /// Returns a list of all balances with pagination.
297    ///
298    /// # Arguments
299    ///
300    /// * `cursor` - The index to start from.
301    /// * `len` - The number of items to return.
302    ///
303    /// # Returns
304    ///
305    /// A vector of `(ActorId, U256)` pairs.
306    #[export(unwrap_result)]
307    pub fn balances(&self, cursor: u32, len: u32) -> Result<Vec<(ActorId, U256)>, Error> {
308        Ok(self
309            .balances
310            .get()?
311            .iter()
312            .skip(cursor as usize)
313            .take(len as usize)
314            .map(|(&account, &v)| (account.into(), (*v).into()))
315            .collect())
316    }
317
318    /// Returns the configured allowance expiry period.
319    #[export(unwrap_result)]
320    pub fn expiry_period(&self) -> Result<u32, Error> {
321        Ok(self.allowances.get()?.expiry_period())
322    }
323
324    /// Returns the amount of value (tokens) that are currently "unused" or reserved.
325    #[export(unwrap_result)]
326    pub fn unused_value(&self) -> Result<U256, Error> {
327        Ok(self.balances.get()?.unused_value())
328    }
329}
330
331/// Error indicating that an attempt was made to remove an allowance that has not yet expired.
332#[derive(
333    Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Decode, Encode, TypeInfo, thiserror::Error,
334)]
335#[codec(crate = sails_rs::scale_codec)]
336#[error("allowance is not expired")]
337#[scale_info(crate = sails_rs::scale_info)]
338pub struct AllowanceNotExpiredError;