Skip to main content

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}