From 9d123b37429da8322efca05a711277ed9d3c7c93 Mon Sep 17 00:00:00 2001 From: Jamie Harding Date: Mon, 30 Sep 2024 14:27:06 +0200 Subject: [PATCH] format --- .../[strategyName]/[deployment]/page.tsx | 121 +- app/[projectName]/[strategyName]/page.tsx | 72 +- app/[projectName]/page.tsx | 42 +- app/_components/DeploymentCard.tsx | 48 +- app/_components/FrameImage.tsx | 513 +- app/_components/NavBar.tsx | 64 +- app/_components/ProgressBar.tsx | 58 +- app/_components/QuotesTable.tsx | 174 +- app/_components/ShareStateAsUrl.tsx | 72 +- app/_components/StrategyAnalytics.tsx | 219 +- app/_components/StrategyCard.tsx | 34 +- app/_components/SubmissionModal.tsx | 899 +- app/_components/TokenAndBalance.tsx | 37 +- app/_components/TradesTable.tsx | 131 +- app/_components/WebappFrame.tsx | 492 +- app/_components/WithdrawalModal.tsx | 267 +- app/_queries/getOrders.tsx | 62 +- app/_queries/strategyAnalytics.tsx | 66 +- app/_queries/subgraphs.tsx | 10 +- app/_schemas/failsafeWithNumbers.ts | 20 +- app/_services/buildProjectHome.tsx | 95 +- app/_services/buttonsData.ts | 410 +- app/_services/compress.ts | 46 +- app/_services/dates.ts | 8 +- app/_services/frameButtons.tsx | 92 +- app/_services/frameState.ts | 303 +- app/_services/frameTransactions.ts | 158 +- app/_services/getPublicClient.tsx | 38 +- app/_services/getTokenInfo.tsx | 107 +- app/_services/parseDotrainFrontmatter.tsx | 55 +- app/_services/tokenApproval.ts | 64 +- app/_services/transactionData.ts | 104 +- app/_types/frame.ts | 30 +- app/_types/yamlData.ts | 106 +- .../[strategyName]/[deployment]/frames.ts | 116 +- .../[strategyName]/[deployment]/route.tsx | 175 +- app/my-strategies/[transactionId]/page.tsx | 10 +- app/my-strategies/page.tsx | 3 +- components/ui/button.tsx | 91 +- components/ui/dialog.tsx | 146 +- components/ui/form.tsx | 273 +- components/ui/input.tsx | 39 +- components/ui/label.tsx | 31 +- package.json | 2 +- postcss.config.js | 10 +- public/_abis/OrderBook.ts | 7851 ++++++++--------- 46 files changed, 6685 insertions(+), 7079 deletions(-) diff --git a/app/[projectName]/[strategyName]/[deployment]/page.tsx b/app/[projectName]/[strategyName]/[deployment]/page.tsx index f6a3c75b..6f7535b7 100644 --- a/app/[projectName]/[strategyName]/[deployment]/page.tsx +++ b/app/[projectName]/[strategyName]/[deployment]/page.tsx @@ -1,82 +1,73 @@ -import { fetchMetadata } from "frames.js/next"; -import { Metadata } from "next"; -import WebappFrame from "@/app/_components/WebappFrame"; -import fs from "fs"; -import path from "path"; +import { fetchMetadata } from 'frames.js/next'; +import { Metadata } from 'next'; +import WebappFrame from '@/app/_components/WebappFrame'; +import fs from 'fs'; +import path from 'path'; interface generateMetadataProps { - params: { - projectName: string; - strategyName: string; - deployment: string; - }; + params: { + projectName: string; + strategyName: string; + deployment: string; + }; } -export async function generateMetadata({ - params, -}: generateMetadataProps): Promise { - // get all dirs in the project name and find the one that ends with the project name - const allDirs = fs.readdirSync( - path.join(process.cwd(), "public", "_strategies", params.projectName) - ); +export async function generateMetadata({ params }: generateMetadataProps): Promise { + // get all dirs in the project name and find the one that ends with the project name + const allDirs = fs.readdirSync( + path.join(process.cwd(), 'public', '_strategies', params.projectName) + ); - const strategyDir = allDirs.find((dir) => dir.endsWith(params.strategyName)); + const strategyDir = allDirs.find((dir) => dir.endsWith(params.strategyName)); - if (!strategyDir) { - throw new Error( - `No directory found for strategy: ${params.strategyName} in project: ${params.projectName}` - ); - } - return { - other: { - ...(await fetchMetadata( - new URL( - `/frames/${params.projectName}/${params.strategyName}/${params.deployment}`, - process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000" - ) - )), - }, - }; + if (!strategyDir) { + throw new Error( + `No directory found for strategy: ${params.strategyName} in project: ${params.projectName}` + ); + } + return { + other: { + ...(await fetchMetadata( + new URL( + `/frames/${params.projectName}/${params.strategyName}/${params.deployment}`, + process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000' + ) + )) + } + }; } interface homeProps { - params: { - projectName: string; - strategyName: string; - deployment: string; - }; + params: { + projectName: string; + strategyName: string; + deployment: string; + }; } export default async function Home({ params }: homeProps) { - // get all dirs in the project name and find the one that ends with the project name - const allDirs = fs.readdirSync( - path.join(process.cwd(), "public", "_strategies", params.projectName) - ); + // get all dirs in the project name and find the one that ends with the project name + const allDirs = fs.readdirSync( + path.join(process.cwd(), 'public', '_strategies', params.projectName) + ); - const strategyDir = allDirs.find((dir) => dir.endsWith(params.strategyName)); + const strategyDir = allDirs.find((dir) => dir.endsWith(params.strategyName)); - if (!strategyDir) { - throw new Error( - `No directory found for strategy: ${params.strategyName} in project: ${params.projectName}` - ); - } + if (!strategyDir) { + throw new Error( + `No directory found for strategy: ${params.strategyName} in project: ${params.projectName}` + ); + } - const filePath = path.join( - process.cwd(), - "public", - "_strategies", - params.projectName, - strategyDir, - `${params.strategyName}.rain` - ); - const dotrainText = fs.readFileSync(filePath, "utf8"); + const filePath = path.join( + process.cwd(), + 'public', + '_strategies', + params.projectName, + strategyDir, + `${params.strategyName}.rain` + ); + const dotrainText = fs.readFileSync(filePath, 'utf8'); - return ( - - ); + return ; } diff --git a/app/[projectName]/[strategyName]/page.tsx b/app/[projectName]/[strategyName]/page.tsx index a872d155..d00ceb26 100644 --- a/app/[projectName]/[strategyName]/page.tsx +++ b/app/[projectName]/[strategyName]/page.tsx @@ -1,50 +1,46 @@ -import { fetchMetadata } from "frames.js/next"; -import { Metadata } from "next"; -import WebappFrame from "../../_components/WebappFrame"; -import fs from "fs"; -import path from "path"; +import { fetchMetadata } from 'frames.js/next'; +import { Metadata } from 'next'; +import WebappFrame from '../../_components/WebappFrame'; +import fs from 'fs'; +import path from 'path'; interface generateMetadataProps { - params: { - projectName: string; - strategyName: string; - }; + params: { + projectName: string; + strategyName: string; + }; } -export async function generateMetadata({ - params, -}: generateMetadataProps): Promise { - return { - other: { - ...(await fetchMetadata( - new URL( - `/frames/${params.projectName}/${params.strategyName}`, - process.env.VERCEL_URL - ? `https://${process.env.VERCEL_URL}` - : "http://localhost:3000" - ) - )), - }, - }; +export async function generateMetadata({ params }: generateMetadataProps): Promise { + return { + other: { + ...(await fetchMetadata( + new URL( + `/frames/${params.projectName}/${params.strategyName}`, + process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000' + ) + )) + } + }; } interface homeProps { - params: { - projectName: string; - strategyName: string; - }; + params: { + projectName: string; + strategyName: string; + }; } export default async function Home({ params }: homeProps) { - const filePath = path.join( - process.cwd(), - "public", - "_strategies", - params.projectName, - params.strategyName, - `${params.strategyName}.rain` - ); - const dotrainText = fs.readFileSync(filePath, "utf8"); + const filePath = path.join( + process.cwd(), + 'public', + '_strategies', + params.projectName, + params.strategyName, + `${params.strategyName}.rain` + ); + const dotrainText = fs.readFileSync(filePath, 'utf8'); - return ; + return ; } diff --git a/app/[projectName]/page.tsx b/app/[projectName]/page.tsx index 976bd943..80e574d9 100644 --- a/app/[projectName]/page.tsx +++ b/app/[projectName]/page.tsx @@ -1,29 +1,27 @@ -import { retrieveProjectData } from "../_services/buildProjectHome"; -import { StrategyCard } from "../_components/StrategyCard"; -import Markdown from "react-markdown"; -import rehypeRaw from "rehype-raw"; +import { retrieveProjectData } from '../_services/buildProjectHome'; +import { StrategyCard } from '../_components/StrategyCard'; +import Markdown from 'react-markdown'; +import rehypeRaw from 'rehype-raw'; interface homeProps { - params: { - projectName: string; - }; + params: { + projectName: string; + }; } export default async function ProjectHome({ params }: homeProps) { - const projectData = await retrieveProjectData(params.projectName); + const projectData = await retrieveProjectData(params.projectName); - return ( -
-
- - {projectData.webappMDText} - -
-
- {projectData.yamlDatas.map((data) => ( - - ))} -
-
- ); + return ( +
+
+ {projectData.webappMDText} +
+
+ {projectData.yamlDatas.map((data) => ( + + ))} +
+
+ ); } diff --git a/app/_components/DeploymentCard.tsx b/app/_components/DeploymentCard.tsx index f5e89ec2..084cf3ed 100644 --- a/app/_components/DeploymentCard.tsx +++ b/app/_components/DeploymentCard.tsx @@ -1,31 +1,31 @@ -"use client"; +'use client'; -import { useRouter } from "nextjs-toploader/app"; -import { Button } from "flowbite-react"; -import { YamlData } from "../_types/yamlData"; +import { useRouter } from 'nextjs-toploader/app'; +import { Button } from 'flowbite-react'; +import { YamlData } from '../_types/yamlData'; export const DeploymentCard = ({ - deployment, - slug, + deployment, + slug }: { - deployment: YamlData["gui"]["deployments"][0]; - slug: string; + deployment: YamlData['gui']['deployments'][0]; + slug: string; }) => { - const router = useRouter(); - const currentHostname = window.location.hostname; - const handleClick = () => { - router.push(`${slug}/${deployment.deployment}`); - }; + const router = useRouter(); + const currentHostname = window.location.hostname; + const handleClick = () => { + router.push(`${slug}/${deployment.deployment}`); + }; - return ( -
-
-
{deployment.name}
-
{deployment.description}
-
- -
- ); + return ( +
+
+
{deployment.name}
+
{deployment.description}
+
+ +
+ ); }; diff --git a/app/_components/FrameImage.tsx b/app/_components/FrameImage.tsx index 942eb5e5..2ce461c5 100644 --- a/app/_components/FrameImage.tsx +++ b/app/_components/FrameImage.tsx @@ -1,272 +1,247 @@ -import { FrameState } from "../_types/frame"; -import { ProgressBar } from "./ProgressBar"; +import { FrameState } from '../_types/frame'; +import { ProgressBar } from './ProgressBar'; export const FrameImage = ({ currentState }: { currentState: FrameState }) => { - return ( -
-
- -
- {currentState.deploymentOption ? ( -
- {currentState.deploymentOption.name} -
- ) : ( - "" - )} -
- {currentState.currentStep === "start" ? ( -
-
{currentState.strategyName}
-
- {currentState.strategyDescription} -
-
- ) : ( - "" - )} - {currentState.currentStep === "deployment" - ? "Choose a strategy variation." - : ""} - {currentState.currentStep === "fields" && - currentState.deploymentOption ? ( -
-
- { - currentState.deploymentOption.fields[ - Object.keys(currentState.bindings).length - ].name - } -
-
- { - currentState.deploymentOption.fields[ - Object.keys(currentState.bindings).length - ].description - } -
-
- ) : ( - "" - )} - {currentState.currentStep === "deposit" && currentState.deploymentOption - ? `Choose your deposit amount for ${ - currentState.tokenInfos.find( - (info) => - info.yamlName === - currentState.deploymentOption!.deposits[ - Object.keys(currentState.deposits).length - ].token - )?.symbol - }.` - : ""} - {currentState.currentStep === "review" && - currentState.deploymentOption ? ( - - - - - - - - - - {Object.keys(currentState.bindings).map((binding: string) => { - if (!currentState.deploymentOption) return; - const field = currentState.deploymentOption.fields.find( - (field: any) => field.binding === binding - ); - if (!field) return; - return ( - - - - - ); - })} - - - - {currentState.deposits.map(({ tokenInfo, amount }) => { - return ( - - - - - ); - })} - -
- Review choices -
- Deployment - - {currentState.deploymentOption.name} -
- {field.name} - - {currentState.bindings[binding]} -
- Deposits -
- {tokenInfo.symbol} - - {amount} -
- ) : ( - "" - )} - {currentState.currentStep === "done" ? "Done!" : ""} -
-
- - - - - - - - - - - - - - - - - - - - - - - - -
-
- ); + return ( +
+
+ +
+ {currentState.deploymentOption ? ( +
+ {currentState.deploymentOption.name} +
+ ) : ( + '' + )} +
+ {currentState.currentStep === 'start' ? ( +
+
{currentState.strategyName}
+
+ {currentState.strategyDescription} +
+
+ ) : ( + '' + )} + {currentState.currentStep === 'deployment' ? 'Choose a strategy variation.' : ''} + {currentState.currentStep === 'fields' && currentState.deploymentOption ? ( +
+
+ {currentState.deploymentOption.fields[Object.keys(currentState.bindings).length].name} +
+
+ { + currentState.deploymentOption.fields[Object.keys(currentState.bindings).length] + .description + } +
+
+ ) : ( + '' + )} + {currentState.currentStep === 'deposit' && currentState.deploymentOption + ? `Choose your deposit amount for ${ + currentState.tokenInfos.find( + (info) => + info.yamlName === + currentState.deploymentOption!.deposits[Object.keys(currentState.deposits).length] + .token + )?.symbol + }.` + : ''} + {currentState.currentStep === 'review' && currentState.deploymentOption ? ( + + + + + + + + + + {Object.keys(currentState.bindings).map((binding: string) => { + if (!currentState.deploymentOption) return; + const field = currentState.deploymentOption.fields.find( + (field: any) => field.binding === binding + ); + if (!field) return; + return ( + + + + + ); + })} + + + + {currentState.deposits.map(({ tokenInfo, amount }) => { + return ( + + + + + ); + })} + +
+ Review choices +
+ Deployment + + {currentState.deploymentOption.name} +
+ {field.name} + + {currentState.bindings[binding]} +
+ Deposits +
+ {tokenInfo.symbol} + + {amount} +
+ ) : ( + '' + )} + {currentState.currentStep === 'done' ? 'Done!' : ''} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ ); }; diff --git a/app/_components/NavBar.tsx b/app/_components/NavBar.tsx index 2ae1b1b4..39e7ef34 100644 --- a/app/_components/NavBar.tsx +++ b/app/_components/NavBar.tsx @@ -1,39 +1,39 @@ -"use client"; +'use client'; -import { ConnectButton } from "@rainbow-me/rainbowkit"; -import { Button, Navbar } from "flowbite-react"; +import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { Button, Navbar } from 'flowbite-react'; export function NavItems() { - return ( - <> - - Browse strategies - - - My strategies - - - ); + return ( + <> + + Browse strategies + + + My strategies + + + ); } export function Nav() { - return ( - -
- - - - - - -
-
- - -
- - - -
- ); + return ( + +
+ + + + + + +
+
+ + +
+ + + +
+ ); } diff --git a/app/_components/ProgressBar.tsx b/app/_components/ProgressBar.tsx index 6d81058e..8db62562 100644 --- a/app/_components/ProgressBar.tsx +++ b/app/_components/ProgressBar.tsx @@ -1,32 +1,32 @@ export const ProgressBar = ({ currentState }: any) => { - // Total steps are the number of fields + 4 (deployment, deposit, review, and done) - // TODO: calculate this all dynamically based on fields for more accurate progress bar - const totalStepCount = 5; - let currentStepCount = 0; - if (currentState.currentStep === "deployment") { - currentStepCount = 1; - } else if (currentState.currentStep === "fields") { - currentStepCount = 2; - } else if (currentState.currentStep === "deposit") { - currentStepCount = 3; - } else if (currentState.currentStep === "review") { - currentStepCount = 4; - } else if (currentState.currentStep === "done") { - currentStepCount = 5; - } + // Total steps are the number of fields + 4 (deployment, deposit, review, and done) + // TODO: calculate this all dynamically based on fields for more accurate progress bar + const totalStepCount = 5; + let currentStepCount = 0; + if (currentState.currentStep === 'deployment') { + currentStepCount = 1; + } else if (currentState.currentStep === 'fields') { + currentStepCount = 2; + } else if (currentState.currentStep === 'deposit') { + currentStepCount = 3; + } else if (currentState.currentStep === 'review') { + currentStepCount = 4; + } else if (currentState.currentStep === 'done') { + currentStepCount = 5; + } - return ( -
-
-
-
-
- ); + return ( +
+
+
+
+
+ ); }; diff --git a/app/_components/QuotesTable.tsx b/app/_components/QuotesTable.tsx index daf3dfa6..b4a252e6 100644 --- a/app/_components/QuotesTable.tsx +++ b/app/_components/QuotesTable.tsx @@ -1,109 +1,93 @@ -import { Table } from "flowbite-react"; -import { quote } from "@rainlanguage/orderbook"; -import * as allChains from "wagmi/chains"; -import { useState } from "react"; -import { formatEther, formatUnits, fromHex } from "viem"; +import { Table } from 'flowbite-react'; +import { quote } from '@rainlanguage/orderbook'; +import * as allChains from 'wagmi/chains'; +import { useState } from 'react'; +import { formatEther, formatUnits, fromHex } from 'viem'; interface props { - order: any; + order: any; } const QuotesTable = ({ order }: props) => { - const [quotes, setQuotes] = useState([]); - const { ...chains } = allChains; - const orderChainKey = Object.keys(chains).find( - (chain) => chain === order.network - ); + const [quotes, setQuotes] = useState([]); + const { ...chains } = allChains; + const orderChainKey = Object.keys(chains).find((chain) => chain === order.network); - const specs = order.inputs.reduce( - (acc: quote.QuoteSpec[], input: any, inputIndex: number) => { - return [ - ...acc, - ...order.outputs.reduce( - (acc: any[], output: any, outputIndex: number) => { - // Prevents TokenSelfTrade error - if ( - order.inputs[inputIndex].token.address === - order.outputs[outputIndex].token.address - ) { - return acc; - } + const specs = order.inputs.reduce((acc: quote.QuoteSpec[], input: any, inputIndex: number) => { + return [ + ...acc, + ...order.outputs.reduce((acc: any[], output: any, outputIndex: number) => { + // Prevents TokenSelfTrade error + if (order.inputs[inputIndex].token.address === order.outputs[outputIndex].token.address) { + return acc; + } - return [ - ...acc, - { - orderHash: order.orderHash, - inputIOIndex: inputIndex, - outputIOIndex: outputIndex, - signedContext: [], - orderbook: order.orderbook.id, - }, - ]; - }, - [] - ), - ]; - }, - [] - ); + return [ + ...acc, + { + orderHash: order.orderHash, + inputIOIndex: inputIndex, + outputIOIndex: outputIndex, + signedContext: [], + orderbook: order.orderbook.id + } + ]; + }, []) + ]; + }, []); - const getQuotes = async () => { - if (orderChainKey === undefined) return; + const getQuotes = async () => { + if (orderChainKey === undefined) return; - try { - const result = await quote.doQuoteSpecs( - specs, - order.subgraphUrl, - (chains as any)[orderChainKey].rpcUrls.default.http[0] - ); - setQuotes(result); - } catch (e) { - console.error(e); - } - }; + try { + const result = await quote.doQuoteSpecs( + specs, + order.subgraphUrl, + (chains as any)[orderChainKey].rpcUrls.default.http[0] + ); + setQuotes(result); + } catch (e) { + console.error(e); + } + }; - if (!quotes.length) { - getQuotes(); - } + if (!quotes.length) { + getQuotes(); + } - return ( -
- - - PAIR - MAXIMUM OUTPUT - PRICE - MAXIMUM INPUT - - - {quotes.map((quote: any, i: number) => { - if (typeof quote === "string") return; - return ( - - - {order.inputs[specs[i].inputIOIndex].token.symbol}/ - {order.outputs[specs[i].outputIOIndex].token.symbol} - - - {formatEther(fromHex(quote.maxOutput, "bigint"))} - - - {formatEther(fromHex(quote.ratio, "bigint"))} - - - {formatUnits( - fromHex(quote.maxOutput, "bigint") * - fromHex(quote.ratio, "bigint"), - 36 - )} - - - ); - })} - -
-
- ); + return ( +
+ + + PAIR + MAXIMUM OUTPUT + PRICE + MAXIMUM INPUT + + + {quotes.map((quote: any, i: number) => { + if (typeof quote === 'string') return; + return ( + + + {order.inputs[specs[i].inputIOIndex].token.symbol}/ + {order.outputs[specs[i].outputIOIndex].token.symbol} + + {formatEther(fromHex(quote.maxOutput, 'bigint'))} + {formatEther(fromHex(quote.ratio, 'bigint'))} + + {formatUnits( + fromHex(quote.maxOutput, 'bigint') * fromHex(quote.ratio, 'bigint'), + 36 + )} + + + ); + })} + +
+
+ ); }; export default QuotesTable; diff --git a/app/_components/ShareStateAsUrl.tsx b/app/_components/ShareStateAsUrl.tsx index f02ad5fc..80c3d332 100644 --- a/app/_components/ShareStateAsUrl.tsx +++ b/app/_components/ShareStateAsUrl.tsx @@ -1,48 +1,48 @@ -import { useState } from "react"; -import { FrameState } from "../_types/frame"; -import { cn } from "@/lib/utils"; -import { Button } from "flowbite-react"; -import { compress } from "../_services/compress"; +import { useState } from 'react'; +import { FrameState } from '../_types/frame'; +import { cn } from '@/lib/utils'; +import { Button } from 'flowbite-react'; +import { compress } from '../_services/compress'; interface ShareStateAsUrlProps { - currentState: FrameState; + currentState: FrameState; } const ShareStateAsUrl: React.FC = ({ currentState }) => { - const [showTooltip, setShowTooltip] = useState(false); + const [showTooltip, setShowTooltip] = useState(false); - const handleClick = () => { - setShowTooltip(true); - getUrlWithState(); + const handleClick = () => { + setShowTooltip(true); + getUrlWithState(); - setTimeout(() => { - setShowTooltip(false); - }, 5000); - }; + setTimeout(() => { + setShowTooltip(false); + }, 5000); + }; - const getUrlWithState = async () => { - const url = new URL(window.location.href); - const jsonString = JSON.stringify(currentState); - const compressed = await compress(jsonString); - url.searchParams.set("currentState", compressed); - navigator.clipboard.writeText(url.href); - }; + const getUrlWithState = async () => { + const url = new URL(window.location.href); + const jsonString = JSON.stringify(currentState); + const compressed = await compress(jsonString); + url.searchParams.set('currentState', compressed); + navigator.clipboard.writeText(url.href); + }; - return ( -
- -
- Shareable URL copied to clipboard -
-
- ); + return ( +
+ +
+ Shareable URL copied to clipboard +
+
+ ); }; export default ShareStateAsUrl; diff --git a/app/_components/StrategyAnalytics.tsx b/app/_components/StrategyAnalytics.tsx index 576b8950..4afcbe7a 100644 --- a/app/_components/StrategyAnalytics.tsx +++ b/app/_components/StrategyAnalytics.tsx @@ -1,133 +1,122 @@ -"use client"; -import { getTransactionAnalyticsData } from "@/app/_queries/strategyAnalytics"; -import { useQuery } from "@tanstack/react-query"; -import { TokenAndBalance } from "./TokenAndBalance"; -import { formatTimestampSecondsAsLocal } from "../_services/dates"; -import { Button } from "@/components/ui/button"; -import { orderBookJson } from "@/public/_abis/OrderBook"; -import { useWriteContract } from "wagmi"; -import { decodeAbiParameters } from "viem"; -import TradesTable from "./TradesTable"; -import QuotesTable from "./QuotesTable"; +'use client'; +import { getTransactionAnalyticsData } from '@/app/_queries/strategyAnalytics'; +import { useQuery } from '@tanstack/react-query'; +import { TokenAndBalance } from './TokenAndBalance'; +import { formatTimestampSecondsAsLocal } from '../_services/dates'; +import { Button } from '@/components/ui/button'; +import { orderBookJson } from '@/public/_abis/OrderBook'; +import { useWriteContract } from 'wagmi'; +import { decodeAbiParameters } from 'viem'; +import TradesTable from './TradesTable'; +import QuotesTable from './QuotesTable'; interface props { - transactionId: string; + transactionId: string; } const Property = ({ - name, - value, - children, + name, + value, + children }: { - name: string; - value?: string; - children?: React.ReactNode; + name: string; + value?: string; + children?: React.ReactNode; }) => ( -
- {name} - {value || children} -
+
+ {name} + {value || children} +
); const StrategyAnalytics = ({ transactionId }: props) => { - const query = useQuery({ - queryKey: [transactionId], - queryFn: () => getTransactionAnalyticsData(transactionId), - enabled: !!transactionId, - refetchInterval: 10000, - }); + const query = useQuery({ + queryKey: [transactionId], + queryFn: () => getTransactionAnalyticsData(transactionId), + enabled: !!transactionId, + refetchInterval: 10000 + }); - const { writeContractAsync } = useWriteContract(); + const { writeContractAsync } = useWriteContract(); - const removeOrder = async () => { - const orderStruct = [orderBookJson.abi[17].inputs[2]]; - const order = decodeAbiParameters( - orderStruct, - query.data.order.orderBytes - )[0]; + const removeOrder = async () => { + const orderStruct = [orderBookJson.abi[17].inputs[2]]; + const order = decodeAbiParameters(orderStruct, query.data.order.orderBytes)[0]; - await writeContractAsync({ - abi: orderBookJson.abi, - address: query.data.order.orderbook.id, - functionName: "removeOrder2", - args: [order, []], - }); + await writeContractAsync({ + abi: orderBookJson.abi, + address: query.data.order.orderbook.id, + functionName: 'removeOrder2', + args: [order, []] + }); - query.refetch(); - }; + query.refetch(); + }; - return ( -
- {query.isLoading &&
Loading...
} - {query.isError &&
{query.error.message}
} - {query.data && ( - <> -
-
-

Strategy Analytics

- {query.data.order.active && ( - - )} -
- - {query.data.transaction.id} - - - - - {query.data.order.owner} - - -
- {query.data.order.inputs && ( -
-

Input tokens

- {query.data.order.inputs.map((vault: any) => { - return ( -
- -
- ); - })} -
- )} - {query.data.order.inputs && ( -
-

Output tokens

- {query.data.order.outputs.map((vault: any) => { - return ( -
- -
- ); - })} -
- )} -
-
- - - - )} -
- ); + return ( +
+ {query.isLoading &&
Loading...
} + {query.isError &&
{query.error.message}
} + {query.data && ( + <> +
+
+

Strategy Analytics

+ {query.data.order.active && ( + + )} +
+ + {query.data.transaction.id} + + + + + {query.data.order.owner} + + +
+ {query.data.order.inputs && ( +
+

Input tokens

+ {query.data.order.inputs.map((vault: any) => { + return ( +
+ +
+ ); + })} +
+ )} + {query.data.order.inputs && ( +
+

Output tokens

+ {query.data.order.outputs.map((vault: any) => { + return ( +
+ +
+ ); + })} +
+ )} +
+
+ + + + )} +
+ ); }; export default StrategyAnalytics; diff --git a/app/_components/StrategyCard.tsx b/app/_components/StrategyCard.tsx index 3d71cf9d..7ba61573 100644 --- a/app/_components/StrategyCard.tsx +++ b/app/_components/StrategyCard.tsx @@ -1,20 +1,20 @@ -import Markdown from "react-markdown"; -import { StrategyFile } from "../_services/buildProjectHome"; -import { DeploymentCard } from "./DeploymentCard"; -import rehypeRaw from "rehype-raw"; +import Markdown from 'react-markdown'; +import { StrategyFile } from '../_services/buildProjectHome'; +import { DeploymentCard } from './DeploymentCard'; +import rehypeRaw from 'rehype-raw'; export const StrategyCard = ({ data }: { data: StrategyFile }) => { - return ( -
-
{data.yamlData.gui.name}
-
- {data.descriptionMD} -
-
- {data.yamlData.gui.deployments.map((deployment) => ( - - ))} -
-
- ); + return ( +
+
{data.yamlData.gui.name}
+
+ {data.descriptionMD} +
+
+ {data.yamlData.gui.deployments.map((deployment) => ( + + ))} +
+
+ ); }; diff --git a/app/_components/SubmissionModal.tsx b/app/_components/SubmissionModal.tsx index 88f2a4f1..f7db4e6d 100644 --- a/app/_components/SubmissionModal.tsx +++ b/app/_components/SubmissionModal.tsx @@ -1,494 +1,443 @@ -import React, { SetStateAction, useEffect, useState } from "react"; -import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"; -import { ConnectButton } from "@rainbow-me/rainbowkit"; -import { - useAccount, - useChainId, - useSwitchChain, - useWriteContract, -} from "wagmi"; -import * as allChains from "wagmi/chains"; -import { readContract } from "viem/actions"; -import { waitForTransactionReceipt } from "viem/actions"; -import { config } from "../providers"; -import { Hex, erc20Abi, formatUnits, parseUnits } from "viem"; -import { orderBookJson } from "@/public/_abis/OrderBook"; -import { getOrderDetailsGivenDeployment } from "../_services/parseDotrainFrontmatter"; -import { getSubmissionTransactionData } from "../_services/transactionData"; -import yaml from "js-yaml"; -import { Referral, YamlData } from "../_types/yamlData"; -import { FrameState } from "../_types/frame"; -import { useRouter } from "next/navigation"; -import { TokenInfo } from "../_services/getTokenInfo"; -import { Alert, Button } from "flowbite-react"; -import { TriangleAlert } from "lucide-react"; +import React, { SetStateAction, useEffect, useState } from 'react'; +import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'; +import { ConnectButton } from '@rainbow-me/rainbowkit'; +import { useAccount, useChainId, useSwitchChain, useWriteContract } from 'wagmi'; +import * as allChains from 'wagmi/chains'; +import { readContract } from 'viem/actions'; +import { waitForTransactionReceipt } from 'viem/actions'; +import { config } from '../providers'; +import { Hex, erc20Abi, formatUnits, parseUnits } from 'viem'; +import { orderBookJson } from '@/public/_abis/OrderBook'; +import { getOrderDetailsGivenDeployment } from '../_services/parseDotrainFrontmatter'; +import { getSubmissionTransactionData } from '../_services/transactionData'; +import yaml from 'js-yaml'; +import { Referral, YamlData } from '../_types/yamlData'; +import { FrameState } from '../_types/frame'; +import { useRouter } from 'next/navigation'; +import { TokenInfo } from '../_services/getTokenInfo'; +import { Alert, Button } from 'flowbite-react'; +import { TriangleAlert } from 'lucide-react'; interface SubmissionModalProps { - yamlData: YamlData; - currentState: FrameState; - buttonText: string; - dotrainText: string; - setError: React.Dispatch>; + yamlData: YamlData; + currentState: FrameState; + buttonText: string; + dotrainText: string; + setError: React.Dispatch>; } enum SubmissionStatus { - ApprovingTokens = "ApprovingTokens", - DeployingStrategy = "DeployingStrategy", - WaitingForDeploymentConfirmation = "WaitingForDeploymentConfirmation", - Done = "Done", + ApprovingTokens = 'ApprovingTokens', + DeployingStrategy = 'DeployingStrategy', + WaitingForDeploymentConfirmation = 'WaitingForDeploymentConfirmation', + Done = 'Done' } export interface TokenDepositWithStatus { - tokenAddress: Hex; - tokenInfo: TokenInfo; - amount: number; - referrals?: Referral[] | undefined; - status: TokenDepositStatus; + tokenAddress: Hex; + tokenInfo: TokenInfo; + amount: number; + referrals?: Referral[] | undefined; + status: TokenDepositStatus; } export interface TokenDeposit { - amount: number; - tokenInfo: TokenInfo; - referrals?: Referral[] | undefined; + amount: number; + tokenInfo: TokenInfo; + referrals?: Referral[] | undefined; } enum TokenDepositStatus { - Pending = "Pending", - CheckingAllowance = "CheckingAllowance", - ApprovingTokens = "ApprovingTokens", - WaitingForApprovalConfirmation = "WaitingForApprovalConfirmation", - TokensApproved = "TokensApproved", + Pending = 'Pending', + CheckingAllowance = 'CheckingAllowance', + ApprovingTokens = 'ApprovingTokens', + WaitingForApprovalConfirmation = 'WaitingForApprovalConfirmation', + TokensApproved = 'TokensApproved' } export const SubmissionModal = ({ - yamlData, - buttonText, - currentState, - dotrainText, - setError, + yamlData, + buttonText, + currentState, + dotrainText, + setError }: SubmissionModalProps) => { - const [showDisclaimer, setShowDisclaimer] = useState(true); - - const router = useRouter(); - - const account = useAccount(); - const currentWalletChainId = useChainId(); - const { switchChainAsync } = useSwitchChain(); - const { writeContractAsync } = useWriteContract(); - - const { orderBookAddress, network, scenario } = - getOrderDetailsGivenDeployment( - yamlData, - currentState.deploymentOption?.deployment || "" - ); - const { ...chains } = allChains; - - const [submissionState, setSubmissionState] = useState( - SubmissionStatus.ApprovingTokens - ); - const [tokenDeposits, setTokenDeposits] = useState( - currentState.deposits.map((deposit) => ({ - tokenAddress: deposit.tokenInfo.address as Hex, - tokenInfo: currentState.tokenInfos.find( - (info) => info.address === deposit.tokenInfo.address - )!, - amount: deposit.amount, - referrals: deposit.referrals, - status: TokenDepositStatus.Pending, - })) - ); - - const [open, setOpen] = useState(false); - const [showFinalMessage, setShowFinalMessage] = useState(false); - - const [hash, setHash] = useState(null); - - useEffect(() => { - if (submissionState === SubmissionStatus.Done) { - setTimeout(() => { - setShowFinalMessage(true); - }, 1000); - } - }, [submissionState]); - - const submitStrategy = async () => { - try { - // Make sure the user is on the correct chain - if (currentWalletChainId !== network["chain-id"]) { - await switchChainAsync({ chainId: network["chain-id"] }); - } - - // Check if the user has sufficient funds - for (const deposit of tokenDeposits) { - if (!deposit.tokenInfo) throw new Error(`Token info not found`); - - const depositAmount = parseUnits( - String(deposit.amount), - deposit.tokenInfo.decimals - ); - - const balance = await readContract(config.getClient(), { - abi: erc20Abi, - address: deposit.tokenInfo.address, - functionName: "balanceOf", - args: [account.address as `0x${string}`], - }); - - if (balance < depositAmount) { - setError( - <> -

- You don't have enough {deposit.tokenInfo.symbol} to cover this - deposit. -

-

- Your balance:{" "} - {formatUnits(BigInt(balance), deposit.tokenInfo.decimals)} -
- Deposit amount: {deposit.amount} -

- {deposit.referrals ? ( - <> - You may be able to get {deposit.tokenInfo.symbol} from:{" "} - - - ) : ( - "" - )} - - ); - setOpen(false); - return; - } - } - - // Check if the user has sufficient token approvals - for (const deposit of tokenDeposits) { - if (!deposit.tokenInfo) throw new Error(`Token info not found`); - - const depositAmount = parseUnits( - String(deposit.amount), - deposit.tokenInfo.decimals - ); - - const existingAllowance = await readContract(config.getClient(), { - abi: erc20Abi, - address: deposit.tokenInfo.address, - functionName: "allowance", - args: [account.address as `0x${string}`, orderBookAddress], - }); - - if (existingAllowance < depositAmount) { - setTokenDeposits((prev) => - prev.map((prevDeposit) => - prevDeposit.tokenInfo.address === deposit.tokenInfo.address - ? { ...prevDeposit, status: TokenDepositStatus.ApprovingTokens } - : prevDeposit - ) - ); - - // Send approval transaction - const approveTx = await writeContractAsync({ - address: deposit.tokenInfo.address, - abi: erc20Abi, - functionName: "approve", - args: [orderBookAddress, depositAmount], - }); - - setTokenDeposits((prev) => - prev.map((prevDeposit) => - prevDeposit.tokenInfo.address === deposit.tokenInfo.address - ? { - ...prevDeposit, - status: TokenDepositStatus.WaitingForApprovalConfirmation, - } - : prevDeposit - ) - ); - - // Wait for approval transaction confirmation - await waitForTransactionReceipt(config.getClient(), { - hash: approveTx, - confirmations: 1, - }); - - setTokenDeposits((prev) => - prev.map((prevDeposit) => - prevDeposit.tokenInfo.address === deposit.tokenInfo.address - ? { ...prevDeposit, status: TokenDepositStatus.TokensApproved } - : prevDeposit - ) - ); - } else { - setTokenDeposits((prev) => - prev.map((prevDeposit) => - prevDeposit.tokenInfo.address === deposit.tokenInfo.address - ? { ...prevDeposit, status: TokenDepositStatus.TokensApproved } - : prevDeposit - ) - ); - } - } - - const convertedBindings = Object.keys(currentState.bindings).reduce( - (acc, key) => { - const value = currentState.bindings[key]; - if (isNaN(value)) { - return { ...acc, [key]: value }; - } - return { ...acc, [key]: Number(value) }; - }, - {} - ); - scenario.bindings = { - ...scenario.bindings, - ...convertedBindings, - }; - - // Get multicall data for addOrder and deposit - const updatedDotrainText = - yaml.dump(yamlData) + "---" + dotrainText.split("---")[1]; - - const { addOrderCalldata, depositCalldatas } = - await getSubmissionTransactionData( - currentState, - updatedDotrainText, - tokenDeposits - ); - - // Send deployment transaction - const deployTx = await writeContractAsync({ - address: orderBookAddress, - abi: orderBookJson.abi, - functionName: "multicall", - args: [[addOrderCalldata, ...depositCalldatas]], - }); - - setSubmissionState(SubmissionStatus.WaitingForDeploymentConfirmation); - - // Wait for deployment transaction confirmation - await waitForTransactionReceipt(config.getClient(), { - hash: deployTx, - confirmations: 4, - }); - - setHash(deployTx); - - setSubmissionState(SubmissionStatus.Done); - } catch (e: any) { - if ( - e?.cause?.message?.includes("addEthereumChain") || - e?.message?.includes("addEthereumChain") - ) { - setError( - `Your wallet doesn't support switching to ${ - Object.values(chains).find( - (chain) => chain.id === network["chain-id"] - )?.name - }` - ); - } else { - setError(e?.cause?.message || e?.message || "An error occurred"); - } - setOpen(false); - console.error(e); - } - }; - - return ( - - {account.isConnected ? ( - - ) : ( - - )} - - {showDisclaimer && ( -
- - Wait! - -
- -
- - - Before you deploy your strategy, make sure you understand - the following: - -
-
-
    -
  • - This front end is provided as a tool to interact with the - Raindex smart contracts. -
  • -
  • - You are deploying your own strategy and depositing funds to an - immutable smart contract using your own wallet and private - keys. -
  • -
  • - Nobody is custodying your funds, there is no recourse for - recovery of funds if lost. -
  • -
  • - There is no endorsement or guarantee provided with these - strategies. -
  • -
  • - Do not proceed if you do not understand the strategy you are - deploying. -
  • -
  • - Do not invest unless you are prepared to lose all funds. -
  • -
-
- -
- )} - - {!showDisclaimer && !showFinalMessage && ( -
- - Deploying your strategy - -
- {tokenDeposits.map((deposit, i) => ( -
-
- {i + 1} -
-
- {deposit.status === TokenDepositStatus.Pending ? ( - - Approve {deposit.tokenInfo.symbol} - - ) : deposit.status === - TokenDepositStatus.CheckingAllowance ? ( - - Checking allowance for {deposit.tokenInfo.symbol}... - - ) : deposit.status === - TokenDepositStatus.ApprovingTokens ? ( - - Approving allowance for {deposit.tokenInfo.symbol}... - - ) : deposit.status === - TokenDepositStatus.WaitingForApprovalConfirmation ? ( - - Waiting for approval confirmation... - - ) : ( - `${deposit.tokenInfo.symbol} allowance approved` - )} -
-
- ))} - -
-
- {currentState.deposits.length + 1} -
-
- {submissionState === SubmissionStatus.DeployingStrategy ? ( - Deploying strategy... - ) : submissionState === - SubmissionStatus.WaitingForDeploymentConfirmation ? ( - - Waiting for deployment confirmation... - - ) : submissionState === SubmissionStatus.Done ? ( - "Strategy deployed" - ) : ( - Deploy strategy - )} -
-
-
-
- )} - {showFinalMessage ? ( -
- - Your strategy is live! - -
- It will continue to trade until removed. If you're interested in - creating your own strategies from scratch, try{" "} - Raindex. -
- -
- ) : null} -
-
- ); + const [showDisclaimer, setShowDisclaimer] = useState(true); + + const router = useRouter(); + + const account = useAccount(); + const currentWalletChainId = useChainId(); + const { switchChainAsync } = useSwitchChain(); + const { writeContractAsync } = useWriteContract(); + + const { orderBookAddress, network, scenario } = getOrderDetailsGivenDeployment( + yamlData, + currentState.deploymentOption?.deployment || '' + ); + const { ...chains } = allChains; + + const [submissionState, setSubmissionState] = useState( + SubmissionStatus.ApprovingTokens + ); + const [tokenDeposits, setTokenDeposits] = useState( + currentState.deposits.map((deposit) => ({ + tokenAddress: deposit.tokenInfo.address as Hex, + tokenInfo: currentState.tokenInfos.find( + (info) => info.address === deposit.tokenInfo.address + )!, + amount: deposit.amount, + referrals: deposit.referrals, + status: TokenDepositStatus.Pending + })) + ); + + const [open, setOpen] = useState(false); + const [showFinalMessage, setShowFinalMessage] = useState(false); + + const [hash, setHash] = useState(null); + + useEffect(() => { + if (submissionState === SubmissionStatus.Done) { + setTimeout(() => { + setShowFinalMessage(true); + }, 1000); + } + }, [submissionState]); + + const submitStrategy = async () => { + try { + // Make sure the user is on the correct chain + if (currentWalletChainId !== network['chain-id']) { + await switchChainAsync({ chainId: network['chain-id'] }); + } + + // Check if the user has sufficient funds + for (const deposit of tokenDeposits) { + if (!deposit.tokenInfo) throw new Error(`Token info not found`); + + const depositAmount = parseUnits(String(deposit.amount), deposit.tokenInfo.decimals); + + const balance = await readContract(config.getClient(), { + abi: erc20Abi, + address: deposit.tokenInfo.address, + functionName: 'balanceOf', + args: [account.address as `0x${string}`] + }); + + if (balance < depositAmount) { + setError( + <> +

+ You don't have enough {deposit.tokenInfo.symbol} to cover this deposit. +

+

+ Your balance: {formatUnits(BigInt(balance), deposit.tokenInfo.decimals)} +
+ Deposit amount: {deposit.amount} +

+ {deposit.referrals ? ( + <> + You may be able to get {deposit.tokenInfo.symbol} from:{' '} + + + ) : ( + '' + )} + + ); + setOpen(false); + return; + } + } + + // Check if the user has sufficient token approvals + for (const deposit of tokenDeposits) { + if (!deposit.tokenInfo) throw new Error(`Token info not found`); + + const depositAmount = parseUnits(String(deposit.amount), deposit.tokenInfo.decimals); + + const existingAllowance = await readContract(config.getClient(), { + abi: erc20Abi, + address: deposit.tokenInfo.address, + functionName: 'allowance', + args: [account.address as `0x${string}`, orderBookAddress] + }); + + if (existingAllowance < depositAmount) { + setTokenDeposits((prev) => + prev.map((prevDeposit) => + prevDeposit.tokenInfo.address === deposit.tokenInfo.address + ? { ...prevDeposit, status: TokenDepositStatus.ApprovingTokens } + : prevDeposit + ) + ); + + // Send approval transaction + const approveTx = await writeContractAsync({ + address: deposit.tokenInfo.address, + abi: erc20Abi, + functionName: 'approve', + args: [orderBookAddress, depositAmount] + }); + + setTokenDeposits((prev) => + prev.map((prevDeposit) => + prevDeposit.tokenInfo.address === deposit.tokenInfo.address + ? { + ...prevDeposit, + status: TokenDepositStatus.WaitingForApprovalConfirmation + } + : prevDeposit + ) + ); + + // Wait for approval transaction confirmation + await waitForTransactionReceipt(config.getClient(), { + hash: approveTx, + confirmations: 1 + }); + + setTokenDeposits((prev) => + prev.map((prevDeposit) => + prevDeposit.tokenInfo.address === deposit.tokenInfo.address + ? { ...prevDeposit, status: TokenDepositStatus.TokensApproved } + : prevDeposit + ) + ); + } else { + setTokenDeposits((prev) => + prev.map((prevDeposit) => + prevDeposit.tokenInfo.address === deposit.tokenInfo.address + ? { ...prevDeposit, status: TokenDepositStatus.TokensApproved } + : prevDeposit + ) + ); + } + } + + const convertedBindings = Object.keys(currentState.bindings).reduce((acc, key) => { + const value = currentState.bindings[key]; + if (isNaN(value)) { + return { ...acc, [key]: value }; + } + return { ...acc, [key]: Number(value) }; + }, {}); + scenario.bindings = { + ...scenario.bindings, + ...convertedBindings + }; + + // Get multicall data for addOrder and deposit + const updatedDotrainText = yaml.dump(yamlData) + '---' + dotrainText.split('---')[1]; + + const { addOrderCalldata, depositCalldatas } = await getSubmissionTransactionData( + currentState, + updatedDotrainText, + tokenDeposits + ); + + // Send deployment transaction + const deployTx = await writeContractAsync({ + address: orderBookAddress, + abi: orderBookJson.abi, + functionName: 'multicall', + args: [[addOrderCalldata, ...depositCalldatas]] + }); + + setSubmissionState(SubmissionStatus.WaitingForDeploymentConfirmation); + + // Wait for deployment transaction confirmation + await waitForTransactionReceipt(config.getClient(), { + hash: deployTx, + confirmations: 4 + }); + + setHash(deployTx); + + setSubmissionState(SubmissionStatus.Done); + } catch (e: any) { + if ( + e?.cause?.message?.includes('addEthereumChain') || + e?.message?.includes('addEthereumChain') + ) { + setError( + `Your wallet doesn't support switching to ${ + Object.values(chains).find((chain) => chain.id === network['chain-id'])?.name + }` + ); + } else { + setError(e?.cause?.message || e?.message || 'An error occurred'); + } + setOpen(false); + console.error(e); + } + }; + + return ( + + {account.isConnected ? ( + + ) : ( + + )} + + {showDisclaimer && ( +
+ Wait! +
+ +
+ + + Before you deploy your strategy, make sure you understand the following: + +
+
+
    +
  • + This front end is provided as a tool to interact with the Raindex smart contracts. +
  • +
  • + You are deploying your own strategy and depositing funds to an immutable smart + contract using your own wallet and private keys. +
  • +
  • + Nobody is custodying your funds, there is no recourse for recovery of funds if + lost. +
  • +
  • + There is no endorsement or guarantee provided with these strategies. +
  • +
  • + Do not proceed if you do not understand the strategy you are deploying. +
  • +
  • Do not invest unless you are prepared to lose all funds.
  • +
+
+ +
+ )} + + {!showDisclaimer && !showFinalMessage && ( +
+ + Deploying your strategy + +
+ {tokenDeposits.map((deposit, i) => ( +
+
+ {i + 1} +
+
+ {deposit.status === TokenDepositStatus.Pending ? ( + Approve {deposit.tokenInfo.symbol} + ) : deposit.status === TokenDepositStatus.CheckingAllowance ? ( + + Checking allowance for {deposit.tokenInfo.symbol}... + + ) : deposit.status === TokenDepositStatus.ApprovingTokens ? ( + + Approving allowance for {deposit.tokenInfo.symbol}... + + ) : deposit.status === TokenDepositStatus.WaitingForApprovalConfirmation ? ( + Waiting for approval confirmation... + ) : ( + `${deposit.tokenInfo.symbol} allowance approved` + )} +
+
+ ))} + +
+
+ {currentState.deposits.length + 1} +
+
+ {submissionState === SubmissionStatus.DeployingStrategy ? ( + Deploying strategy... + ) : submissionState === SubmissionStatus.WaitingForDeploymentConfirmation ? ( + Waiting for deployment confirmation... + ) : submissionState === SubmissionStatus.Done ? ( + 'Strategy deployed' + ) : ( + Deploy strategy + )} +
+
+
+
+ )} + {showFinalMessage ? ( +
+ Your strategy is live! +
+ It will continue to trade until removed. If you're interested in creating your own + strategies from scratch, try Raindex. +
+ +
+ ) : null} +
+
+ ); }; diff --git a/app/_components/TokenAndBalance.tsx b/app/_components/TokenAndBalance.tsx index 934a07ba..64cef597 100644 --- a/app/_components/TokenAndBalance.tsx +++ b/app/_components/TokenAndBalance.tsx @@ -1,25 +1,16 @@ -import { formatUnits } from "viem"; -import { WithdrawalModal } from "./WithdrawalModal"; +import { formatUnits } from 'viem'; +import { WithdrawalModal } from './WithdrawalModal'; -export function TokenAndBalance({ - input, - withdraw, -}: { - input: any; - withdraw?: boolean; -}) { - return ( -
-
-
{withdraw ? input.token.name : input.token.symbol}
-
- Balance:{" "} - {Number( - Number(formatUnits(input.balance, input.token.decimals)).toFixed(8) - )} -
-
- {withdraw && } -
- ); +export function TokenAndBalance({ input, withdraw }: { input: any; withdraw?: boolean }) { + return ( +
+
+
{withdraw ? input.token.name : input.token.symbol}
+
+ Balance: {Number(Number(formatUnits(input.balance, input.token.decimals)).toFixed(8))} +
+
+ {withdraw && } +
+ ); } diff --git a/app/_components/TradesTable.tsx b/app/_components/TradesTable.tsx index db8fa3d7..6c59d0d2 100644 --- a/app/_components/TradesTable.tsx +++ b/app/_components/TradesTable.tsx @@ -1,76 +1,71 @@ -import { formatUnits } from "viem"; -import { Table } from "flowbite-react"; +import { formatUnits } from 'viem'; +import { Table } from 'flowbite-react'; interface props { - trades: any[]; + trades: any[]; } const TradesTable = ({ trades }: props) => { - return ( -
- - - DATE - SENDER - TRANSACTION HASH - INPUT - OUTPUT - IO RATIO - - - {trades.map((trade: any, i: number) => ( - - {trade.tradeEvent.transaction.timestamp} - - navigator.clipboard.writeText(trade.tradeEvent.sender) - } - className="cursor-pointer" - > - {trade.tradeEvent.sender.slice(0, 5)}... - {trade.tradeEvent.sender.slice(-1 * 5)} - - - navigator.clipboard.writeText(trade.tradeEvent.transaction.id) - } - className="cursor-pointer" - > - {trade.tradeEvent.transaction.id.slice(0, 5)}... - {trade.tradeEvent.transaction.id.slice(-1 * 5)} - - -
- {formatUnits( - trade.inputVaultBalanceChange.amount, - trade.inputVaultBalanceChange.vault.token.decimals - )}{" "} - {trade.inputVaultBalanceChange.vault.token.symbol} -
-
- -
- {formatUnits( - trade.outputVaultBalanceChange.amount, - trade.outputVaultBalanceChange.vault.token.decimals - )}{" "} - {trade.outputVaultBalanceChange.vault.token.symbol} -
-
- - {( - trade.inputVaultBalanceChange.amount / - trade.outputVaultBalanceChange.amount - ).toFixed(2)}{" "} - {trade.inputVaultBalanceChange.vault.token.symbol}/ - {trade.outputVaultBalanceChange.vault.token.symbol} - -
- ))} -
-
-
- ); + return ( +
+ + + DATE + SENDER + TRANSACTION HASH + INPUT + OUTPUT + IO RATIO + + + {trades.map((trade: any, i: number) => ( + + {trade.tradeEvent.transaction.timestamp} + navigator.clipboard.writeText(trade.tradeEvent.sender)} + className="cursor-pointer" + > + {trade.tradeEvent.sender.slice(0, 5)}... + {trade.tradeEvent.sender.slice(-1 * 5)} + + navigator.clipboard.writeText(trade.tradeEvent.transaction.id)} + className="cursor-pointer" + > + {trade.tradeEvent.transaction.id.slice(0, 5)}... + {trade.tradeEvent.transaction.id.slice(-1 * 5)} + + +
+ {formatUnits( + trade.inputVaultBalanceChange.amount, + trade.inputVaultBalanceChange.vault.token.decimals + )}{' '} + {trade.inputVaultBalanceChange.vault.token.symbol} +
+
+ +
+ {formatUnits( + trade.outputVaultBalanceChange.amount, + trade.outputVaultBalanceChange.vault.token.decimals + )}{' '} + {trade.outputVaultBalanceChange.vault.token.symbol} +
+
+ + {( + trade.inputVaultBalanceChange.amount / trade.outputVaultBalanceChange.amount + ).toFixed(2)}{' '} + {trade.inputVaultBalanceChange.vault.token.symbol}/ + {trade.outputVaultBalanceChange.vault.token.symbol} + +
+ ))} +
+
+
+ ); }; export default TradesTable; diff --git a/app/_components/WebappFrame.tsx b/app/_components/WebappFrame.tsx index c7c14ee0..b5ae685d 100644 --- a/app/_components/WebappFrame.tsx +++ b/app/_components/WebappFrame.tsx @@ -1,258 +1,250 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { generateButtonsData } from "../_services/buttonsData"; -import { YamlData } from "../_types/yamlData"; -import { FrameImage } from "./FrameImage"; -import { getUpdatedFrameState } from "../_services/frameState"; -import { FrameState } from "../_types/frame"; -import yaml from "js-yaml"; -import { ProgressBar } from "./ProgressBar"; -import _ from "lodash"; -import { FailsafeSchemaWithNumbers } from "../_schemas/failsafeWithNumbers"; -import { SubmissionModal } from "./SubmissionModal"; -import { useSearchParams } from "next/navigation"; -import { Dialog, DialogClose, DialogContent } from "@/components/ui/dialog"; -import { TriangleAlert } from "lucide-react"; -import { TokenInfo, getTokenInfos } from "../_services/getTokenInfo"; -import { Button, Spinner } from "flowbite-react"; -import ShareStateAsUrl from "./ShareStateAsUrl"; -import { decompress } from "../_services/compress"; +'use client'; + +import { useEffect, useState } from 'react'; +import { generateButtonsData } from '../_services/buttonsData'; +import { YamlData } from '../_types/yamlData'; +import { FrameImage } from './FrameImage'; +import { getUpdatedFrameState } from '../_services/frameState'; +import { FrameState } from '../_types/frame'; +import yaml from 'js-yaml'; +import { ProgressBar } from './ProgressBar'; +import _ from 'lodash'; +import { FailsafeSchemaWithNumbers } from '../_schemas/failsafeWithNumbers'; +import { SubmissionModal } from './SubmissionModal'; +import { useSearchParams } from 'next/navigation'; +import { Dialog, DialogClose, DialogContent } from '@/components/ui/dialog'; +import { TriangleAlert } from 'lucide-react'; +import { TokenInfo, getTokenInfos } from '../_services/getTokenInfo'; +import { Button, Spinner } from 'flowbite-react'; +import ShareStateAsUrl from './ShareStateAsUrl'; +import { decompress } from '../_services/compress'; interface props { - dotrainText: string; - deploymentOption: string | null; + dotrainText: string; + deploymentOption: string | null; } const WebappFrame = ({ dotrainText, deploymentOption }: props) => { - const yamlData = yaml.load(dotrainText.split("---")[0], { - schema: FailsafeSchemaWithNumbers, - }) as YamlData; - - const defaultState: FrameState = { - strategyName: yamlData.gui.name, - strategyDescription: yamlData.gui.description, - currentStep: deploymentOption ? "fields" : "start", - deploymentOption: - yamlData.gui.deployments.find( - (deployment) => deployment.deployment === deploymentOption - ) || undefined, - bindings: {}, - deposits: [], - buttonPage: 0, - buttonMax: 10, - textInputLabel: (() => { - const deployment = - yamlData.gui.deployments.find( - (deployment) => deployment.deployment === deploymentOption - ) || undefined; - if (!deployment) { - return ""; - } - const fields = deployment.fields; - const currentField = fields[0]; - if (currentField.min !== undefined && !currentField.presets) { - return `Enter a number greater than ${currentField.min}`; - } - return ""; - })(), - error: null, - isWebapp: true, - tokenInfos: [] as TokenInfo[], - }; - - const [currentState, setCurrentState] = useState(defaultState); - const [loading, setLoading] = useState({ - fetchingTokens: false, - decodingState: true, - }); - const [error, setError] = useState(null); - const [inputText, setInputText] = useState(""); - - const searchParams = useSearchParams(); - - const getUrlState = async () => { - const encodedState = searchParams.get("currentState"); - if (encodedState) { - try { - const decompressedState = await decompress(encodedState); - return { - ...JSON.parse(decompressedState), - requiresTokenApproval: false, - isWebapp: true, - }; - } catch (e: any) { - // If decompression fails, try decoding the state without decompression - if (e.message.includes("not correctly encoded")) { - const decodedState = decodeURI(encodedState); - return { - ...JSON.parse(decodedState), - requiresTokenApproval: false, - isWebapp: true, - }; - } - } - } - return null; - }; - - useEffect(() => { - const initializeState = async () => { - try { - const urlState = await getUrlState(); - if (urlState) setCurrentState((prev) => ({ ...prev, ...urlState })); - } catch (e) { - console.error("Error decoding state:", e); - } finally { - setLoading((prev) => ({ ...prev, decodingState: false })); - } - }; - initializeState(); - }, [searchParams]); // Run only when searchParams change - - useEffect(() => { - const fetchTokenInfos = async () => { - if (currentState.tokenInfos.length === 0 && !loading.fetchingTokens) { - try { - setLoading((prev) => ({ ...prev, fetchingTokens: true })); - const tokenInfos = await getTokenInfos(yamlData); - setCurrentState((prevState) => ({ - ...prevState, - tokenInfos, - })); - } catch (e) { - console.error(e); - setError("Failed to fetch token information"); - } finally { - setLoading((prev) => ({ ...prev, fetchingTokens: false })); - } - } - }; - - if (!loading.decodingState) { - fetchTokenInfos(); - } - }, [yamlData, currentState.tokenInfos.length, loading.decodingState]); // Dependent on decodingState to ensure token fetch happens after decoding - - const handleButtonClick = async (buttonData: any) => { - setError(null); - // Handle page navigation - if (buttonData.buttonTarget === "textInputLabel") { - setCurrentState((prevState) => ({ - ...prevState, - textInputLabel: buttonData.buttonValue, - })); - return; - } else if (buttonData.buttonTarget === "buttonPage") { - setCurrentState((prevState) => ({ - ...prevState, - buttonPage: buttonData.buttonValue, - })); - return; - } else if ( - buttonData.buttonTarget === "buttonValue" && - buttonData.buttonValue === "back" - ) { - setCurrentState((prevState) => ({ - ...prevState, - textInputLabel: "", - })); - } - - const updatedState = getUpdatedFrameState( - yamlData, - currentState, - buttonData.buttonValue, - inputText - ); - - setCurrentState({ ...updatedState }); - - if (inputText) { - setInputText(""); - } - }; - - const buttonsData = generateButtonsData(yamlData, currentState); - - return loading.decodingState || loading.fetchingTokens ? ( -
- -
- ) : ( -
-
- -
- - {currentState.textInputLabel && ( -
- setInputText(e.target.value)} - /> -
-
- )} -
- {buttonsData.map((buttonData) => { - return buttonData.buttonValue === "finalSubmit" ? ( -
- - -
- ) : ( - - ); - })} -
- {currentState.error ? ( -
- {currentState.error} -
- ) : ( - "" - )} -
-
- - - -
{error}
- - - -
-
-
- ); + const yamlData = yaml.load(dotrainText.split('---')[0], { + schema: FailsafeSchemaWithNumbers + }) as YamlData; + + const defaultState: FrameState = { + strategyName: yamlData.gui.name, + strategyDescription: yamlData.gui.description, + currentStep: deploymentOption ? 'fields' : 'start', + deploymentOption: + yamlData.gui.deployments.find((deployment) => deployment.deployment === deploymentOption) || + undefined, + bindings: {}, + deposits: [], + buttonPage: 0, + buttonMax: 10, + textInputLabel: (() => { + const deployment = + yamlData.gui.deployments.find((deployment) => deployment.deployment === deploymentOption) || + undefined; + if (!deployment) { + return ''; + } + const fields = deployment.fields; + const currentField = fields[0]; + if (currentField.min !== undefined && !currentField.presets) { + return `Enter a number greater than ${currentField.min}`; + } + return ''; + })(), + error: null, + isWebapp: true, + tokenInfos: [] as TokenInfo[] + }; + + const [currentState, setCurrentState] = useState(defaultState); + const [loading, setLoading] = useState({ + fetchingTokens: false, + decodingState: true + }); + const [error, setError] = useState(null); + const [inputText, setInputText] = useState(''); + + const searchParams = useSearchParams(); + + const getUrlState = async () => { + const encodedState = searchParams.get('currentState'); + if (encodedState) { + try { + const decompressedState = await decompress(encodedState); + return { + ...JSON.parse(decompressedState), + requiresTokenApproval: false, + isWebapp: true + }; + } catch (e: any) { + // If decompression fails, try decoding the state without decompression + if (e.message.includes('not correctly encoded')) { + const decodedState = decodeURI(encodedState); + return { + ...JSON.parse(decodedState), + requiresTokenApproval: false, + isWebapp: true + }; + } + } + } + return null; + }; + + useEffect(() => { + const initializeState = async () => { + try { + const urlState = await getUrlState(); + if (urlState) setCurrentState((prev) => ({ ...prev, ...urlState })); + } catch (e) { + console.error('Error decoding state:', e); + } finally { + setLoading((prev) => ({ ...prev, decodingState: false })); + } + }; + initializeState(); + }, [searchParams]); // Run only when searchParams change + + useEffect(() => { + const fetchTokenInfos = async () => { + if (currentState.tokenInfos.length === 0 && !loading.fetchingTokens) { + try { + setLoading((prev) => ({ ...prev, fetchingTokens: true })); + const tokenInfos = await getTokenInfos(yamlData); + setCurrentState((prevState) => ({ + ...prevState, + tokenInfos + })); + } catch (e) { + console.error(e); + setError('Failed to fetch token information'); + } finally { + setLoading((prev) => ({ ...prev, fetchingTokens: false })); + } + } + }; + + if (!loading.decodingState) { + fetchTokenInfos(); + } + }, [yamlData, currentState.tokenInfos.length, loading.decodingState]); // Dependent on decodingState to ensure token fetch happens after decoding + + const handleButtonClick = async (buttonData: any) => { + setError(null); + // Handle page navigation + if (buttonData.buttonTarget === 'textInputLabel') { + setCurrentState((prevState) => ({ + ...prevState, + textInputLabel: buttonData.buttonValue + })); + return; + } else if (buttonData.buttonTarget === 'buttonPage') { + setCurrentState((prevState) => ({ + ...prevState, + buttonPage: buttonData.buttonValue + })); + return; + } else if (buttonData.buttonTarget === 'buttonValue' && buttonData.buttonValue === 'back') { + setCurrentState((prevState) => ({ + ...prevState, + textInputLabel: '' + })); + } + + const updatedState = getUpdatedFrameState( + yamlData, + currentState, + buttonData.buttonValue, + inputText + ); + + setCurrentState({ ...updatedState }); + + if (inputText) { + setInputText(''); + } + }; + + const buttonsData = generateButtonsData(yamlData, currentState); + + return loading.decodingState || loading.fetchingTokens ? ( +
+ +
+ ) : ( +
+
+ +
+ + {currentState.textInputLabel && ( +
+ setInputText(e.target.value)} + /> +
+
+ )} +
+ {buttonsData.map((buttonData) => { + return buttonData.buttonValue === 'finalSubmit' ? ( +
+ + +
+ ) : ( + + ); + })} +
+ {currentState.error ? ( +
+ {currentState.error} +
+ ) : ( + '' + )} +
+
+ + + +
{error}
+ + + +
+
+
+ ); }; export default WebappFrame; diff --git a/app/_components/WithdrawalModal.tsx b/app/_components/WithdrawalModal.tsx index 932cef95..3b90779c 100644 --- a/app/_components/WithdrawalModal.tsx +++ b/app/_components/WithdrawalModal.tsx @@ -1,158 +1,155 @@ -"use client"; -import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { Button } from "@/components/ui/button"; +'use client'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Button } from '@/components/ui/button'; import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { Input } from "@/components/ui/input"; -import { useEffect, useState } from "react"; + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage +} from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import { useEffect, useState } from 'react'; import { - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { useWriteContract } from "wagmi"; -import { orderBookJson } from "@/public/_abis/OrderBook"; -import { parseUnits, formatUnits } from "viem"; + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger +} from '@/components/ui/dialog'; +import { useWriteContract } from 'wagmi'; +import { orderBookJson } from '@/public/_abis/OrderBook'; +import { parseUnits, formatUnits } from 'viem'; const formSchema = z.object({ - withdrawalAmount: z.preprocess( - (value) => Number(value), - z.number().min(0, "Amount must be a positive number") - ), + withdrawalAmount: z.preprocess( + (value) => Number(value), + z.number().min(0, 'Amount must be a positive number') + ) }); interface Vault { - token: any; - vaultId: any; - balance: any; - orderbook: any; + token: any; + vaultId: any; + balance: any; + orderbook: any; } interface WithdrawalModalProps { - vault: Vault; + vault: Vault; } export const WithdrawalModal = ({ vault }: WithdrawalModalProps) => { - const { writeContractAsync } = useWriteContract(); - const [open, setOpen] = useState(false); - const [rawAmount, setRawAmount] = useState("0"); // Store the raw 18-decimal amount + const { writeContractAsync } = useWriteContract(); + const [open, setOpen] = useState(false); + const [rawAmount, setRawAmount] = useState('0'); // Store the raw 18-decimal amount - const [error, setError] = useState(null); + const [error, setError] = useState(null); - useEffect(() => { - setError(null); - if (BigInt(rawAmount) > BigInt(vault.balance)) { - setError("Amount exceeds vault balance"); - } - }, [rawAmount, vault.balance]); + useEffect(() => { + setError(null); + if (BigInt(rawAmount) > BigInt(vault.balance)) { + setError('Amount exceeds vault balance'); + } + }, [rawAmount, vault.balance]); - // Vault balance in human-readable format (i.e., converted from 18 decimals) - const readableBalance = formatUnits(vault.balance, vault.token.decimals); + // Vault balance in human-readable format (i.e., converted from 18 decimals) + const readableBalance = formatUnits(vault.balance, vault.token.decimals); - // Define your form. - const form = useForm>({ - resolver: zodResolver(formSchema), - defaultValues: { - withdrawalAmount: 0, - }, - }); + // Define your form. + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + withdrawalAmount: 0 + } + }); - const withdraw = async (amount: string) => { - // Send raw value to the contract (no conversion needed here) - await writeContractAsync({ - abi: orderBookJson.abi, - address: vault.orderbook.id, - functionName: "withdraw2", - args: [vault.token.address, BigInt(vault.vaultId), BigInt(amount), []], - }); - }; + const withdraw = async (amount: string) => { + // Send raw value to the contract (no conversion needed here) + await writeContractAsync({ + abi: orderBookJson.abi, + address: vault.orderbook.id, + functionName: 'withdraw2', + args: [vault.token.address, BigInt(vault.vaultId), BigInt(amount), []] + }); + }; - const handleMaxClick = () => { - // Set the form field to the readable max balance for display - form.setValue("withdrawalAmount", parseFloat(readableBalance)); - // Set the raw balance directly - setRawAmount(vault.balance); // Use raw vault balance directly - form.setFocus("withdrawalAmount"); // Optional: focus the field after setting value - }; + const handleMaxClick = () => { + // Set the form field to the readable max balance for display + form.setValue('withdrawalAmount', parseFloat(readableBalance)); + // Set the raw balance directly + setRawAmount(vault.balance); // Use raw vault balance directly + form.setFocus('withdrawalAmount'); // Optional: focus the field after setting value + }; - const handleUserChange = (e: React.ChangeEvent) => { - const userInput = e.target.value; - form.setValue("withdrawalAmount", parseFloat(userInput)); + const handleUserChange = (e: React.ChangeEvent) => { + const userInput = e.target.value; + form.setValue('withdrawalAmount', parseFloat(userInput)); - // Update the raw amount based on the user input (convert back to raw value) - if (userInput) { - try { - const parsedRawAmount = parseUnits( - userInput, - vault.token.decimals - ).toString(); - setRawAmount(parsedRawAmount); // Update raw amount on every user change - } catch (err) { - setRawAmount("0"); // Fallback to 0 if input is invalid - } - } else { - setRawAmount("0"); // Fallback to 0 if input is empty - } - }; + // Update the raw amount based on the user input (convert back to raw value) + if (userInput) { + try { + const parsedRawAmount = parseUnits(userInput, vault.token.decimals).toString(); + setRawAmount(parsedRawAmount); // Update raw amount on every user change + } catch (err) { + setRawAmount('0'); // Fallback to 0 if input is invalid + } + } else { + setRawAmount('0'); // Fallback to 0 if input is empty + } + }; - return ( - - - - - - - Withdraw -
- { - // Always submit the raw amount stored in state - await withdraw(rawAmount); - setOpen(false); - })} - className="space-y-8" - > - ( - - Amount - - {/* Use onChange to listen to user typing */} - - - {error} - - - - )} - /> - - - -
-
-
- ); + return ( + + + + + + + Withdraw +
+ { + // Always submit the raw amount stored in state + await withdraw(rawAmount); + setOpen(false); + })} + className="space-y-8" + > + ( + + Amount + + {/* Use onChange to listen to user typing */} + + + {error} + + + + )} + /> + + + +
+
+
+ ); }; diff --git a/app/_queries/getOrders.tsx b/app/_queries/getOrders.tsx index d79ab4bc..516be106 100644 --- a/app/_queries/getOrders.tsx +++ b/app/_queries/getOrders.tsx @@ -1,9 +1,9 @@ -import { getNetworkSubgraphs } from "./subgraphs"; +import { getNetworkSubgraphs } from './subgraphs'; export const getOrders = async (owner?: string) => { - const networks = await getNetworkSubgraphs(); - if (!owner) return; - const query = `{ + const networks = await getNetworkSubgraphs(); + if (!owner) return; + const query = `{ orders(orderBy: timestampAdded, orderDirection: desc, where: { owner: "${owner}" }) { orderBytes orderHash @@ -47,33 +47,31 @@ export const getOrders = async (owner?: string) => { } }`; - // make a query for each network - const promises: Promise[] = []; - Object.entries(networks).forEach(([network, subgraphUrl]) => { - promises.push( - fetch(subgraphUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ query }), - }) - .then((res) => res.json()) - .then((res) => { - if (res.errors) throw new Error(res.errors[0].message); - if (res.data && res.data.orders) { - return res.data.orders.map((order: any) => { - order.network = network; - return order; - }); - } - }) - ); - }); + // make a query for each network + const promises: Promise[] = []; + Object.entries(networks).forEach(([network, subgraphUrl]) => { + promises.push( + fetch(subgraphUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ query }) + }) + .then((res) => res.json()) + .then((res) => { + if (res.errors) throw new Error(res.errors[0].message); + if (res.data && res.data.orders) { + return res.data.orders.map((order: any) => { + order.network = network; + return order; + }); + } + }) + ); + }); - const results = await Promise.all(promises).then((res) => res.flat()); - const orderedResults = results.sort( - (a, b) => b.timestampAdded - a.timestampAdded - ); - return orderedResults; + const results = await Promise.all(promises).then((res) => res.flat()); + const orderedResults = results.sort((a, b) => b.timestampAdded - a.timestampAdded); + return orderedResults; }; diff --git a/app/_queries/strategyAnalytics.tsx b/app/_queries/strategyAnalytics.tsx index 83c1d13c..6640c129 100644 --- a/app/_queries/strategyAnalytics.tsx +++ b/app/_queries/strategyAnalytics.tsx @@ -1,4 +1,4 @@ -import { getNetworkSubgraphs } from "./subgraphs"; +import { getNetworkSubgraphs } from './subgraphs'; export const transactionAnalytics = (transactionId: string) => ` { @@ -76,37 +76,35 @@ export const transactionAnalytics = (transactionId: string) => ` }`; export const getTransactionAnalyticsData = async (transactionId: string) => { - const networks = getNetworkSubgraphs(); - // Find which subgraph has the transaction data by checking each subgraph - for (let [network, subgraphUrl] of Object.entries(networks)) { - try { - const response = await fetch(subgraphUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - query: transactionAnalytics(transactionId), - }), - }); - if (!response.ok) { - throw new Error(`HTTP error! Status: ${response.status}`); - } - const result = await response.json(); - if (result.errors) { - throw new Error(result.errors[0].message); - } - return { - ...result.data.addOrders[0], - order: { ...result.data.addOrders[0].order, network, subgraphUrl }, - }; - } catch (error: any) { - if (error?.message) - throw new Error( - `Error fetching transaction data from ${network} subgraph: ${ - error?.message || "" - }` - ); - } - } + const networks = getNetworkSubgraphs(); + // Find which subgraph has the transaction data by checking each subgraph + for (let [network, subgraphUrl] of Object.entries(networks)) { + try { + const response = await fetch(subgraphUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + query: transactionAnalytics(transactionId) + }) + }); + if (!response.ok) { + throw new Error(`HTTP error! Status: ${response.status}`); + } + const result = await response.json(); + if (result.errors) { + throw new Error(result.errors[0].message); + } + return { + ...result.data.addOrders[0], + order: { ...result.data.addOrders[0].order, network, subgraphUrl } + }; + } catch (error: any) { + if (error?.message) + throw new Error( + `Error fetching transaction data from ${network} subgraph: ${error?.message || ''}` + ); + } + } }; diff --git a/app/_queries/subgraphs.tsx b/app/_queries/subgraphs.tsx index e53045dc..74c4ae89 100644 --- a/app/_queries/subgraphs.tsx +++ b/app/_queries/subgraphs.tsx @@ -1,7 +1,7 @@ export const getNetworkSubgraphs = () => { - return { - flare: - "https://api.goldsky.com/api/public/project_clv14x04y9kzi01saerx7bxpg/subgraphs/ob4-flare/0.2/gn", - base: "https://api.goldsky.com/api/public/project_clv14x04y9kzi01saerx7bxpg/subgraphs/ob4-base/0.7/gn", - }; + return { + flare: + 'https://api.goldsky.com/api/public/project_clv14x04y9kzi01saerx7bxpg/subgraphs/ob4-flare/0.2/gn', + base: 'https://api.goldsky.com/api/public/project_clv14x04y9kzi01saerx7bxpg/subgraphs/ob4-base/0.7/gn' + }; }; diff --git a/app/_schemas/failsafeWithNumbers.ts b/app/_schemas/failsafeWithNumbers.ts index 2af31d56..9edb5b45 100644 --- a/app/_schemas/failsafeWithNumbers.ts +++ b/app/_schemas/failsafeWithNumbers.ts @@ -1,15 +1,15 @@ -import yaml from "js-yaml"; +import yaml from 'js-yaml'; -const numberType = new yaml.Type("!", { - kind: "scalar", - resolve: (data) => { - return !isNaN(data) && !/^0x[0-9a-fA-F]{1,64}$/.test(data); - }, - construct: (data) => Number(data), - instanceOf: Number, - represent: (data) => Number(data), +const numberType = new yaml.Type('!', { + kind: 'scalar', + resolve: (data) => { + return !isNaN(data) && !/^0x[0-9a-fA-F]{1,64}$/.test(data); + }, + construct: (data) => Number(data), + instanceOf: Number, + represent: (data) => Number(data) }); export const FailsafeSchemaWithNumbers = yaml.FAILSAFE_SCHEMA.extend({ - implicit: [numberType], + implicit: [numberType] }); diff --git a/app/_services/buildProjectHome.tsx b/app/_services/buildProjectHome.tsx index cc8c20d8..88f5c4b1 100644 --- a/app/_services/buildProjectHome.tsx +++ b/app/_services/buildProjectHome.tsx @@ -1,69 +1,60 @@ -import fs from "fs"; -import path from "path"; -import yaml from "js-yaml"; -import { YamlData } from "../_types/yamlData"; -import { FailsafeSchemaWithNumbers } from "../_schemas/failsafeWithNumbers"; +import fs from 'fs'; +import path from 'path'; +import yaml from 'js-yaml'; +import { YamlData } from '../_types/yamlData'; +import { FailsafeSchemaWithNumbers } from '../_schemas/failsafeWithNumbers'; interface ProjectHomeData { - yamlDatas: StrategyFile[]; - webappMDText: string; - frameMDText: string; + yamlDatas: StrategyFile[]; + webappMDText: string; + frameMDText: string; } export interface StrategyFile { - fileName: string; - yamlData: YamlData; - descriptionMD: string; + fileName: string; + yamlData: YamlData; + descriptionMD: string; } -export const retrieveProjectData = async ( - projectName: string -): Promise => { - const dirPath = path.join( - process.cwd(), - "public", - "_strategies", - projectName - ); - const subDirs = fs - .readdirSync(dirPath, { withFileTypes: true }) - .filter((dirent) => dirent.isDirectory()) - .map((dirent) => dirent.name); +export const retrieveProjectData = async (projectName: string): Promise => { + const dirPath = path.join(process.cwd(), 'public', '_strategies', projectName); + const subDirs = fs + .readdirSync(dirPath, { withFileTypes: true }) + .filter((dirent) => dirent.isDirectory()) + .map((dirent) => dirent.name); - const yamlDatas = subDirs.map((subDir) => { - const subDirPath = path.join(dirPath, subDir); + const yamlDatas = subDirs.map((subDir) => { + const subDirPath = path.join(dirPath, subDir); - // Find the .rain file in the subdirectory - const rainFile = fs - .readdirSync(subDirPath) - .find((filename) => filename.endsWith(".rain")); + // Find the .rain file in the subdirectory + const rainFile = fs.readdirSync(subDirPath).find((filename) => filename.endsWith('.rain')); - if (!rainFile) { - throw new Error(`No .rain file found in directory: ${subDirPath}`); - } + if (!rainFile) { + throw new Error(`No .rain file found in directory: ${subDirPath}`); + } - const rainFilePath = path.join(subDirPath, rainFile); - const descriptionFilePath = path.join(subDirPath, "description.md"); + const rainFilePath = path.join(subDirPath, rainFile); + const descriptionFilePath = path.join(subDirPath, 'description.md'); - // Read and parse the .rain file - const rainFileContent = fs.readFileSync(rainFilePath, "utf8"); - const yamlData = yaml.load(rainFileContent.split("---")[0], { - schema: FailsafeSchemaWithNumbers, - }) as YamlData; + // Read and parse the .rain file + const rainFileContent = fs.readFileSync(rainFilePath, 'utf8'); + const yamlData = yaml.load(rainFileContent.split('---')[0], { + schema: FailsafeSchemaWithNumbers + }) as YamlData; - // Read the description.md file - const descriptionMD = fs.readFileSync(descriptionFilePath, "utf8"); + // Read the description.md file + const descriptionMD = fs.readFileSync(descriptionFilePath, 'utf8'); - return { - fileName: rainFile.replace(".rain", ""), - yamlData, - descriptionMD, - }; - }); + return { + fileName: rainFile.replace('.rain', ''), + yamlData, + descriptionMD + }; + }); - // Reading the webapp.md and frame.md - const webappMDText = fs.readFileSync(path.join(dirPath, "webapp.md"), "utf8"); - const frameMDText = fs.readFileSync(path.join(dirPath, "frame.md"), "utf8"); + // Reading the webapp.md and frame.md + const webappMDText = fs.readFileSync(path.join(dirPath, 'webapp.md'), 'utf8'); + const frameMDText = fs.readFileSync(path.join(dirPath, 'frame.md'), 'utf8'); - return { yamlDatas, webappMDText, frameMDText }; + return { yamlDatas, webappMDText, frameMDText }; }; diff --git a/app/_services/buttonsData.ts b/app/_services/buttonsData.ts index 3c83bfbf..1d6e91ba 100644 --- a/app/_services/buttonsData.ts +++ b/app/_services/buttonsData.ts @@ -1,240 +1,204 @@ -import { FrameState } from "../_types/frame"; -import { - DeploymentOption, - Deposit, - Preset, - YamlData, - Field, -} from "../_types/yamlData"; -import { TokenInfo } from "./getTokenInfo"; +import { FrameState } from '../_types/frame'; +import { DeploymentOption, Deposit, Preset, YamlData, Field } from '../_types/yamlData'; +import { TokenInfo } from './getTokenInfo'; export const getPaginatedButtons = ( - allButtons: any[], - buttonPage: number, - buttonMax = 4 + allButtons: any[], + buttonPage: number, + buttonMax = 4 ): any[] => { - const buttonPageOffset = buttonPage * 3; - let buttonEndIndex = buttonPageOffset + buttonMax; - const includeMoreButton = buttonEndIndex < allButtons.length; - if (includeMoreButton) { - buttonEndIndex--; - } - return [ - ...(allButtons.length <= 3 - ? allButtons - : [ - ...(buttonPage > 0 - ? [ - { - buttonTarget: "buttonPage", - buttonValue: buttonPage - 1, - buttonText: "←", - }, - ] - : []), - ...allButtons.slice(buttonPageOffset, buttonEndIndex), - ...(includeMoreButton - ? [ - { - buttonTarget: "buttonPage", - buttonValue: buttonPage + 1, - buttonText: "More", - }, - ] - : []), - ]), - ]; + const buttonPageOffset = buttonPage * 3; + let buttonEndIndex = buttonPageOffset + buttonMax; + const includeMoreButton = buttonEndIndex < allButtons.length; + if (includeMoreButton) { + buttonEndIndex--; + } + return [ + ...(allButtons.length <= 3 + ? allButtons + : [ + ...(buttonPage > 0 + ? [ + { + buttonTarget: 'buttonPage', + buttonValue: buttonPage - 1, + buttonText: '←' + } + ] + : []), + ...allButtons.slice(buttonPageOffset, buttonEndIndex), + ...(includeMoreButton + ? [ + { + buttonTarget: 'buttonPage', + buttonValue: buttonPage + 1, + buttonText: 'More' + } + ] + : []) + ]) + ]; }; export const getFieldPresetsButtons = (field: Field): any[] => { - return [ - { - buttonTarget: "buttonValue", - buttonValue: "back", - buttonText: "←", - }, - ...(field.presets - ? field.presets.map((preset: Preset) => ({ - buttonTarget: "buttonValue", - buttonValue: `${preset.value}`, - buttonText: `${preset.name}`, - })) - : []), - ...(field.min !== undefined - ? [ - { - buttonTarget: "textInputLabel", - buttonValue: `Enter a number greater than ${field.min}`, - buttonText: "Custom", - }, - ] - : []), - ]; + return [ + { + buttonTarget: 'buttonValue', + buttonValue: 'back', + buttonText: '←' + }, + ...(field.presets + ? field.presets.map((preset: Preset) => ({ + buttonTarget: 'buttonValue', + buttonValue: `${preset.value}`, + buttonText: `${preset.name}` + })) + : []), + ...(field.min !== undefined + ? [ + { + buttonTarget: 'textInputLabel', + buttonValue: `Enter a number greater than ${field.min}`, + buttonText: 'Custom' + } + ] + : []) + ]; }; -export const getDepositPresetsButtons = ( - deposit: Deposit, - token: TokenInfo -): any[] => { - return [ - { - buttonTarget: "buttonValue", - buttonValue: "back", - buttonText: "←", - }, - ...(deposit?.presets - ? deposit.presets?.map((preset: number) => ({ - buttonTarget: "buttonValue", - buttonValue: `${preset}`, - buttonText: `${preset} ${token.symbol}`, - })) - : []), - ...(deposit?.min !== undefined - ? [ - { - buttonTarget: "textInputLabel", - buttonValue: `Enter a number greater than ${deposit.min}`, - buttonText: "Custom", - }, - ] - : []), - ]; +export const getDepositPresetsButtons = (deposit: Deposit, token: TokenInfo): any[] => { + return [ + { + buttonTarget: 'buttonValue', + buttonValue: 'back', + buttonText: '←' + }, + ...(deposit?.presets + ? deposit.presets?.map((preset: number) => ({ + buttonTarget: 'buttonValue', + buttonValue: `${preset}`, + buttonText: `${preset} ${token.symbol}` + })) + : []), + ...(deposit?.min !== undefined + ? [ + { + buttonTarget: 'textInputLabel', + buttonValue: `Enter a number greater than ${deposit.min}`, + buttonText: 'Custom' + } + ] + : []) + ]; }; -export const generateButtonsData = ( - yamlData: YamlData, - currentState: FrameState -): any[] => { - let buttons: any[] = []; - if (currentState.textInputLabel) { - return [ - { - buttonTarget: "buttonValue", - buttonValue: "back", - buttonText: "←", - }, - { - buttonTarget: "buttonValue", - buttonValue: "submit", - buttonText: "Submit", - }, - ]; - } - switch (currentState.currentStep) { - case "start": - buttons = [ - { - buttonTarget: "buttonValue", - buttonValue: "start", - buttonText: "Start", - }, - ]; - break; - case "deployment": - const allButtons = yamlData.gui.deployments.map( - (deploymentOption: DeploymentOption) => ({ - buttonTarget: "buttonValue", - buttonValue: JSON.stringify(deploymentOption), - buttonText: deploymentOption.name, - }) - ); - buttons = getPaginatedButtons( - allButtons, - currentState.buttonPage, - currentState.buttonMax - ); - break; - case "fields": - if (!currentState.deploymentOption) { - return buttons; - } - const field = - currentState.deploymentOption.fields[ - Object.keys(currentState.bindings).length - ]; - const fieldButtons = getFieldPresetsButtons(field); - buttons = getPaginatedButtons( - fieldButtons, - currentState.buttonPage, - currentState.buttonMax - ); - break; - case "deposit": - if (!currentState.deploymentOption) { - return buttons; - } +export const generateButtonsData = (yamlData: YamlData, currentState: FrameState): any[] => { + let buttons: any[] = []; + if (currentState.textInputLabel) { + return [ + { + buttonTarget: 'buttonValue', + buttonValue: 'back', + buttonText: '←' + }, + { + buttonTarget: 'buttonValue', + buttonValue: 'submit', + buttonText: 'Submit' + } + ]; + } + switch (currentState.currentStep) { + case 'start': + buttons = [ + { + buttonTarget: 'buttonValue', + buttonValue: 'start', + buttonText: 'Start' + } + ]; + break; + case 'deployment': + const allButtons = yamlData.gui.deployments.map((deploymentOption: DeploymentOption) => ({ + buttonTarget: 'buttonValue', + buttonValue: JSON.stringify(deploymentOption), + buttonText: deploymentOption.name + })); + buttons = getPaginatedButtons(allButtons, currentState.buttonPage, currentState.buttonMax); + break; + case 'fields': + if (!currentState.deploymentOption) { + return buttons; + } + const field = currentState.deploymentOption.fields[Object.keys(currentState.bindings).length]; + const fieldButtons = getFieldPresetsButtons(field); + buttons = getPaginatedButtons(fieldButtons, currentState.buttonPage, currentState.buttonMax); + break; + case 'deposit': + if (!currentState.deploymentOption) { + return buttons; + } - const deposit = - currentState.deploymentOption.deposits[ - Object.keys(currentState.deposits).length - ]; + const deposit = + currentState.deploymentOption.deposits[Object.keys(currentState.deposits).length]; - const token = currentState.tokenInfos.find( - (token) => token.yamlName == deposit.token - ); + const token = currentState.tokenInfos.find((token) => token.yamlName == deposit.token); - if (!token) - throw new Error( - "Token from deposit not found in retrieved token infos" - ); + if (!token) throw new Error('Token from deposit not found in retrieved token infos'); - const depositButtons = getDepositPresetsButtons(deposit, token); - buttons = getPaginatedButtons( - depositButtons, - currentState.buttonPage, - currentState.buttonMax - ); - break; - case "review": - buttons = [ - { - buttonTarget: "buttonValue", - buttonValue: "back", - buttonText: "←", - }, - ]; + const depositButtons = getDepositPresetsButtons(deposit, token); + buttons = getPaginatedButtons( + depositButtons, + currentState.buttonPage, + currentState.buttonMax + ); + break; + case 'review': + buttons = [ + { + buttonTarget: 'buttonValue', + buttonValue: 'back', + buttonText: '←' + } + ]; - const supportedNetworks = { - Ethereum: 1, - Arbitrum: 42161, - Base: 8453, - Degen: 666666666, - Gnosis: 100, - Optimism: 10, - Zora: 7777777, - }; - const deployment = - yamlData.deployments[currentState.deploymentOption?.deployment || ""]; - const order = yamlData.orders[deployment.order]; - const network = yamlData.networks[order.network]; + const supportedNetworks = { + Ethereum: 1, + Arbitrum: 42161, + Base: 8453, + Degen: 666666666, + Gnosis: 100, + Optimism: 10, + Zora: 7777777 + }; + const deployment = yamlData.deployments[currentState.deploymentOption?.deployment || '']; + const order = yamlData.orders[deployment.order]; + const network = yamlData.networks[order.network]; - if ( - currentState.isWebapp || - Object.values(supportedNetworks).includes(network["chain-id"]) - ) { - buttons.push({ - buttonTarget: "buttonValue", - buttonValue: "finalSubmit", - buttonText: "Deposit tokens and deploy strategy", - }); - } else { - buttons.push({ - buttonAction: "link", - buttonTarget: "currentState", - buttonValue: encodeURIComponent(JSON.stringify(currentState)), - buttonText: "Deposit tokens and deploy strategy", - }); - } - break; - case "done": - buttons = [ - { - buttonTarget: "buttonValue", - buttonValue: "restart", - buttonText: "Start over", - }, - ]; - break; - } - return buttons; + if (currentState.isWebapp || Object.values(supportedNetworks).includes(network['chain-id'])) { + buttons.push({ + buttonTarget: 'buttonValue', + buttonValue: 'finalSubmit', + buttonText: 'Deposit tokens and deploy strategy' + }); + } else { + buttons.push({ + buttonAction: 'link', + buttonTarget: 'currentState', + buttonValue: encodeURIComponent(JSON.stringify(currentState)), + buttonText: 'Deposit tokens and deploy strategy' + }); + } + break; + case 'done': + buttons = [ + { + buttonTarget: 'buttonValue', + buttonValue: 'restart', + buttonText: 'Start over' + } + ]; + break; + } + return buttons; }; diff --git a/app/_services/compress.ts b/app/_services/compress.ts index a23af9d6..24a0aab4 100644 --- a/app/_services/compress.ts +++ b/app/_services/compress.ts @@ -1,30 +1,30 @@ // Compresses string to GZIP. Retruns a Promise with Base64 string export const compress = (string: string): Promise => { - const blobToBase64 = (blob: Blob) => - new Promise((resolve, _) => { - const reader = new FileReader(); - reader.onloadend = () => { - const res = reader.result; - if (typeof res === "string") return resolve(res.split(",")[1]); - }; - reader.readAsDataURL(blob); - }); - const byteArray = new TextEncoder().encode(string); - const cs = new CompressionStream("gzip"); - const writer = cs.writable.getWriter(); - writer.write(byteArray); - writer.close(); - return new Response(cs.readable).blob().then(blobToBase64); + const blobToBase64 = (blob: Blob) => + new Promise((resolve, _) => { + const reader = new FileReader(); + reader.onloadend = () => { + const res = reader.result; + if (typeof res === 'string') return resolve(res.split(',')[1]); + }; + reader.readAsDataURL(blob); + }); + const byteArray = new TextEncoder().encode(string); + const cs = new CompressionStream('gzip'); + const writer = cs.writable.getWriter(); + writer.write(byteArray); + writer.close(); + return new Response(cs.readable).blob().then(blobToBase64); }; // Decompresses base64 encoded GZIP string. Returns a string with original text. export const decompress = (base64string: string) => { - const bytes = Uint8Array.from(atob(base64string), (c) => c.charCodeAt(0)); - const cs = new DecompressionStream("gzip"); - const writer = cs.writable.getWriter(); - writer.write(bytes); - writer.close(); - return new Response(cs.readable).arrayBuffer().then(function (arrayBuffer) { - return new TextDecoder().decode(arrayBuffer); - }); + const bytes = Uint8Array.from(atob(base64string), (c) => c.charCodeAt(0)); + const cs = new DecompressionStream('gzip'); + const writer = cs.writable.getWriter(); + writer.write(bytes); + writer.close(); + return new Response(cs.readable).arrayBuffer().then(function (arrayBuffer) { + return new TextDecoder().decode(arrayBuffer); + }); }; diff --git a/app/_services/dates.ts b/app/_services/dates.ts index 6af7c14b..d809eb59 100644 --- a/app/_services/dates.ts +++ b/app/_services/dates.ts @@ -1,9 +1,9 @@ -import dayjs from "dayjs"; -import bigIntSupport from "dayjs/plugin/bigIntSupport"; -import localizedFormat from "dayjs/plugin/localizedFormat"; +import dayjs from 'dayjs'; +import bigIntSupport from 'dayjs/plugin/bigIntSupport'; +import localizedFormat from 'dayjs/plugin/localizedFormat'; dayjs.extend(bigIntSupport); dayjs.extend(localizedFormat); export function formatTimestampSecondsAsLocal(timestampSeconds: bigint) { - return dayjs(timestampSeconds * BigInt("1000")).format("L LT"); + return dayjs(timestampSeconds * BigInt('1000')).format('L LT'); } diff --git a/app/_services/frameButtons.tsx b/app/_services/frameButtons.tsx index a52fb2c0..da78eae5 100644 --- a/app/_services/frameButtons.tsx +++ b/app/_services/frameButtons.tsx @@ -1,53 +1,43 @@ -import { Button } from "frames.js/next"; -import { FrameState } from "frames.js/next/types"; +import { Button } from 'frames.js/next'; +import { FrameState } from 'frames.js/next/types'; -export const getFrameButtons = ( - buttonsData: any[], - currentState: FrameState, - urlContext: any -) => { - return buttonsData.map((button) => { - if (button.buttonAction === "link") { - const projectName = urlContext.pathname.split("/")[2]; - const strategyName = urlContext.pathname.split("/")[3]; - const deployment = urlContext.pathname.split("/")[4]; - return ( - - ); - } else if ( - button.buttonValue === "approve" || - button.buttonValue === "submit" - ) { - // Update state after tokens have been approved or submission has been made - const updatedState = { - ...(currentState as object), - ...(button.buttonValue === "approve" ? { tokensApproved: true } : {}), - ...(button.buttonValue === "submit" ? { currentStep: "done" } : {}), - }; - return ( - - ); - } - return ( - - ); - }); +export const getFrameButtons = (buttonsData: any[], currentState: FrameState, urlContext: any) => { + return buttonsData.map((button) => { + if (button.buttonAction === 'link') { + const projectName = urlContext.pathname.split('/')[2]; + const strategyName = urlContext.pathname.split('/')[3]; + const deployment = urlContext.pathname.split('/')[4]; + return ( + + ); + } else if (button.buttonValue === 'approve' || button.buttonValue === 'submit') { + // Update state after tokens have been approved or submission has been made + const updatedState = { + ...(currentState as object), + ...(button.buttonValue === 'approve' ? { tokensApproved: true } : {}), + ...(button.buttonValue === 'submit' ? { currentStep: 'done' } : {}) + }; + return ( + + ); + } + return ( + + ); + }); }; diff --git a/app/_services/frameState.ts b/app/_services/frameState.ts index 85b76dbd..1a37a615 100644 --- a/app/_services/frameState.ts +++ b/app/_services/frameState.ts @@ -1,168 +1,163 @@ -import { FrameState } from "../_types/frame"; -import { YamlData } from "../_types/yamlData"; +import { FrameState } from '../_types/frame'; +import { YamlData } from '../_types/yamlData'; export const getUpdatedFrameState = ( - yamlData: YamlData, - currentState: FrameState, - buttonValue: any, - inputText?: string + yamlData: YamlData, + currentState: FrameState, + buttonValue: any, + inputText?: string ): FrameState => { - const updatedState = { ...currentState }; - switch (currentState.currentStep) { - case "start": - const deploymentOptions = Object.values(yamlData.gui.deployments); - if (deploymentOptions.length === 1 || currentState.deploymentOption) { - // Deployment step can be skipped if there is only one deployment or if the deployment is already selected - updatedState.deploymentOption = deploymentOptions[0]; - updatedState.currentStep = "fields"; - const firstField = updatedState.deploymentOption.fields[0]; - if (firstField.min !== undefined && !firstField.presets) { - updatedState.textInputLabel = `Enter a number greater than ${firstField.min}`; - } - } else if (buttonValue) { - updatedState.currentStep = "deployment"; - } - break; - case "deployment": - if (buttonValue) { - updatedState.deploymentOption = JSON.parse(buttonValue); - if (!updatedState.deploymentOption) { - break; - } - updatedState.currentStep = "fields"; - const firstField = updatedState.deploymentOption.fields[0]; - if (firstField.min !== undefined && !firstField.presets) { - updatedState.textInputLabel = `Enter a number greater than ${firstField.min}`; - } - } - break; - case "fields": - if (!updatedState.deploymentOption) - throw new Error("Deployment option is required"); - let currentBindingsCount = Object.keys(updatedState.bindings).length; - const fields = updatedState.deploymentOption.fields; - const currentField = fields[currentBindingsCount]; + const updatedState = { ...currentState }; + switch (currentState.currentStep) { + case 'start': + const deploymentOptions = Object.values(yamlData.gui.deployments); + if (deploymentOptions.length === 1 || currentState.deploymentOption) { + // Deployment step can be skipped if there is only one deployment or if the deployment is already selected + updatedState.deploymentOption = deploymentOptions[0]; + updatedState.currentStep = 'fields'; + const firstField = updatedState.deploymentOption.fields[0]; + if (firstField.min !== undefined && !firstField.presets) { + updatedState.textInputLabel = `Enter a number greater than ${firstField.min}`; + } + } else if (buttonValue) { + updatedState.currentStep = 'deployment'; + } + break; + case 'deployment': + if (buttonValue) { + updatedState.deploymentOption = JSON.parse(buttonValue); + if (!updatedState.deploymentOption) { + break; + } + updatedState.currentStep = 'fields'; + const firstField = updatedState.deploymentOption.fields[0]; + if (firstField.min !== undefined && !firstField.presets) { + updatedState.textInputLabel = `Enter a number greater than ${firstField.min}`; + } + } + break; + case 'fields': + if (!updatedState.deploymentOption) throw new Error('Deployment option is required'); + let currentBindingsCount = Object.keys(updatedState.bindings).length; + const fields = updatedState.deploymentOption.fields; + const currentField = fields[currentBindingsCount]; - if (currentField.min !== undefined && !currentField.presets) { - updatedState.textInputLabel = `Enter a number greater than ${currentField.min}`; - } + if (currentField.min !== undefined && !currentField.presets) { + updatedState.textInputLabel = `Enter a number greater than ${currentField.min}`; + } - const setBindingValue = (value: string) => { - updatedState.bindings[currentField.binding] = value; - currentBindingsCount++; - updatedState.buttonPage = 0; - updatedState.error = null; - }; + const setBindingValue = (value: string) => { + updatedState.bindings[currentField.binding] = value; + currentBindingsCount++; + updatedState.buttonPage = 0; + updatedState.error = null; + }; - if (buttonValue === "submit") { - if (inputText && isNaN(Number(inputText))) { - updatedState.error = "Value must be a number"; - } else if ( - inputText && - currentField.min !== undefined && - Number(inputText) >= Number(currentField.min) - ) { - setBindingValue(inputText); - updatedState.textInputLabel = ""; - } else { - updatedState.error = `Value must be at least ${currentField.min}`; - } - } else if (buttonValue === "back") { - if (currentBindingsCount === 0) { - updatedState.currentStep = "deployment"; - updatedState.deploymentOption = undefined; - updatedState.textInputLabel = ""; - } else { - const currentField = fields[currentBindingsCount - 1]; - if (currentField.min !== undefined && !currentField.presets) { - updatedState.textInputLabel = `Enter a number greater than ${currentField.min}`; - } - delete updatedState.bindings[currentField.binding]; - } - updatedState.error = null; - } else if (buttonValue) { - setBindingValue(buttonValue); - } - // If all bindings are filled, we can move to the next step - if (currentBindingsCount >= fields.length) { - updatedState.currentStep = "deposit"; - } + if (buttonValue === 'submit') { + if (inputText && isNaN(Number(inputText))) { + updatedState.error = 'Value must be a number'; + } else if ( + inputText && + currentField.min !== undefined && + Number(inputText) >= Number(currentField.min) + ) { + setBindingValue(inputText); + updatedState.textInputLabel = ''; + } else { + updatedState.error = `Value must be at least ${currentField.min}`; + } + } else if (buttonValue === 'back') { + if (currentBindingsCount === 0) { + updatedState.currentStep = 'deployment'; + updatedState.deploymentOption = undefined; + updatedState.textInputLabel = ''; + } else { + const currentField = fields[currentBindingsCount - 1]; + if (currentField.min !== undefined && !currentField.presets) { + updatedState.textInputLabel = `Enter a number greater than ${currentField.min}`; + } + delete updatedState.bindings[currentField.binding]; + } + updatedState.error = null; + } else if (buttonValue) { + setBindingValue(buttonValue); + } + // If all bindings are filled, we can move to the next step + if (currentBindingsCount >= fields.length) { + updatedState.currentStep = 'deposit'; + } - break; - case "deposit": - if (!updatedState.deploymentOption) - throw new Error("Deployment option is required"); + break; + case 'deposit': + if (!updatedState.deploymentOption) throw new Error('Deployment option is required'); - let currentDepositCount = updatedState.deposits.length; - const deposits = updatedState.deploymentOption.deposits; - const currentDeposit = deposits[currentDepositCount]; + let currentDepositCount = updatedState.deposits.length; + const deposits = updatedState.deploymentOption.deposits; + const currentDeposit = deposits[currentDepositCount]; - if (currentDeposit.min !== undefined && !currentDeposit.presets) { - updatedState.textInputLabel = `Enter a number greater than ${currentDeposit.min}`; - } + if (currentDeposit.min !== undefined && !currentDeposit.presets) { + updatedState.textInputLabel = `Enter a number greater than ${currentDeposit.min}`; + } - const setDepositValue = (value: number) => { - const tokenInfo = updatedState.tokenInfos.find( - (info) => info.yamlName === currentDeposit.token - ); - if (!tokenInfo) - throw new Error(`Token info not found for ${currentDeposit.token}`); + const setDepositValue = (value: number) => { + const tokenInfo = updatedState.tokenInfos.find( + (info) => info.yamlName === currentDeposit.token + ); + if (!tokenInfo) throw new Error(`Token info not found for ${currentDeposit.token}`); - updatedState.deposits.push({ - tokenInfo, - referrals: currentDeposit.referrals, - amount: value, - }); - updatedState.error = null; + updatedState.deposits.push({ + tokenInfo, + referrals: currentDeposit.referrals, + amount: value + }); + updatedState.error = null; - if (currentDepositCount >= deposits.length - 1) { - updatedState.currentStep = "review"; - updatedState.buttonPage = 0; - } - }; + if (currentDepositCount >= deposits.length - 1) { + updatedState.currentStep = 'review'; + updatedState.buttonPage = 0; + } + }; - if (buttonValue === "submit" && currentDeposit.min !== undefined) { - if (inputText && isNaN(Number(inputText))) { - updatedState.error = "Value must be a number"; - } else if (inputText && parseFloat(inputText) >= currentDeposit.min) { - setDepositValue(Number(inputText)); - updatedState.textInputLabel = ""; - } else { - updatedState.error = `Value must be at least ${currentDeposit.min}`; - } - } else if (buttonValue === "back") { - if (currentDepositCount === 0) { - const currentField = - updatedState.deploymentOption.fields[ - Object.keys(updatedState.bindings).length - 1 - ]; - updatedState.textInputLabel = ""; - delete updatedState.bindings[currentField.binding]; - updatedState.currentStep = "fields"; - } else { - updatedState.deposits.pop(); - currentDepositCount--; - } - updatedState.error = null; - } else { - setDepositValue(Number(buttonValue)); - } - break; - case "review": - if (buttonValue === "back") { - updatedState.deposits.pop(); - updatedState.currentStep = "deposit"; - } else if (buttonValue === "submit") { - updatedState.currentStep = "done"; - } - break; - case "done": - if (buttonValue) { - updatedState.currentStep = "start"; - updatedState.deploymentOption = undefined; - updatedState.bindings = {}; - } - break; - } - return updatedState; + if (buttonValue === 'submit' && currentDeposit.min !== undefined) { + if (inputText && isNaN(Number(inputText))) { + updatedState.error = 'Value must be a number'; + } else if (inputText && parseFloat(inputText) >= currentDeposit.min) { + setDepositValue(Number(inputText)); + updatedState.textInputLabel = ''; + } else { + updatedState.error = `Value must be at least ${currentDeposit.min}`; + } + } else if (buttonValue === 'back') { + if (currentDepositCount === 0) { + const currentField = + updatedState.deploymentOption.fields[Object.keys(updatedState.bindings).length - 1]; + updatedState.textInputLabel = ''; + delete updatedState.bindings[currentField.binding]; + updatedState.currentStep = 'fields'; + } else { + updatedState.deposits.pop(); + currentDepositCount--; + } + updatedState.error = null; + } else { + setDepositValue(Number(buttonValue)); + } + break; + case 'review': + if (buttonValue === 'back') { + updatedState.deposits.pop(); + updatedState.currentStep = 'deposit'; + } else if (buttonValue === 'submit') { + updatedState.currentStep = 'done'; + } + break; + case 'done': + if (buttonValue) { + updatedState.currentStep = 'start'; + updatedState.deploymentOption = undefined; + updatedState.bindings = {}; + } + break; + } + return updatedState; }; diff --git a/app/_services/frameTransactions.ts b/app/_services/frameTransactions.ts index c3ee945c..6e0949ed 100644 --- a/app/_services/frameTransactions.ts +++ b/app/_services/frameTransactions.ts @@ -1,101 +1,89 @@ -import { transaction } from "frames.js/core"; -import { Abi, encodeFunctionData, erc20Abi, parseUnits, toHex } from "viem"; -import { YamlData } from "@/app/_types/yamlData"; -import { FrameState } from "@/app/_types/frame"; -import { readContract } from "viem/actions"; -import { orderBookJson } from "@/public/_abis/OrderBook"; -import { getSubmissionTransactionData } from "./transactionData"; -import { getOrderDetailsGivenDeployment } from "./parseDotrainFrontmatter"; -import { getPublicClient } from "./getPublicClient"; +import { transaction } from 'frames.js/core'; +import { Abi, encodeFunctionData, erc20Abi, parseUnits, toHex } from 'viem'; +import { YamlData } from '@/app/_types/yamlData'; +import { FrameState } from '@/app/_types/frame'; +import { readContract } from 'viem/actions'; +import { orderBookJson } from '@/public/_abis/OrderBook'; +import { getSubmissionTransactionData } from './transactionData'; +import { getOrderDetailsGivenDeployment } from './parseDotrainFrontmatter'; +import { getPublicClient } from './getPublicClient'; -export const getApprovalTransaction = async ( - currentState: FrameState, - yamlData: YamlData -) => { - if (!currentState.deploymentOption) { - throw new Error( - "Deployment option is required to get approval transaction" - ); - } - // Get network and orderbook data from the yaml file - const deployment = - yamlData.deployments[currentState.deploymentOption.deployment]; - const order = yamlData.orders[deployment.order]; - const network = yamlData.networks[order.network]; +export const getApprovalTransaction = async (currentState: FrameState, yamlData: YamlData) => { + if (!currentState.deploymentOption) { + throw new Error('Deployment option is required to get approval transaction'); + } + // Get network and orderbook data from the yaml file + const deployment = yamlData.deployments[currentState.deploymentOption.deployment]; + const order = yamlData.orders[deployment.order]; + const network = yamlData.networks[order.network]; - const orderBook = yamlData.orderbooks[order.orderbook]; - const orderBookAddress = toHex(BigInt(orderBook.address)); + const orderBook = yamlData.orderbooks[order.orderbook]; + const orderBookAddress = toHex(BigInt(orderBook.address)); - const outputToken = yamlData.tokens[order.outputs[0].token]; - const outputTokenAddress = toHex(BigInt(outputToken.address)); + const outputToken = yamlData.tokens[order.outputs[0].token]; + const outputTokenAddress = toHex(BigInt(outputToken.address)); - const client = getPublicClient(network); - const outputTokenDecimals = await readContract(client, { - abi: erc20Abi, - address: outputTokenAddress, - functionName: "decimals", - }); + const client = getPublicClient(network); + const outputTokenDecimals = await readContract(client, { + abi: erc20Abi, + address: outputTokenAddress, + functionName: 'decimals' + }); - const depositAmount = parseUnits( - String(currentState.deposits[0].amount), - outputTokenDecimals - ); + const depositAmount = parseUnits(String(currentState.deposits[0].amount), outputTokenDecimals); - const approvalCalldata = encodeFunctionData({ - abi: erc20Abi, - functionName: "approve", - args: [orderBookAddress, depositAmount], - }); + const approvalCalldata = encodeFunctionData({ + abi: erc20Abi, + functionName: 'approve', + args: [orderBookAddress, depositAmount] + }); - // Return transaction data that conforms to the correct type - return transaction({ - chainId: `eip155:${network["chain-id"]}`, - method: "eth_sendTransaction", - params: { - abi: erc20Abi, - to: outputTokenAddress, - data: approvalCalldata, - }, - }); + // Return transaction data that conforms to the correct type + return transaction({ + chainId: `eip155:${network['chain-id']}`, + method: 'eth_sendTransaction', + params: { + abi: erc20Abi, + to: outputTokenAddress, + data: approvalCalldata + } + }); }; export const getSubmissionTransaction = async ( - currentState: FrameState, - yamlData: YamlData, - dotrainText: string + currentState: FrameState, + yamlData: YamlData, + dotrainText: string ) => { - if (!currentState.deploymentOption) { - throw new Error( - "Deployment option is required to get submission transaction" - ); - } + if (!currentState.deploymentOption) { + throw new Error('Deployment option is required to get submission transaction'); + } - const { addOrderCalldata, depositCalldatas } = - await getSubmissionTransactionData( - currentState, - dotrainText, - currentState.deposits - ); + const { addOrderCalldata, depositCalldatas } = await getSubmissionTransactionData( + currentState, + dotrainText, + currentState.deposits + ); - const multicallCalldata = encodeFunctionData({ - functionName: "multicall", - abi: orderBookJson.abi, - args: [[addOrderCalldata, ...depositCalldatas]], - }); + const multicallCalldata = encodeFunctionData({ + functionName: 'multicall', + abi: orderBookJson.abi, + args: [[addOrderCalldata, ...depositCalldatas]] + }); - const { orderBookAddress, network } = getOrderDetailsGivenDeployment( - yamlData, - currentState.deploymentOption.deployment - ); + const { orderBookAddress, network } = getOrderDetailsGivenDeployment( + yamlData, + currentState.deploymentOption.deployment + ); - // Return transaction data that conforms to the correct type - return transaction({ - chainId: `eip155:${network["chain-id"]}`, - method: "eth_sendTransaction", - params: { - abi: orderBookJson.abi as Abi, - to: orderBookAddress, - data: multicallCalldata, - }, - }); + // Return transaction data that conforms to the correct type + return transaction({ + chainId: `eip155:${network['chain-id']}`, + method: 'eth_sendTransaction', + params: { + abi: orderBookJson.abi as Abi, + to: orderBookAddress, + data: multicallCalldata + } + }); }; diff --git a/app/_services/getPublicClient.tsx b/app/_services/getPublicClient.tsx index e489787d..849872c9 100644 --- a/app/_services/getPublicClient.tsx +++ b/app/_services/getPublicClient.tsx @@ -1,23 +1,21 @@ -import * as chains from "viem/chains"; -import { createPublicClient, http } from "viem"; +import * as chains from 'viem/chains'; +import { createPublicClient, http } from 'viem'; export const getPublicClient = (network: any) => { - let chain = Object.values(chains).find( - (chain) => chain.id === Number(network["chain-id"]) - ); - if (chain?.id === 14) { - chain = { - ...chain, - contracts: { - multicall3: { - address: "0xcA11bde05977b3631167028862bE2a173976CA11", - blockCreated: 3002461, - }, - }, - }; - } - return createPublicClient({ - chain, - transport: http(), - }); + let chain = Object.values(chains).find((chain) => chain.id === Number(network['chain-id'])); + if (chain?.id === 14) { + chain = { + ...chain, + contracts: { + multicall3: { + address: '0xcA11bde05977b3631167028862bE2a173976CA11', + blockCreated: 3002461 + } + } + }; + } + return createPublicClient({ + chain, + transport: http() + }); }; diff --git a/app/_services/getTokenInfo.tsx b/app/_services/getTokenInfo.tsx index deb54bd2..907ee64e 100644 --- a/app/_services/getTokenInfo.tsx +++ b/app/_services/getTokenInfo.tsx @@ -1,70 +1,65 @@ -import { YamlData } from "../_types/yamlData"; -import { Address, erc20Abi, isHex } from "viem"; -import { getPublicClient } from "./getPublicClient"; +import { YamlData } from '../_types/yamlData'; +import { Address, erc20Abi, isHex } from 'viem'; +import { getPublicClient } from './getPublicClient'; export interface TokenInfo { - decimals: number; - symbol: string; - name: string; - yamlName: string; - address: Address; + decimals: number; + symbol: string; + name: string; + yamlName: string; + address: Address; } export const getTokenInfos = async (yaml: YamlData): Promise => { - let tokenInfos = []; + let tokenInfos = []; - for (const [tokenName, token] of Object.entries(yaml.tokens)) { - if (!isHex(token.address)) - throw new Error( - `Token address for ${tokenName} is not a hex string: got ${token.address}` - ); + for (const [tokenName, token] of Object.entries(yaml.tokens)) { + if (!isHex(token.address)) + throw new Error(`Token address for ${tokenName} is not a hex string: got ${token.address}`); - if (!token.network) - throw new Error(`Token ${tokenName} does not have a network`); + if (!token.network) throw new Error(`Token ${tokenName} does not have a network`); - const tokenContract = { - address: token.address, - abi: erc20Abi, - } as const; + const tokenContract = { + address: token.address, + abi: erc20Abi + } as const; - const client = getPublicClient(yaml.networks[token.network]); + const client = getPublicClient(yaml.networks[token.network]); - const res = await client.multicall({ - contracts: [ - { - ...tokenContract, - functionName: "decimals", - }, - { - ...tokenContract, - functionName: "symbol", - }, - { - ...tokenContract, - functionName: "name", - }, - ], - }); + const res = await client.multicall({ + contracts: [ + { + ...tokenContract, + functionName: 'decimals' + }, + { + ...tokenContract, + functionName: 'symbol' + }, + { + ...tokenContract, + functionName: 'name' + } + ] + }); - if (res.some((r) => r.status !== "success")) { - throw new Error( - `Failed to fetch token info for ${tokenName}: ${res - .map((r) => r.error) - .join(", ")}` - ); - } + if (res.some((r) => r.status !== 'success')) { + throw new Error( + `Failed to fetch token info for ${tokenName}: ${res.map((r) => r.error).join(', ')}` + ); + } - if (!res[0].result) throw new Error(`Token ${tokenName} has no decimals`); - if (!res[1].result) throw new Error(`Token ${tokenName} has no symbol`); - if (!res[2].result) throw new Error(`Token ${tokenName} has no name`); + if (!res[0].result) throw new Error(`Token ${tokenName} has no decimals`); + if (!res[1].result) throw new Error(`Token ${tokenName} has no symbol`); + if (!res[2].result) throw new Error(`Token ${tokenName} has no name`); - tokenInfos.push({ - yamlName: tokenName, - address: token.address, - decimals: res[0].result, - symbol: res[1].result, - name: res[2].result, - }); - } - return tokenInfos; + tokenInfos.push({ + yamlName: tokenName, + address: token.address, + decimals: res[0].result, + symbol: res[1].result, + name: res[2].result + }); + } + return tokenInfos; }; diff --git a/app/_services/parseDotrainFrontmatter.tsx b/app/_services/parseDotrainFrontmatter.tsx index fe666aa2..088b28b0 100644 --- a/app/_services/parseDotrainFrontmatter.tsx +++ b/app/_services/parseDotrainFrontmatter.tsx @@ -1,34 +1,31 @@ -import { toHex } from "viem"; -import { YamlData } from "../_types/yamlData"; -import _ from "lodash"; +import { toHex } from 'viem'; +import { YamlData } from '../_types/yamlData'; +import _ from 'lodash'; -export const getOrderDetailsGivenDeployment = ( - yamlData: YamlData, - deploymentOption: string -) => { - const deployment = yamlData.deployments[deploymentOption]; - const order = yamlData.orders[deployment.order]; - const orderBook = yamlData.orderbooks[order.orderbook]; - const orderBookAddress = toHex(BigInt(orderBook.address)); - const network = yamlData.networks[order.network]; +export const getOrderDetailsGivenDeployment = (yamlData: YamlData, deploymentOption: string) => { + const deployment = yamlData.deployments[deploymentOption]; + const order = yamlData.orders[deployment.order]; + const orderBook = yamlData.orderbooks[order.orderbook]; + const orderBookAddress = toHex(BigInt(orderBook.address)); + const network = yamlData.networks[order.network]; - const tokens = yamlData.tokens; + const tokens = yamlData.tokens; - const fullScenarioPath = deployment.scenario - .split(".") - .map((scenario, index, array) => - index === array.length - 1 ? scenario : scenario + ".scenarios" - ) - .join("."); - const scenario = _.get(yamlData.scenarios, fullScenarioPath); + const fullScenarioPath = deployment.scenario + .split('.') + .map((scenario, index, array) => + index === array.length - 1 ? scenario : scenario + '.scenarios' + ) + .join('.'); + const scenario = _.get(yamlData.scenarios, fullScenarioPath); - return { - deployment, - order, - orderBook, - orderBookAddress, - network, - tokens, - scenario, - }; + return { + deployment, + order, + orderBook, + orderBookAddress, + network, + tokens, + scenario + }; }; diff --git a/app/_services/tokenApproval.ts b/app/_services/tokenApproval.ts index f211c2d6..65d2c5ac 100644 --- a/app/_services/tokenApproval.ts +++ b/app/_services/tokenApproval.ts @@ -1,42 +1,38 @@ -import { erc20Abi, parseUnits, toHex } from "viem"; -import { FrameState } from "../_types/frame"; -import { YamlData } from "../_types/yamlData"; -import { readContract } from "viem/actions"; -import { getPublicClient } from "./getPublicClient"; +import { erc20Abi, parseUnits, toHex } from 'viem'; +import { FrameState } from '../_types/frame'; +import { YamlData } from '../_types/yamlData'; +import { readContract } from 'viem/actions'; +import { getPublicClient } from './getPublicClient'; export const hasEnoughTokenApproval = async ( - currentState: FrameState, - yamlData: YamlData, - requesterCustodyAddress: `0x${string}` + currentState: FrameState, + yamlData: YamlData, + requesterCustodyAddress: `0x${string}` ) => { - const deployment = - yamlData.deployments[currentState.deploymentOption?.deployment || ""]; - const order = yamlData.orders[deployment.order]; + const deployment = yamlData.deployments[currentState.deploymentOption?.deployment || '']; + const order = yamlData.orders[deployment.order]; - const orderBook = yamlData.orderbooks[order.orderbook]; - const orderBookAddress = toHex(BigInt(orderBook.address)); + const orderBook = yamlData.orderbooks[order.orderbook]; + const orderBookAddress = toHex(BigInt(orderBook.address)); - const network = yamlData.networks[order.network]; + const network = yamlData.networks[order.network]; - const outputToken = yamlData.tokens[order.outputs[0].token]; - const outputTokenAddress = toHex(BigInt(outputToken.address)); - const client = getPublicClient(network); - const outputTokenDecimals = await readContract(client, { - abi: erc20Abi, - address: outputTokenAddress, - functionName: "decimals", - }); + const outputToken = yamlData.tokens[order.outputs[0].token]; + const outputTokenAddress = toHex(BigInt(outputToken.address)); + const client = getPublicClient(network); + const outputTokenDecimals = await readContract(client, { + abi: erc20Abi, + address: outputTokenAddress, + functionName: 'decimals' + }); - // Get token approval for output token, if required - const depositAmount = parseUnits( - String(currentState.deposits[0].amount), - outputTokenDecimals - ); - const existingAllowance = await readContract(client, { - abi: erc20Abi, - address: outputTokenAddress, - functionName: "allowance", - args: [requesterCustodyAddress, orderBookAddress], - }); - return existingAllowance >= depositAmount; + // Get token approval for output token, if required + const depositAmount = parseUnits(String(currentState.deposits[0].amount), outputTokenDecimals); + const existingAllowance = await readContract(client, { + abi: erc20Abi, + address: outputTokenAddress, + functionName: 'allowance', + args: [requesterCustodyAddress, orderBookAddress] + }); + return existingAllowance >= depositAmount; }; diff --git a/app/_services/transactionData.ts b/app/_services/transactionData.ts index 1eb23b0f..b1c212f9 100644 --- a/app/_services/transactionData.ts +++ b/app/_services/transactionData.ts @@ -1,64 +1,54 @@ -import { getAddOrderCalldata } from "@rainlanguage/orderbook/common"; -import { - decodeFunctionData, - encodeFunctionData, - getAddress, - parseUnits, - toHex, -} from "viem"; -import { FrameState } from "../_types/frame"; -import { orderBookJson } from "@/public/_abis/OrderBook"; -import { TokenDeposit } from "../_components/SubmissionModal"; +import { getAddOrderCalldata } from '@rainlanguage/orderbook/common'; +import { decodeFunctionData, encodeFunctionData, getAddress, parseUnits, toHex } from 'viem'; +import { FrameState } from '../_types/frame'; +import { orderBookJson } from '@/public/_abis/OrderBook'; +import { TokenDeposit } from '../_components/SubmissionModal'; interface DecodedAddOrderCallData { - args?: { - validOutputs?: { - vaultId: string; - token: string; - }[]; - }[]; + args?: { + validOutputs?: { + vaultId: string; + token: string; + }[]; + }[]; } export const getSubmissionTransactionData = async ( - currentState: FrameState, - dotrainText: string, - tokenDeposits: TokenDeposit[] + currentState: FrameState, + dotrainText: string, + tokenDeposits: TokenDeposit[] ) => { - const addOrderCalldata = await getAddOrderCalldata( - dotrainText, - currentState.deploymentOption?.deployment || "" - ); - - // Get the vault ids from the decoded calldata - const decodedAddOrderCalldata = decodeFunctionData({ - data: toHex(addOrderCalldata), - abi: orderBookJson.abi, - }) as DecodedAddOrderCallData; - - const depositCalldatas = tokenDeposits.map((tokenDeposit) => { - const depositAmount = parseUnits( - String(tokenDeposit.amount), - tokenDeposit.tokenInfo.decimals - ); - - const vaultId = decodedAddOrderCalldata.args?.[0]?.validOutputs?.find( - (io) => - getAddress(io.token) === getAddress(tokenDeposit.tokenInfo.address) - )?.vaultId; - - if (!vaultId) { - throw new Error("Vault id not found"); - } - - return encodeFunctionData({ - functionName: "deposit2", - abi: orderBookJson.abi, - args: [tokenDeposit.tokenInfo.address, toHex(vaultId), depositAmount, []], - }); - }); - - return { - addOrderCalldata: toHex(addOrderCalldata), - depositCalldatas, - }; + const addOrderCalldata = await getAddOrderCalldata( + dotrainText, + currentState.deploymentOption?.deployment || '' + ); + + // Get the vault ids from the decoded calldata + const decodedAddOrderCalldata = decodeFunctionData({ + data: toHex(addOrderCalldata), + abi: orderBookJson.abi + }) as DecodedAddOrderCallData; + + const depositCalldatas = tokenDeposits.map((tokenDeposit) => { + const depositAmount = parseUnits(String(tokenDeposit.amount), tokenDeposit.tokenInfo.decimals); + + const vaultId = decodedAddOrderCalldata.args?.[0]?.validOutputs?.find( + (io) => getAddress(io.token) === getAddress(tokenDeposit.tokenInfo.address) + )?.vaultId; + + if (!vaultId) { + throw new Error('Vault id not found'); + } + + return encodeFunctionData({ + functionName: 'deposit2', + abi: orderBookJson.abi, + args: [tokenDeposit.tokenInfo.address, toHex(vaultId), depositAmount, []] + }); + }); + + return { + addOrderCalldata: toHex(addOrderCalldata), + depositCalldatas + }; }; diff --git a/app/_types/frame.ts b/app/_types/frame.ts index 4fa5ae1e..05889b44 100644 --- a/app/_types/frame.ts +++ b/app/_types/frame.ts @@ -1,18 +1,18 @@ -import { TokenDeposit } from "../_components/SubmissionModal"; -import { TokenInfo } from "../_services/getTokenInfo"; -import { YamlData } from "./yamlData"; +import { TokenDeposit } from '../_components/SubmissionModal'; +import { TokenInfo } from '../_services/getTokenInfo'; +import { YamlData } from './yamlData'; export type FrameState = { - strategyName: string | null; - strategyDescription: string | null; - currentStep: string; - deploymentOption?: YamlData["gui"]["deployments"][0]; - bindings: any; - deposits: TokenDeposit[]; - buttonPage: number; - buttonMax?: number; - textInputLabel: string; - error: string | null; - isWebapp?: boolean; - tokenInfos: TokenInfo[]; + strategyName: string | null; + strategyDescription: string | null; + currentStep: string; + deploymentOption?: YamlData['gui']['deployments'][0]; + bindings: any; + deposits: TokenDeposit[]; + buttonPage: number; + buttonMax?: number; + textInputLabel: string; + error: string | null; + isWebapp?: boolean; + tokenInfos: TokenInfo[]; }; diff --git a/app/_types/yamlData.ts b/app/_types/yamlData.ts index a47d05ef..970593e6 100644 --- a/app/_types/yamlData.ts +++ b/app/_types/yamlData.ts @@ -1,100 +1,100 @@ export interface Field { - binding: string; - name: string; - description: string; - min?: number; - presets?: Preset[]; + binding: string; + name: string; + description: string; + min?: number; + presets?: Preset[]; } export interface Preset { - name: string; - value: number; + name: string; + value: number; } export interface Referral { - name: string; - url: string; + name: string; + url: string; } export interface Deposit { - token: string; - min?: number; - presets?: number[]; - referrals?: Referral[]; + token: string; + min?: number; + presets?: number[]; + referrals?: Referral[]; } export interface DeploymentOption { - deployment: string; - name: string; - description: string; - fields: Field[]; - deposit: Deposit; - deposits: Deposit[]; + deployment: string; + name: string; + description: string; + fields: Field[]; + deposit: Deposit; + deposits: Deposit[]; } export interface Gui { - name: string; - description: string; - deployments: DeploymentOption[]; + name: string; + description: string; + deployments: DeploymentOption[]; } interface Network { - rpc: string; - "chain-id": number; - "network-id": number; - currency: string; + rpc: string; + 'chain-id': number; + 'network-id': number; + currency: string; } interface Orderbook { - address: string; + address: string; } interface Deployer { - address: string; - network: string; + address: string; + network: string; } interface Token { - network: string; - address: string; - decimals: number; + network: string; + address: string; + decimals: number; } interface OrderInputOutput { - token: string; - "vault-id": string; + token: string; + 'vault-id': string; } interface Order { - orderbook: string; - inputs: OrderInputOutput[]; - outputs: OrderInputOutput[]; - network: string; + orderbook: string; + inputs: OrderInputOutput[]; + outputs: OrderInputOutput[]; + network: string; } interface ScenarioBinding { - [key: string]: number; + [key: string]: number; } interface Scenario { - deployer: string; - runs: number; - bindings: ScenarioBinding; + deployer: string; + runs: number; + bindings: ScenarioBinding; } interface Deployment { - scenario: string; - order: string; + scenario: string; + order: string; } export interface YamlData { - networks: { [key: string]: Network }; - subgraphs: { [key: string]: string }; - orderbooks: { [key: string]: Orderbook }; - deployers: { [key: string]: Deployer }; - tokens: { [key: string]: Token }; - orders: { [key: string]: Order }; - scenarios: { [key: string]: Scenario }; - deployments: { [key: string]: Deployment }; - gui: Gui; + networks: { [key: string]: Network }; + subgraphs: { [key: string]: string }; + orderbooks: { [key: string]: Orderbook }; + deployers: { [key: string]: Deployer }; + tokens: { [key: string]: Token }; + orders: { [key: string]: Order }; + scenarios: { [key: string]: Scenario }; + deployments: { [key: string]: Deployment }; + gui: Gui; } diff --git a/app/frames/[projectName]/[strategyName]/[deployment]/frames.ts b/app/frames/[projectName]/[strategyName]/[deployment]/frames.ts index 77dbb7f3..99e0f313 100644 --- a/app/frames/[projectName]/[strategyName]/[deployment]/frames.ts +++ b/app/frames/[projectName]/[strategyName]/[deployment]/frames.ts @@ -1,72 +1,68 @@ -import { YamlData } from "@/app/_types/yamlData"; -import { farcasterHubContext } from "frames.js/middleware"; -import { createFrames, types } from "frames.js/next"; -import { FrameState } from "frames.js/next/types"; -import path from "path"; -import yaml from "js-yaml"; -import fs from "fs"; -import { FailsafeSchemaWithNumbers } from "@/app/_schemas/failsafeWithNumbers"; +import { YamlData } from '@/app/_types/yamlData'; +import { farcasterHubContext } from 'frames.js/middleware'; +import { createFrames, types } from 'frames.js/next'; +import { FrameState } from 'frames.js/next/types'; +import path from 'path'; +import yaml from 'js-yaml'; +import fs from 'fs'; +import { FailsafeSchemaWithNumbers } from '@/app/_schemas/failsafeWithNumbers'; const dotrainContext: types.FramesMiddleware< - any, - { dotrainText: string; yamlData: YamlData } + any, + { dotrainText: string; yamlData: YamlData } > = async (ctx, next) => { - const projectName = ctx.url.pathname.split("/")[2]; - const strategyName = ctx.url.pathname.split("/")[3]; + const projectName = ctx.url.pathname.split('/')[2]; + const strategyName = ctx.url.pathname.split('/')[3]; - const allDirs = fs.readdirSync( - path.join(process.cwd(), "public", "_strategies", projectName) - ); + const allDirs = fs.readdirSync(path.join(process.cwd(), 'public', '_strategies', projectName)); - const strategyDir = allDirs.find((dir) => dir.endsWith(strategyName)); + const strategyDir = allDirs.find((dir) => dir.endsWith(strategyName)); - if (!strategyDir) { - throw new Error( - `No directory found for strategy: ${strategyName} in project: ${projectName}` - ); - } + if (!strategyDir) { + throw new Error(`No directory found for strategy: ${strategyName} in project: ${projectName}`); + } - const filePath = path.join( - process.cwd(), - "public", - "_strategies", - projectName, - strategyDir, - `${strategyName}.rain` - ); - const dotrainText = fs.readFileSync(filePath, "utf8"); - const yamlData = yaml.load(dotrainText.split("---")[0], { - schema: FailsafeSchemaWithNumbers, - }) as YamlData; + const filePath = path.join( + process.cwd(), + 'public', + '_strategies', + projectName, + strategyDir, + `${strategyName}.rain` + ); + const dotrainText = fs.readFileSync(filePath, 'utf8'); + const yamlData = yaml.load(dotrainText.split('---')[0], { + schema: FailsafeSchemaWithNumbers + }) as YamlData; - return next({ dotrainText, yamlData }); + return next({ dotrainText, yamlData }); }; export const frames = createFrames({ - basePath: "", - middleware: [ - farcasterHubContext({ - // remove if you aren't using @frames.js/debugger or you just don't want to use the debugger hub - ...(process.env.NODE_ENV === "production" - ? {} - : { - hubHttpUrl: "http://localhost:3010/hub", - }), - }), - dotrainContext, - ], - initialState: { - strategyName: null, - strategyDescription: null, - currentStep: "start", - deploymentOption: null, - bindings: {}, - deposits: [], - buttonPage: 0, - textInputLabel: "", - error: null, - requiresTokenApproval: true, - tokensApproved: false, - tokenInfos: [], - }, + basePath: '', + middleware: [ + farcasterHubContext({ + // remove if you aren't using @frames.js/debugger or you just don't want to use the debugger hub + ...(process.env.NODE_ENV === 'production' + ? {} + : { + hubHttpUrl: 'http://localhost:3010/hub' + }) + }), + dotrainContext + ], + initialState: { + strategyName: null, + strategyDescription: null, + currentStep: 'start', + deploymentOption: null, + bindings: {}, + deposits: [], + buttonPage: 0, + textInputLabel: '', + error: null, + requiresTokenApproval: true, + tokensApproved: false, + tokenInfos: [] + } }); diff --git a/app/frames/[projectName]/[strategyName]/[deployment]/route.tsx b/app/frames/[projectName]/[strategyName]/[deployment]/route.tsx index 80972114..20c0f62f 100644 --- a/app/frames/[projectName]/[strategyName]/[deployment]/route.tsx +++ b/app/frames/[projectName]/[strategyName]/[deployment]/route.tsx @@ -1,115 +1,92 @@ -import { FrameImage } from "../../../../_components/FrameImage"; -import { generateButtonsData } from "../../../../_services/buttonsData"; -import { getUpdatedFrameState } from "../../../../_services/frameState"; -import { frames } from "./frames"; -import { FrameState } from "@/app/_types/frame"; +import { FrameImage } from '../../../../_components/FrameImage'; +import { generateButtonsData } from '../../../../_services/buttonsData'; +import { getUpdatedFrameState } from '../../../../_services/frameState'; +import { frames } from './frames'; +import { FrameState } from '@/app/_types/frame'; import { - getApprovalTransaction, - getSubmissionTransaction, -} from "@/app/_services/frameTransactions"; -import { getFrameButtons } from "@/app/_services/frameButtons"; -import * as fs from "node:fs/promises"; -import * as path from "node:path"; -import yaml from "js-yaml"; -import { FrameState as FrameJsFrameState } from "frames.js/next/types"; -import { getTokenInfos } from "@/app/_services/getTokenInfo"; + getApprovalTransaction, + getSubmissionTransaction +} from '@/app/_services/frameTransactions'; +import { getFrameButtons } from '@/app/_services/frameButtons'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import yaml from 'js-yaml'; +import { FrameState as FrameJsFrameState } from 'frames.js/next/types'; +import { getTokenInfos } from '@/app/_services/getTokenInfo'; const handleRequest = frames(async (ctx) => { - const yamlData = ctx.yamlData; - let currentState: FrameState = ctx.state as unknown as FrameState; + const yamlData = ctx.yamlData; + let currentState: FrameState = ctx.state as unknown as FrameState; - // Handle state restoration after transactions - if (ctx.url.searchParams.has("currentState")) { - currentState = JSON.parse( - decodeURI(ctx.url.searchParams.get("currentState")!) - ); - } + // Handle state restoration after transactions + if (ctx.url.searchParams.has('currentState')) { + currentState = JSON.parse(decodeURI(ctx.url.searchParams.get('currentState')!)); + } - if (currentState && !currentState.strategyName) { - currentState.strategyName = yamlData.gui.name; - } + if (currentState && !currentState.strategyName) { + currentState.strategyName = yamlData.gui.name; + } - if ( - currentState && - !currentState.deploymentOption && - ctx.url.pathname.split("/")[4] - ) { - currentState.deploymentOption = - yamlData.gui.deployments.find( - (deployment: any) => - deployment.deployment === ctx.url.pathname.split("/")[4] - ) || undefined; - } + if (currentState && !currentState.deploymentOption && ctx.url.pathname.split('/')[4]) { + currentState.deploymentOption = + yamlData.gui.deployments.find( + (deployment: any) => deployment.deployment === ctx.url.pathname.split('/')[4] + ) || undefined; + } - if (currentState && !currentState.strategyDescription) { - currentState.strategyDescription = yamlData.gui.description; - } + if (currentState && !currentState.strategyDescription) { + currentState.strategyDescription = yamlData.gui.description; + } - if (currentState && !currentState.tokenInfos.length) { - const tokenInfos = await getTokenInfos(yamlData); - currentState.tokenInfos = tokenInfos; - } + if (currentState && !currentState.tokenInfos.length) { + const tokenInfos = await getTokenInfos(yamlData); + currentState.tokenInfos = tokenInfos; + } - // Handle page navigation - if (ctx.url.searchParams.has("textInputLabel")) { - currentState.textInputLabel = ctx.url.searchParams.get( - "textInputLabel" - ) as string; - currentState.error = null; - } else if (ctx.url.searchParams.has("buttonPage")) { - currentState.buttonPage = parseInt(ctx.url.searchParams.get("buttonPage")!); - } else if (ctx.url.searchParams.get("buttonValue")) { - const buttonValue = ctx.url.searchParams.get("buttonValue"); - // Handle transactions - if (buttonValue === "approve") { - return getApprovalTransaction(currentState, yamlData); - } else if (buttonValue === "submit") { - const updatedDotrainText = - yaml.dump(yamlData) + "---" + ctx.dotrainText.split("---")[1]; - return getSubmissionTransaction( - currentState, - yamlData, - updatedDotrainText - ); - } - // Handle state transitions - const inputText = ctx.message?.inputText; - currentState = getUpdatedFrameState( - yamlData, - currentState, - buttonValue, - inputText - ); - } + // Handle page navigation + if (ctx.url.searchParams.has('textInputLabel')) { + currentState.textInputLabel = ctx.url.searchParams.get('textInputLabel') as string; + currentState.error = null; + } else if (ctx.url.searchParams.has('buttonPage')) { + currentState.buttonPage = parseInt(ctx.url.searchParams.get('buttonPage')!); + } else if (ctx.url.searchParams.get('buttonValue')) { + const buttonValue = ctx.url.searchParams.get('buttonValue'); + // Handle transactions + if (buttonValue === 'approve') { + return getApprovalTransaction(currentState, yamlData); + } else if (buttonValue === 'submit') { + const updatedDotrainText = yaml.dump(yamlData) + '---' + ctx.dotrainText.split('---')[1]; + return getSubmissionTransaction(currentState, yamlData, updatedDotrainText); + } + // Handle state transitions + const inputText = ctx.message?.inputText; + currentState = getUpdatedFrameState(yamlData, currentState, buttonValue, inputText); + } - // Generate buttons based on current state - const buttonsData = await generateButtonsData(yamlData, currentState); + // Generate buttons based on current state + const buttonsData = await generateButtonsData(yamlData, currentState); - const dmSansLight = fs.readFile( - path.join(path.resolve(process.cwd(), "public/_fonts"), "DMSans-Light.ttf") - ); + const dmSansLight = fs.readFile( + path.join(path.resolve(process.cwd(), 'public/_fonts'), 'DMSans-Light.ttf') + ); - const [dmSansLightData] = await Promise.all([dmSansLight]); + const [dmSansLightData] = await Promise.all([dmSansLight]); - return { - image: , - buttons: getFrameButtons( - buttonsData, - currentState as unknown as FrameJsFrameState, - ctx.url - ), - textInput: currentState.textInputLabel, - state: currentState as unknown as FrameJsFrameState, - imageOptions: { - fonts: [ - { - name: "DM Sans", - data: dmSansLightData, - weight: 400, - }, - ], - }, - }; + return { + image: , + buttons: getFrameButtons(buttonsData, currentState as unknown as FrameJsFrameState, ctx.url), + textInput: currentState.textInputLabel, + state: currentState as unknown as FrameJsFrameState, + imageOptions: { + fonts: [ + { + name: 'DM Sans', + data: dmSansLightData, + weight: 400 + } + ] + } + }; }); export const GET = handleRequest; diff --git a/app/my-strategies/[transactionId]/page.tsx b/app/my-strategies/[transactionId]/page.tsx index 83546996..97f1f1cd 100644 --- a/app/my-strategies/[transactionId]/page.tsx +++ b/app/my-strategies/[transactionId]/page.tsx @@ -1,13 +1,13 @@ -import StrategyAnalytics from "@/app/_components/StrategyAnalytics"; +import StrategyAnalytics from '@/app/_components/StrategyAnalytics'; interface props { - params: { - transactionId: string; - }; + params: { + transactionId: string; + }; } const Home = ({ params: { transactionId } }: props) => { - return ; + return ; }; export default Home; diff --git a/app/my-strategies/page.tsx b/app/my-strategies/page.tsx index 46cc896d..d3d13c18 100644 --- a/app/my-strategies/page.tsx +++ b/app/my-strategies/page.tsx @@ -42,7 +42,8 @@ export default function MyStrategies() { router.push( `${window.location.origin}/my-strategies/${order.addEvents[0].transaction.id}` ); - }}> + }} + > {order.network} {order.active ? ( diff --git a/components/ui/button.tsx b/components/ui/button.tsx index f8a9cbd1..7ed0f80c 100644 --- a/components/ui/button.tsx +++ b/components/ui/button.tsx @@ -1,59 +1,56 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { cva, type VariantProps } from "class-variance-authority"; +import * as React from 'react'; +import { Slot } from '@radix-ui/react-slot'; +import { cva, type VariantProps } from 'class-variance-authority'; -import { cn } from "@/lib/utils"; +import { cn } from '@/lib/utils'; const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-accent hover:text-accent-foreground", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", - lg: "h-11 rounded-md px-8", - icon: "h-10 w-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } + 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + default: 'bg-primary text-primary-foreground hover:bg-primary/90', + destructive: 'bg-destructive text-destructive-foreground hover:bg-destructive/90', + outline: 'border border-input bg-background hover:bg-accent hover:text-accent-foreground', + secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/80', + ghost: 'hover:bg-accent hover:text-accent-foreground', + link: 'text-primary underline-offset-4 hover:underline' + }, + size: { + default: 'h-10 px-4 py-2', + sm: 'h-9 rounded-md px-3', + lg: 'h-11 rounded-md px-8', + icon: 'h-10 w-10' + } + }, + defaultVariants: { + variant: 'default', + size: 'default' + } + } ); export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; } const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button"; - return ( - - ); - } + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button'; + return ( + + ); + } ); -Button.displayName = "Button"; +Button.displayName = 'Button'; export { Button, buttonVariants }; diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index e66c053c..affd2c9f 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -1,10 +1,10 @@ -"use client"; +'use client'; -import * as React from "react"; -import * as DialogPrimitive from "@radix-ui/react-dialog"; -import { X } from "lucide-react"; +import * as React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { X } from 'lucide-react'; -import { cn } from "@/lib/utils"; +import { cn } from '@/lib/utils'; const Dialog = DialogPrimitive.Root; @@ -15,108 +15,90 @@ const DialogPortal = DialogPrimitive.Portal; const DialogClose = DialogPrimitive.Close; const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, children, ...props }, ref) => ( - - - - {children} - {/* + + + + {children} + {/* Close */} - - + + )); DialogContent.displayName = DialogPrimitive.Content.displayName; -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
+const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => ( +
); -DialogHeader.displayName = "DialogHeader"; +DialogHeader.displayName = 'DialogHeader'; -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
+const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
); -DialogFooter.displayName = "DialogFooter"; +DialogFooter.displayName = 'DialogFooter'; const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); DialogTitle.displayName = DialogPrimitive.Title.displayName; const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - + )); DialogDescription.displayName = DialogPrimitive.Description.displayName; export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, + Dialog, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription }; diff --git a/components/ui/form.tsx b/components/ui/form.tsx index ce264aef..d83be0ec 100644 --- a/components/ui/form.tsx +++ b/components/ui/form.tsx @@ -1,178 +1,169 @@ -"use client" +'use client'; -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { Slot } from "@radix-ui/react-slot" +import * as React from 'react'; +import * as LabelPrimitive from '@radix-ui/react-label'; +import { Slot } from '@radix-ui/react-slot'; import { - Controller, - ControllerProps, - FieldPath, - FieldValues, - FormProvider, - useFormContext, -} from "react-hook-form" + Controller, + ControllerProps, + FieldPath, + FieldValues, + FormProvider, + useFormContext +} from 'react-hook-form'; -import { cn } from "@/lib/utils" -import { Label } from "@/components/ui/label" +import { cn } from '@/lib/utils'; +import { Label } from '@/components/ui/label'; -const Form = FormProvider +const Form = FormProvider; type FormFieldContextValue< - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath > = { - name: TName -} + name: TName; +}; -const FormFieldContext = React.createContext( - {} as FormFieldContextValue -) +const FormFieldContext = React.createContext({} as FormFieldContextValue); const FormField = < - TFieldValues extends FieldValues = FieldValues, - TName extends FieldPath = FieldPath + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath >({ - ...props + ...props }: ControllerProps) => { - return ( - - - - ) -} + return ( + + + + ); +}; const useFormField = () => { - const fieldContext = React.useContext(FormFieldContext) - const itemContext = React.useContext(FormItemContext) - const { getFieldState, formState } = useFormContext() + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); - const fieldState = getFieldState(fieldContext.name, formState) + const fieldState = getFieldState(fieldContext.name, formState); - if (!fieldContext) { - throw new Error("useFormField should be used within ") - } + if (!fieldContext) { + throw new Error('useFormField should be used within '); + } - const { id } = itemContext + const { id } = itemContext; - return { - id, - name: fieldContext.name, - formItemId: `${id}-form-item`, - formDescriptionId: `${id}-form-item-description`, - formMessageId: `${id}-form-item-message`, - ...fieldState, - } -} + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState + }; +}; type FormItemContextValue = { - id: string -} + id: string; +}; -const FormItemContext = React.createContext( - {} as FormItemContextValue -) +const FormItemContext = React.createContext({} as FormItemContextValue); -const FormItem = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => { - const id = React.useId() +const FormItem = React.forwardRef>( + ({ className, ...props }, ref) => { + const id = React.useId(); - return ( - -
- - ) -}) -FormItem.displayName = "FormItem" + return ( + +
+ + ); + } +); +FormItem.displayName = 'FormItem'; const FormLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef + React.ElementRef, + React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => { - const { error, formItemId } = useFormField() - - return ( -