Skip to content

Commit caf0eb3

Browse files
committed
a
1 parent f2dd208 commit caf0eb3

16 files changed

+1807
-41
lines changed

app/admin-console.tsx

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,65 @@
1-
import React from 'react'
1+
'use client'
2+
3+
import React, { useEffect, useState } from 'react'
24

35
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
6+
import { get } from '@vercel/blob'
47

8+
import { CreateProposals } from './create-proposals'
59
import { ExecuteConsole } from './execute-console'
10+
import Proposals from './proposals'
611
import { SignConsole } from './sign-console'
712

13+
export interface Proposal {
14+
id: string
15+
title: string
16+
description: string
17+
status: 'pending' | 'approved' | 'rejected'
18+
createdAt: string
19+
blobUrl?: string // URL to the stored proposal document if any
20+
signatures?: string[] // Array of wallet addresses that signed
21+
}
22+
823
export const AdminConsole = () => {
24+
const [proposals, setProposals] = useState<Proposal[]>([])
25+
const [loading, setLoading] = useState(true)
26+
const [error, setError] = useState<string | null>(null)
27+
28+
useEffect(() => {
29+
fetchProposals()
30+
}, [])
31+
32+
const fetchProposals = async () => {
33+
try {
34+
setLoading(true)
35+
// Try to fetch the proposals index file from Vercel Blob
36+
const response = await fetch('/api/proposals')
37+
38+
if (!response.ok) {
39+
throw new Error('Failed to fetch proposals')
40+
}
41+
42+
const data = await response.json()
43+
setProposals(data)
44+
} catch (err) {
45+
console.error('Error fetching proposals:', err)
46+
setError(err instanceof Error ? err.message : 'Failed to fetch proposals')
47+
} finally {
48+
setLoading(false)
49+
}
50+
}
51+
952
return (
10-
<Tabs defaultValue="sign" className="p-8 bg-zinc-100 border-2 border-white rounded-md w-[500px]">
53+
<Tabs defaultValue="proposals" className="p-8 bg-zinc-100 border-2 border-white rounded-md w-full h-full">
1154
<TabsList>
12-
<TabsTrigger value="sign">Sign</TabsTrigger>
13-
<TabsTrigger value="execute">Execute</TabsTrigger>
55+
<TabsTrigger value="proposals">Proposals</TabsTrigger>
56+
<TabsTrigger value="create">Create</TabsTrigger>
1457
</TabsList>
15-
<TabsContent value="sign">
16-
<SignConsole />
58+
<TabsContent value="proposals">
59+
<Proposals />
1760
</TabsContent>
18-
<TabsContent value="execute">
19-
<ExecuteConsole />
61+
<TabsContent value="create">
62+
<CreateProposals />
2063
</TabsContent>
2164
</Tabs>
2265
)

app/create-proposals.tsx

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
'use client'
2+
3+
import React, { useState } from 'react'
4+
5+
import { Button } from '@/components/ui/button'
6+
import { Form, FormControl, FormField, FormItem, FormLabel } from '@/components/ui/form'
7+
import { Input } from '@/components/ui/input'
8+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
9+
import { Textarea } from '@/components/ui/textarea'
10+
import { addProposal, addSignature } from '@/db/functions'
11+
import { keepAbi } from '@/lib/keep'
12+
import { zodResolver } from '@hookform/resolvers/zod'
13+
import { useForm } from 'react-hook-form'
14+
import { toast } from 'sonner'
15+
import { Address, Hex, isAddress, parseEther, zeroAddress } from 'viem'
16+
import { useAccount, usePublicClient, useSignTypedData } from 'wagmi'
17+
import { z } from 'zod'
18+
19+
import { Operation } from './types'
20+
21+
const formSchema = z.object({
22+
account: z
23+
.string()
24+
.min(1, 'Account is required')
25+
.refine((val) => isAddress(val))
26+
.transform((val) => val as Address),
27+
to: z
28+
.string()
29+
.min(1, 'To address is required')
30+
.refine((val) => isAddress(val))
31+
.transform((val) => val as Address),
32+
operation: z.nativeEnum(Operation, {
33+
errorMap: () => ({ message: 'Operation type is required' }),
34+
}),
35+
value: z.string().min(1, 'Value is required'),
36+
data: z.string().min(1, 'Data is required'),
37+
chainId: z.number().int().positive('Chain ID must be a positive integer'),
38+
})
39+
40+
type FormData = z.infer<typeof formSchema>
41+
42+
export const CreateProposals = () => {
43+
const { address } = useAccount()
44+
const form = useForm<FormData>({
45+
resolver: zodResolver(formSchema),
46+
defaultValues: {
47+
account: zeroAddress,
48+
to: zeroAddress,
49+
operation: Operation.call,
50+
value: '',
51+
data: '',
52+
chainId: 1,
53+
},
54+
})
55+
const { signTypedDataAsync } = useSignTypedData()
56+
const [signature, setSignature] = useState<Hex>()
57+
const publicClient = usePublicClient()
58+
59+
const onSubmit = async (data: FormData) => {
60+
try {
61+
if (!publicClient) {
62+
throw new Error('Public client not found')
63+
}
64+
if (!address) {
65+
throw new Error('Wallet not connected')
66+
}
67+
68+
// Get nonce from contract
69+
const nonce = await publicClient.readContract({
70+
address: data.account,
71+
abi: keepAbi,
72+
functionName: 'nonce',
73+
args: [],
74+
})
75+
76+
// Sign the transaction
77+
const signature = await signTypedDataAsync({
78+
domain: {
79+
name: 'Keep',
80+
version: '1',
81+
chainId: data.chainId,
82+
verifyingContract: data.account,
83+
},
84+
types: {
85+
Execute: [
86+
{ name: 'op', type: 'uint8' },
87+
{ name: 'to', type: 'address' },
88+
{ name: 'value', type: 'uint256' },
89+
{ name: 'data', type: 'bytes' },
90+
{ name: 'nonce', type: 'uint120' },
91+
],
92+
},
93+
primaryType: 'Execute',
94+
message: {
95+
op: data.operation,
96+
to: data.to as Address,
97+
value: parseEther(data.value),
98+
data: data.data as Hex,
99+
nonce: nonce,
100+
},
101+
})
102+
103+
// Store proposal in database
104+
await addProposal({
105+
nonce: Number(nonce),
106+
sender: data.account,
107+
target: data.to,
108+
value: data.value,
109+
data: data.data,
110+
proposer: address,
111+
})
112+
113+
// Store signature in database
114+
await addSignature({
115+
signer: address,
116+
nonce: Number(nonce),
117+
sender: data.account,
118+
signature: signature,
119+
})
120+
121+
setSignature(signature)
122+
toast.success('Proposal created and signed successfully')
123+
} catch (error) {
124+
console.error('Error:', error)
125+
toast.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`)
126+
}
127+
}
128+
129+
if (!address) {
130+
return <div>Please connect your wallet</div>
131+
}
132+
133+
if (signature !== undefined) {
134+
return (
135+
<div>
136+
<p>Signature:</p>
137+
<pre>{signature}</pre>
138+
</div>
139+
)
140+
}
141+
142+
return (
143+
<div className="space-y-6">
144+
<h1 className="text-2xl font-bold">Create New Proposal</h1>
145+
<Form {...form}>
146+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
147+
<FormField
148+
control={form.control}
149+
name="account"
150+
render={({ field }) => (
151+
<FormItem>
152+
<FormLabel>Account</FormLabel>
153+
<FormControl>
154+
<Input {...field} />
155+
</FormControl>
156+
</FormItem>
157+
)}
158+
/>
159+
<FormField
160+
control={form.control}
161+
name="chainId"
162+
render={({ field }) => (
163+
<FormItem>
164+
<FormLabel>Chain ID</FormLabel>
165+
<FormControl>
166+
<Input {...field} type="number" />
167+
</FormControl>
168+
</FormItem>
169+
)}
170+
/>
171+
<FormField
172+
control={form.control}
173+
name="to"
174+
render={({ field }) => (
175+
<FormItem>
176+
<FormLabel>To</FormLabel>
177+
<FormControl>
178+
<Input {...field} />
179+
</FormControl>
180+
</FormItem>
181+
)}
182+
/>
183+
<FormField
184+
control={form.control}
185+
name="operation"
186+
render={({ field }) => (
187+
<FormItem>
188+
<FormLabel>Operation</FormLabel>
189+
<Select onValueChange={field.onChange} defaultValue={field.value.toString()}>
190+
<FormControl>
191+
<SelectTrigger>
192+
<SelectValue placeholder="Select an operation" />
193+
</SelectTrigger>
194+
</FormControl>
195+
<SelectContent>
196+
{Object.entries(Operation).map(([key, value]) => (
197+
<SelectItem key={key} value={value.toString()}>
198+
{key}
199+
</SelectItem>
200+
))}
201+
</SelectContent>
202+
</Select>
203+
</FormItem>
204+
)}
205+
/>
206+
<FormField
207+
control={form.control}
208+
name="value"
209+
render={({ field }) => (
210+
<FormItem>
211+
<FormLabel>Value (ETH)</FormLabel>
212+
<FormControl>
213+
<Input {...field} type="number" step="0.000000000000000001" />
214+
</FormControl>
215+
</FormItem>
216+
)}
217+
/>
218+
<FormField
219+
control={form.control}
220+
name="data"
221+
render={({ field }) => (
222+
<FormItem>
223+
<FormLabel>Data (bytes)</FormLabel>
224+
<FormControl>
225+
<Textarea {...field} />
226+
</FormControl>
227+
</FormItem>
228+
)}
229+
/>
230+
<Button type="submit">Submit</Button>
231+
</form>
232+
</Form>
233+
</div>
234+
)
235+
}

0 commit comments

Comments
 (0)