diff --git a/packages/dappkit b/packages/dappkit index 49cb1b4e..cc2714c1 160000 --- a/packages/dappkit +++ b/packages/dappkit @@ -1 +1 @@ -Subproject commit 49cb1b4e4696c5688d18a287728ea0a8fc6b4811 +Subproject commit cc2714c1df79c1c06eafbddace823a2fbba84cd5 diff --git a/src/api/services/chain.service.ts b/src/api/services/chain.service.ts index b5b6f5f8..ab4b2450 100644 --- a/src/api/services/chain.service.ts +++ b/src/api/services/chain.service.ts @@ -1,12 +1,13 @@ import type { Chain } from "@merkl/api"; import { api } from "../index.server"; +import { fetchWithLogs } from "../utils"; export abstract class ChainService { - static async #fetch( + static async #fetch( call: () => Promise, resource = "Chain", ): Promise> { - const { data, status } = await call(); + const { data, status } = await fetchWithLogs(call); if (status === 404) throw new Response(`${resource} not found`, { status }); if (status === 500) throw new Response(`${resource} unavailable`, { status }); @@ -14,7 +15,7 @@ export abstract class ChainService { return data; } - static async getAll(): Promise { + static async getAll() { const chains = await ChainService.#fetch(async () => api.v4.chains.index.get({ query: {} })); //TODO: add some cache here diff --git a/src/api/services/opportunity.service.ts b/src/api/services/opportunity.service.ts index ae4ca252..6ef58452 100644 --- a/src/api/services/opportunity.service.ts +++ b/src/api/services/opportunity.service.ts @@ -1,13 +1,14 @@ import type { Opportunity } from "@merkl/api"; import config from "merkl.config"; import { api } from "../index.server"; +import { fetchWithLogs } from "../utils"; export abstract class OpportunityService { - static async #fetch( + static async #fetch( call: () => Promise, resource = "Opportunity", ): Promise> { - const { data, status } = await call(); + const { data, status } = await fetchWithLogs(call); if (status === 404) throw new Response(`${resource} not found`, { status }); if (status === 500) throw new Response(`${resource} unavailable`, { status }); diff --git a/src/api/services/protocol.service.ts b/src/api/services/protocol.service.ts index e3446108..8d405643 100644 --- a/src/api/services/protocol.service.ts +++ b/src/api/services/protocol.service.ts @@ -1,12 +1,13 @@ import type { Protocol } from "@merkl/api"; import { api } from "../index.server"; +import { fetchWithLogs } from "../utils"; export abstract class ProtocolService { - static async #fetch( + static async #fetch( call: () => Promise, resource = "Protocol", ): Promise> { - const { data, status } = await call(); + const { data, status } = await fetchWithLogs(call); if (status === 404) throw new Response(`${resource} not found`, { status }); if (status === 500) throw new Response(`${resource} unavailable`, { status }); diff --git a/src/api/services/reward.service.ts b/src/api/services/reward.service.ts index a3bdb278..8f58df16 100644 --- a/src/api/services/reward.service.ts +++ b/src/api/services/reward.service.ts @@ -1,12 +1,13 @@ import type { Reward } from "@merkl/api"; import { api } from "../index.server"; +import { fetchWithLogs } from "../utils"; export abstract class RewardService { - static async #fetch( + static async #fetch( call: () => Promise, resource = "Reward", ): Promise> { - const { data, status } = await call(); + const { data, status } = await fetchWithLogs(call); if (status === 404) throw new Response(`${resource} not found`, { status }); if (status === 500) throw new Response(`${resource} unavailable`, { status }); diff --git a/src/api/services/token.service.ts b/src/api/services/token.service.ts index 2f6767d7..b092303e 100644 --- a/src/api/services/token.service.ts +++ b/src/api/services/token.service.ts @@ -1,12 +1,13 @@ import type { Token } from "@merkl/api"; import { api } from "../index.server"; +import { fetchWithLogs } from "../utils"; export abstract class TokenService { - static async #fetch( + static async #fetch( call: () => Promise, resource = "Token", ): Promise> { - const { data, status } = await call(); + const { data, status } = await fetchWithLogs(call); if (status === 404) throw new Response(`${resource} not found`, { status }); if (status === 500) throw new Response(`${resource} unavailable`, { status }); diff --git a/src/api/utils.ts b/src/api/utils.ts new file mode 100644 index 00000000..44e02cff --- /dev/null +++ b/src/api/utils.ts @@ -0,0 +1,34 @@ +import chalk from "chalk"; + +function logStatus(status: number) { + if (status === 200) return chalk.green(`[${status}]`); + if (status < 500) return chalk.yellow(`[${status}]`); + return chalk.red(`[${status}]`); +} + +function logPerformance(ms: number) { + if (ms < 100) return chalk.green(`[${Math.round(ms)}ms]`); + if (ms < 500) return chalk.yellow(`[${Math.round(ms)}ms]`); + return chalk.red(`[${Math.round(ms)}ms]`); +} + +function logSize(bytes: number) { + const kb = Math.round(bytes / 1000); + + return `[${kb}kb]`; +} + +export async function fetchWithLogs( + call: () => Promise, +) { + const start = performance.now(); + const response = await call(); + const end = performance.now() - start; + + process.env.NODE_ENV === "development" && + console.log( + `${logStatus(response.status)}${logPerformance(end)}${logSize(+(response.response.headers.get("content-length") ?? 0))}: ${response.response.url}`, + ); + + return response; +} diff --git a/src/components/element/Tag.tsx b/src/components/element/Tag.tsx index 8a3da624..c78ac718 100644 --- a/src/components/element/Tag.tsx +++ b/src/components/element/Tag.tsx @@ -1,7 +1,8 @@ -import type { Opportunity, Token } from "@angleprotocol/merkl-api"; +import type { Opportunity, Token } from "@merkl/api"; import type { Chain } from "@merkl/api"; import { Button, Divider, Dropdown, Group, Hash, Icon, PrimitiveTag, Text } from "dappkit"; import type { ButtonProps } from "dappkit"; +import { useWalletContext } from "packages/dappkit/src/context/Wallet.context"; import { type Action, actions } from "src/config/actions"; import type { Protocol } from "src/config/protocols"; import { statuses } from "src/config/status"; @@ -25,6 +26,8 @@ export type TagProps = ButtonProps & { }; export default function Tag({ type, value, ...props }: TagProps) { + const { chains } = useWalletContext(); + switch (type) { case "status": { const status = statuses[value as TagTypes["status"]] ?? statuses.LIVE; @@ -153,10 +156,21 @@ export default function Tag({ type, value, ...props }: {token?.symbol} on Merkl - + {chains + .find(c => c.id === token.chainId) + ?.explorers?.map(explorer => { + return ( + + ); + })} }> diff --git a/src/routes/_merkl.(home).tsx b/src/routes/_merkl.(home).tsx index 9ed43004..07c91a44 100644 --- a/src/routes/_merkl.(home).tsx +++ b/src/routes/_merkl.(home).tsx @@ -1,35 +1,11 @@ import type { MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import Hero from "src/components/composite/Hero"; -import { http, createConfig } from "wagmi"; -import { mainnet, sepolia } from "wagmi/chains"; -import { coinbaseWallet, walletConnect } from "wagmi/connectors"; export const meta: MetaFunction = () => { - return [{ title: "New Remix App" }, { name: "description", content: "Welcome to Remix!" }]; + return [{ title: "Merkl" }]; }; -export const config = createConfig({ - chains: [mainnet, sepolia], - connectors: [ - coinbaseWallet(), - walletConnect({ - customStoragePrefix: "wagmi", - projectId: "26c912aadd2132cd869a5edc00aeea0f", - metadata: { - name: "Example", - description: "Example website", - url: "https://example.com", - icons: [], - }, - }), - ], - transports: { - [mainnet.id]: http(), - [sepolia.id]: http(), - }, -}); - export default function Index() { return ( = ({ data }) => { + return [{ title: `${data?.chain?.name} on Merkl` }]; +}; + export default function Index() { const { chain } = useLoaderData(); const label = chain.name.toLowerCase(); diff --git a/src/routes/_merkl.chains.(all).tsx b/src/routes/_merkl.chains.(all).tsx index c7e8b85c..9079df71 100644 --- a/src/routes/_merkl.chains.(all).tsx +++ b/src/routes/_merkl.chains.(all).tsx @@ -1,6 +1,11 @@ +import type { MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import Hero from "src/components/composite/Hero"; +export const meta: MetaFunction = () => { + return [{ title: "Chains on Merkl" }]; +}; + export default function Index() { return ( = ({ data, error }) => { if (error) return [{ title: error }]; - return [{ title: `${data?.name} on Merkl` }]; + return [{ title: `${data?.opportunity.name} on Merkl` }]; }; export type OutletContextOpportunity = { diff --git a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx b/src/routes/_merkl.opportunity.$chain.$type.$id.tsx deleted file mode 100644 index 67578d3b..00000000 --- a/src/routes/_merkl.opportunity.$chain.$type.$id.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import type { Opportunity } from "@angleprotocol/merkl-api"; -import { type LoaderFunctionArgs, type MetaFunction, json } from "@remix-run/node"; -import { Meta, Outlet, useLoaderData } from "@remix-run/react"; -import { useMemo } from "react"; -import { ChainService } from "src/api/services/chain.service"; -import { OpportunityService } from "src/api/services/opportunity.service"; -import Hero from "src/components/composite/Hero"; -import Tag from "src/components/element/Tag"; -import { ErrorHeading } from "src/components/layout/ErrorHeading"; -import useOpportunity from "src/hooks/resources/useOpportunity"; - -export async function loader({ params: { id, type, chain: chainId } }: LoaderFunctionArgs) { - if (!chainId || !id || !type) throw ""; - - const chain = await ChainService.get({ search: chainId }); - - const opportunity = await OpportunityService.getCampaignsByParams({ - chainId: chain.id, - type: type, - identifier: id, - }); - - return json({ opportunity }); -} - -export const meta: MetaFunction = ({ data, error }) => { - if (error) return [{ title: error }]; - return [{ title: `${data?.name} on Merkl` }]; -}; - -export type OutletContextOpportunity = { - opportunity: Opportunity; -}; - -export default function Index() { - const { opportunity } = useLoaderData(); - const { tags, description, link } = useOpportunity(opportunity); - - const styleName = useMemo(() => { - const spaced = opportunity?.name.split(" "); - - return spaced - .map(str => { - const key = str + crypto.randomUUID(); - if (!str.match(/[\p{Letter}\p{Mark}]+/gu)) - return [ - - {str} - , - ]; - if (str.includes("-")) - return str - .split("-") - .flatMap((s, i, arr) => [s, i !== arr.length - 1 && -]); - if (str.includes("/")) - return str - .split("/") - .flatMap((s, i, arr) => [s, i !== arr.length - 1 && /]); - return [{str}]; - }) - .flatMap((str, index, arr) => [str, index !== arr.length - 1 && " "]); - }, [opportunity]); - - return ( - <> - - ({ src: t.icon }))} - breadcrumbs={[ - { link: "/", name: "Opportunities" }, - { link: "/", name: opportunity.name }, - ]} - title={styleName} - description={description} - tabs={[ - { label: "Overview", link }, - { label: "Leaderboard", link: `${link}/leaderboard` }, - ]} - tags={tags.map(tag => )} - opportunity={opportunity}> - - - - ); -} - -export function ErrorBoundary() { - return ; -} diff --git a/src/routes/_merkl.protocols.$id.tsx b/src/routes/_merkl.protocols.$id.tsx index cc5b51a5..dc93af55 100644 --- a/src/routes/_merkl.protocols.$id.tsx +++ b/src/routes/_merkl.protocols.$id.tsx @@ -1,4 +1,4 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/node"; +import { type LoaderFunctionArgs, type MetaFunction, json } from "@remix-run/node"; import { Outlet, useLoaderData } from "@remix-run/react"; import { ProtocolService } from "src/api/services/protocol.service"; import Hero from "src/components/composite/Hero"; @@ -9,6 +9,12 @@ export async function loader({ params: { id } }: LoaderFunctionArgs) { return json({ protocol }); } +export const meta: MetaFunction = ({ data }) => { + if (!data?.protocol) return [{ title: "Merkl" }]; + + return [{ title: `${data?.protocol?.name} on Merkl` }]; +}; + export default function Index() { const { protocol } = useLoaderData(); diff --git a/src/routes/_merkl.protocols.(all).tsx b/src/routes/_merkl.protocols.(all).tsx index c4c38afb..11eb1406 100644 --- a/src/routes/_merkl.protocols.(all).tsx +++ b/src/routes/_merkl.protocols.(all).tsx @@ -1,6 +1,11 @@ +import type { MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import Hero from "src/components/composite/Hero"; +export const meta: MetaFunction = () => { + return [{ title: "Protocols on Merkl" }]; +}; + export default function Index() { return ( = ({ data }) => { + if (!data?.status) return [{ title: "Merkl" }]; + + const status = data.status.charAt(0).toUpperCase() + data.status.slice(1).toLowerCase(); + + return [{ title: `${status} opportunities on Merkl` }]; +}; + export default function Index() { const { status: _status } = useLoaderData(); const status = statuses[_status as Status]; diff --git a/src/routes/_merkl.tokens.$symbol.tsx b/src/routes/_merkl.tokens.$symbol.tsx index 130dded9..60c7f08d 100644 --- a/src/routes/_merkl.tokens.$symbol.tsx +++ b/src/routes/_merkl.tokens.$symbol.tsx @@ -1,4 +1,4 @@ -import { type LoaderFunctionArgs, json } from "@remix-run/node"; +import { type LoaderFunctionArgs, type MetaFunction, json } from "@remix-run/node"; import { Outlet, useLoaderData } from "@remix-run/react"; import { useMemo } from "react"; import { ChainService } from "src/api/services/chain.service"; @@ -15,6 +15,14 @@ export async function loader({ params: { symbol } }: LoaderFunctionArgs) { return json({ tokens, chains }); } +export const meta: MetaFunction = ({ data }) => { + const symbol = data?.tokens?.[0]?.symbol; + + if (!symbol) return [{ title: "Merkl" }]; + + return [{ title: `${symbol} on Merkl` }]; +}; + export default function Index() { const { tokens, chains } = useLoaderData(); const token = tokens?.[0]; @@ -41,8 +49,8 @@ export default function Index() { return ( t.icon && t.icon !== "")?.icon }]} navigation={{ label: "Back to opportunities", link: "/" }} @@ -55,7 +63,7 @@ export default function Index() { tabs={[ { label: "Opportunities", - link: `/token/${token.symbol?.toLowerCase()}`, + link: `/tokens/${token.symbol?.toLowerCase()}`, }, ]} tags={tags.map(tag => )}> diff --git a/src/routes/_merkl.tokens.(all).tsx b/src/routes/_merkl.tokens.(all).tsx index e12d9d27..08306789 100644 --- a/src/routes/_merkl.tokens.(all).tsx +++ b/src/routes/_merkl.tokens.(all).tsx @@ -1,6 +1,11 @@ +import type { MetaFunction } from "@remix-run/node"; import { Outlet } from "@remix-run/react"; import Hero from "src/components/composite/Hero"; +export const meta: MetaFunction = () => { + return [{ title: "Tokens on Merkl" }]; +}; + export default function Index() { return ( = ({ data, error }) => { + if (error) return [{ title: error }]; + return [{ title: `${data?.address?.substring(0, 6)}…${data?.address.substring(data?.address.length - 4)} on Merkl` }]; +}; export default function Index() { - const { address } = useParams(); + const { rewards, address } = useLoaderData(); const [_isEditingAddress] = useState(false); + const { earned, unclaimed } = useMemo(() => { + return rewards.reduce( + ({ earned, unclaimed }, chain) => { + const valueUnclaimed = chain.rewards.reduce((sum, token) => { + const value = + Number.parseFloat(formatUnits(token.amount - token.claimed, token.token.decimals)) * + (token.token.price ?? 0); + + return sum + value; + }, 0); + const valueEarned = chain.rewards.reduce((sum, token) => { + const value = Number.parseFloat(formatUnits(token.claimed, token.token.decimals)) * (token.token.price ?? 0); + + return sum + value; + }, 0); + + return { earned: earned + valueEarned, unclaimed: unclaimed + valueUnclaimed }; + }, + { earned: 0, unclaimed: 0 }, + ); + }, [rewards]); + return ( {/* TODO: Make it dynamic this */} - - $6k - + + {earned} + Total earned - - $1.2k - - - Earned today - - - - - $3k - + + {unclaimed} + Claimable