From ee053cfe042c96372641e3f897085739d889858a Mon Sep 17 00:00:00 2001 From: Tobiloba0 Date: Thu, 30 Jul 2026 01:08:49 +0100 Subject: [PATCH] Add vesting pending admin visibility --- contracts/vesting/src/lib.rs | 39 ++++++++++++++ frontend/app/claim/ClaimVesting.tsx | 29 ++++++++-- .../[contractId]/components/AdminPanel.tsx | 35 +++--------- frontend/components/PendingAdminBanner.tsx | 54 +++++++++++++++++++ frontend/lib/vesting.ts | 29 ++++++++++ 5 files changed, 155 insertions(+), 31 deletions(-) create mode 100644 frontend/components/PendingAdminBanner.tsx diff --git a/contracts/vesting/src/lib.rs b/contracts/vesting/src/lib.rs index 8f007fd5..94151806 100644 --- a/contracts/vesting/src/lib.rs +++ b/contracts/vesting/src/lib.rs @@ -121,6 +121,12 @@ impl VestingContract { env.events().publish((symbol_short!("acc_adm"),), pending); } + /// Cancel a proposed admin transfer. Must be called by the current admin. + pub fn cancel_admin_proposal(env: Env) { + Self::_require_admin(&env); + env.storage().instance().remove(&DataKey::PendingAdmin); + } + /// Create a cliff + linear vesting schedule for `recipient`. /// /// `cliff_ledger` — ledger number when tokens start unlocking. @@ -495,6 +501,12 @@ impl VestingContract { .expect("not initialized") } + /// Returns the address proposed via `propose_admin` that has not yet + /// accepted the role, or `None` when no transfer is in progress. + pub fn pending_admin(env: Env) -> Option
{ + env.storage().instance().get(&DataKey::PendingAdmin) + } + /// Returns the token contract address managed by this vesting contract. pub fn get_token_contract(env: Env) -> Address { env.storage() @@ -913,6 +925,33 @@ mod test { assert!(!schedule.revoked); } + #[test] + fn test_pending_admin_getter_and_cancel() { + let env = Env::default(); + env.mock_all_auths(); + + let contract_id = env.register_contract(None, VestingContract); + let client = VestingContractClient::new(&env, &contract_id); + + let admin = Address::generate(&env); + let proposed = Address::generate(&env); + let token = Address::generate(&env); + + client.initialize(&admin, &token); + + assert_eq!(client.pending_admin(), None); + + client.propose_admin(&proposed); + assert_eq!(client.pending_admin(), Some(proposed.clone())); + + client.cancel_admin_proposal(); + assert_eq!(client.pending_admin(), None); + + client.propose_admin(&proposed); + client.accept_admin(); + assert_eq!(client.pending_admin(), None); + } + #[test] fn test_vested_before_cliff() { let env = Env::default(); diff --git a/frontend/app/claim/ClaimVesting.tsx b/frontend/app/claim/ClaimVesting.tsx index e3f714bd..01968f4f 100644 --- a/frontend/app/claim/ClaimVesting.tsx +++ b/frontend/app/claim/ClaimVesting.tsx @@ -2,15 +2,17 @@ import { useCallback, useState } from "react"; import { Button } from "@/components/ui/Button"; +import { PendingAdminBanner } from "@/components/PendingAdminBanner"; import { useToast } from "@/app/providers/ToastProvider"; import { useWallet } from "@/app/hooks/useWallet"; import { + fetchVestingAdminState, fetchScheduleCount, fetchVestingInfo, buildReleaseTx, submitTx, formatTokenAmount, - truncateAddress, + type VestingAdminState, type VestingInfo, } from "@/lib/vesting"; @@ -24,6 +26,7 @@ export function ClaimVesting() { const [contractId, setContractId] = useState(""); // One VestingInfo entry per schedule index const [schedules, setSchedules] = useState([]); + const [adminState, setAdminState] = useState(null); const [loading, setLoading] = useState(false); // Track which schedule index is currently releasing const [releasingIndex, setReleasingIndex] = useState(null); @@ -48,13 +51,25 @@ export function ClaimVesting() { setError(null); setSchedules([]); + setAdminState(null); setLoading(true); try { - const count = await fetchScheduleCount(trimmed, publicKey); + const [count, vestingAdmin] = await Promise.all([ + fetchScheduleCount(trimmed, publicKey), + fetchVestingAdminState(trimmed), + ]); + setAdminState(vestingAdmin); if (count === 0) { - setError("No vesting schedule found for your wallet on this contract."); + if ( + vestingAdmin.pendingAdmin === publicKey || + vestingAdmin.admin === publicKey + ) { + setError(null); + } else { + setError("No vesting schedule found for your wallet on this contract."); + } return; } @@ -192,6 +207,14 @@ export function ClaimVesting() { )} + {adminState?.pendingAdmin && ( + + )} + {/* ── One card per schedule ─────────────────────────────────── */} {schedules.map((info, idx) => ( -