stellar_access/role_transfer/storage.rs
1use soroban_sdk::{contracttype, panic_with_error, Address, Env, IntoVal, Val};
2
3use crate::role_transfer::RoleTransferError;
4
5/// Stores the pending role holder and the explicit deadline for acceptance.
6#[contracttype]
7pub struct PendingTransfer {
8 pub address: Address,
9 pub live_until_ledger: u32,
10}
11
12/// Initiates the role transfer. If `live_until_ledger == 0`, cancels the
13/// pending transfer.
14///
15/// Does not emit any events.
16///
17/// # Arguments
18///
19/// * `e` - Access to the Soroban environment.
20/// * `new` - The proposed new role holder.
21/// * `pending_key` - Storage key for the pending role holder.
22/// * `live_until_ledger` - Ledger number until which the new role holder can
23/// accept. A value of `0` cancels the pending transfer. If the specified
24/// ledger is in the past or exceeds the maximum allowed TTL extension for a
25/// temporary storage entry, the function will panic.
26///
27/// # Errors
28///
29/// * [`RoleTransferError::NoPendingTransfer`] - If trying to cancel a transfer
30/// that doesn't exist.
31/// * [`RoleTransferError::InvalidLiveUntilLedger`] - If the specified ledger is
32/// in the past, or exceeds the maximum allowed TTL extension for a temporary
33/// storage entry.
34/// * [`RoleTransferError::InvalidPendingAccount`] - If the specified pending
35/// account is not the same as the provided `new` address.
36///
37/// # Notes
38///
39/// * This function does not enforce authorization. Ensure that authorization is
40/// handled at a higher level.
41/// * `live_until_ledger` is stored explicitly inside [`PendingTransfer`] and is
42/// checked on every [`accept_transfer`] call, regardless of the storage
43/// entry's TTL. This means the deadline is always enforced, even if the
44/// underlying temporary entry is kept alive longer by the network minimum TTL
45/// or by a permissionless `extend_ttl` call.
46/// * To extend the acceptance window after a transfer has already been
47/// initiated, the current role holder can call this function again with the
48/// same `new` address and a later `live_until_ledger`. This overwrites the
49/// existing [`PendingTransfer`] in place, updating the deadline.
50pub fn transfer_role<T>(e: &Env, new: &Address, pending_key: &T, live_until_ledger: u32)
51where
52 T: IntoVal<Env, Val>,
53{
54 if live_until_ledger == 0 {
55 let Some(pending) = e.storage().temporary().get::<T, PendingTransfer>(pending_key) else {
56 panic_with_error!(e, RoleTransferError::NoPendingTransfer);
57 };
58 if pending.address != *new {
59 panic_with_error!(e, RoleTransferError::InvalidPendingAccount);
60 }
61 e.storage().temporary().remove(pending_key);
62
63 return;
64 }
65
66 let current_ledger = e.ledger().sequence();
67 if live_until_ledger > e.ledger().max_live_until_ledger() || live_until_ledger < current_ledger
68 {
69 panic_with_error!(e, RoleTransferError::InvalidLiveUntilLedger);
70 }
71
72 let live_for = live_until_ledger - current_ledger;
73 let pending = PendingTransfer { address: new.clone(), live_until_ledger };
74 e.storage().temporary().set(pending_key, &pending);
75 e.storage().temporary().extend_ttl(pending_key, live_for, live_for);
76}
77
78/// Returns `true` if the given pending-transfer key holds an **active**
79/// (non-expired) [`PendingTransfer`].
80///
81/// An entry whose `live_until_ledger` is less than the current ledger
82/// sequence is treated as absent and removed from storage as cleanup.
83///
84/// # Arguments
85///
86/// * `e` - Access to the Soroban environment.
87/// * `pending_key` - Storage key for the pending role holder.
88pub fn has_active_pending_transfer<T>(e: &Env, pending_key: &T) -> bool
89where
90 T: IntoVal<Env, Val>,
91{
92 match e.storage().temporary().get::<T, PendingTransfer>(pending_key) {
93 Some(pending) if e.ledger().sequence() <= pending.live_until_ledger => true,
94 Some(_) => {
95 // Expired entry — clean it up.
96 e.storage().temporary().remove(pending_key);
97 false
98 }
99 None => false,
100 }
101}
102
103/// Completes the role transfer if authorization is provided by the pending role
104/// holder. Pending role holder is retrieved from the storage.
105///
106/// # Arguments
107///
108/// * `e` - Access to the Soroban environment.
109/// * `active_key` - Storage key for the current role holder.
110/// * `pending_key` - Storage key for the pending role holder.
111///
112/// # Errors
113///
114/// * [`RoleTransferError::NoPendingTransfer`] - If there is no pending transfer
115/// to accept.
116/// * [`RoleTransferError::TransferExpired`] - If the current ledger is past the
117/// `live_until_ledger` stored in [`PendingTransfer`]. The deadline is checked
118/// explicitly here, so it is enforced even if the storage entry is still
119/// alive due to the network minimum TTL or a permissionless `extend_ttl`
120/// call.
121pub fn accept_transfer<T, U>(e: &Env, active_key: &T, pending_key: &U) -> Address
122where
123 T: IntoVal<Env, Val>,
124 U: IntoVal<Env, Val>,
125{
126 let pending = e
127 .storage()
128 .temporary()
129 .get::<U, PendingTransfer>(pending_key)
130 .unwrap_or_else(|| panic_with_error!(e, RoleTransferError::NoPendingTransfer));
131
132 if e.ledger().sequence() > pending.live_until_ledger {
133 panic_with_error!(e, RoleTransferError::TransferExpired);
134 }
135
136 pending.address.require_auth();
137
138 e.storage().temporary().remove(pending_key);
139 e.storage().instance().set(active_key, &pending.address);
140
141 pending.address
142}