diff --git a/Cargo.lock b/Cargo.lock index 2729ed8ae..fb6861852 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1067,8 +1067,8 @@ dependencies = [ [[package]] name = "fixed-point" -version = "0.15.7" -source = "git+https://github.com/delvtech/hyperdrive-rs.git?rev=v0.15.7#a39a260f3d5957d27e65eabda6fc8396435cc403" +version = "0.16.0" +source = "git+https://github.com/delvtech/hyperdrive-rs.git?rev=v0.16.0#705a65f2d0c0669042481aa63f000da3fab41791" dependencies = [ "ethers", "eyre", @@ -1443,8 +1443,8 @@ dependencies = [ [[package]] name = "hyperdrive-math" -version = "0.15.7" -source = "git+https://github.com/delvtech/hyperdrive-rs.git?rev=v0.15.7#a39a260f3d5957d27e65eabda6fc8396435cc403" +version = "0.16.0" +source = "git+https://github.com/delvtech/hyperdrive-rs.git?rev=v0.16.0#705a65f2d0c0669042481aa63f000da3fab41791" dependencies = [ "ethers", "eyre", @@ -1477,8 +1477,8 @@ dependencies = [ [[package]] name = "hyperdrive-wrappers" -version = "0.15.7" -source = "git+https://github.com/delvtech/hyperdrive-rs.git?rev=v0.15.7#a39a260f3d5957d27e65eabda6fc8396435cc403" +version = "0.16.0" +source = "git+https://github.com/delvtech/hyperdrive-rs.git?rev=v0.16.0#705a65f2d0c0669042481aa63f000da3fab41791" dependencies = [ "dotenv", "ethers", diff --git a/apps/hyperdrive-trading/.env.sample b/apps/hyperdrive-trading/.env.sample index 36d0c846a..7e6ef6d13 100644 --- a/apps/hyperdrive-trading/.env.sample +++ b/apps/hyperdrive-trading/.env.sample @@ -14,8 +14,11 @@ VITE_CUSTOM_CHAIN_CHAIN_ID= VITE_CAPSULE_API_KEY= VITE_CAPSULE_ENV= # -# Sepolia +# Sepolia, or VITE_SEPOLIA_RPC_URL= +# +# Mainnet +VITE_MAINNET_RPC_URL= ################################################## # Used to screen ineligible addresses. diff --git a/apps/hyperdrive-trading/README.md b/apps/hyperdrive-trading/README.md index 580bf6f2e..78a394553 100644 --- a/apps/hyperdrive-trading/README.md +++ b/apps/hyperdrive-trading/README.md @@ -48,6 +48,8 @@ cd apps/hyperdrive-trading ### Run locally +First, copy the .env.sample to .env and you'll need to set the VITE_SEPOLIA_RPC_URL variable to an active rpc url. + Serve with hot reload at . ```bash diff --git a/apps/hyperdrive-trading/package.json b/apps/hyperdrive-trading/package.json index 4d4ea6e9b..2126a73bb 100644 --- a/apps/hyperdrive-trading/package.json +++ b/apps/hyperdrive-trading/package.json @@ -26,23 +26,25 @@ "gen:walletconnect": "bash ./scripts/generate-walletconnect.sh" }, "dependencies": { + "@delvtech/gopher": "*", + "@delvtech/hyperdrive-viem": "^2.3.0", "@heroicons/react": "^2.0.16", "@hyperdrive/appconfig": "*", - "@delvtech/hyperdrive-viem": "^2.3.0", "@rainbow-me/rainbowkit": "^2.0.0", + "@rollbar/react": "^0.11.1", "@tanstack/react-query": "^4.29.12", "@tanstack/react-router": "^1.9.0", "@tanstack/react-table": "^8.10.3", - "@types/lodash.sortby": "^4.7.9", - "lodash.sortby": "^4.7.0", "@types/d3-format": "^3.0.4", + "@types/lodash.sortby": "^4.7.9", "assert-never": "^1.2.1", "buffer": "^6.0.3", "calendar-link": "^2.6.0", "classnames": "^2.3.2", "d3-format": "^3.1.0", - "daisyui": "^4.11.1", + "daisyui": "^4.12.2", "dnum": "^2.9.0", + "lodash.sortby": "^4.7.0", "process": "^0.11.10", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -50,38 +52,38 @@ "react-hot-toast": "^2.4.0", "react-loading-skeleton": "^3.3.1", "react-use": "^17.4.2", - "viem": "^2.7.8", - "@rollbar/react": "^0.11.1", "rollbar": "^2.26.4", + "viem": "^2.7.8", "wagmi": "^2.5.12" }, "devDependencies": { "@hyperdrive/eslint-config": "*", "@hyperdrive/prettier-config": "*", "@hyperdrive/tsconfig": "*", + "@tanstack/eslint-plugin-query": "^5.28.11", "@tanstack/router-devtools": "^1.9.0", "@tanstack/router-vite-plugin": "^1.8.2", - "@tanstack/eslint-plugin-query": "^5.28.11", "@types/react": "^18.0.9", "@types/react-dom": "^18.0.10", "@types/react-helmet": "^6.1.6", "@typescript-eslint/eslint-plugin": "^7.5.0", "@typescript-eslint/parser": "^7.5.0", + "@usecapsule/rainbowkit-wallet": "^0.8.5", "@vitejs/plugin-react": "^3.1.0", "autoprefixer": "^10.4.13", "eslint-plugin-react": "^7.34.1", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-tailwindcss": "^3.15.1", + "msw": "^2.3.1", "postcss": "^8.4.21", "prettier": "3.2.5", "prettier-plugin-organize-imports": "3.2.4", "prettier-plugin-tailwindcss": "^0.5.13", - "vite-plugin-node-polyfills": "^0.21.0", - "@usecapsule/rainbowkit-wallet": "^0.8.5", "tailwindcss": "^3.4.3", "tailwindcss-debug-screens": "2.2.1", "typescript": "^5.0.2", "vite": "^4.1.0", + "vite-plugin-node-polyfills": "^0.21.0", "vite-tsconfig-paths": "^4.0.5", "vitest": "^0.28.5" } diff --git a/apps/hyperdrive-trading/src/base/formatRate.ts b/apps/hyperdrive-trading/src/base/formatRate.ts index e5c617454..6dd46b87a 100644 --- a/apps/hyperdrive-trading/src/base/formatRate.ts +++ b/apps/hyperdrive-trading/src/base/formatRate.ts @@ -6,5 +6,8 @@ export function formatRate(rate: bigint, decimals = 18): string { // the decimal, and format those to a percent, eg: 0.0499999999 * 100 = 4.99. const truncatedAPR = +formatUnits(rate, decimals).slice(0, 10); const formatted = `${Number((100 * truncatedAPR).toFixed(2)).toLocaleString()}`; + if (formatted.indexOf(".") === -1) { + return `${formatted}.00`; + } return formatted; } diff --git a/apps/hyperdrive-trading/src/network/wagmiClient.ts b/apps/hyperdrive-trading/src/network/wagmiClient.ts index f6341507d..119ae7ed3 100644 --- a/apps/hyperdrive-trading/src/network/wagmiClient.ts +++ b/apps/hyperdrive-trading/src/network/wagmiClient.ts @@ -12,7 +12,7 @@ import { cloudChain } from "src/chains/cloudChain"; import { CreateWalletFn } from "src/wallets/CreateWalletFn"; import { capsuleWallet } from "src/wallets/capsule"; import { Transport } from "viem"; -import { foundry, sepolia } from "wagmi/chains"; +import { foundry, mainnet, sepolia } from "wagmi/chains"; const { VITE_LOCALHOST_NODE_RPC_URL, @@ -21,6 +21,7 @@ const { VITE_CUSTOM_CHAIN_CHAIN_ID, VITE_WALLET_CONNECT_PROJECT_ID, VITE_SEPOLIA_RPC_URL, + VITE_MAINNET_RPC_URL, } = import.meta.env; export const chains: Chain[] = []; @@ -62,6 +63,17 @@ if (VITE_SEPOLIA_RPC_URL) { } } +if (VITE_MAINNET_RPC_URL) { + chains.push(mainnet); + transports[mainnet.id] = http(VITE_MAINNET_RPC_URL); + + // TODO: push this into the custom wallets for local and cloudchain once + // capsule support is verified + if (capsuleWallet) { + customWallets.push(capsuleWallet); + } +} + export const wagmiConfig = getDefaultConfig({ appName: "Hyperdrive", projectId: VITE_WALLET_CONNECT_PROJECT_ID || "0", diff --git a/apps/hyperdrive-trading/src/public/delorean.svg b/apps/hyperdrive-trading/src/public/delorean.svg new file mode 100644 index 000000000..6554aa284 --- /dev/null +++ b/apps/hyperdrive-trading/src/public/delorean.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/hyperdrive-trading/src/public/dot-matrix.svg b/apps/hyperdrive-trading/src/public/dot-matrix.svg new file mode 100644 index 000000000..1df357408 --- /dev/null +++ b/apps/hyperdrive-trading/src/public/dot-matrix.svg @@ -0,0 +1,1290 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/hyperdrive-trading/src/public/mockServiceWorker.js b/apps/hyperdrive-trading/src/public/mockServiceWorker.js new file mode 100644 index 000000000..d0a7ac4db --- /dev/null +++ b/apps/hyperdrive-trading/src/public/mockServiceWorker.js @@ -0,0 +1,284 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + * - Please do NOT serve this file on production. + */ + +const PACKAGE_VERSION = "2.3.1"; +const INTEGRITY_CHECKSUM = "26357c79639bfa20d64c0efca2a87423"; +const IS_MOCKED_RESPONSE = Symbol("isMockedResponse"); +const activeClientIds = new Set(); + +self.addEventListener("install", function () { + self.skipWaiting(); +}); + +self.addEventListener("activate", function (event) { + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("message", async function (event) { + const clientId = event.source.id; + + if (!clientId || !self.clients) { + return; + } + + const client = await self.clients.get(clientId); + + if (!client) { + return; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + switch (event.data) { + case "KEEPALIVE_REQUEST": { + sendToClient(client, { + type: "KEEPALIVE_RESPONSE", + }); + break; + } + + case "INTEGRITY_CHECK_REQUEST": { + sendToClient(client, { + type: "INTEGRITY_CHECK_RESPONSE", + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }); + break; + } + + case "MOCK_ACTIVATE": { + activeClientIds.add(clientId); + + sendToClient(client, { + type: "MOCKING_ENABLED", + payload: true, + }); + break; + } + + case "MOCK_DEACTIVATE": { + activeClientIds.delete(clientId); + break; + } + + case "CLIENT_CLOSED": { + activeClientIds.delete(clientId); + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId; + }); + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister(); + } + + break; + } + } +}); + +self.addEventListener("fetch", function (event) { + const { request } = event; + + // Bypass navigation requests. + if (request.mode === "navigate") { + return; + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if (request.cache === "only-if-cached" && request.mode !== "same-origin") { + return; + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been deleted (still remains active until the next reload). + if (activeClientIds.size === 0) { + return; + } + + // Generate unique request ID. + const requestId = crypto.randomUUID(); + event.respondWith(handleRequest(event, requestId)); +}); + +async function handleRequest(event, requestId) { + const client = await resolveMainClient(event); + const response = await getResponse(event, client, requestId); + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + (async function () { + const responseClone = response.clone(); + + sendToClient( + client, + { + type: "RESPONSE", + payload: { + requestId, + isMockedResponse: IS_MOCKED_RESPONSE in response, + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + body: responseClone.body, + headers: Object.fromEntries(responseClone.headers.entries()), + }, + }, + [responseClone.body], + ); + })(); + } + + return response; +} + +// Resolve the main client for the given event. +// Client that issues a request doesn't necessarily equal the client +// that registered the worker. It's with the latter the worker should +// communicate with during the response resolving phase. +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId); + + if (client?.frameType === "top-level") { + return client; + } + + const allClients = await self.clients.matchAll({ + type: "window", + }); + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === "visible"; + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id); + }); +} + +async function getResponse(event, client, requestId) { + const { request } = event; + + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = request.clone(); + + function passthrough() { + const headers = Object.fromEntries(requestClone.headers.entries()); + + // Remove internal MSW request header so the passthrough request + // complies with any potential CORS preflight checks on the server. + // Some servers forbid unknown request headers. + delete headers["x-msw-intention"]; + + return fetch(requestClone, { headers }); + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough(); + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough(); + } + + // Notify the client that a request has been intercepted. + const requestBuffer = await request.arrayBuffer(); + const clientMessage = await sendToClient( + client, + { + type: "REQUEST", + payload: { + id: requestId, + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: requestBuffer, + keepalive: request.keepalive, + }, + }, + [requestBuffer], + ); + + switch (clientMessage.type) { + case "MOCK_RESPONSE": { + return respondWithMock(clientMessage.data); + } + + case "PASSTHROUGH": { + return passthrough(); + } + } + + return passthrough(); +} + +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel(); + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error); + } + + resolve(event.data); + }; + + client.postMessage( + message, + [channel.port2].concat(transferrables.filter(Boolean)), + ); + }); +} + +async function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error(); + } + + const mockedResponse = new Response(response.body, response); + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }); + + return mockedResponse; +} diff --git a/apps/hyperdrive-trading/src/public/sine-wave.svg b/apps/hyperdrive-trading/src/public/sine-wave.svg new file mode 100644 index 000000000..4b0e8378e --- /dev/null +++ b/apps/hyperdrive-trading/src/public/sine-wave.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/hyperdrive-trading/src/routeTree.gen.ts b/apps/hyperdrive-trading/src/routeTree.gen.ts index 8998148e4..acb6360b2 100644 --- a/apps/hyperdrive-trading/src/routeTree.gen.ts +++ b/apps/hyperdrive-trading/src/routeTree.gen.ts @@ -10,54 +10,84 @@ // Import Routes -import { Route as rootRoute } from "./ui/routes/__root"; -import { Route as IndexImport } from "./ui/routes/index"; -import { Route as MarketAddressImport } from "./ui/routes/market.$address"; -import { Route as MarketsImport } from "./ui/routes/markets"; -import { Route as VoidImport } from "./ui/routes/void"; +import { Route as rootRoute } from './ui/routes/__root' +import { Route as VpnImport } from './ui/routes/vpn' +import { Route as VoidImport } from './ui/routes/void' +import { Route as RestrictedcountriesImport } from './ui/routes/restricted_countries' +import { Route as MarketsImport } from './ui/routes/markets' +import { Route as BridgeImport } from './ui/routes/bridge' +import { Route as IndexImport } from './ui/routes/index' +import { Route as MarketAddressImport } from './ui/routes/market.$address' // Create/Update Routes +const VpnRoute = VpnImport.update({ + path: '/vpn', + getParentRoute: () => rootRoute, +} as any) + const VoidRoute = VoidImport.update({ - path: "/void", + path: '/void', + getParentRoute: () => rootRoute, +} as any) + +const RestrictedcountriesRoute = RestrictedcountriesImport.update({ + path: '/restricted_countries', getParentRoute: () => rootRoute, -} as any); +} as any) const MarketsRoute = MarketsImport.update({ - path: "/markets", + path: '/markets', + getParentRoute: () => rootRoute, +} as any) + +const BridgeRoute = BridgeImport.update({ + path: '/bridge', getParentRoute: () => rootRoute, -} as any); +} as any) const IndexRoute = IndexImport.update({ - path: "/", + path: '/', getParentRoute: () => rootRoute, -} as any); +} as any) const MarketAddressRoute = MarketAddressImport.update({ - path: "/market/$address", + path: '/market/$address', getParentRoute: () => rootRoute, -} as any); +} as any) // Populate the FileRoutesByPath interface -declare module "@tanstack/react-router" { +declare module '@tanstack/react-router' { interface FileRoutesByPath { - "/": { - preLoaderRoute: typeof IndexImport; - parentRoute: typeof rootRoute; - }; - "/markets": { - preLoaderRoute: typeof MarketsImport; - parentRoute: typeof rootRoute; - }; - "/void": { - preLoaderRoute: typeof VoidImport; - parentRoute: typeof rootRoute; - }; - "/market/$address": { - preLoaderRoute: typeof MarketAddressImport; - parentRoute: typeof rootRoute; - }; + '/': { + preLoaderRoute: typeof IndexImport + parentRoute: typeof rootRoute + } + '/bridge': { + preLoaderRoute: typeof BridgeImport + parentRoute: typeof rootRoute + } + '/markets': { + preLoaderRoute: typeof MarketsImport + parentRoute: typeof rootRoute + } + '/restricted_countries': { + preLoaderRoute: typeof RestrictedcountriesImport + parentRoute: typeof rootRoute + } + '/void': { + preLoaderRoute: typeof VoidImport + parentRoute: typeof rootRoute + } + '/vpn': { + preLoaderRoute: typeof VpnImport + parentRoute: typeof rootRoute + } + '/market/$address': { + preLoaderRoute: typeof MarketAddressImport + parentRoute: typeof rootRoute + } } } @@ -65,9 +95,12 @@ declare module "@tanstack/react-router" { export const routeTree = rootRoute.addChildren([ IndexRoute, + BridgeRoute, MarketsRoute, + RestrictedcountriesRoute, VoidRoute, + VpnRoute, MarketAddressRoute, -]); +]) /* prettier-ignore-end */ diff --git a/apps/hyperdrive-trading/src/ui/app/Footer/Footer.tsx b/apps/hyperdrive-trading/src/ui/app/Footer/Footer.tsx index e65fca268..8dfa3ec88 100644 --- a/apps/hyperdrive-trading/src/ui/app/Footer/Footer.tsx +++ b/apps/hyperdrive-trading/src/ui/app/Footer/Footer.tsx @@ -1,13 +1,12 @@ import { Link } from "@tanstack/react-router"; import { ReactElement } from "react"; import { HyperdriveLogo } from "src/ui/app/Navbar/HyperdriveLogo"; -import DiscordIcon from "src/ui/base/icons/discord"; +import FarcasterIcon from "src/ui/base/icons/farcaster"; +import LinkedInIcon from "src/ui/base/icons/linkedin"; +import XIcon from "src/ui/base/icons/x"; import { privacyPolicyUrl } from "src/ui/compliance/privacyPolicy"; import { termsOfUseUrl } from "src/ui/compliance/termsOfUse"; -import { sepolia } from "viem/chains"; -import { useChainId } from "wagmi"; export default function Footer(): ReactElement { - const chainId = useChainId(); const footerTitleClassName = "daisy-footer-title text-neutral-content opacity-100"; // need to set opacity here to override daisy-footer-title return ( @@ -19,52 +18,122 @@ export default function Footer(): ReactElement { > + Yield, Your Way. +
+ + + + + + + + + +
+ +