Skip to main content

sui_types/
allowance.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rust bindings for `sui::allowance`.
5//!
6//! At signing, a transaction's declared (funder, allowance) pair is checked against the loaded
7//! `Allowance` object, and the withdrawal is reserved against the funder's balance. At execution,
8//! the adapter mints the `AllowanceWithdrawal<T>` that the allowance's spend paths consume.
9
10use crate::SUI_FRAMEWORK_ADDRESS;
11use crate::base_types::SuiAddress;
12use crate::error::{UserInputError, UserInputResult};
13use crate::id::UID;
14use crate::object::Object;
15use move_core_types::account_address::AccountAddress;
16use move_core_types::ident_str;
17use move_core_types::identifier::IdentStr;
18use move_core_types::language_storage::{StructTag, TypeTag};
19use move_core_types::u256::U256;
20use mysten_common::debug_fatal;
21use serde::{Deserialize, Serialize};
22
23pub const ALLOWANCE_MODULE_NAME: &IdentStr = ident_str!("allowance");
24pub const ALLOWANCE_STRUCT_NAME: &IdentStr = ident_str!("Allowance");
25pub const ALLOWANCE_WITHDRAWAL_STRUCT_NAME: &IdentStr = ident_str!("AllowanceWithdrawal");
26pub const RESOLVED_ALLOWANCE_WITHDRAWAL_STRUCT: (&AccountAddress, &IdentStr, &IdentStr) = (
27    &SUI_FRAMEWORK_ADDRESS,
28    ALLOWANCE_MODULE_NAME,
29    ALLOWANCE_WITHDRAWAL_STRUCT_NAME,
30);
31
32/// Mirror of the Move struct `sui::allowance::Allowance<T>`.
33#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
34pub struct Allowance {
35    pub id: UID,
36    pub settings: Settings,
37    pub current_spend: U256,
38}
39
40/// Mirror of the Move struct `sui::allowance::Settings`.
41#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
42pub struct Settings {
43    pub funder: SuiAddress,
44    pub spender: Option<SuiAddress>,
45    /// `Option<std::type_name::TypeName>`.
46    pub app: Option<String>,
47    pub lifetime_cap: Option<U256>,
48    pub start_timestamp_ms: Option<u64>,
49    pub expiration_timestamp_ms: Option<u64>,
50    pub rate_limit: Option<RateLimit>,
51    pub name: String,
52}
53
54/// Mirror of the Move enum `sui::allowance::RateLimit`.
55#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
56pub enum RateLimit {
57    Windowed {
58        limit: U256,
59        spent: U256,
60        anchor_ms: Option<u64>,
61        index: u64,
62        window: Window,
63    },
64}
65
66/// Mirror of the Move enum `sui::allowance::Window`.
67#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
68pub enum Window {
69    PeriodicMs(u64),
70    CalendarMonths(u8),
71}
72
73impl Allowance {
74    pub fn type_(type_param: TypeTag) -> StructTag {
75        StructTag {
76            address: SUI_FRAMEWORK_ADDRESS,
77            module: ALLOWANCE_MODULE_NAME.to_owned(),
78            name: ALLOWANCE_STRUCT_NAME.to_owned(),
79            type_params: vec![type_param],
80        }
81    }
82
83    pub fn is_allowance(s: &StructTag) -> bool {
84        s.address == SUI_FRAMEWORK_ADDRESS
85            && s.module.as_ident_str() == ALLOWANCE_MODULE_NAME
86            && s.name.as_ident_str() == ALLOWANCE_STRUCT_NAME
87            && s.type_params.len() == 1
88    }
89}
90
91/// Sign-time view of an `Allowance<T>`.
92/// NB: The spender can rotate from transaction to transaction.
93#[derive(Debug, Clone)]
94pub struct ResolvedAllowance {
95    pub funder: SuiAddress,
96    pub spender: Option<SuiAddress>,
97    /// The accumulated type `T` of `Allowance<T>` (e.g. `Balance<SUI>`).
98    pub funds_type: TypeTag,
99}
100
101/// Parses an object as an `Allowance`, extracting the sign-time-relevant fields.
102pub fn parse_allowance_object(object: &Object) -> UserInputResult<ResolvedAllowance> {
103    let invalid = |error: String| UserInputError::InvalidWithdrawReservation { error };
104    let id = object.id();
105    let Some(move_obj) = object.data.try_as_move() else {
106        return Err(invalid(format!(
107            "Specified allowance {id} is not a Move object"
108        )));
109    };
110    let tag: StructTag = move_obj.type_().clone().into();
111    if !Allowance::is_allowance(&tag) {
112        return Err(invalid(format!(
113            "Specified allowance {id} is not a sui::allowance::Allowance"
114        )));
115    }
116    if !object.owner.is_shared() {
117        return Err(invalid(format!("Allowance {id} is not a shared object")));
118    }
119    let funds_type = tag
120        .type_params
121        .into_iter()
122        .next()
123        .expect("checked by is_allowance");
124
125    let Ok(allowance) = bcs::from_bytes::<Allowance>(move_obj.contents()) else {
126        debug_fatal!("allowance {id} did not match the `Allowance` rust type");
127        return Err(invalid(format!("Failed to read allowance {id}")));
128    };
129
130    Ok(ResolvedAllowance {
131        funder: allowance.settings.funder,
132        spender: allowance.settings.spender,
133        funds_type,
134    })
135}