Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"contracts:clean": "bun --filter @intuition-fee-proxy/contracts clean",
"contracts:node": "bun --filter @intuition-fee-proxy/contracts node",
"contracts:deploy:local": "bun --filter @intuition-fee-proxy/contracts deploy:local",
"contracts:deploy:fork": "bun --filter @intuition-fee-proxy/contracts deploy:fork",
"contracts:e2e:local": "bun --filter @intuition-fee-proxy/contracts e2e:local",
"contracts:e2e:testnet": "bun --filter @intuition-fee-proxy/contracts e2e:testnet",
"contracts:e2e:sponsored:local": "bun --filter @intuition-fee-proxy/contracts e2e:sponsored:local",
Expand Down
9 changes: 9 additions & 0 deletions packages/contracts/hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,15 @@ const config: HardhatUserConfig = {
url: "http://127.0.0.1:8545",
chainId: 31337,
},
// Anvil fork of Intuition mainnet — used to test Safe-aware webapp
// flows that require the real mainnet state (the Safe at 0xf10D...
// exists in the fork, real proxies don't because none are deployed
// on Intuition mainnet yet).
intuitionFork: {
url: "http://127.0.0.1:8545",
chainId: 1155,
accounts: [LOCAL_DEV_KEY],
},
// Intuition Mainnet
intuition: {
url: process.env.INTUITION_RPC_URL || "https://rpc.intuition.systems",
Expand Down
1 change: 1 addition & 0 deletions packages/contracts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"clean": "hardhat clean",
"node": "hardhat node",
"deploy:local": "hardhat run scripts/deploy.ts --network localhost",
"deploy:fork": "hardhat run scripts/deploy.ts --network intuitionFork",
"deploy:testnet": "hardhat run scripts/deploy.ts --network intuitionTestnet",
"deploy:mainnet": "hardhat run scripts/deploy.ts --network intuition",
"e2e:local": "hardhat run scripts/e2e-validate.ts --network localhost",
Expand Down
Empty file modified packages/safe-tx/bin/safe-propose.ts
100644 → 100755
Empty file.
1 change: 1 addition & 0 deletions packages/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@intuition-fee-proxy/safe-tx": "workspace:*",
"@intuition-fee-proxy/sdk": "workspace:*",
"@rainbow-me/rainbowkit": "^2.1.0",
"@tanstack/react-query": "^5.51.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/webapp/src/components/AdminsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import type { Address } from 'viem'

import { useSafeAdmin } from '../hooks/useSafeAdmin'
import { AdminsPanel } from './AdminsPanel'
import { PendingSafeTxsPanel } from './PendingSafeTxsPanel'
import { UpgradeAuthorityPanel } from './UpgradeAuthorityPanel'

interface Props {
Expand All @@ -22,6 +24,7 @@ export function AdminsTab({
isVersionsFetching,
onWriteDone,
}: Props) {
const { safe: feeAdminSafe } = useSafeAdmin(proxy)
return (
<div className="space-y-6">
<p className="text-xs text-muted leading-relaxed max-w-3xl">
Expand All @@ -40,6 +43,7 @@ export function AdminsTab({
isRefreshing={isVersionsFetching}
/>
<AdminsPanel proxy={proxy} connectedAccount={account} />
{feeAdminSafe && <PendingSafeTxsPanel safe={feeAdminSafe} />}
</div>
)
}
110 changes: 110 additions & 0 deletions packages/webapp/src/components/PendingSafeTxsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { Address } from 'viem'
import { usePendingSafeTxs } from '../hooks/usePendingSafeTxs'
import AddressDisplay from './Address'
import { Spinner } from './Spinner'

interface Props {
safe: Address
}

/**
* Read-only listing of Safe transactions currently waiting on owner
* co-signatures or execution. Each row links to Den's UI for that Safe
* + tx, where owners actually confirm and execute.
*
* Webapp's role here is "you can see what's queued and where to act
* on it" — it deliberately does not duplicate Den's signing surface.
*/
export function PendingSafeTxsPanel({ safe }: Props) {
const { txs, isLoading, error, refetch } = usePendingSafeTxs(safe)

const denBaseUrl = `https://safe.onchainden.com/transactions/queue?safe=int:${safe}`

return (
<section className="card border-l-4 border-l-line-strong space-y-3">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-baseline gap-3">
<span className="text-[10px] font-mono uppercase tracking-widest text-muted">
Safe queue
</span>
<h2 className="font-semibold inline-flex items-baseline gap-2">
Pending Safe transactions
{isLoading && <Spinner ariaLabel="Loading" />}
</h2>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={refetch}
disabled={isLoading}
className="text-[11px] text-muted hover:text-ink transition-colors"
>
Refresh
</button>
<a
href={denBaseUrl}
target="_blank"
rel="noreferrer"
className="text-[11px] text-brand underline decoration-brand/60 hover:decoration-brand"
>
Open in Den ↗
</a>
</div>
</div>
<p className="text-xs text-muted leading-relaxed">
Read-only view of transactions awaiting signatures or execution
on the Safe that admins this proxy. Co-sign and execute happen in
Den UI — click any row to jump there.
</p>

{error && (
<p className="text-sm font-mono text-rose-400">
Failed to load: {error}
</p>
)}

{!isLoading && !error && txs.length === 0 && (
<p className="text-xs text-subtle border-l-2 border-line pl-3">
No pending transactions.
</p>
)}

{txs.length > 0 && (
<ul className="divide-y divide-line rounded-xl border border-line bg-surface overflow-hidden">
{txs.map((tx) => {
const denUrl = `https://safe.onchainden.com/transactions/tx?safe=int:${safe}&id=multisig_${safe}_${tx.contractTransactionHash}`
return (
<li key={tx.contractTransactionHash} className="px-5 py-3 space-y-1">
<div className="flex items-center justify-between gap-3 flex-wrap">
<div className="flex items-center gap-3 min-w-0">
<span className="text-[10px] font-mono uppercase text-subtle tracking-wider border border-line rounded px-1.5 py-0.5">
nonce {tx.nonce}
</span>
<span className="text-xs text-muted">to</span>
<AddressDisplay value={tx.to} variant="short" />
</div>
<div className="flex items-center gap-3">
<span className="text-[11px] text-subtle">
{tx.confirmations.length} confirmation{tx.confirmations.length === 1 ? '' : 's'}
</span>
<a
href={denUrl}
target="_blank"
rel="noreferrer"
className="text-[11px] text-brand underline decoration-brand/60 hover:decoration-brand"
>
Sign in Den ↗
</a>
</div>
</div>
<div className="text-[10px] font-mono text-subtle break-all">
{tx.contractTransactionHash}
</div>
</li>
)
})}
</ul>
)}
</section>
)
}
19 changes: 17 additions & 2 deletions packages/webapp/src/components/ProxyAdminSafeBanner.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Link } from 'react-router-dom'
import type { Address } from 'viem'
import { useChainId } from 'wagmi'
import { useSafeStatus } from '../hooks/useSafeStatus'
Expand Down Expand Up @@ -47,14 +48,28 @@ export function ProxyAdminSafeBanner({ proxyAdmin }: Props) {
if (onMainnet) {
return (
<div className="rounded-lg border border-rose-400/50 bg-rose-400/5 px-4 py-3 text-xs text-rose-300">
<strong>EOA proxyAdmin on mainnet — high risk.</strong> This single key can swap the proxy&apos;s implementation, replacing the entire logic of the contract. A key compromise here means total loss of control. Rotate to a Gnosis Safe before any production use.
<strong>EOA proxyAdmin on mainnet — high risk.</strong> This single key can swap the proxy&apos;s implementation, replacing the entire logic of the contract. A key compromise here means total loss of control. Rotate to a Gnosis Safe before any production use.{' '}
<Link
to="/docs/safe-admin"
className="underline decoration-rose-400/60 hover:decoration-rose-200 font-medium"
>
Read the Safe admin guide
</Link>
.
</div>
)
}

return (
<div className="rounded-lg border border-amber-400/30 bg-amber-400/5 px-4 py-2.5 text-xs text-amber-300">
<strong>EOA proxyAdmin.</strong> Fine for dev / testing. Rotate to a Safe before this proxy goes near mainnet.
<strong>EOA proxyAdmin.</strong> Fine for dev / testing. Rotate to a Safe before this proxy goes near mainnet.{' '}
<Link
to="/docs/safe-admin"
className="underline decoration-amber-400/60 hover:decoration-amber-200"
>
Safe admin guide
</Link>
.
</div>
)
}
80 changes: 77 additions & 3 deletions packages/webapp/src/components/SetFeesPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { useEffect, useState } from 'react'
import { formatEther, parseEther, type Address } from 'viem'
import { useWaitForTransactionReceipt } from 'wagmi'

import { ops } from '@intuition-fee-proxy/safe-tx'
import { useSetFees } from '../hooks/useProxy'
import { useSafeAdmin } from '../hooks/useSafeAdmin'
import { useSafePropose } from '../hooks/useSafePropose'

interface Props {
proxy: Address
Expand All @@ -19,6 +22,9 @@ export function SetFeesPanel({ proxy, currentFixed, currentPct, onDone }: Props)
useSetFees(proxy)
const receipt = useWaitForTransactionReceipt({ hash })

const { safe } = useSafeAdmin(proxy)
const safePropose = useSafePropose({ safeAddress: safe })

useEffect(() => {
if (receipt.isSuccess) onDone()
}, [hash, receipt.isSuccess])
Expand Down Expand Up @@ -47,12 +53,33 @@ export function SetFeesPanel({ proxy, currentFixed, currentPct, onDone }: Props)
}
}

async function onProposeFixed() {
if (!fixedValid || !safe) return
safePropose.reset()
try {
await safePropose.propose(ops.v2Admin.setDepositFixedFee(proxy, parseEther(fixedEth)))
} catch (e) {
console.error(e)
}
}

async function onProposePct() {
if (!pctValid || !safe) return
safePropose.reset()
try {
await safePropose.propose(ops.v2Admin.setDepositPercentageFee(proxy, BigInt(pctBps)))
} catch (e) {
console.error(e)
}
}

return (
<section className="card space-y-4">
<div>
<h2 className="font-semibold">Update fees</h2>
<p className="text-xs text-subtle">
Admin-only. Takes effect immediately.
Admin-only. Direct write takes effect immediately. Safe propose
opens a multisig transaction for owners to co-sign in Den.
</p>
</div>

Expand All @@ -70,11 +97,21 @@ export function SetFeesPanel({ proxy, currentFixed, currentPct, onDone }: Props)
<button
type="button"
onClick={onUpdateFixed}
disabled={!fixedValid || isPending}
disabled={!fixedValid || isPending || safePropose.isProposing}
className="btn-primary w-full mt-1"
>
Update fixed
</button>
{safe && (
<button
type="button"
onClick={onProposeFixed}
disabled={!fixedValid || isPending || safePropose.isProposing}
className="btn-secondary w-full mt-1 text-xs"
>
{safePropose.isProposing ? 'Proposing…' : 'Propose via Safe'}
</button>
)}
</label>

<label className="block space-y-1">
Expand All @@ -91,14 +128,51 @@ export function SetFeesPanel({ proxy, currentFixed, currentPct, onDone }: Props)
<button
type="button"
onClick={onUpdatePct}
disabled={!pctValid || isPending}
disabled={!pctValid || isPending || safePropose.isProposing}
className="btn-primary w-full mt-1"
>
Update percentage
</button>
{safe && (
<button
type="button"
onClick={onProposePct}
disabled={!pctValid || isPending || safePropose.isProposing}
className="btn-secondary w-full mt-1 text-xs"
>
{safePropose.isProposing ? 'Proposing…' : 'Propose via Safe'}
</button>
)}
</label>
</div>

{safePropose.proposed && (
<div className="rounded-md border border-emerald-400/30 bg-emerald-400/5 px-3 py-2 text-xs text-emerald-300 space-y-1">
<div>
<strong>Proposed.</strong> safeTxHash:{' '}
<code className="font-mono break-all">{safePropose.proposed.safeTxHash}</code>
</div>
<div>
Owners can co-sign and execute in{' '}
<a
href={safePropose.proposed.denUrl}
target="_blank"
rel="noreferrer"
className="underline decoration-emerald-400/60 hover:decoration-emerald-200"
>
Den
</a>
.
</div>
</div>
)}

{safePropose.error && (
<p className="text-xs text-rose-400 font-mono">
Safe propose: {safePropose.error}
</p>
)}

{error && (
<p className="text-xs text-rose-400 font-mono">
{error.message.split('\n')[0]}
Expand Down
Loading
Loading