- Single slot · 2-step grant
+ {proxyAdmins.length} address{proxyAdmins.length === 1 ? '' : 'es'} ·
+ instant grant/revoke
- Only one address can hold this role at a time.
- For production, put a Gnosis Safe multisig here —
- the Safe itself handles N signers / threshold / signer rotation
- internally, so you get “multi-human proxyAdmin”
- without the contract knowing anything about it.
+ Controls implementation registration and default version. Mirrors
+ Role 2's pattern (whitelist + last-admin guard). Use a Safe for
+ production — its internal quorum is the safety net we used to
+ get from the 2-step transfer.
-
+
- {acceptedFlash && (
-
-
- ✓
-
-
- You are now proxyAdmin — the transfer has been
- finalised on-chain.
-
+ {isLoading && proxyAdmins.length === 0 && (
+
- A transfer has been initiated. The pending address must sign{' '}
- acceptProxyAdmin() from
- their wallet to finalise. Until then the current admin keeps all
- powers and can overwrite the pending candidate.
-
- Grants Role 1 only — the target must then call{' '}
- acceptProxyAdmin().
- Fee admin rights (Role 2) are untouched. Use the combined grant
- below if you hold both roles.
-
)}
- {hasBothRoles && }
+
-
- Role 1 controls which logic the proxy delegates to —
- register new implementations, change default version, rename. It{' '}
- cannot touch fees, withdrawals, or the sponsor
- pool; those are Role 2 below.
-
+ {
+ refetch()
+ onTransferred()
+ postTx.start()
+ }}
+ />
)
}
/**
- * Subcomponent for the "grant both roles" convenience flow. Pure renderer
- * over the rotation state machine — no local state of its own.
+ * Collapsible "Advanced" section — closed by default.
+ *
+ * Houses two things:
+ * 1. The grant-both-roles convenience (only enabled when the caller is
+ * a direct admin on BOTH roles — granting both via Safe is just two
+ * separate ProposeViaSafe flows, no point bundling).
+ * 2. The Role 1 vs Role 2 explainer paragraph (operational reference,
+ * not needed in the main flow once the user is familiar).
*/
-function RotateBothRolesForm({
- rotation,
+function AdvancedSection({
+ proxy,
+ canGrantBoth,
+ onWriteDone,
}: {
- rotation: ReturnType
+ proxy: Address
+ canGrantBoth: boolean
+ onWriteDone: () => void
}) {
- const buttonLabel = (() => {
- switch (rotation.stage) {
- case 'grant':
- return rotation.grantPending ? 'Sign Role 2…' : 'Granting Role 2…'
- case 'transfer':
- return rotation.transferPending ? 'Sign Role 1…' : 'Granting Role 1…'
- case 'done':
- return 'Waiting for accept…'
- default:
- return 'Grant both roles'
+ const [open, setOpen] = useState(false)
+
+ return (
+
+
+
+ {open && (
+
+ {canGrantBoth && }
+
+ Role 1 vs Role 2.{' '}
+ Role 1 (proxyAdmins) controls which logic the proxy
+ delegates to — register new implementations, change default
+ version, rename. It cannot touch fees,
+ withdrawals, or the sponsor pool; those are Role 2 below.
+ Both roles use the same whitelist model with last-admin
+ guard. Keep them disjoint if your dev team and ops team
+ are distinct.
+
+
+ )}
+
+ )
+}
+
+/**
+ * Convenience: grant Role 1 + Role 2 to a new address in two back-to-back
+ * transactions. Only available when the caller currently holds both roles
+ * (direct admin on each). For Safe-mediated grants, use the per-role
+ * Propose via Safe buttons — each goes through its own quorum anyway.
+ */
+function GrantBothRolesForm({
+ proxy,
+ onDone,
+}: {
+ proxy: Address
+ onDone: () => void
+}) {
+ const [input, setInput] = useState('')
+ const [stage, setStage] = useState<'idle' | 'role1' | 'role2'>('idle')
+
+ const role1 = useSetProxyAdmin(proxy)
+ const role1Receipt = useWaitForTransactionReceipt({ hash: role1.hash })
+ const role2 = useSetWhitelistedAdmin(proxy)
+ const role2Receipt = useWaitForTransactionReceipt({ hash: role2.hash })
+
+ const inputValid = isAddress(input.trim())
+
+ // Once Role 1 grant mines, fire Role 2 grant.
+ useEffect(() => {
+ if (stage === 'role1' && role1Receipt.isSuccess) {
+ setStage('role2')
+ role2.setAdmin(input.trim() as Address, true).catch(() => setStage('idle'))
+ }
+ }, [stage, role1Receipt.isSuccess])
+
+ // Once Role 2 grant mines, refresh parent.
+ useEffect(() => {
+ if (stage === 'role2' && role2Receipt.isSuccess) {
+ setStage('idle')
+ setInput('')
+ onDone()
+ }
+ }, [stage, role2Receipt.isSuccess])
+
+ const busy = stage !== 'idle'
+
+ function start() {
+ if (!inputValid) return
+ setStage('role1')
+ role1.setProxyAdmin(input.trim() as Address, true).catch(() => setStage('idle'))
+ }
+
+ const label = (() => {
+ if (stage === 'role1') {
+ return role1.isPending ? 'Sign Role 1…' : 'Granting Role 1…'
+ }
+ if (stage === 'role2') {
+ return role2.isPending ? 'Sign Role 2…' : 'Granting Role 2…'
}
+ return 'Grant both roles'
})()
return (
@@ -317,79 +439,34 @@ function RotateBothRolesForm({
You currently hold both roles. This runs{' '}
+ setProxyAdmin(new, true){' '}
+ then{' '}
setWhitelistedAdmin(new, true){' '}
- then transferProxyAdmin(new){' '}
- back-to-back (2 signatures). After the new admin calls{' '}
- acceptProxyAdmin(), they
- can revoke you as fee admin from their wallet.
+ back-to-back (2 signatures). Both grants are instant — no
+ 2-step ceremony.
- (optional) Revoke your old fee admin rights from the new
- admin's wallet
-
-
- )}
- {rotation.stage === 'complete' && (
-
-
- ✓ Rotation complete — the new address is now proxyAdmin + fee
- admin. You can optionally revoke yourself as fee admin from the
- new admin's wallet.
-
diff --git a/packages/webapp/src/hooks/useProxyAdminRotation.ts b/packages/webapp/src/hooks/useProxyAdminRotation.ts
deleted file mode 100644
index d119073..0000000
--- a/packages/webapp/src/hooks/useProxyAdminRotation.ts
+++ /dev/null
@@ -1,165 +0,0 @@
-import { useEffect, useRef, useState } from 'react'
-import { isAddress, type Address } from 'viem'
-import { useWaitForTransactionReceipt } from 'wagmi'
-
-import { useSetWhitelistedAdmin } from './useProxy'
-import { useTransferProxyAdmin } from './useVersionedProxy'
-import type { AdminRotateStage } from '../types'
-
-export interface ProxyAdminRotation {
- /** Input address for the target admin. */
- input: string
- setInput: (v: string) => void
- /** Where we are in the 5-state machine. */
- stage: AdminRotateStage
- /** User-facing error from any step of the chain. */
- error: string | null
- /** True while any signature/mining inside the rotation is in flight. */
- busy: boolean
- /** Receipts — exposed so the caller can show step-by-step progress. */
- grantConfirmed: boolean
- transferConfirmed: boolean
- /** Granular flags for per-step button text ("Sign Role 2…" etc). */
- grantPending: boolean
- transferPending: boolean
- /** True when the trimmed input is a valid hex address. */
- isValid: boolean
- /** Start the 2-tx chain. Precondition: isValid. */
- start: () => Promise
- /** Clear all state — run manually once the user acknowledges completion. */
- reset: () => void
-}
-
-/**
- * Encapsulates the "grant both roles" 5-state machine + the 2 writes it
- * drives (setWhitelistedAdmin + transferProxyAdmin) and the external
- * acceptance detection. Keeps UpgradeAuthorityPanel free of state-machine
- * wiring — the panel just renders based on `stage`.
- *
- * Callbacks:
- * - `onWriteSuccess` — fired when EITHER write mines. Callers refetch
- * `useProxyVersions` to pick up the new pending/current admin.
- */
-export function useProxyAdminRotation({
- proxy,
- proxyAdmin,
- account,
- onWriteSuccess,
-}: {
- proxy: Address
- proxyAdmin: Address | undefined
- account: Address | undefined
- onWriteSuccess: () => void
-}): ProxyAdminRotation {
- const {
- setAdmin: grantFeeAdmin,
- hash: grantHash,
- isPending: grantPending,
- reset: resetGrant,
- } = useSetWhitelistedAdmin(proxy)
- const grantReceipt = useWaitForTransactionReceipt({ hash: grantHash })
-
- const {
- transferAdmin,
- hash: transferHash,
- isPending: transferPending,
- reset: resetTransfer,
- } = useTransferProxyAdmin(proxy)
- const transferReceipt = useWaitForTransactionReceipt({ hash: transferHash })
-
- const [input, setInput] = useState('')
- const [stage, setStage] = useState('idle')
- const [error, setError] = useState(null)
-
- function resetAll() {
- setStage('idle')
- setInput('')
- setError(null)
- resetGrant()
- resetTransfer()
- }
-
- // Fire proxyAdmin transfer once the fee-admin grant confirms.
- useEffect(() => {
- if (stage === 'grant' && grantReceipt.isSuccess) {
- resetGrant()
- setStage('transfer')
- transferAdmin(input.trim() as Address).catch((e: any) => {
- setError(e?.message ?? 'transferProxyAdmin failed')
- setStage('idle')
- })
- }
- }, [stage, grantReceipt.isSuccess, resetGrant, input, transferAdmin])
-
- // Transfer mined → move to "done" (awaiting external accept).
- useEffect(() => {
- if (transferReceipt.isSuccess) {
- resetTransfer()
- onWriteSuccess()
- if (stage === 'transfer') setStage('done')
- }
- }, [transferReceipt.isSuccess, resetTransfer, onWriteSuccess, stage])
-
- // Refresh parent reads when grant mines too (not strictly needed for
- // versions, but keeps behaviour symmetric with the panel's onTransferred).
- useEffect(() => {
- if (grantReceipt.isSuccess) onWriteSuccess()
- }, [grantReceipt.isSuccess, onWriteSuccess])
-
- // Detect external acceptance: on-chain proxyAdmin flipped to our target.
- useEffect(() => {
- if (stage !== 'done' || !proxyAdmin || !input) return
- if (proxyAdmin.toLowerCase() === input.trim().toLowerCase()) {
- setStage('complete')
- }
- }, [stage, proxyAdmin, input])
-
- // Fresh wallet = fresh rotation form. Previous wallet's in-flight state
- // would be confusing for the new principal.
- const prevAccount = useRef(account)
- useEffect(() => {
- if (prevAccount.current !== account) {
- resetAll()
- prevAccount.current = account
- }
- // `resetAll` is intentionally omitted — it's stable for our purposes
- // and listing it would require a useCallback dance for no gain.
-
- }, [account])
-
- const isValid = isAddress(input.trim())
-
- async function start() {
- setError(null)
- setStage('grant')
- try {
- await grantFeeAdmin(input.trim() as Address, true)
- } catch (e: any) {
- setError(e?.message ?? 'setWhitelistedAdmin failed')
- setStage('idle')
- }
- }
-
- const busy =
- grantPending ||
- grantReceipt.isLoading ||
- transferPending ||
- transferReceipt.isLoading ||
- stage === 'grant' ||
- stage === 'transfer'
-
- return {
- input,
- setInput,
- stage,
- error,
- busy,
- grantConfirmed: grantReceipt.isSuccess,
- transferConfirmed: transferReceipt.isSuccess,
- grantPending,
- transferPending,
- isValid,
- start,
- reset: resetAll,
- }
-}
diff --git a/packages/webapp/src/hooks/useProxyRoles.ts b/packages/webapp/src/hooks/useProxyRoles.ts
index 9b6180c..fd4666f 100644
--- a/packages/webapp/src/hooks/useProxyRoles.ts
+++ b/packages/webapp/src/hooks/useProxyRoles.ts
@@ -1,9 +1,12 @@
+import { useReadContract } from 'wagmi'
import type { Address } from 'viem'
+import { IntuitionVersionedFeeProxyABI } from '@intuition-fee-proxy/sdk'
+
export interface ProxyRoles {
/** True when the connected wallet is a whitelisted fee admin (Role 2). */
isFeeAdmin: boolean
- /** True when the connected wallet is the on-chain proxyAdmin (Role 1). */
+ /** True when the connected wallet is in the proxyAdmins whitelist (Role 1). */
isProxyAdmin: boolean
/** True when the connected wallet holds both roles simultaneously. */
hasBothRoles: boolean
@@ -12,21 +15,30 @@ export interface ProxyRoles {
}
/**
- * Derives the four role booleans from `account` + `proxyAdmin` + a precomputed
- * `isFeeAdmin`. Pure — no side effects, no wagmi calls.
+ * Derives the four role booleans from `account` + on-chain
+ * `isProxyAdmin(account)` lookup + a precomputed `isFeeAdmin`.
+ *
+ * Role 1 became a whitelist (post 2-step retirement), so a single
+ * `proxyAdmin` address comparison is no longer enough — we hit the
+ * contract's `isProxyAdmin(addr)` view.
*/
export function useProxyRoles({
+ proxy,
account,
- proxyAdmin,
isFeeAdmin,
}: {
+ proxy: Address | undefined
account: Address | undefined
- proxyAdmin: Address | undefined
isFeeAdmin: boolean
}): ProxyRoles {
- const isProxyAdmin = Boolean(
- account && proxyAdmin && account.toLowerCase() === proxyAdmin.toLowerCase(),
- )
+ const result = useReadContract({
+ abi: IntuitionVersionedFeeProxyABI as any,
+ address: proxy,
+ functionName: 'isProxyAdmin',
+ args: account ? [account] : undefined,
+ query: { enabled: Boolean(proxy && account) },
+ })
+ const isProxyAdmin = Boolean(result.data)
const hasBothRoles = isProxyAdmin && isFeeAdmin
const isViewer = !isFeeAdmin && !isProxyAdmin
diff --git a/packages/webapp/src/hooks/useVersionedProxy.ts b/packages/webapp/src/hooks/useVersionedProxy.ts
index 4a8fed1..9937268 100644
--- a/packages/webapp/src/hooks/useVersionedProxy.ts
+++ b/packages/webapp/src/hooks/useVersionedProxy.ts
@@ -1,5 +1,18 @@
-import { useReadContract, useReadContracts, useWriteContract } from 'wagmi'
-import { hexToString, stringToHex, type Address, type Hex } from 'viem'
+import { useEffect, useState } from 'react'
+import {
+ useBlockNumber,
+ usePublicClient,
+ useReadContract,
+ useReadContracts,
+ useWriteContract,
+} from 'wagmi'
+import {
+ getAddress,
+ hexToString,
+ stringToHex,
+ type Address,
+ type Hex,
+} from 'viem'
import { IntuitionVersionedFeeProxyABI } from '@intuition-fee-proxy/sdk'
@@ -10,15 +23,13 @@ export function useProxyVersions(proxy: Address | undefined) {
contracts: [
{ abi, address: proxy, functionName: 'getVersions' },
{ abi, address: proxy, functionName: 'getDefaultVersion' },
- { abi, address: proxy, functionName: 'proxyAdmin' },
- { abi, address: proxy, functionName: 'pendingProxyAdmin' },
+ { abi, address: proxy, functionName: 'proxyAdminCount' },
],
allowFailure: false,
query: {
enabled: Boolean(proxy),
- // Auto-poll so `proxyAdmin` / `pendingProxyAdmin` reflect
- // acceptance that happens from another wallet or tab without
- // forcing the user to refresh.
+ // Auto-poll so the admin count reflects grants/revokes happening
+ // from another wallet or tab without forcing the user to refresh.
refetchInterval: 10_000,
},
})
@@ -27,15 +38,14 @@ export function useProxyVersions(proxy: Address | undefined) {
...result,
versions: (result.data?.[0] as Hex[] | undefined) ?? [],
defaultVersion: result.data?.[1] as Hex | undefined,
- proxyAdmin: result.data?.[2] as Address | undefined,
- pendingProxyAdmin: result.data?.[3] as Address | undefined,
+ proxyAdminCount: (result.data?.[2] as bigint | undefined) ?? 0n,
}
}
/**
* Cheap 1-read hook for pages that only need the currently-active version
- * label (Explore card, etc.). Avoids the 3-read overhead of
- * `useProxyVersions` when the versions list / proxyAdmin aren't needed.
+ * label (Explore card, etc.). Avoids the overhead of `useProxyVersions`
+ * when the versions list / admin count aren't needed.
*
* Decodes the bytes32 to a human-readable label ("v2.0.0"). Empty string
* if the proxy has no default set yet (shouldn't happen — Factory always
@@ -108,44 +118,130 @@ export function useSetDefaultVersion(proxy: Address | undefined) {
}
/**
- * Step 1 of the 2-step proxy-admin transfer. Only callable by the current
- * `proxyAdmin`. Sets `pendingProxyAdmin = newAdmin`; the target must then
- * call `acceptProxyAdmin()` from their own wallet to finalise. Passing a
- * wrong address is recoverable — just call again with the correct one.
+ * Grant or revoke the Role 1 (proxyAdmin) whitelist for an address.
+ * Mirrors the Role 2 `setWhitelistedAdmin` pattern — instant, no 2-step
+ * ceremony. The contract enforces:
+ * - idempotent reject (revert if status already matches)
+ * - last-admin guard (revert if revoke would empty the whitelist)
*/
-export function useTransferProxyAdmin(proxy: Address | undefined) {
+export function useSetProxyAdmin(proxy: Address | undefined) {
const { writeContractAsync, data, isPending, error, reset } = useWriteContract()
- function transferAdmin(newAdmin: Address) {
+ function setProxyAdmin(admin: Address, status: boolean) {
if (!proxy) throw new Error('Proxy address missing')
return writeContractAsync({
abi,
address: proxy,
- functionName: 'transferProxyAdmin',
- args: [newAdmin],
+ functionName: 'setProxyAdmin',
+ args: [admin, status],
})
}
- return { transferAdmin, hash: data, isPending, error, reset }
+ return { setProxyAdmin, hash: data, isPending, error, reset }
}
/**
- * Step 2 of the 2-step proxy-admin transfer. Must be called by the address
- * currently set as `pendingProxyAdmin`. Promotes caller to `proxyAdmin`.
+ * Reconstruct the current proxyAdmin whitelist from the on-chain
+ * `ProxyAdminGranted` / `ProxyAdminRevoked` event log. Mirrors the
+ * Role 2 `useAdmins` pattern — the contract doesn't expose a getter
+ * for the full list, so we reduce events.
*/
-export function useAcceptProxyAdmin(proxy: Address | undefined) {
- const { writeContractAsync, data, isPending, error, reset } = useWriteContract()
+export function useProxyAdmins(proxy: Address | undefined) {
+ const publicClient = usePublicClient()
+ const { data: currentBlock } = useBlockNumber({ watch: true })
+ const [admins, setAdmins] = useState([])
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [refreshKey, setRefreshKey] = useState(0)
- function acceptAdmin() {
- if (!proxy) throw new Error('Proxy address missing')
- return writeContractAsync({
- abi,
- address: proxy,
- functionName: 'acceptProxyAdmin',
- })
- }
+ useEffect(() => {
+ if (!publicClient || !proxy || !currentBlock) return
+ let cancelled = false
+ setIsLoading(true)
+ setError(null)
+ Promise.all([
+ publicClient.getLogs({
+ address: proxy,
+ event: {
+ type: 'event',
+ name: 'ProxyAdminGranted',
+ inputs: [{ type: 'address', name: 'admin', indexed: true }],
+ },
+ fromBlock: 0n,
+ toBlock: currentBlock,
+ }),
+ publicClient.getLogs({
+ address: proxy,
+ event: {
+ type: 'event',
+ name: 'ProxyAdminRevoked',
+ inputs: [{ type: 'address', name: 'admin', indexed: true }],
+ },
+ fromBlock: 0n,
+ toBlock: currentBlock,
+ }),
+ ])
+ .then(([grants, revokes]) => {
+ if (cancelled) return
+ type LogShape = { args: { admin: Address }; blockNumber: bigint; logIndex: number }
+ // Order all events by (block, logIndex) and replay them.
+ const events: Array<{ admin: Address; granted: boolean; bn: bigint; li: number }> =
+ [
+ ...grants.map((l) => {
+ const x = l as unknown as LogShape
+ return {
+ admin: x.args.admin,
+ granted: true,
+ bn: x.blockNumber,
+ li: x.logIndex,
+ }
+ }),
+ ...revokes.map((l) => {
+ const x = l as unknown as LogShape
+ return {
+ admin: x.args.admin,
+ granted: false,
+ bn: x.blockNumber,
+ li: x.logIndex,
+ }
+ }),
+ ].sort((a, b) => (a.bn === b.bn ? a.li - b.li : a.bn < b.bn ? -1 : 1))
+
+ const set = new Set()
+ for (const e of events) {
+ const a = getAddress(e.admin)
+ if (e.granted) set.add(a)
+ else set.delete(a)
+ }
+ setAdmins(Array.from(set) as Address[])
+ setIsLoading(false)
+ })
+ .catch((e) => {
+ if (cancelled) return
+ setError(e as Error)
+ setIsLoading(false)
+ })
+ return () => {
+ cancelled = true
+ }
+ }, [publicClient, proxy, currentBlock ? Number(currentBlock) : 0, refreshKey])
- return { acceptAdmin, hash: data, isPending, error, reset }
+ return { admins, isLoading, error, refetch: () => setRefreshKey((k) => k + 1) }
+}
+
+/** Read whether a given address is currently a proxyAdmin. */
+export function useIsProxyAdmin(
+ proxy: Address | undefined,
+ candidate: Address | undefined,
+) {
+ const result = useReadContract({
+ abi,
+ address: proxy,
+ functionName: 'isProxyAdmin',
+ args: candidate ? [candidate] : undefined,
+ query: { enabled: Boolean(proxy && candidate) },
+ })
+ return { ...result, isProxyAdmin: Boolean(result.data) }
}
/** Read the proxy's human-readable name (bytes32, decoded to string). */
diff --git a/packages/webapp/src/pages/Docs.tsx b/packages/webapp/src/pages/Docs.tsx
index d53a8e3..3d84096 100644
--- a/packages/webapp/src/pages/Docs.tsx
+++ b/packages/webapp/src/pages/Docs.tsx
@@ -504,8 +504,8 @@ function Architecture() {
}
/>
-
Proxy admin (single address / Safe)
+
Proxy admins (whitelisted)
- Both admin roles are rotatable, but the mechanics differ on
- purpose — the slower path covers the higher-impact role.
+ Both admin roles are whitelists with the same shape — N addresses,
+ instant grant/revoke, last-admin guard. The 2-step ceremony Role
+ 1 used to have was retired in favour of the multi-admin model
+ (any current admin can grant a replacement, then the old admin
+ revokes itself).
- Current admin calls transferProxyAdmin(newAdmin),
- which only sets pendingProxyAdmin — no powers
- move. The target must then sign{' '}
- acceptProxyAdmin() from their own wallet to
- finalise. Until then the outgoing admin keeps full powers
- and can overwrite the pending candidate.
+ Whitelist of upgrade-authority addresses.{' '}
+ setProxyAdmin(addr, true/false) grants or
+ revokes in a single tx. Any current proxyAdmin can mutate
+ the list. The contract reverts if status already matches
+ and refuses to revoke the last remaining admin.
>
}
/>
@@ -986,10 +988,10 @@ function AdminRotation() {
term="Role 2 — fee admins (instant, N addresses)"
desc={
<>
- Whitelist-style. setWhitelistedAdmin(addr, true/false){' '}
- adds or removes in a single tx. Any fee admin can grant or
- revoke any other — except the last one cannot self-revoke.
- Multiple fee admins can coexist; all share the same powers.
+ Same shape as Role 1, different surface.{' '}
+ setWhitelistedAdmin(addr, true/false) mutates
+ the fee-admin whitelist. Any fee admin can grant or revoke
+ any other — except the last one cannot self-revoke.
>
}
/>
@@ -997,15 +999,13 @@ function AdminRotation() {
term="Convenience — Grant both roles"
desc={
<>
- When a single wallet holds both roles and wants to hand the
- whole proxy off, the webapp exposes a combined flow that
- fires setWhitelistedAdmin(new, true) then{' '}
- transferProxyAdmin(new) back-to-back (2 sigs,
- 1 click). The target still has to{' '}
- acceptProxyAdmin() from their side; acceptance
- is auto-detected via the on-chain state poll. The outgoing
- admin optionally revokes themselves as fee admin from the
- new owner's wallet afterwards.
+ When a single wallet holds both roles and wants to hand
+ the whole proxy off, the webapp exposes a combined flow
+ that fires setProxyAdmin(new, true) then{' '}
+ setWhitelistedAdmin(new, true) back-to-back
+ (2 sigs, 1 click). Both grants are instant. The outgoing
+ admin optionally revokes itself from each whitelist
+ afterwards.
>
}
/>
diff --git a/packages/webapp/src/pages/ProxyDetail.tsx b/packages/webapp/src/pages/ProxyDetail.tsx
index 6fcd16b..4f67531 100644
--- a/packages/webapp/src/pages/ProxyDetail.tsx
+++ b/packages/webapp/src/pages/ProxyDetail.tsx
@@ -59,8 +59,6 @@ function ProxyDetail({ proxy }: { proxy: Address }) {
const {
versions,
defaultVersion,
- proxyAdmin,
- pendingProxyAdmin,
refetch: refetchVersions,
isFetching: isVersionsFetching,
} = useProxyVersions(proxy)
@@ -78,8 +76,8 @@ function ProxyDetail({ proxy }: { proxy: Address }) {
const family: ProxyFamily = channel === 'sponsored' ? 'sponsored' : 'standard'
const { isProxyAdmin, isViewer } = useProxyRoles({
+ proxy,
account,
- proxyAdmin,
isFeeAdmin,
})
@@ -173,8 +171,6 @@ function ProxyDetail({ proxy }: { proxy: Address }) {
{tab === 'admins' && (