1use 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#[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#[derive(Debug, Serialize, Deserialize, Clone, Eq, PartialEq)]
42pub struct Settings {
43 pub funder: SuiAddress,
44 pub spender: Option<SuiAddress>,
45 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#[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#[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#[derive(Debug, Clone)]
94pub struct ResolvedAllowance {
95 pub funder: SuiAddress,
96 pub spender: Option<SuiAddress>,
97 pub funds_type: TypeTag,
99}
100
101pub 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}