11"use client" ;
22
33import { useEffect , useState , useTransition } from "react" ;
4+ import Image from "next/image" ;
45import { AlertTriangle , CheckCircle , ExternalLink , Info , Loader2 } from "lucide-react" ;
56import { useFormContext , useWatch } from "react-hook-form" ;
6- import { getContract , prepareContractCall , readContract , sendTransaction , waitForReceipt } from "thirdweb" ;
7- import { base } from "thirdweb/chains" ;
87import { toast } from "sonner" ;
9- import { useUserAddress } from "@/hooks/use-user-address" ;
10- import { useWriteAccount } from "@/hooks/use-write-account" ;
11- import { getThirdwebClient } from "@/lib/thirdweb" ;
12- import { ensureOnChain } from "@/lib/thirdweb-tx" ;
13- import Image from "next/image" ;
8+ import {
9+ getContract ,
10+ prepareContractCall ,
11+ readContract ,
12+ sendTransaction ,
13+ waitForReceipt ,
14+ } from "thirdweb" ;
15+ import { base } from "thirdweb/chains" ;
16+ import { parseEventLogs } from "viem" ;
1417import { TransactionsSummaryList } from "@/components/proposals/preview/TransactionsSummaryList" ;
1518import { ProposalDebugPanel } from "@/components/proposals/ProposalDebugPanel" ;
1619import { useProposalEligibilityContext } from "@/components/proposals/ProposalEligibilityContext" ;
1720import { Alert , AlertDescription } from "@/components/ui/alert" ;
1821import { Button } from "@/components/ui/button" ;
1922import { Card , CardContent } from "@/components/ui/card" ;
2023import { createProposalAction } from "@/app/propose/actions" ;
24+ import { useProposalIndexing } from "@/hooks/use-proposal-indexing" ;
25+ import { useUserAddress } from "@/hooks/use-user-address" ;
26+ import { useWriteAccount } from "@/hooks/use-write-account" ;
2127import { DAO_ADDRESSES } from "@/lib/config" ;
2228import { ipfsToGatewayUrl } from "@/lib/pinata" ;
2329import { encodeTransactions } from "@/lib/proposal-utils" ;
30+ import { getThirdwebClient } from "@/lib/thirdweb" ;
31+ import { ensureOnChain } from "@/lib/thirdweb-tx" ;
2432import { type ProposalFormValues } from "./schema" ;
2533
2634const governorAbi = [
@@ -43,6 +51,38 @@ const governorAbi = [
4351 inputs : [ ] ,
4452 outputs : [ { name : "" , type : "uint256" } ] ,
4553 } ,
54+ {
55+ name : "ProposalCreated" ,
56+ type : "event" ,
57+ anonymous : false ,
58+ inputs : [
59+ { name : "proposalId" , type : "bytes32" , indexed : false } ,
60+ { name : "targets" , type : "address[]" , indexed : false } ,
61+ { name : "values" , type : "uint256[]" , indexed : false } ,
62+ { name : "calldatas" , type : "bytes[]" , indexed : false } ,
63+ { name : "description" , type : "string" , indexed : false } ,
64+ { name : "descriptionHash" , type : "bytes32" , indexed : false } ,
65+ {
66+ name : "proposal" ,
67+ type : "tuple" ,
68+ indexed : false ,
69+ components : [
70+ { name : "proposer" , type : "address" } ,
71+ { name : "timeCreated" , type : "uint32" } ,
72+ { name : "againstVotes" , type : "uint32" } ,
73+ { name : "forVotes" , type : "uint32" } ,
74+ { name : "abstainVotes" , type : "uint32" } ,
75+ { name : "voteStart" , type : "uint32" } ,
76+ { name : "voteEnd" , type : "uint32" } ,
77+ { name : "proposalThreshold" , type : "uint32" } ,
78+ { name : "quorumVotes" , type : "uint32" } ,
79+ { name : "executed" , type : "bool" } ,
80+ { name : "canceled" , type : "bool" } ,
81+ { name : "vetoed" , type : "bool" } ,
82+ ] ,
83+ } ,
84+ ] ,
85+ } ,
4686] as const ;
4787
4888// Minimal token ABI used only for the send-time voting-power pre-check.
@@ -60,21 +100,30 @@ const tokenGetVotesAbi = [
60100
61101export function ProposalPreview ( ) {
62102 const eligibility = useProposalEligibilityContext ( ) ;
63- const { getValues, handleSubmit, formState : { errors } } = useFormContext < ProposalFormValues > ( ) ;
103+ const {
104+ getValues,
105+ handleSubmit,
106+ formState : { errors } ,
107+ } = useFormContext < ProposalFormValues > ( ) ;
64108 const [ isActionPending , startTransition ] = useTransition ( ) ;
65109 const [ preparedDescription , setPreparedDescription ] = useState < string > ( "" ) ;
66110 const [ validationError , setValidationError ] = useState < string | null > ( null ) ;
67- const [ encodedTxData , setEncodedTxData ] = useState < {
68- targets : `0x${string } `[ ] ;
69- values : bigint [ ] ;
70- calldatas : `0x${string } `[ ] ;
71- } | undefined > ( ) ;
111+ const [ encodedTxData , setEncodedTxData ] = useState <
112+ | {
113+ targets : `0x${string } `[ ] ;
114+ values : bigint [ ] ;
115+ calldatas : `0x${string } `[ ] ;
116+ }
117+ | undefined
118+ > ( ) ;
72119 const { address, isConnected } = useUserAddress ( ) ;
73120 const writer = useWriteAccount ( ) ;
74121 const [ hash , setHash ] = useState < `0x${string } ` | undefined > ( undefined ) ;
75122 const [ isConfirming , setIsConfirming ] = useState ( false ) ;
76123 const [ isWalletPending , setIsWalletPending ] = useState ( false ) ;
77124 const [ isSuccess , setIsSuccess ] = useState ( false ) ;
125+ const [ onchainProposalId , setOnchainProposalId ] = useState < `0x${string } ` | undefined > ( undefined ) ;
126+ const indexing = useProposalIndexing ( onchainProposalId ) ;
78127
79128 // Watch form values for reactive preview
80129 const watchedData = useWatch < ProposalFormValues > ( ) ;
@@ -88,7 +137,7 @@ export function ProposalPreview() {
88137 try {
89138 const result = await createProposalAction ( data ) ;
90139 setPreparedDescription ( result . description ) ;
91-
140+
92141 // Also encode the transactions for preview
93142 if ( data . transactions && data . transactions . length > 0 ) {
94143 const encoded = encodeTransactions ( data . transactions ) ;
@@ -110,6 +159,7 @@ export function ProposalPreview() {
110159 setHash ( undefined ) ;
111160 setIsConfirming ( false ) ;
112161 setIsSuccess ( false ) ;
162+ setOnchainProposalId ( undefined ) ;
113163
114164 startTransition ( async ( ) => {
115165 console . log ( "Inside startTransition" ) ;
@@ -198,8 +248,23 @@ export function ProposalPreview() {
198248 setIsWalletPending ( false ) ;
199249
200250 setIsConfirming ( true ) ;
201- await waitForReceipt ( { client, chain : base , transactionHash : txHash } ) ;
251+ const receipt = await waitForReceipt ( { client, chain : base , transactionHash : txHash } ) ;
202252 setIsConfirming ( false ) ;
253+
254+ try {
255+ const events = parseEventLogs ( {
256+ abi : governorAbi ,
257+ eventName : "ProposalCreated" ,
258+ logs : receipt . logs ,
259+ } ) ;
260+ const created = events [ 0 ] ;
261+ if ( created ?. args ?. proposalId ) {
262+ setOnchainProposalId ( created . args . proposalId as `0x${string } `) ;
263+ }
264+ } catch ( parseErr ) {
265+ console . warn ( "Could not decode ProposalCreated event:" , parseErr ) ;
266+ }
267+
203268 setIsSuccess ( true ) ;
204269 } catch ( error ) {
205270 console . error ( "Error submitting proposal:" , error ) ;
@@ -217,32 +282,45 @@ export function ProposalPreview() {
217282 const onValidationError = ( errors : Record < string , unknown > ) => {
218283 console . error ( "Form validation errors:" , errors ) ;
219284 console . log ( "Full errors object:" , JSON . stringify ( errors , null , 2 ) ) ;
220-
285+
221286 // Collect all error messages
222287 const errorMessages : string [ ] = [ ] ;
223-
224- if ( errors . title && typeof errors . title === 'object' && errors . title !== null && 'message' in errors . title ) {
288+
289+ if (
290+ errors . title &&
291+ typeof errors . title === "object" &&
292+ errors . title !== null &&
293+ "message" in errors . title
294+ ) {
225295 errorMessages . push ( `Title: ${ ( errors . title as { message : string } ) . message } ` ) ;
226296 }
227- if ( errors . description && typeof errors . description === 'object' && errors . description !== null && 'message' in errors . description ) {
297+ if (
298+ errors . description &&
299+ typeof errors . description === "object" &&
300+ errors . description !== null &&
301+ "message" in errors . description
302+ ) {
228303 errorMessages . push ( `Description: ${ ( errors . description as { message : string } ) . message } ` ) ;
229304 }
230305 if ( errors . transactions && Array . isArray ( errors . transactions ) ) {
231306 errors . transactions . forEach ( ( txError : unknown , index : number ) => {
232- if ( txError && typeof txError === ' object' && txError !== null ) {
307+ if ( txError && typeof txError === " object" && txError !== null ) {
233308 Object . entries ( txError ) . forEach ( ( [ field , error ] : [ string , unknown ] ) => {
234- if ( error && typeof error === 'object' && error !== null && 'message' in error ) {
235- errorMessages . push ( `Transaction ${ index + 1 } - ${ field } : ${ ( error as { message : string } ) . message } ` ) ;
309+ if ( error && typeof error === "object" && error !== null && "message" in error ) {
310+ errorMessages . push (
311+ `Transaction ${ index + 1 } - ${ field } : ${ ( error as { message : string } ) . message } ` ,
312+ ) ;
236313 }
237314 } ) ;
238315 }
239316 } ) ;
240317 }
241-
242- const errorMessage = errorMessages . length > 0
243- ? errorMessages . join ( "; " )
244- : "Please fix validation errors before submitting" ;
245-
318+
319+ const errorMessage =
320+ errorMessages . length > 0
321+ ? errorMessages . join ( "; " )
322+ : "Please fix validation errors before submitting" ;
323+
246324 console . log ( "Validation error message:" , errorMessage ) ;
247325 setValidationError ( errorMessage ) ;
248326 toast . error ( "Validation Failed" , {
@@ -271,26 +349,60 @@ export function ProposalPreview() {
271349 } , [ canSubmit , data . title , data . transactions , isActionPending , isWalletPending , isConfirming ] ) ;
272350
273351 if ( isSuccess ) {
352+ const isIndexed = indexing . status === "ready" && indexing . proposalNumber !== null ;
353+ const isPolling = indexing . status === "pending" && ! ! onchainProposalId ;
354+ const timedOut = indexing . status === "timeout" || indexing . status === "error" ;
355+
274356 return (
275357 < Card >
276358 < CardContent className = "p-8 text-center" >
277359 < CheckCircle className = "h-16 w-16 text-green-500 mx-auto mb-4" />
278360 < h3 className = "text-2xl font-bold mb-2" > Proposal Submitted!</ h3 >
279- < p className = "text-muted-foreground mb-4" >
280- Your proposal has been successfully submitted to the Gnars DAO.
281- </ p >
282- { hash && (
283- < Button variant = "outline" asChild >
284- < a
285- href = { `https://basescan.org/tx/${ hash } ` }
286- target = "_blank"
287- rel = "noopener noreferrer"
288- className = "inline-flex items-center"
289- >
290- View Transaction < ExternalLink className = "h-4 w-4 ml-1" />
291- </ a >
292- </ Button >
361+
362+ { isIndexed ? (
363+ < p className = "text-muted-foreground mb-4" >
364+ Proposal #{ indexing . proposalNumber } is live on the Gnars DAO.
365+ </ p >
366+ ) : isPolling ? (
367+ < p className = "text-muted-foreground mb-4 inline-flex items-center justify-center gap-2" >
368+ < Loader2 className = "h-4 w-4 animate-spin" />
369+ Transaction confirmed. Waiting for the subgraph to index your proposal…
370+ </ p >
371+ ) : timedOut ? (
372+ < p className = "text-muted-foreground mb-4" >
373+ Transaction confirmed. Indexing is taking longer than expected — refresh the proposals
374+ page in a minute.
375+ </ p >
376+ ) : (
377+ < p className = "text-muted-foreground mb-4" >
378+ Your proposal has been successfully submitted to the Gnars DAO.
379+ </ p >
293380 ) }
381+
382+ < div className = "flex flex-wrap gap-2 justify-center" >
383+ { isIndexed && indexing . proposalNumber !== null && (
384+ < Button asChild >
385+ < a
386+ href = { `/proposals/base/${ indexing . proposalNumber } ` }
387+ className = "inline-flex items-center"
388+ >
389+ View Proposal < ExternalLink className = "h-4 w-4 ml-1" />
390+ </ a >
391+ </ Button >
392+ ) }
393+ { hash && (
394+ < Button variant = "outline" asChild >
395+ < a
396+ href = { `https://basescan.org/tx/${ hash } ` }
397+ target = "_blank"
398+ rel = "noopener noreferrer"
399+ className = "inline-flex items-center"
400+ >
401+ View Transaction < ExternalLink className = "h-4 w-4 ml-1" />
402+ </ a >
403+ </ Button >
404+ ) }
405+ </ div >
294406 </ CardContent >
295407 </ Card >
296408 ) ;
@@ -331,8 +443,8 @@ export function ProposalPreview() {
331443 < TransactionsSummaryList transactions = { data . transactions ?? [ ] } />
332444
333445 { /* Debug Panel */ }
334- < ProposalDebugPanel
335- formData = { data }
446+ < ProposalDebugPanel
447+ formData = { data }
336448 preparedDescription = { preparedDescription }
337449 encodedTransactions = { encodedTxData }
338450 />
@@ -370,7 +482,9 @@ export function ProposalPreview() {
370482 { ! isConnected && (
371483 < Alert className = "mb-4" >
372484 < Info className = "h-4 w-4" />
373- < AlertDescription > Connect your wallet to check eligibility and submit.</ AlertDescription >
485+ < AlertDescription >
486+ Connect your wallet to check eligibility and submit.
487+ </ AlertDescription >
374488 </ Alert >
375489 ) }
376490
@@ -393,8 +507,8 @@ export function ProposalPreview() {
393507 { typeof eligibility . votes === "bigint" && (
394508 < >
395509 { " " }
396- You currently have < span className = "font-semibold" > { eligibility . votes . toString ( ) } </ span > { " " }
397- votes.
510+ You currently have{ " " }
511+ < span className = "font-semibold" > { eligibility . votes . toString ( ) } </ span > votes.
398512 </ >
399513 ) }
400514 { eligibility . isDelegating && eligibility . delegatedTo && address && (
0 commit comments