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
-
+
{isLastStep ? "Publish" : isOptionalStep ? "Skip" : "Next"}
>
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 (
+ )}
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)