diff --git a/.cursorrules b/.cursorrules index dfab376..9fcdb15 100644 --- a/.cursorrules +++ b/.cursorrules @@ -1,67 +1,64 @@ - You are an expert in Solidity, TypeScript, Node.js, Next.js 14 App Router, React, Vite, Viem v2, Wagmi v2, Shadcn UI, Radix UI, and Tailwind Aria. - - Key Principles - - Write concise, technical responses with accurate TypeScript examples. - - Use functional, declarative programming. Avoid classes. - - Prefer iteration and modularization over duplication. - - Use descriptive variable names with auxiliary verbs (e.g., isLoading). - - Use lowercase with dashes for directories (e.g., components/auth-wizard). - - Favor named exports for components. - - Use the Receive an Object, Return an Object (RORO) pattern. - - JavaScript/TypeScript - - Use "function" keyword for pure functions. Omit semicolons. - - Use TypeScript for all code. Prefer interfaces over types. Avoid enums, use maps. - - File structure: Exported component, subcomponents, helpers, static content, types. - - Avoid unnecessary curly braces in conditional statements. - - For single-line statements in conditionals, omit curly braces. - - Use concise, one-line syntax for simple conditional statements (e.g., if (condition) doSomething()). - - Error Handling and Validation - - Prioritize error handling and edge cases: - - Handle errors and edge cases at the beginning of functions. - - Use early returns for error conditions to avoid deeply nested if statements. - - Place the happy path last in the function for improved readability. - - Avoid unnecessary else statements; use if-return pattern instead. - - Use guard clauses to handle preconditions and invalid states early. - - Implement proper error logging and user-friendly error messages. - - Consider using custom error types or error factories for consistent error handling. - - React/Next.js - - Use functional components and TypeScript interfaces. - - Use declarative JSX. - - Use function, not const, for components. - - Use Shadcn UI, Radix, and Tailwind Aria for components and styling. - - Implement responsive design with Tailwind CSS. - - Use mobile-first approach for responsive design. - - Place static content and interfaces at file end. - - Use content variables for static content outside render functions. - - Minimize 'use client', 'useEffect', and 'setState'. Favor RSC. - - Use Zod for form validation. - - Wrap client components in Suspense with fallback. - - Use dynamic loading for non-critical components. - - Optimize images: WebP format, size data, lazy loading. - - Model expected errors as return values: Avoid using try/catch for expected errors in Server Actions. Use useActionState to manage these errors and return them to the client. - - Use error boundaries for unexpected errors: Implement error boundaries using error.tsx and global-error.tsx files to handle unexpected errors and provide a fallback UI. - - Use useActionState with react-hook-form for form validation. - - Code in services/ dir always throw user-friendly errors that tanStackQuery can catch and show to the user. - - Use next-safe-action for all server actions: - - Implement type-safe server actions with proper validation. - - Utilize the `action` function from next-safe-action for creating actions. - - Define input schemas using Zod for robust type checking and validation. - - Handle errors gracefully and return appropriate responses. - - Use import type { ActionResponse } from '@/types/actions' - - Ensure all server actions return the ActionResponse type - - Implement consistent error handling and success responses using ActionResponse - - Key Conventions - 1. Rely on Next.js App Router for state changes. - 2. Prioritize Web Vitals (LCP, CLS, FID). - 3. Minimize 'use client' usage: - - Prefer server components and Next.js SSR features. - - Use 'use client' only for Web API access in small components. - - Avoid using 'use client' for data fetching or state management. - - Refer to Next.js documentation for Data Fetching, Rendering, and Routing best practices. - \ No newline at end of file +Key Principles +- Write concise, technical responses with accurate TypeScript examples. +- Use functional, declarative programming. Avoid classes. +- Prefer iteration and modularization over duplication. +- Use descriptive variable names with auxiliary verbs (e.g., isLoading). +- Use lowercase with dashes for directories (e.g., components/auth-wizard). +- Favor named exports for components. +- Use the Receive an Object, Return an Object (RORO) pattern. + +JavaScript/TypeScript +- Use "function" keyword for pure functions. Omit semicolons. +- Use TypeScript for all code. Prefer interfaces over types. Avoid enums, use maps. +- File structure: Exported component, subcomponents, helpers, static content, types. +- Avoid unnecessary curly braces in conditional statements. +- For single-line statements in conditionals, omit curly braces. +- Use concise, one-line syntax for simple conditional statements (e.g., if (condition) doSomething()). + +Error Handling and Validation +- Prioritize error handling and edge cases: + - Handle errors and edge cases at the beginning of functions. + - Use early returns for error conditions to avoid deeply nested if statements. + - Place the happy path last in the function for improved readability. + - Avoid unnecessary else statements; use if-return pattern instead. + - Use guard clauses to handle preconditions and invalid states early. + - Implement proper error logging and user-friendly error messages. + - Consider using custom error types or error factories for consistent error handling. + +React/Next.js +- Use functional components and TypeScript interfaces. +- Use declarative JSX. +- Use function, not const, for components. +- Use Shadcn UI, Radix, and Tailwind Aria for components and styling. +- Implement responsive design with Tailwind CSS. +- Use mobile-first approach for responsive design. +- Place static content and interfaces at file end. +- Use content variables for static content outside render functions. +- Minimize 'use client', 'useEffect', and 'setState'. Favor RSC. +- Use Zod for form validation. +- Wrap client components in Suspense with fallback. +- Use dynamic loading for non-critical components. +- Optimize images: WebP format, size data, lazy loading. + +RainbowKit v2 & Wagmi Integration +- Setup RainbowKit v2 with Wagmi v2 using `configureChains` and `createClient` from Wagmi. +- Initialize chains and providers within a `RainbowKitProvider` wrapper. +- Store mnemonics in `expo-secure-store` or other secure storage solutions. +- Use React hooks like `useAccount`, `useConnect`, `useDisconnect` from Wagmi for wallet actions. +- Favor the use of hooks over directly accessing global state. + +tRPC v11 Integration +- Use `@trpc/react-query` and define tRPC routers on the server. +- Set up queries and mutations using `trpc.useQuery` and `trpc.useMutation` on the client. +- Use Zod for input validation at both client and server levels. +- Handle optimistic updates in React Query using the `onMutate` and `onSuccess` methods. +- For error handling, ensure tRPC procedures return a consistent error structure. + +Key Conventions +1. Rely on Next.js App Router for state changes. +2. Prioritize Web Vitals (LCP, CLS, FID). +3. Minimize 'use client' usage: + - Prefer server components and Next.js SSR features. + - Use 'use client' only for Web API access in small components. + - Avoid using 'use client' for data fetching or state management. diff --git a/src/components/contract/ContractFunctions.tsx b/src/components/contract/ContractFunctions.tsx index 86a3964..d3ad343 100644 --- a/src/components/contract/ContractFunctions.tsx +++ b/src/components/contract/ContractFunctions.tsx @@ -13,17 +13,17 @@ import { useWaitForTransactionReceipt, useWriteContract, } from "wagmi"; -import { Button } from "~/components/ui/button"; -import { Input } from "~/components/ui/input"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, -} from "../ui/accordion"; -import { Label } from "../ui/label"; -import { SearchInput } from "../ui/search-input"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +} from "~/components/ui/accordion"; +import { Button } from "~/components/ui/button"; +import { Input } from "~/components/ui/input"; +import { Label } from "~/components/ui/label"; +import { SearchInput } from "~/components/ui/search-input"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/components/ui/tabs"; interface ContractFunctionsProps { abi: Abi; diff --git a/src/components/deploy-status.tsx b/src/components/deploy-status.tsx new file mode 100644 index 0000000..557371d --- /dev/null +++ b/src/components/deploy-status.tsx @@ -0,0 +1,71 @@ +import { AlertCircle, CheckIcon } from "lucide-react"; +import { Loading } from "./loading"; + +interface StatusStep { + message: string; + status: "success" | "error" | "loading"; + error?: string; + details?: string; +} + +interface StatusDisplayProps { + title: string; + steps: StatusStep[]; +} + +function StatusDisplay({ title, steps }: StatusDisplayProps) { + return ( +
+

{title}

+
+ {steps.length > 0 ? ( + steps.map((step, index) => ( + + )) + ) : ( + + )} +
+
+ ); +} + +function StatusStep({ + step, + isLatest, +}: { + step: StatusStep; + isLatest: boolean; +}) { + const statusIcons = { + success: , + error: , + loading: isLatest ? : null, + }; + + return ( +
+ {statusIcons[step.status]} +
+

{step.message}

+ {step.status === "error" && ( +

{step.error}

+ )} + {step.status === "success" && step.details && ( +

{step.details}

+ )} +
+
+ ); +} + +export default StatusDisplay; diff --git a/src/components/dialogs/send-dialog.tsx b/src/components/dialogs/send-dialog.tsx index 3923378..c5286fa 100644 --- a/src/components/dialogs/send-dialog.tsx +++ b/src/components/dialogs/send-dialog.tsx @@ -18,6 +18,7 @@ import { } from "wagmi"; import { useAuth } from "~/hooks/useAuth"; import { useDebounce } from "~/hooks/useDebounce"; +import { trpc } from "~/lib/trpc"; import { cn } from "~/lib/utils"; import { toUserUnits, toUserUnitsString } from "~/utils/units"; import { AddressField } from "../forms/fields/address-field"; @@ -37,7 +38,6 @@ import { FormMessage, } from "../ui/form"; import { Input } from "../ui/input"; -import { trpc } from "~/lib/trpc"; const FormSchema = z.object({ voucherAddress: z.string().refine(isAddress, "Invalid voucher address"), @@ -83,14 +83,33 @@ const SendForm = (props: { const debouncedAmount = useDebounce(amount, 500); const debouncedRecipientAddress = useDebounce(recipientAddress, 500); const { data: voucherDetails } = useVoucherDetails(voucherAddress); + console.log({ + address: voucherAddress, + functionName: "transfer", + args: [ + debouncedRecipientAddress, + parseUnits(debouncedAmount?.toString() ?? "", voucherDetails?.decimals ?? 0), + ], + query: { + enabled: Boolean( + voucherDetails?.decimals && + debouncedAmount && + debouncedRecipientAddress && + voucherAddress && + isValid + ), + }, + gas: 350_000n, + maxFeePerGas: parseGwei("10"), + maxPriorityFeePerGas: 5n, + }); const simulateContract = useSimulateContract({ address: voucherAddress, abi: erc20Abi, functionName: "transfer", args: [ debouncedRecipientAddress, - // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain - parseUnits(debouncedAmount?.toString() ?? "", voucherDetails?.decimals!), + parseUnits(debouncedAmount?.toString() ?? "", voucherDetails?.decimals), ], query: { enabled: Boolean( @@ -244,11 +263,8 @@ const SendForm = (props: { }; export const SendDialog = (props: SendDialogProps) => { - const [open, setOpen] = useState(false); return ( } title="Send Voucher" > diff --git a/src/components/modal.tsx b/src/components/modal.tsx index 3f1aae7..3032aaf 100644 --- a/src/components/modal.tsx +++ b/src/components/modal.tsx @@ -16,6 +16,7 @@ import { DrawerTitle, DrawerTrigger, } from "./ui/drawer"; +import { ScrollArea } from "./ui/scroll-area"; type PopoverProps = ControlledPopoverProps | UnControlledPopoverProps; @@ -48,13 +49,13 @@ export const Modal = (props: PopoverProps) => { ); -}; +}; export const BottomDrawer = (props: PopoverProps) => { return ( {props.button} @@ -62,7 +63,7 @@ export const BottomDrawer = (props: PopoverProps) => { {props.title} {props.description} - {props.children} + {props.children} ); diff --git a/src/components/pools/create-pool-status.tsx b/src/components/pools/create-pool-status.tsx deleted file mode 100644 index 30e4254..0000000 --- a/src/components/pools/create-pool-status.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { AlertCircle, CheckIcon } from "lucide-react"; -import React from "react"; -import "tailwindcss/tailwind.css"; -import { Loading } from "../loading"; - -interface ProgressStep { - message: string; - status: string; - error?: string; - address?: string; -} - -const CreatePoolStats: React.FC<{ status: ProgressStep[] }> = ({ status }) => { - return ( -
-

- Please wait while we deploy your pool -

-
- {status.length > 0 ? ( - status.map((step, index) => ( -
- {step.status === "success" || - (index != status.length - 1 && ( - - ))} - {step.status === "error" && ( - - )} - {step.status === "loading" && index == status.length - 1 && ( - - )} -
-

{step.message}

- {step.status === "error" && ( -

{step.error}

- )} - {step.status === "success" && step.address && ( -

Address: {step.address}

- )} -
-
- )) - ) : ( - - )} -
-
- ); -}; - -export default CreatePoolStats; diff --git a/src/components/pools/forms/create-pool-form.tsx b/src/components/pools/forms/create-pool-form.tsx index bc11825..b6c4723 100644 --- a/src/components/pools/forms/create-pool-form.tsx +++ b/src/components/pools/forms/create-pool-form.tsx @@ -6,6 +6,7 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; import { ConnectButton } from "~/components/buttons/connect-button"; +import StatusDisplay from "~/components/deploy-status"; import { ComboBoxField } from "~/components/forms/fields/combo-box-field"; import { ImageUploadField } from "~/components/forms/fields/image-upload-field"; import { InputField } from "~/components/forms/fields/input-field"; @@ -14,10 +15,9 @@ import { Button } from "~/components/ui/button"; import { Form } from "~/components/ui/form"; import { PoolIndex } from "~/contracts"; import { useAuth } from "~/hooks/useAuth"; +import { trpc } from "~/lib/trpc"; import { type RouterOutput } from "~/server/api/root"; import { type InferAsyncGenerator } from "~/server/api/routers/pool"; -import CreatePoolStats from "../create-pool-status"; -import { trpc } from "~/lib/trpc"; const createPoolSchema = z.object({ poolName: z.string().min(3, "Pool name must be at least 3 characters"), poolSymbol: z @@ -152,7 +152,10 @@ export function CreatePoolForm() { ) : ( - + )} ); diff --git a/src/components/voucher/forms/create-voucher-form/controls.tsx b/src/components/voucher/forms/create-voucher-form/controls.tsx index 5bd23fc..7378b3e 100644 --- a/src/components/voucher/forms/create-voucher-form/controls.tsx +++ b/src/components/voucher/forms/create-voucher-form/controls.tsx @@ -6,9 +6,10 @@ import { useVoucherStepper } from "./provider"; interface StepControlsProps { onNext?: () => Promise | void; onPrev?: () => void; + disabled?: boolean; } -export function StepControls({ onNext, onPrev }: StepControlsProps) { +export function StepControls({ onNext, onPrev, disabled }: StepControlsProps) { const { nextStep, prevStep, @@ -53,7 +54,7 @@ export function StepControls({ onNext, onPrev }: StepControlsProps) { > Prev - diff --git a/src/components/voucher/forms/create-voucher-form/provider.tsx b/src/components/voucher/forms/create-voucher-form/provider.tsx index b3f6939..b199c51 100644 --- a/src/components/voucher/forms/create-voucher-form/provider.tsx +++ b/src/components/voucher/forms/create-voucher-form/provider.tsx @@ -1,16 +1,13 @@ "use client"; import { createContext, useContext, useMemo } from "react"; -import { toast } from "sonner"; -import { z } from "zod"; import { useStepper, type Steps, type UseStepperReturn, } from "~/components/ui/use-stepper"; import { useLocalStorage } from "~/hooks/useLocalStorage"; -import { useVoucherDeploy as useDeploy } from "~/hooks/useVoucherDeploy"; -import { schemas, type VoucherPublishingSchema } from "./schemas"; +import { type VoucherPublishingSchema } from "./schemas"; type CreateVoucherContextType = { state: Partial; @@ -89,47 +86,6 @@ export function useVoucherForm( return { values, onValid }; } -export function useVoucherDeploy() { - const context = useContext(CreateVoucherContext); - const { deploy, ...other } = useDeploy(); - - if (!context) { - throw new Error( - "useVoucherDeploy must be used within a CreateVoucherProvider" - ); - } - - const onValid = async ( - data: VoucherPublishingSchema["signingAndPublishing"] - ) => { - const formData = { ...context.state, signingAndPublishing: data }; - const validation = await z.object(schemas).safeParseAsync(formData); - context.setState(formData); - if (!validation.success) { - toast.error(`Validation failed: ${validation.error.message}`); - console.error(validation.error); - return; - } - try { - await deploy(validation.data); - context.setState({}); - } catch (error) { - let message = "Something went wrong"; - if (error && typeof error === "object") { - message = - "shortMessage" in error - ? (error.shortMessage as string) - : "message" in error - ? (error.message as string) - : message; - } - toast.error(message); - } - }; - - return { onValid, ...other }; -} - export function useVoucherStepper() { const context = useContext(CreateVoucherContext); diff --git a/src/components/voucher/forms/create-voucher-form/steps/sigining-and-publishing.tsx b/src/components/voucher/forms/create-voucher-form/steps/sigining-and-publishing.tsx index 7545b43..b94ae1f 100644 --- a/src/components/voucher/forms/create-voucher-form/steps/sigining-and-publishing.tsx +++ b/src/components/voucher/forms/create-voucher-form/steps/sigining-and-publishing.tsx @@ -2,17 +2,23 @@ import { zodResolver } from "@hookform/resolvers/zod"; import Link from "next/link"; import { useRouter } from "next/navigation"; +import { useState } from "react"; import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; +import StatusDisplay from "~/components/deploy-status"; import { CheckBoxField } from "~/components/forms/fields/checkbox-field"; -import { Loading } from "~/components/loading"; import { buttonVariants } from "~/components/ui/button"; import { Form } from "~/components/ui/form"; import { VoucherDeclaration } from "~/components/voucher/voucher-declaration"; import { useAuth } from "~/hooks/useAuth"; +import { trpc } from "~/lib/trpc"; import { cn } from "~/lib/utils"; +import { RouterOutput } from "~/server/api/root"; +import { InferAsyncGenerator } from "~/server/api/routers/pool"; import { StepControls } from "../controls"; -import { useVoucherData, useVoucherDeploy } from "../provider"; -import { type VoucherPublishingSchema } from "../schemas"; +import { useVoucherData } from "../provider"; +import { schemas, type VoucherPublishingSchema } from "../schemas"; import { signingAndPublishingSchema, type SigningAndPublishingFormValues, @@ -21,112 +27,146 @@ import { // This can come from your database or API. const defaultValues: Partial = {}; export const ReviewStep = () => { - const data = useVoucherData() as VoucherPublishingSchema; const router = useRouter(); + const data = useVoucherData() as VoucherPublishingSchema; const auth = useAuth(); - const { voucher, loading, info, onValid } = useVoucherDeploy(); - + const { mutateAsync: deploy } = trpc.voucher.deploy.useMutation({ + trpc: { + context: { + // Use HTTP streaming for this request + stream: true, + }, + }, + }); + const [status, setStatus] = useState< + InferAsyncGenerator[] + >([]); const form = useForm({ resolver: zodResolver(signingAndPublishingSchema), mode: "onChange", defaultValues: defaultValues, }); - if (voucher && !loading) { - void router.push(`/vouchers/${voucher.voucher_address}`); - } - if (loading || voucher) { - return ; - } + const handleDeploy = async ( + d: VoucherPublishingSchema["signingAndPublishing"] + ) => { + setStatus([]); + const formData = { ...data, signingAndPublishing: d }; + const validation = await z.object(schemas).safeParseAsync(formData); + if (!validation.success) { + toast.error("Form validation failed"); + return; + } + const generator = await deploy(validation.data); + for await (const state of generator) { + setStatus((s) => [...s, state]); + if (state.status === "success") { + router.push(`/vouchers/${state.address}`); + break; + } + if (state.status === "error") { + toast.error(state?.error ?? "An error occurred"); + break; + } + } + }; return (
-
- + +
+ +
+ + Accept + + Terms and Conditions + + + } + description="You agree to our Terms of Service and Privacy Policy" + /> + + Accept + + Public Awareness & Transparent Heritage (PATH) License + + + } + description="You allow your CAV to be freely traded/resold." + /> +
+
+ +
+ ) : ( + -
- -
- - Accept - - Terms and Conditions - - - } - description="You agree to our Terms of Service and Privacy Policy" - /> - - Accept - - Public Awareness & Transparent Heritage (PATH) License - - - } - description="You allow your CAV to be freely traded/resold." - /> -
-
- -
+ )} console.error(e))} + onNext={form.handleSubmit(handleDeploy, (e) => console.error(e))} + disabled={!form.formState.isValid || form.formState.isSubmitting} /> ); diff --git a/src/contracts/erc20-demurrage-token/index.ts b/src/contracts/erc20-demurrage-token/index.ts new file mode 100644 index 0000000..4b5e971 --- /dev/null +++ b/src/contracts/erc20-demurrage-token/index.ts @@ -0,0 +1,126 @@ +import { + parseGwei, + parseUnits, + type Chain, + type PublicClient, + type Transport, +} from "viem"; +import { calculateDecayLevel } from "~/utils/dmr-helpers"; +import { getWriterWalletClient } from "../writer"; +import { abi, bytecode } from "./contract"; + +interface DMRConstructorArgs { + name: string; + symbol: string; + expiration_rate: number; + expiration_period: number; + sink_address: `0x${string}`; +} + +export type DMRContractArgs = [ + name: string, + symbol: string, + decimals: number, + decay_level: bigint, + periodMins: bigint, + sink_address: `0x${string}`, +]; +export class DMRToken { + address: `0x${string}`; + + publicClient: PublicClient; + contract: { address: `0x${string}`; abi: typeof abi }; + private _decimals: bigint | undefined; + constructor(publicClient: PublicClient, address: `0x${string}`) { + if (!publicClient) { + throw new Error("publicClient is required"); + } + this.address = address; + this.contract = { address: this.address, abi: abi } as const; + this.publicClient = publicClient; + } + static async deploy( + publicClient: PublicClient, + args: DMRConstructorArgs + ) { + const walletClient = getWriterWalletClient(); + const decayLevel = calculateDecayLevel( + args.expiration_rate / 100, + BigInt(args.expiration_period) + ); + const contract_args: DMRContractArgs = [ + args.name, + args.symbol, + 6, // decimals + decayLevel, + BigInt(args.expiration_period), + args.sink_address, + ]; + + const hash = await walletClient.deployContract({ + abi: abi, + bytecode: bytecode, + args: contract_args, + }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + if (receipt.status !== "success" || !receipt.contractAddress) { + throw new Error("Failed to Deploy Contract"); + } + return new DMRToken(publicClient, receipt.contractAddress); + } + async getDecimals() { + if (this._decimals) return this._decimals; + this._decimals = await this.publicClient.readContract({ + ...this.contract, + functionName: "decimals", + args: [], + }); + return this._decimals; + } + async addWriter(writerAddress: `0x${string}`) { + const walletClient = getWriterWalletClient(); + const hash = await walletClient.writeContract({ + ...this.contract, + functionName: "addWriter", + args: [writerAddress], + }); + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + return receipt.status === "success"; + } + async mintTo(to: `0x${string}`, amount: number) { + const walletClient = getWriterWalletClient(); + const decimals = await this.getDecimals(); + const hash = await walletClient.writeContract({ + ...this.contract, + functionName: "mintTo", + gas: 350_000n, + maxFeePerGas: parseGwei("10"), + maxPriorityFeePerGas: 5n, + args: [to, parseUnits(amount.toString(), Number(decimals))], + }); + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + return receipt.status === "success"; + } + async transferOwnership(newOwner: `0x${string}`) { + const walletClient = getWriterWalletClient(); + const hash = await walletClient.writeContract({ + ...this.contract, + functionName: "transferOwnership", + args: [newOwner], + }); + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + return receipt.status === "success"; + } +} diff --git a/src/contracts/erc20-giftable-token/index.ts b/src/contracts/erc20-giftable-token/index.ts new file mode 100644 index 0000000..002c85d --- /dev/null +++ b/src/contracts/erc20-giftable-token/index.ts @@ -0,0 +1,118 @@ +import { + parseGwei, + parseUnits, + type Chain, + type PublicClient, + type Transport, +} from "viem"; +import { getWriterWalletClient } from "../writer"; +import { abi, bytecode } from "./contract"; + +interface GiftableConstructorArgs { + name: string; + symbol: string; + expireTimestamp: bigint; +} + +export type GiftableContractArgs = [ + name: string, + symbol: string, + decimals: number, + expireTimestamp: bigint, +]; +export class GiftableToken { + address: `0x${string}`; + + publicClient: PublicClient; + contract: { address: `0x${string}`; abi: typeof abi }; + private _decimals: number | undefined; + constructor(publicClient: PublicClient, address: `0x${string}`) { + if (!publicClient) { + throw new Error("publicClient is required"); + } + this.address = address; + this.contract = { address: this.address, abi: abi } as const; + this.publicClient = publicClient; + } + static async deploy( + publicClient: PublicClient, + args: GiftableConstructorArgs + ) { + const walletClient = getWriterWalletClient(); + const contract_args: GiftableContractArgs = [ + args.name, + args.symbol, + 6, // decimals + BigInt(0), + ]; + + const hash = await walletClient.deployContract({ + abi: abi, + bytecode: bytecode, + args: contract_args, + gas: 350_000n, + maxFeePerGas: parseGwei("10"), + maxPriorityFeePerGas: 5n, + }); + const receipt = await publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + if (receipt.status !== "success" || !receipt.contractAddress) { + throw new Error("Failed to Deploy Contract"); + } + return new GiftableToken(publicClient, receipt.contractAddress); + } + async getDecimals() { + if (this._decimals) return this._decimals; + this._decimals = await this.publicClient.readContract({ + ...this.contract, + functionName: "decimals", + args: [], + }); + return this._decimals; + } + async addWriter(writerAddress: `0x${string}`) { + const walletClient = getWriterWalletClient(); + const hash = await walletClient.writeContract({ + ...this.contract, + functionName: "addWriter", + args: [writerAddress], + }); + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + return receipt.status === "success"; + } + async mintTo(to: `0x${string}`, amount: number) { + const walletClient = getWriterWalletClient(); + const decimals = await this.getDecimals(); + const hash = await walletClient.writeContract({ + ...this.contract, + functionName: "mintTo", + gas: 350_000n, + maxFeePerGas: parseGwei("10"), + maxPriorityFeePerGas: 5n, + args: [to, parseUnits(amount.toString(), Number(decimals))], + }); + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + return receipt.status === "success"; + } + async transferOwnership(newOwner: `0x${string}`) { + const walletClient = getWriterWalletClient(); + const hash = await walletClient.writeContract({ + ...this.contract, + functionName: "transferOwnership", + args: [newOwner], + }); + const receipt = await this.publicClient.waitForTransactionReceipt({ + hash, + confirmations: 2, + }); + return receipt.status === "success"; + } +} diff --git a/src/hooks/useAuth.tsx b/src/hooks/useAuth.tsx index 6beb28a..68dfe6e 100644 --- a/src/hooks/useAuth.tsx +++ b/src/hooks/useAuth.tsx @@ -33,6 +33,7 @@ const AuthContext = createContext(undefined); export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const { data: session, status } = useSession(); // useSession() + console.log(session?.address, status) const me = trpc.me.get.useQuery(undefined, { enabled: !!session?.address, }); diff --git a/src/hooks/useVoucherDeploy.ts b/src/hooks/useVoucherDeploy.ts deleted file mode 100644 index 821d49e..0000000 --- a/src/hooks/useVoucherDeploy.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { useCallback, useState } from "react"; -import { - getAddress, - isAddress, - parseGwei, - parseUnits, - type Hash, - type TransactionReceipt, -} from "viem"; -import { usePublicClient, useWalletClient } from "wagmi"; -import { type VoucherPublishingSchema } from "~/components/voucher/forms/create-voucher-form/schemas"; -import * as dmrContract from "~/contracts/erc20-demurrage-token/contract"; -import * as giftableContract from "~/contracts/erc20-giftable-token/contract"; - -import { type RouterOutputs } from "~/lib/trpc"; -import { config } from "~/lib/web3"; -import { calculateDecayLevel } from "../utils/dmr-helpers"; -import { trpc } from "~/lib/trpc"; -export type DMRConstructorArgs = [ - name: string, - symbol: string, - decimals: number, - decay_level: bigint, - periodMins: bigint, - sink_address: `0x${string}`, -]; -export type GiftableConstructorArgs = [ - name: string, - symbol: string, - decimals: number, - expireTimestamp: bigint, -]; -// TODO: Move to the backend -export const useVoucherDeploy = ( - options: { - onSuccess?: (receipt: TransactionReceipt) => void; - } = {} -) => { - const [voucher, setVoucher] = useState(); - const [receipt, setReceipt] = useState(); - const [loading, setLoading] = useState(false); - const [info, setInfo] = useState(""); - const [hash, setHash] = useState(); - const deployMutation = trpc.voucher.deploy.useMutation({ - retry: false, - }); - const publicClient = usePublicClient({ config }); - const { data: walletClient } = useWalletClient(); - - const deployDMRContract = async (args: DMRConstructorArgs) => { - const hash = await walletClient?.deployContract({ - abi: dmrContract.abi, - args, - bytecode: dmrContract.bytecode, - gas: 7_000_000n, - maxFeePerGas: parseGwei("10"), - maxPriorityFeePerGas: 5n, - }); - if (!hash) { - throw new Error("Failed to deploy contract"); - } - setHash(hash); - return hash; - }; - - const deployGiftableContract = async (args: GiftableConstructorArgs) => { - const hash = await walletClient?.deployContract({ - abi: giftableContract.abi, - args: [args[0], args[1], args[2], args[3]], - bytecode: giftableContract.bytecode, - gas: 7_000_000n, - maxFeePerGas: parseGwei("10"), - maxPriorityFeePerGas: 5n, - }); - if (!hash) { - throw new Error("Failed to deploy contract"); - } - setHash(hash); - return hash; - }; - - const waitForReceiptAndSet = async (hash: Hash) => { - const txReceipt = await publicClient.waitForTransactionReceipt({ hash }); - if (!txReceipt.contractAddress || !isAddress(txReceipt.contractAddress)) { - throw new Error("No valid contract address found"); - } - setReceipt(txReceipt); - return txReceipt; - }; - - const mintTokens = async ( - address: `0x${string}`, - supply: number, - symbol: string - ) => { - const mintHash = await walletClient?.writeContract({ - abi: dmrContract.abi, - address, - functionName: "mintTo", - args: [walletClient.account.address, parseUnits(supply.toString(), 6)], - }); - if (!mintHash) { - throw Error("No mint hash"); - } - await publicClient.waitForTransactionReceipt({ hash: mintHash }); - setInfo(`Minted ${supply} ${symbol}`); - }; - - const deploy = useCallback( - async (input: Omit) => { - if (!walletClient) throw Error("No wallet client"); - if (!["gradual", "none"].includes(input.expiration.type)) - throw Error("Only gradual or no expiration is supported"); - - setLoading(true); - setInfo("Deploying contract"); - let txReceipt: TransactionReceipt; - try { - if (input.expiration.type === "gradual") { - if (!isAddress(input.expiration.communityFund)) - throw Error("Invalid Community Fund Address"); - const decayLevel = calculateDecayLevel( - input.expiration.rate / 100, - BigInt(input.expiration.period) - ); - const args: DMRConstructorArgs = [ - input.nameAndProducts.name, - input.nameAndProducts.symbol, - 6, // decimals - decayLevel, - BigInt(input.expiration.period), - input.expiration.communityFund, - ]; - - setInfo("Waiting for transaction receipt"); - txReceipt = await waitForReceiptAndSet(await deployDMRContract(args)); - } else if (input.expiration.type === "none") { - const args: GiftableConstructorArgs = [ - input.nameAndProducts.name, - input.nameAndProducts.symbol, - 6, // decimals - BigInt(0), - ]; - setInfo("Waiting for transaction receipt"); - txReceipt = await waitForReceiptAndSet( - await deployGiftableContract(args) - ); - } else { - throw Error("Invalid Contract Type"); - } - - if (!txReceipt.contractAddress) { - throw new Error("No contract address"); - } - setInfo("Writing to Token Index and CIC Graph"); - const voucherData = await deployMutation.mutateAsync({ - ...input, - voucherAddress: getAddress(txReceipt.contractAddress), - contractVersion: - input.expiration.type === "gradual" - ? dmrContract.version - : giftableContract.version, - type: - input.expiration.type === "gradual" - ? dmrContract.type - : giftableContract.type, - }); - setVoucher(voucherData); - - setInfo( - `Please Approve the transaction in your wallet to mint ${input.valueAndSupply.supply} ${input.nameAndProducts.symbol}` - ); - await mintTokens( - txReceipt.contractAddress, - input.valueAndSupply.supply, - input.nameAndProducts.symbol - ); - setInfo("Deployment complete! Please wait while we redirect you."); - options.onSuccess?.(txReceipt); // Updated to handle potential state delay - } finally { - setLoading(false); - } - }, - [ - walletClient, - publicClient, - deployMutation, - deployDMRContract, - deployGiftableContract, - waitForReceiptAndSet, - mintTokens, - options.onSuccess, - ] - ); - - return { deploy, info, receipt, voucher, loading, hash }; -}; diff --git a/src/lib/query-client.ts b/src/lib/query-client.ts index 85ab95a..3ae7edf 100644 --- a/src/lib/query-client.ts +++ b/src/lib/query-client.ts @@ -1,12 +1,14 @@ -import { QueryClient } from '@tanstack/react-query'; +import { QueryClient } from "@tanstack/react-query"; +import { hashFn } from "wagmi/query"; export const createQueryClient = () => new QueryClient({ defaultOptions: { queries: { + queryKeyHashFn: hashFn, // With SSR, we usually want to set some default staleTime // above 0 to avoid refetching immediately on the client staleTime: 30 * 1000, }, }, - }); \ No newline at end of file + }); diff --git a/src/server/api/models/voucher.ts b/src/server/api/models/voucher.ts new file mode 100644 index 0000000..923f067 --- /dev/null +++ b/src/server/api/models/voucher.ts @@ -0,0 +1,181 @@ +import { sql, type Kysely } from "kysely"; +import { type GraphDB } from "~/server/db"; +import { CommodityType } from "~/server/enums"; + +export class VoucherModel { + constructor(private db: Kysely) {} + + async findVoucherByAddress(address: string) { + return this.db + .selectFrom("vouchers") + .where("voucher_address", "=", address) + .select([ + "id", + "voucher_address", + "voucher_name", + "voucher_description", + "symbol", + "voucher_value", + "voucher_uoa", + "voucher_type", + "sink_address", + "contract_version", + ]) + .executeTakeFirst(); + } + + async createVoucher(voucherData: { + symbol: string; + voucher_name: string; + voucher_address: string; + voucher_description: string; + sink_address: string; + voucher_email: string; + voucher_value: string; + voucher_website: string | null; + voucher_uoa: string; + voucher_type: string; + geo: { x: number; y: number } | null; + location_name: string; + internal: boolean; + contract_version: string; + }) { + return this.db.transaction().execute(async (trx) => { + const voucher = await trx + .insertInto("vouchers") + .values(voucherData) + .returning("id") + .executeTakeFirstOrThrow(); + + return voucher; + }); + } + + async getVoucherInfo(voucherId: number) { + return this.db + .selectFrom("vouchers") + .where("id", "=", voucherId) + .select([ + "id", + "voucher_address", + "voucher_name", + "voucher_description", + "symbol", + "voucher_value", + "voucher_uoa", + "voucher_type", + "sink_address", + "voucher_email", + "voucher_website", + "geo", + "location_name", + "internal", + "contract_version", + "created_at", + ]) + .executeTakeFirstOrThrow(); + } + + async getVoucherIssuers(voucherId: number) { + return this.db + .selectFrom("voucher_issuers") + .innerJoin("personal_information", "voucher_issuers.backer", "personal_information.user_identifier") + .where("voucher_issuers.voucher", "=", voucherId) + .select(["backer", "given_names", "family_name"]) + .execute(); + } + + async addVoucherIssuer(voucherId: number, backerId: number) { + return this.db + .insertInto("voucher_issuers") + .values({ + voucher: voucherId, + backer: backerId, + }) + .executeTakeFirstOrThrow(); + } + + async getVoucherCommodities(voucherId: number) { + return this.db + .selectFrom("product_listings") + .where("voucher", "=", voucherId) + .select([ + "id", + "price", + "commodity_name", + "commodity_description", + sql`commodity_type`.as("commodity_type"), + "quantity", + "frequency", + ]) + .execute(); + } + + async addVoucherCommodity(commodityData: { + commodity_name: string; + commodity_description: string; + commodity_type: CommodityType; + voucher: number; + quantity: number; + location_name: string; + frequency: string; + account: number; + }) { + return this.db + .insertInto("product_listings") + .values(commodityData) + .executeTakeFirstOrThrow(); + } + + async updateVoucher(voucherAddress: string, updateData: { + geo?: { x: number; y: number } | null; + location_name?: string; + voucher_description?: string; + voucher_email?: string | null; + banner_url?: string | null; + icon_url?: string | null; + voucher_website?: string | null; + }) { + return this.db + .updateTable("vouchers") + .set(updateData) + .where("voucher_address", "=", voucherAddress) + .returningAll() + .executeTakeFirstOrThrow(); + } + + async deleteVoucher(voucherAddress: string) { + return this.db.transaction().execute(async (trx) => { + const voucher = await trx + .selectFrom("vouchers") + .where("voucher_address", "=", voucherAddress) + .select("id") + .executeTakeFirstOrThrow(); + + await trx + .deleteFrom("transactions") + .where("voucher_address", "=", voucherAddress) + .execute(); + + await trx + .deleteFrom("voucher_issuers") + .where("voucher", "=", voucher.id) + .execute(); + + await trx + .deleteFrom("voucher_certifications") + .where("voucher", "=", voucher.id) + .execute(); + + await trx + .deleteFrom("product_listings") + .where("voucher", "=", voucher.id) + .execute(); + + return trx + .deleteFrom("vouchers") + .where("id", "=", voucher.id) + .executeTakeFirstOrThrow(); + }); + } +} \ No newline at end of file diff --git a/src/server/api/routers/pool.ts b/src/server/api/routers/pool.ts index efba6ae..5e606d6 100644 --- a/src/server/api/routers/pool.ts +++ b/src/server/api/routers/pool.ts @@ -20,7 +20,7 @@ import { hasPermission } from "~/utils/permissions"; export type GeneratorYieldType = { message: string; - status: string; + status: "loading" | "success" | "error"; address?: `0x${string}`; error?: string; }; @@ -29,6 +29,7 @@ export type InferAsyncGenerator = // eslint-disable-next-line @typescript-eslint/no-explicit-any Gen extends AsyncGenerator ? T : never; +// Add types for the yeilds export const poolRouter = router({ create: authenticatedProcedure .input( @@ -41,7 +42,10 @@ export const poolRouter = router({ tags: z.array(z.string()).optional(), }) ) - .mutation(async function* ({ ctx, input }) { + .mutation(async function* ({ + ctx, + input, + }): AsyncGenerator { try { yield { message: "Deploying", status: "loading" }; @@ -195,9 +199,9 @@ export const poolRouter = router({ }; } catch (error) { console.error("Error during pool retrieval:", error); - if((error as Error).message.includes("no result")){ + if ((error as Error).message.includes("no result")) { return null; - } + } throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Pool not found", diff --git a/src/server/api/routers/voucher.ts b/src/server/api/routers/voucher.ts index 5f35084..29dd563 100644 --- a/src/server/api/routers/voucher.ts +++ b/src/server/api/routers/voucher.ts @@ -3,24 +3,22 @@ import { getAddress, isAddress } from "viem"; import { z } from "zod"; import { schemas } from "~/components/voucher/forms/create-voucher-form/schemas"; import { VoucherIndex } from "~/contracts"; +import { DMRToken } from "~/contracts/erc20-demurrage-token"; +import * as dmrContract from "~/contracts/erc20-demurrage-token/contract"; +import { GiftableToken } from "~/contracts/erc20-giftable-token"; +import * as giftableContract from "~/contracts/erc20-giftable-token/contract"; import { getIsOwner } from "~/contracts/helpers"; import { authenticatedProcedure, publicProcedure, router, } from "~/server/api/trpc"; +import { publicClient } from "~/server/client"; import { sendVoucherEmbed } from "~/server/discord"; import { AccountRoleType, CommodityType } from "~/server/enums"; import { getPermissions } from "~/utils/permissions"; -const insertVoucherInput = z.object({ - ...schemas, - voucherAddress: z - .string() - .refine(isAddress, { message: "Invalid address format" }), - contractVersion: z.string(), - type: z.enum(["DEMURRAGE", "GIFTABLE"]), -}); +const insertVoucherInput = z.object(schemas); const updateVoucherInput = z.object({ geo: z .object({ @@ -42,6 +40,15 @@ export type UpdateVoucherInput = z.infer; export type DeployVoucherInput = z.infer; +export type GeneratorYieldType = { + message: string; + status: "loading" | "success" | "error"; + address?: `0x${string}`; + error?: string; +}; +export type DeploymentStatus = { + step: number; +}; export const voucherRouter = router({ list: publicProcedure.query(({ ctx }) => { return ctx.graphDB.selectFrom("vouchers").selectAll().execute(); @@ -213,115 +220,179 @@ export const voucherRouter = router({ }), deploy: authenticatedProcedure .input(insertVoucherInput) - .mutation(async ({ ctx, input }) => { - const voucherAddress = getAddress(input.voucherAddress); + .mutation(async function* ({ + ctx, + input, + }): AsyncGenerator { + if (!ctx.session?.user?.id) { + throw new TRPCError({ + code: "UNAUTHORIZED", + message: `You must be logged in to deploy a voucher`, + }); + } if (!["gradual", "none"].includes(input.expiration.type)) { throw new TRPCError({ code: "BAD_REQUEST", message: `Only gradual or none expiration is supported`, }); } - const communityFund = - input.expiration.type === "gradual" - ? input.expiration.communityFund - : ""; - if (!ctx.session?.user?.id) { + + try { + const userAddress = getAddress(ctx.session.address); + yield { message: "Deploying your Token", status: "loading" }; throw new TRPCError({ - code: "UNAUTHORIZED", - message: `You must be logged in to deploy a voucher`, + code: "INTERNAL_SERVER_ERROR", + message: `Failed to deploy Token`, }); - } - const internal = ctx.session.user.role !== AccountRoleType.USER; - - const voucher = await ctx.graphDB.transaction().execute(async (trx) => { - // Create Voucher in DB - const v = await trx - .insertInto("vouchers") - .values({ + let token; + let communityFund = ""; + if (input.expiration.type === "gradual") { + communityFund = input.expiration.communityFund; + token = await DMRToken.deploy(publicClient, { + name: input.nameAndProducts.name, symbol: input.nameAndProducts.symbol, - voucher_name: input.nameAndProducts.name, - voucher_address: voucherAddress, - voucher_description: input.nameAndProducts.description, - sink_address: communityFund, - voucher_email: input.aboutYou.email, - voucher_value: input.valueAndSupply.value, - voucher_website: input.aboutYou.website, - voucher_uoa: input.valueAndSupply.uoa, - voucher_type: input.type, - geo: input.aboutYou.geo, - location_name: input.aboutYou.location ?? " ", - internal: internal, - contract_version: input.contractVersion, - }) - .returningAll() - .executeTakeFirst() - .catch((error) => { - console.error("Failed to insert voucher:", error); - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to add voucher to graph`, - cause: error, - }); + expiration_rate: input.expiration.rate, + expiration_period: input.expiration.period, + sink_address: input.expiration.communityFund, + }); + } + if (input.expiration.type === "none") { + token = await GiftableToken.deploy(publicClient, { + name: input.nameAndProducts.name, + symbol: input.nameAndProducts.symbol, + expireTimestamp: BigInt(0), }); - if (!v || !v.id) { + } + if (!token) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Failed to add voucher to graph`, + message: `Failed to deploy Token`, }); } - // Add Issuer to DB - await trx - .insertInto("voucher_issuers") - .values({ - voucher: v.id, - backer: ctx.session.user.account_id, - }) - .returningAll() - .executeTakeFirst(); + const contractVersion = + input.expiration.type === "gradual" + ? dmrContract.version + : giftableContract.version; + const type = + input.expiration.type === "gradual" + ? dmrContract.type + : giftableContract.type; + const voucherAddress = getAddress(token.address); + + yield { + message: `Minting ${input.valueAndSupply.supply} ${input.nameAndProducts.symbol}`, + status: "loading", + }; + await token.mintTo(userAddress, input.valueAndSupply.supply); + + yield { message: "Transferring Ownership", status: "loading" }; + await token.transferOwnership(userAddress); - if ( - input.nameAndProducts?.products && - input.nameAndProducts.products.length >= 1 - ) { + yield { message: "Adding to Database", status: "loading" }; + + const internal = ctx.session.user.role !== AccountRoleType.USER; + const voucher = await ctx.graphDB.transaction().execute(async (trx) => { + // Create Voucher in DB + const v = await trx + .insertInto("vouchers") + .values({ + symbol: input.nameAndProducts.symbol, + voucher_name: input.nameAndProducts.name, + voucher_address: voucherAddress, + voucher_description: input.nameAndProducts.description, + sink_address: communityFund, + voucher_email: input.aboutYou.email, + voucher_value: input.valueAndSupply.value, + voucher_website: input.aboutYou.website, + voucher_uoa: input.valueAndSupply.uoa, + voucher_type: type, + geo: input.aboutYou.geo, + location_name: input.aboutYou.location ?? " ", + internal: internal, + contract_version: contractVersion, + }) + .returningAll() + .executeTakeFirst() + .catch((error) => { + console.error("Failed to insert voucher:", error); + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Failed to add voucher to graph`, + cause: error, + }); + }); + if (!v || !v.id) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Failed to add voucher to graph`, + }); + } + // Add Issuer to DB await trx - .insertInto("product_listings") - .values( - input.nameAndProducts.products.map((product) => ({ - commodity_name: product.name, - commodity_description: product.description ?? "", - commodity_type: CommodityType.GOOD, - voucher: v.id, - quantity: product.quantity, - location_name: input.aboutYou.location ?? " ", - frequency: product.frequency, - account: ctx.session.user.account_id, - })) - ) + .insertInto("voucher_issuers") + .values({ + voucher: v.id, + backer: ctx.session.user.account_id, + }) .returningAll() - .execute(); - } - // Add Voucher to Token Index Contract - try { - const success = await VoucherIndex.add(voucherAddress); + .executeTakeFirst(); - if (!success) { + if ( + input.nameAndProducts?.products && + input.nameAndProducts.products.length >= 1 + ) { + await trx + .insertInto("product_listings") + .values( + input.nameAndProducts.products.map((product) => ({ + commodity_name: product.name, + commodity_description: product.description ?? "", + commodity_type: CommodityType.GOOD, + voucher: v.id, + quantity: product.quantity, + location_name: input.aboutYou.location ?? " ", + frequency: product.frequency, + account: ctx.session.user.account_id, + })) + ) + .returningAll() + .execute(); + } + // Add Voucher to Token Index Contract + try { + const success = await VoucherIndex.add(voucherAddress); + + if (!success) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: `Transaction Reverted`, + }); + } + } catch (error) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", - message: `Transaction Reverted`, + message: `Failed to write to Token Index`, + cause: error, }); } - } catch (error) { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: `Failed to write to Token Index`, - cause: error, - }); - } - return v; - }); - await sendVoucherEmbed(voucher, "Create"); + return v; + }); + yield { + message: "Deployment Complete", + address: voucherAddress, + status: "success", + }; + await sendVoucherEmbed(voucher, "Create"); - return voucher; + return voucher; + } catch (error) { + console.error("Failed to deploy Token", error); + yield { + message: "Deployment Failed", + status: "error", + error: (error as Error)?.message, + }; + } }), update: authenticatedProcedure .input(updateVoucherInput)