feat: Safe-aware webapp panels + Ledger/Trezor signers + feeQuote docs#10
Merged
feat: Safe-aware webapp panels + Ledger/Trezor signers + feeQuote docs#10
Conversation
WithdrawPanel now mirrors the SetFeesPanel pattern: when the proxy's fee-admin set contains a Safe, a "Propose via Safe" button appears next to the direct "Withdraw" button. Click → useSafePropose builds the SafeTx, signs via wagmi, posts to Den STS. Also extracts the proposed/error feedback block into a reusable SafeProposeFeedback component (renders nothing in idle, success card with safeTxHash + Den link, or one-line error). SetFeesPanel and WithdrawPanel both consume it — keeps copy + styling in sync as we diffuse the pattern to the other admin panels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the SetFees / Withdraw pattern but extended to handle the panel's two flows (grant new admin, revoke existing) and the new "Safe owner can propose without being a direct admin" case: - Grant form is now visible to (a) direct admins or (b) anyone when a Safe sits in the admins list — Safe owners who aren't direct admins still need to be able to propose - "Grant" direct button stays gated by connectedIsAdmin - "Propose via Safe" button shown when a Safe is in the admin list - Per-row: Direct "Revoke" gated by direct admin + last-admin guard; "Propose revoke via Safe" shown when Safe present + not last admin (the contract enforces last-admin guard regardless of caller) Single useSafePropose instance reused across grant + every revoke — the feedback block at the bottom shows the latest proposal regardless of which action triggered it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new ops namespace `versionedProxy` covering the Role 1 (proxyAdmin) operations on IntuitionVersionedFeeProxy: - transferProxyAdmin(proxy, newAdmin) — initiates 2-step rotation - acceptProxyAdmin(proxy) — completes from the new owner side - registerVersion(proxy, version, impl) — used by VersionsPanel next - setDefaultVersion(proxy, version) — same UpgradeAuthorityPanel now detects whether proxyAdmin (or the pending proxyAdmin) is itself a Safe via useSafeStatus, and exposes "Propose via Safe" buttons accordingly: - Grant form: visible to direct admin OR when proxyAdmin is a Safe (Safe owners need to propose); Direct "Grant" stays gated by isYou, "Propose via Safe" gated by proxyAdminSafe presence - Pending area: in addition to the direct "Accept proxyAdmin role" for an EOA pending owner, surfaces "Propose acceptProxyAdmin via Safe" when the pending owner is itself a Safe (the standard handoff path when rotating Role 1 to a Safe) Single useSafePropose instance + SafeProposeFeedback at the bottom. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ersion) Reads proxyAdmin from chain via useReadContract + IntuitionVersionedFeeProxyABI (no prop plumbing through ProxyDetail/OverviewTab needed). When the proxyAdmin resolves to a known Safe singleton, surfaces "Propose via Safe" alternatives in each VersionRow: - "Propose register via Safe" on rows with status === 'available' (in parallel to the direct "Promote" button when the user is also the direct proxyAdmin) - "Propose via Safe" on rows with status === 'registered' (set as default), in parallel to the direct "Make default" button Both proposed ops use the new ops.versionedProxy.registerVersion / setDefaultVersion builders, post via api-kit to Den STS, and surface the safeTxHash + Den link via SafeProposeFeedback at the panel level. Bottom-of-panel hint updated: "Registering or promoting versions is proxy-admin only" now switches to "The proxyAdmin is a Safe — actions above open a multisig proposal" when applicable. AdvancedCustomPaste section stays gated by direct isProxyAdmin only — Safe propose for arbitrary paste is non-trivial (input would need to plumb through useSafePropose with a custom AdminOp builder); deferred. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the missing sponsored AdminOp builders to safe-tx and surfaces "Propose via Safe" alternatives on the two admin-gated sponsored panels: src/ops/sponsored.ts (new namespace): - setClaimLimits(proxy, maxPerTx, maxPerWindow, maxVolumePerWindow, windowSec) - reclaimFromPool(proxy, amount, to) Both are gated by onlyWhitelistedAdmin on V2Sponsored. ReclaimFromPoolPanel: same pattern as Withdraw — direct + propose buttons, SafeProposeFeedback at the bottom. ClaimLimitsPanel: same pattern, four-arg builder. FundPoolPanel intentionally untouched — fundPool() is `payable` and NOT admin-gated (anyone can credit the pool). Safe-via-treasury funding would require allowing value > 0n in AdminOp (currently all builders set value: 0n), deferred as a separate refactor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the test gap for the two AdminOp namespaces added during the webapp Safe-aware diffusion: test/unit/ops/versioned-proxy.test.ts (5 assertions): - transferProxyAdmin / acceptProxyAdmin / registerVersion / setDefaultVersion: selector match, ABI round-trip via decodeFunctionData, value == 0n, target == proxy test/unit/ops/sponsored.test.ts (3 assertions): - setClaimLimits encoding (4 uint256 args in canonical order) - reclaimFromPool encoding (amount + recipient) - description includes recipient + amount for log traceability test/integration/rotation-script.test.ts (3 smoke tests via spawnSync): - `bun transferAdminToSafe.ts --help` exits 0 with usage - missing --proxy surfaces a clear "required option" error - --dry-run runs without PROPOSER_PK env (just network revert is OK) Full e2e rotation against a live mock proxy on Anvil fork is deliberately out of scope for this commit — the rotation mechanics (EOA setWhitelistedAdmin + Safe execTransaction) are already covered by direct-sign.test.ts and v2-admin.test.ts. Adding a deployable mock contract would need ~1-2h of solc/forge plumbing for marginal extra coverage; deferred until a real proxy lands on mainnet for true end-to-end validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the stub with a working Ledger over USB implementation.
Deps are declared as `optionalDependencies` so users who don't need
Ledger don't have to install hundreds of MB of native bindings.
src/signers/ledger.ts:
- Dynamic imports @ledgerhq/hw-app-eth + @ledgerhq/hw-transport-node-hid
- Falls back with an actionable "install" hint when deps missing
- Falls back with "is the device unlocked / Ledger Live closed" hint
when the USB transport can't be opened
- toAccount-wrapped Signer with:
* signTypedData via signEIP712Message (full struct on-device,
requires Ledger Eth app v1.10+; bigints serialized as decimal
strings to round-trip JSON.stringify cleanly)
* signMessage via signPersonalMessage
* signTransaction explicitly NOT implemented — the Safe execution
path uses signTypedData; raw tx signing is a different flow
package.json:
- @ledgerhq/hw-app-eth ^6.39.0 + @ledgerhq/hw-transport-node-hid
^6.29.5 in optionalDependencies
- bun install ignores failures here so the rest of the workspace
installs cleanly even if a developer's platform can't build the
native HID bindings
test/unit/signers/factory.test.ts:
- Updated the ledger assertion to match the new "requires optional
deps" error path (since the deps aren't installed by default in CI,
we always hit the dynamic-import catch)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two issues from the initial Ledger commit (c3e3f70): 1. signEIP712Message takes an EIP712Message *object*, not the JSON.stringify-ed string — the previous code threw TS2345 at typecheck. Round-trip through JSON only to coerce bigints to decimal strings (which the SDK requires) but pass the resulting object back to the SDK. 2. TransportNodeHid.create() can hang for the full vitest test timeout (30s) when no Ledger is plugged in and the @LedgerHQ deps are installed. Wrap in a Promise.race with a 3s default timeout (configurable via opts.transportTimeoutMs) so the signer surfaces "cannot open USB transport" quickly instead of stalling. test/unit/signers/factory.test.ts: - Updated assertion to accept either failure path (deps missing OR USB transport timeout) — both are user-actionable. - Pass transportTimeoutMs: 1500 + a 5s test timeout so the test itself stays snappy in CI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
UpgradeAuthorityPanel: collapse "grant both roles" rotation flow into a <details> block (advanced), promote standalone Grant Role 1 to btn-primary, drop ✓ glyphs in step states (Tailwind colors instead), strip verbose intros and trailing role-1 explainer. AdminsPanel / AdminsTab: trim role-2 description and tab intro to one line each. Inline the "prefer a Safe" note next to the Grant Role 2 header. SponsoringTab: replace the 4 Stat cards with a compact ClaimLimitsStrip (single inline row), kills the orphaned 4th cell. Fund + Reclaim now share an equal-height grid (h-full + mt-auto) instead of stacking. FundPoolPanel / ReclaimFromPoolPanel: short one-line intros, remove "Counterpart to Fund pool" eyebrow, drop redundant pool-balance hint (already in PoolHealthBadge), drop unused poolBalance prop. HistoryTab / MetricsTab: remove implementation-detail paragraphs that weren't load-bearing for end users. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Trezor support alongside Ledger so the template covers both
mainstream hardware wallet brands. Same architecture as Ledger:
optional dep + dynamic import + toAccount wrapper.
src/signers/trezor.ts:
- TrezorConnect.init() guarded by a 30s timeout (Trezor Bridge can
be slow to start on cold launch); accepts opts.initTimeoutMs override
- "already initialized" is treated as success (TrezorConnect is a
process-level singleton)
- ethereumGetAddress with showOnTrezor: false to skip the device
prompt during initial address resolution
- ethereumSignTypedData with metamask_v4_compat: true (matches the
EIP-712 v4 layout viem produces for Safe)
- ethereumSignMessage for personal_sign
- signTransaction explicitly NOT implemented (Safe execution uses
signTypedData)
src/signers/factory.ts + index.ts: 'trezor' added to SignerStrategy
union, getSigner dispatch, and public re-exports.
src/cli/commands/{propose,confirm,execute}.ts: 'trezor' added to the
--signer choices array.
package.json: @trezor/connect ^9.4.0 in optionalDependencies.
test/unit/signers/factory.test.ts: trezor strategy assertion mirrors
ledger's (deps missing OR Bridge timeout, both user-actionable).
Pre-flight to actually use it: install Trezor Bridge from
https://trezor.io/start, plug + unlock device, run
bun add @trezor/connect
then `bun safe:tx propose ... --signer trezor`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lets a developer validate the Trezor signer end-to-end without risking a meaningful signature. Three phases, opt-in via flags: bun scripts/test-trezor.ts # Phase A only bun scripts/test-trezor.ts --sign-harmless # A + B bun scripts/test-trezor.ts --sign-typed # A + B + C A. Resolve address (no device prompt, no signature). Proves Bridge + @trezor/connect + USB connection all work. B. personal_sign a timestamped test string. Device displays the string, user confirms physically. Signature is bound to that exact text — cannot be replayed for any tx / transfer / contract call. C. EIP-712 sign with verifyingContract: 0x000…dEaD (intentionally non-existent). Signature is bound to a contract that doesn't exist on any chain — useless if leaked. Validates the SafeTx typed-data flow without producing anything that could be replayed against a real Safe. Each phase prints the next-step hint so the user can ratchet up incrementally. Each device confirmation is preceded by a clear "look at your Trezor — confirm X matches" message so the user knows what to verify on the device screen before pressing OK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous `typeof import('@ledgerhq/...')` / `typeof import('@trezor/...')`
type annotations made tsc fail with TS2307 when the optional deps
weren't installed in node_modules — defeating the whole purpose of
declaring them as optionalDependencies.
Both signers now type the dynamically-imported module surface as
`any` and cast the dynamic import path through `as string` so TS
doesn't try to resolve the module type at compile time. The runtime
shape is still validated implicitly through the SDK's response
checking (`addrRes.success`, `res.payload.signature`, etc.).
This means a contributor (or CI) can typecheck the package without
having Ledger/Trezor SDKs installed. Installing them remains
required to actually USE the signers at runtime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…g → success The deploy page now collapses to a single progress card the moment the user clicks Deploy. Step 1 (proxy deployment) is progressive — signing in wallet → mining (with tx hash exposed) → success (proxy address + copy) — and Step 2 (Intuition atom) renders once the proxy lands. The form, heading and factory warning are hidden during the flow so the user only sees the in-flight state. Errors (wallet rejection or tx revert) naturally drop back to the form with the existing error message below the submit button — no extra state machine needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stops three classes of build artifacts from polluting future commits: - *.tsbuildinfo (incremental tsc cache, regenerated on every build) - vite.config.d.ts + vite.config.js (compiled config emitted by some Vite + tsc combinations) Removes the two tracked tsbuildinfo files via `git rm --cached` so the working tree stops showing them as modified after every build. Pre-existing bun.lock + package.json changes from the user's `bun add @trezor/connect` are deliberately not bundled here — those belong with the Trezor signer story. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The L-02 audit fix (commit 9f9c018) shipped fetchLiveFees + feeCapsExact + feeCapsWithBuffer in the SDK, but integrators had no discoverable example showing how to use them. Without docs, the path of least resistance was passing MAX_FEE_PERCENTAGE / MAX_FIXED_FEE, which fully opts out of the front-run protection that V2's deposit() guards exist for in the first place. New "Call deposit() with front-run protection" section in /docs/integration: - Three-step recipe (snapshot live fees, choose strict vs buffer, splice into deposit args) - Both feeCapsExact and feeCapsWithBuffer shown side-by-side, comment explaining the tradeoff - Computed `value:` line shows the right msg.value for the strict case (amount + pct + fixed) so integrators don't have to figure it out themselves - Callout explicitly warning against hard-coding the bytecode maxima ("opting out of front-run protection") Inserted between "Fund a sponsor pool" and "Canonical versions" so it sits naturally in the writer-side recipes block. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-ups to the Safe admin integration. See branch commits for per-track scope. Skipped #4 WalletConnect + #5 CLI direct-sign as non-critical (deferred until use case emerges).