feat(dashboard): import a proposal from JSON when creating one - #2102
feat(dashboard): import a proposal from JSON when creating one#2102brunod-e wants to merge 42 commits into
Conversation
The create proposal form gets an Import JSON button that opens a modal (drawer on mobile, via the shared Modal) with a textarea and a written spec of the expected document, plus a one-click example. Applying the document fills title, discussion URL, description and actions. Parsing is strict on the fields it recognizes since they feed a form that can publish an on-chain transaction, and lenient about the rest, so a saved draft (which carries id/daoId/timestamps) pastes in as-is. Fields the document omits are left untouched, and the import goes through setValue rather than reset so the form stays dirty and the navigation guard stays armed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The modal was carrying a full reference document: a field table, a prose paragraph, and a scrollable example block with a button to load it. That is more than a modal should hold. The format now lives in the textarea placeholder, and the body is just a list of the fields. The reference constant shrinks to a two-action hint and the Use example button is gone. Two fixes found while trimming: the textarea gets a max height so a long paste or a drag on the native resize grip can't push the footer off screen, and the field prose is short enough to survive formatting. A line break right after a <code> element swallows the space that follows it, which had eaten the one in "tokenAddress and". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
🚅 Deployed to the anticapture-pr-2102 environment in anticapture-infra
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c8f5cb058d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
🎨 UI Review
Create Proposal — Import JSON rowLayout
Touch target
Import JSON modal
Question for author
No Figma-confirmed section this time — see the caveat above; there is nothing to cross-check the implementation against yet for this specific feature. Generated by Claude Code |
|
🔍 Vercel preview: https://anticapture-qsyu0nmnc-ful.vercel.app |
A custom action imported with calldata "transfer(...)", or any other non-hex string, was accepted. ProposalFormSchema then treats the action as valid, since it only requires functionName when calldata is empty, so the form becomes publishable and encodeActions casts the string straight to Hex. The paste failed only once the user was already preparing the on-chain write. Apply the same isHex plus even-length rule the custom-action modal uses, so the bad value is rejected while the import modal is still open and points at actions[n].calldata. Also validate `value` the same way: encodeActions runs BigInt() on it, which throws on anything that isn't a plain integer, "1e18" being the easy mistake. This caught the modal placeholder, which writes "0x…" where real values go and so is not a parseable document. Its test now guards the hint's structure rather than parsing it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b0de3eb230
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The ABI path only checked that an abi was present. A functionName absent from that abi, or a function imported with fewer args than it takes, sailed through into a form ProposalFormSchema calls publishable. The throw then landed in encodeActions during publish: the function lookup raises outright, and a short args array reaches viem, which rejects the arity. Resolve the function and check the call at import time. Lookup mirrors encodeActions (full signature or bare name) so the two never disagree on which overload was meant, arity is compared against fn.inputs, and each argument goes through the same argsToTrees plus isArgComplete pair the custom-action modal uses to release its Confirm button. The checks are skipped when raw calldata is present, matching the precedence encodeActions already applies. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cb77457f49
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Three ways a pasted action cleared the import and broke later.
ERC-20 decimals were taken on faith. encodeActions hands them to
parseUnits, so importing 1 USDC at decimals 18 encoded 1e18 base units
while the row still read 1: a million times the intended payment, with
nothing on screen showing the difference. decimals is now optional and
read off the token contract, the way the add-transfer modal already
does; a supplied value is treated as an assertion and rejected when the
contract disagrees. Only ERC-20 transfers need the round trip, so other
documents still import with no RPC, and a failed read blocks the import
rather than falling back to the pasted number.
A malformed ABI entry crashed the dialog. parseAbiStrict only checks for
a string type, so a { "type": "function" } with no name or inputs
survived it and reached toFunctionSignature, which throws out of
safeParse and takes the modal down. Such entries are now reported as a
paste error, and the formatter call is guarded.
Six fields the form validates went unchecked on the way in: recipient,
amount, tokenAddress and contractAddress. Because action rows render no
field errors, anything the import accepted but ProposalFormSchema
rejected showed up only as a Publish button that never enabled. The
import now reuses the form's own schemas, exported for the purpose so
the two cannot drift.
Also accept a hex `value`, which BigInt() handles and the previous
digits-only rule wrongly refused.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fd7f6fd5b4
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
JSON numbers past 2^53 arrive already rounded. JSON.parse turns
1000000000000000001 into 1000000000000000000 before any validation runs,
and the result looks like a clean integer, so a wei arg or value encoded
a different number than the document said. Unquoted numbers are now
refused above MAX_SAFE_INTEGER, pointing at the quoted form, which
survives intact. This covers args and amount as well as value; args is
the likeliest place to write a wei figure unquoted.
An ABI input of {} passed the well-formed check, which only looked at
whether inputs was an array. parseArrayType calls .match on the type, so
the argument walk threw a TypeError out of safeParse and unmounted the
dialog. Reached through the bare function name, which skips the
signature formatter that was already guarded. Parameters are now checked
for a string type, recursing through tuple components.
Malformed composite args were accepted as empty. storageToArg turns
unparseable JSON into an empty container, which isArgComplete calls a
complete dynamic array, and encodeActions then re-parses the original
text at publish and throws. Composite args are now required to parse as
JSON arrays before the completeness walk.
A decimals lookup outlived the modal. Cancel, Escape and the close
button all stay live while it runs, and the resolved value still called
onImport, so a cancelled paste, or the previous one after a reopen,
could overwrite the form. Each attempt takes a ticket and a stale result
is dropped; dismissing through Escape or X now routes through the same
close path as Cancel.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8e47afaf69
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Two gaps the previous pass left one level down, both reproduced as failing tests before the fix. A number nested inside a composite arg hit the same rounding as a bare one. Checking that "[1000000000000000001]" parses to an array said nothing about its leaves, and JSON.parse had already flattened the last digit, so publish re-parsed the same text and encoded the rounded figure. Composite values are now walked for numbers past MAX_SAFE_INTEGER, pointing at the quoted form the way scalars already did. A tuple input with no components passed as well formed. The completeness walk reads components as an empty list, so a struct with no fields calls any "[]" complete, while encodeActions refuses the same parameter outright and throws mid-publish. Tuples, including tuple arrays at any depth, now have to declare components, and those components are checked recursively like any other parameter. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8535f72237
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Two things, both in the conversion layer between a stored arg and calldata. The safe conversion is now the default one. `storageToArg` and `argsToTrees` are the strict pair that refuses anything the ABI cannot hold, and the forgiving pair is `storageToArgForDisplay` and `argsToTreesForDisplay`, named for the only thing that answer is good for. Every bug in this file came from a caller reaching for the forgiving variant to decide whether an action was complete, so the plain name is the one that fails closed and the other has to be asked for. The custom action modal now draws its inputs from the display conversion and gates its Add button and calldata preview on the strict one, which it was not doing. And the utils are smaller. The JSON scanner loses its class and tracks only numbers, which are the only values the parser rejects for being unquoted, so it still reports the line and the digits as written for a third of the size. importIssueCopy was three string formatters with one consumer, so it lives in that consumer now. importedArgs loses the per-failure object literals to three helpers, without changing a message. Comments across all of them are cut back to invariants, from up to 75 percent of a file down to the repo's own range. scanJsonSource 316 to 153, plus 200 to 110 in its test importedArgs 299 to 215 importHandoff 76 to 49 address 29 to 11 importIssueCopy 149 to 0, deleted with its test No behaviour change beyond the modal gate: 463 tests pass, and the three that asserted forgiveness now ask for it by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`argsToTrees` mapped over the ABI's inputs alone, so a missing arg read as "" and an extra one was dropped. An action whose args and function drifted apart in a draft that never passed `ProposalFormSchema` therefore published calldata its row never described: `setMessage(string)` with `args: []` encoded an empty string instead of failing. Same fail-closed reasoning as the tuple arity check in `coerceStrict`, one level up. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The comment described `storageToArg` as forgiving and referred to `argsToTreesStrict`; both names moved when the strict variants became the plain ones. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b7e4d287c
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
An import stashes before it navigates, because a sign-in can leave the page and the values have to survive it. Signed out, `goToNewProposal` only opened the sign-in modal, and dismissing it cleared the redirect while leaving the stash: the next "Create new" for that DAO in the tab drained it and filled a form that was asked to be blank, announcing an import the author had walked away from and arming the dirty flag NavigationGuard reads. `openLogin` now takes an `onDismiss`, so whoever staged for the post-sign-in route owns undoing it. Deferring the stash instead was not available: magic link and OAuth unload the page, which is why the handoff is sessionStorage rather than state. The undo is not gated on there being no session. `LoginProvider` reads `useSession` while its callers gate on `useAuthSession`, and the two disagree for a stale wallet session — which an open modal actively keeps alive, since `authFlowActive` stands the sign-out effect down. Gating would have skipped the undo in exactly the case the caller was signed out from. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b266d1fbe8
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The magic-link sign-in mails an absolute URL back to the creation form, and following it from a mail client opens a new browsing context. sessionStorage is per-context, so `takeImportedProposal` found nothing there: the author signed in successfully and landed on a blank form with their document gone and nothing said. Surviving a sign-in that leaves the page is the whole job of this module, and only cross-context storage does it. The comment above `keyFor` had justified sessionStorage over the URL, which is a question about size, and that was mistaken for having settled the question about scope. localStorage costs what sessionStorage was buying: any tab can drain the stash, and it outlives the tab that wrote it. So the record now carries its write time and is refused after an hour — long enough for a mail round trip, short enough not to fill a form in a later session that nobody asked to fill — and is cleared whether it was accepted, expired, or malformed. Two tabs importing for the same DAO share the key and the last write wins, which is accepted and written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 96aecf5984
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
# Conflicts: # apps/dashboard/features/create-proposal/schema.ts
…e style AGENTS.md asks for arrow functions; `createMemoryStorage` in the import handoff test was the one `function` declaration this PR adds, so it is converted. The other declarations in these files come from dev untouched and are left alone. Comment blocks were the bigger drift: 27 of them ran past 8 lines, against a ceiling of 3-8 in dev's own version of the same files. All 26 that this PR introduced are cut to the essential reason; the 16-line block in LoginProvider is dev's and stays as it is. Comments and one declaration keyword only — no behaviour change. 487 tests, typecheck and lint all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 21ceb96949
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
The JSON import restated rules the form already owned, and the parameter
walk they both need was written out at every call site. Three passes
answered "can this argument be encoded as what it claims to be" —
isArgComplete for the editor, tupleArityError for a better arity message,
and the import's own translation — and they disagreed on arity wording and
on whether an empty dynamic array counted as filled in.
- argIssues is now that single answer, and isArgComplete is it as a
boolean. tupleArityError and the composite pre-pass are gone.
- customActionIssues moves from ProposalFormSchema's superRefine onto
ProposalActionSchema, so the import dialog reaches it before the form
exists. A paste naming a function that isn't in its ABI is refused in
the dialog instead of turning up later as a Publish button that will
not enable.
- The import's parallel action schemas are derived from the form's
members (PendingProposalActionSchema) rather than kept in step by hand.
- shapeOf replaces the array/tuple/leaf preamble that ten walkers each
spelled out, and arityError is the one place a container's declared
size is compared with what it holds.
- Four identical {path, message} types collapse to one Issue.
The Solidity type grammar moves to abitype's zod schemas, which viem is
already built on: uint257, uint255, bytes33, bytes0, fixed128x18 and a
tuple with no components were all being caught by hand-written regexes.
Readability stays a separate, looser check, because judging it with the
grammar drops the offending function from the ABI list and loses the
message that names the bad type. `function` stays manual — legal ABI that
viem's encoder refuses.
Numbers no longer have to arrive quoted. The document is read through
jsonc-parser, so 1000000000000000001 and 1.000000000000000001 survive as
the text they were written as instead of as rounded doubles, which is what
the quoting rule existed to work around. That also retires the
hand-written source scanner: line numbers now come from the syntax tree,
for every path rather than only for figures.
One consequence worth naming: a number where text belongs is now read as
that text, so {"title": 42} imports a title of "42" rather than being
refused.
Both already resolved in the tree — abitype as viem's own dependency, jsonc-parser transitively — so this only declares them.
987 comment lines across the feature, 19% of everything the branch added. Most of it narrated the code beside it or recorded what the code used to be, which is review context with a shelf life, not something the next reader needs. What stays is the set of things that cost a bug or would cause one: a pasted `decimals` reaching `parseUnits`, raw calldata winning over `functionName` in the encoder, the modal's ticket against a stale decimals lookup, `storageToArg` vs the forgiving variant, the checksum-agnostic resolver, `function` being legal ABI that viem refuses, and why the document is not read with `JSON.parse`. One or two lines each, on the line they guard. Comments now 164 of 4,303 added lines (3%): 144 in production, 20 in tests, where a name already says what a case is for. No code changed — 521 tests pass, lint clean, tsc identical to baseline.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 906fc445aa
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| router.push(`${basePath}/proposals/new`); | ||
| openLogin({ | ||
| redirectTo: target, | ||
| onDismiss: () => clearImportedProposal(daoId), |
There was a problem hiding this comment.
Keep the import stash while email sign-in is pending
When an unauthenticated import uses the Email path, LoginModal moves to its “sent” screen after sending the magic link but no session exists yet; if the author closes that screen before opening the emailed link, LoginProvider treats it as a dismiss and this callback deletes the localStorage handoff. The magic-link callback then reaches /proposals/new with nothing for takeImportedProposal() to consume, so preserve the stash after a link has been sent or clear it only on true cancellation.
Useful? React with 👍 / 👎.
What
Adds an Import JSON button to the create proposal form. It opens a modal (drawer on mobile, via the shared
Modal) where you paste a proposal document, and applying it fills the title, discussion URL, description, and actions.Why
Proposals are often drafted outside the app (forum posts, scripts, another tool's export). Re-entering four fields and every action by hand is slow and error prone, especially for multi-action proposals where a mistyped address or amount is not obvious in the form.
Format
Documented in the modal itself: the textarea placeholder shows the shape, and the body lists the fields.
{ "title": "Proposal title", "discussionUrl": "https://discuss…", "body": "## Synopsis\n\nMarkdown description.", "actions": [ { "type": "eth-transfer", "recipient": "0x… or ENS", "amount": "1.5" }, { "type": "erc20-transfer", "recipient": "0x…", "tokenAddress": "0x…", "amount": "250000", "decimals": 6 }, { "type": "custom", "contractAddress": "0x…", "abi": [], "functionName": "approve(address,uint256)", "args": ["0x…", "1"] }, { "type": "custom", "contractAddress": "0x…", "calldata": "0x…" } ] }Decisions worth a look during review:
{"title": "x"}will not wipe a body you already wrote.id,daoId, timestamps) pastes in unchanged.decimalsis required onerc20-transfer. Defaulting to 18 would silently scale a USDC transfer by 12 orders of magnitude, and there is no async chain read available at parse time.customaction needs eitherfunctionName+abi, or rawcalldata, mirroring the cross-field rule already inProposalFormSchema.setValue, notform.reset, so the form stays dirty and the unsaved-changesNavigationGuardstays armed.Validation is strict on the fields it recognizes, since they feed a form that can publish an on-chain transaction, and lenient about everything else. Errors carry the path:
actions[0].decimals: Required.Changes
utils/parseProposalJson.tsutils/parseProposalJson.test.tscomponents/modals/ImportJsonModal.tsxcomponents/ProposalCreationForm.tsxconstants.tsPROPOSAL_JSON_PLACEHOLDERThe second commit slims the modal down after a first pass that carried too much reference material inline.
Verification
pnpm dashboard typecheckandpnpm dashboard lintclean/ens/proposals/new: a four-action document filled every field and produced the right action cards (ETH transfer, ERC-20 to an ENS name, custom-with-ABI, custom-with-calldata). Bad input showedactions[0].decimals: Requiredwith the modal open and the form untouched. Checked the drawer at 375px.Two things caught while verifying, both fixed here: the textarea needed a max height so a long paste or a drag on the native resize grip could not push the footer off screen, and a line break right after a
<code>element was swallowing the space after it in the field list.🤖 Generated with Claude Code