├── tooling
├── eslint
│ ├── vitejs.js
│ ├── react.js
│ ├── package.json
│ └── base.js
└── prettier
│ ├── tsconfig.json
│ ├── package.json
│ └── index.js
├── packages
├── apibara
│ ├── .gitignore
│ ├── eslint.config.js
│ ├── apibara.config.ts
│ ├── tsconfig.json
│ ├── env.ts
│ └── package.json
├── starknet-types-07
│ ├── index.mjs
│ └── package.json
├── starknet-types-08
│ ├── index.mjs
│ └── package.json
├── starknet-types-09
│ ├── index.mjs
│ └── package.json
├── db
│ ├── tsconfig.json
│ ├── src
│ │ ├── schema
│ │ │ ├── index.ts
│ │ │ ├── dune.ts
│ │ │ ├── velords.ts
│ │ │ ├── auth.ts
│ │ │ └── bridge.ts
│ │ ├── index.ts
│ │ ├── client.ts
│ │ ├── poolClient.ts
│ │ └── int8range.ts
│ ├── eslint.config.js
│ ├── drizzle.config.ts
│ ├── env.ts
│ └── package.json
└── constants
│ ├── src
│ ├── index.ts
│ ├── chains.ts
│ ├── marketplace.ts
│ └── bridge-addresses.ts
│ ├── eslint.config.js
│ └── package.json
├── apps
└── account-portal
│ ├── src
│ ├── hooks
│ │ ├── bridge
│ │ │ ├── useBridgeL1Realms.ts
│ │ │ ├── useWriteInitiateWithdrawRealms.ts
│ │ │ ├── useWriteDepositRealms.ts
│ │ │ ├── useWriteFinalizeWithdrawRealms.ts
│ │ │ └── useBridgeL2Realms.ts
│ │ ├── use-stark-name.ts
│ │ ├── use-mobile.tsx
│ │ ├── governance
│ │ │ ├── use-voting-power.ts
│ │ │ ├── use-current-delegate.ts
│ │ │ ├── use-delegate-realms.ts
│ │ │ └── use-vote-proposal.ts
│ │ ├── token
│ │ │ ├── L1
│ │ │ │ ├── useERC721SetApprovalForAll.tsx
│ │ │ │ └── useERC721Approval.tsx
│ │ │ └── L2
│ │ │ │ └── useERC721Approval.ts
│ │ ├── use-wrong-network.ts
│ │ ├── use-starknet-wallet.ts
│ │ ├── use-velords-claims.ts
│ │ ├── use-ipfs-pin.ts
│ │ ├── use-l2-realms-claims.ts
│ │ └── useSimulateTransactions.ts
│ ├── gql
│ │ ├── eternum
│ │ │ ├── index.ts
│ │ │ └── gql.ts
│ │ └── snapshot
│ │ │ └── index.ts
│ ├── types
│ │ ├── vite-env.d.ts
│ │ └── snapshot.ts
│ ├── components
│ │ ├── layout
│ │ │ ├── ethereum-connect.tsx
│ │ │ ├── not-found.tsx
│ │ │ ├── breadcrumbs.tsx
│ │ │ ├── search-form.tsx
│ │ │ ├── mode-toggle.tsx
│ │ │ ├── starknet-wallet-button.tsx
│ │ │ ├── default-catch-boundary.tsx
│ │ │ ├── login-card.tsx
│ │ │ └── nav-main.tsx
│ │ ├── ui
│ │ │ ├── skeleton.tsx
│ │ │ ├── collapsible.tsx
│ │ │ ├── textarea.tsx
│ │ │ ├── input.tsx
│ │ │ ├── label.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── toaster.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── checkbox.tsx
│ │ │ ├── slider.tsx
│ │ │ ├── switch.tsx
│ │ │ ├── badge.tsx
│ │ │ ├── tooltip.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── read-more.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── radio-group.tsx
│ │ │ ├── toggle.tsx
│ │ │ ├── scroll-area.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── toggle-group.tsx
│ │ │ ├── tabs.tsx
│ │ │ ├── card.tsx
│ │ │ ├── button.tsx
│ │ │ ├── accordion.tsx
│ │ │ ├── calendar.tsx
│ │ │ ├── breadcrumb.tsx
│ │ │ ├── pagination.tsx
│ │ │ └── table.tsx
│ │ ├── modules
│ │ │ ├── governance
│ │ │ │ ├── delegate-list.tsx
│ │ │ │ ├── proposal-user-vote-badge.tsx
│ │ │ │ ├── proposal-results.tsx
│ │ │ │ ├── delegate-card-skeleton.tsx
│ │ │ │ ├── proposal-list.tsx
│ │ │ │ └── delegate-list-actions.tsx
│ │ │ └── realms
│ │ │ │ ├── bridge-tx-chains.tsx
│ │ │ │ ├── realm-card.tsx
│ │ │ │ ├── realm-resources.tsx
│ │ │ │ └── bridge-tx-skeleton.tsx
│ │ └── icons
│ │ │ ├── ethereum.svg
│ │ │ └── bridge.svg
│ ├── routes
│ │ ├── -components
│ │ │ └── spinner.tsx
│ │ ├── realms.claims.tsx
│ │ ├── api
│ │ │ └── auth
│ │ │ │ └── $.tsx
│ │ ├── coming-soon.index.tsx
│ │ ├── index.tsx
│ │ ├── delegate.list.tsx
│ │ └── realms.index.tsx
│ ├── utils
│ │ ├── auth-client.ts
│ │ ├── auth
│ │ │ └── auth-siws-client.ts
│ │ ├── auth.ts
│ │ ├── seo.ts
│ │ ├── time.ts
│ │ ├── utils.ts
│ │ └── cursorPagination.ts
│ ├── lib
│ │ ├── constants.ts
│ │ ├── eternum
│ │ │ ├── apiClient.ts
│ │ │ └── getRealms.ts
│ │ ├── queries
│ │ │ ├── execute.ts
│ │ │ └── torii.ts
│ │ ├── getLordsPrice.ts
│ │ ├── getRealmsLordsClaims.ts
│ │ ├── getVeLordsBurns.ts
│ │ ├── getBridgeTransactions.ts
│ │ └── snapshot
│ │ │ └── getUserVotes.ts
│ ├── router.tsx
│ ├── abi
│ │ └── L2
│ │ │ └── RealmsBridge.ts
│ └── providers
│ │ ├── theme.tsx
│ │ └── ethereum.tsx
│ ├── README.md
│ ├── public
│ ├── favicon.ico
│ ├── apple-touch-icon.png
│ ├── realms
│ │ └── resources
│ │ │ ├── 1.png
│ │ │ ├── 2.png
│ │ │ ├── 3.png
│ │ │ ├── 4.png
│ │ │ ├── 5.png
│ │ │ ├── 6.png
│ │ │ ├── 7.png
│ │ │ ├── 8.png
│ │ │ ├── 9.png
│ │ │ ├── 10.png
│ │ │ ├── 11.png
│ │ │ ├── 12.png
│ │ │ ├── 13.png
│ │ │ ├── 14.png
│ │ │ ├── 15.png
│ │ │ ├── 16.png
│ │ │ ├── 17.png
│ │ │ ├── 18.png
│ │ │ ├── 19.png
│ │ │ ├── 20.png
│ │ │ ├── 21.png
│ │ │ ├── 22.png
│ │ │ ├── 254.png
│ │ │ ├── 255.png
│ │ │ ├── 29.png
│ │ │ └── coin.png
│ ├── android-chrome-192x192.png
│ ├── android-chrome-512x512.png
│ └── manifest.webmanifest
│ ├── .gitignore
│ ├── vercel.json
│ ├── eslint.config.js
│ ├── components.json
│ ├── tsconfig.json
│ ├── .env.example
│ ├── codegen.ts
│ ├── env.ts
│ └── vite.config.ts
├── tsconfig.json
├── pnpm-workspace.yaml
├── .env.example
├── tsconfig.node.json
├── .gitignore
├── tsconfig.app.json
├── package.json
└── README.md
/tooling/eslint/vitejs.js:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/packages/apibara/.gitignore:
--------------------------------------------------------------------------------
1 | .apibara
2 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/bridge/useBridgeL1Realms.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/account-portal/src/gql/eternum/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./gql";
--------------------------------------------------------------------------------
/apps/account-portal/src/gql/snapshot/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./gql";
--------------------------------------------------------------------------------
/packages/starknet-types-07/index.mjs:
--------------------------------------------------------------------------------
1 | export * from "@starknet-io/types-js";
2 |
--------------------------------------------------------------------------------
/packages/starknet-types-08/index.mjs:
--------------------------------------------------------------------------------
1 | export * from "@starknet-io/types-js";
2 |
--------------------------------------------------------------------------------
/packages/starknet-types-09/index.mjs:
--------------------------------------------------------------------------------
1 | export * from "@starknet-io/types-js";
2 |
--------------------------------------------------------------------------------
/apps/account-portal/src/types/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/apps/account-portal/src/types/snapshot.ts:
--------------------------------------------------------------------------------
1 | export enum Choice {
2 | Against = 0,
3 | For = 1,
4 | Abstain = 2,
5 | }
6 |
--------------------------------------------------------------------------------
/apps/account-portal/README.md:
--------------------------------------------------------------------------------
1 | # Example
2 |
3 | To run this example:
4 |
5 | - `npm install` or `yarn`
6 | - `npm start` or `yarn start`
7 |
--------------------------------------------------------------------------------
/apps/account-portal/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/favicon.ico
--------------------------------------------------------------------------------
/packages/db/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../../tsconfig.node.json",
3 | "include": ["src"],
4 | "exclude": ["node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/tooling/prettier/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@realms-world/tsconfig/base.json",
3 | "include": ["."],
4 | "exclude": ["node_modules"]
5 | }
6 |
--------------------------------------------------------------------------------
/apps/account-portal/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/1.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/2.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/3.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/4.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/5.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/6.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/7.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/8.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/9.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/9.png
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [],
3 | "references": [
4 | { "path": "./tsconfig.app.json" },
5 | { "path": "./tsconfig.node.json" }
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/10.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/10.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/11.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/12.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/13.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/14.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/14.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/15.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/16.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/17.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/18.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/19.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/19.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/20.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/21.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/21.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/22.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/22.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/254.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/254.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/255.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/255.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/29.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/29.png
--------------------------------------------------------------------------------
/apps/account-portal/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/apps/account-portal/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/apps/account-portal/public/realms/resources/coin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BibliothecaDAO/account-portal/HEAD/apps/account-portal/public/realms/resources/coin.png
--------------------------------------------------------------------------------
/packages/constants/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./bridge-addresses";
2 | export * from "./chains";
3 | export * from "./contracts";
4 | export * from "./marketplace";
5 |
--------------------------------------------------------------------------------
/packages/db/src/schema/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./bridge";
2 | export * from "./auth";
3 | export * from "./governance";
4 | export * from "./dune";
5 | export * from "./velords";
6 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/ethereum-connect.tsx:
--------------------------------------------------------------------------------
1 |
2 | export const EthereumConnect = ({ label }: { label?: string }) => {
3 | return (
4 |
5 |
6 | );
7 | };
8 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/-components/spinner.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | export function Spinner() {
4 | return
⍥
5 | }
6 |
--------------------------------------------------------------------------------
/packages/db/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@realms-world/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | ...baseConfig,
6 | ];
7 |
--------------------------------------------------------------------------------
/packages/constants/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@realms-world/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | ...baseConfig,
6 | ];
7 |
--------------------------------------------------------------------------------
/apps/account-portal/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | package-lock.json
3 | yarn.lock
4 |
5 | !.env
6 | .DS_Store
7 | .cache
8 | .vercel
9 | .output
10 | .tanstack
11 | .nitro
12 | /build/
13 | /api/
14 | /server/build
15 | /public/build
--------------------------------------------------------------------------------
/packages/starknet-types-07/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@starknet-io/starknet-types-07",
3 | "version": "0.7.10",
4 | "main": "index.mjs",
5 | "type": "module",
6 | "dependencies": {
7 | "@starknet-io/types-js": "0.7.10"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/starknet-types-08/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@starknet-io/starknet-types-08",
3 | "version": "0.8.4",
4 | "main": "index.mjs",
5 | "type": "module",
6 | "dependencies": {
7 | "@starknet-io/types-js": "0.8.4"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/starknet-types-09/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@starknet-io/starknet-types-09",
3 | "version": "0.9.1",
4 | "main": "index.mjs",
5 | "type": "module",
6 | "dependencies": {
7 | "@starknet-io/types-js": "0.9.1"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/packages/apibara/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@realms-world/eslint-config/base";
2 |
3 | /** @type {import('typescript-eslint').Config} */
4 | export default [
5 | {
6 | ignores: [".apibara/**"],
7 | },
8 | ...baseConfig,
9 | ];
10 |
--------------------------------------------------------------------------------
/apps/account-portal/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "env": {
4 | "NODE_OPTIONS": "--max-old-space-size=4096 --max-http-header-size=16384"
5 | }
6 | },
7 | "env": {
8 | "NODE_ENV": "production",
9 | "VERCEL_ENV": "production"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/packages/db/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "drizzle-kit";
2 | import { env } from "./env";
3 |
4 | export default {
5 | schema: "./src/schema/index.ts",
6 | dialect: "postgresql",
7 | dbCredentials: {
8 | url: env.DATABASE_URL,
9 | ssl: true,
10 | },
11 | } satisfies Config;
12 |
--------------------------------------------------------------------------------
/apps/account-portal/public/manifest.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "icons": [
3 | {
4 | "src": "/android-chrome-192x192.png",
5 | "sizes": "192x192",
6 | "type": "image/png"
7 | },
8 | {
9 | "src": "/android-chrome-512x512.png",
10 | "sizes": "512x512",
11 | "type": "image/png"
12 | }
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | # pnpm-monrepo/pnpm-workspace.yaml
2 | packages:
3 | - "./apps/**"
4 | - "./packages/**"
5 | - "./tooling/**"
6 | catalog:
7 | eslint: ^9.9.0
8 | prettier: ^3.3.3
9 | zod: ^3.24.1
10 | dotenv-cli: ^7.4.2
11 | starknet: ^8.1.2
12 | viem: ^2.31.3
13 | drizzle-kit: ^0.31.4
14 | drizzle-orm: ^0.44.2
15 | typescript: ^5.8.2
16 |
--------------------------------------------------------------------------------
/packages/db/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "drizzle-orm";
2 | export * from "./schema/bridge";
3 | export * from "./schema/auth";
4 |
5 | // Export commonly used query builders
6 | export {
7 | eq,
8 | and,
9 | or,
10 | desc,
11 | asc,
12 | like,
13 | sql,
14 | gt,
15 | lt
16 | } from "drizzle-orm";
17 | export type { SQL, AnyColumn } from "drizzle-orm";
18 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/utils/utils";
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/apps/account-portal/eslint.config.js:
--------------------------------------------------------------------------------
1 | import baseConfig from "@realms-world/eslint-config/base";
2 | import reactConfig from "@realms-world/eslint-config/react";
3 |
4 | /** @type {import('typescript-eslint').Config} */
5 | export default [
6 | {
7 | ignores: ["dist/**", ".storybook/**", ".vercel", ".output", "byukd/"],
8 | },
9 | ...baseConfig,
10 | ...reactConfig,
11 | ];
12 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/collapsible.tsx:
--------------------------------------------------------------------------------
1 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
2 |
3 | const Collapsible = CollapsiblePrimitive.Root
4 |
5 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger
6 |
7 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent
8 |
9 | export { Collapsible, CollapsibleTrigger, CollapsibleContent }
10 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/realms.claims.tsx:
--------------------------------------------------------------------------------
1 | import { ClaimRewards } from "@/components/modules/realms/claims";
2 | import { createFileRoute } from "@tanstack/react-router";
3 |
4 | export const Route = createFileRoute("/realms/claims")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | return (
10 |
11 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/packages/constants/src/chains.ts:
--------------------------------------------------------------------------------
1 | export enum ChainId {
2 | MAINNET = 1,
3 | SEPOLIA = 11155111,
4 | MISSISSIPPI_TESTNET = 33784,
5 |
6 | SN_MAIN = "0x534e5f4d41494e",
7 | SN_SEPOLIA = "0x534e5f5345504f4c4941",
8 |
9 | REALMS_L3 = "420",
10 |
11 | SLOT_TESTNET = 555, // TODO: update with the real value
12 |
13 | SN_DEVNET = 556, // TODO: update with the real value
14 | }
15 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/auth-client.ts:
--------------------------------------------------------------------------------
1 | import { siwsClientPlugin } from "@/utils/auth/auth-siws-client";
2 | import { createAuthClient } from "better-auth/react";
3 |
4 | // Use type assertion to tell TypeScript this is safe
5 | export const authClient = createAuthClient({
6 | baseURL:
7 | (import.meta.env.VITE_BASE_URL as string | undefined) ??
8 | "http://localhost:3000",
9 | plugins: [siwsClientPlugin()],
10 | });
11 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/auth/auth-siws-client.ts:
--------------------------------------------------------------------------------
1 | import type { BetterAuthClientPlugin } from "better-auth";
2 |
3 | import type { siws } from "./auth-siws-plugin";
4 |
5 | type SignInWithStarknetPlugin = typeof siws;
6 |
7 | export const siwsClientPlugin = () => {
8 | return {
9 | id: "sign-in-with-starknet",
10 | $InferServerPlugin: {} as ReturnType,
11 | } satisfies BetterAuthClientPlugin;
12 | };
13 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/api/auth/$.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from "@/utils/auth";
2 | import { createServerFileRoute } from "@tanstack/react-start/server";
3 |
4 | export const ServerRoute = createServerFileRoute("/api/auth/$").methods({
5 | GET: ({ request }) => {
6 | return auth.handler(request);
7 | },
8 | POST: ({ request }) => {
9 | console.log("POST request received");
10 | return auth.handler(request);
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/packages/apibara/apibara.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "apibara/config";
2 | import esbuild from "rollup-plugin-esbuild";
3 |
4 | export default defineConfig({
5 | runtimeConfig: {
6 | streamUrl: "https://starknet.preview.apibara.org",
7 | contractAddress:
8 | "0x028d709c875c0ceac3dce7065bec5328186dc89fe254527084d1689910954b0a",
9 | },
10 | exportConditions: ["node"],
11 | rolldownConfig: {
12 | plugins: [esbuild()],
13 | },
14 | });
15 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | VITE_PUBLIC_CHAIN=mainnet
2 | VITE_PUBLIC_SLOT=eternum-prod
3 |
4 | VITE_PUBLIC_IMAGE_CDN_URL=https://media.arkproject.dev
5 | VITE_PUBLIC_IMAGE_PROXY_URL=https://imgproxy.arkproject.dev
6 | VITE_PUBLIC_IPFS_GATEWAY=https://ipfs.arkproject.dev/ipfs/
7 |
8 | DATABASE_URL=postgresql://XXXXXX:XXXXXXXXX@XXXXXXXX-pooler.us-east-2.aws.neon.tech/XXXX?sslmode=require
9 |
10 | VITE_RESERVOIR_API_KEY= # required for L1 Realms
11 |
12 | VITE_DUNE_API_KEY= # optional
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-stark-name.ts:
--------------------------------------------------------------------------------
1 | import { shortenAddress, shortenName } from "@/utils/utils";
2 | import { useStarkName as useStarkNameReact } from "@starknet-react/core";
3 |
4 | export function useStarkDisplayName(address?: `0x${string}`): string {
5 | const { data: domain } = useStarkNameReact({ address });
6 | const shortened = domain
7 | ? shortenName(domain)
8 | : address
9 | ? shortenAddress(address)
10 | : "none";
11 |
12 | return shortened;
13 | }
14 |
--------------------------------------------------------------------------------
/apps/account-portal/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "new-york",
4 | "rsc": false,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "src/index.css",
9 | "baseColor": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/utils/utils",
16 | "ui": "@/components/ui",
17 | "lib": "@/lib",
18 | "hooks": "@/hooks"
19 | },
20 | "iconLibrary": "lucide"
21 | }
22 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/constants.ts:
--------------------------------------------------------------------------------
1 | // Contract addresses for veLORDS ecosystem
2 |
3 | // Time constants matching the Cairo contract
4 | export const TIME_CONSTANTS = {
5 | DAY: 86400, // 3600 * 24
6 | WEEK: 604800, // DAY * 7
7 | TOKEN_CHECKPOINT_DEADLINE: 86400, // DAY
8 | ITERATION_LIMIT: 500,
9 | PROTOCOL_START_TIME: 1725494400, // September 5, 2024 - veLORDS protocol launch
10 | };
11 |
12 | // APY calculation constants
13 | export const APY_CONSTANTS = {
14 | BLOCKS_PER_YEAR: 52, // 52 weeks per year
15 | BASIS_POINTS: 10000,
16 | DECIMALS: 18,
17 | };
18 |
--------------------------------------------------------------------------------
/packages/constants/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@realms-world/constants",
3 | "version": "0.1.0",
4 | "types": "./src/index.ts",
5 | "license": "MIT",
6 | "main": "./src/index.ts",
7 | "type": "module",
8 | "files": [
9 | "dist/**",
10 | "src/**"
11 | ],
12 | "scripts": {
13 | "lint": "eslint",
14 | "format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
15 | "typecheck": "tsc --noEmit"
16 | },
17 | "dependencies": {
18 | "dotenv": "^16.4.7"
19 | },
20 | "devDependencies": {
21 | },
22 | "prettier": "@realms-world/prettier-config"
23 | }
24 |
--------------------------------------------------------------------------------
/tooling/eslint/react.js:
--------------------------------------------------------------------------------
1 | import reactPlugin from "eslint-plugin-react";
2 | import hooksPlugin from "eslint-plugin-react-hooks";
3 |
4 | /** @type {Awaited} */
5 | export default [
6 | {
7 | files: ["**/*.ts", "**/*.tsx"],
8 | plugins: {
9 | react: reactPlugin,
10 | "react-hooks": hooksPlugin,
11 | },
12 | rules: {
13 | ...reactPlugin.configs["jsx-runtime"].rules,
14 | ...hooksPlugin.configs.recommended.rules,
15 | },
16 | languageOptions: {
17 | globals: {
18 | React: "writable",
19 | },
20 | },
21 | },
22 | ];
23 |
--------------------------------------------------------------------------------
/packages/apibara/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "display": "Default",
4 | "compilerOptions": {
5 | "forceConsistentCasingInFileNames": true,
6 | "target": "ES2022",
7 | "lib": ["ES2023"],
8 | "module": "ESNext",
9 | "moduleResolution": "bundler",
10 | "skipLibCheck": true,
11 | "types": ["node"],
12 | "noEmit": true,
13 | "strict": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "@/*": ["./*"]
17 | }
18 | },
19 | "include": [".", "./.apibara/types"],
20 | "exclude": ["node_modules"]
21 | }
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/tsconfig",
3 | "compilerOptions": {
4 | "target": "ES2022",
5 | "lib": ["ES2023"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "isolatedModules": true,
13 | "moduleDetection": "force",
14 | "noEmit": true,
15 |
16 | /* Linting */
17 | "strict": true,
18 | "noUnusedLocals": true,
19 | "noUnusedParameters": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "noUncheckedSideEffectImports": true
22 | },
23 | }
24 |
--------------------------------------------------------------------------------
/packages/apibara/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const envSchema = z.object({
4 | // Version and chain info
5 | VITE_PUBLIC_CHAIN: z.enum(["sepolia", "mainnet", "testnet", "local"]), // Add other chains as needed
6 |
7 | });
8 |
9 | let env: z.infer;
10 | try {
11 | env = envSchema.parse(process.env);
12 | } catch (error) {
13 | if (error instanceof z.ZodError) {
14 | console.error("❌ Invalid environment variables:", JSON.stringify(error.errors, null, 2));
15 | }
16 | throw new Error("Invalid environment variables");
17 | }
18 |
19 | export { env };
20 |
21 | // Type for your validated env
22 | export type Env = z.infer;
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-mobile.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | const MOBILE_BREAKPOINT = 768
4 |
5 | export function useIsMobile() {
6 | const [isMobile, setIsMobile] = React.useState(undefined)
7 |
8 | React.useEffect(() => {
9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`)
10 | const onChange = () => {
11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
12 | }
13 | mql.addEventListener("change", onChange)
14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT)
15 | return () => mql.removeEventListener("change", onChange)
16 | }, [])
17 |
18 | return !!isMobile
19 | }
20 |
--------------------------------------------------------------------------------
/tooling/prettier/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@realms-world/prettier-config",
3 | "private": true,
4 | "version": "0.1.0",
5 | "type": "module",
6 | "exports": {
7 | ".": "./index.js"
8 | },
9 | "scripts": {
10 | "clean": "git clean -xdf .cache .turbo node_modules",
11 | "format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
12 | "typecheck": "tsc --noEmit"
13 | },
14 | "dependencies": {
15 | "@ianvs/prettier-plugin-sort-imports": "^4.4.1",
16 | "prettier": "catalog:",
17 | "prettier-plugin-tailwindcss": "^0.6.11"
18 | },
19 | "devDependencies": {
20 | "typescript": "catalog:"
21 | },
22 | "prettier": "@realms-world/prettier-config"
23 | }
24 |
--------------------------------------------------------------------------------
/packages/db/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const envSchema = z.object({
4 | // Version and chain info
5 | DATABASE_URL: z.string(), // Add other chains as needed
6 | VERCEL_ENV: z.string().optional(),
7 | });
8 |
9 | let env: z.infer;
10 | try {
11 | env = envSchema.parse(process.env);
12 | } catch (error) {
13 | if (error instanceof z.ZodError) {
14 | console.error(
15 | "❌ Invalid environment variables:",
16 | JSON.stringify(error.errors, null, 2)
17 | );
18 | }
19 | throw new Error("Invalid environment variables");
20 | }
21 |
22 | export { env };
23 |
24 | // Type for your validated env
25 | export type Env = z.infer;
26 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/eternum/apiClient.ts:
--------------------------------------------------------------------------------
1 | import { env } from "../../../env";
2 |
3 | const API_BASE_URL = env.VITE_TORII_API_URL + "/sql";
4 |
5 | /**
6 | * Generic API client for making SQL queries to the backend.
7 | * Handles URL construction, error handling, and JSON parsing.
8 | * @param query - The SQL query string
9 | * @returns The parsed JSON response
10 | */
11 | export async function fetchSQL(query: string): Promise {
12 | const url = `${API_BASE_URL}?query=${encodeURIComponent(query)}`;
13 | const response = await fetch(url);
14 |
15 | if (!response.ok) {
16 | throw new Error(`Failed to fetch: ${response.statusText}`);
17 | }
18 | return await response.json();
19 | }
20 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/governance/use-voting-power.ts:
--------------------------------------------------------------------------------
1 | import { RealmsABI } from "@/abi/L2/Realms";
2 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
3 | import { useAccount, useReadContract } from "@starknet-react/core";
4 |
5 | import { CollectionAddresses } from "@realms-world/constants";
6 |
7 | export const useVotingPower = () => {
8 | const { address } = useAccount();
9 |
10 | const l2RealmsAddress = CollectionAddresses.realms[
11 | SUPPORTED_L2_CHAIN_ID
12 | ] as `0x${string}`;
13 |
14 | return useReadContract({
15 | abi: RealmsABI,
16 | address: l2RealmsAddress,
17 | args: address ? [address] : undefined,
18 | functionName: "get_votes",
19 | watch: true,
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/governance/use-current-delegate.ts:
--------------------------------------------------------------------------------
1 | import { RealmsABI } from "@/abi/L2/Realms";
2 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
3 | import { useAccount, useReadContract } from "@starknet-react/core";
4 |
5 | import { CollectionAddresses } from "@realms-world/constants";
6 |
7 | export const useCurrentDelegate = () => {
8 | const { address } = useAccount();
9 |
10 | const l2RealmsAddress = CollectionAddresses.realms[
11 | SUPPORTED_L2_CHAIN_ID
12 | ] as `0x${string}`;
13 |
14 | return useReadContract({
15 | abi: RealmsABI,
16 | address: l2RealmsAddress,
17 | args: address ? [address] : undefined,
18 | functionName: "delegates",
19 | watch: true,
20 | });
21 | };
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 | .env
27 |
28 | app.config.timestamp-*
29 | vite.config.js.timestamp-*
30 | vite.config.ts.timestamp-*
31 | app.config.js.timestamp-*
32 | app.config.ts.timestamp-*
33 |
34 | app.config.timestamp_*
35 | vite.config.js.timestamp_*
36 | vite.config.ts.timestamp_*
37 | app.config.js.timestamp_*
38 | app.config.ts.timestamp_*
39 | packages/db/drizzle/meta/
40 |
41 | *.sql
42 |
--------------------------------------------------------------------------------
/apps/account-portal/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "strict": true,
5 | "esModuleInterop": true,
6 | "jsx": "react-jsx",
7 | "module": "ESNext",
8 | "moduleResolution": "bundler",
9 | "moduleDetection": "force",
10 | "lib": ["DOM", "DOM.Iterable", "ES2022"],
11 | "isolatedModules": true,
12 | "resolveJsonModule": true,
13 | "skipLibCheck": true,
14 | "target": "ES2022",
15 | "allowJs": true,
16 | "forceConsistentCasingInFileNames": true,
17 | "baseUrl": ".",
18 | "allowImportingTsExtensions": true,
19 | "allowSyntheticDefaultImports": true,
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 | "noEmit": true
24 | }
25 | }
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4 | "target": "ES2020",
5 | "useDefineForClassFields": true,
6 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
7 | "module": "ESNext",
8 | "skipLibCheck": true,
9 |
10 | /* Bundler mode */
11 | "moduleResolution": "bundler",
12 | "allowImportingTsExtensions": true,
13 | "isolatedModules": true,
14 | "moduleDetection": "force",
15 | "noEmit": true,
16 | "jsx": "react-jsx",
17 |
18 | /* Linting */
19 | "strict": true,
20 | "noUnusedLocals": true,
21 | "noUnusedParameters": true,
22 | "noFallthroughCasesInSwitch": true,
23 | "noUncheckedSideEffectImports": true
24 | },
25 | }
26 |
--------------------------------------------------------------------------------
/apps/account-portal/.env.example:
--------------------------------------------------------------------------------
1 | # Chain configuration (mainnet or sepolia)
2 | VITE_PUBLIC_CHAIN=mainnet
3 |
4 | # Required: Alchemy API Key
5 | # Get your free API key at: https://www.alchemy.com/
6 | # The free tier includes:
7 | # - 300M compute units/month
8 | # - NFT API access
9 | # - No credit card required
10 | VITE_ALCHEMY_API_KEY=your_alchemy_api_key_here
11 |
12 | # Optional: Other API keys
13 | VITE_RESERVOIR_API_KEY=
14 | VITE_DUNE_API_KEY=
15 | VITE_ETHPLORER_APIKEY=
16 |
17 | # Required: Public configuration
18 | VITE_PUBLIC_SLOT=0
19 |
20 | # Optional: URLs (defaults will be used if not provided)
21 | VITE_BASE_URL=
22 | VITE_PUBLIC_IMAGE_CDN_URL=
23 | VITE_PUBLIC_IMAGE_PROXY_URL=
24 | VITE_PUBLIC_IPFS_GATEWAY=
25 | VITE_TORII_API_URL=
26 | VITE_PUBLIC_NODE_URL=
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/utils/utils"
4 |
5 | const Textarea = React.forwardRef<
6 | HTMLTextAreaElement,
7 | React.ComponentProps<"textarea">
8 | >(({ className, ...props }, ref) => {
9 | return (
10 |
18 | )
19 | })
20 | Textarea.displayName = "Textarea"
21 |
22 | export { Textarea }
23 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/coming-soon.index.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@/components/ui/card";
2 | import { createFileRoute } from "@tanstack/react-router";
3 |
4 | export const Route = createFileRoute("/coming-soon/")({
5 | component: RouteComponent,
6 | });
7 |
8 | function RouteComponent() {
9 | return (
10 |
11 |
12 | Coming Soon
13 |
14 | We're working hard to bring you an amazing new experience. Stay tuned!
15 |
16 |
17 |
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/auth.ts:
--------------------------------------------------------------------------------
1 | import { betterAuth } from "better-auth";
2 | import { drizzleAdapter } from "better-auth/adapters/drizzle";
3 | import { env } from "env";
4 |
5 | import { db } from "@realms-world/db/client";
6 |
7 | import { siws } from "./auth/auth-siws-plugin";
8 |
9 | // Create Starknet auth plug
10 |
11 | export const auth = betterAuth({
12 | baseURL: env.VITE_BASE_URL ?? "http://localhost:3000",
13 | database: drizzleAdapter(db, {
14 | provider: "pg",
15 | }),
16 |
17 | // https://www.better-auth.com/docs/concepts/session-management#session-caching
18 | session: {
19 | cookieCache: {
20 | enabled: true,
21 | maxAge: 60 * 60, // 60 minutes
22 | },
23 | },
24 |
25 | plugins: [siws({ domain: env.VITE_BASE_URL ?? "http://localhost:3000" })],
26 | });
27 |
--------------------------------------------------------------------------------
/tooling/eslint/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@realms-world/eslint-config",
3 | "private": true,
4 | "version": "0.3.0",
5 | "type": "module",
6 | "exports": {
7 | "./base": "./base.js",
8 | "./vitejsjs": "./vitejs.js",
9 | "./react": "./react.js"
10 | },
11 | "scripts": {
12 | "clean": "git clean -xdf .cache .turbo dist node_modules",
13 | "format": "prettier --check . --ignore-path ../../.gitignore",
14 | "typecheck": "tsc --noEmit"
15 | },
16 | "dependencies": {
17 | "typescript-eslint": "^8.23.0",
18 | "eslint-plugin-import": "^2.31.0",
19 | "eslint-plugin-jsx-a11y": "^6.10.2",
20 | "eslint-plugin-react": "^7.37.4",
21 | "eslint-plugin-react-hooks": "^5.1.0"
22 | },
23 | "devDependencies": {
24 | "eslint": "catalog:",
25 | "prettier": "catalog:",
26 | "typescript": "catalog:"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/queries/execute.ts:
--------------------------------------------------------------------------------
1 | import type { TypedDocumentString } from "@/gql/graphql";
2 |
3 | export async function execute(
4 | query: TypedDocumentString,
5 | ...[variables]: TVariables extends Record ? [] : [TVariables]
6 | ) {
7 | const response = await fetch("https://api.snapshot.box", {
8 | method: "POST",
9 | headers: {
10 | "Content-Type": "application/json",
11 | Accept: "application/graphql-response+json",
12 | },
13 | body: JSON.stringify({
14 | query,
15 | variables,
16 | }),
17 | });
18 |
19 | if (!response.ok) {
20 | throw new Error("Network response was not ok");
21 | }
22 |
23 | const result = (await response.json()) as { data: TResult };
24 | // Extract the data property from the GraphQL response
25 | return result.data;
26 | }
27 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/utils/utils"
4 |
5 | const Input = React.forwardRef>(
6 | ({ className, type, ...props }, ref) => {
7 | return (
8 |
17 | )
18 | }
19 | )
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/not-found.tsx:
--------------------------------------------------------------------------------
1 | import { Link } from "@tanstack/react-router";
2 |
3 | export function NotFound({ children }: { children?: any }) {
4 | return (
5 |
6 |
7 | {children ||
The page you are looking for does not exist.
}
8 |
9 |
10 |
16 |
20 | Start Over
21 |
22 |
23 |
24 | );
25 | }
26 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva } from "class-variance-authority"
6 | import type {VariantProps} from "class-variance-authority";
7 |
8 | import { cn } from "@/utils/utils"
9 |
10 | const labelVariants = cva(
11 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
12 | )
13 |
14 | const Label = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef &
17 | VariantProps
18 | >(({ className, ...props }, ref) => (
19 |
24 | ))
25 | Label.displayName = LabelPrimitive.Root.displayName
26 |
27 | export { Label }
28 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/breadcrumbs.tsx:
--------------------------------------------------------------------------------
1 | import { isMatch, Link, useMatches } from "@tanstack/react-router";
2 |
3 | export const Breadcrumbs = () => {
4 | const matches = useMatches();
5 | if (matches.some((match) => match.status === "pending")) return null;
6 |
7 | const matchesWithCrumbs = matches.filter((match) =>
8 | isMatch(match, "loaderData.crumb")
9 | );
10 |
11 | return (
12 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
3 |
4 | import { cn } from "@/utils/utils"
5 |
6 | const Separator = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(
10 | (
11 | { className, orientation = "horizontal", decorative = true, ...props },
12 | ref
13 | ) => (
14 |
25 | )
26 | )
27 | Separator.displayName = SeparatorPrimitive.Root.displayName
28 |
29 | export { Separator }
30 |
--------------------------------------------------------------------------------
/packages/db/src/client.ts:
--------------------------------------------------------------------------------
1 | import type { NeonQueryFunction } from "@neondatabase/serverless";
2 | import type { NodePgDatabase } from "drizzle-orm/node-postgres";
3 | import { neon, neonConfig } from "@neondatabase/serverless";
4 | import { drizzle } from "drizzle-orm/neon-http";
5 |
6 | import config from "../drizzle.config";
7 | import { env } from "../env";
8 | //import * as authSchema from "./schema/auth";
9 | import * as schema from "./schema";
10 |
11 | if (!env.VERCEL_ENV) {
12 | neonConfig.wsProxy = (/*host*/) => `127.0.0.1/v1`;
13 | neonConfig.useSecureWebSocket = false;
14 | neonConfig.pipelineTLS = false;
15 | neonConfig.pipelineConnect = false;
16 | }
17 |
18 | export const neonSql = neon(
19 | config.dbCredentials.url,
20 | ) satisfies NeonQueryFunction;
21 |
22 | export const db = drizzle(neonSql, { schema: { ...schema } });
23 |
24 | export type Database = NodePgDatabase;
25 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/governance/delegate-list.tsx:
--------------------------------------------------------------------------------
1 | import { getDelegatesQueryOptions } from "@/lib/getDelegates";
2 | import { useSuspenseQuery } from "@tanstack/react-query";
3 |
4 | import { DelegateCard } from "./delegate-card";
5 |
6 | function DelegateList({
7 | searchQuery,
8 | sortMethod,
9 | }: {
10 | sortMethod: "desc" | "random";
11 | searchQuery: string;
12 | }) {
13 | const delegatesQuery = useSuspenseQuery(
14 | getDelegatesQueryOptions({
15 | limit: 200,
16 | cursor: 0,
17 | search: searchQuery.toLowerCase(),
18 | orderBy: sortMethod,
19 | }),
20 | );
21 | return (
22 |
23 | {delegatesQuery.data.items.map((delegate) => (
24 |
25 | ))}
26 |
27 | );
28 | }
29 |
30 | export default DelegateList;
31 |
--------------------------------------------------------------------------------
/apps/account-portal/codegen.ts:
--------------------------------------------------------------------------------
1 | import type { CodegenConfig } from "@graphql-codegen/cli";
2 |
3 | const config: CodegenConfig = {
4 | generates: {
5 | "./src/gql/eternum/": {
6 | documents: ["src/lib/eternum/*.ts", "!src/gql/**/*"],
7 | preset: "client",
8 | schema: "https://api.cartridge.gg/x/seasonpass-mainnet-2/torii/graphql",
9 | config: {
10 | documentMode: "string",
11 | },
12 | presetConfig: {
13 | fragmentMasking: false,
14 | },
15 | plugins: [],
16 | },
17 | "./src/gql/snapshot/": {
18 | documents: ["src/lib/snapshot/*.ts", "!src/gql/**/*"],
19 | preset: "client",
20 | schema: "https://api.snapshot.box/",
21 | config: {
22 | documentMode: "string",
23 | },
24 | presetConfig: {
25 | fragmentMasking: false,
26 | },
27 | plugins: [],
28 | },
29 | },
30 | };
31 |
32 | export default config;
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | import { useToast } from "@/hooks/use-toast"
2 | import {
3 | Toast,
4 | ToastClose,
5 | ToastDescription,
6 | ToastProvider,
7 | ToastTitle,
8 | ToastViewport,
9 | } from "@/components/ui/toast"
10 |
11 | export function Toaster() {
12 | const { toasts } = useToast()
13 |
14 | return (
15 |
16 | {toasts.map(function ({ id, title, description, action, ...props }) {
17 | return (
18 |
19 |
20 | {title && {title}}
21 | {description && (
22 | {description}
23 | )}
24 |
25 | {action}
26 |
27 |
28 | )
29 | })}
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/search-form.tsx:
--------------------------------------------------------------------------------
1 | import { Search } from "lucide-react"
2 |
3 | import { Label } from "@/components/ui/label"
4 | import {
5 | SidebarGroup,
6 | SidebarGroupContent,
7 | SidebarInput,
8 | } from "@/components/ui/sidebar"
9 |
10 | export function SearchForm({ ...props }: React.ComponentProps<"form">) {
11 | return (
12 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/queries/torii.ts:
--------------------------------------------------------------------------------
1 | import type { TypedDocumentString } from "@/gql/graphql";
2 | import { env } from "env";
3 |
4 | export async function executeTorii(
5 | query: TypedDocumentString,
6 | ...[variables]: TVariables extends Record ? [] : [TVariables]
7 | ) {
8 | const response = await fetch(
9 | (env.VITE_TORII_API_URL ??
10 | "https://api.cartridge.gg/x/eternum-empire-mainnet-2/torii") + "/graphql",
11 | {
12 | method: "POST",
13 | headers: {
14 | "Content-Type": "application/json",
15 | Accept: "application/graphql-response+json",
16 | },
17 | body: JSON.stringify({
18 | query,
19 | variables,
20 | }),
21 | },
22 | );
23 |
24 | if (!response.ok) {
25 | throw new Error("Network response was not ok");
26 | }
27 |
28 | const result = (await response.json()) as { data: TResult };
29 | return result.data;
30 | }
31 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/governance/use-delegate-realms.ts:
--------------------------------------------------------------------------------
1 | import { RealmsABI } from "@/abi/L2/Realms";
2 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
3 | import { useContract, useSendTransaction } from "@starknet-react/core";
4 |
5 | import { CollectionAddresses } from "@realms-world/constants";
6 |
7 | export const useDelegateRealms = ({ delegatee }: { delegatee?: string }) => {
8 | const l2RealmsAddress = CollectionAddresses.realms[
9 | SUPPORTED_L2_CHAIN_ID
10 | ] as `0x${string}`;
11 |
12 | const { contract } = useContract({
13 | abi: RealmsABI,
14 | address: l2RealmsAddress,
15 | });
16 |
17 | const {
18 | sendAsync,
19 | data: withdrawHash,
20 | ...writeReturn
21 | } = useSendTransaction({
22 | calls:
23 | contract && delegatee
24 | ? [contract.populate("delegate", [delegatee])]
25 | : undefined,
26 | });
27 |
28 | return {
29 | sendAsync,
30 | withdrawHash,
31 | ...writeReturn,
32 | };
33 | };
34 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/seo.ts:
--------------------------------------------------------------------------------
1 | export const seo = ({
2 | title,
3 | description,
4 | keywords,
5 | image,
6 | }: {
7 | title: string;
8 | description?: string;
9 | image?: string;
10 | keywords?: string;
11 | }) => {
12 | const tags = [
13 | { title },
14 | { name: "description", content: description },
15 | { name: "keywords", content: keywords },
16 | { name: "twitter:title", content: title },
17 | { name: "twitter:description", content: description },
18 | { name: "twitter:creator", content: "@realmsworld" },
19 | { name: "twitter:site", content: "@realmsworld" },
20 | { name: "og:type", content: "website" },
21 | { name: "og:title", content: title },
22 | { name: "og:description", content: description },
23 | ...(image
24 | ? [
25 | { name: "twitter:image", content: image },
26 | { name: "twitter:card", content: "summary_large_image" },
27 | { name: "og:image", content: image },
28 | ]
29 | : []),
30 | ];
31 |
32 | return tags;
33 | };
34 |
--------------------------------------------------------------------------------
/packages/constants/src/marketplace.ts:
--------------------------------------------------------------------------------
1 | import { ChainId } from "./chains";
2 |
3 | export const marketplaceCollections = {
4 | realms: {
5 | id: {
6 | [ChainId.SN_MAIN]: 2,
7 | [ChainId.SN_SEPOLIA]: 4,
8 | },
9 | name: "Realms",
10 | image: "/collections/realms.png",
11 | },
12 | /*"season-passes": {
13 | address: CollectionAddresses,
14 | id: COLLECTION_IDS[currentNetwork]["season-passes"],
15 | name: "Season 1 Pass",
16 | image: "/collections/season-1-pass.png",
17 | },*/
18 | } as const;
19 |
20 | export function getCollectionByAddress(
21 | address: string,
22 | ): (typeof marketplaceCollections)[keyof typeof marketplaceCollections] | null {
23 | const collection = Object.entries(marketplaceCollections).find(
24 | ([_, data]) => {
25 | return (
26 | trimAddress(data.address)?.toLowerCase() ===
27 | trimAddress(address)?.toLowerCase()
28 | );
29 | },
30 | );
31 | return collection ? collection[1] : null; // Default to season passes if not found
32 | }
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/time.ts:
--------------------------------------------------------------------------------
1 | import {
2 | differenceInSeconds,
3 | formatDistanceToNow,
4 | fromUnixTime,
5 | } from "date-fns";
6 |
7 | export const WEEK_IN_SECONDS = 7 * 24 * 60 * 60; // 1 week in seconds
8 | export const YEAR_IN_SECONDS = 365 * 24 * 60 * 60; // 1 year in seconds
9 |
10 | export function toTime(time: number | string | undefined) {
11 | return Number(time ?? 0);
12 | }
13 |
14 | export function getTimeUntil(time?: number): number {
15 | if (!time) return 0;
16 | const duration = differenceInSeconds(fromUnixTime(time), new Date());
17 | return Math.max(duration, 0);
18 | }
19 |
20 | export function toWeeks(time?: number): number {
21 | return Math.floor(toTime(time) / WEEK_IN_SECONDS);
22 | }
23 |
24 | export function roundToWeek(time: number) {
25 | return Math.floor(toTime(time) / WEEK_IN_SECONDS) * WEEK_IN_SECONDS;
26 | }
27 | export function formatLockEndTime(timestamp: number): string {
28 | const date = new Date(timestamp * 1000);
29 | return `${date.toLocaleDateString()} (${formatDistanceToNow(date, { addSuffix: true })})`;
30 | }
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "core",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "engines": {
7 | "node": ">=20.16.0"
8 | },
9 | "packageManager": "pnpm@9.15.2",
10 | "scripts": {
11 | "dev": "pnpm --filter @realms-world/account-portal dev",
12 | "apibara:start": "pnpm --filter @realms-world/apibara start",
13 | "apibara:build": "pnpm --filter @realms-world/apibara build",
14 | "db:push": "pnpm --filter @realms-world/db with-env drizzle-kit push",
15 | "db:pull": "pnpm --filter @realms-world/db with-env drizzle-kit pull",
16 | "lint": "pnpm --recursive run lint"
17 | },
18 | "devDependencies": {
19 | "@eslint/js": "^9.19.0",
20 | "@realms-world/eslint-config": "workspace:*",
21 | "eslint": "^9.19.0",
22 | "eslint-plugin-react-hooks": "^5.0.0",
23 | "eslint-plugin-react-refresh": "^0.4.16",
24 | "typescript-eslint": "^8.23.0"
25 | },
26 | "prettier": "@realms-world/prettier-config",
27 | "pnpm": {
28 | "overrides": {
29 | "@noble/ciphers": "1.3.0",
30 | "starknet": "^8.1.2"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/progress.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cn } from "@/utils/utils";
3 | import * as ProgressPrimitive from "@radix-ui/react-progress";
4 |
5 | interface ProgressProps
6 | extends React.ComponentPropsWithoutRef {
7 | indicatorColor?: string;
8 | }
9 |
10 | const Progress = React.forwardRef<
11 | React.ElementRef,
12 | ProgressProps
13 | >(({ className, value, indicatorColor, ...props }, ref) => (
14 |
22 |
29 |
30 | ));
31 | Progress.displayName = ProgressPrimitive.Root.displayName;
32 |
33 | export { Progress };
34 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/icons/ethereum.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/token/L1/useERC721SetApprovalForAll.tsx:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { ERC721_ABI } from "@/abi/L1/ERC721";
3 | import { useWriteContract } from "wagmi";
4 |
5 | const FUNCTION = "setApprovalForAll";
6 |
7 | export function useERC721SetApprovalForAll({
8 | onSuccess,
9 | }: {
10 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
11 | onSuccess?: (data: any) => void;
12 | }) {
13 | const { writeContractAsync, error, ...writeReturn } = useWriteContract({
14 | mutation: { onSuccess: (data) => onSuccess?.(data) },
15 | });
16 |
17 | // if (!l2Address) throw new Error("Missing L2 Address");
18 |
19 | const writeAsync = useCallback(
20 | async ({
21 | contractAddress,
22 | operator,
23 | }: {
24 | contractAddress: `0x${string}`;
25 | operator: `0x${string}`;
26 | }) => {
27 | return await writeContractAsync({
28 | address: contractAddress,
29 | abi: ERC721_ABI,
30 | functionName: FUNCTION,
31 | args: [operator, true],
32 | });
33 | },
34 | [writeContractAsync],
35 | );
36 | return { writeAsync, error, ...writeReturn };
37 | }
38 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/getLordsPrice.ts:
--------------------------------------------------------------------------------
1 | import { createServerFn } from "@tanstack/react-start";
2 | import { z } from "zod";
3 |
4 | export interface EthplorerToken {
5 | address: string;
6 | decimals: string;
7 | symbol: string;
8 | name: string;
9 | price?: {
10 | rate: string;
11 | diff7d: string;
12 | marketCapUsd: string;
13 | volume24h: string;
14 | };
15 | }
16 |
17 | /* -------------------------------------------------------------------------- */
18 | /* getLordsInfo Endpoint */
19 | /* -------------------------------------------------------------------------- */
20 |
21 | const GetLordsInfoInput = z.object({}).optional();
22 |
23 | export const getLordsInfo = createServerFn({ method: "GET" })
24 | .validator((input: unknown) => GetLordsInfoInput.parse(input))
25 | .handler(async (_ctx) => {
26 | const response = await fetch(
27 | `https://api.ethplorer.io/getTokenInfo/0x686f2404e77ab0d9070a46cdfb0b7fecdd2318b0?apiKey=${import.meta.env.VITE_ETHPLORER_APIKEY}&chainId=1`,
28 | );
29 | const data = (await response.json()) as EthplorerToken;
30 |
31 | return data;
32 | });
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/utils/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as SliderPrimitive from "@radix-ui/react-slider"
3 |
4 | import { cn } from "@/utils/utils"
5 |
6 | const Slider = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 |
19 |
20 |
21 |
22 |
23 | ))
24 | Slider.displayName = SliderPrimitive.Root.displayName
25 |
26 | export { Slider }
27 |
--------------------------------------------------------------------------------
/apps/account-portal/src/router.tsx:
--------------------------------------------------------------------------------
1 | import { DefaultCatchBoundary } from "@/components/layout/default-catch-boundary";
2 | import { NotFound } from "@/components/layout/not-found";
3 | import { QueryClient } from "@tanstack/react-query";
4 | import { createRouter as createTanStackRouter } from "@tanstack/react-router";
5 | import { routerWithQueryClient } from "@tanstack/react-router-with-query";
6 |
7 | import { routeTree } from "./routeTree.gen";
8 |
9 | export function createRouter() {
10 | const queryClient = new QueryClient();
11 |
12 | return routerWithQueryClient(
13 | createTanStackRouter({
14 | routeTree,
15 | context: {
16 | queryClient,
17 | session: {
18 | address: "0x123",
19 | chain: "mainnet",
20 | provider: "starknet",
21 | },
22 | },
23 | defaultPreload: "intent",
24 | defaultErrorComponent: DefaultCatchBoundary,
25 | defaultNotFoundComponent: () => ,
26 | //scrollRestoration: true,
27 | }),
28 | queryClient,
29 | );
30 | }
31 |
32 | declare module "@tanstack/react-router" {
33 | interface Register {
34 | router: ReturnType;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/packages/db/src/schema/dune.ts:
--------------------------------------------------------------------------------
1 | import {
2 | numeric,
3 | pgTable,
4 | primaryKey,
5 | text,
6 | timestamp,
7 | } from "drizzle-orm/pg-core";
8 |
9 | export const velords_burns = pgTable(
10 | "dune_velords_burns",
11 | {
12 | //_id: text("_id"),
13 | source: text("source").notNull(),
14 | amount: numeric("amount").notNull(),
15 | transaction_hash: text("transaction_hash").notNull(),
16 | //block_time: timestamp("block_time").notNull(),
17 | epoch: timestamp("epoch", {
18 | mode: "date",
19 | precision: 3,
20 | }).notNull(),
21 | epoch_total_amount: numeric("epoch_total_amount").notNull(),
22 | sender_epoch_total_amount: numeric("sender_epoch_total_amount").notNull(),
23 | },
24 | (t) => [primaryKey({ columns: [t.amount, t.transaction_hash] })],
25 | );
26 |
27 | export const velords_supply = pgTable("dune_velords_supply", {
28 | //_id: text("_id"),
29 | old_supply: text("old_supply").notNull(),
30 | new_supply: numeric("new_supply").notNull(),
31 | transaction_hash: text("transaction_hash").notNull().primaryKey(),
32 | block_time: timestamp("block_time", {
33 | mode: "date",
34 | precision: 3,
35 | }).notNull(),
36 | });
37 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/index.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense } from "react";
2 | import { EthereumConnect } from "@/components/layout/ethereum-connect";
3 | import { LoginCard } from "@/components/layout/login-card";
4 | import { Homepage } from "@/components/modules/homepage/homepage";
5 | import { HomepageSkeleton } from "@/components/modules/homepage/homepage-skeleteon";
6 | import { useAccount } from "@starknet-react/core";
7 | import { createFileRoute } from "@tanstack/react-router";
8 |
9 | export const Route = createFileRoute("/")({
10 | component: IndexComponent,
11 | });
12 |
13 | function IndexComponent() {
14 | const { address } = useAccount();
15 | if (!address) {
16 | return ;
17 | }
18 | return (
19 |
20 | {/* Dashboard Statistics */}
21 |
22 |
23 |
Dashboard
24 |
25 |
26 |
}>
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/governance/proposal-user-vote-badge.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge";
2 | import { CheckCircle2, MinusCircle, XCircle } from "lucide-react";
3 |
4 | export const ProposalUserVoteBadge = ({ choice }: { choice: 1 | 2 | 3 }) => {
5 | return (
6 | <>
7 | {choice === 1 && (
8 |
12 | Yes
13 |
14 | )}
15 |
16 | {choice === 2 && (
17 |
21 | No
22 |
23 | )}
24 |
25 | {choice === 3 && (
26 |
30 | Abstain
31 |
32 | )}
33 | >
34 | );
35 | };
36 |
--------------------------------------------------------------------------------
/packages/constants/src/bridge-addresses.ts:
--------------------------------------------------------------------------------
1 | import { ChainId } from "./chains";
2 |
3 | export const REALMS_BRIDGE_ADDRESS: Record = {
4 | [ChainId.MAINNET]: "0xA425Fa1678f7A5DaFe775bEa3F225c4129cdbD25",
5 | [ChainId.SEPOLIA]: "0x345Eaf46F42228670489B47764b0Bd21f2141bd1",
6 | [ChainId.SN_MAIN]:
7 | "0x013ae4e41ff29ee8311c84b024ac59a0c13f73fa1ba0cea02fbbf7880ec4835a",
8 | [ChainId.SN_SEPOLIA]:
9 | "0x0467f6b080db9734b8b0a2ccb7fd020914e47f2f62aa668f56c4124946e4eb70",
10 | };
11 |
12 | export const LORDS_BRIDGE_ADDRESS: Record = {
13 | [ChainId.MAINNET]: "0x023A2aAc5d0fa69E3243994672822BA43E34E5C9",
14 | [ChainId.SEPOLIA]: "0x6406465603487eE0Ad7A813b2bB6B0DFfB8f6aa7",
15 | [ChainId.SN_MAIN]:
16 | "0x7c76a71952ce3acd1f953fd2a3fda8564408b821ff367041c89f44526076633",
17 | [ChainId.SN_SEPOLIA]:
18 | "0x042331a29c53f6084f08964cbd83b94c1a141e6d14009052d55b03793b21d5b3",
19 | };
20 |
21 | export const STARKNET_MESSAGING =
22 | {
23 | [ChainId.MAINNET]: "0xc662c410C0ECf747543f5bA90660f6ABeBD9C8c4",
24 | [ChainId.SEPOLIA]: "0xe2bb56ee936fd6433dc0f6e7e3b8365c906aa057",
25 | [ChainId.SN_MAIN]: "",
26 | [ChainId.SN_SEPOLIA]: ""
27 | }
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-wrong-network.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import { mainnet, sepolia } from "@starknet-react/chains";
3 | import { useAccount, useNetwork } from "@starknet-react/core";
4 | import { env } from "env";
5 | import { Chain } from "starknet"
6 |
7 | const useIsWrongNetwork = () => {
8 | const { chain } = useNetwork();
9 | const { account, chainId } = useAccount();
10 | const [isWrongNetwork, setIsWrongNetwork] = useState(false);
11 |
12 | function bigintToStringHex(element: bigint | undefined): string {
13 | if (element === undefined) return "";
14 |
15 | const hex = element.toString(16);
16 | return element < 0 ? `-0x${hex.slice(1)}` : `0x${hex}`;
17 | }
18 |
19 | useEffect(() => {
20 | if (!account) {
21 | setIsWrongNetwork(false);
22 | return;
23 | }
24 | setIsWrongNetwork(
25 | env.VITE_PUBLIC_CHAIN === "sepolia"
26 | ? bigintToStringHex(chainId) === bigintToStringHex(mainnet.id)
27 | : bigintToStringHex(chainId) === bigintToStringHex(sepolia.id),
28 | );
29 | }, [account, chainId]);
30 |
31 | return {
32 | isWrongNetwork,
33 | setIsWrongNetwork,
34 | };
35 | };
36 |
37 | export default useIsWrongNetwork;
38 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/realms/bridge-tx-chains.tsx:
--------------------------------------------------------------------------------
1 | import EthereumIcon from "@/components/icons/ethereum.svg?react";
2 | import StarknetIcon from "@/components/icons/starknet.svg?react";
3 | import { ChainId } from "@realms-world/constants";
4 | import { ArrowRight } from "lucide-react";
5 | export const TransactionChains: React.FC<{ fromChain: string | number }> = ({
6 | fromChain,
7 | }) => {
8 | const isFromEthereum = [ChainId.MAINNET, ChainId.SEPOLIA].includes(
9 | Number(fromChain) as ChainId
10 | );
11 |
12 | const sourceChain = isFromEthereum
13 | ? { icon: , label: "Ethereum" }
14 | : { icon: , label: "Starknet" };
15 |
16 | const destinationChain = isFromEthereum
17 | ? { icon: , label: "Starknet" }
18 | : { icon: , label: "Ethereum" };
19 |
20 | return (
21 | <>
22 |
23 | {sourceChain.icon} {sourceChain.label}
24 |
25 |
26 |
27 | {destinationChain.icon} {destinationChain.label}
28 |
29 | >
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/apps/account-portal/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | const envSchema = z.object({
4 | // Version and chain info
5 | VITE_PUBLIC_CHAIN: z.enum(["sepolia", "mainnet", "testnet", "local"]), // Add other chains as needed
6 | VITE_BASE_URL: z.string().url().optional(),
7 | VITE_PUBLIC_IMAGE_CDN_URL: z.string().url().optional(),
8 | VITE_PUBLIC_IMAGE_PROXY_URL: z.string().url().optional(),
9 | VITE_PUBLIC_IPFS_GATEWAY: z.string().url().optional(),
10 | VITE_TORII_API_URL: z.string().url().optional(),
11 | VITE_PUBLIC_SLOT: z.string(),
12 | VITE_PUBLIC_NODE_URL: z.string().url().optional(),
13 |
14 | VITE_RESERVOIR_API_KEY: z.string().optional(),
15 | VITE_ALCHEMY_API_KEY: z.string(),
16 | VITE_DUNE_API_KEY: z.string().optional(),
17 | VITE_ETHPLORER_APIKEY: z.string().optional()
18 | });
19 |
20 | let env: z.infer;
21 | try {
22 | env = envSchema.parse(import.meta.env);
23 | } catch (error) {
24 | if (error instanceof z.ZodError) {
25 | console.error(
26 | "❌ Invalid environment variables:",
27 | JSON.stringify(error.errors, null, 2),
28 | );
29 | }
30 | throw new Error("Invalid environment variables");
31 | }
32 |
33 | export { env };
34 |
35 | // Type for your validated env
36 | export type Env = z.infer;
37 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/token/L2/useERC721Approval.ts:
--------------------------------------------------------------------------------
1 | import type { Call } from "starknet";
2 | import { useMemo } from "react";
3 | import { ERC721 } from "@/abi/L2/ERC721";
4 | import {
5 | useContract,
6 | useSendTransaction as useL2ContractWrite,
7 | } from "@starknet-react/core";
8 | import { useAccount as useL1Account } from "wagmi";
9 |
10 | export const useERC721Approval = ({
11 | contractAddress,
12 | operator,
13 | removeApproval,
14 | }: {
15 | contractAddress: string;
16 | operator: string;
17 | removeApproval?: boolean;
18 | }) => {
19 | const { address: addressL1 } = useL1Account();
20 |
21 | const { contract } = useContract({
22 | abi: ERC721,
23 | address: contractAddress as `0x${string}`,
24 | });
25 |
26 | const calls: Call[] = useMemo(() => {
27 | if (!contractAddress || !operator || !addressL1 || !contract) return [];
28 | return [
29 | contract.populate("set_approval_for_all", [
30 | operator,
31 | removeApproval ? false : true,
32 | ]),
33 | ];
34 | }, [contractAddress, operator, addressL1, contract, removeApproval]);
35 |
36 | const { sendAsync, ...writeReturn } = useL2ContractWrite({ calls });
37 |
38 | return {
39 | calls,
40 | sendAsync,
41 | ...writeReturn,
42 | };
43 | };
44 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/utils/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/bridge/useWriteInitiateWithdrawRealms.ts:
--------------------------------------------------------------------------------
1 | import type { Call } from "starknet";
2 | import { useMemo } from "react";
3 | import { RealmsBridge } from "@/abi/L2/RealmsBridge";
4 | import { useContract, useSendTransaction } from "@starknet-react/core";
5 | import { useAccount as useL1Account } from "wagmi";
6 |
7 | import { REALMS_BRIDGE_ADDRESS } from "@realms-world/constants";
8 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
9 |
10 | export const useWriteInitiateWithdrawRealms = ({
11 | selectedTokenIds,
12 | }: {
13 | selectedTokenIds: string[];
14 | }) => {
15 | const { address: addressL1 } = useL1Account();
16 |
17 | const { contract } = useContract({
18 | abi: RealmsBridge,
19 | address: REALMS_BRIDGE_ADDRESS[SUPPORTED_L2_CHAIN_ID] as `0x${string}`,
20 | });
21 |
22 | const calls: Call[] = useMemo(() => {
23 | if (!selectedTokenIds.length || !addressL1 || !contract) return [];
24 |
25 | return [
26 | contract.populate("deposit_tokens", [
27 | Date.now(),
28 | addressL1,
29 | selectedTokenIds.map((tokenId) => BigInt(tokenId)),
30 | ]),
31 | ];
32 | }, [selectedTokenIds, addressL1, contract]);
33 |
34 | const sendTx = useSendTransaction({ calls });
35 |
36 | return {
37 | calls,
38 | ...sendTx,
39 | };
40 | };
41 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-starknet-wallet.ts:
--------------------------------------------------------------------------------
1 | import type { Connector, StarknetkitConnector } from "starknetkit";
2 | import { useEffect, useState } from "react";
3 | import { getLastConnector } from "@/utils/connectWallet";
4 | import { useAccount, useConnect } from "@starknet-react/core";
5 | import { useStarknetkitConnectModal } from "starknetkit";
6 |
7 | export function useStarknetWallet() {
8 | const { isConnected } = useAccount();
9 | const { connectAsync, connectors } = useConnect();
10 | const [lastConnector, setLastConnector] = useState(null);
11 |
12 | const { starknetkitConnectModal } = useStarknetkitConnectModal({
13 | connectors: connectors as StarknetkitConnector[],
14 | modalMode: "alwaysAsk",
15 | });
16 |
17 | useEffect(() => {
18 | setLastConnector(getLastConnector(connectors));
19 | }, [isConnected, connectors]);
20 |
21 | async function openStarknetKitModal() {
22 | const { connector } = await starknetkitConnectModal();
23 | if (!connector) return;
24 | await connectAsync({ connector });
25 | }
26 |
27 | async function connectWallet(connector: Connector) {
28 | await connectAsync({ connector });
29 | localStorage.setItem("connectedWallet", connector.id);
30 | }
31 |
32 | return { lastConnector, openStarknetKitModal, connectWallet };
33 | }
34 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/getRealmsLordsClaims.ts:
--------------------------------------------------------------------------------
1 | import { queryOptions } from "@tanstack/react-query";
2 | import { createServerFn } from "@tanstack/react-start";
3 | import { z } from "zod";
4 |
5 | import { desc, eq, realmsLordsClaims } from "@realms-world/db";
6 | import { db } from "@realms-world/db/client";
7 |
8 | /* -------------------------------------------------------------------------- */
9 | /* getRealmsLordsClaims Endpoint */
10 | /* -------------------------------------------------------------------------- */
11 |
12 | const GetRealmsLordsClaimsInput = z.object({
13 | address: z.string(),
14 | });
15 |
16 | export const getRealmsLordsClaims = createServerFn({ method: "GET" })
17 | .validator((input: unknown) => GetRealmsLordsClaimsInput.parse(input))
18 | .handler(async (ctx) => {
19 | const { address } = ctx.data;
20 | return db.query.realmsLordsClaims.findMany({
21 | where: eq(realmsLordsClaims.recipient, address),
22 | orderBy: desc(realmsLordsClaims.timestamp),
23 | });
24 | });
25 |
26 | export const getRealmsLordsClaimsQueryOptions = (
27 | input: z.infer,
28 | ) =>
29 | queryOptions({
30 | queryKey: ["getRealmsLordsClaims", input.address],
31 | queryFn: () => getRealmsLordsClaims({ data: input }),
32 | });
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva } from "class-variance-authority"
3 | import type {VariantProps} from "class-variance-authority";
4 |
5 | import { cn } from "@/utils/utils"
6 |
7 | const badgeVariants = cva(
8 | "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "border-transparent bg-primary text-primary-foreground shadow-sm hover:bg-primary/80",
14 | secondary:
15 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
16 | destructive:
17 | "border-transparent bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/80",
18 | outline: "text-foreground",
19 | },
20 | },
21 | defaultVariants: {
22 | variant: "default",
23 | },
24 | }
25 | )
26 |
27 | export interface BadgeProps
28 | extends React.HTMLAttributes,
29 | VariantProps {}
30 |
31 | function Badge({ className, variant, ...props }: BadgeProps) {
32 | return (
33 |
34 | )
35 | }
36 |
37 | export { Badge, badgeVariants }
38 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
5 |
6 | import { cn } from "@/utils/utils"
7 |
8 | const TooltipProvider = TooltipPrimitive.Provider
9 |
10 | const Tooltip = TooltipPrimitive.Root
11 |
12 | const TooltipTrigger = TooltipPrimitive.Trigger
13 |
14 | const TooltipContent = React.forwardRef<
15 | React.ElementRef,
16 | React.ComponentPropsWithoutRef
17 | >(({ className, sideOffset = 4, ...props }, ref) => (
18 |
19 |
28 |
29 | ))
30 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
31 |
32 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/mode-toggle.tsx:
--------------------------------------------------------------------------------
1 | import { Moon, Sun } from "lucide-react"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from "@/components/ui/dropdown-menu"
10 | import { useTheme } from "@/providers/theme"
11 |
12 | export function ModeToggle() {
13 | const { setTheme } = useTheme()
14 |
15 | return (
16 |
17 |
18 |
23 |
24 |
25 | setTheme("light")}>
26 | Light
27 |
28 | setTheme("dark")}>
29 | Dark
30 |
31 | setTheme("system")}>
32 | System
33 |
34 |
35 |
36 | )
37 | }
--------------------------------------------------------------------------------
/apps/account-portal/vite.config.ts:
--------------------------------------------------------------------------------
1 | import tailwindcss from "@tailwindcss/vite";
2 | import { tanstackStart } from "@tanstack/react-start/plugin/vite";
3 | import viteReact from "@vitejs/plugin-react";
4 | import { defineConfig } from "vite";
5 | import svgr from "vite-plugin-svgr";
6 | import tsconfigPaths from "vite-tsconfig-paths";
7 |
8 | export default defineConfig({
9 | server: {
10 | port: 3000,
11 | },
12 |
13 | plugins: [
14 | tsconfigPaths({
15 | projects: ["./tsconfig.json"],
16 | }),
17 | tanstackStart({
18 | target: "vercel",
19 | customViteReactPlugin: true,
20 | }),
21 | viteReact(),
22 | svgr({
23 | include: "**/*.svg?react",
24 | }),
25 | tailwindcss(),
26 | ],
27 | build: {
28 | chunkSizeWarningLimit: 1000,
29 | target: "esnext",
30 | },
31 | optimizeDeps: {
32 | include: [
33 | "@realms-world/db",
34 | "@reown/appkit",
35 | "@reown/appkit-adapter-wagmi",
36 | "viem",
37 | "wagmi",
38 | "starknet",
39 | "@starknet-react/core",
40 | "zod",
41 | ],
42 | },
43 | ssr: {
44 | noExternal: ["@realms-world/db", "@realms-world/constants", "zod"],
45 | external: [
46 | "wagmi",
47 | "@reown/appkit/react",
48 | "@reown/appkit",
49 | "@reown/appkit-adapter-wagmi",
50 | "@starknet-io/starknet-types-08",
51 | ],
52 | },
53 | });
54 |
--------------------------------------------------------------------------------
/packages/db/src/poolClient.ts:
--------------------------------------------------------------------------------
1 | import type { NeonQueryFunction } from "@neondatabase/serverless";
2 | import { neon, neonConfig, Pool } from "@neondatabase/serverless";
3 | import { drizzle } from "drizzle-orm/neon-serverless";
4 | import ws from "ws";
5 |
6 | import config from "../drizzle.config";
7 | // Import only the specific schema tables that are actually used
8 | import {
9 | bridgeEventTypeEnum,
10 | realmsBridgeEvents,
11 | realmsBridgeEventsRelations,
12 | realmsBridgeRequests,
13 | realmsBridgeRequestsRelations,
14 | realmsLordsClaims,
15 | } from "./schema/bridge";
16 |
17 | neonConfig.webSocketConstructor = ws;
18 |
19 | export const neonSql = neon(
20 | config.dbCredentials.url,
21 | ) satisfies NeonQueryFunction;
22 |
23 | // Optimize pool configuration for Vercel
24 | const pool = new Pool({
25 | connectionString: process.env.DATABASE_URL,
26 | max: 10, // Limit max connections
27 | idleTimeoutMillis: 30000, // Close idle connections after 30s
28 | connectionTimeoutMillis: 2000, // Connection timeout
29 | });
30 |
31 | // Create schema object with only the tables we actually use
32 | const schema = {
33 | realmsBridgeRequests,
34 | realmsBridgeEvents,
35 | realmsLordsClaims,
36 | bridgeEventTypeEnum,
37 | // Include relations
38 | realmsBridgeRequestsRelations,
39 | realmsBridgeEventsRelations,
40 | };
41 |
42 | export const db = drizzle(pool, { schema });
43 |
--------------------------------------------------------------------------------
/tooling/prettier/index.js:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 |
3 | /** @typedef {import("prettier").Config} PrettierConfig */
4 | /** @typedef {import("prettier-plugin-tailwindcss").PluginOptions} TailwindConfig */
5 | /** @typedef {import("@ianvs/prettier-plugin-sort-imports").PluginConfig} SortImportsConfig */
6 |
7 | /** @type { PrettierConfig | SortImportsConfig | TailwindConfig } */
8 | const config = {
9 | plugins: [
10 | "@ianvs/prettier-plugin-sort-imports",
11 | "prettier-plugin-tailwindcss",
12 | ],
13 | /*tailwindConfig: fileURLToPath(
14 | new URL("../../tooling/tailwind/web.ts", import.meta.url),
15 | ),*/
16 | tailwindFunctions: ["cn", "cva"],
17 | importOrder: [
18 | "",
19 | "^(react/(.*)$)|^(react$)|^(react-native(.*)$)",
20 | "^(next/(.*)$)|^(next$)",
21 | "",
22 | "",
23 | "^@acme",
24 | "^@realms-world/(.*)$",
25 | "",
26 | "^[.|..|~]",
27 | "^~/",
28 | "^[../]",
29 | "^[./]",
30 | ],
31 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
32 | importOrderTypeScriptVersion: "4.4.0",
33 | overrides: [
34 | {
35 | files: "*.json.hbs",
36 | options: {
37 | parser: "json",
38 | },
39 | },
40 | {
41 | files: "*.js.hbs",
42 | options: {
43 | parser: "babel",
44 | },
45 | },
46 | ],
47 | };
48 |
49 | export default config;
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as PopoverPrimitive from "@radix-ui/react-popover"
3 | import { cn } from "@/utils/utils"
4 |
5 |
6 | const Popover = PopoverPrimitive.Root
7 |
8 | const PopoverTrigger = PopoverPrimitive.Trigger
9 |
10 | const PopoverAnchor = PopoverPrimitive.Anchor
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
32 |
--------------------------------------------------------------------------------
/packages/db/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@realms-world/db",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "exports": {
7 | ".": {
8 | "types": "./dist/index.d.ts",
9 | "default": "./src/index.ts"
10 | },
11 | "./client": {
12 | "types": "./dist/client.d.ts",
13 | "default": "./src/client.ts"
14 | },
15 | "./poolClient": {
16 | "types": "./dist/poolClient.d.ts",
17 | "default": "./src/poolClient.ts"
18 | },
19 | "./schema": {
20 | "types": "./dist/schema.d.ts",
21 | "default": "./src/schema/index.ts"
22 | }
23 | },
24 | "license": "MIT",
25 | "scripts": {
26 | "build": "tsc --noEmit",
27 | "dev": "tsc --watch",
28 | "clean": "git clean -xdf .cache .turbo dist node_modules",
29 | "lint": "eslint",
30 | "format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
31 | "push": "pnpm with-env drizzle-kit push",
32 | "studio": "pnpm with-env drizzle-kit studio",
33 | "with-env": "dotenv -e ../../.env --"
34 | },
35 | "dependencies": {
36 | "@neondatabase/serverless": "1.0.1",
37 | "drizzle-orm": "catalog:",
38 | "drizzle-zod": "^0.8.2",
39 | "ws": "^8.18.0",
40 | "zod": "catalog:"
41 | },
42 | "devDependencies": {
43 | "@types/node": "^22.13.1",
44 | "@types/ws": "^8.5.14",
45 | "dotenv-cli": "catalog:",
46 | "drizzle-kit": "catalog:",
47 | "pg": "^8.13.1",
48 | "postgres-range": "^1.1.4",
49 | "typescript": "catalog:"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/packages/apibara/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@realms-world/apibara",
3 | "version": "1.0.0",
4 | "description": "",
5 | "keywords": [],
6 | "author": "",
7 | "license": "ISC",
8 | "type": "module",
9 | "scripts": {
10 | "build": "apibara build",
11 | "dev": "pnpm with-env apibara dev",
12 | "start": "pnpm with-env apibara start",
13 | "lint": "eslint",
14 | "lint:fix": "pnpm lint --write",
15 | "test": "vitest",
16 | "drizzle:generate": "drizzle-kit generate",
17 | "drizzle:migrate": "drizzle-kit migrate",
18 | "drizzle:push": "drizzle-kit push",
19 | "with-env": "dotenv -e ../../.env --"
20 | },
21 | "devDependencies": {
22 | "@types/node": "^20.5.2",
23 | "@types/pg": "^8.11.10",
24 | "dotenv-cli": "catalog:",
25 | "drizzle-kit": "catalog:",
26 | "rollup-plugin-esbuild": "^6.2.1",
27 | "typescript": "catalog:",
28 | "vite-tsconfig-paths": "^5.1.4",
29 | "vitest": "^1.6.0"
30 | },
31 | "dependencies": {
32 | "@apibara/evm": "2.1.0-beta.38",
33 | "@apibara/indexer": "2.1.0-beta.38",
34 | "@apibara/plugin-drizzle": "2.1.0-beta.38",
35 | "@apibara/protocol": "2.1.0-beta.38",
36 | "@apibara/starknet": "2.1.0-beta.38",
37 | "@electric-sql/pglite": "^0.2.17",
38 | "@realms-world/constants": "workspace:*",
39 | "@realms-world/db": "workspace:*",
40 | "apibara": "2.1.0-beta.38",
41 | "drizzle-orm": "catalog:",
42 | "pg": "^8.13.1",
43 | "starknet": "catalog:",
44 | "viem": "catalog:",
45 | "zod": "catalog:"
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/bridge/useWriteDepositRealms.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from "react";
2 | import { StarknetBridgeRealms as L1_REALMS_BRIDGE_ABI } from "@/abi/L1/StarknetBridgeRealms";
3 | import { SUPPORTED_L1_CHAIN_ID } from "@/utils/utils";
4 | import { parseGwei } from "viem";
5 | import { useWriteContract } from "wagmi";
6 |
7 | import { REALMS_BRIDGE_ADDRESS } from "@realms-world/constants";
8 |
9 | const FUNCTION = "depositTokens";
10 |
11 | export function useWriteDepositRealms({
12 | onSuccess,
13 | }: {
14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
15 | onSuccess?: (data: any) => void;
16 | }) {
17 | const { writeContractAsync, error, ...writeReturn } = useWriteContract({
18 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
19 | mutation: { onSuccess: (data: any) => onSuccess?.(data) },
20 | });
21 |
22 | const writeAsync = useCallback(
23 | async ({
24 | tokenIds,
25 | l2Address,
26 | }: {
27 | tokenIds: bigint[];
28 | l2Address: string;
29 | }) => {
30 | if (!l2Address) throw new Error("Missing L2 Address");
31 |
32 | return await writeContractAsync({
33 | address: REALMS_BRIDGE_ADDRESS[SUPPORTED_L1_CHAIN_ID] as `0x${string}`,
34 | abi: L1_REALMS_BRIDGE_ABI,
35 | functionName: FUNCTION,
36 | args: [BigInt(Date.now()), BigInt(l2Address), tokenIds],
37 | value: parseGwei((40000 * tokenIds.length).toString()),
38 | });
39 | },
40 | [writeContractAsync],
41 | );
42 | return { writeAsync, error, ...writeReturn };
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | [](https://discord.gg/realmsworld)
9 | [](https://opensource.org/licenses/MIT)
10 |
11 | # Realms.World Account Portal
12 |
13 | ### The account potal
14 |
15 | [Realms.World](https://realms.world) is the central hub for the Realms Autonomous World, offering a comprehensive marketplace NFTs on Ethereum and Starknet. Features include:
16 |
17 | - Realms Bridge (Ethereum <> Starknet)
18 | - Realms rewards and VeLords functionality
19 | - Governance (delegation and voting)
20 |
21 | ### Contributing
22 |
23 | We welcome contributions from the community to help improve Realms.World.
24 |
25 | ## Setup
26 |
27 | 1. Copy `.env.example` to `.env` in the root and update the variables
28 | 2. `pnpm i`
29 | 3. `pnpm dev` for running the dash
30 |
31 | We will review your contribution and provide feedback. Once your changes have been approved, they will be merged into the main branch.
32 |
33 | ## License
34 |
35 | Realms.World is an open-source project released under the MIT License. This license allows you to freely use, modify, and distribute the code, as long as you include the original copyright and permission notice in any copy of the software or substantial portions of it
36 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/governance/proposal-results.tsx:
--------------------------------------------------------------------------------
1 | import type { Proposal } from "@/gql/graphql";
2 | import { Badge } from "@/components/ui/badge";
3 | import { CheckCircle2, MinusCircle, XCircle } from "lucide-react";
4 |
5 | export const ProposalResults = ({ proposal }: { proposal: Proposal }) => {
6 | return (
7 |
8 |
12 |
13 | Yes:
14 |
15 | {proposal.scores_1 || 0}
16 |
17 |
18 |
22 |
23 | No:
24 |
25 | {proposal.scores_2 || 0}
26 |
27 |
31 |
32 | Abstain:
33 |
34 | {proposal.scores_3 || 0}
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/read-more.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | interface ReadMoreProps {
4 | id: string;
5 | text: string;
6 | amountOfWords?: number;
7 | }
8 |
9 | export const ReadMore = ({ id, text, amountOfWords = 24 }: ReadMoreProps) => {
10 | const [isExpanded, setIsExpanded] = useState(false);
11 | const splittedText = text.split(" ");
12 | const itCanOverflow = splittedText.length > amountOfWords;
13 | const beginText = itCanOverflow
14 | ? splittedText.slice(0, amountOfWords - 1).join(" ")
15 | : text;
16 | const endText = splittedText.slice(amountOfWords - 1).join(" ");
17 |
18 | const handleKeyboard = (e: React.KeyboardEvent) => {
19 | if (e.code === "Space" || e.code === "Enter") {
20 | setIsExpanded(!isExpanded);
21 | }
22 | };
23 |
24 | return (
25 |
26 | {beginText}
27 | {itCanOverflow && (
28 | <>
29 | {!isExpanded && ... }
30 |
34 | {endText}
35 |
36 | setIsExpanded(!isExpanded)}
44 | >
45 | {isExpanded ? "show less" : "show more"}
46 |
47 | >
48 | )}
49 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
3 |
4 | import { cn } from "@/utils/utils"
5 |
6 | const Avatar = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, ...props }, ref) => (
10 |
18 | ))
19 | Avatar.displayName = AvatarPrimitive.Root.displayName
20 |
21 | const AvatarImage = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, ...props }, ref) => (
25 |
30 | ))
31 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
32 |
33 | const AvatarFallback = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef
36 | >(({ className, ...props }, ref) => (
37 |
45 | ))
46 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
47 |
48 | export { Avatar, AvatarImage, AvatarFallback }
49 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
5 | import { Circle } from "lucide-react"
6 |
7 | import { cn } from "@/utils/utils"
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | )
20 | })
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | )
41 | })
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
43 |
44 | export { RadioGroup, RadioGroupItem }
45 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/toggle.tsx:
--------------------------------------------------------------------------------
1 | import type { VariantProps } from "class-variance-authority";
2 | import * as React from "react";
3 | import { cn } from "@/utils/utils";
4 | import * as TogglePrimitive from "@radix-ui/react-toggle";
5 | import { cva } from "class-variance-authority";
6 |
7 | const toggleVariants = cva(
8 | "hover:bg-muted hover:text-muted-foreground focus-visible:ring-ring data-[state=on]:bg-accent data-[state=on]:text-accent-foreground inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-transparent",
13 | outline:
14 | "border-input hover:bg-accent hover:text-accent-foreground border bg-transparent shadow-sm",
15 | },
16 | size: {
17 | default: "h-9 px-3",
18 | sm: "h-8 px-2",
19 | lg: "h-10 px-3",
20 | },
21 | },
22 | defaultVariants: {
23 | variant: "default",
24 | size: "default",
25 | },
26 | },
27 | );
28 |
29 | const Toggle = React.forwardRef<
30 | React.ElementRef,
31 | React.ComponentPropsWithoutRef &
32 | VariantProps
33 | >(({ className, variant, size, ...props }, ref) => (
34 |
39 | ));
40 |
41 | Toggle.displayName = TogglePrimitive.Root.displayName;
42 |
43 | export { Toggle, toggleVariants };
44 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/starknet-wallet-button.tsx:
--------------------------------------------------------------------------------
1 | import { useStarknetWallet } from "@/hooks/use-starknet-wallet";
2 | import { getConnectorIcon } from "@/utils/connectWallet";
3 | import { ArrowDownIcon } from "lucide-react";
4 |
5 | import { Button } from "../ui/button";
6 | import { Separator } from "../ui/separator";
7 |
8 | export const StarknetWalletButton = ({
9 | className,
10 | label,
11 | }: {
12 | className?: string;
13 | label?: string;
14 | autoConnect?: boolean;
15 | }) => {
16 | const { lastConnector, openStarknetKitModal, connectWallet } =
17 | useStarknetWallet();
18 |
19 | return (
20 |
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/tooling/eslint/base.js:
--------------------------------------------------------------------------------
1 | import eslint from "@eslint/js";
2 | import importPlugin from "eslint-plugin-import";
3 | import tseslint from "typescript-eslint";
4 |
5 | export default tseslint.config(
6 | {
7 | // Globally ignored files
8 | ignores: ["**/*.config.*"],
9 | },
10 | {
11 | files: ["**/*.js", "**/*.ts", "**/*.tsx"],
12 | plugins: {
13 | import: importPlugin,
14 | },
15 | extends: [
16 | eslint.configs.recommended,
17 | ...tseslint.configs.recommended,
18 | ...tseslint.configs.recommendedTypeChecked,
19 | ...tseslint.configs.stylisticTypeChecked,
20 | ],
21 | rules: {
22 | "@typescript-eslint/no-unused-vars": [
23 | "error",
24 | { argsIgnorePattern: "^_", varsIgnorePattern: "^_" },
25 | ],
26 | "@typescript-eslint/consistent-type-imports": [
27 | "warn",
28 | { prefer: "type-imports", fixStyle: "separate-type-imports" },
29 | ],
30 | "@typescript-eslint/no-misused-promises": [
31 | 2,
32 | { checksVoidReturn: { attributes: false } },
33 | ],
34 | "@typescript-eslint/no-unnecessary-condition": [
35 | "error",
36 | {
37 | allowConstantLoopConditions: true,
38 | },
39 | ],
40 | "@typescript-eslint/no-non-null-assertion": "error",
41 | "import/consistent-type-specifier-style": ["error", "prefer-top-level"],
42 | "@typescript-eslint/non-nullable-type-assertion-style": "off"
43 |
44 | },
45 | },
46 | {
47 | linterOptions: { reportUnusedDisableDirectives: true },
48 | languageOptions: { parserOptions: { projectService: true } },
49 | }
50 | );
51 |
--------------------------------------------------------------------------------
/packages/db/src/int8range.ts:
--------------------------------------------------------------------------------
1 | import type { Range } from "postgres-range";
2 | import { customType } from "drizzle-orm/pg-core";
3 | import {
4 | parse as rangeParse,
5 | serialize as rangeSerialize,
6 | } from "postgres-range";
7 |
8 | type Comparable = string | number;
9 |
10 | type RangeBound =
11 | | T
12 | | {
13 | value: T;
14 | inclusive: boolean;
15 | };
16 |
17 | export class Int8Range {
18 | constructor(public readonly range: Range) {}
19 |
20 | get start(): RangeBound | null {
21 | return this.range.lower != null
22 | ? {
23 | value: this.range.lower,
24 | inclusive: this.range.isLowerBoundClosed(),
25 | }
26 | : null;
27 | }
28 |
29 | get end(): RangeBound | null {
30 | return this.range.upper != null
31 | ? {
32 | value: this.range.upper,
33 | inclusive: this.range.isUpperBoundClosed(),
34 | }
35 | : null;
36 | }
37 |
38 | /*static fromInput(input: TimeRangeInput): Int8Range {
39 | const range = new Range(input.startMs, input.endMs, RANGE_LB_INC);
40 |
41 | return new Int8Range(range);
42 | }*/
43 | }
44 |
45 | export const int8range = customType<{
46 | data: Int8Range;
47 | }>({
48 | dataType: () => "int8range",
49 | fromDriver: (value: unknown): Int8Range => {
50 | if (typeof value !== "string") {
51 | throw new Error("Expected string");
52 | }
53 |
54 | const parsed = rangeParse(value, (val) => parseInt(val, 10));
55 | return new Int8Range(parsed);
56 | },
57 | toDriver: (value: Int8Range): string => rangeSerialize(value.range),
58 | });
59 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/default-catch-boundary.tsx:
--------------------------------------------------------------------------------
1 | import type { ErrorComponentProps } from "@tanstack/react-router";
2 | import {
3 | ErrorComponent,
4 | Link,
5 | rootRouteId,
6 | useMatch,
7 | useRouter,
8 | } from "@tanstack/react-router";
9 |
10 | export function DefaultCatchBoundary({ error }: ErrorComponentProps) {
11 | const router = useRouter();
12 | const isRoot = useMatch({
13 | strict: false,
14 | select: (state) => state.id === rootRouteId,
15 | });
16 |
17 | console.error(error);
18 |
19 | return (
20 |
21 |
22 |
23 |
31 | {isRoot ? (
32 |
36 | Home
37 |
38 | ) : (
39 | {
43 | e.preventDefault();
44 | window.history.back();
45 | }}
46 | >
47 | Go Back
48 |
49 | )}
50 |
51 |
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/apps/account-portal/src/abi/L2/RealmsBridge.ts:
--------------------------------------------------------------------------------
1 | import type { Abi } from "starknet";
2 |
3 | export const RealmsBridge = [
4 | {
5 | name: "deposit_tokens",
6 | type: "function",
7 | inputs: [
8 | {
9 | name: "salt",
10 | type: "core::felt252",
11 | },
12 | {
13 | name: "owner_l1",
14 | type: "core::starknet::eth_address::EthAddress",
15 | },
16 | {
17 | name: "token_ids",
18 | type: "core::array::Span::",
19 | },
20 | ],
21 | outputs: [],
22 | state_mutability: "external",
23 | },
24 | {
25 | name: "set_l1_bridge_address",
26 | type: "function",
27 | inputs: [
28 | {
29 | name: "address",
30 | type: "core::starknet::eth_address::EthAddress",
31 | },
32 | ],
33 | outputs: [],
34 | state_mutability: "external",
35 | },
36 | {
37 | name: "set_l2_token_address",
38 | type: "function",
39 | inputs: [
40 | {
41 | name: "address",
42 | type: "core::starknet::contract_address::ContractAddress",
43 | },
44 | ],
45 | outputs: [],
46 | state_mutability: "external",
47 | },
48 | {
49 | name: "get_l1_bridge_address",
50 | type: "function",
51 | inputs: [],
52 | outputs: [
53 | {
54 | type: "core::starknet::eth_address::EthAddress",
55 | },
56 | ],
57 | state_mutability: "view",
58 | },
59 | {
60 | name: "get_l2_token_address",
61 | type: "function",
62 | inputs: [],
63 | outputs: [
64 | {
65 | type: "core::starknet::contract_address::ContractAddress",
66 | },
67 | ],
68 | state_mutability: "view",
69 | },
70 | ] as const satisfies Abi;
71 |
--------------------------------------------------------------------------------
/packages/db/src/schema/velords.ts:
--------------------------------------------------------------------------------
1 | import {
2 | integer,
3 | numeric,
4 | pgTable,
5 | primaryKey,
6 | text,
7 | timestamp,
8 | } from "drizzle-orm/pg-core";
9 |
10 | export const velords_rewards_received = pgTable(
11 | "velords_rewards_received",
12 | {
13 | //_id: text("_id"),
14 | sender: text("sender").notNull(),
15 | amount: numeric("amount").notNull(),
16 | transaction_hash: text("transaction_hash").notNull(),
17 | //block_time: timestamp("block_time").notNull(),
18 | timestamp: timestamp("epoch", {
19 | mode: "date",
20 | precision: 3,
21 | }).notNull(),
22 | },
23 | (t) => [primaryKey({ columns: [t.amount, t.transaction_hash] })],
24 | );
25 |
26 | export const velords_lords_locked = pgTable(
27 | "velords_lords_locked",
28 | {
29 | //_id: text("_id"),
30 | owner: text("owner").notNull(),
31 | amount: numeric("amount").notNull(),
32 | transaction_hash: text("transaction_hash").notNull(),
33 | //block_time: timestamp("block_time").notNull(),
34 | timestamp: timestamp("epoch", {
35 | mode: "date",
36 | precision: 3,
37 | }).notNull(),
38 | end_time: integer("end_time"),
39 | },
40 | (t) => [primaryKey({ columns: [t.amount, t.transaction_hash] })],
41 | );
42 | export const velords_burner_transfers = pgTable(
43 | "velords_burner_transfers",
44 | {
45 | sender: text("sender").notNull(),
46 | amount: numeric("amount").notNull(),
47 | transaction_hash: text("transaction_hash").notNull(),
48 | //block_time: timestamp("block_time").notNull(),
49 | timestamp: timestamp("timestamp", {
50 | mode: "date",
51 | precision: 3,
52 | }).notNull(),
53 | },
54 | (t) => [primaryKey({ columns: [t.amount, t.transaction_hash] })],
55 | );
56 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3 |
4 | import { cn } from "@/utils/utils"
5 |
6 | const ScrollArea = React.forwardRef<
7 | React.ElementRef,
8 | React.ComponentPropsWithoutRef
9 | >(({ className, children, ...props }, ref) => (
10 |
15 |
16 | {children}
17 |
18 |
19 |
20 |
21 | ))
22 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
23 |
24 | const ScrollBar = React.forwardRef<
25 | React.ElementRef,
26 | React.ComponentPropsWithoutRef
27 | >(({ className, orientation = "vertical", ...props }, ref) => (
28 |
41 |
42 |
43 | ))
44 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
45 |
46 | export { ScrollArea, ScrollBar }
47 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/bridge/useWriteFinalizeWithdrawRealms.ts:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCallback } from "react";
4 | import { StarknetBridgeRealms as L1_REALMS_BRIDGE_ABI } from "@/abi/L1/StarknetBridgeRealms";
5 | import { uint256 } from "starknet";
6 | import { useWriteContract } from "wagmi";
7 |
8 | import { REALMS_BRIDGE_ADDRESS } from "@realms-world/constants";
9 | import { SUPPORTED_L1_CHAIN_ID } from "@/utils/utils";
10 |
11 | const FUNCTION = "withdrawTokens";
12 |
13 | export function useWriteFinalizeWithdrawRealms() {
14 | const { writeContractAsync, data, ...writeReturn } = useWriteContract();
15 |
16 | // if (!l2Address) throw new Error("Missing L2 Address");
17 |
18 | const writeAsync = useCallback(
19 | async ({
20 | hash,
21 | l1Address,
22 | l2Address,
23 | tokenIds,
24 | }: {
25 | hash: string | bigint;
26 | l1Address: string;
27 | l2Address: string;
28 | tokenIds: string[] | bigint[];
29 | }) => {
30 | const parsedTokenIds = tokenIds.map((id) => {
31 | const uInt = uint256.bnToUint256(BigInt(id));
32 | return [BigInt(uInt.low), BigInt(uInt.high)];
33 | });
34 | const hashUint = uint256.bnToUint256(BigInt(hash));
35 | return await writeContractAsync({
36 | address: REALMS_BRIDGE_ADDRESS[SUPPORTED_L1_CHAIN_ID] as `0x${string}`,
37 | abi: L1_REALMS_BRIDGE_ABI,
38 | functionName: FUNCTION,
39 | args: [
40 | [
41 | BigInt(hashUint.low),
42 | BigInt(hashUint.high),
43 | BigInt(l1Address),
44 | BigInt(l2Address),
45 | BigInt(tokenIds.length),
46 | ...parsedTokenIds.flat(),
47 | ],
48 | ],
49 | });
50 | },
51 | [writeContractAsync],
52 | );
53 | return { writeAsync, data, ...writeReturn };
54 | }
55 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/governance/delegate-card-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardFooter,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 | import { Skeleton } from "@/components/ui/skeleton";
9 |
10 | export function DelegateCardSkeleton() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | {/* Votes count skeleton */}
23 |
24 |
25 |
26 |
27 |
28 | {/* Interest badges skeleton */}
29 |
30 |
31 |
32 |
33 |
34 |
35 | {/* Statement skeleton */}
36 |
37 |
38 |
39 |
40 |
41 |
42 | {/* Social media icons skeleton */}
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/governance/proposal-list.tsx:
--------------------------------------------------------------------------------
1 | import { getProposalsQueryOptions } from "@/lib/snapshot/getProposals";
2 | import { getUserVotesQueryOptions } from "@/lib/snapshot/getUserVotes";
3 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
4 | import { useQuery, useSuspenseQuery } from "@tanstack/react-query";
5 |
6 | import { SnapshotSpaceAddresses } from "@realms-world/constants";
7 |
8 | import { ProposalListItem } from "./proposal-list-item";
9 |
10 | export function ProposalList({
11 | limit = 5,
12 | delegateId,
13 | }: {
14 | limit?: number;
15 | delegateId?: string;
16 | }) {
17 | const { data: proposalsQuery } = useSuspenseQuery(
18 | getProposalsQueryOptions({
19 | spaceIds: [SnapshotSpaceAddresses[SUPPORTED_L2_CHAIN_ID] as string],
20 | limit,
21 | skip: 0,
22 | current: 1,
23 | searchQuery: "",
24 | }),
25 | );
26 | const proposals = proposalsQuery.proposals;
27 |
28 | const { data: userVotesQuery } = useQuery(
29 | getUserVotesQueryOptions({
30 | spaceIds: [SnapshotSpaceAddresses[SUPPORTED_L2_CHAIN_ID] as string],
31 | limit,
32 | skip: 0,
33 | voter: delegateId ?? "",
34 | }),
35 | );
36 | const userVotes = userVotesQuery?.votes;
37 |
38 | return (
39 |
40 | {proposals?.map((proposal) => {
41 | const matchingVote = userVotes?.find((vote) => {
42 | return vote?.proposal === Number(proposal?.id.split("/")[1]);
43 | });
44 | return (
45 |
54 | );
55 | })}
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/getVeLordsBurns.ts:
--------------------------------------------------------------------------------
1 | import type { SQL } from "@realms-world/db";
2 | import { queryOptions } from "@tanstack/react-query";
3 | import { createServerFn } from "@tanstack/react-start";
4 | import { z } from "zod";
5 |
6 | import { and, eq, gte, lte } from "@realms-world/db";
7 | import { db } from "@realms-world/db/client";
8 | import { velords_rewards_received } from "@realms-world/db/schema";
9 |
10 | /* -------------------------------------------------------------------------- */
11 | /* getVelordsBurns Endpoint */
12 | /* -------------------------------------------------------------------------- */
13 |
14 | const GetVelordsBurnsInput = z.object({
15 | sender: z.string().optional(),
16 | startTimestamp: z.date().optional(),
17 | endTimestamp: z.date().optional(),
18 | });
19 |
20 | export const getVelordsBurns = createServerFn({ method: "GET" })
21 | .validator((input: unknown) => GetVelordsBurnsInput.parse(input))
22 | .handler(async (ctx) => {
23 | const { sender, startTimestamp, endTimestamp } = ctx.data;
24 | const whereFilter: SQL[] = [];
25 | if (sender) {
26 | whereFilter.push(
27 | eq(velords_rewards_received.sender, sender.toLowerCase()),
28 | );
29 | }
30 | if (startTimestamp) {
31 | whereFilter.push(gte(velords_rewards_received.timestamp, startTimestamp));
32 | }
33 | if (endTimestamp) {
34 | whereFilter.push(lte(velords_rewards_received.timestamp, endTimestamp));
35 | }
36 | return db.query.velords_rewards_received.findMany({
37 | where: and(...whereFilter),
38 | });
39 | });
40 |
41 | export const getVelordsBurnsQueryOptions = (
42 | input?: z.infer,
43 | ) =>
44 | queryOptions({
45 | queryKey: ["getVelordsBurns", input],
46 | queryFn: () => getVelordsBurns({ data: input ?? {} }),
47 | });
48 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva } from "class-variance-authority"
3 | import type {VariantProps} from "class-variance-authority";
4 |
5 | import { cn } from "@/utils/utils"
6 |
7 | const alertVariants = cva(
8 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-background text-foreground",
13 | destructive:
14 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
15 | },
16 | },
17 | defaultVariants: {
18 | variant: "default",
19 | },
20 | }
21 | )
22 |
23 | const Alert = React.forwardRef<
24 | HTMLDivElement,
25 | React.HTMLAttributes & VariantProps
26 | >(({ className, variant, ...props }, ref) => (
27 |
33 | ))
34 | Alert.displayName = "Alert"
35 |
36 | const AlertTitle = React.forwardRef<
37 | HTMLParagraphElement,
38 | React.HTMLAttributes
39 | >(({ className, ...props }, ref) => (
40 |
45 | ))
46 | AlertTitle.displayName = "AlertTitle"
47 |
48 | const AlertDescription = React.forwardRef<
49 | HTMLParagraphElement,
50 | React.HTMLAttributes
51 | >(({ className, ...props }, ref) => (
52 |
57 | ))
58 | AlertDescription.displayName = "AlertDescription"
59 |
60 | export { Alert, AlertTitle, AlertDescription }
61 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/toggle-group.tsx:
--------------------------------------------------------------------------------
1 | import type { VariantProps } from "class-variance-authority";
2 | import * as React from "react";
3 | import { cn } from "@/utils/utils";
4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
5 |
6 | import { toggleVariants } from "./toggle";
7 |
8 | const ToggleGroupContext = React.createContext<
9 | VariantProps
10 | >({
11 | size: "default",
12 | variant: "default",
13 | });
14 |
15 | const ToggleGroup = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef &
18 | VariantProps
19 | >(({ className, variant, size, children, ...props }, ref) => (
20 |
25 |
26 | {children}
27 |
28 |
29 | ));
30 |
31 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName;
32 |
33 | const ToggleGroupItem = React.forwardRef<
34 | React.ElementRef,
35 | React.ComponentPropsWithoutRef &
36 | VariantProps
37 | >(({ className, children, variant, size, ...props }, ref) => {
38 | const context = React.useContext(ToggleGroupContext);
39 |
40 | return (
41 |
52 | {children}
53 |
54 | );
55 | });
56 |
57 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName;
58 |
59 | export { ToggleGroup, ToggleGroupItem };
60 |
--------------------------------------------------------------------------------
/packages/db/src/schema/auth.ts:
--------------------------------------------------------------------------------
1 | import {
2 | boolean,
3 | pgTable,
4 | text,
5 | timestamp,
6 | } from "drizzle-orm/pg-core";
7 |
8 | export const user = pgTable("user", {
9 | id: text("id").primaryKey(),
10 | name: text("name").notNull(),
11 | email: text("email").notNull().unique(),
12 | emailVerified: boolean("email_verified").notNull(),
13 | image: text("image"),
14 | createdAt: timestamp("created_at").notNull(),
15 | updatedAt: timestamp("updated_at").notNull(),
16 | address: text("address").unique(),
17 | });
18 |
19 | export const session = pgTable("session", {
20 | id: text("id").primaryKey(),
21 | expiresAt: timestamp("expires_at").notNull(),
22 | token: text("token").notNull().unique(),
23 | createdAt: timestamp("created_at").notNull(),
24 | updatedAt: timestamp("updated_at").notNull(),
25 | ipAddress: text("ip_address"),
26 | userAgent: text("user_agent"),
27 | userId: text("user_id")
28 | .notNull()
29 | .references(() => user.id, { onDelete: "cascade" }),
30 | });
31 |
32 | export const account = pgTable("account", {
33 | id: text("id").primaryKey(),
34 | accountId: text("account_id").notNull(),
35 | providerId: text("provider_id").notNull(),
36 | userId: text("user_id")
37 | .notNull()
38 | .references(() => user.id, { onDelete: "cascade" }),
39 | accessToken: text("access_token"),
40 | refreshToken: text("refresh_token"),
41 | idToken: text("id_token"),
42 | accessTokenExpiresAt: timestamp("access_token_expires_at"),
43 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
44 | scope: text("scope"),
45 | password: text("password"),
46 | createdAt: timestamp("created_at").notNull(),
47 | updatedAt: timestamp("updated_at").notNull(),
48 | });
49 |
50 | export const verification = pgTable("verification", {
51 | id: text("id").primaryKey(),
52 | identifier: text("identifier").notNull(),
53 | value: text("value").notNull(),
54 | expiresAt: timestamp("expires_at").notNull(),
55 | createdAt: timestamp("created_at"),
56 | updatedAt: timestamp("updated_at"),
57 | });
58 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/token/L1/useERC721Approval.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { SUPPORTED_L1_CHAIN_ID } from "@/utils/utils";
3 | import { erc721Abi } from "viem";
4 | import {
5 | useAccount,
6 | useBlockNumber,
7 | useReadContract,
8 | useWaitForTransactionReceipt,
9 | } from "wagmi";
10 |
11 | import {
12 | CollectionAddresses,
13 | REALMS_BRIDGE_ADDRESS,
14 | } from "@realms-world/constants";
15 |
16 | import { useERC721SetApprovalForAll } from "./useERC721SetApprovalForAll";
17 |
18 | export default function useERC721Approval() {
19 | const { address } = useAccount();
20 | const { writeAsync, data, isPending } = useERC721SetApprovalForAll({
21 | onSuccess: (data) => console.log("approved" + data),
22 | });
23 | const { data: isApprovedForAll, refetch } = useReadContract({
24 | abi: erc721Abi,
25 | address: CollectionAddresses.realms[SUPPORTED_L1_CHAIN_ID] as `0x${string}`,
26 | args: address && [
27 | address,
28 | REALMS_BRIDGE_ADDRESS[SUPPORTED_L1_CHAIN_ID] as `0x${string}`,
29 | ],
30 | functionName: "isApprovedForAll",
31 | query: {
32 | enabled: !!address,
33 | },
34 | });
35 |
36 | const { data: blockNumber } = useBlockNumber({ watch: true });
37 |
38 | useEffect(() => {
39 | if (address && !isApprovedForAll) {
40 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
41 | refetch();
42 | }
43 | }, [blockNumber, refetch, address, isApprovedForAll]);
44 |
45 | const approveForAll = async () =>
46 | await writeAsync({
47 | contractAddress: CollectionAddresses.realms[
48 | SUPPORTED_L1_CHAIN_ID
49 | ] as `0x${string}`,
50 | operator: REALMS_BRIDGE_ADDRESS[SUPPORTED_L1_CHAIN_ID] as `0x${string}`,
51 | });
52 |
53 | const { isLoading: approveForAllLoading } = useWaitForTransactionReceipt({
54 | hash: data,
55 | });
56 |
57 | return {
58 | isApprovedForAll,
59 | approveForAll,
60 | approveForAllLoading: approveForAllLoading && data !== undefined,
61 | isPending,
62 | };
63 | }
64 |
--------------------------------------------------------------------------------
/packages/db/src/schema/bridge.ts:
--------------------------------------------------------------------------------
1 | import { relations } from "drizzle-orm";
2 | import {
3 | integer,
4 | numeric,
5 | pgEnum,
6 | pgTable,
7 | primaryKey,
8 | text,
9 | timestamp,
10 | } from "drizzle-orm/pg-core";
11 |
12 | export const bridgeEventTypeEnum = pgEnum("BridgeEventType", [
13 | "deposit_initiated_l1",
14 | "deposit_initiated_l2",
15 | "withdraw_available_l1",
16 | "withdraw_completed_l1",
17 | "withdraw_completed_l2",
18 | ]);
19 |
20 | export const realmsBridgeRequests = pgTable("realms_bridge_requests", {
21 | _id: text("_id").notNull().primaryKey(),
22 | from_chain: text("from_chain").notNull(),
23 | token_ids: integer("token_ids").array().notNull(),
24 | from_address: text("from_address").notNull(),
25 | to_address: text("to_address").notNull(),
26 | timestamp: timestamp("timestamp").notNull(),
27 | tx_hash: text("tx_hash").notNull(),
28 | req_hash: numeric("req_hash").notNull(),
29 | });
30 |
31 | export const realmsBridgeRequestsRelations = relations(
32 | realmsBridgeRequests,
33 | ({ many }) => ({
34 | events: many(realmsBridgeEvents),
35 | }),
36 | );
37 | export const realmsBridgeEvents = pgTable(
38 | "realms_bridge_events",
39 | {
40 | _id: text("_id").notNull(),
41 | hash: text("hash").notNull(),
42 | type: bridgeEventTypeEnum().notNull(),
43 | timestamp: timestamp("timestamp").notNull(),
44 | },
45 | (t) => [primaryKey({ columns: [t._id, t.type] })],
46 | );
47 | export const realmsBridgeEventsRelations = relations(
48 | realmsBridgeEvents,
49 | ({ one }) => ({
50 | request: one(realmsBridgeRequests, {
51 | fields: [realmsBridgeEvents._id],
52 | references: [realmsBridgeRequests._id],
53 | }),
54 | }),
55 | );
56 |
57 | export const realmsLordsClaims = pgTable(
58 | "realms_lords_claims",
59 | {
60 | _id: text("_id"),
61 | hash: text("hash").notNull(),
62 | amount: numeric("amount", { scale: 0 }).notNull(),
63 | recipient: text("recipient").notNull(),
64 | timestamp: timestamp({ mode: "string" }).notNull(),
65 | },
66 | (t) => [primaryKey({ columns: [t.amount, t.hash] })],
67 | );
68 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as TabsPrimitive from "@radix-ui/react-tabs"
3 |
4 | import { cn } from "@/utils/utils"
5 |
6 | const Tabs = TabsPrimitive.Root
7 |
8 | const TabsList = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | TabsList.displayName = TabsPrimitive.List.displayName
22 |
23 | const TabsTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
37 |
38 | const TabsContent = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
50 | ))
51 | TabsContent.displayName = TabsPrimitive.Content.displayName
52 |
53 | export { Tabs, TabsList, TabsTrigger, TabsContent }
54 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/delegate.list.tsx:
--------------------------------------------------------------------------------
1 | import { Suspense, useState } from "react";
2 | import { DelegateCardSkeleton } from "@/components/modules/governance/delegate-card-skeleton";
3 | import DelegateList from "@/components/modules/governance/delegate-list";
4 | import { DelegateListActions } from "@/components/modules/governance/delegate-list-actions";
5 | import { Input } from "@/components/ui/input";
6 | import { getDelegatesQueryOptions } from "@/lib/getDelegates";
7 | import { createFileRoute } from "@tanstack/react-router";
8 |
9 | export const Route = createFileRoute("/delegate/list")({
10 | component: RouteComponent,
11 | loader: async ({ context }) => {
12 | await context.queryClient.ensureQueryData(
13 | getDelegatesQueryOptions({
14 | limit: 200,
15 | cursor: 0,
16 | search: "",
17 | orderBy: "random",
18 | }),
19 | );
20 | },
21 | });
22 |
23 | function RouteComponent() {
24 | // Local state for the search string.
25 | const [searchQuery, setSearchQuery] = useState("");
26 | const [sortMethod, setSortMethod] = useState<"desc" | "random">("random");
27 |
28 | return (
29 |
30 | {/* Sticky search input */}
31 |
32 | setSearchQuery(e.target.value)}
38 | />
39 |
40 |
41 |
44 | {Array.from({ length: 6 }).map((_, index) => (
45 |
46 | ))}
47 |
48 | }
49 | >
50 |
51 |
52 |
53 | );
54 | }
55 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/utils/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLDivElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLDivElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/eternum/getRealms.ts:
--------------------------------------------------------------------------------
1 | import type { PortfolioTokensApiResponse } from "@/types/ark";
2 | import { queryOptions } from "@tanstack/react-query";
3 | import { createServerFn } from "@tanstack/react-start";
4 | import { z } from "zod";
5 |
6 | const ARK_MARKETPLACE_API_URL = `https://api.marketplace${
7 | process.env.VITE_PUBLIC_CHAIN === "sepolia" ? ".dev" : ""
8 | }.arkproject.dev`;
9 |
10 | /* -------------------------------------------------------------------------- */
11 | /* getRealms Endpoint */
12 | /* -------------------------------------------------------------------------- */
13 |
14 | const GetRealmsInput = z.object({
15 | address: z.string().optional(),
16 | collectionAddress: z.string().optional(),
17 | itemsPerPage: z.number().optional(),
18 | page: z.number().optional(),
19 | });
20 |
21 | export const getRealms = createServerFn({ method: "GET" })
22 | .validator((input: unknown) => GetRealmsInput.parse(input))
23 | .handler(async (ctx) => {
24 | const {
25 | address,
26 | collectionAddress,
27 | itemsPerPage = 100,
28 | page = 1,
29 | } = ctx.data;
30 | const queryParams = [
31 | `items_per_page=${itemsPerPage}`,
32 | `page=${page}`,
33 | collectionAddress ? `collection=${collectionAddress}` : null,
34 | ]
35 | .filter(Boolean)
36 | .join("&");
37 |
38 | const response = await fetch(
39 | `${ARK_MARKETPLACE_API_URL}/portfolio/${address}?${queryParams}`,
40 | {
41 | method: "GET",
42 | headers: {
43 | "Content-Type": "application/json",
44 | "Access-Control-Allow-Origin": "*",
45 | },
46 | },
47 | );
48 | const data = (await response.json()) as PortfolioTokensApiResponse;
49 | return data;
50 | });
51 |
52 | export const getRealmsQueryOptions = (input: z.infer) =>
53 | queryOptions({
54 | queryKey: [
55 | "getRealms",
56 | input.address,
57 | input.collectionAddress,
58 | input.page,
59 | input.itemsPerPage,
60 | ],
61 | queryFn: () => (input.address ? getRealms({ data: input }) : null),
62 | });
63 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/getBridgeTransactions.ts:
--------------------------------------------------------------------------------
1 | import type { SQL } from "@realms-world/db";
2 | import { queryOptions } from "@tanstack/react-query";
3 | import { createServerFn } from "@tanstack/react-start";
4 | import { z } from "zod";
5 |
6 | import { desc, eq, or, realmsBridgeRequests } from "@realms-world/db";
7 | import { db } from "@realms-world/db/client";
8 |
9 | /* -------------------------------------------------------------------------- */
10 | /* getBridgeTransactions Endpoint */
11 | /* -------------------------------------------------------------------------- */
12 |
13 | const GetBridgeTransactionsInput = z.object({
14 | l1Account: z.string().nullable().optional(),
15 | l2Account: z.string().nullable().optional(),
16 | });
17 |
18 | export const getBridgeTransactions = createServerFn({ method: "GET" })
19 | .validator((input: unknown) => GetBridgeTransactionsInput.parse(input))
20 | .handler(async (ctx) => {
21 | const { l1Account, l2Account } = ctx.data;
22 | const whereFilter: SQL[] = [];
23 |
24 | if (l1Account) {
25 | whereFilter.push(
26 | eq(realmsBridgeRequests.from_address, l1Account.toLowerCase()),
27 | eq(realmsBridgeRequests.to_address, l1Account.toLowerCase()),
28 | );
29 | }
30 | if (l2Account) {
31 | whereFilter.push(
32 | eq(realmsBridgeRequests.from_address, l2Account.toLowerCase()),
33 | eq(realmsBridgeRequests.to_address, l2Account.toLowerCase()),
34 | );
35 | }
36 | return db.query.realmsBridgeRequests.findMany({
37 | where: or(...whereFilter),
38 | orderBy: desc(realmsBridgeRequests.timestamp),
39 | with: {
40 | events: true,
41 | },
42 | });
43 | });
44 |
45 | export const getBridgeTransactionsQueryOptions = (
46 | input?: z.infer,
47 | ) =>
48 | queryOptions({
49 | queryKey: ["getBridgeTransactions", input],
50 | queryFn: () =>
51 | !!input?.l2Account || !!input?.l1Account
52 | ? getBridgeTransactions({ data: input })
53 | : null,
54 | enabled: !!input?.l2Account || !!input?.l1Account,
55 | refetchInterval: 10000,
56 | });
57 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva } from "class-variance-authority"
4 | import type {VariantProps} from "class-variance-authority";
5 |
6 | import { cn } from "@/utils/utils"
7 |
8 | const buttonVariants = cva(
9 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-hidden focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
10 | {
11 | variants: {
12 | variant: {
13 | default:
14 | "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
15 | destructive:
16 | "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
17 | outline:
18 | "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
19 | secondary:
20 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
21 | ghost: "hover:bg-accent hover:text-accent-foreground",
22 | link: "text-primary underline-offset-4 hover:underline",
23 | },
24 | size: {
25 | default: "h-9 px-4 py-2",
26 | sm: "h-8 rounded-md px-3 text-xs",
27 | lg: "h-10 rounded-md px-8",
28 | icon: "h-9 w-9",
29 | },
30 | },
31 | defaultVariants: {
32 | variant: "default",
33 | size: "default",
34 | },
35 | }
36 | )
37 |
38 | export interface ButtonProps
39 | extends React.ButtonHTMLAttributes,
40 | VariantProps {
41 | asChild?: boolean
42 | }
43 |
44 | const Button = React.forwardRef(
45 | ({ className, variant, size, asChild = false, ...props }, ref) => {
46 | const Comp = asChild ? Slot : "button"
47 | return (
48 |
53 | )
54 | }
55 | )
56 | Button.displayName = "Button"
57 |
58 | export { Button, buttonVariants }
59 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as AccordionPrimitive from "@radix-ui/react-accordion"
3 | import { ChevronDown } from "lucide-react"
4 |
5 | import { cn } from "@/utils/utils"
6 |
7 | const Accordion = AccordionPrimitive.Root
8 |
9 | const AccordionItem = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
18 | ))
19 | AccordionItem.displayName = "AccordionItem"
20 |
21 | const AccordionTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef
24 | >(({ className, children, ...props }, ref) => (
25 |
26 | svg]:rotate-180",
30 | className
31 | )}
32 | {...props}
33 | >
34 | {children}
35 |
36 |
37 |
38 | ))
39 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
40 |
41 | const AccordionContent = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef
44 | >(({ className, children, ...props }, ref) => (
45 |
50 | {children}
51 |
52 | ))
53 | AccordionContent.displayName = AccordionPrimitive.Content.displayName
54 |
55 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
56 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/login-card.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Card,
3 | CardContent,
4 | CardDescription,
5 | CardHeader,
6 | CardTitle,
7 | } from "@/components/ui/card";
8 |
9 | import { StarknetWalletButton } from "./starknet-wallet-button";
10 |
11 | export function LoginCard() {
12 | return (
13 |
14 |
15 |
16 | Welcome to Realms Portal
17 |
18 |
19 | Connect your wallet to access the Realms ecosystem
20 |
21 |
22 |
23 | {/* Feature list */}
24 |
25 |
30 |
35 |
40 |
45 |
46 |
47 | {/* Connect wallet button */}
48 |
49 |
50 |
51 |
52 |
53 | );
54 | }
55 |
56 | function FeatureItem({
57 | title,
58 | description,
59 | icon,
60 | }: {
61 | title: string;
62 | description: string;
63 | icon: string;
64 | }) {
65 | return (
66 |
67 |
{icon}
68 |
69 |
{title}
70 |
{description}
71 |
72 |
73 | );
74 | }
75 |
--------------------------------------------------------------------------------
/apps/account-portal/src/providers/theme.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 |
3 | type Theme = "dark" | "light" | "system";
4 |
5 | interface ThemeProviderProps {
6 | children: React.ReactNode;
7 | defaultTheme?: Theme;
8 | storageKey?: string;
9 | }
10 |
11 | interface ThemeProviderState {
12 | theme: Theme;
13 | setTheme: (theme: Theme) => void;
14 | }
15 |
16 | const initialState: ThemeProviderState = {
17 | theme: "system",
18 | setTheme: () => null,
19 | };
20 |
21 | const ThemeProviderContext = createContext(initialState);
22 |
23 | export function ThemeProvider({
24 | children,
25 | defaultTheme = "system",
26 | storageKey = "vite-ui-theme",
27 | ...props
28 | }: ThemeProviderProps) {
29 | const [theme, setTheme] = useState(() => defaultTheme);
30 | // Only update theme from localStorage on the client.
31 | useEffect(() => {
32 | if (typeof window !== "undefined") {
33 | const savedTheme = localStorage.getItem(storageKey);
34 | if (savedTheme) {
35 | setTheme(savedTheme as Theme);
36 | }
37 | }
38 | }, [storageKey]);
39 |
40 | useEffect(() => {
41 | const root = window.document.documentElement;
42 |
43 | root.classList.remove("light", "dark");
44 |
45 | if (theme === "system") {
46 | const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
47 | .matches
48 | ? "dark"
49 | : "light";
50 |
51 | root.classList.add(systemTheme);
52 | return;
53 | }
54 |
55 | root.classList.add(theme);
56 | }, [theme]);
57 |
58 | const value = {
59 | theme,
60 | setTheme: (theme: Theme) => {
61 | localStorage.setItem(storageKey, theme);
62 | setTheme(theme);
63 | },
64 | };
65 |
66 | return (
67 |
68 | {children}
69 |
70 | );
71 | }
72 |
73 | export const useTheme = () => {
74 | const context = useContext(ThemeProviderContext);
75 |
76 | // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
77 | if (context === undefined)
78 | throw new Error("useTheme must be used within a ThemeProvider");
79 |
80 | return context;
81 | };
82 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/icons/bridge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/apps/account-portal/src/gql/eternum/gql.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as types from './graphql';
3 |
4 |
5 |
6 | /**
7 | * Map of all GraphQL operations in the project.
8 | *
9 | * This map has several performance disadvantages:
10 | * 1. It is not tree-shakeable, so it will include all operations in the project.
11 | * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
12 | * 3. It does not support dead code elimination, so it will add unused operations.
13 | *
14 | * Therefore it is highly recommended to use the babel or swc plugin for production.
15 | * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
16 | */
17 | type Documents = {
18 | "\n query getAccountTokens($address: String!) {\n tokenBalances(accountAddress: $address, limit: 8000) {\n edges {\n node {\n tokenMetadata {\n __typename\n ... on ERC721__Token {\n tokenId\n metadataDescription\n imagePath\n contractAddress\n metadata\n }\n }\n }\n }\n }\n }\n": typeof types.GetAccountTokensDocument,
19 | };
20 | const documents: Documents = {
21 | "\n query getAccountTokens($address: String!) {\n tokenBalances(accountAddress: $address, limit: 8000) {\n edges {\n node {\n tokenMetadata {\n __typename\n ... on ERC721__Token {\n tokenId\n metadataDescription\n imagePath\n contractAddress\n metadata\n }\n }\n }\n }\n }\n }\n": types.GetAccountTokensDocument,
22 | };
23 |
24 | /**
25 | * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
26 | */
27 | export function graphql(source: "\n query getAccountTokens($address: String!) {\n tokenBalances(accountAddress: $address, limit: 8000) {\n edges {\n node {\n tokenMetadata {\n __typename\n ... on ERC721__Token {\n tokenId\n metadataDescription\n imagePath\n contractAddress\n metadata\n }\n }\n }\n }\n }\n }\n"): typeof import('./graphql').GetAccountTokensDocument;
28 |
29 |
30 | export function graphql(source: string) {
31 | return (documents as any)[source] ?? {};
32 | }
33 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-velords-claims.ts:
--------------------------------------------------------------------------------
1 | import type { Address } from "@starknet-react/core";
2 | import type { Call } from "starknet";
3 | import { useMemo, useState } from "react";
4 | import { RewardPool } from "@/abi/L2/RewardPool";
5 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
6 | import {
7 | useAccount,
8 | useContract,
9 | useSendTransaction,
10 | } from "@starknet-react/core";
11 |
12 | import { StakingAddresses } from "@realms-world/constants";
13 |
14 | import { useSimulateTransactions } from "./useSimulateTransactions";
15 |
16 | export default function useVeLordsClaims() {
17 | const { address } = useAccount();
18 |
19 | // Determine the reward pool address via chain configuration.
20 | const rewardPoolAddress = StakingAddresses.rewardpool[SUPPORTED_L2_CHAIN_ID];
21 |
22 | // Allow room to override the recipient (defaults to current account address).
23 | const [recipient, setRecipient] = useState(undefined);
24 |
25 | // Initialize the reward pool contract.
26 | const { contract: rewardPool } = useContract({
27 | abi: RewardPool,
28 | address: rewardPoolAddress as Address,
29 | });
30 |
31 | // Create the call to claim rewards. If no recipient is provided, use the current account.
32 | const claimCall: Call[] | undefined = useMemo(() => {
33 | const finalRecipient = recipient ?? address;
34 | return finalRecipient !== undefined && rewardPool
35 | ? [rewardPool.populate("claim", [finalRecipient])]
36 | : undefined;
37 | }, [address, recipient, rewardPool]);
38 |
39 | // Simulate the claim rewards call to get the potential rewards amount.
40 | const { data: simulateData } = useSimulateTransactions({
41 | calls: claimCall,
42 | });
43 |
44 | // Retrieve the claimable amount (ensure this aligns with your contract's response shape).
45 | const lordsClaimable = useMemo(
46 | () =>
47 | BigInt(
48 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
49 | ((simulateData as any)?.[0]?.transaction_trace?.execute_invocation
50 | ?.result?.[2] ?? "0") as string,
51 | ),
52 | [simulateData],
53 | );
54 |
55 | // Prepare the function to send the claim rewards transaction.
56 | const { sendAsync: claimRewards, isPending: claimIsSubmitting } =
57 | useSendTransaction({
58 | calls: claimCall,
59 | });
60 |
61 | return {
62 | recipient,
63 | setRecipient,
64 | claimCall,
65 | lordsClaimable,
66 | claimRewards,
67 | claimIsSubmitting,
68 | };
69 | }
70 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/realms/realm-card.tsx:
--------------------------------------------------------------------------------
1 | import type { RawTokenBalanceWithMetadata } from "@/lib/eternum/getPortfolioCollections";
2 | import { AnimatedMap } from "@/components/icons/AnimatedMap";
3 | import { Card, CardContent } from "@/components/ui/card";
4 |
5 | import Media from "./media";
6 | import RealmResources from "./realm-resources";
7 |
8 | export interface RealmMetadata {
9 | name: string;
10 | description: string;
11 | image: string;
12 | attributes: {
13 | trait_type: string;
14 | value: string | number | undefined;
15 | }[];
16 | }
17 |
18 | export const RealmCard = ({
19 | token,
20 | isGrid,
21 | }: {
22 | token: RawTokenBalanceWithMetadata;
23 | isGrid?: boolean;
24 | }) => {
25 | const { metadata } = token;
26 | const parsedMetadata = metadata
27 | ? (JSON.parse(metadata) as RealmMetadata)
28 | : null;
29 | const { name, image } = parsedMetadata ?? {};
30 |
31 | return (
32 |
33 |
34 | {image ? (
35 |
43 | ) : (
44 |
47 | )}
48 | {isGrid && (
49 |
50 | #{Number(token.token_id)}
51 |
52 | )}
53 |
54 |
55 |
56 |
57 |
58 | );
59 | };
60 |
61 | const GridDetails = ({
62 | token,
63 | }: {
64 | token: RealmMetadata | null;
65 | address?: string;
66 | }) => (
67 |
68 |
69 |
{token?.name}
70 |
71 | {/*
*/}
72 | {/*token.last_price && (
73 |
74 | {token.last_price}
75 |
76 |
77 | )*/}
78 |
79 |
80 |
81 |
82 |
83 |
84 | );
85 |
--------------------------------------------------------------------------------
/apps/account-portal/src/providers/ethereum.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useEffect, useState } from "react";
4 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
5 |
6 | // 0. Setup queryClient
7 | const queryClient = new QueryClient();
8 |
9 | export function AppKitProvider({ children }: { children: React.ReactNode }) {
10 | const [WagmiProvider, setWagmiProvider] = useState(null);
11 | const [wagmiConfig, setWagmiConfig] = useState(null);
12 |
13 | useEffect(() => {
14 | let isMounted = true;
15 |
16 | async function loadClientOnlyDeps() {
17 | // Dynamically import client-only packages
18 | const [{ WagmiAdapter }, { mainnet, sepolia }, { createAppKit }] =
19 | await Promise.all([
20 | import("@reown/appkit-adapter-wagmi"),
21 | import("@reown/appkit/networks"),
22 | import("@reown/appkit/react"),
23 | ]);
24 | const { WagmiProvider } = await import("wagmi");
25 |
26 | // 1. Get projectId from https://cloud.reown.com
27 | const projectId = "d80d873dcad4b8636dd7314223238a59";
28 |
29 | // 2. Create a metadata object - optional
30 | const metadata = {
31 | name: "Realms World",
32 | description: "Connect your Ethereum wallet to Realms World",
33 | url: "https://account.realms.world",
34 | icons: ["https://assets.reown.com/reown-profile-pic.png"],
35 | };
36 |
37 | // 3. Set the networks
38 | const networks = [mainnet, sepolia];
39 |
40 | // 4. Create Wagmi Adapter
41 | const wagmiAdapter = new WagmiAdapter({
42 | networks,
43 | projectId,
44 | ssr: true,
45 | });
46 |
47 | // 5. Create modal
48 | createAppKit({
49 | adapters: [wagmiAdapter],
50 | networks,
51 | projectId,
52 | metadata,
53 | features: {
54 | analytics: true,
55 | },
56 | });
57 |
58 | if (isMounted) {
59 | setWagmiProvider(() => WagmiProvider);
60 | setWagmiConfig(wagmiAdapter.wagmiConfig);
61 | }
62 | }
63 |
64 | if (typeof window !== "undefined") {
65 | loadClientOnlyDeps();
66 | }
67 |
68 | return () => {
69 | isMounted = false;
70 | };
71 | }, []);
72 |
73 | if (!WagmiProvider || !wagmiConfig) {
74 | // Optionally render a loading spinner here
75 | return null;
76 | }
77 |
78 | return (
79 |
80 | {children}
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/governance/delegate-list-actions.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Badge } from "@/components/ui/badge";
3 | import { Button } from "@/components/ui/button";
4 | import { Label } from "@/components/ui/label";
5 | import { useCurrentDelegate } from "@/hooks/governance/use-current-delegate";
6 | import { useDelegateRealms } from "@/hooks/governance/use-delegate-realms";
7 | import { useStarkDisplayName } from "@/hooks/use-stark-name";
8 | import { useAccount } from "@starknet-react/core";
9 | import { Shuffle, TrendingDown } from "lucide-react";
10 | import { num } from "starknet";
11 |
12 | export interface DelegateListActionsProps {
13 | onSortChange?: (sortMethod: "desc" | "random") => void;
14 | }
15 |
16 | export function DelegateListActions({
17 | onSortChange,
18 | }: DelegateListActionsProps) {
19 | const { address } = useAccount();
20 | const { sendAsync: delegateRealms } = useDelegateRealms({
21 | delegatee: address,
22 | });
23 | const { data: currentDelegate } = useCurrentDelegate();
24 |
25 | const name = useStarkDisplayName(
26 | num.toHex(currentDelegate ?? "") as `0x${string}`,
27 | );
28 |
29 | // New state for sort/filter method
30 | const [sortMethod, setSortMethod] = useState<"desc" | "random">("random");
31 |
32 | const toggleSortMethod = () => {
33 | // Toggle sort method and notify the parent via the onSortChange callback
34 | setSortMethod((prevMethod) => {
35 | const newMethod = prevMethod === "desc" ? "random" : "desc";
36 | if (onSortChange) {
37 | onSortChange(newMethod);
38 | }
39 | return newMethod;
40 | });
41 | };
42 |
43 | return (
44 |
45 | {currentDelegate ? (
46 | <>
47 |
48 |
49 | {name}
50 |
51 | >
52 | ) : (
53 | address && (
54 |
55 | )
56 | )}
57 |
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/bridge/useBridgeL2Realms.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useMemo } from "react";
2 | import { ERC721 } from "@/abi/L2/ERC721";
3 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
4 |
5 | import {
6 | useAccount,
7 | useReadContract,
8 | useSendTransaction,
9 | } from "@starknet-react/core";
10 |
11 | import {
12 | CollectionAddresses,
13 | REALMS_BRIDGE_ADDRESS,
14 | } from "@realms-world/constants";
15 |
16 | import { useERC721Approval } from "@/hooks/token/L2/useERC721Approval";
17 | import { useWriteInitiateWithdrawRealms } from "./useWriteInitiateWithdrawRealms";
18 |
19 | export function useBridgeL2Realms({
20 | selectedTokenIds,
21 | }: {
22 | selectedTokenIds: string[];
23 | }) {
24 | const l2BridgeAddress = REALMS_BRIDGE_ADDRESS[SUPPORTED_L2_CHAIN_ID];
25 | const { address } = useAccount();
26 | const l2RealmsAddress = CollectionAddresses.realms[
27 | SUPPORTED_L2_CHAIN_ID
28 | ] as `0x${string}`;
29 |
30 | const { data: isApprovedForAll } = useReadContract({
31 | abi: ERC721,
32 | address: l2RealmsAddress,
33 | args: l2BridgeAddress && address ? [address, l2BridgeAddress] : undefined,
34 | functionName: "is_approved_for_all",
35 | watch: true,
36 | });
37 |
38 | const { calls: approveCall } = useERC721Approval({
39 | contractAddress: l2RealmsAddress,
40 | operator: l2BridgeAddress as `0x${string}`,
41 | });
42 | const { calls: removeApprovalCall } = useERC721Approval({
43 | contractAddress: l2RealmsAddress,
44 | operator: l2BridgeAddress as `0x${string}`,
45 | removeApproval: true,
46 | });
47 | const { calls: depositCall } = useWriteInitiateWithdrawRealms({
48 | selectedTokenIds,
49 | });
50 |
51 | const depositCalls = useMemo(() => {
52 | return [...approveCall, ...depositCall, ...removeApprovalCall];
53 | }, [approveCall, depositCall, removeApprovalCall]);
54 |
55 | const { sendAsync, ...writeReturn } = useSendTransaction({
56 | calls: depositCalls,
57 | });
58 |
59 | //const transactions = useStore(useTransactionManager, (state) => state);
60 |
61 | const initiateWithdraw = useCallback(async () => {
62 | const tx = await sendAsync();
63 | /*transactions?.addTx({
64 | hash: tx.transaction_hash,
65 | type: TransactionType.BRIDGE_REALMS_L2_TO_L1_INITIATE,
66 | chainId: SUPPORTED_L2_CHAIN_ID,
67 | status: "pending",
68 | timestamp: new Date(Date.now()),
69 | });*/
70 |
71 | return tx;
72 | }, [sendAsync /*, transactions*/]);
73 |
74 | return {
75 | isApprovedForAll,
76 | initiateWithdraw,
77 | ...writeReturn,
78 | };
79 | }
80 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/realms/realm-resources.tsx:
--------------------------------------------------------------------------------
1 | import type { TokenMetadataAttribute } from "@/types/ark";
2 | // Assuming you're using a tooltip library
3 | import { Button } from "@/components/ui/button";
4 | import { ResourceIcon } from "@/components/ui/resource-icon";
5 | import {
6 | Tooltip,
7 | TooltipContent,
8 | TooltipProvider,
9 | TooltipTrigger,
10 | } from "@/components/ui/tooltip";
11 | import { useIsMobile } from "@/hooks/use-mobile";
12 |
13 | export default function RealmResources({
14 | traits,
15 | }: {
16 | traits: TokenMetadataAttribute[];
17 | }) {
18 | const resources = traits.filter((trait) => trait.trait_type === "Resource");
19 | const hiddenCount = resources.length - 4;
20 | const isMobile = useIsMobile();
21 |
22 | return (
23 |
24 | {resources.slice(0, isMobile ? 2 : 4).map((resource, index) => (
25 |
29 | {resource.value && (
30 |
31 | )}
32 |
33 | ))}
34 | {resources.length > (isMobile ? 2 : 4) && (
35 | <>
36 |
37 |
38 |
39 |
42 |
43 |
44 |
49 |
50 | {resources.slice(4).map((resource, index) => (
51 |
52 |
53 | {resource.value}
54 | {resource.value && (
55 |
60 | )}
61 |
62 |
63 | ))}
64 |
65 |
66 |
67 |
68 | >
69 | )}
70 |
71 | );
72 | }
73 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/utils.ts:
--------------------------------------------------------------------------------
1 | import type { ClassValue } from "clsx";
2 | import { clsx } from "clsx";
3 | import { env } from "env";
4 | import { validateAndParseAddress } from "starknet";
5 | import { twMerge } from "tailwind-merge";
6 |
7 | import { ChainId } from "@realms-world/constants";
8 |
9 | export const SUPPORTED_L1_CHAIN_ID =
10 | env.VITE_PUBLIC_CHAIN == "sepolia" ? ChainId.SEPOLIA : ChainId.MAINNET;
11 |
12 | export const SUPPORTED_L2_CHAIN_ID =
13 | SUPPORTED_L1_CHAIN_ID === ChainId.SEPOLIA
14 | ? ChainId.SN_SEPOLIA
15 | : ChainId.SN_MAIN;
16 |
17 | export function cn(...inputs: ClassValue[]) {
18 | return twMerge(clsx(inputs));
19 | }
20 |
21 | export function shortenAddress(str = "") {
22 | let formatted = formatAddress(str);
23 |
24 | // Only remove extra zeros after "0x" if there's at least one non-zero digit.
25 | if (/[^0]/.test(formatted.slice(2))) {
26 | formatted = formatted.replace(/^(0x)0+/, "$1");
27 | }
28 |
29 | return `${formatted.slice(0, 6)}...${formatted.slice(formatted.length - 4)}`;
30 | }
31 |
32 | export function formatAddress(address: string) {
33 | //if (address.length === 42) return getAddress(address);
34 | try {
35 | return validateAndParseAddress(address);
36 | } catch {
37 | return address;
38 | }
39 | }
40 | export function formatNumber(
41 | amount: number | null | undefined,
42 | maximumFractionDigits = 2,
43 | ) {
44 | if (amount === null || amount === undefined) {
45 | return "-";
46 | }
47 | const formatter = new Intl.NumberFormat("en-US", {
48 | maximumFractionDigits,
49 | });
50 |
51 | return formatter.format(amount);
52 | }
53 | export function abbreviateNumber(value: number | string): string {
54 | const num = typeof value === "number" ? value : parseFloat(value);
55 | if (isNaN(num)) return "0";
56 | if (Math.abs(num) < 1e3) return num.toString();
57 | if (Math.abs(num) >= 1e9)
58 | return (num / 1e9).toFixed(1).replace(/\.0$/, "") + "b";
59 | if (Math.abs(num) >= 1e6)
60 | return (num / 1e6).toFixed(1).replace(/\.0$/, "") + "m";
61 | if (Math.abs(num) >= 1e3)
62 | return (num / 1e3).toFixed(1).replace(/\.0$/, "") + "k";
63 | return num.toString();
64 | }
65 |
66 | export function shortenName(name: string, charLength?: number): string {
67 | if (name.length > (charLength ?? 18)) {
68 | const firstPart = name.substring(0, 22);
69 | return (firstPart + "...").toLowerCase();
70 | } else {
71 | return name.toLowerCase();
72 | }
73 | }
74 | export const trimAddress = (addr?: string): string => {
75 | if (!addr?.startsWith("0x")) return addr ?? "";
76 | return "0x" + addr.slice(2).replace(/^0+/, "");
77 | };
78 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/layout/nav-main.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import type { LucideIcon } from "lucide-react";
4 | import {
5 | Collapsible,
6 | CollapsibleContent,
7 | CollapsibleTrigger,
8 | } from "@/components/ui/collapsible";
9 | import {
10 | SidebarGroup,
11 | SidebarGroupLabel,
12 | SidebarMenu,
13 | SidebarMenuButton,
14 | SidebarMenuItem,
15 | SidebarMenuSub,
16 | SidebarMenuSubButton,
17 | SidebarMenuSubItem,
18 | } from "@/components/ui/sidebar";
19 | import { Link } from "@tanstack/react-router";
20 | import { ChevronRight } from "lucide-react";
21 |
22 | export function NavMain({
23 | items,
24 | label,
25 | }: {
26 | items: {
27 | title: string;
28 | url: string;
29 | icon?: LucideIcon;
30 | isActive?: boolean;
31 | items?: {
32 | title: string;
33 | url: string;
34 | }[];
35 | }[];
36 | label: string;
37 | }) {
38 | return (
39 |
40 | {label}
41 |
42 | {items.map((item) => (
43 |
49 |
50 |
51 |
52 | {item.icon && }
53 |
58 | {item.title}
59 |
60 | {item.items && (
61 |
62 | )}
63 |
64 |
65 |
66 |
67 | {item.items?.map((subItem) => (
68 |
69 |
70 |
74 | {subItem.title}
75 |
76 |
77 |
78 | ))}
79 |
80 |
81 |
82 |
83 | ))}
84 |
85 |
86 | );
87 | }
88 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/governance/use-vote-proposal.ts:
--------------------------------------------------------------------------------
1 | import type { Proposal } from "@/gql/graphql";
2 | import type { Choice } from "@/types/snapshot";
3 | import { useCallback, useState } from "react";
4 | import { StarkTxAuthenticator } from "@/abi/L2/StarkTxAuthenticator";
5 | import {
6 | useAccount,
7 | useContract,
8 | useSendTransaction,
9 | } from "@starknet-react/core";
10 | import { CairoCustomEnum, shortString } from "starknet";
11 |
12 | import { useIPFSPin } from "../use-ipfs-pin";
13 |
14 | export function getChoiceEnum(choice: 0 | 1 | 2) {
15 | return new CairoCustomEnum({
16 | Against: choice === 0 ? 0 : undefined,
17 | For: choice === 1 ? 1 : undefined,
18 | Abstain: choice === 2 ? 2 : undefined,
19 | });
20 | }
21 | export function getUserAddressEnum(
22 | type: "ETHEREUM" | "STARKNET" | "CUSTOM",
23 | address: string,
24 | ) {
25 | return new CairoCustomEnum({
26 | Starknet: type === "STARKNET" ? address : undefined,
27 | Ethereum: type === "ETHEREUM" ? address : undefined,
28 | Custom: type === "CUSTOM" ? address : undefined,
29 | });
30 | }
31 |
32 | export function useVoteProposal(proposal: Proposal) {
33 | const { address } = useAccount();
34 | const [selectedChoice, setSelectedChoice] = useState(null);
35 | const { pinToIPFS } = useIPFSPin();
36 |
37 | const { contract } = useContract({
38 | abi: StarkTxAuthenticator,
39 | address: proposal.space.authenticators[0] as `0x${string}`,
40 | });
41 |
42 | // Initialize with an empty calls array
43 | const { sendAsync, data: txHash, ...txState } = useSendTransaction({});
44 |
45 | const vote = useCallback(
46 | async (reason: string) => {
47 | if (!contract || !selectedChoice) return null;
48 | let pinned: { cid: string; provider: string } | null = null;
49 | if (reason) pinned = await pinToIPFS({ reason });
50 |
51 | try {
52 | return await sendAsync([
53 | contract.populate("authenticate_vote", [
54 | proposal.space.id,
55 | address as string,
56 | proposal.proposal_id,
57 | getChoiceEnum(selectedChoice),
58 | [{ index: 0, params: [] }], // ERC20Votes strategy
59 | pinned ? shortString.splitLongString(`ipfs://${pinned.cid}`) : [],
60 | ]),
61 | ]);
62 | // You'll need to adjust these parameters based on your contract's requirements
63 | } catch (error) {
64 | console.error("Error voting on proposal:", error);
65 | return null;
66 | }
67 | },
68 | [
69 | contract,
70 | selectedChoice,
71 | pinToIPFS,
72 | sendAsync,
73 | proposal.space.id,
74 | proposal.proposal_id,
75 | address,
76 | ],
77 | );
78 |
79 | return {
80 | selectedChoice,
81 | setSelectedChoice,
82 | vote,
83 | txHash,
84 | ...txState,
85 | };
86 | }
87 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-ipfs-pin.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useState } from "react";
2 | import { pin } from "@snapshot-labs/pineapple";
3 | import { create } from "ipfs-http-client";
4 |
5 | // Define a proper type for the payload instead of using 'any'
6 | export type IPFSPayload = Record;
7 |
8 | export interface PinResult {
9 | provider: string;
10 | cid: string;
11 | }
12 |
13 | export type PinFunction = (payload: IPFSPayload) => Promise;
14 |
15 | function createIpfsPinner(name: string, url: string): PinFunction {
16 | const client = create({ url });
17 |
18 | return async (payload: IPFSPayload) => {
19 | const res = await client.add(JSON.stringify(payload), { pin: true });
20 |
21 | return {
22 | provider: name,
23 | cid: res.cid.toV0().toString(),
24 | };
25 | };
26 | }
27 |
28 | const pinGraph = createIpfsPinner(
29 | "graph",
30 | "https://api.thegraph.com/ipfs/api/v0",
31 | );
32 |
33 | const pinMantle = createIpfsPinner(
34 | "mantle",
35 | "https://subgraph-api.mantle.xyz/ipfs",
36 | );
37 |
38 | async function pinPineapple(payload: IPFSPayload): Promise {
39 | const pinned = await pin(payload);
40 | if (!pinned) throw new Error("Failed to pin");
41 |
42 | return {
43 | provider: pinned.provider as string,
44 | cid: pinned.cid as string,
45 | };
46 | }
47 |
48 | export type PinProvider = "graph" | "mantle" | "pineapple";
49 |
50 | /**
51 | * React hook for pinning data to IPFS using different providers
52 | */
53 | export function useIPFSPin() {
54 | const [isLoading, setIsLoading] = useState(false);
55 | const [error, setError] = useState(null);
56 | const [result, setResult] = useState(null);
57 |
58 | const pinToIPFS = useCallback(
59 | async (
60 | payload: IPFSPayload,
61 | provider: PinProvider = "pineapple",
62 | ): Promise => {
63 | setIsLoading(true);
64 | setError(null);
65 | try {
66 | let pinResult: PinResult;
67 |
68 | switch (provider) {
69 | case "graph":
70 | pinResult = await pinGraph(payload);
71 | break;
72 | case "mantle":
73 | pinResult = await pinMantle(payload);
74 | break;
75 | case "pineapple":
76 | default:
77 | pinResult = await pinPineapple(payload);
78 | break;
79 | }
80 |
81 | setResult(pinResult);
82 | return pinResult;
83 | } catch (err) {
84 | const error =
85 | err instanceof Error
86 | ? err
87 | : new Error("Unknown error while pinning to IPFS");
88 | setError(error);
89 | return null;
90 | } finally {
91 | setIsLoading(false);
92 | }
93 | },
94 | [],
95 | );
96 |
97 | return {
98 | pinToIPFS,
99 | isLoading,
100 | error,
101 | result,
102 | };
103 | }
104 |
--------------------------------------------------------------------------------
/apps/account-portal/src/routes/realms.index.tsx:
--------------------------------------------------------------------------------
1 | import type { ErrorComponentProps } from "@tanstack/react-router";
2 | import { useEffect } from "react";
3 | import BridgeIcon from "@/components/icons/bridge.svg?react";
4 | import { RealmCard } from "@/components/modules/realms/realm-card";
5 | import { Button } from "@/components/ui/button";
6 | import { getAccountTokensQueryOptions } from "@/lib/eternum/getPortfolioCollections";
7 | import { formatAddress, SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
8 | import { useAccount } from "@starknet-react/core";
9 | import {
10 | useQueryErrorResetBoundary,
11 | useSuspenseQuery,
12 | } from "@tanstack/react-query";
13 | import {
14 | createFileRoute,
15 | ErrorComponent,
16 | Link,
17 | useRouter,
18 | } from "@tanstack/react-router";
19 | import { HandCoins } from "lucide-react";
20 |
21 | import { CollectionAddresses } from "@realms-world/constants";
22 |
23 | export class RealmsNotFoundError extends Error {}
24 |
25 | export const Route = createFileRoute("/realms/")({
26 | errorComponent: RealmsErrorComponent,
27 | component: RealmsComponent,
28 | });
29 | function RealmsErrorComponent({ error }: ErrorComponentProps) {
30 | const router = useRouter();
31 | const queryErrorResetBoundary = useQueryErrorResetBoundary();
32 |
33 | useEffect(() => {
34 | queryErrorResetBoundary.reset();
35 | }, [queryErrorResetBoundary]);
36 |
37 | if (error instanceof RealmsNotFoundError) {
38 | return {error.message}
;
39 | }
40 |
41 | return (
42 |
43 |
50 |
51 |
52 | );
53 | }
54 | function RealmsComponent() {
55 | const { address } = useAccount();
56 |
57 | const l2RealmsQuery = useSuspenseQuery(
58 | getAccountTokensQueryOptions({
59 | address: address,
60 | collectionAddress: CollectionAddresses.realms[
61 | SUPPORTED_L2_CHAIN_ID
62 | ] as string,
63 | }),
64 | );
65 | const l2Realms = l2RealmsQuery.data;
66 | if (!address) {
67 | return Connect Starknet Wallet to view your Realms
;
68 | }
69 |
70 | return (
71 |
72 |
73 |
74 |
77 |
78 |
79 |
82 |
83 |
84 |
85 | {l2Realms?.length
86 | ? l2Realms.map((realm) => {
87 | return (
88 |
89 | );
90 | })
91 | : "No Realms Found in wallet"}
92 |
93 |
94 | );
95 | }
96 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/modules/realms/bridge-tx-skeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import {
3 | Accordion,
4 | AccordionContent,
5 | AccordionItem,
6 | AccordionTrigger,
7 | } from "@/components/ui/accordion";
8 | import { Badge } from "@/components/ui/badge";
9 | import { Button } from "@/components/ui/button";
10 | import { cn } from "@/utils/utils";
11 | import { ArrowRight } from "lucide-react";
12 |
13 | const BridgeTransactionHistorySkeleton: React.FC = () => {
14 | return (
15 |
16 | {[...Array(3)].map((_, index) => (
17 |
18 |
19 |
20 |
28 |
29 |
30 | {[...Array(3)].map((_, idx) => (
31 |
36 | ))}
37 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
58 |
59 | {[...Array(2)].map((_, idx) => (
60 |
65 | ))}
66 |
67 |
68 |
69 |
70 | ))}
71 |
72 | );
73 | };
74 |
75 | export default BridgeTransactionHistorySkeleton;
76 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight } from "lucide-react"
3 | import { DayPicker } from "react-day-picker"
4 |
5 | import { cn } from "@/utils/utils"
6 | import { buttonVariants } from "@/components/ui/button"
7 |
8 | export type CalendarProps = React.ComponentProps
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: CalendarProps) {
16 | return (
17 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41 | : "[&:has([aria-selected])]:rounded-md"
42 | ),
43 | day: cn(
44 | buttonVariants({ variant: "ghost" }),
45 | "h-8 w-8 p-0 font-normal aria-selected:opacity-100"
46 | ),
47 | day_range_start: "day-range-start",
48 | day_range_end: "day-range-end",
49 | day_selected:
50 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
51 | day_today: "bg-accent text-accent-foreground",
52 | day_outside:
53 | "day-outside text-muted-foreground aria-selected:bg-accent/50 aria-selected:text-muted-foreground",
54 | day_disabled: "text-muted-foreground opacity-50",
55 | day_range_middle:
56 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
57 | day_hidden: "invisible",
58 | ...classNames,
59 | }}
60 | components={{
61 | IconLeft: ({ className, ...props }) => (
62 |
63 | ),
64 | IconRight: ({ className, ...props }) => (
65 |
66 | ),
67 | }}
68 | {...props}
69 | />
70 | )
71 | }
72 | Calendar.displayName = "Calendar"
73 |
74 | export { Calendar }
75 |
--------------------------------------------------------------------------------
/apps/account-portal/src/lib/snapshot/getUserVotes.ts:
--------------------------------------------------------------------------------
1 | /*import { networkId, opts } from "@/config";
2 | import {
3 | clone,
4 | formatProposal,
5 | isProposalWithMetadata,
6 | joinHighlightProposal,
7 | } from "@/utils/helpers";*/
8 | import { graphql } from "@/gql/snapshot";
9 | import { queryOptions } from "@tanstack/react-query";
10 | import { createServerFn } from "@tanstack/react-start";
11 | import { z } from "zod";
12 |
13 | import { execute } from "../queries/execute";
14 |
15 | export const VoteFieldsFragment = graphql(`
16 | fragment voteFields on Vote {
17 | id
18 | voter {
19 | id
20 | }
21 | space {
22 | id
23 | }
24 | metadata {
25 | reason
26 | }
27 | proposal
28 | choice
29 | vp
30 | created
31 | tx
32 | }
33 | `);
34 | const USER_VOTES_QUERY = graphql(`
35 | query UserVotes(
36 | $first: Int
37 | $skip: Int
38 | $spaceIds: [String]
39 | $voter: String
40 | ) {
41 | votes(
42 | first: $first
43 | skip: $skip
44 | orderBy: proposal
45 | orderDirection: desc
46 | where: { space_in: $spaceIds, voter: $voter }
47 | ) {
48 | ...voteFields
49 | }
50 | }
51 | `);
52 |
53 | /* -------------------------------------------------------------------------- */
54 | /* loadUserVotes Server Function */
55 | /* -------------------------------------------------------------------------- */
56 |
57 | // Define a Zod schema for the UserVotes request input.
58 | const LoadUserVotesInput = z.object({
59 | spaceIds: z.array(z.string()),
60 | voter: z.string(),
61 | limit: z.number().min(1),
62 | skip: z.number().min(0).default(0),
63 | });
64 |
65 | /**
66 | * This function wraps the loadUserVotes logic and uses generic fetch
67 | * to execute GraphQL POST calls instead of Apollo.
68 | */
69 | export const getUserVotes = createServerFn({ method: "POST" })
70 | .validator((input: unknown) => LoadUserVotesInput.parse(input))
71 | .handler(async (ctx) => {
72 | const { spaceIds, limit, skip, voter } = ctx.data;
73 | // Define variables for the UserVotes query.
74 | const variables = {
75 | first: limit,
76 | skip,
77 | space_in: spaceIds,
78 | voter: voter,
79 | };
80 |
81 | // Fetch UserVotes using the generic fetch helper.
82 | const UserVotesData = await execute(USER_VOTES_QUERY, variables);
83 |
84 | // Filter and format the UserVotes before returning them.
85 | return UserVotesData;
86 | });
87 |
88 | /* -------------------------------------------------------------------------- */
89 | /* React Query Options for loadUserVotes */
90 | /* -------------------------------------------------------------------------- */
91 |
92 | export const getUserVotesQueryOptions = (
93 | input: z.infer,
94 | ) =>
95 | queryOptions({
96 | queryKey: [
97 | "loadUserVotess",
98 | input.spaceIds,
99 | input.voter,
100 | input.limit,
101 | input.skip,
102 | ],
103 | queryFn: () => getUserVotes({ data: input }),
104 | enabled: !!input.voter,
105 | });
106 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/breadcrumb.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { ChevronRight, MoreHorizontal } from "lucide-react"
4 |
5 | import { cn } from "@/utils/utils"
6 |
7 | const Breadcrumb = React.forwardRef<
8 | HTMLElement,
9 | React.ComponentPropsWithoutRef<"nav"> & {
10 | separator?: React.ReactNode
11 | }
12 | >(({ ...props }, ref) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:w-3.5 [&>svg]:h-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/use-l2-realms-claims.ts:
--------------------------------------------------------------------------------
1 | import type { BlockNumber, Call } from "starknet";
2 | import { useCallback, useEffect, useMemo, useState } from "react";
3 | import { RealmsABI } from "@/abi/L2/Realms";
4 | import { SUPPORTED_L2_CHAIN_ID } from "@/utils/utils";
5 | import {
6 | useAccount,
7 | useContract,
8 | useReadContract,
9 | useSendTransaction,
10 | } from "@starknet-react/core";
11 | import { BlockTag } from "starknet";
12 | import { formatEther } from "viem";
13 |
14 | import { CollectionAddresses } from "@realms-world/constants";
15 |
16 | import { useToast } from "./use-toast";
17 |
18 | export const useL2RealmsClaims = () => {
19 | const { toast } = useToast();
20 | const { address: l2Address } = useAccount();
21 | const l2RealmsAddress = CollectionAddresses.realms[
22 | SUPPORTED_L2_CHAIN_ID
23 | ] as `0x${string}`;
24 |
25 | const [previousBalance, setPreviousBalance] = useState(null);
26 | const [easedBalance, setEasedBalance] = useState(null);
27 |
28 | const { data: balance, isFetching } = useReadContract({
29 | address: l2RealmsAddress,
30 | abi: RealmsABI,
31 | functionName: "get_reward_balance_for",
32 | enabled: !!l2Address,
33 | watch: true,
34 | args: l2Address ? [l2Address] : undefined,
35 | blockIdentifier: BlockTag.PENDING as BlockNumber,
36 | });
37 |
38 | useEffect(() => {
39 | if (balance !== undefined && balance !== previousBalance) {
40 | const start = previousBalance ?? BigInt(Number(balance));
41 | const end = BigInt(Number(balance));
42 | const duration = 1000; // duration of the easing in ms
43 | const startTime = Date.now();
44 |
45 | const ease = () => {
46 | const now = Date.now();
47 | const elapsed = now - startTime;
48 | const progress = Math.min(elapsed / duration, 1);
49 | const easedValue =
50 | start + BigInt(Math.round(Number(end - start) * progress));
51 |
52 | setEasedBalance(easedValue);
53 |
54 | if (progress < 1) {
55 | requestAnimationFrame(ease);
56 | } else {
57 | setPreviousBalance(BigInt(Number(balance)));
58 | }
59 | };
60 |
61 | ease();
62 | }
63 | }, [balance, previousBalance, isFetching]);
64 |
65 | const { contract } = useContract({
66 | abi: RealmsABI,
67 | address: l2RealmsAddress,
68 | });
69 |
70 | const calls: Call[] = useMemo(() => {
71 | if (!l2Address || !contract) return [];
72 | return [contract.populate("reward_claim", [])];
73 | }, [l2Address, contract]);
74 |
75 | const {
76 | sendAsync,
77 | data: claimHash,
78 | isPending: isSubmitting,
79 | } = useSendTransaction({ calls });
80 |
81 | const claimRewards = useCallback(async () => {
82 | const tx = await sendAsync();
83 | if (tx.transaction_hash) {
84 | toast({
85 | title: "Realms' Lords Claim Submitted",
86 | description: `Claim of ${formatEther(balance as bigint)} Lords in progress`,
87 | });
88 | }
89 | return tx;
90 | }, [sendAsync, toast, balance]);
91 |
92 | return {
93 | balance: easedBalance,
94 | calls,
95 | isFetching,
96 | isSubmitting,
97 | claimRewards,
98 | claimHash,
99 | };
100 | };
101 |
--------------------------------------------------------------------------------
/apps/account-portal/src/hooks/useSimulateTransactions.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | QueryKey,
3 | UseQueryOptions as UseQueryOptions_,
4 | UseQueryResult,
5 | } from "@tanstack/react-query";
6 | import type {
7 | AccountInterface,
8 | Call,
9 | Invocations,
10 | SimulateTransactionDetails,
11 | SimulateTransactionResponse,
12 | } from "starknet";
13 | import { useMemo } from "react";
14 | import { useAccount, useInvalidateOnBlock } from "@starknet-react/core";
15 | import { useQuery } from "@tanstack/react-query";
16 | import { TransactionType } from "starknet";
17 |
18 | export interface SimulateTransactionsArgs {
19 | /** List of smart contract calls to simulate. */
20 | calls?: Call[];
21 | /** Simualte Transaction options. */
22 | options?: SimulateTransactionDetails;
23 | }
24 | type UseQueryProps<
25 | TQueryFnData = unknown,
26 | TError = unknown,
27 | TData = TQueryFnData,
28 | TQueryKey extends QueryKey = QueryKey,
29 | > = Pick<
30 | UseQueryOptions_,
31 | "enabled" | "refetchInterval" | "retry" | "retryDelay"
32 | >;
33 | /** Options for `useSimulateTransactions`. */
34 | export type UseSimulateTransactionsProps = SimulateTransactionsArgs &
35 | UseQueryProps<
36 | SimulateTransactionResponse,
37 | Error,
38 | SimulateTransactionResponse,
39 | ReturnType
40 | > & {
41 | /** Refresh data at every block. */
42 | watch?: boolean;
43 | };
44 |
45 | /** Value returned from `useSimulateTransactions`. */
46 | export type UseSimulateTransactionsResult = UseQueryResult<
47 | SimulateTransactionResponse,
48 | Error
49 | >;
50 |
51 | /**
52 | * Hook to estimate fees for smart contract calls.
53 | *
54 | * @remarks
55 | *
56 | * The hook only performs estimation if the `calls` is not undefined.
57 | */
58 | export function useSimulateTransactions({
59 | calls,
60 | options,
61 | watch = false,
62 | enabled: enabled_ = true,
63 | ...props
64 | }: UseSimulateTransactionsProps): UseSimulateTransactionsResult {
65 | const { account } = useAccount();
66 |
67 | const queryKey_ = useMemo(
68 | () => queryKey({ calls, options }),
69 | [calls, options],
70 | );
71 |
72 | const enabled = useMemo(() => Boolean(enabled_ && calls), [enabled_, calls]);
73 |
74 | useInvalidateOnBlock({
75 | enabled: Boolean(enabled && watch),
76 | queryKey: queryKey_,
77 | });
78 |
79 | return useQuery({
80 | queryKey: queryKey_,
81 | queryFn: queryFn({
82 | account,
83 | calls,
84 | options,
85 | }),
86 | enabled,
87 | ...props,
88 | });
89 | }
90 |
91 | function queryKey({ calls, options }: SimulateTransactionsArgs) {
92 | return [
93 | {
94 | entity: "simulateTransactions",
95 | calls,
96 | options,
97 | },
98 | ] as const;
99 | }
100 |
101 | function queryFn({
102 | account,
103 | calls,
104 | options,
105 | }: { account?: AccountInterface } & SimulateTransactionsArgs) {
106 | return async () => {
107 | if (!account) throw new Error("account is required");
108 | if (!calls || calls.length === 0) throw new Error("calls are required");
109 | const callMap = calls.map((call) => ({
110 | type: TransactionType.INVOKE,
111 | ...call,
112 | }));
113 | return account.simulateTransaction(callMap as Invocations, options);
114 | };
115 | }
116 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
3 |
4 | import { cn } from "@/utils/utils"
5 | import type { ButtonProps} from "@/components/ui/button";
6 | import { buttonVariants } from "@/components/ui/button"
7 |
8 | const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
9 |
15 | )
16 | Pagination.displayName = "Pagination"
17 |
18 | const PaginationContent = React.forwardRef<
19 | HTMLUListElement,
20 | React.ComponentProps<"ul">
21 | >(({ className, ...props }, ref) => (
22 |
27 | ))
28 | PaginationContent.displayName = "PaginationContent"
29 |
30 | const PaginationItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentProps<"li">
33 | >(({ className, ...props }, ref) => (
34 |
35 | ))
36 | PaginationItem.displayName = "PaginationItem"
37 |
38 | type PaginationLinkProps = {
39 | isActive?: boolean
40 | } & Pick &
41 | React.ComponentProps<"a">
42 |
43 | const PaginationLink = ({
44 | className,
45 | isActive,
46 | size = "icon",
47 | ...props
48 | }: PaginationLinkProps) => (
49 |
60 | )
61 | PaginationLink.displayName = "PaginationLink"
62 |
63 | const PaginationPrevious = ({
64 | className,
65 | ...props
66 | }: React.ComponentProps) => (
67 |
73 |
74 | Previous
75 |
76 | )
77 | PaginationPrevious.displayName = "PaginationPrevious"
78 |
79 | const PaginationNext = ({
80 | className,
81 | ...props
82 | }: React.ComponentProps) => (
83 |
89 | Next
90 |
91 |
92 | )
93 | PaginationNext.displayName = "PaginationNext"
94 |
95 | const PaginationEllipsis = ({
96 | className,
97 | ...props
98 | }: React.ComponentProps<"span">) => (
99 |
104 |
105 | More pages
106 |
107 | )
108 | PaginationEllipsis.displayName = "PaginationEllipsis"
109 |
110 | export {
111 | Pagination,
112 | PaginationContent,
113 | PaginationLink,
114 | PaginationItem,
115 | PaginationPrevious,
116 | PaginationNext,
117 | PaginationEllipsis,
118 | }
119 |
--------------------------------------------------------------------------------
/apps/account-portal/src/utils/cursorPagination.ts:
--------------------------------------------------------------------------------
1 | import type { AnyColumn, SQL } from "@realms-world/db";
2 | import { and, asc, desc, eq, gt, lt, or } from "@realms-world/db";
3 |
4 | // With multiple cursors
5 | export function withCursorPagination<
6 | PrimaryColumn extends AnyColumn,
7 | // Make sure the secondary column is from the same table as the primary column:
8 | TableColumns extends PrimaryColumn["table"]["_"]["columns"],
9 | SecondaryColumn extends TableColumns[keyof TableColumns],
10 | PrimaryCursor extends
11 | | ReturnType
12 | | undefined,
13 | SecondaryCursor extends
14 | | ReturnType
15 | | undefined,
16 | Order extends "asc" | "desc",
17 | >({
18 | cursors,
19 | limit,
20 | where: inputWhere,
21 | }: { limit: number; where?: SQL } & (
22 | | {
23 | cursors: [[PrimaryColumn, Order] | [PrimaryColumn, Order, PrimaryCursor]]; // A single unique + sequential field
24 | }
25 | | {
26 | cursors: [
27 | [PrimaryColumn, Order] | [PrimaryColumn, Order, PrimaryCursor], // A non-unique sequential field
28 | [SecondaryColumn, Order] | [SecondaryColumn, Order, SecondaryCursor], // A unique field
29 | ];
30 | }
31 | )): {
32 | orderBy: SQL[];
33 | limit: number;
34 | where?: SQL;
35 | } {
36 | // Primary cursor
37 | const primaryColumn = cursors[0][0];
38 | const primaryOrder = cursors[0][1] === "asc" ? asc : desc;
39 | const primaryOperator = cursors[0][1] === "asc" ? gt : lt;
40 | const primaryCursor = cursors[0][2];
41 |
42 | // Secondary cursor (unique fallback like an id field for a stable sort)
43 | const secondaryColumn = cursors[1] ? cursors[1][0] : null;
44 | const secondaryOrder = cursors[1]
45 | ? cursors[1][1] === "asc"
46 | ? asc
47 | : desc
48 | : null;
49 | const secondaryOperator = cursors[1]
50 | ? cursors[1][1] === "asc"
51 | ? gt
52 | : lt
53 | : null;
54 | const secondaryCursor = cursors[1] ? cursors[1][2] : undefined;
55 |
56 | // Single cursor pagination
57 | const singleColumnPaginationWhere =
58 | typeof primaryCursor !== "undefined"
59 | ? primaryOperator(primaryColumn, primaryCursor)
60 | : undefined;
61 |
62 | // Double cursor pagination
63 | const doubleColumnPaginationWhere =
64 | secondaryColumn &&
65 | secondaryOperator &&
66 | typeof primaryCursor !== "undefined" &&
67 | typeof secondaryCursor !== "undefined"
68 | ? or(
69 | primaryOperator(primaryColumn, primaryCursor),
70 | and(
71 | eq(primaryColumn, primaryCursor),
72 | secondaryOperator(secondaryColumn, secondaryCursor),
73 | ),
74 | )
75 | : undefined;
76 |
77 | // Generate the final where clause
78 | const paginationWhere = secondaryColumn
79 | ? doubleColumnPaginationWhere
80 | : singleColumnPaginationWhere;
81 |
82 | const where = inputWhere
83 | ? paginationWhere
84 | ? and(inputWhere, paginationWhere)
85 | : inputWhere
86 | : paginationWhere;
87 |
88 | // Return object which can be easily spread into a query
89 | return {
90 | orderBy: [
91 | primaryOrder(primaryColumn),
92 | ...(secondaryColumn && secondaryOrder
93 | ? [secondaryOrder(secondaryColumn)]
94 | : []),
95 | ],
96 | limit,
97 | ...(where ? { where } : {}),
98 | };
99 | }
100 |
--------------------------------------------------------------------------------
/apps/account-portal/src/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/utils/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | [role=checkbox]]:translate-y-[2px]",
77 | className
78 | )}
79 | {...props}
80 | />
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
| |