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;