diff --git a/apps/web/.env.example b/apps/web/.env.example index 79c3856..ef3b496 100644 --- a/apps/web/.env.example +++ b/apps/web/.env.example @@ -1,5 +1,5 @@ NEXT_PUBLIC_EVENTS_FACTORY_CONTRACT_ADDRESS= -NEXT_PUBLIC_PINATA_GATEWAY= +PINATA_IPFS_ENDPOINT= NEXT_PUBLIC_GOOGLE_MAPS_API_KEY= LINEA_TEST_RPC_ENDPOINT= @@ -8,6 +8,6 @@ PINATA_API_KEY= INFURA_API_KEY= INFURA_API_SECRET= - +ACCOUNT_PRIVATE_KEY NETWORK=localhost diff --git a/apps/web/.gitignore b/apps/web/.gitignore index 1437c53..849ad97 100644 --- a/apps/web/.gitignore +++ b/apps/web/.gitignore @@ -32,3 +32,6 @@ yarn-error.log* # vercel .vercel + +# generated contract artifacts +lib/contracts/* \ No newline at end of file diff --git a/apps/web/app/api/gas/route.ts b/apps/web/app/api/gas/route.ts index e5c80dc..20b73bb 100644 --- a/apps/web/app/api/gas/route.ts +++ b/apps/web/app/api/gas/route.ts @@ -1,11 +1,12 @@ import { NextResponse } from "next/server"; +import { env } from "@/env.mjs"; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const chainId = searchParams.get("chainId"); const Auth = Buffer.from( - process.env.INFURA_API_KEY + ":" + process.env.INFURA_API_SECRET + env.INFURA_API_KEY + ":" + env.INFURA_API_SECRET ).toString("base64"); const gasPricesResp = await fetch( diff --git a/apps/web/app/api/ipfs/route.ts b/apps/web/app/api/ipfs/route.ts new file mode 100644 index 0000000..af132a6 --- /dev/null +++ b/apps/web/app/api/ipfs/route.ts @@ -0,0 +1,28 @@ +import { env } from "@/env.mjs"; + +export async function POST(request: Request) { + const data = await request.formData(); + const baseUrl = env.INFURA_IPFS_ENDPOINT; + + try { + const res = await fetch(`${baseUrl}/api/v0/add`, { + method: "POST", + headers: { + Authorization: + "Basic " + + Buffer.from( + env.INFURA_API_KEY + ":" + env.INFURA_API_SECRET + ).toString("base64"), + }, + body: data, + }); + + + const json = await res.json(); + + return Response.json({ hash: json.Hash }); + } catch (error) { + console.error(error); + throw new Error("Error adding file"); + } +} diff --git a/apps/web/app/events/[eventId]/layout.tsx b/apps/web/app/events/[eventId]/layout.tsx index a164dfa..97d84f5 100644 --- a/apps/web/app/events/[eventId]/layout.tsx +++ b/apps/web/app/events/[eventId]/layout.tsx @@ -1,5 +1,4 @@ -import { getEventContract } from "@/lib/getEventContract"; -import { ContractPermission } from "@/types"; +import { getEventById } from "@/lib/actions"; import Image from "next/image"; import { notFound } from "next/navigation"; @@ -12,17 +11,7 @@ export async function generateMetadata({ return notFound(); } - const eventContract = await getEventContract({ - address: eventId, - permission: ContractPermission.READ, - }); - - const eventData = await Promise.all([ - eventContract.title(), - eventContract.description(), - ]); - - const [title, description] = eventData; + const { title, description } = await getEventById(eventId); return { title: `${title} - Eventsea`, diff --git a/apps/web/app/events/[eventId]/page.tsx b/apps/web/app/events/[eventId]/page.tsx index f418d52..edf0704 100644 --- a/apps/web/app/events/[eventId]/page.tsx +++ b/apps/web/app/events/[eventId]/page.tsx @@ -1,12 +1,12 @@ import { notFound } from "next/navigation"; import GetTickets from "@/components/GetTickets"; import Image from "next/image"; -import { getEventContract } from "@/lib/getEventContract"; import { format } from "date-fns"; -import { getTicketContract } from "@/lib/getTicketContract"; -import { ContractPermission } from "@/types"; import EventLocationMap from "@/components/event-location-map"; +import { env } from "@/env.mjs"; +import { getEventById, getTicketById } from "@/lib/actions"; + type PageProps = { params: { eventId: string; @@ -18,48 +18,16 @@ const EventPage = async ({ params: { eventId } }: PageProps) => { return notFound(); } - const eventContract = await getEventContract({ - address: eventId, - permission: ContractPermission.READ, - }); - - const eventData = await Promise.all([ - eventContract.title(), - eventContract.description(), - eventContract.location(), - eventContract.eventType(), - eventContract.image(), - eventContract.date(), - eventContract.ticketNFT(), - ]); - - const [title, description, location, eventType, image, date, ticketNFT] = - eventData; - - const ticketContract = await getTicketContract({ - address: ticketNFT, - permission: ContractPermission.READ, - }); - - const ticketPrice = await ticketContract._ticketPrice(); - - const ticketId = await ticketContract.tokenId(); + const { title, eventType, description, location, date, ticketNFT, image } = + await getEventById(eventId); + const { id, price } = await getTicketById(ticketNFT); const formattedDate = format(new Date(Number(date) * 1000), "MMM. d"); return (
- {title} + {title}
@@ -88,11 +56,11 @@ const EventPage = async ({ params: { eventId } }: PageProps) => {
diff --git a/apps/web/components/CardSkeleton.tsx b/apps/web/components/CardSkeleton.tsx index e381c3f..f90b908 100644 --- a/apps/web/components/CardSkeleton.tsx +++ b/apps/web/components/CardSkeleton.tsx @@ -17,7 +17,7 @@ export const CardSkeleton = ({ onLoad={() => setIsLoading(false)} onError={() => setIsLoading(false)} className="opacity-0" - /> + />
); }; \ No newline at end of file diff --git a/apps/web/components/EventCard.tsx b/apps/web/components/EventCard.tsx index 66cd12c..0752491 100644 --- a/apps/web/components/EventCard.tsx +++ b/apps/web/components/EventCard.tsx @@ -14,7 +14,7 @@ export const EventCard = ({ event }: { event: EventSea.Event }) => { return ( <> - {isLoading ? ( + {false ? ( ) : (
{
setIsLoading(false)} objectFit="cover" alt={event.title} diff --git a/apps/web/components/ForYou.tsx b/apps/web/components/ForYou.tsx index a5d7ec1..265a6f9 100644 --- a/apps/web/components/ForYou.tsx +++ b/apps/web/components/ForYou.tsx @@ -36,7 +36,7 @@ const ForYou: React.FC = ({ events }) => { {events.map((event, index) => { return ( - + = ({ events }) => { >
{event.title} = ({ }; const handleMinTickets = async () => { - const nftContract = await getTicketContract({ - address: ticketNFT, - permission: ContractPermission.WRITE, - }); + const provider = new ethers.BrowserProvider(window.ethereum!); + + const signer = await provider.getSigner(); + + const ticketsContract = new ethers.Contract( + ticketNFT, + TicketContract.abi, + signer + ) as unknown as Ticket; + if (numberOfTickets <= 0) { console.log("Please select number of tickets"); return; @@ -78,7 +84,7 @@ const GetTickets: React.FC = ({ const Hash = await handleUploadSVG(); const totalAmount = ticketPrice * BigInt(numberOfTickets); const token = ( - await nftContract.mint( + await ticketsContract.mint( numberOfTickets, `https://ipfs.io/ipfs/${Hash}`, { diff --git a/apps/web/components/Navbar.tsx b/apps/web/components/Navbar.tsx index 6355d5d..e3dc5d4 100644 --- a/apps/web/components/Navbar.tsx +++ b/apps/web/components/Navbar.tsx @@ -2,45 +2,37 @@ import Link from "next/link"; import { useSDK } from "@metamask/sdk-react"; +import { isHexString } from "ethers"; import EventSeaLogo from "../public/icons/EventSeaLogo"; import WalletIcon from "../public/icons/WalletIcon"; import CreateEvent from "@/components/create-event/create-event-form"; import { Button } from "./ui/Button"; import { SearchBar } from "./SearchBar"; -import { formatAddress } from "./../lib/utils"; +import { formatAddress, getAppChainId } from "./../lib/utils"; import { Popover, PopoverTrigger, PopoverContent, } from "@/components/ui/popover"; import MetaMaskProvider from "@/providers/MetamaskProvider"; -import { useEffect, useState } from "react"; -const LINEA_TESTNET_CHAIN = "0xe704"; +import { env } from "@/env.mjs"; + +const appChainId = getAppChainId(); const switchEthereumChain = async () => { if (!window.ethereum) return; await window.ethereum.request({ method: "wallet_switchEthereumChain", - params: [{ chainId: LINEA_TESTNET_CHAIN }], + params: [{ chainId: appChainId }], }); window.location.reload(); }; export const ConnectWalletButton = () => { - const [chainId, setChainId] = useState(null); - const { sdk, connected, connecting, account } = useSDK(); - - useEffect(() => { - if (window?.ethereum?.chainId) { - setChainId(window?.ethereum?.chainId); - } - }, []); - - const isOnLineaTestnet = chainId === LINEA_TESTNET_CHAIN; - const isOnLocal = chainId === "0x7a69"; + const { sdk, connected, connecting, chainId, account } = useSDK(); const connect = async () => { try { @@ -59,7 +51,7 @@ export const ConnectWalletButton = () => { return (
{connected ? ( - isOnLineaTestnet || isOnLocal ? ( + chainId === appChainId ? ( @@ -81,7 +73,7 @@ export const ConnectWalletButton = () => { ) : ( ) ) : ( diff --git a/apps/web/components/create-event/create-event-form.tsx b/apps/web/components/create-event/create-event-form.tsx index f455a95..4d4747f 100644 --- a/apps/web/components/create-event/create-event-form.tsx +++ b/apps/web/components/create-event/create-event-form.tsx @@ -1,12 +1,16 @@ "use client"; -import { useEffect, useState, useTransition } from "react"; +import { useState } from "react"; import { z } from "zod"; import { useRouter } from "next/navigation"; import { useSDK } from "@metamask/sdk-react"; import { useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; import { RotateCw, Plus } from "lucide-react"; -import { parseEther } from "ethers"; +import { ethers, parseEther } from "ethers"; + +import EventFactoryContract from "lib/contracts/artifacts/EventsFactory.sol/EventsFactory.json"; +import { EventsFactory } from "@/lib/contracts/typechain-types"; import { Dialog, @@ -20,39 +24,30 @@ import { import { Button } from "../ui/Button"; import { Form } from "@/components/ui/form"; -import { getEventFactoryContract } from "@/lib/getEventFactoryContract"; -import { add } from "@/lib/ipfs"; import { formSchema } from "./schema"; -import { ContractPermission, EventSea } from "@/types"; +import { EventSea } from "@/types"; import Step1 from "./step-1"; import Step2 from "./step-2"; import Step3 from "./step-3"; +import { getAppChainId } from "@/lib/utils"; +import { env } from "@/env.mjs"; const NUM_OF_STEPS = 3; -const LINEA_TESTNET_CHAIN = "0xe704"; const CreateEventForm = () => { const [step, setStep] = useState(1); const [open, setOpen] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); - const [isPending, startTransition] = useTransition(); - const [chainId, setChainId] = useState(null); - useEffect(() => { - if (window?.ethereum?.chainId) { - setChainId(window?.ethereum?.chainId); - } - }, []); + const appChainId = getAppChainId(); - const { connected } = useSDK(); - const isOnLineaTestnet = chainId === LINEA_TESTNET_CHAIN; - const isOnLocal = chainId === "0x7a69"; + const { connected, chainId } = useSDK(); const router = useRouter(); const form = useForm>({ - // resolver: zodResolver(formSchema), + resolver: zodResolver(formSchema), defaultValues: { title: "", location: { @@ -95,9 +90,6 @@ const CreateEventForm = () => { async function onSubmit(values: z.infer) { setIsSubmitting(true); - const eventFactory = await getEventFactoryContract({ - permission: ContractPermission.WRITE, - }); const { title, @@ -110,39 +102,54 @@ const CreateEventForm = () => { image, } = values; - startTransition(async () => { - const formData = new FormData(); - let imageHash: string | undefined; + const formData = new FormData(); + let imageHash: string | undefined; - if (image) { - formData.append("file", image); - imageHash = await add(formData); - } + if (image) { + formData.append("file", image); + const response = await fetch('/api/ipfs', { + method: "POST", + body: formData + }) - const ticketPriceInWei = parseEther(ticketPrice.price.toString()); - - try { - const resp = await eventFactory.createEvent( - title, - description, - location.placeId, - type, - imageHash || "", - Math.floor(dateTime.getTime() / 1000), - ticketPriceInWei, - BigInt(amountOfTickets) - ); - - await resp.wait(); - form.reset(); - setIsSubmitting(false); - router.refresh(); - setOpen((open) => !open); - } catch (error) { - console.log(error); - setIsSubmitting(false); + if(response.ok ) { + imageHash = (await response.json()).hash } - }); + } + + const ticketPriceInWei = parseEther(ticketPrice.price.toString()); + + const provider = new ethers.BrowserProvider(window.ethereum!); + + const signer = await provider.getSigner(); + + const eventFactory = new ethers.Contract( + env.NEXT_PUBLIC_EVENTS_FACTORY_CONTRACT_ADDRESS, + EventFactoryContract.abi, + signer + ) as unknown as EventsFactory; + + try { + const resp = await eventFactory.createEvent( + title, + description, + location.placeId, + type, + imageHash || "", + Math.floor(dateTime.getTime() / 1000), + ticketPriceInWei, + BigInt(amountOfTickets) + ); + + await resp.wait(); + form.reset(); + setIsSubmitting(false); + router.refresh(); + setOpen((open) => !open); + } catch (error) { + console.log(error); + setIsSubmitting(false); + } } return ( @@ -155,13 +162,12 @@ const CreateEventForm = () => { }} > - {(connected && isOnLineaTestnet) || - (isOnLocal && ( - - ))} + {connected && chainId === appChainId && ( + + )}
diff --git a/apps/web/components/event-location-map.tsx b/apps/web/components/event-location-map.tsx index 3ec821e..6234799 100644 --- a/apps/web/components/event-location-map.tsx +++ b/apps/web/components/event-location-map.tsx @@ -10,6 +10,8 @@ import { FC, useEffect, useMemo, useState } from "react"; import { getDetails } from "use-places-autocomplete"; import LocationIcon from "@/components/icons/LocationIcon"; +import { env } from "@/env.mjs"; + interface EventLocationMapProps { location: string; } @@ -25,7 +27,7 @@ const EventLocationMap: FC = ({ location }) => { const libraries: Libraries = useMemo(() => ["places"], []); const { isLoaded } = useLoadScript({ - googleMapsApiKey: process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, + googleMapsApiKey: env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY!, libraries, }); diff --git a/apps/web/components/location-autosuggest-input.tsx b/apps/web/components/location-autosuggest-input.tsx index 1a1c992..0cd87ea 100644 --- a/apps/web/components/location-autosuggest-input.tsx +++ b/apps/web/components/location-autosuggest-input.tsx @@ -14,6 +14,8 @@ import { CommandList, } from "./ui/command"; +import { env } from "@/env.mjs"; + interface LocationAutoSuggestInputProps { fieldValue: { placeId: string; @@ -50,7 +52,7 @@ const LocationAutoSuggestInput: FC = ({ }} >