From d36fb9dc255e0ed88ce9bbe3912e532262715ab5 Mon Sep 17 00:00:00 2001 From: Fred Date: Fri, 29 Mar 2024 04:47:00 +0100 Subject: [PATCH 01/43] =?UTF-8?q?=F0=9F=9A=A7=20API=20endpoints=20to=20get?= =?UTF-8?q?=20and=20create=20local=20chains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../local-chain-form-schema.ts | 13 +++ .../app/(register)/add-local-chain/page.tsx | 11 ++ apps/web/app/api/local-chains/[slug]/route.ts | 34 ++++++ apps/web/app/api/local-chains/route.ts | 100 ++++++++++++++++++ apps/web/lib/cache-keys.ts | 13 ++- apps/web/lib/fs-cache-dev.ts | 17 ++- apps/web/lib/network.ts | 16 +++ apps/web/lib/shared-utils.ts | 6 ++ apps/web/package.json | 2 + package-lock.json | 61 +++++++++++ 10 files changed, 264 insertions(+), 9 deletions(-) create mode 100644 apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts create mode 100644 apps/web/app/(register)/add-local-chain/page.tsx create mode 100644 apps/web/app/api/local-chains/[slug]/route.ts create mode 100644 apps/web/app/api/local-chains/route.ts diff --git a/apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts b/apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts new file mode 100644 index 000000000..c76dbdffc --- /dev/null +++ b/apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts @@ -0,0 +1,13 @@ +import { z } from "zod"; +import { zfd } from "zod-form-data"; + +export const localChainFormSchema = zfd.formData({ + chainName: z.string(), + namespace: z.string().optional(), + startHeight: z.string().optional(), + logo: zfd.file(z.instanceof(File).optional()), + rpcUrl: z.string().url(), + rpcPlatform: z.enum(["cosmos"]).default("cosmos"), + tokenDecimals: z.coerce.number().int().positive(), + tokenName: z.string(), +}); diff --git a/apps/web/app/(register)/add-local-chain/page.tsx b/apps/web/app/(register)/add-local-chain/page.tsx new file mode 100644 index 000000000..5b0d282ff --- /dev/null +++ b/apps/web/app/(register)/add-local-chain/page.tsx @@ -0,0 +1,11 @@ +import { redirect } from "next/navigation"; +import { env } from "~/env"; + +export interface PageProps {} + +export default function Page(props: PageProps) { + if (env.NEXT_PUBLIC_TARGET !== "electron") { + redirect("/"); + } + return <>; +} diff --git a/apps/web/app/api/local-chains/[slug]/route.ts b/apps/web/app/api/local-chains/[slug]/route.ts new file mode 100644 index 000000000..aee5ea4bb --- /dev/null +++ b/apps/web/app/api/local-chains/[slug]/route.ts @@ -0,0 +1,34 @@ +import { NextRequest } from "next/server"; +import { FileSystemCacheDEV } from "~/lib/fs-cache-dev"; +import { env } from "~/env"; +import type { SingleNetwork } from "~/lib/network"; +import { CACHE_KEYS } from "~/lib/cache-keys"; + +export async function GET( + request: NextRequest, + ctx: { params: { slug: string } }, +) { + if (env.NEXT_PUBLIC_TARGET === "electron") { + return Response.json( + { + errors: { + root: ["this feature is only available for the electron target"], + }, + }, + { status: 403 }, + ); + } + + const slug = ctx.params.slug; + const baseURL = `${request.nextUrl.protocol}//${request.nextUrl.host}`; + const fsCache = new FileSystemCacheDEV(); + let data = await fsCache.get(CACHE_KEYS.networks.single(slug)); + + if (data !== null) { + data = { + ...data, + config: { ...data.config, logoUrl: `${baseURL}/${data.config.logoUrl}` }, + }; + } + return Response.json(data); +} diff --git a/apps/web/app/api/local-chains/route.ts b/apps/web/app/api/local-chains/route.ts new file mode 100644 index 000000000..8004316a4 --- /dev/null +++ b/apps/web/app/api/local-chains/route.ts @@ -0,0 +1,100 @@ +import { NextRequest } from "next/server"; +import { FileSystemCacheDEV } from "~/lib/fs-cache-dev"; +import fs from "node:fs/promises"; +import { env } from "~/env"; +import { localChainFormSchema } from "~/app/(register)/add-local-chain/local-chain-form-schema"; +import slugify from "@sindresorhus/slugify"; +import type { SingleNetwork } from "~/lib/network"; +import { CACHE_KEYS } from "~/lib/cache-keys"; +import { generateRandomString } from "~/lib/shared-utils"; +import crypto from "node:crypto"; + +export async function POST(request: NextRequest) { + if (env.NEXT_PUBLIC_TARGET === "electron") { + return Response.json( + { + errors: { + root: ["this feature is only available for the electron target"], + }, + }, + { status: 403 }, + ); + } + const result = localChainFormSchema.safeParse(await request.formData()); + if (!result.success) { + return Response.json( + { + errors: result.error.flatten().fieldErrors, + }, + { status: 422 }, + ); + } + + const { logo, ...body } = result.data; + const fsCache = new FileSystemCacheDEV(); + const items = await fsCache.search(CACHE_KEYS.networks.single("local")); + + const networkSlug = `local-${slugify(body.chainName)}`; + + let logoUrl = `/images/rollkit-logo.svg`; + if (logo) { + await writeFileToPath( + `../../../public/images/logo-${networkSlug}.png`, + logo, + ); + + logoUrl = new URL( + `../../../public/images/logo-${networkSlug}.png`, + import.meta.url, + ).toString(); + } + const chainData = { + chainName: body.chainName, + brand: "local", + slug: networkSlug, + config: { + rpcUrls: { + [body.rpcPlatform]: body.rpcUrl, + }, + token: { + name: body.tokenName, + decimals: body.tokenDecimals, + }, + logoUrl, + ecosystems: [], + cssGradient: `linear-gradient(97deg, #000 -5.89%, #1E1E1E 83.12%, #000 103.23%)`, // ecplise's default + primaryColor: "236 15% 18%", // ecplise's default + }, + namespace: body.namespace, + startHeight: body.startHeight, + paidVersion: false, + accountId: generateRandomString(20), + internalId: (items.length + 1).toString(), + integrationId: crypto.randomUUID(), + createdTime: new Date(), + } satisfies SingleNetwork; + + await fsCache.set(CACHE_KEYS.networks.single(networkSlug), chainData); + return Response.json({ + success: true, + }); +} + +async function fileToBuffer(file: File) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + const arrayBuffer = event.target?.result as ArrayBuffer; + const buffer = Buffer.from(arrayBuffer); + resolve(buffer); + }; + reader.onerror = (err) => { + reject(err); + }; + reader.readAsArrayBuffer(file); + }); +} + +async function writeFileToPath(filePath: string, data: File): Promise { + await fs.writeFile(filePath, await fileToBuffer(data)); +} diff --git a/apps/web/lib/cache-keys.ts b/apps/web/lib/cache-keys.ts index 6847ef447..75c5eba68 100644 --- a/apps/web/lib/cache-keys.ts +++ b/apps/web/lib/cache-keys.ts @@ -59,10 +59,13 @@ export const CACHE_KEYS = { txHash, msgIndex, ], - ibcResolve: (resolverId: string, input: any) => [ - "IBC_RESOLVE", - resolverId, - input, - ], + ibcResolve: ( + resolverId: string, + input: { + hash: string; + step: number; + slug: string; + }, + ) => ["IBC_RESOLVE", resolverId, input] as const, blob: (url: string) => ["BLOB", url], } as const; diff --git a/apps/web/lib/fs-cache-dev.ts b/apps/web/lib/fs-cache-dev.ts index 7d3b5a8c3..6d25f1350 100644 --- a/apps/web/lib/fs-cache-dev.ts +++ b/apps/web/lib/fs-cache-dev.ts @@ -17,7 +17,7 @@ export class FileSystemCacheDEV { } } - private async computeCacheKey(id: CacheId, updatedAt?: Date | number) { + private computeCacheKey(id: CacheId, updatedAt?: Date | number) { let fullKey = Array.isArray(id) ? id.join("-") : id.toString(); if (updatedAt) { fullKey += `-${new Date(updatedAt).getTime()}`; @@ -30,12 +30,12 @@ export class FileSystemCacheDEV { value, expiry: ttl ? Date.now() + ttl * 1000 : null, }; - const filePath = this.getFilePath(await this.computeCacheKey(key)); + const filePath = this.getFilePath(this.computeCacheKey(key)); await fs.writeFile(filePath, JSON.stringify(cacheEntry), "utf-8"); } - async get(key: string): Promise { - const filePath = this.getFilePath(key); + async get(key: CacheId): Promise { + const filePath = this.getFilePath(this.computeCacheKey(key)); try { const data = await fs.readFile(filePath, "utf-8"); const cacheEntry: CacheEntry = JSON.parse(data); @@ -52,6 +52,15 @@ export class FileSystemCacheDEV { } } + async search(key: CacheId): Promise { + const files = await fs.readdir(this.cacheDir); + return Promise.all( + files.filter((fileName) => + fileName.startsWith(this.computeCacheKey(fileName)), + ), + ); + } + private getFilePath(key: string): string { return path.join(this.cacheDir, `${key}.json`); } diff --git a/apps/web/lib/network.ts b/apps/web/lib/network.ts index 84d406f33..51881e391 100644 --- a/apps/web/lib/network.ts +++ b/apps/web/lib/network.ts @@ -4,6 +4,7 @@ import { env } from "~/env.js"; import { CACHE_KEYS } from "./cache-keys"; import { cache } from "react"; import { integrations, integrationList } from "~/lib/cache"; +import { jsonFetch } from "~/lib/shared-utils"; export const singleNetworkSchema = z.object({ config: z.object({ @@ -41,6 +42,8 @@ export const singleNetworkSchema = z.object({ internalId: z.string(), integrationId: z.string().uuid(), createdTime: z.coerce.date(), + namespace: z.string().optional(), + startHeight: z.string().optional(), }); export type SingleNetwork = z.infer; @@ -54,6 +57,19 @@ export const getAllNetworks = cache(function getAllNetworks() { export const getSingleNetwork = cache(async function getSingleNetwork( slug: string, ) { + if (env.NEXT_PUBLIC_TARGET === "electron" && slug.startsWith("local")) { + try { + return await fetch( + // eslint-disable-next-line turbo/no-undeclared-env-vars + `http://127.0.0.1/${process.env.PORT}/api/local-chains/${slug}`, + ) + .then((r) => r.json()) + .then(singleNetworkSchema.parse); + } catch (error) { + return null; + } + } + try { // @ts-expect-error const found = integrations[slug].value; diff --git a/apps/web/lib/shared-utils.ts b/apps/web/lib/shared-utils.ts index 698e13cea..12168581f 100644 --- a/apps/web/lib/shared-utils.ts +++ b/apps/web/lib/shared-utils.ts @@ -288,3 +288,9 @@ export function getMetadata( )} ${capitalize(network.chainName)}, brought to you by Modular Cloud.`, }; } + +export function generateRandomString(length: number): string { + return Array.from({ length }, () => + Math.floor(Math.random() * 36).toString(36), + ).join(""); +} diff --git a/apps/web/package.json b/apps/web/package.json index a400763b6..cc2500adc 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -39,6 +39,7 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", + "@sindresorhus/slugify": "^2.2.1", "@solana/web3.js": "^1.78.4", "@t3-oss/env-nextjs": "^0.6.1", "@tanstack/react-query": "^5.0.0", @@ -72,6 +73,7 @@ "tsx": "^4.7.1", "web3": "^1.8.2", "zod": "^3.20.6", + "zod-form-data": "^2.0.2", "zustand": "^4.4.6" }, "devDependencies": { diff --git a/package-lock.json b/package-lock.json index 64217a479..c7a0c8086 100644 --- a/package-lock.json +++ b/package-lock.json @@ -88,6 +88,7 @@ "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toggle-group": "^1.0.4", "@radix-ui/react-tooltip": "^1.0.6", + "@sindresorhus/slugify": "^2.2.1", "@solana/web3.js": "^1.78.4", "@t3-oss/env-nextjs": "^0.6.1", "@tanstack/react-query": "^5.0.0", @@ -121,6 +122,7 @@ "tsx": "^4.7.1", "web3": "^1.8.2", "zod": "^3.20.6", + "zod-form-data": "^2.0.2", "zustand": "^4.4.6" }, "devDependencies": { @@ -4711,6 +4713,57 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, + "node_modules/@sindresorhus/slugify": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@sindresorhus/slugify/-/slugify-2.2.1.tgz", + "integrity": "sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==", + "dependencies": { + "@sindresorhus/transliterate": "^1.0.0", + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/slugify/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/transliterate/-/transliterate-1.6.0.tgz", + "integrity": "sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==", + "dependencies": { + "escape-string-regexp": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sindresorhus/transliterate/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.0", "license": "BSD-3-Clause", @@ -17802,6 +17855,14 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/zod-form-data": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/zod-form-data/-/zod-form-data-2.0.2.tgz", + "integrity": "sha512-sKTi+k0fvkxdakD0V5rq+9WVJA3cuTQUfEmNqvHrTzPLvjfLmkkBLfR0ed3qOi9MScJXTHIDH/jUNnEJ3CBX4g==", + "peerDependencies": { + "zod": ">= 3.11.0" + } + }, "node_modules/zustand": { "version": "4.4.6", "license": "MIT", From 19645cc95ef8cc1ca98b68f1941b85c45f51cb28 Mon Sep 17 00:00:00 2001 From: Fred Date: Sat, 30 Mar 2024 02:22:34 +0100 Subject: [PATCH 02/43] =?UTF-8?q?=F0=9F=8D=B1=20add=20Camera=20Icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/ui/icon-svgs/Camera.svg | 4 ++++ apps/web/ui/icons/Camera.tsx | 26 ++++++++++++++++++++++++++ apps/web/ui/icons/index.ts | 1 + 3 files changed, 31 insertions(+) create mode 100644 apps/web/ui/icon-svgs/Camera.svg create mode 100644 apps/web/ui/icons/Camera.tsx diff --git a/apps/web/ui/icon-svgs/Camera.svg b/apps/web/ui/icon-svgs/Camera.svg new file mode 100644 index 000000000..9db8d85bc --- /dev/null +++ b/apps/web/ui/icon-svgs/Camera.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apps/web/ui/icons/Camera.tsx b/apps/web/ui/icons/Camera.tsx new file mode 100644 index 000000000..be0ef9911 --- /dev/null +++ b/apps/web/ui/icons/Camera.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import type { SVGProps } from "react"; +const SvgCamera = (props: SVGProps) => ( + + + + +); +export default SvgCamera; diff --git a/apps/web/ui/icons/index.ts b/apps/web/ui/icons/index.ts index bf37d6dfc..d22b527cf 100644 --- a/apps/web/ui/icons/index.ts +++ b/apps/web/ui/icons/index.ts @@ -12,6 +12,7 @@ export { default as BellOn } from "./BellOn"; export { default as Building } from "./Building"; export { default as Calendar } from "./Calendar"; export { default as CalendarTime } from "./CalendarTime"; +export { default as Camera } from "./Camera"; export { default as CardOff } from "./CardOff"; export { default as CardOn } from "./CardOn"; export { default as CardView } from "./CardView"; From 318f1c2ce3fbf86f89cbd244892e56700c5d7d04 Mon Sep 17 00:00:00 2001 From: Fred Date: Sat, 30 Mar 2024 02:22:48 +0100 Subject: [PATCH 03/43] =?UTF-8?q?=F0=9F=9A=A7=20working=20on=20the=20regis?= =?UTF-8?q?ter=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/.gitignore | 1 + .../app/(register)/add-local-chain/page.tsx | 11 ----- apps/web/app/(register)/layout.tsx | 29 ++++++++++++ .../local-chain}/local-chain-form-schema.ts | 1 + .../(register)/register/local-chain/page.tsx | 37 +++++++++++++++ .../local-chain/register-local-chain-form.tsx | 19 ++++++++ apps/web/app/(register)/register/page.tsx | 22 +-------- apps/web/app/api/local-chains/route.ts | 7 +-- apps/web/lib/network.ts | 1 + apps/web/public/images/mc-logo.svg | 46 +++++++++++++++++-- apps/web/ui/header/index.tsx | 2 +- apps/web/ui/register-form/index.tsx | 2 +- 12 files changed, 136 insertions(+), 42 deletions(-) create mode 100644 apps/.gitignore delete mode 100644 apps/web/app/(register)/add-local-chain/page.tsx create mode 100644 apps/web/app/(register)/layout.tsx rename apps/web/app/(register)/{add-local-chain => register/local-chain}/local-chain-form-schema.ts (92%) create mode 100644 apps/web/app/(register)/register/local-chain/page.tsx create mode 100644 apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx diff --git a/apps/.gitignore b/apps/.gitignore new file mode 100644 index 000000000..35504927e --- /dev/null +++ b/apps/.gitignore @@ -0,0 +1 @@ +public/images/local-chains/ \ No newline at end of file diff --git a/apps/web/app/(register)/add-local-chain/page.tsx b/apps/web/app/(register)/add-local-chain/page.tsx deleted file mode 100644 index 5b0d282ff..000000000 --- a/apps/web/app/(register)/add-local-chain/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { redirect } from "next/navigation"; -import { env } from "~/env"; - -export interface PageProps {} - -export default function Page(props: PageProps) { - if (env.NEXT_PUBLIC_TARGET !== "electron") { - redirect("/"); - } - return <>; -} diff --git a/apps/web/app/(register)/layout.tsx b/apps/web/app/(register)/layout.tsx new file mode 100644 index 000000000..b16822704 --- /dev/null +++ b/apps/web/app/(register)/layout.tsx @@ -0,0 +1,29 @@ +import * as React from "react"; +import { HomeBg } from "~/ui/home-bg"; +import { HomeBgMobile } from "~/ui/home-bg/mobile"; + +export default function RegisterLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ ); +} diff --git a/apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts b/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts similarity index 92% rename from apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts rename to apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts index c76dbdffc..2f51d18f2 100644 --- a/apps/web/app/(register)/add-local-chain/local-chain-form-schema.ts +++ b/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts @@ -5,6 +5,7 @@ export const localChainFormSchema = zfd.formData({ chainName: z.string(), namespace: z.string().optional(), startHeight: z.string().optional(), + daLayer: z.string().optional(), logo: zfd.file(z.instanceof(File).optional()), rpcUrl: z.string().url(), rpcPlatform: z.enum(["cosmos"]).default("cosmos"), diff --git a/apps/web/app/(register)/register/local-chain/page.tsx b/apps/web/app/(register)/register/local-chain/page.tsx new file mode 100644 index 000000000..98f91e394 --- /dev/null +++ b/apps/web/app/(register)/register/local-chain/page.tsx @@ -0,0 +1,37 @@ +/* eslint-disable @next/next/no-img-element */ +import * as React from "react"; + +import { redirect } from "next/navigation"; +import { env } from "~/env"; +import { RegisterLocalChainForm } from "./register-local-chain-form"; + +import type { Metadata } from "next"; + +export interface PageProps {} + +export const metadata: Metadata = { + title: "Register a rollup", +}; + +export default function Page(props: PageProps) { + if (env.NEXT_PUBLIC_TARGET !== "electron") { + redirect("/"); + } + + return ( + <> +
+
+ Modular Cloud logo +

Register a Rollup

+
+ + +
+ + ); +} diff --git a/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx b/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx new file mode 100644 index 000000000..62d8682cf --- /dev/null +++ b/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx @@ -0,0 +1,19 @@ +"use client"; + +export type RegisterLocalChainFormProps = {}; + +export function RegisterLocalChainForm({}: RegisterLocalChainFormProps) { + return ( +
{}}> +
+
+
+ +
+ +
+
+
+
+ ); +} diff --git a/apps/web/app/(register)/register/page.tsx b/apps/web/app/(register)/register/page.tsx index d577ea6ac..06c850ae4 100644 --- a/apps/web/app/(register)/register/page.tsx +++ b/apps/web/app/(register)/register/page.tsx @@ -1,8 +1,6 @@ import * as React from "react"; import type { Metadata } from "next"; -import { HomeBg } from "~/ui/home-bg"; -import { HomeBgMobile } from "~/ui/home-bg/mobile"; import { RegisterForm } from "~/ui/register-form"; export const metadata: Metadata = { @@ -10,23 +8,5 @@ export const metadata: Metadata = { }; export default function RegisterPage() { - return ( -
-
- ); + return ; } diff --git a/apps/web/app/api/local-chains/route.ts b/apps/web/app/api/local-chains/route.ts index 8004316a4..b9e204428 100644 --- a/apps/web/app/api/local-chains/route.ts +++ b/apps/web/app/api/local-chains/route.ts @@ -2,7 +2,7 @@ import { NextRequest } from "next/server"; import { FileSystemCacheDEV } from "~/lib/fs-cache-dev"; import fs from "node:fs/promises"; import { env } from "~/env"; -import { localChainFormSchema } from "~/app/(register)/add-local-chain/local-chain-form-schema"; +import { localChainFormSchema } from "~/app/(register)/register/local-chain/local-chain-form-schema"; import slugify from "@sindresorhus/slugify"; import type { SingleNetwork } from "~/lib/network"; import { CACHE_KEYS } from "~/lib/cache-keys"; @@ -39,12 +39,12 @@ export async function POST(request: NextRequest) { let logoUrl = `/images/rollkit-logo.svg`; if (logo) { await writeFileToPath( - `../../../public/images/logo-${networkSlug}.png`, + `../../../public/images/local-chains/logo-${networkSlug}.png`, logo, ); logoUrl = new URL( - `../../../public/images/logo-${networkSlug}.png`, + `../../../public/images/local-chains/logo-${networkSlug}.png`, import.meta.url, ).toString(); } @@ -67,6 +67,7 @@ export async function POST(request: NextRequest) { }, namespace: body.namespace, startHeight: body.startHeight, + daLayer: body.daLayer, paidVersion: false, accountId: generateRandomString(20), internalId: (items.length + 1).toString(), diff --git a/apps/web/lib/network.ts b/apps/web/lib/network.ts index 51881e391..3e63be5c5 100644 --- a/apps/web/lib/network.ts +++ b/apps/web/lib/network.ts @@ -44,6 +44,7 @@ export const singleNetworkSchema = z.object({ createdTime: z.coerce.date(), namespace: z.string().optional(), startHeight: z.string().optional(), + daLayer: z.string().optional(), }); export type SingleNetwork = z.infer; diff --git a/apps/web/public/images/mc-logo.svg b/apps/web/public/images/mc-logo.svg index ba4d5f489..d2e813352 100644 --- a/apps/web/public/images/mc-logo.svg +++ b/apps/web/public/images/mc-logo.svg @@ -1,9 +1,45 @@ - - + + + + + + + + + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/web/ui/header/index.tsx b/apps/web/ui/header/index.tsx index 187512d7a..e56410bde 100644 --- a/apps/web/ui/header/index.tsx +++ b/apps/web/ui/header/index.tsx @@ -33,7 +33,7 @@ export async function Header({ networkSlug }: Props) { ModularCloud Logo

Modular Cloud

diff --git a/apps/web/ui/register-form/index.tsx b/apps/web/ui/register-form/index.tsx index b458f24c2..d8ed0eb8f 100644 --- a/apps/web/ui/register-form/index.tsx +++ b/apps/web/ui/register-form/index.tsx @@ -235,7 +235,7 @@ export function RegisterForm() { Modular Cloud logo

{title}

{subTitle}

From 8edd4a164398d88190a36cd2d1e1a9f8bce36c1b Mon Sep 17 00:00:00 2001 From: Fred Date: Sat, 30 Mar 2024 04:20:34 +0100 Subject: [PATCH 04/43] =?UTF-8?q?=F0=9F=92=84=20first=20UI=20pass=20of=20r?= =?UTF-8?q?egister=20local=20page=20finished?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../local-chain/local-chain-form-schema.ts | 7 +- .../(register)/register/local-chain/page.tsx | 4 +- .../local-chain/register-local-chain-form.tsx | 124 +++++++++++++++++- apps/web/ui/input/index.tsx | 25 ++-- apps/web/ui/select/index.tsx | 6 +- 5 files changed, 144 insertions(+), 22 deletions(-) diff --git a/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts b/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts index 2f51d18f2..b3ebf58f4 100644 --- a/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts +++ b/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { z, preprocess } from "zod"; import { zfd } from "zod-form-data"; export const localChainFormSchema = zfd.formData({ @@ -8,7 +8,10 @@ export const localChainFormSchema = zfd.formData({ daLayer: z.string().optional(), logo: zfd.file(z.instanceof(File).optional()), rpcUrl: z.string().url(), - rpcPlatform: z.enum(["cosmos"]).default("cosmos"), + rpcPlatform: preprocess( + (arg) => (typeof arg === "string" ? arg.toLowerCase() : arg), + z.enum(["cosmos"]).default("cosmos"), + ), tokenDecimals: z.coerce.number().int().positive(), tokenName: z.string(), }); diff --git a/apps/web/app/(register)/register/local-chain/page.tsx b/apps/web/app/(register)/register/local-chain/page.tsx index 98f91e394..c64731842 100644 --- a/apps/web/app/(register)/register/local-chain/page.tsx +++ b/apps/web/app/(register)/register/local-chain/page.tsx @@ -20,14 +20,14 @@ export default function Page(props: PageProps) { return ( <> -
+
Modular Cloud logo -

Register a Rollup

+

Register a Local chain

diff --git a/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx b/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx index 62d8682cf..7b6b7a9ad 100644 --- a/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx +++ b/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx @@ -1,19 +1,133 @@ "use client"; +import { Button } from "~/ui/button"; +import { Camera } from "~/ui/icons"; +import { Input } from "~/ui/input"; +import { LoadingIndicator } from "~/ui/loading-indicator"; +import { Select, SelectContent, SelectItem } from "~/ui/select"; + export type RegisterLocalChainFormProps = {}; export function RegisterLocalChainForm({}: RegisterLocalChainFormProps) { return (
{}}>
-
-
- + + +
+ +
+
+
+
+ + + + -
-
+ + +
+ +
+ +
+ + + +
+ + ); } diff --git a/apps/web/ui/input/index.tsx b/apps/web/ui/input/index.tsx index 23cee3d97..299a25f83 100644 --- a/apps/web/ui/input/index.tsx +++ b/apps/web/ui/input/index.tsx @@ -58,21 +58,24 @@ export const Input = React.forwardRef, InputProps>( "ring-red-500/20 focus-within:border-red-500 focus-within:ring-2": !!error, "py-2 px-2": size === "medium", - "py-1 text-sm": size === "small", + "py-1 text-sm": size === "small" && renderLeadingIcon, + "py-2 text-sm": size === "small" && !renderLeadingIcon, "py-3": size === "large", - "cursor-not-allowed bg-disabled": disabled, + "cursor-not-allowed bg-muted-100": disabled, }, className, )} > -
- {renderLeadingIcon?.( - cn("text-muted flex-shrink-0 m-1.5", { - "h-4 w-4": size !== "large", - "h-5 w-5": size === "large", - }), - )} -
+ {renderLeadingIcon && ( +
+ {renderLeadingIcon?.( + cn("text-muted flex-shrink-0 m-1.5", { + "h-4 w-4": size !== "large", + "h-5 w-5": size === "large", + }), + )} +
+ )} , InputProps>( disabled={disabled} required={required} className={cn( - "w-full bg-transparent focus:outline-none disabled:text-foreground/30 placeholder:text-muted", + "w-full bg-transparent focus:outline-none disabled:text-foreground/70 placeholder:text-muted", inputClassName, { "cursor-not-allowed": disabled, diff --git a/apps/web/ui/select/index.tsx b/apps/web/ui/select/index.tsx index ded355d54..d9527b47d 100644 --- a/apps/web/ui/select/index.tsx +++ b/apps/web/ui/select/index.tsx @@ -46,7 +46,10 @@ export const Select = React.forwardRef< disabled={disabled} > @@ -68,7 +71,6 @@ export const Select = React.forwardRef< "py-3": size === "large", "cursor-not-allowed bg-disabled": disabled, }, - className, )} > From a3d9efee486319d15fb019ee183cc7c11784f826 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 4 Apr 2024 04:21:07 +0200 Subject: [PATCH 05/43] =?UTF-8?q?=F0=9F=8D=B1=20add=20Link=20Icon?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/ui/icon-svgs/Link.svg | 3 +++ apps/web/ui/icons/Link.tsx | 18 ++++++++++++++++++ apps/web/ui/icons/index.ts | 1 + 3 files changed, 22 insertions(+) create mode 100644 apps/web/ui/icon-svgs/Link.svg create mode 100644 apps/web/ui/icons/Link.tsx diff --git a/apps/web/ui/icon-svgs/Link.svg b/apps/web/ui/icon-svgs/Link.svg new file mode 100644 index 000000000..d467644c4 --- /dev/null +++ b/apps/web/ui/icon-svgs/Link.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/apps/web/ui/icons/Link.tsx b/apps/web/ui/icons/Link.tsx new file mode 100644 index 000000000..429abc615 --- /dev/null +++ b/apps/web/ui/icons/Link.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; +import type { SVGProps } from "react"; +const SvgLink = (props: SVGProps) => ( + + + +); +export default SvgLink; diff --git a/apps/web/ui/icons/index.ts b/apps/web/ui/icons/index.ts index d22b527cf..bf373fcdb 100644 --- a/apps/web/ui/icons/index.ts +++ b/apps/web/ui/icons/index.ts @@ -63,6 +63,7 @@ export { default as KeyboardArrowDown } from "./KeyboardArrowDown"; export { default as KeyboardArrowLeft } from "./KeyboardArrowLeft"; export { default as KeyboardArrowRight } from "./KeyboardArrowRight"; export { default as KeyboardArrowUp } from "./KeyboardArrowUp"; +export { default as Link } from "./Link"; export { default as LinkOut } from "./LinkOut"; export { default as ListView } from "./ListView"; export { default as List } from "./List"; From 4825a351dc474be81a4bdaec091a2126e64aa357 Mon Sep 17 00:00:00 2001 From: Fred Date: Thu, 4 Apr 2024 04:23:15 +0200 Subject: [PATCH 06/43] =?UTF-8?q?=F0=9F=9A=A7=20Working=20on=20the=20form?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../local-chain/local-chain-form-schema.ts | 54 ++++++-- .../(register)/register/local-chain/page.tsx | 2 +- .../local-chain/register-local-chain-form.tsx | 122 +++++++++++++++--- apps/web/app/api/local-chains/[slug]/route.ts | 3 + apps/web/app/api/local-chains/route.ts | 3 + apps/web/package.json | 1 + apps/web/ui/input/index.tsx | 7 +- apps/web/ui/register-form/form-steps.tsx | 45 ------- apps/web/ui/select/index.tsx | 1 + apps/web/ui/shadcn/components/ui/alert.tsx | 60 +++++++++ package-lock.json | 32 +++++ 11 files changed, 252 insertions(+), 78 deletions(-) create mode 100644 apps/web/ui/shadcn/components/ui/alert.tsx diff --git a/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts b/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts index b3ebf58f4..7cca26508 100644 --- a/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts +++ b/apps/web/app/(register)/register/local-chain/local-chain-form-schema.ts @@ -1,17 +1,43 @@ +import { fileTypeFromBuffer } from "file-type"; import { z, preprocess } from "zod"; import { zfd } from "zod-form-data"; -export const localChainFormSchema = zfd.formData({ - chainName: z.string(), - namespace: z.string().optional(), - startHeight: z.string().optional(), - daLayer: z.string().optional(), - logo: zfd.file(z.instanceof(File).optional()), - rpcUrl: z.string().url(), - rpcPlatform: preprocess( - (arg) => (typeof arg === "string" ? arg.toLowerCase() : arg), - z.enum(["cosmos"]).default("cosmos"), - ), - tokenDecimals: z.coerce.number().int().positive(), - tokenName: z.string(), -}); +async function readFileAsBuffer(file: File) { + return await new Promise((resolve, reject) => { + let fileReader = new FileReader(); + fileReader.onload = (e) => { + if (fileReader.result instanceof ArrayBuffer) { + resolve(fileReader.result); + } + }; + fileReader.readAsArrayBuffer(file); + }); +} + +export const localChainFormSchema = z + .object({ + chainName: z.string().min(1), + namespace: z.string().optional(), + startHeight: z.string().optional(), + daLayer: z.string().optional(), + logo: z.instanceof(File).optional(), + rpcUrl: z.string().url(), + rpcPlatform: preprocess( + (arg) => (typeof arg === "string" ? arg.toLowerCase() : arg), + z.enum(["cosmos"]).default("cosmos"), + ), + tokenDecimals: z.coerce.number().int().positive(), + tokenName: z.string().min(1), + }) + .refine(async (data) => { + if (data.logo) { + console.log({ + dataLogo: data.logo, + }); + const fileType = await fileTypeFromBuffer( + await readFileAsBuffer(data.logo), + ); + return fileType && fileType.mime.startsWith("image/"); + } + return true; + }, "The file you tried to upload is not an image."); diff --git a/apps/web/app/(register)/register/local-chain/page.tsx b/apps/web/app/(register)/register/local-chain/page.tsx index c64731842..a670287ec 100644 --- a/apps/web/app/(register)/register/local-chain/page.tsx +++ b/apps/web/app/(register)/register/local-chain/page.tsx @@ -10,7 +10,7 @@ import type { Metadata } from "next"; export interface PageProps {} export const metadata: Metadata = { - title: "Register a rollup", + title: "Register a Chain", }; export default function Page(props: PageProps) { diff --git a/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx b/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx index 7b6b7a9ad..ecc75dbc9 100644 --- a/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx +++ b/apps/web/app/(register)/register/local-chain/register-local-chain-form.tsx @@ -1,25 +1,110 @@ +/* eslint-disable @next/next/no-img-element */ "use client"; +import * as React from "react"; +import { useFormState } from "react-dom"; +import { toast } from "sonner"; +import { localChainFormSchema } from "~/app/(register)/register/local-chain/local-chain-form-schema"; import { Button } from "~/ui/button"; -import { Camera } from "~/ui/icons"; +import { Camera, Link as LinkIcon, Warning } from "~/ui/icons"; import { Input } from "~/ui/input"; import { LoadingIndicator } from "~/ui/loading-indicator"; import { Select, SelectContent, SelectItem } from "~/ui/select"; +import { fileTypeFromBuffer } from "file-type"; +import { + Alert, + AlertDescription, + AlertTitle, +} from "~/ui/shadcn/components/ui/alert"; export type RegisterLocalChainFormProps = {}; +async function readFileAsDataURL(file: File) { + return await new Promise((resolve, reject) => { + let fileReader = new FileReader(); + fileReader.onload = (e) => { + if (fileReader.result?.toString().indexOf("data:image/") === 0) { + resolve(fileReader.result.toString()); + } else { + toast.error("Error", { + description: "The file you tried to upload is not an image", + }); + reject(new Error("The file you tried to upload is not an image")); + } + }; + fileReader.readAsDataURL(file); + }); +} + export function RegisterLocalChainForm({}: RegisterLocalChainFormProps) { + async function formAction(_: any, formData: FormData) { + const parseResult = await localChainFormSchema.safeParseAsync({ + ...Object.fromEntries(formData.entries()), + logo: + formRef.current?.logo.files.length > 0 + ? formRef.current?.logo.files[0] + : null, + }); + if (parseResult.success) { + console.log({ data: parseResult.data }); + } else { + return parseResult.error.flatten(); + } + } + + const [logoFileDataURL, setLogoFileDataURL] = React.useState( + null, + ); + const [errors, action] = useFormState(formAction, null); + const formRef = React.useRef>(null); + + const fieldErrors = errors?.fieldErrors; + const formErrors = errors?.formErrors; + console.log({ + fieldErrors, + formErrors, + }); + return ( -
{}}> + + {formErrors && formErrors?.length > 0 && ( + + + )} +
@@ -36,7 +121,7 @@ export function RegisterLocalChainForm({}: RegisterLocalChainFormProps) { required defaultValue={""} autoFocus - error={""} + error={fieldErrors?.chainName} />
@@ -48,7 +133,7 @@ export function RegisterLocalChainForm({}: RegisterLocalChainFormProps) { placeholder="00 11 446 694" name="daLayer" defaultValue={""} - error={""} + error={fieldErrors?.daLayer} /> -
+
( +
@@ -105,8 +193,8 @@ export function RegisterLocalChainForm({}: RegisterLocalChainFormProps) { type="text" placeholder="TIA" name="tokenName" - defaultValue={""} - error={""} + required + error={fieldErrors?.tokenName} />
diff --git a/apps/web/app/api/local-chains/[slug]/route.ts b/apps/web/app/api/local-chains/[slug]/route.ts index aee5ea4bb..bedc2d727 100644 --- a/apps/web/app/api/local-chains/[slug]/route.ts +++ b/apps/web/app/api/local-chains/[slug]/route.ts @@ -4,6 +4,9 @@ import { env } from "~/env"; import type { SingleNetwork } from "~/lib/network"; import { CACHE_KEYS } from "~/lib/cache-keys"; +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + export async function GET( request: NextRequest, ctx: { params: { slug: string } }, diff --git a/apps/web/app/api/local-chains/route.ts b/apps/web/app/api/local-chains/route.ts index b9e204428..37d865ea0 100644 --- a/apps/web/app/api/local-chains/route.ts +++ b/apps/web/app/api/local-chains/route.ts @@ -9,6 +9,9 @@ import { CACHE_KEYS } from "~/lib/cache-keys"; import { generateRandomString } from "~/lib/shared-utils"; import crypto from "node:crypto"; +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + export async function POST(request: NextRequest) { if (env.NEXT_PUBLIC_TARGET === "electron") { return Response.json( diff --git a/apps/web/package.json b/apps/web/package.json index cc2500adc..21d92f7db 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -56,6 +56,7 @@ "date-fns": "^3.3.1", "dayjs": "^1.11.7", "decimal.js": "^10.4.3", + "file-type": "^19.0.0", "lucide-react": "^0.259.0", "next": "^14.2.0-canary.39", "react": "^18.2.0", diff --git a/apps/web/ui/input/index.tsx b/apps/web/ui/input/index.tsx index 299a25f83..7cdc556a5 100644 --- a/apps/web/ui/input/index.tsx +++ b/apps/web/ui/input/index.tsx @@ -6,6 +6,7 @@ export type InputProps = Omit< "size" | "defaultValue" > & { inputClassName?: string; + labelClassName?: string; helpText?: string; renderLeadingIcon?: (classNames: string) => React.ReactNode; renderTrailingIcon?: (classNames: string) => React.ReactNode; @@ -22,6 +23,7 @@ export const Input = React.forwardRef, InputProps>( className, helpText, inputClassName, + labelClassName, label, renderLeadingIcon, renderTrailingIcon, @@ -44,7 +46,10 @@ export const Input = React.forwardRef, InputProps>( return (
-