diff --git a/package.json b/package.json index 741a856..8af17a3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "create-turbo-eth", - "version": "0.6.0", + "version": "0.6.1", "description": "Create web3 apps in turbo mode.", "author": "Vitor @marthendalnunes", "license": "MIT", diff --git a/src/config/integrations.ts b/src/config/integrations.ts index ffc21c5..7bfdbab 100644 --- a/src/config/integrations.ts +++ b/src/config/integrations.ts @@ -1,6 +1,6 @@ -import type { Integrations } from '../types' import path from 'node:path' import { z } from 'zod' +import type { Integrations } from '../types' const exampleDemosPath = path.join('components', 'shared', 'example-demos.tsx') const dataConfigPath = path.join('data', 'turbo-integrations.ts') @@ -372,4 +372,19 @@ export const integrationOptions: Integrations = { }, ], }, + 'defi-llama': { + name: 'DefiLlama', + pageDependencies: [ + { + dependencyPath: dataConfigPath, + type: 'snippet', + regexList: [/\n\s*defiLlama: \{\s*name: "DefiLlama",[\s\S]*?imgDark: "\/integrations\/defi-llama\.png",\s*\},/g], + }, + { + dependencyPath: exampleDemosPath, + type: 'snippet', + regexList: [/\n\s*{\s*title: turboIntegrations\.defiLlama\.name,[\s\S]*?\/>\s*<\/div>\s*\),\s*},/g], + }, + ], + }, } diff --git a/src/types.ts b/src/types.ts index 78c75e5..3ead4de 100644 --- a/src/types.ts +++ b/src/types.ts @@ -78,6 +78,7 @@ export type AvailableIntegrations = | 'arweave' | 'gitcoin-passport' | 'lens-protocol' + | 'defi-llama' | 'starter' export type Integrations = Record diff --git a/template/base/app/twitter-image.png b/template/base/app/twitter-image.png new file mode 100644 index 0000000..62dda55 Binary files /dev/null and b/template/base/app/twitter-image.png differ diff --git a/template/base/components/shared/example-demos.tsx b/template/base/components/shared/example-demos.tsx index 2ea6bbf..1ae6b14 100644 --- a/template/base/components/shared/example-demos.tsx +++ b/template/base/components/shared/example-demos.tsx @@ -504,6 +504,22 @@ const demos = [ ), }, + { + title: turboIntegrations.defiLlama.name, + description: turboIntegrations.defiLlama.description, + href: turboIntegrations.defiLlama.href, + demo: ( +
+ +
+ ), + }, { title: turboIntegrations.starter.name, description: turboIntegrations.starter.description, diff --git a/template/base/data/turbo-integrations.ts b/template/base/data/turbo-integrations.ts index 3729cfd..2fd3d7c 100644 --- a/template/base/data/turbo-integrations.ts +++ b/template/base/data/turbo-integrations.ts @@ -14,7 +14,7 @@ interface TurboIntegration { category: (typeof integrationCategories)[number] } -export const turboIntegrations: Record = { +export const turboIntegrations = { siwe: { name: "SIWE", href: "/integration/sign-in-with-ethereum", @@ -199,6 +199,15 @@ export const turboIntegrations: Record = { imgLight: "/integrations/gitcoin-passport.svg", imgDark: "/integrations/gitcoin-passport.svg", }, + defiLlama: { + name: "DefiLlama", + href: "/integration/defi-llama", + url: "https://defillama.com/docs/api", + description: "Open and transparent DeFi analytics. ", + category: "services", + imgLight: "/integrations/defi-llama.png", + imgDark: "/integrations/defi-llama.png", + }, starter: { name: "Starter Template", href: "/integration/starter", @@ -209,4 +218,4 @@ export const turboIntegrations: Record = { imgLight: "/logo-gradient.png", imgDark: "/logo-gradient.png", }, -} +} as const diff --git a/template/base/public/integrations/defi-llama.png b/template/base/public/integrations/defi-llama.png new file mode 100644 index 0000000..8d1852b Binary files /dev/null and b/template/base/public/integrations/defi-llama.png differ diff --git a/template/integrations/defi-llama/core/defi-llama/README.md b/template/integrations/defi-llama/core/defi-llama/README.md new file mode 100644 index 0000000..6eda4b3 --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/README.md @@ -0,0 +1,71 @@ +# DefiLlama TurboETH Integration + +This integration allows you to fetch data from [DefiLlama](https://defillama.com/) and convert it to a readable format. + +## Features + +- Fetch DefiLlama data +- Convert DefiLlama input/output data to easily readable format + +## Components + +`FormChart`: Renders a form to input a token info and historical data chart + +`FormCurrentPrice`: Renders a form to input a token info and current price + +`FormHistoricalPrice`: Renders a form to input a token info and historical price given a timestamp + +`FormPercentageChange`: Renders a form to input a token info and percentage change given a timestamp and period of time + +`OutputData`: Renders a textArea with the output data + +## Hooks + +These hooks are react-query wrappers for the [DefiLlama API](https://defillama.com/docs/api). + +### Query hooks + +`useChart`: Fetches the token prices at regular time intervals. + +`useCurrentTokenPrice`: Fetches the current price of a token from DeFi Llama. + +`useCurrentNativeTokenPrice`: Wrapper around `useCurrentTokenPrice` that fetches the current price of the native token of the given chain. Defaults to the current chain. + +`useCurrentERC20TokenPrice`: Wrapper around `useCurrentTokenPrice` that fetches the current price of an ERC20 token for the given chain. Defaults to the current chain. + +`useCurrentTokenPrice`: Fetches the historical price of a token from DeFi Llama given a timestamp. + +`useHistoricalNativeTokenPrice`: Wrapper around `useHistoricalTokenPrice` that fetches the historical price of the native token for the given chain. Defaults to the current chain. + +`useHistoricalERC20TokenPrice`: Wrapper around `useHistoricalTokenPrice` that fetches the historical price of an ERC20 token for the given chain. Defaults to the current chain. + +`useTokenPercentageChange`: Fetches the percentage change in price of a token from DeFi Llama given a timestamp and period of time. + +`useNativeTokenPercentageChange`: Wrapper around `useTokenPercentageChange` that fetches the percentage change in price of the native token for the given chain. Defaults to the current chain. + +`useERC20TokenPercentageChange`: Wrapper around `useTokenPercentageChange` that fetches the percentage change in price of an ERC20 token for the given chain. Defaults to the current chain. + +## File Structure + +``` +integrations/defi-llama/ +├─ components/ +│ ├─ form-chart.tsx +│ ├─ form-current-price.tsx +│ ├─ form-historical-price.tsx +│ ├─ form-percentage-change.tsx +│ ├─ index.ts +│ ├─ output-data.tsx +├─ hooks/ +│ ├─ coins/ +│ │ ├─ index.ts +│ │ ├─ use-chart.ts +│ │ ├─ use-current-token-price.ts +│ │ ├─ use-historical-token-price.ts +│ │ ├─ use-token-percentage-change.ts +├─ utils/ +│ ├─ index.ts +│ ├─ types.ts +├─ index.ts +├─ README.md +``` diff --git a/template/integrations/defi-llama/core/defi-llama/components/form-chart.tsx b/template/integrations/defi-llama/core/defi-llama/components/form-chart.tsx new file mode 100644 index 0000000..1ec70c7 --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/components/form-chart.tsx @@ -0,0 +1,239 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" + +import { useChart } from "../hooks/coins" +import { calculatePeriod } from "../utils" +import { OutputData } from "./output-data" + +const formSchema = z + .object({ + chain: z.string({ + required_error: "Please select a chain", + }), + tokenType: z.union([z.literal("erc20"), z.literal("native")], { + required_error: "Please select a token type", + }), + span: z + .string() + .refine( + (val) => !isNaN(Number(val)) && Number(val) > 1, + "Please enter a valid number" + ), + tokenAddress: z.string().optional(), + timeInterval: z.union( + [z.literal("1d"), z.literal("5d"), z.literal("30d"), z.literal("365d")], + { + required_error: "Please select a time interval", + } + ), + }) + .superRefine((values, ctx) => { + if (values.tokenType === "native") return true + if (values.tokenAddress && /^0x[a-fA-F0-9]{40}$/.test(values.tokenAddress)) + return true + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Please enter a valid address", + path: ["tokenAddress"], + }) + }) + +export function FormChart() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + span: "10", + }, + }) + + const { data, isFetching, error, refetch } = useChart({ + coins: { + address: form.watch("tokenAddress"), + chainId: Number(form.watch("chain")), + type: form.watch("tokenType") as any, + }, + timestamp: { + type: "end", + value: Math.floor(Date.now() / 1000), + }, + searchWidth: "1h", + period: calculatePeriod( + form.watch("timeInterval"), + Number(form.watch("span")) + ), + spanDataPoints: isNaN(Number(form.watch("span"))) + ? 0 + : Number(form.watch("span")), + enabled: false, + }) + + async function onSubmit() { + await refetch?.() + } + + return ( + + + <> +
+ + ( + + Chain + + + + + + )} + /> + ( + + Token Type + + + + + + )} + /> + {form.watch("tokenType") === "erc20" && ( + ( + + Address + + + + + + )} + /> + )} + ( + + Time Interval + + + + + + )} + /> + ( + + Data points + + + + + + )} + /> + + + + + {error && {String(error)}} + +
+ + +

Chart

+

+ Get token prices at regular time intervals +

+
+
+ ) +} diff --git a/template/integrations/defi-llama/core/defi-llama/components/form-current-price.tsx b/template/integrations/defi-llama/core/defi-llama/components/form-current-price.tsx new file mode 100644 index 0000000..d81a11e --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/components/form-current-price.tsx @@ -0,0 +1,171 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" + +import { useCurrentTokenPrice } from "../hooks/coins" +import { OutputData } from "./output-data" + +const formSchema = z + .object({ + chain: z.string({ + required_error: "Please select a chain", + }), + tokenType: z.union([z.literal("erc20"), z.literal("native")], { + required_error: "Please select a token type", + }), + tokenAddress: z.string().optional(), + }) + .superRefine((values, ctx) => { + if (values.tokenType === "native") return true + if (values.tokenAddress && /^0x[a-fA-F0-9]{40}$/.test(values.tokenAddress)) + return true + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Please enter a valid address", + path: ["tokenAddress"], + }) + }) + +export function FormCurrentPrice() { + const form = useForm>({ + resolver: zodResolver(formSchema), + }) + + const { data, isFetching, error, refetch } = useCurrentTokenPrice({ + coins: { + address: form.watch("tokenAddress"), + chainId: Number(form.watch("chain")), + type: form.watch("tokenType") as any, + }, + enabled: false, + }) + + async function onSubmit() { + await refetch?.() + } + + return ( + + + <> +
+ + ( + + Chain + + + + + + )} + /> + ( + + Token Type + + + + + + )} + /> + {form.watch("tokenType") === "erc20" && ( + ( + + Address + + + + + + )} + /> + )} + + + + + + {error && {String(error)}} + +
+ + +

Current Price

+

+ Get the current price of tokens by contract address. +

+
+
+ ) +} diff --git a/template/integrations/defi-llama/core/defi-llama/components/form-historical-price.tsx b/template/integrations/defi-llama/core/defi-llama/components/form-historical-price.tsx new file mode 100644 index 0000000..1eec5ba --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/components/form-historical-price.tsx @@ -0,0 +1,194 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" + +import { useHistoricalTokenPrice } from "../hooks/coins" +import { OutputData } from "./output-data" + +const formSchema = z + .object({ + chain: z.string({ + required_error: "Please select a chain", + }), + tokenType: z.union([z.literal("erc20"), z.literal("native")], { + required_error: "Please select a token type", + }), + tokenAddress: z.string().optional(), + timestamp: z + .string() + .refine( + (val) => !isNaN(Number(val)) && Number(val) > 0, + "Please enter a valid number" + ), + }) + .superRefine((values, ctx) => { + if (values.tokenType === "native") return true + if (values.tokenAddress && /^0x[a-fA-F0-9]{40}$/.test(values.tokenAddress)) + return true + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Please enter a valid address", + path: ["tokenAddress"], + }) + }) + +export function FormHistoricalPrice() { + const form = useForm>({ + resolver: zodResolver(formSchema), + }) + + const { data, isFetching, error, refetch } = useHistoricalTokenPrice({ + coins: { + address: form.watch("tokenAddress"), + chainId: Number(form.watch("chain")), + type: form.watch("tokenType") as any, + }, + timestamp: Number(form.watch("timestamp")), + enabled: false, + }) + + async function onSubmit() { + await refetch?.() + } + + return ( + + + <> +
+ + ( + + Chain + + + + + + )} + /> + ( + + Token Type + + + + + + )} + /> + {form.watch("tokenType") === "erc20" && ( + ( + + Address + + + + + + )} + /> + )} + ( + + Timestamp + + + + + + )} + /> + + + + + {error && {String(error)}} + +
+ + +

Historical Price

+

+ Get the historical price of tokens at a timestamp by contract address. +

+
+
+ ) +} diff --git a/template/integrations/defi-llama/core/defi-llama/components/form-percentage-change.tsx b/template/integrations/defi-llama/core/defi-llama/components/form-percentage-change.tsx new file mode 100644 index 0000000..276a857 --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/components/form-percentage-change.tsx @@ -0,0 +1,231 @@ +"use client" + +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" + +import { Button } from "@/components/ui/button" +import { Card, CardContent, CardFooter } from "@/components/ui/card" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectGroup, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { Separator } from "@/components/ui/separator" + +import { useTokenPercentageChange } from "../hooks/coins" +import { OutputData } from "./output-data" + +const formSchema = z + .object({ + chain: z.string({ + required_error: "Please select a chain", + }), + tokenType: z.union([z.literal("erc20"), z.literal("native")], { + required_error: "Please select a token type", + }), + tokenAddress: z.string().optional(), + timestamp: z + .string() + .refine( + (val) => !isNaN(Number(val)) && Number(val) > 0, + "Please enter a valid number" + ), + period: z.union( + [z.literal("1d"), z.literal("5d"), z.literal("30d"), z.literal("365d")], + { + required_error: "Please select a time interval", + } + ), + }) + .superRefine((values, ctx) => { + if (values.tokenType === "native") return true + if (values.tokenAddress && /^0x[a-fA-F0-9]{40}$/.test(values.tokenAddress)) + return true + + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Please enter a valid address", + path: ["tokenAddress"], + }) + }) + +export function FormPercentageChange() { + const form = useForm>({ + resolver: zodResolver(formSchema), + }) + + const { data, isFetching, error, refetch } = useTokenPercentageChange({ + coins: { + address: form.watch("tokenAddress"), + chainId: Number(form.watch("chain")), + type: form.watch("tokenType") as any, + }, + period: form.watch("period"), + timestamp: Number(form.watch("timestamp")), + enabled: false, + }) + + async function onSubmit() { + console.log("onSubmit") + await refetch?.() + } + + return ( + + + <> +
+ + ( + + Chain + + + + + + )} + /> + ( + + Token Type + + + + + + )} + /> + {form.watch("tokenType") === "erc20" && ( + ( + + Address + + + + + + )} + /> + )} + ( + + Timestamp + + + + + + )} + /> + ( + + Time Interval + + + + + + )} + /> + + + + + {error && {String(error)}} + +
+ + +

Percentage Change

+

+ Get the percentage change in price of tokens at a timestamp by + contract address. +

+
+
+ ) +} diff --git a/template/integrations/defi-llama/core/defi-llama/components/index.ts b/template/integrations/defi-llama/core/defi-llama/components/index.ts new file mode 100644 index 0000000..5a2b21e --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/components/index.ts @@ -0,0 +1,4 @@ +export * from "./form-chart" +export * from "./form-current-price" +export * from "./form-historical-price" +export * from "./form-percentage-change" diff --git a/template/integrations/defi-llama/core/defi-llama/components/output-data.tsx b/template/integrations/defi-llama/core/defi-llama/components/output-data.tsx new file mode 100644 index 0000000..0a6e054 --- /dev/null +++ b/template/integrations/defi-llama/core/defi-llama/components/output-data.tsx @@ -0,0 +1,47 @@ +import { HTMLAttributes } from "react" +import CopyToClipboard from "react-copy-to-clipboard" +import { FaCopy } from "react-icons/fa" + +import { useToast } from "@/lib/hooks/use-toast" +import { Textarea } from "@/components/ui/textarea" + +interface OutputDataProps extends HTMLAttributes { + data?: any +} + +export function OutputData({ data, ...props }: OutputDataProps) { + const { toast, dismiss } = useToast() + + const handleToast = () => { + toast({ + title: "Data Copied", + description: "The output has been copied to your clipboard.", + }) + + setTimeout(() => { + dismiss() + }, 4200) + } + if (!data) return null + + return ( +
+
+

Output

+ + + + + +
+