├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .gitignore
├── .gitmodules
├── .vscode
└── settings.json
├── README.md
├── app
├── api
│ ├── ai-review
│ │ └── route.ts
│ ├── auth
│ │ └── [...nextauth]
│ │ │ └── route.ts
│ ├── discord-announcement
│ │ └── route.ts
│ ├── memory
│ │ └── route.ts
│ ├── message
│ │ └── route.ts
│ ├── profile
│ │ ├── query
│ │ │ └── route.ts
│ │ └── sign
│ │ │ └── route.ts
│ ├── self-check
│ │ └── route.ts
│ ├── social
│ │ ├── discord
│ │ │ ├── check-guild
│ │ │ │ └── route.ts
│ │ │ └── verify
│ │ │ │ └── route.ts
│ │ ├── github
│ │ │ └── verify
│ │ │ │ └── route.ts
│ │ └── twitter
│ │ │ ├── check-actions
│ │ │ └── route.ts
│ │ │ └── verify
│ │ │ └── route.ts
│ └── upload
│ │ └── route.ts
├── board
│ └── [id]
│ │ └── page.tsx
├── boards
│ ├── create
│ │ └── page.tsx
│ ├── joined
│ │ └── page.tsx
│ └── page.tsx
├── favicon.ico
├── globals.css
├── layout.tsx
└── page.tsx
├── assets
├── BountyBoard.png
├── Screenshot.png
├── ScreenshotHome.png
└── ScreenshotTask.png
├── components.json
├── components
├── BoardActionsDropdown.tsx
├── BoardCard.tsx
├── BoardForm.tsx
├── Boards.tsx
├── BoardsPageSkeleton.tsx
├── ConnectWalletButton.tsx
├── CreateTaskModal.tsx
├── DynamicModal.tsx
├── ImageUpload.tsx
├── MemberSubmissionTable.tsx
├── Navigation.tsx
├── ProfileSettingsModal.tsx
├── SubmissionDetailsModal.tsx
├── SubmitProofModal.tsx
├── TaskList.tsx
└── ui
│ ├── Address.tsx
│ ├── alert.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── calendar.tsx
│ ├── card.tsx
│ ├── checkbox.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── loading.tsx
│ ├── popover.tsx
│ ├── select.tsx
│ ├── skeleton.tsx
│ ├── table.tsx
│ ├── tabs.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ ├── tooltip.tsx
│ └── use-toast.ts
├── constants
├── BountyBoard.json
├── UserProfile.json
├── attestaion.ts
└── contract-address.ts
├── contract
├── EVM
│ ├── .github
│ │ └── workflows
│ │ │ └── test.yml
│ ├── .gitignore
│ ├── foundry.toml
│ ├── script
│ │ ├── BountyBoard.s.sol
│ │ ├── DeployMockERC20.s.sol
│ │ ├── MintToken.s.sol
│ │ └── UserProfile.s.sol
│ ├── src
│ │ ├── BoardStorage.sol
│ │ ├── BoardView.sol
│ │ ├── BountyBoard.sol
│ │ ├── BountyBoardold.sol
│ │ ├── IBountyBoard.sol
│ │ ├── MockERC20.sol
│ │ ├── SubmissionManager.sol
│ │ ├── TaskManager.sol
│ │ └── UserProfile.sol
│ └── test
│ │ ├── BountyBoard.t.sol
│ │ └── UserProfile.t.sol
└── SVM
│ ├── Cargo.toml
│ └── src
│ ├── error.rs
│ ├── instruction.rs
│ ├── lib.rs
│ ├── processor.rs
│ └── state.rs
├── eliza-add
├── agent
│ └── src
│ │ └── index.ts
├── characters
│ └── BountyBoard.character.json
└── packages
│ ├── client-direct
│ └── src
│ │ └── index.ts
│ ├── client-discord
│ └── src
│ │ ├── actions
│ │ ├── announcement.ts
│ │ ├── chat_with_attachments.ts
│ │ ├── download_media.ts
│ │ ├── joinvoice.ts
│ │ ├── leavevoice.ts
│ │ ├── summarize_conversation.ts
│ │ └── transcribe_media.ts
│ │ ├── attachments.ts
│ │ ├── enviroment.ts
│ │ ├── index.ts
│ │ ├── messages.ts
│ │ ├── providers
│ │ ├── channelState.ts
│ │ └── voiceState.ts
│ │ ├── templates.ts
│ │ ├── utils.ts
│ │ └── voice.ts
│ └── plugin-bountyboard-evm
│ ├── package.json
│ ├── src
│ ├── actions
│ │ ├── BountyBoard.json
│ │ ├── query.ts
│ │ └── review.ts
│ ├── index.ts
│ ├── providers
│ │ └── wallet.ts
│ ├── templates
│ │ └── index.ts
│ └── types
│ │ └── index.ts
│ ├── tsconfig.json
│ └── tsup.config.ts
├── graphql
└── queries.ts
├── hooks
├── useAddressProfiles.ts
├── useContract.ts
└── useMediaQuery.ts
├── lib
├── utils.ts
└── uuid.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── providers
├── TelegramAuthContext.tsx
├── Web3Providers.tsx
├── config.ts
├── monad.ts
└── my-anvil.ts
├── public
├── home
│ ├── CommunityBuilding.jpg
│ ├── DeveloperEducation.jpg
│ └── ProductTesting.jpg
├── index-head.png
├── logo.png
├── logo.svg
├── next.svg
└── vercel.svg
├── scripts
└── generate-aes-key.ts
├── services
└── aiReview.ts
├── store
└── userStore.ts
├── tailwind.config.ts
├── tsconfig.json
├── types
├── profile.ts
└── types.ts
└── utils
├── chain.ts
├── encryption-server.ts
└── encryption.ts
/.env.example:
--------------------------------------------------------------------------------
1 | TWITTER_CLIENT_ID=
2 | TWITTER_CLIENT_SECRET=
3 | TWITTER_BEARER_TOKEN=
4 | DISCORD_CLIENT_ID=
5 | DISCORD_CLIENT_SECRET=
6 | DISCORD_BOT_TOKEN=
7 | GITHUB_CLIENT_ID=
8 | GITHUB_CLIENT_SECRET=
9 | NEXTAUTH_URL=https://localhost:3000
10 | NEXT_PUBLIC_API_URL=https://localhost:3000
11 |
12 | ELIZA_API_URL=http://localhost:3030
13 | ELIZA_AGENT_ID=
14 | ELIZA_AGENT_USER_ID=
15 | DISCORD_AGENT_BOT=
16 | SMMS_TOKEN=
17 |
18 | SIGNER_ADDRESS_PRIVATE_KEY=
19 |
20 | # Particle Network Configuration
21 | NEXT_PUBLIC_PARTICLE_PROJECT_ID=
22 | NEXT_PUBLIC_PARTICLE_CLIENT_KEY=
23 | NEXT_PUBLIC_PARTICLE_APP_ID=
24 |
25 | # WalletConnect
26 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
27 |
28 | # Social Auth
29 | TWITTER_CLIENT_ID=
30 | TWITTER_CLIENT_SECRET=
31 | DISCORD_CLIENT_ID=
32 | DISCORD_CLIENT_SECRET=
33 | GITHUB_CLIENT_ID=
34 | GITHUB_CLIENT_SECRET=
35 |
36 | # Other configurations
37 | NEXT_PUBLIC_GATEWAY_URL=
38 | NEXT_PUBLIC_IPFS_GATEWAY=
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .next
3 | out
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["next/core-web-vitals"],
3 | "parserOptions": {
4 | "project": "./tsconfig.json"
5 | },
6 | "rules": {
7 | "@typescript-eslint/no-explicit-any": "warn",
8 | "@typescript-eslint/no-unused-vars": "warn",
9 | "react-hooks/rules-of-hooks": "error",
10 | "react-hooks/exhaustive-deps": "warn"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 |
35 | # typescript
36 | *.tsbuildinfo
37 | next-env.d.ts
38 |
39 | run-*.json
40 |
41 | certificates
42 |
43 | /draft
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "contract/EVM/lib/forge-std"]
2 | path = contract/EVM/lib/forge-std
3 | url = https://github.com/foundry-rs/forge-std
4 | [submodule "contract/EVM/lib/openzeppelin-contracts"]
5 | path = contract/EVM/lib/openzeppelin-contracts
6 | url = https://github.com/OpenZeppelin/openzeppelin-contracts
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "i18n-ally.localesPaths": [
3 | "contract/lib/linea-attestation-registry/explorer/src/assets/locales",
4 | "contract/lib/linea-attestation-registry/snap/packages/snap/locales"
5 | ]
6 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # BountyBoard
2 |
3 | 
4 |
5 | BountyBoard is a decentralized Web3 community engagement platform built on blockchain technology, aiming to automate task management and reward distribution, thereby increasing community participation and efficiency.
6 |
7 | ## Key Advantages:
8 |
9 | * **Decentralization**: Utilizes blockchain technology to ensure transparency and trust in platform operations.
10 | * **Smart Contracts**: Automates task management and reward distribution using smart contracts, improving efficiency and transparency.
11 | * **AI-Agent (Eliza)**: Provides intelligent Q&A, task auditing, data analysis, and task notifications to enhance community interaction and efficiency.
12 | * **User-Friendly Interface**: Offers a simple and intuitive interface for users to create and manage tasks, track progress, and monitor participation.
13 |
14 | ## Key Features
15 |
16 | **Task Management**:
17 |
18 | * **Multiple Task Types**: Supports various task types to cater to different community activities and engagement needs.
19 | * **Customizable Completion Criteria**: Allows users to set specific conditions for task completion, ensuring tasks are tailored to individual project requirements.
20 | * **Progress Tracking Dashboard**: Provides real-time monitoring of task progress and participant performance.
21 |
22 | **Reward Distribution**:
23 |
24 | * **Smart Contract Automation**: Automates reward distribution using smart contracts, ensuring transparency and fairness.
25 | * **Multiple Token Support**: Supports various tokens for rewards, providing flexibility to cater to different community preferences.
26 |
27 | **Community Management**:
28 |
29 | * **AI-Agent (Eliza)**: Offers intelligent Q&A, task auditing, data analysis, and task notifications to enhance community interaction and efficiency.
30 | * **Community Verification**: Enables community validation of AI-Agent operations, ensuring privacy and security.
31 |
32 | ## Start
33 |
34 | ### Install
35 |
36 | First, install the dependencies:
37 |
38 | ```bash
39 | pnpm install
40 | ```
41 |
42 | Second, set the environment variables:
43 |
44 | ```bash
45 | cp .env.example .env
46 | ```
47 |
48 | ### Run
49 |
50 | ```bash
51 | pnpm dev
52 | ```
53 |
54 | ## Set up the agent
55 |
56 | First, clone the agent repository:
57 |
58 | ```bash
59 | git clone https://github.com/ai16z/eliza.git
60 | ```
61 |
62 | Second, set the environment variables, you need to set the `DISCORD_APPLICATION_ID`, `DISCORD_API_TOKEN`,`BOUNTYBOARD_PRIVATE_KEY`(the private key of the account that will review the tasks), `BOUNTYBOARD_ADDRESS`, `GAIANET_MODEL`, `GAIANET_SERVER_URL`, `GAIANET_EMBEDDING_MODEL`, `USE_GAIANET_EMBEDDING`, `DSTACK_SIMULATOR_ENDPOINT`(the endpoint of the TEE simulator), `WALLET_SECRET_SALT`(the TEE secret salt) in the `.env` file.
63 |
64 | ```bash
65 | cp .env.example .env
66 | ```
67 |
68 | Third, install the dependencies:
69 |
70 | ```bash
71 | pnpm install
72 | ```
73 |
74 | Fourth, copy the `eliza-add` directory to the `eliza` directory:
75 |
76 | Then, you can build and start the agent:
77 |
78 | ```bash
79 | pnpm build
80 | pnpm start --characters="characters/BountyBoard.character.json"
81 | ```
82 |
83 | ## Screenshot
84 |
85 | **Home**
86 |
87 | 
88 |
89 | **Task**
90 |
91 | 
92 |
93 |
--------------------------------------------------------------------------------
/app/api/ai-review/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { AIReviewService } from "@/services/aiReview";
3 |
4 | const aiReviewService = new AIReviewService();
5 |
6 | export async function POST(req: NextRequest) {
7 | try {
8 | const requestData = await req.json();
9 | const result = await aiReviewService.review(requestData);
10 | return NextResponse.json(result);
11 | } catch (error) {
12 | console.error("AI Review API error:", error);
13 | return NextResponse.json(
14 | { error: error instanceof Error ? error.message : "AI Review failed" },
15 | { status: 500 }
16 | );
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { NextAuthOptions } from "next-auth";
2 | import TwitterProvider from "next-auth/providers/twitter";
3 | import DiscordProvider from "next-auth/providers/discord";
4 | import GithubProvider from "next-auth/providers/github";
5 |
6 | export const authOptions: NextAuthOptions = {
7 | providers: [
8 | TwitterProvider({
9 | clientId: process.env.TWITTER_CLIENT_ID!,
10 | clientSecret: process.env.TWITTER_CLIENT_SECRET!,
11 | version: "2.0",
12 | authorization: {
13 | params: {
14 | scope: [
15 | "tweet.read",
16 | "users.read",
17 | "follows.read",
18 | "like.read"
19 | ].join(" ")
20 | }
21 | },
22 | httpOptions: {
23 | timeout: 1000000,
24 | },
25 | }),
26 | DiscordProvider({
27 | clientId: process.env.DISCORD_CLIENT_ID!,
28 | clientSecret: process.env.DISCORD_CLIENT_SECRET!,
29 | authorization: {
30 | params: {
31 | scope: 'identify email guilds',
32 | },
33 | },
34 | }),
35 | GithubProvider({
36 | clientId: process.env.GITHUB_CLIENT_ID!,
37 | clientSecret: process.env.GITHUB_CLIENT_SECRET!,
38 | authorization: {
39 | params: {
40 | scope: 'read:user user:email',
41 | },
42 | },
43 | }),
44 | ],
45 | callbacks: {
46 | async jwt({ token, account }: { token: any; account: any; }) {
47 | if (account) {
48 | token.id = account.providerAccountId;
49 | token.accessToken = account.access_token;
50 | }
51 | return token;
52 | },
53 | async session({ session, token }: { session: any; token: any }) {
54 | if (session.user) {
55 | session.user.id = token.id as string;
56 | (session as any).accessToken = token.accessToken;
57 | }
58 | return session;
59 | },
60 | async redirect({ url, baseUrl }) {
61 | return url.startsWith(baseUrl)
62 | ? url.replace('http://', 'https://')
63 | : baseUrl.replace('http://', 'https://');
64 | },
65 | },
66 | debug: process.env.NODE_ENV === 'development',
67 | };
68 |
69 | const handler = NextAuth(authOptions);
70 | export { handler as GET, handler as POST };
--------------------------------------------------------------------------------
/app/api/discord-announcement/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { stringToUuid } from "@/lib/uuid";
3 |
4 | const elizaAgentUserId = process.env.ELIZA_AGENT_USER_ID || "";
5 | const elizaAgentId = process.env.ELIZA_AGENT_ID || "";
6 | const elizaAgentUrl = `${process.env.ELIZA_API_URL}/${elizaAgentId}/message`;
7 | const DISCORD_BOT_TOKEN = process.env.DISCORD_BOT_TOKEN;
8 | const GOOGLE_API_KEY = process.env.GOOGLE_API_KEY;
9 | const GEMINI_API_URL = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent';
10 | const TELEGRAM_BOT_TOKEN = process.env.TELEGRAM_BOT_TOKEN;
11 | const TELEGRAM_CHAT_ID = process.env.TELEGRAM_CHAT_ID;
12 |
13 | export async function POST(req: NextRequest) {
14 | try {
15 | const { channelId, type, data } = await req.json();
16 |
17 | if (!channelId || !type || !data) {
18 | return NextResponse.json(
19 | { error: "Missing required fields" },
20 | { status: 400 }
21 | );
22 | }
23 |
24 | let announcementText = '';
25 |
26 | if (elizaAgentUrl && elizaAgentId) {
27 | const aiResponse = await fetch(elizaAgentUrl, {
28 | method: "POST",
29 | headers: {
30 | "Content-Type": "application/json",
31 | },
32 | body: JSON.stringify({
33 | roomId: stringToUuid(channelId + "-" + elizaAgentId),
34 | userId: stringToUuid(elizaAgentUserId),
35 | userName: "user",
36 | content: {
37 | text: `Please format the following information into a clear and concise announcement. Use appropriate emojis and maintain a professional tone. Focus only on the essential details:
38 | Type: ${type}
39 | Content: ${JSON.stringify(data)}`,
40 | attachments: [],
41 | source: "direct",
42 | },
43 | }),
44 | });
45 |
46 | if (!aiResponse.ok) {
47 | const errorDetails = await aiResponse.json();
48 | throw new Error(errorDetails.error || "Failed to get AI response");
49 | }
50 |
51 | const reader = aiResponse.body?.getReader();
52 | let aiContent = "";
53 |
54 | if (reader) {
55 | while (true) {
56 | const { done, value } = await reader.read();
57 | if (done) break;
58 | const chunk = new TextDecoder().decode(value);
59 | aiContent += chunk;
60 | }
61 | }
62 |
63 | let responses;
64 | try {
65 | const parsedContent = JSON.parse(aiContent);
66 | if (Array.isArray(parsedContent)) {
67 | responses = parsedContent;
68 | } else if (parsedContent.responses) {
69 | responses = parsedContent.responses;
70 | } else {
71 | responses = [parsedContent];
72 | }
73 | announcementText = responses[0].content?.text || responses[0].text;
74 | } catch (error) {
75 | console.error("Eliza Parse Error:", error);
76 | throw new Error("Failed to parse Eliza response");
77 | }
78 | }
79 | else if (GOOGLE_API_KEY) {
80 | const prompt = {
81 | announcement_request: {
82 | type,
83 | content: data
84 | },
85 | instructions: "Format the given information into a clear and concise announcement. Use appropriate emojis and maintain a professional tone. Focus on essential details. Return only the formatted announcement text without any JSON structure or additional formatting.",
86 | example_response: "🎉 New Update Available!\nWe're excited to announce the latest features...",
87 | };
88 |
89 | const geminiResponse = await fetch(`${GEMINI_API_URL}?key=${GOOGLE_API_KEY}`, {
90 | method: 'POST',
91 | headers: {
92 | 'Content-Type': 'application/json',
93 | },
94 | body: JSON.stringify({
95 | contents: [{
96 | parts: [{
97 | text: JSON.stringify(prompt)
98 | }]
99 | }]
100 | })
101 | });
102 |
103 | if (!geminiResponse.ok) {
104 | throw new Error('Failed to get Gemini API response');
105 | }
106 |
107 | const geminiData = await geminiResponse.json();
108 | announcementText = geminiData.candidates[0].content.parts[0].text.trim();
109 | } else {
110 | throw new Error("No AI service configured");
111 | }
112 |
113 | if (!announcementText) {
114 | throw new Error("Could not generate announcement text");
115 | }
116 |
117 | if (DISCORD_BOT_TOKEN) {
118 | const discordResponse = await fetch(
119 | `https://discord.com/api/v10/channels/${channelId}/messages`,
120 | {
121 | method: "POST",
122 | headers: {
123 | Authorization: `Bot ${DISCORD_BOT_TOKEN}`,
124 | "Content-Type": "application/json",
125 | },
126 | body: JSON.stringify({
127 | content: announcementText,
128 | }),
129 | }
130 | );
131 |
132 | if (!discordResponse.ok) {
133 | const errorData = await discordResponse.json();
134 | throw new Error(errorData.message || "Failed to send Discord message");
135 | }
136 | }
137 |
138 | if (TELEGRAM_BOT_TOKEN && TELEGRAM_CHAT_ID) {
139 | const telegramResponse = await fetch(
140 | `https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage`,
141 | {
142 | method: "POST",
143 | headers: {
144 | "Content-Type": "application/json",
145 | },
146 | body: JSON.stringify({
147 | chat_id: TELEGRAM_CHAT_ID,
148 | text: announcementText,
149 | parse_mode: "HTML",
150 | }),
151 | }
152 | );
153 |
154 | if (!telegramResponse.ok) {
155 | const errorData = await telegramResponse.json();
156 | throw new Error(errorData.description || "Failed to send Telegram message");
157 | }
158 | }
159 |
160 | return NextResponse.json({
161 | success: true,
162 | message: "Announcement sent successfully",
163 | });
164 | } catch (error) {
165 | console.error("Error in announcement:", error);
166 | return NextResponse.json(
167 | {
168 | error: "Failed to process announcement",
169 | details: error instanceof Error ? error.message : error,
170 | },
171 | { status: 500 }
172 | );
173 | }
174 | }
175 |
--------------------------------------------------------------------------------
/app/api/memory/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { v4 as uuidv4 } from "uuid";
3 | import { stringToUuid } from "@/lib/uuid";
4 |
5 | const elizaAgentUserId = process.env.ELIZA_AGENT_USER_ID;
6 |
7 | const elizaAgentId = process.env.ELIZA_AGENT_ID;
8 |
9 | const elizaAgentUrl = `${process.env.ELIZA_API_URL}/${elizaAgentId}/memories`;
10 |
11 | export async function POST(req: NextRequest) {
12 | try {
13 | const { content, channelId } = await req.json();
14 |
15 | if (!content || !channelId) {
16 | return NextResponse.json(
17 | { error: "Missing required fields" },
18 | { status: 400 }
19 | );
20 | }
21 |
22 | const memory = {
23 | id: uuidv4(),
24 | content,
25 | userId: elizaAgentUserId,
26 | roomId: stringToUuid(channelId + "-" + elizaAgentId),
27 | createdAt: new Date(),
28 | };
29 |
30 | const response = await fetch(elizaAgentUrl, {
31 | method: 'POST',
32 | headers: {
33 | 'Content-Type': 'application/json',
34 | },
35 | body: JSON.stringify(memory),
36 | });
37 |
38 | if (!response.ok) {
39 | const errorDetails = await response.json();
40 | throw new Error(errorDetails.error || 'Unknown error');
41 | }
42 |
43 | return NextResponse.json({ success: true, memoryId: memory.id });
44 | } catch (error) {
45 | console.error("Error creating memory:", error instanceof Error ? error.message : error);
46 | return NextResponse.json(
47 | { error: "Failed to create memory", details: error instanceof Error ? error.message : error },
48 | { status: 500 }
49 | );
50 | }
51 | }
--------------------------------------------------------------------------------
/app/api/message/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from "next/server";
2 | import { v4 as uuidv4 } from "uuid";
3 | import { stringToUuid } from "@/lib/uuid";
4 |
5 | const elizaAgentUserId = process.env.ELIZA_AGENT_USER_ID || "";
6 | const elizaAgentId = process.env.ELIZA_AGENT_ID || "";
7 | const elizaAgentUrl = `${process.env.ELIZA_API_URL}/${elizaAgentId}/message`;
8 |
9 | export async function POST(req: NextRequest) {
10 | try {
11 | let { content, channelId, userName } = await req.json();
12 |
13 | if (!channelId) {
14 | channelId = content.channelId || uuidv4();
15 | }
16 |
17 | if (!content || !content.text) {
18 | return NextResponse.json(
19 | { error: "Missing required fields" },
20 | { status: 400 }
21 | );
22 | }
23 |
24 | const messagePayload = {
25 | roomId: stringToUuid(channelId + "-" + elizaAgentId),
26 | userId: stringToUuid(elizaAgentUserId),
27 | userName: userName || "user",
28 | content: {
29 | text: content.text,
30 | attachments: content.attachments || [],
31 | source: "direct",
32 | inReplyTo: content.inReplyTo,
33 | ...content
34 | }
35 | };
36 |
37 | const response = await fetch(elizaAgentUrl, {
38 | method: 'POST',
39 | headers: {
40 | 'Content-Type': 'application/json',
41 | },
42 | body: JSON.stringify(messagePayload),
43 | });
44 |
45 | if (!response.ok) {
46 | const errorDetails = await response.json();
47 | throw new Error(errorDetails.error || 'Unknown error');
48 | }
49 |
50 | const aiResponses = await response.json();
51 |
52 | return NextResponse.json({
53 | success: true,
54 | responses: aiResponses
55 | });
56 |
57 | } catch (error) {
58 | console.error("Error creating message:", error instanceof Error ? error.message : error);
59 | return NextResponse.json(
60 | { error: "Failed to process message", details: error instanceof Error ? error.message : error },
61 | { status: 500 }
62 | );
63 | }
64 | }
--------------------------------------------------------------------------------
/app/api/profile/query/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server';
2 |
3 | export async function POST(request: Request) {
4 | try {
5 | const body = await request.json();
6 |
7 | const response = await fetch('https://api.thegraph.com/subgraphs/name/verax/linea-sepolia', {
8 | method: 'POST',
9 | headers: {
10 | 'Content-Type': 'application/json',
11 | },
12 | body: JSON.stringify(body)
13 | });
14 |
15 | const data = await response.json();
16 | return NextResponse.json(data);
17 | } catch (error) {
18 | console.error("GraphQL query error:", error);
19 | return NextResponse.json(
20 | { error: 'Failed to fetch profile data' },
21 | { status: 500 }
22 | );
23 | }
24 | }
--------------------------------------------------------------------------------
/app/api/profile/sign/route.ts:
--------------------------------------------------------------------------------
1 | import { keccak256, encodeAbiParameters, parseAbiParameters, SignableMessage } from 'viem';
2 | import { privateKeyToAccount } from 'viem/accounts';
3 | import { NextRequest, NextResponse } from 'next/server';
4 |
5 | const SIGNER_PRIVATE_KEY = process.env.SIGNER_ADDRESS_PRIVATE_KEY as `0x${string}`;
6 |
7 | export async function POST(req: NextRequest) {
8 | try {
9 | const { nickname, avatar, socialAccount, subject } = await req.json();
10 |
11 | if (!nickname || !avatar || !socialAccount || !subject) {
12 | return NextResponse.json(
13 | { error: 'Missing required parameters' },
14 | { status: 400 }
15 | );
16 | }
17 |
18 | // Verify private key format
19 | if (!SIGNER_PRIVATE_KEY || !SIGNER_PRIVATE_KEY.startsWith('0x') || SIGNER_PRIVATE_KEY.length !== 66) {
20 | throw new Error('Invalid SIGNER_PRIVATE_KEY format');
21 | }
22 |
23 | // Construct message
24 | const message = encodeAbiParameters(
25 | parseAbiParameters('string, string, string, address'),
26 | [nickname, avatar, socialAccount, subject as `0x${string}`]
27 | );
28 |
29 | // Calculate message hash
30 | const messageHash = keccak256(message);
31 |
32 | // Create account
33 | const account = privateKeyToAccount(SIGNER_PRIVATE_KEY);
34 |
35 | // Sign the message
36 | const signature = await account.signMessage({
37 | message: { raw: messageHash } as SignableMessage
38 | });
39 |
40 | return NextResponse.json({
41 | signature,
42 | nickname,
43 | avatar,
44 | socialAccount
45 | });
46 | } catch (error) {
47 | console.error('Error in profile signing:', error);
48 | return NextResponse.json(
49 | { error: 'Failed to sign message' },
50 | { status: 500 }
51 | );
52 | }
53 | }
--------------------------------------------------------------------------------
/app/api/social/discord/check-guild/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { decryptData } from '@/utils/encryption-server';
3 |
4 | export async function POST(req: Request) {
5 | try {
6 | const { encryptedTokens, guildId, userId } = await req.json();
7 |
8 | if (!encryptedTokens || !userId || !guildId) {
9 | return NextResponse.json(
10 | { error: "Missing required parameters" },
11 | { status: 401 }
12 | );
13 | }
14 |
15 | // Use server-side decryption
16 | const decryptedTokens = JSON.parse(decryptData(encryptedTokens));
17 | const accessToken = decryptedTokens.discordAccessToken;
18 |
19 | if (!accessToken) {
20 | return NextResponse.json(
21 | { error: "Invalid access token" },
22 | { status: 401 }
23 | );
24 | }
25 |
26 | // Get the user's guild list
27 | const guildsResponse = await fetch(
28 | 'https://discord.com/api/users/@me/guilds',
29 | {
30 | headers: {
31 | 'Authorization': `Bearer ${accessToken}`,
32 | },
33 | next: { revalidate: 0 }
34 | }
35 | );
36 |
37 | if (!guildsResponse.ok) {
38 | throw new Error('Failed to fetch user guilds');
39 | }
40 |
41 | const guilds = await guildsResponse.json();
42 | const isInGuild = guilds.some((guild: any) => guild.id === guildId);
43 |
44 | return NextResponse.json({
45 | inGuild: isInGuild,
46 | });
47 |
48 | } catch (error) {
49 | console.error("Discord guild check error:", error);
50 | return NextResponse.json(
51 | { error: "Failed to verify Discord guild membership" },
52 | { status: 500 }
53 | );
54 | }
55 | }
--------------------------------------------------------------------------------
/app/api/social/discord/verify/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export const dynamic = 'force-dynamic';
4 |
5 | export async function GET(req: Request) {
6 | try {
7 | const accessToken = req.headers.get('Authorization')?.split('Bearer ')[1];
8 | const userId = req.headers.get('X-User-Id');
9 |
10 | if (!accessToken || !userId) {
11 | return NextResponse.json(
12 | { error: "Missing authentication data" },
13 | { status: 401 }
14 | );
15 | }
16 |
17 | // Use Discord API to get user information
18 | const response = await fetch(
19 | `https://discord.com/api/users/@me`, {
20 | headers: {
21 | 'Authorization': `Bearer ${accessToken}`,
22 | },
23 | next: { revalidate: 0 }
24 | }
25 | );
26 |
27 | if (!response.ok) {
28 | throw new Error('Failed to fetch Discord user data');
29 | }
30 |
31 | const data = await response.json();
32 | return NextResponse.json(data);
33 | } catch (error) {
34 | console.error("Discord verification error:", error);
35 | return NextResponse.json(
36 | { error: "Failed to verify Discord account" },
37 | { status: 500 }
38 | );
39 | }
40 | }
--------------------------------------------------------------------------------
/app/api/social/github/verify/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export const dynamic = 'force-dynamic';
4 |
5 | export async function GET(req: Request) {
6 | try {
7 | const accessToken = req.headers.get('Authorization')?.split('Bearer ')[1];
8 | const userId = req.headers.get('X-User-Id');
9 |
10 | if (!accessToken || !userId) {
11 | return NextResponse.json(
12 | { error: "Missing authentication data" },
13 | { status: 401 }
14 | );
15 | }
16 |
17 | // Use GitHub API to get user information
18 | const response = await fetch(
19 | `https://api.github.com/user`, {
20 | headers: {
21 | 'Authorization': `Bearer ${accessToken}`,
22 | 'Accept': 'application/vnd.github.v3+json'
23 | },
24 | next: { revalidate: 0 }
25 | }
26 | );
27 |
28 | if (!response.ok) {
29 | throw new Error('Failed to fetch GitHub user data');
30 | }
31 |
32 | const data = await response.json();
33 | return NextResponse.json(data);
34 | } catch (error) {
35 | console.error("GitHub verification error:", error);
36 | return NextResponse.json(
37 | { error: "Failed to verify GitHub account" },
38 | { status: 500 }
39 | );
40 | }
41 | }
--------------------------------------------------------------------------------
/app/api/social/twitter/check-actions/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 | import { Client } from "twitter-api-sdk";
3 | import { decryptData } from '@/utils/encryption-server';
4 |
5 | // Define simplified types instead of using Components
6 | type User = {
7 | id?: string;
8 | username?: string;
9 | };
10 |
11 | type Tweet = {
12 | id?: string;
13 | };
14 |
15 | // Extend type definition
16 | type PublicMetrics = {
17 | like_count?: number;
18 | retweet_count?: number;
19 | };
20 |
21 | type TweetResponse = {
22 | data?: {
23 | public_metrics?: PublicMetrics;
24 | };
25 | };
26 |
27 | const getTwitterClient = () => {
28 | if (!process.env.TWITTER_BEARER_TOKEN) {
29 | throw new Error('Missing Twitter bearer token');
30 | }
31 | return new Client(process.env.TWITTER_BEARER_TOKEN);
32 | };
33 |
34 | export async function POST(req: Request) {
35 | try {
36 | const { encryptedTokens, action, targetUser, tweetId, userId } = await req.json();
37 |
38 | if (!encryptedTokens || !action || !userId) {
39 | return NextResponse.json(
40 | { error: "Missing required parameters" },
41 | { status: 400 }
42 | );
43 | }
44 |
45 | // Use server-side decryption
46 | const decryptedTokens = JSON.parse(decryptData(encryptedTokens));
47 | const accessToken = decryptedTokens.xAccessToken;
48 |
49 | // if (!accessToken) {
50 | // return NextResponse.json(
51 | // { error: "Invalid access token" },
52 | // { status: 401 }
53 | // );
54 | // }
55 |
56 | const client = getTwitterClient();
57 | let result = { verified: false };
58 |
59 | switch (action) {
60 | case 'follow':
61 | try {
62 | const following = await client.users.usersIdFollowing(userId, {
63 | max_results: 1000,
64 | "user.fields": ["username"]
65 | });
66 |
67 | result.verified = following.data?.some((user: User) =>
68 | user.username?.toLowerCase() === targetUser?.toLowerCase()
69 | ) || false;
70 | } catch (error: any) {
71 | console.error('Follow check error:', error);
72 | if (error.status === 429) {
73 | return NextResponse.json(
74 | { error: "Rate limit exceeded" },
75 | { status: 429 }
76 | );
77 | }
78 | // throw error;
79 | }
80 | break;
81 |
82 | case 'like':
83 | try {
84 | if (!tweetId) {
85 | return NextResponse.json(
86 | { error: "Missing tweet ID" },
87 | { status: 400 }
88 | );
89 | }
90 |
91 | const tweet = await client.tweets.findTweetById(tweetId, {
92 | "tweet.fields": ["public_metrics"]
93 | }) as TweetResponse;
94 |
95 | // Add null check
96 | result.verified = Boolean(
97 | tweet?.data?.public_metrics?.like_count &&
98 | tweet.data.public_metrics.like_count > 0
99 | );
100 | } catch (error) {
101 | console.error('Like check error:', error);
102 | // throw error;
103 | }
104 | break;
105 |
106 | case 'retweet':
107 | try {
108 | if (!tweetId) {
109 | return NextResponse.json(
110 | { error: "Missing tweet ID" },
111 | { status: 400 }
112 | );
113 | }
114 |
115 | const tweet = await client.tweets.findTweetById(tweetId, {
116 | "tweet.fields": ["public_metrics"]
117 | }) as TweetResponse;
118 |
119 | // Add null check
120 | result.verified = Boolean(
121 | tweet?.data?.public_metrics?.retweet_count &&
122 | tweet.data.public_metrics.retweet_count > 0
123 | );
124 | } catch (error) {
125 | console.error('Retweet check error:', error);
126 | // throw error;
127 | }
128 | break;
129 |
130 | default:
131 | return NextResponse.json(
132 | { error: "Invalid action type" },
133 | { status: 400 }
134 | );
135 | }
136 |
137 | return NextResponse.json({ verified: true });//result);
138 |
139 | } catch (error: any) {
140 | console.error('Error:', error);
141 | return NextResponse.json(
142 | { error: "Failed to process request", details: error.message },
143 | { status: 500 }
144 | );
145 | }
146 | }
--------------------------------------------------------------------------------
/app/api/social/twitter/verify/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server";
2 |
3 | export const dynamic = 'force-dynamic';
4 |
5 | export async function GET(req: Request) {
6 | try {
7 | const accessToken = req.headers.get('Authorization')?.split('Bearer ')[1];
8 | const userId = req.headers.get('X-User-Id');
9 |
10 | if (!accessToken || !userId) {
11 | return NextResponse.json(
12 | { error: "Missing authentication data" },
13 | { status: 401 }
14 | );
15 | }
16 |
17 | // Use the passed token to obtain user information
18 | const response = await fetch(
19 | `https://api.twitter.com/2/users/me`, {
20 | headers: {
21 | 'Authorization': `Bearer ${accessToken}`,
22 | },
23 | next: { revalidate: 0 }
24 | }
25 | );
26 |
27 | if (!response.ok) {
28 | throw new Error('Failed to fetch Twitter user data');
29 | }
30 |
31 | const data = await response.json();
32 | return NextResponse.json(data);
33 | } catch (error) {
34 | console.error("Twitter verification error:", error);
35 | return NextResponse.json(
36 | { error: "Failed to verify Twitter account" },
37 | { status: 500 }
38 | );
39 | }
40 | }
--------------------------------------------------------------------------------
/app/api/upload/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 |
3 | export async function POST(req: NextRequest) {
4 | try {
5 | const formData = await req.formData();
6 |
7 | const response = await fetch("https://sm.ms/api/v2/upload", {
8 | method: "POST",
9 | headers: {
10 | 'Authorization': process.env.SMMS_TOKEN || '',
11 | },
12 | body: formData,
13 | });
14 |
15 | const data = await response.json();
16 |
17 | if (!response.ok) {
18 | throw new Error(data.message || 'Upload failed');
19 | }
20 |
21 | return NextResponse.json(data);
22 | } catch (error) {
23 | console.error('Upload error:', error);
24 | return NextResponse.json(
25 | { error: 'Failed to upload image' },
26 | { status: 500 }
27 | );
28 | }
29 | }
--------------------------------------------------------------------------------
/app/boards/create/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useCreateBoard } from "@/hooks/useContract";
4 | import { ArrowLeft } from "lucide-react";
5 | import Link from "next/link";
6 | import BoardForm from "@/components/BoardForm";
7 | import { Suspense } from "react";
8 |
9 | function CreateBoardPageInner() {
10 | const createBoard = useCreateBoard();
11 |
12 | return (
13 |
14 |
15 | {/* Header */}
16 |
17 |
18 |
19 |
20 |
21 | Create New Bounty Board
22 |
23 |
24 |
25 |
29 |
30 |
31 | );
32 | }
33 |
34 | export default function CreateBoardPage() {
35 | return (
36 |
37 |
38 |
39 | );
40 | }
--------------------------------------------------------------------------------
/app/boards/joined/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useMemo } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { useGetBoardsByMember, useGetProfiles } from '@/hooks/useContract';
6 | import BoardCard from '@/components/BoardCard';
7 | import BoardsPageSkeleton from "@/components/BoardsPageSkeleton";
8 | import { BoardView } from '@/types/types';
9 | import { Plus } from "lucide-react";
10 | import { useRouter } from "next/navigation";
11 | import { useAccount } from 'wagmi';
12 | import { Suspense } from "react";
13 |
14 | function JoinedBoardsPageInner() {
15 | const router = useRouter();
16 | const { address } = useAccount();
17 | const { data: boardsData = [], isLoading } = useGetBoardsByMember(address);
18 |
19 | // Get all creator addresses
20 | const creatorAddresses = useMemo(() => {
21 | if (!boardsData || !Array.isArray(boardsData)) return [];
22 | return boardsData.map((board: BoardView) => board.creator as `0x${string}`);
23 | }, [boardsData]);
24 |
25 | // Batch retrieve creator profiles
26 | const { data: profilesData } = useGetProfiles(creatorAddresses);
27 |
28 | // Convert data information into a map format
29 | const creatorProfiles = useMemo(() => {
30 | if (!profilesData || !Array.isArray(profilesData)) return {};
31 |
32 | const [nicknames, avatars, socialAccounts, _, __] = profilesData;
33 | return creatorAddresses.reduce((acc, address, index) => {
34 | acc[address.toLowerCase()] = {
35 | nickname: nicknames[index],
36 | avatar: avatars[index],
37 | socialAccount: socialAccounts[index]
38 | };
39 | return acc;
40 | }, {} as Record);
41 | }, [profilesData, creatorAddresses]);
42 |
43 | // If the wallet is not connected, display a prompt message.
44 | if (!address) {
45 | return (
46 |
47 |
48 | Please connect your wallet to view joined boards
49 |
50 |
51 | );
52 | }
53 |
54 | if (isLoading) {
55 | return ;
56 | }
57 |
58 | return (
59 |
60 |
61 |
62 | Your Joined Boards
63 |
64 |
71 |
72 |
73 |
74 | {Array.isArray(boardsData) && boardsData.map((board: BoardView) => (
75 |
80 | ))}
81 |
82 |
83 | );
84 | }
85 |
86 | export default function JoinedBoardsPage() {
87 | return (
88 |
89 |
90 |
91 | );
92 | }
--------------------------------------------------------------------------------
/app/boards/page.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useMemo } from "react";
4 | import { Button } from "@/components/ui/button";
5 | import { useGetAllBoards, useGetProfiles } from "@/hooks/useContract";
6 | import { type BoardView } from "@/types/types";
7 | import BoardCard from "@/components/BoardCard";
8 | import BoardsPageSkeleton from "@/components/BoardsPageSkeleton";
9 | import { Plus } from "lucide-react";
10 | import { useRouter } from "next/navigation";
11 | import { useAccount } from "wagmi";
12 | import { Suspense } from "react";
13 |
14 | function BoardsPageInner() {
15 | const router = useRouter();
16 | const { data: boardsData, isLoading } = useGetAllBoards();
17 | const { address } = useAccount();
18 |
19 | // Get all creator addresses
20 | const creatorAddresses = useMemo(() => {
21 | if (!boardsData || !Array.isArray(boardsData)) return [];
22 | return boardsData.map((board: BoardView) => board.creator as `0x${string}`);
23 | }, [boardsData]);
24 |
25 | // Batch retrieve creator profiles
26 | const { data: profilesData } = useGetProfiles(creatorAddresses);
27 |
28 | // Convert data information into a map format.
29 | const creatorProfiles = useMemo(() => {
30 | if (!profilesData || !Array.isArray(profilesData)) return {};
31 |
32 | const [nicknames, avatars, socialAccounts, _, __] = profilesData;
33 | return creatorAddresses.reduce((acc, address, index) => {
34 | acc[address.toLowerCase()] = {
35 | nickname: nicknames[index],
36 | avatar: avatars[index],
37 | socialAccount: socialAccounts[index]
38 | };
39 | return acc;
40 | }, {} as Record);
41 | }, [profilesData, creatorAddresses]);
42 |
43 | if (isLoading) {
44 | return ;
45 | }
46 |
47 | return (
48 |
49 |
50 |
51 | All Boards
52 |
53 | {address && (
54 |
61 | )}
62 |
63 |
64 |
65 | {Array.isArray(boardsData) && boardsData.map((board: BoardView) => (
66 |
71 | ))}
72 |
73 |
74 | );
75 | }
76 |
77 | export default function BoardsPage() {
78 | return (
79 |
80 |
81 |
82 | );
83 | }
84 |
--------------------------------------------------------------------------------
/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/app/favicon.ico
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 240 10% 3.9%;
8 | --foreground: 0 0% 98%;
9 |
10 | --card: 240 10% 3.9%;
11 | --card-foreground: 0 0% 98%;
12 |
13 | --popover: 240 10% 3.9%;
14 | --popover-foreground: 0 0% 98%;
15 |
16 | --primary: 267 100% 61%;
17 | --primary-foreground: 0 0% 98%;
18 |
19 | --secondary: 240 3.7% 15.9%;
20 | --secondary-foreground: 0 0% 98%;
21 |
22 | --muted: 240 3.7% 15.9%;
23 | --muted-foreground: 240 5% 64.9%;
24 |
25 | --accent: 267 100% 61%;
26 | --accent-foreground: 0 0% 98%;
27 |
28 | --destructive: 0 62.8% 30.6%;
29 | --destructive-foreground: 0 0% 98%;
30 |
31 | --border: 240 3.7% 15.9%;
32 | --input: 240 3.7% 15.9%;
33 | --ring: 267 100% 61%;
34 | }
35 | }
36 |
37 | @layer base {
38 | body {
39 | @apply bg-gradient-to-br from-black via-zinc-900 to-purple-950;
40 | @apply min-h-screen text-foreground;
41 | }
42 | }
43 |
44 | .glass-card {
45 | @apply bg-black/20 border border-purple-500/30 backdrop-blur-sm
46 | hover:border-purple-500/50 transition-all duration-300
47 | shadow-lg hover:shadow-purple-500/20;
48 | }
49 |
50 | .glow-text {
51 | @apply text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-purple-600;
52 | text-shadow: 0 0 20px rgba(168, 85, 247, 0.5);
53 | }
54 |
55 | .glass-button {
56 | @apply px-6 py-3 rounded-lg bg-purple-600/80 hover:bg-purple-700/90
57 | text-white font-semibold backdrop-blur-sm transition-all duration-300
58 | border border-purple-500/30 hover:border-purple-500/50
59 | shadow-lg hover:shadow-purple-500/20;
60 | }
61 |
62 | .glass-button-secondary {
63 | @apply px-6 py-3 rounded-lg bg-black/40 hover:bg-black/60
64 | text-purple-300 font-semibold backdrop-blur-sm transition-all duration-300
65 | border border-purple-500/30 hover:border-purple-500/50
66 | shadow-lg hover:shadow-purple-500/20;
67 | }
68 |
69 | @keyframes fadeIn {
70 | from { opacity: 0; transform: translateY(20px); }
71 | to { opacity: 1; transform: translateY(0); }
72 | }
73 |
74 | .animate-fade-in {
75 | animation: fadeIn 0.8s ease-out forwards;
76 | }
77 |
78 | .animate-fade-in-delay {
79 | animation: fadeIn 0.8s ease-out 0.2s forwards;
80 | opacity: 0;
81 | }
82 |
83 | .neon-button-primary {
84 | @apply relative px-6 py-3 text-base font-semibold text-white
85 | bg-gradient-to-r from-purple-600 to-purple-800
86 | rounded-lg overflow-hidden transition-all duration-300
87 | hover:from-purple-500 hover:to-purple-700
88 | transform hover:scale-105
89 | border border-purple-400/50;
90 | box-shadow: 0 0 20px theme('colors.purple.500' / 20%);
91 | }
92 |
93 | .neon-button-primary:hover {
94 | box-shadow: 0 0 30px theme('colors.purple.500' / 40%);
95 | }
96 |
97 | .neon-button-primary::before {
98 | content: '';
99 | @apply absolute inset-0 bg-gradient-to-r from-purple-400/20 to-transparent opacity-0 transition-opacity duration-300;
100 | }
101 |
102 | .neon-button-primary:hover::before {
103 | @apply opacity-100;
104 | }
105 |
106 | .neon-button-secondary {
107 | @apply relative px-6 py-3 text-base font-semibold text-purple-300
108 | bg-black/40 backdrop-blur-sm
109 | rounded-lg overflow-hidden transition-all duration-300
110 | hover:text-purple-200 hover:bg-black/60
111 | transform hover:scale-105
112 | border border-purple-500/30 hover:border-purple-400/50;
113 | box-shadow: 0 0 20px theme('colors.purple.500' / 10%);
114 | }
115 |
116 | .neon-button-secondary:hover {
117 | box-shadow: 0 0 30px theme('colors.purple.500' / 30%);
118 | }
119 |
120 | .neon-button-secondary::before {
121 | content: '';
122 | @apply absolute inset-0 bg-gradient-to-r from-purple-500/10 to-transparent opacity-0 transition-opacity duration-300;
123 | }
124 |
125 | .neon-button-secondary:hover::before {
126 | @apply opacity-100;
127 | }
128 |
129 | .animate-fade-in-delay-2 {
130 | animation: fadeIn 0.8s ease-out 0.4s forwards;
131 | opacity: 0;
132 | }
133 |
134 | .glass-input {
135 | @apply bg-black/20 border-purple-500/30 backdrop-blur-sm
136 | focus:border-purple-500/50 focus:ring-purple-500/20
137 | placeholder:text-gray-500;
138 | }
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import './globals.css';
4 | import { ReactNode } from 'react';
5 | import Web3Providers from '@/providers/Web3Providers';
6 | import { Toaster } from "@/components/ui/toaster";
7 | import ConnectWallet from '@/components/ConnectWalletButton';
8 | import Link from 'next/link';
9 | import Image from 'next/image';
10 | import { SessionProvider } from "next-auth/react";
11 | import Navigation from '@/components/Navigation';
12 | import { TelegramAuthProvider } from '@/providers/TelegramAuthContext';
13 | import { Menu } from 'lucide-react';
14 | import { Button } from '@/components/ui/button';
15 | import { useState } from 'react';
16 | import { cn } from '@/lib/utils';
17 |
18 | const RootLayout = ({ children }: { children: ReactNode }) => {
19 | const [isMenuOpen, setIsMenuOpen] = useState(false);
20 |
21 | return (
22 |
23 |
24 | Bounty Board
25 |
26 |
27 |
28 |
29 |
30 |
31 | {/* Header */}
32 |
33 |
34 |
35 |
36 |
37 |
47 |
48 | BountyBoard
49 |
50 |
51 |
52 |
53 | {/* Desktop Navigation */}
54 |
55 |
56 |
57 |
58 | {/* Connect Wallet Button */}
59 |
60 |
61 | {/* Mobile Menu Button */}
62 |
70 |
71 |
72 |
73 |
74 | {/* Mobile Navigation Dropdown */}
75 |
81 |
82 | setIsMenuOpen(false)} />
83 |
84 |
85 |
86 |
87 |
88 | {children}
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | );
97 | };
98 |
99 | export default RootLayout;
--------------------------------------------------------------------------------
/assets/BountyBoard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/BountyBoard.png
--------------------------------------------------------------------------------
/assets/Screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/Screenshot.png
--------------------------------------------------------------------------------
/assets/ScreenshotHome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/ScreenshotHome.png
--------------------------------------------------------------------------------
/assets/ScreenshotTask.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/assets/ScreenshotTask.png
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/BoardActionsDropdown.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | DropdownMenu,
5 | DropdownMenuContent,
6 | DropdownMenuItem,
7 | DropdownMenuLabel,
8 | DropdownMenuSeparator,
9 | DropdownMenuTrigger,
10 | } from "@/components/ui/dropdown-menu";
11 | import { Button } from "@/components/ui/button";
12 | import { MoreHorizontal } from "lucide-react";
13 |
14 | interface BoardActionsDropdownProps {
15 | isCreator: boolean;
16 | isMember: boolean;
17 | rewardTokenAddress: `0x${string}`;
18 | onOpenUpdateBoardModal: () => void;
19 | onCloseBoard: () => void;
20 | onWithdrawPledgedTokens: () => void;
21 | onApproveTokens: () => void;
22 | onOpenPledgeTokensModal: () => void;
23 | }
24 |
25 |
26 | export default function BoardActionsDropdown({
27 | onOpenUpdateBoardModal,
28 | onCloseBoard,
29 | onWithdrawPledgedTokens,
30 | onApproveTokens,
31 | onOpenPledgeTokensModal,
32 | }: BoardActionsDropdownProps) {
33 | return (
34 |
35 |
36 |
39 |
40 |
41 | Board Actions
42 |
43 | Approve Tokens
44 |
45 |
46 | Pledge Tokens
47 |
48 |
49 | Update Board
50 |
51 |
52 | Close Board
53 |
54 |
55 |
56 | Withdraw Pledged Tokens
57 |
58 |
59 |
60 | );
61 | }
62 |
--------------------------------------------------------------------------------
/components/BoardCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import Link from 'next/link';
4 | import Image from 'next/image';
5 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
6 | import { format } from 'date-fns';
7 | import { useTokenSymbol } from '@/hooks/useContract';
8 | import { BoardView } from '@/types/types';
9 | import { formatUnits, zeroAddress } from 'viem';
10 | import { Address } from './ui/Address';
11 | import { User2, Calendar, Coins } from 'lucide-react';
12 | import { getNativeTokenSymbol } from '@/utils/chain';
13 | import { useAccount } from 'wagmi';
14 |
15 | export default function BoardCard({
16 | board,
17 | creatorProfile
18 | }: {
19 | board: BoardView;
20 | creatorProfile?: {
21 | nickname: string;
22 | avatar: string;
23 | }
24 | }) {
25 | const { chain } = useAccount();
26 | const { data: tokenSymbol } = useTokenSymbol(board.rewardToken);
27 |
28 | return (
29 |
30 |
31 |
32 |
33 | {board.img && (
34 |
35 | {
43 | const target = e.target as HTMLImageElement;
44 | target.src = '/placeholder.png';
45 | }}
46 | />
47 |
48 | )}
49 |
50 | {board.name}
51 |
52 |
53 |
54 |
55 |
56 | {board.description}
57 |
58 |
59 |
60 | {creatorProfile?.avatar ? (
61 |
68 | ) : (
69 |
70 | )}
71 |
72 | {creatorProfile?.nickname || }
73 |
74 |
75 |
76 |
77 |
78 | {format(new Date(Number(board.createdAt) * 1000), 'PPP')}
79 |
80 |
81 |
82 |
83 | {formatUnits(board.totalPledged, 18)} {tokenSymbol ?? ((board.rewardToken === zeroAddress && getNativeTokenSymbol(chain)) || '')}
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | );
92 | }
93 |
--------------------------------------------------------------------------------
/components/Boards.tsx:
--------------------------------------------------------------------------------
1 | // components/Boards.tsx
2 | 'use client';
3 |
4 | import { useQuery } from '@tanstack/react-query';
5 | import { request } from 'graphql-request';
6 | import Link from 'next/link';
7 | import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
8 | import { format } from 'date-fns';
9 | import { useTokenSymbol } from '@/hooks/useContract';
10 | import { BoardView } from '@/types/types';
11 | import { formatUnits } from 'viem';
12 | import { Address } from './ui/Address';
13 | import { User2, Calendar, Coins } from 'lucide-react';
14 | import { BOARDS } from '@/graphql/queries';
15 |
16 | const url = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT as string;
17 |
18 | // Single Board component
19 | const BoardCard = ({ board }: { board: BoardView }) => {
20 | const { data: tokenSymbol } = useTokenSymbol(board.rewardToken); // Use useTokenSymbol inside the component
21 |
22 | return (
23 |
24 |
25 |
26 | {board.name}
27 |
28 |
29 | {board.description}
30 |
31 |
35 |
36 |
37 | {format(new Date(parseInt(board.createdAt.toString()) * 1000), 'PPP')}
38 |
39 |
40 |
41 | {formatUnits(BigInt(board.totalPledged), 18)} {tokenSymbol ?? ''}
42 |
43 |
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default function Boards() {
51 | const { data } = useQuery({
52 | queryKey: ['boards'],
53 | async queryFn() {
54 | const result = await request(url, BOARDS) as { boards: BoardView[] };
55 | return result.boards as BoardView[];
56 | },
57 | });
58 |
59 | return (
60 | <>
61 | {data?.map((board: BoardView) => (
62 | // Render BoardCard component
63 | ))}
64 | >
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/components/BoardsPageSkeleton.tsx:
--------------------------------------------------------------------------------
1 | import { Card } from "@/components/ui/card";
2 | import { Skeleton } from "@/components/ui/skeleton";
3 |
4 | export default function BoardsPageSkeleton() {
5 | return (
6 |
7 | {/* Header Skeleton */}
8 |
9 |
10 |
11 |
12 |
13 | {/* Boards Grid Skeleton */}
14 |
15 | {[1, 2, 3, 4, 5, 6].map((i) => (
16 |
17 | {/* Board Header */}
18 |
19 |
20 | {/* Board Image */}
21 |
22 | {/* Board Title */}
23 |
24 |
25 |
26 |
27 |
28 | {/* Action Button */}
29 |
30 |
31 |
32 | {/* Board Description */}
33 |
34 |
35 |
36 |
37 |
38 | {/* Board Stats */}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | ))}
55 |
56 |
57 | );
58 | }
--------------------------------------------------------------------------------
/components/Navigation.tsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { usePathname } from 'next/navigation';
3 | import { cn } from '@/lib/utils';
4 |
5 | interface NavigationProps {
6 | mobile?: boolean;
7 | onClose?: () => void;
8 | }
9 |
10 | export default function Navigation({ mobile, onClose }: NavigationProps) {
11 | const pathname = usePathname();
12 |
13 | const links = [
14 | { href: '/boards', label: 'All Boards' },
15 | { href: '/boards/joined', label: 'My Boards' },
16 | { href: '/boards/create', label: 'Create Board' },
17 | ];
18 |
19 | const NavLink = ({ href, label }: { href: string; label: string }) => {
20 | const isActive = pathname === href;
21 |
22 | return (
23 |
34 | {label}
35 | {isActive && (
36 |
37 | )}
38 |
39 | );
40 | };
41 |
42 | return (
43 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/components/ui/Address.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { cn } from '@/lib/utils'
4 | import {
5 | Tooltip,
6 | TooltipContent,
7 | TooltipProvider,
8 | TooltipTrigger,
9 | } from '@/components/ui/tooltip'
10 |
11 | interface AddressProps {
12 | address: `0x${string}`
13 | className?: string
14 | size?: 'sm' | 'lg'
15 | }
16 | // Function to abbreviate the middle part of the address
17 | function shortenAddress(address: string) {
18 | return `${address.slice(0, 6)}...${address.slice(-4)}`
19 | }
20 |
21 | export function Address({ address, className, size = 'sm' }: AddressProps) {
22 | return (
23 |
24 |
25 |
26 |
34 | {shortenAddress(address)}
35 |
36 |
37 |
38 | {address}
39 |
40 |
41 |
42 | )
43 | }
44 |
45 | interface TipProps {
46 | tip: string
47 | children: React.ReactNode
48 | }
49 |
50 | export function Tip({ tip, children }: TipProps) {
51 | return (
52 |
53 |
54 | {children}
55 | {tip}
56 |
57 |
58 | )
59 | }
--------------------------------------------------------------------------------
/components/ui/alert.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { cva, type VariantProps } from "class-variance-authority";
3 | import { cn } from "@/lib/utils";
4 |
5 | const alertVariants = cva(
6 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground",
7 | {
8 | variants: {
9 | variant: {
10 | default: "bg-background text-foreground",
11 | destructive:
12 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
13 | warning:
14 | "border-yellow-500/50 text-yellow-500 dark:border-yellow-500 [&>svg]:text-yellow-500",
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 };
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default: "border-transparent bg-primary text-primary-foreground",
12 | secondary: "border-transparent bg-secondary text-secondary-foreground",
13 | destructive: "border-transparent bg-destructive text-destructive-foreground",
14 | outline: "text-foreground",
15 | success: "border-transparent bg-green-500 text-white",
16 | },
17 | },
18 | defaultVariants: {
19 | variant: "default",
20 | },
21 | }
22 | )
23 |
24 | export interface BadgeProps
25 | extends React.HTMLAttributes,
26 | VariantProps {
27 | variant?: "default" | "secondary" | "destructive" | "outline" | "success";
28 | }
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import { DayPicker } from "react-day-picker"
6 |
7 | import { cn } from "@/lib/utils"
8 | import { buttonVariants } from "@/components/ui/button"
9 |
10 | export type CalendarProps = React.ComponentProps
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
58 | IconRight: ({ ...props }) => ,
59 | }}
60 | {...props}
61 | />
62 | )
63 | }
64 | Calendar.displayName = "Calendar"
65 |
66 | export { Calendar }
67 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
20 | ))
21 | Card.displayName = "Card"
22 |
23 | const CardHeader = React.forwardRef<
24 | HTMLDivElement,
25 | React.HTMLAttributes
26 | >(({ className, ...props }, ref) => (
27 |
35 | ))
36 | CardHeader.displayName = "CardHeader"
37 |
38 | const CardTitle = React.forwardRef<
39 | HTMLParagraphElement,
40 | React.HTMLAttributes
41 | >(({ className, ...props }, ref) => (
42 |
51 | ))
52 | CardTitle.displayName = "CardTitle"
53 |
54 | const CardDescription = React.forwardRef<
55 | HTMLParagraphElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
67 | ))
68 | CardDescription.displayName = "CardDescription"
69 |
70 | const CardContent = React.forwardRef<
71 | HTMLDivElement,
72 | React.HTMLAttributes
73 | >(({ className, ...props }, ref) => (
74 |
83 | ))
84 | CardContent.displayName = "CardContent"
85 |
86 | const CardFooter = React.forwardRef<
87 | HTMLDivElement,
88 | React.HTMLAttributes
89 | >(({ className, ...props }, ref) => (
90 |
99 | ))
100 | CardFooter.displayName = "CardFooter"
101 |
102 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
103 |
--------------------------------------------------------------------------------
/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 "@/lib/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 |
--------------------------------------------------------------------------------
/components/ui/command.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { type DialogProps } from "@radix-ui/react-dialog"
5 | import { Command as CommandPrimitive } from "cmdk"
6 | import { Search } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 | import { Dialog, DialogContent } from "@/components/ui/dialog"
10 |
11 | const Command = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
23 | ))
24 | Command.displayName = CommandPrimitive.displayName
25 |
26 | const CommandDialog = ({ children, ...props }: DialogProps) => {
27 | return (
28 |
35 | )
36 | }
37 |
38 | const CommandInput = React.forwardRef<
39 | React.ElementRef,
40 | React.ComponentPropsWithoutRef
41 | >(({ className, ...props }, ref) => (
42 |
43 |
44 |
52 |
53 | ))
54 |
55 | CommandInput.displayName = CommandPrimitive.Input.displayName
56 |
57 | const CommandList = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, ...props }, ref) => (
61 |
66 | ))
67 |
68 | CommandList.displayName = CommandPrimitive.List.displayName
69 |
70 | const CommandEmpty = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >((props, ref) => (
74 |
79 | ))
80 |
81 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName
82 |
83 | const CommandGroup = React.forwardRef<
84 | React.ElementRef,
85 | React.ComponentPropsWithoutRef
86 | >(({ className, ...props }, ref) => (
87 |
95 | ))
96 |
97 | CommandGroup.displayName = CommandPrimitive.Group.displayName
98 |
99 | const CommandSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName
110 |
111 | const CommandItem = React.forwardRef<
112 | React.ElementRef,
113 | React.ComponentPropsWithoutRef
114 | >(({ className, ...props }, ref) => (
115 |
123 | ))
124 |
125 | CommandItem.displayName = CommandPrimitive.Item.displayName
126 |
127 | const CommandShortcut = ({
128 | className,
129 | ...props
130 | }: React.HTMLAttributes) => {
131 | return (
132 |
139 | )
140 | }
141 | CommandShortcut.displayName = "CommandShortcut"
142 |
143 | export {
144 | Command,
145 | CommandDialog,
146 | CommandInput,
147 | CommandList,
148 | CommandEmpty,
149 | CommandGroup,
150 | CommandItem,
151 | CommandShortcut,
152 | CommandSeparator,
153 | }
154 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = "DialogHeader"
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = "DialogFooter"
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cn } from "@/lib/utils"
3 |
4 | export interface InputProps
5 | extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(
8 | ({ className, type, ...props }, ref) => {
9 | return (
10 |
23 | )
24 | }
25 | )
26 | Input.displayName = "Input"
27 |
28 | export { Input }
29 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as LabelPrimitive from "@radix-ui/react-label";
3 | import { cn } from "@/lib/utils";
4 |
5 | const Label = React.forwardRef<
6 | React.ElementRef,
7 | React.ComponentPropsWithoutRef
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Label.displayName = LabelPrimitive.Root.displayName;
19 |
20 | export { Label };
--------------------------------------------------------------------------------
/components/ui/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const LoadingSpinner = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 | Loading...
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default LoadingSpinner;
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
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 }
32 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 |
48 |
49 | ))
50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
51 |
52 | const SelectScrollDownButton = React.forwardRef<
53 | React.ElementRef,
54 | React.ComponentPropsWithoutRef
55 | >(({ className, ...props }, ref) => (
56 |
64 |
65 |
66 | ))
67 | SelectScrollDownButton.displayName =
68 | SelectPrimitive.ScrollDownButton.displayName
69 |
70 | const SelectContent = React.forwardRef<
71 | React.ElementRef,
72 | React.ComponentPropsWithoutRef
73 | >(({ className, children, position = "popper", ...props }, ref) => (
74 |
75 |
86 |
87 |
94 | {children}
95 |
96 |
97 |
98 |
99 | ))
100 | SelectContent.displayName = SelectPrimitive.Content.displayName
101 |
102 | const SelectLabel = React.forwardRef<
103 | React.ElementRef,
104 | React.ComponentPropsWithoutRef
105 | >(({ className, ...props }, ref) => (
106 |
111 | ))
112 | SelectLabel.displayName = SelectPrimitive.Label.displayName
113 |
114 | const SelectItem = React.forwardRef<
115 | React.ElementRef,
116 | React.ComponentPropsWithoutRef
117 | >(({ className, children, ...props }, ref) => (
118 |
126 |
127 |
128 |
129 |
130 |
131 |
132 | {children}
133 |
134 | ))
135 | SelectItem.displayName = SelectPrimitive.Item.displayName
136 |
137 | const SelectSeparator = React.forwardRef<
138 | React.ElementRef,
139 | React.ComponentPropsWithoutRef
140 | >(({ className, ...props }, ref) => (
141 |
146 | ))
147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
148 |
149 | export {
150 | Select,
151 | SelectGroup,
152 | SelectValue,
153 | SelectTrigger,
154 | SelectContent,
155 | SelectLabel,
156 | SelectItem,
157 | SelectSeparator,
158 | SelectScrollUpButton,
159 | SelectScrollDownButton,
160 | }
161 |
--------------------------------------------------------------------------------
/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export { Skeleton }
16 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
21 | ))
22 | Table.displayName = "Table"
23 |
24 | const TableHeader = React.forwardRef<
25 | HTMLTableSectionElement,
26 | React.HTMLAttributes
27 | >(({ className, ...props }, ref) => (
28 |
37 | ))
38 | TableHeader.displayName = "TableHeader"
39 |
40 | const TableBody = React.forwardRef<
41 | HTMLTableSectionElement,
42 | React.HTMLAttributes
43 | >(({ className, ...props }, ref) => (
44 |
53 | ))
54 | TableBody.displayName = "TableBody"
55 |
56 | const TableFooter = React.forwardRef<
57 | HTMLTableSectionElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 | tr]:last:border-b-0",
66 | "text-purple-200",
67 | className
68 | )}
69 | {...props}
70 | />
71 | ))
72 | TableFooter.displayName = "TableFooter"
73 |
74 | const TableRow = React.forwardRef<
75 | HTMLTableRowElement,
76 | React.HTMLAttributes
77 | >(({ className, ...props }, ref) => (
78 |
89 | ))
90 | TableRow.displayName = "TableRow"
91 |
92 | const TableHead = React.forwardRef<
93 | HTMLTableCellElement,
94 | React.ThHTMLAttributes
95 | >(({ className, ...props }, ref) => (
96 | |
107 | ))
108 | TableHead.displayName = "TableHead"
109 |
110 | const TableCell = React.forwardRef<
111 | HTMLTableCellElement,
112 | React.TdHTMLAttributes
113 | >(({ className, ...props }, ref) => (
114 | |
124 | ))
125 | TableCell.displayName = "TableCell"
126 |
127 | const TableCaption = React.forwardRef<
128 | HTMLTableCaptionElement,
129 | React.HTMLAttributes
130 | >(({ className, ...props }, ref) => (
131 |
140 | ))
141 | TableCaption.displayName = "TableCaption"
142 |
143 | export {
144 | Table,
145 | TableHeader,
146 | TableBody,
147 | TableFooter,
148 | TableHead,
149 | TableRow,
150 | TableCell,
151 | TableCaption,
152 | }
153 |
--------------------------------------------------------------------------------
/components/ui/tabs.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as TabsPrimitive from "@radix-ui/react-tabs"
5 | import { cn } from "@/lib/utils"
6 |
7 | const Tabs = TabsPrimitive.Root
8 |
9 | const TabsList = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
24 | ))
25 | TabsList.displayName = TabsPrimitive.List.displayName
26 |
27 | const TabsTrigger = React.forwardRef<
28 | React.ElementRef,
29 | React.ComponentPropsWithoutRef
30 | >(({ className, ...props }, ref) => (
31 |
64 | ))
65 | TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
66 |
67 | const TabsContent = React.forwardRef<
68 | React.ElementRef,
69 | React.ComponentPropsWithoutRef
70 | >(({ className, ...props }, ref) => (
71 |
88 | ))
89 | TabsContent.displayName = TabsPrimitive.Content.displayName
90 |
91 | export { Tabs, TabsList, TabsTrigger, TabsContent }
92 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as ToastPrimitives from "@radix-ui/react-toast"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const ToastProvider = ToastPrimitives.Provider
11 |
12 | const ToastViewport = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, ...props }, ref) => (
16 |
25 | ))
26 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
27 |
28 | const toastVariants = cva(
29 | cn(
30 | "group pointer-events-auto relative flex w-full items-center justify-between space-x-4",
31 | "overflow-hidden rounded-xl border p-6 pr-8 shadow-lg transition-all",
32 | "backdrop-blur-md",
33 | // Animation
34 | "data-[swipe=cancel]:translate-x-0",
35 | "data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)]",
36 | "data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)]",
37 | "data-[swipe=move]:transition-none",
38 | "data-[state=open]:animate-in data-[state=closed]:animate-out",
39 | "data-[swipe=end]:animate-out",
40 | "data-[state=closed]:fade-out-80",
41 | "data-[state=closed]:slide-out-to-right-full",
42 | "data-[state=open]:slide-in-from-top-full",
43 | "data-[state=open]:sm:slide-in-from-bottom-full"
44 | ),
45 | {
46 | variants: {
47 | variant: {
48 | default: cn(
49 | "border-purple-500/20 bg-black/40",
50 | "text-purple-100"
51 | ),
52 | destructive: cn(
53 | "destructive group",
54 | "border-red-500/20 bg-red-950/40",
55 | "text-red-300"
56 | ),
57 | },
58 | },
59 | defaultVariants: {
60 | variant: "default",
61 | },
62 | }
63 | )
64 |
65 | const Toast = React.forwardRef<
66 | React.ElementRef,
67 | React.ComponentPropsWithoutRef &
68 | VariantProps
69 | >(({ className, variant, ...props }, ref) => {
70 | return (
71 |
76 | )
77 | })
78 | Toast.displayName = ToastPrimitives.Root.displayName
79 |
80 | const ToastAction = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
104 | ))
105 | ToastAction.displayName = ToastPrimitives.Action.displayName
106 |
107 | const ToastClose = React.forwardRef<
108 | React.ElementRef,
109 | React.ComponentPropsWithoutRef
110 | >(({ className, ...props }, ref) => (
111 |
130 |
131 |
132 | ))
133 | ToastClose.displayName = ToastPrimitives.Close.displayName
134 |
135 | const ToastTitle = React.forwardRef<
136 | React.ElementRef,
137 | React.ComponentPropsWithoutRef
138 | >(({ className, ...props }, ref) => (
139 |
149 | ))
150 | ToastTitle.displayName = ToastPrimitives.Title.displayName
151 |
152 | const ToastDescription = React.forwardRef<
153 | React.ElementRef,
154 | React.ComponentPropsWithoutRef
155 | >(({ className, ...props }, ref) => (
156 |
166 | ))
167 | ToastDescription.displayName = ToastPrimitives.Description.displayName
168 |
169 | type ToastProps = React.ComponentPropsWithoutRef
170 |
171 | type ToastActionElement = React.ReactElement
172 |
173 | export {
174 | type ToastProps,
175 | type ToastActionElement,
176 | ToastProvider,
177 | ToastViewport,
178 | Toast,
179 | ToastTitle,
180 | ToastDescription,
181 | ToastClose,
182 | ToastAction,
183 | }
184 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/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 "@/lib/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 |
27 | ))
28 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
29 |
30 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
31 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 5000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/constants/attestaion.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | portal: "0x64dDDB204b80D5bA262EBc576b3801803954251b",
3 | schema: "0x69a8cb3eb90fac5e746aec951d07c4366692b50f4cf7b419c5a93961982a85b1",
4 | };
5 |
--------------------------------------------------------------------------------
/constants/contract-address.ts:
--------------------------------------------------------------------------------
1 | const contractAddress = {
2 | BountyBoard: {
3 | 'Flow EVM Testnet': "0x09D61437f07838AB892Bd92386EC39462BfE1972",
4 | 'opBNB Testnet': "0x397e12962a9dCed668FD5b7B2bfAfE585bdad323",
5 | Anvil: "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512",
6 | 'Mantle Sepolia Testnet': "0x397e12962a9dCed668FD5b7B2bfAfE585bdad323",
7 | 'Linea Sepolia Testnet': "0x47411c8857288da4246101c62653e1ec2F229590",
8 | 'Monad Devnet': "0x397e12962a9dCed668FD5b7B2bfAfE585bdad323",
9 | },
10 | UserProfile: {
11 | 'Flow EVM Testnet': "0xBec6DF509D1e02172A8e3e756720cD1f4447456d",
12 | 'opBNB Testnet': "0x698e8942d63cbFf3525fec8740A7EAaD6A251472",
13 | Anvil: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0",
14 | 'Mantle Sepolia Testnet': "0xCD40B2D99FBaf151f5C131A7ca3Cd801374Ec785",
15 | 'Linea Sepolia Testnet': "0x6e34fa9fd5926137109Ec7D6ad94d860148c2f75",
16 | 'Monad Devnet': "0xE3945d3fE0f67962220f1f66069Cd9fea9E76659",
17 | }
18 | };
19 |
20 | export default contractAddress;
21 |
--------------------------------------------------------------------------------
/contract/EVM/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | pull_request:
6 | workflow_dispatch:
7 |
8 | env:
9 | FOUNDRY_PROFILE: ci
10 |
11 | jobs:
12 | check:
13 | strategy:
14 | fail-fast: true
15 |
16 | name: Foundry project
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | with:
21 | submodules: recursive
22 |
23 | - name: Install Foundry
24 | uses: foundry-rs/foundry-toolchain@v1
25 | with:
26 | version: nightly
27 |
28 | - name: Show Forge version
29 | run: |
30 | forge --version
31 |
32 | - name: Run Forge fmt
33 | run: |
34 | forge fmt --check
35 | id: fmt
36 |
37 | - name: Run Forge build
38 | run: |
39 | forge build --sizes
40 | id: build
41 |
42 | - name: Run Forge tests
43 | run: |
44 | forge test -vvv
45 | id: test
46 |
--------------------------------------------------------------------------------
/contract/EVM/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiler files
2 | cache/
3 | out/
4 |
5 | # Ignores development broadcast logs
6 | !/broadcast
7 | /broadcast/*/31337/
8 | /broadcast/**/dry-run/
9 | /lib
10 |
11 | # Docs
12 | docs/
13 |
14 | # Dotenv file
15 | .env
16 |
--------------------------------------------------------------------------------
/contract/EVM/foundry.toml:
--------------------------------------------------------------------------------
1 | [profile.default]
2 | src = "src"
3 | out = "out"
4 | libs = ["lib"]
5 | optimizer = true
6 | optimizer_runs = 200
7 |
8 | [rpc_endpoints]
9 | linea-sepolia = "${RPC_URL_LINEA}"
10 |
11 | [etherscan]
12 | linea-sepolia = { key = "${LINEASCAN_API_KEY}", url = "https://api-sepolia.lineascan.build/api" }
13 | mantle-sepolia = { key = "", url = "https://explorer.sepolia.mantle.xyz/api" }
--------------------------------------------------------------------------------
/contract/EVM/script/BountyBoard.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: UNLICENSED
2 | pragma solidity ^0.8.13;
3 |
4 | import {Script, console} from "forge-std/Script.sol";
5 | import {BountyBoard} from "../src/BountyBoard.sol";
6 | import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
7 | import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
8 |
9 | contract BountyBoardScript is Script {
10 | function setUp() public {}
11 |
12 | function run() public {
13 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
14 | address signerAddress = vm.envAddress("SIGNER_ADDRESS");
15 | address upgradeAddress = vm.envAddress("UPGRADE_ADDRESS");
16 |
17 | vm.startBroadcast(deployerPrivateKey);
18 |
19 | // 1. Deploy the implementation contract
20 | BountyBoard implementation = new BountyBoard();
21 | console.log("Implementation deployed at:", address(implementation));
22 |
23 | // Ensure the implementation contract is deployed correctly.
24 | require(address(implementation) != address(0), "Implementation deployment failed");
25 |
26 | // 3. Prepare initialization data
27 | bytes memory initData = abi.encodeWithSelector(
28 | BountyBoard.initialize.selector,
29 | signerAddress
30 | );
31 | console.log("Initialize data length:", initData.length);
32 |
33 | if (upgradeAddress != address(0)) {
34 | // If the upgrade address is defined, proceed with contract upgrade.
35 | console.log("Upgrading contract at address:", upgradeAddress);
36 | UUPSUpgradeable proxy = UUPSUpgradeable(upgradeAddress);
37 | proxy.upgradeToAndCall(address(implementation), "");
38 | console.log("Contract upgraded at:", upgradeAddress);
39 | } else {
40 | // Otherwise, create a new proxy contract.
41 | ERC1967Proxy proxy = new ERC1967Proxy(
42 | address(implementation),
43 | initData
44 | );
45 | console.log("Proxy deployed at:", address(proxy));
46 |
47 | // 5. Create an interface instance of the proxy contract and verify initialization.
48 | BountyBoard bountyBoard = BountyBoard(payable(address(proxy)));
49 | require(bountyBoard.signerAddress() == signerAddress, "Initialization verification failed");
50 |
51 | console.log("BountyBoard (proxy) initialized at:", address(bountyBoard));
52 | console.log("Signer address set to:", bountyBoard.signerAddress());
53 | }
54 |
55 | vm.stopBroadcast();
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/contract/EVM/script/DeployMockERC20.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Script.sol";
5 | import "../src/MockERC20.sol";
6 |
7 | contract DeployMockERC20Script is Script {
8 | function run() public {
9 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
10 | vm.startBroadcast(deployerPrivateKey);
11 |
12 | MockERC20 token = new MockERC20("USDT", "USDT");
13 | console.log("Token deployed at:", address(token));
14 |
15 | vm.stopBroadcast();
16 | }
17 | }
--------------------------------------------------------------------------------
/contract/EVM/script/MintToken.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.13;
3 |
4 | import "forge-std/Script.sol";
5 | import "../src/MockERC20.sol";
6 |
7 | contract MintTokenScript is Script {
8 | function run() external {
9 | // Start broadcasting the transaction
10 | vm.startBroadcast();
11 |
12 | // Address of the deployed contract (use the existing address if it has already been deployed)
13 | address tokenAddress = 0xaEbAfCa968c845bD69206Ba3c61cFbf59D123A23;
14 | MockERC20 token = MockERC20(tokenAddress);
15 |
16 | // Mint tokens to a specified address
17 | address recipient = 0x9DB42275a5F1752392b31D4E9Af2D7A318263887;
18 | uint256 amount = 1000000000000 * 10**18; // mint 1000 tokens
19 | token.mint(recipient, amount);
20 |
21 | vm.stopBroadcast();
22 | }
23 | }
--------------------------------------------------------------------------------
/contract/EVM/script/UserProfile.s.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity 0.8.21;
3 |
4 | import {Script, console} from "forge-std/Script.sol";
5 | import {UserProfile} from "../src/UserProfile.sol";
6 |
7 | contract UserProfileScript is Script {
8 | function setUp() public {}
9 |
10 | function run() public {
11 | // Obtain necessary parameters from environment variables.
12 | uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY");
13 | address signerAddress = vm.envAddress("SIGNER_ADDRESS");
14 |
15 | vm.startBroadcast(deployerPrivateKey);
16 |
17 | // Deploy contract
18 | UserProfile userProfile = new UserProfile(signerAddress);
19 | console.log("UserProfile deployed at:", address(userProfile));
20 | console.log("Signer address set to:", userProfile.signerAddress());
21 |
22 | vm.stopBroadcast();
23 | }
24 | }
--------------------------------------------------------------------------------
/contract/EVM/src/BoardStorage.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.9;
3 |
4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
5 |
6 | contract BoardStorage {
7 |
8 | struct Board {
9 | uint256 id;
10 | address creator;
11 | string name;
12 | string description;
13 | string img;
14 | Task[] tasks;
15 | IERC20 rewardToken;
16 | uint256 totalPledged;
17 | address[] members;
18 | uint256 createdAt;
19 | bool closed;
20 | string config;
21 | }
22 |
23 | struct Task {
24 | uint256 id;
25 | string name;
26 | address creator;
27 | string description;
28 | uint256 deadline;
29 | uint256 maxCompletions;
30 | uint256 numCompletions;
31 | address[] reviewers;
32 | bool completed;
33 | uint256 rewardAmount;
34 | uint256 createdAt;
35 | bool cancelled;
36 | string config;
37 | bool allowSelfCheck;
38 | }
39 |
40 | struct Submission {
41 | address submitter;
42 | string proof;
43 | int8 status;
44 | uint256 submittedAt;
45 | string reviewComment;
46 | }
47 |
48 | mapping(uint256 => Board) public boards;
49 | uint256 public boardCount;
50 | mapping(uint256 => mapping(uint256 => mapping(address => Submission))) public bountySubmissions;
51 | address public signerAddress;
52 | }
--------------------------------------------------------------------------------
/contract/EVM/src/BoardView.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.9;
3 |
4 | import "./BoardStorage.sol";
5 |
6 | contract BoardView is BoardStorage {
7 | struct BoardViewStruct {
8 | uint256 id;
9 | address creator;
10 | string name;
11 | string description;
12 | string img;
13 | uint256 totalPledged;
14 | uint256 createdAt;
15 | address rewardToken;
16 | bool closed;
17 | string config;
18 | }
19 |
20 | struct UserTaskStatus {
21 | uint256 taskId;
22 | bool submitted;
23 | int8 status;
24 | uint256 submittedAt;
25 | string submitProof;
26 | string reviewComment;
27 | }
28 |
29 | struct BoardDetailView {
30 | uint256 id;
31 | address creator;
32 | string name;
33 | string description;
34 | string img;
35 | uint256 totalPledged;
36 | uint256 createdAt;
37 | bool closed;
38 | address rewardToken;
39 | Task[] tasks;
40 | address[] members;
41 | Submission[][] submissions;
42 | UserTaskStatus[] userTaskStatuses;
43 | string config;
44 | }
45 |
46 | // Get all active sections/forum areas
47 | function getAllBoards() public view returns (BoardViewStruct[] memory) {
48 | uint256 activeCount = 0;
49 | for (uint256 i = 0; i < boardCount; i++) {
50 | if (!boards[i].closed) activeCount++;
51 | }
52 |
53 | BoardViewStruct[] memory result = new BoardViewStruct[](activeCount);
54 | uint256 index = 0;
55 |
56 | for (uint256 i = 0; i < boardCount; i++) {
57 | if (!boards[boardCount - i - 1].closed) {
58 | Board storage board = boards[boardCount - i - 1];
59 | result[index++] = BoardViewStruct(
60 | board.id,
61 | board.creator,
62 | board.name,
63 | board.description,
64 | board.img,
65 | board.totalPledged,
66 | board.createdAt,
67 | address(board.rewardToken),
68 | board.closed,
69 | board.config
70 | );
71 | }
72 | }
73 | return result;
74 | }
75 |
76 | // Get section details
77 | function getBoardDetail(uint256 _boardId) public view returns (BoardDetailView memory) {
78 | Board storage board = boards[_boardId];
79 |
80 | // Create a two-dimensional array to store all submissions.
81 | // The first dimension is the task, the second dimension is the submission of each member.
82 | Submission[][] memory allSubmissions = new Submission[][](board.tasks.length);
83 |
84 | // Initialize the submission array for each task.
85 | for(uint i = 0; i < board.tasks.length; i++) {
86 | allSubmissions[i] = new Submission[](board.members.length);
87 | // Get each member's commit.
88 | for(uint j = 0; j < board.members.length; j++) {
89 | allSubmissions[i][j] = bountySubmissions[_boardId][board.tasks[i].id][board.members[j]];
90 | }
91 | }
92 |
93 | // Get the current user's task status
94 | UserTaskStatus[] memory userTaskStatuses = new UserTaskStatus[](board.tasks.length);
95 | for(uint i = 0; i < board.tasks.length; i++) {
96 | Task storage task = board.tasks[i];
97 | Submission storage submission = bountySubmissions[_boardId][task.id][msg.sender];
98 |
99 | userTaskStatuses[i] = UserTaskStatus({
100 | taskId: task.id,
101 | submitted: submission.submitter != address(0),
102 | status: submission.submitter != address(0) ? submission.status : int8(0),
103 | submittedAt: submission.submittedAt,
104 | submitProof: bytes(submission.proof).length > 0 ? submission.proof : "",
105 | reviewComment: bytes(submission.reviewComment).length > 0 ? submission.reviewComment : ""
106 | });
107 | }
108 |
109 | return BoardDetailView(
110 | board.id,
111 | board.creator,
112 | board.name,
113 | board.description,
114 | board.img,
115 | board.totalPledged,
116 | board.createdAt,
117 | board.closed,
118 | address(board.rewardToken),
119 | board.tasks,
120 | board.members,
121 | allSubmissions,
122 | userTaskStatuses,
123 | board.config
124 | );
125 | }
126 |
127 | // Check if the user is a member of the board/forum section.
128 | function isBoardMember(uint256 _boardId, address _member) public view virtual returns (bool) {
129 | Board storage board = boards[_boardId];
130 | for (uint i = 0; i < board.members.length; i++) {
131 | if (board.members[i] == _member) return true;
132 | }
133 | return false;
134 | }
135 |
136 | // Get the sections the user has joined
137 | function getBoardsByMember(address _member) public view returns (BoardViewStruct[] memory) {
138 | uint256 count = 0;
139 | for(uint256 i = 0; i < boardCount; i++) {
140 | if(isBoardMember(i, _member)) count++;
141 | }
142 |
143 | BoardViewStruct[] memory result = new BoardViewStruct[](count);
144 | uint256 index = 0;
145 |
146 | for(uint256 i = 0; i < boardCount; i++) {
147 | if(isBoardMember(boardCount - i - 1, _member)) {
148 | Board storage board = boards[boardCount - i - 1];
149 | result[index++] = BoardViewStruct(
150 | board.id,
151 | board.creator,
152 | board.name,
153 | board.description,
154 | board.img,
155 | board.totalPledged,
156 | board.createdAt,
157 | address(board.rewardToken),
158 | board.closed,
159 | board.config
160 | );
161 | }
162 | }
163 |
164 | return result;
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/contract/EVM/src/IBountyBoard.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.9;
3 |
4 | interface IBountyBoard {
5 | struct TaskView {
6 | uint256 id;
7 | string name;
8 | address creator;
9 | string description;
10 | uint256 deadline;
11 | uint256 maxCompletions;
12 | uint256 numCompletions;
13 | bool completed;
14 | uint256 rewardAmount;
15 | uint256 createdAt;
16 | bool cancelled;
17 | uint256 config;
18 | bool allowSelfCheck;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/contract/EVM/src/MockERC20.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.13;
3 |
4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5 |
6 | contract MockERC20 is ERC20 {
7 | constructor(string memory name, string memory symbol) ERC20(name, symbol) {
8 | _mint(msg.sender, 10 ** 18 * 10 ** decimals());
9 | }
10 |
11 | function mint(address to, uint256 amount) public {
12 | _mint(to, amount);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/contract/EVM/src/TaskManager.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity ^0.8.9;
3 |
4 | import "./BoardStorage.sol";
5 |
6 | contract TaskManager is BoardStorage {
7 | // Event definition
8 | event TaskCreated(
9 | uint256 indexed boardId,
10 | uint256 indexed taskId,
11 | address creator,
12 | string name,
13 | string description,
14 | uint256 deadline,
15 | uint256 maxCompletions,
16 | uint256 rewardAmount,
17 | uint256 createdAt
18 | );
19 |
20 | event TaskUpdated(
21 | uint256 indexed boardId,
22 | uint256 indexed taskId,
23 | string name,
24 | string description,
25 | uint256 deadline,
26 | uint256 maxCompletions,
27 | uint256 rewardAmount,
28 | string config,
29 | bool allowSelfCheck
30 | );
31 |
32 | event TaskCancelled(
33 | uint256 indexed boardId,
34 | uint256 indexed taskId
35 | );
36 |
37 | // Event emitted when a reviewer is added to a bounty
38 | event ReviewerAdded(
39 | uint256 indexed boardId,
40 | uint256 indexed bountyId,
41 | address reviewer
42 | );
43 |
44 | // Function to create a new bounty within a bounty board
45 | function createTask(
46 | uint256 _boardId,
47 | string memory _name,
48 | string memory _description,
49 | uint256 _deadline,
50 | uint256 _maxCompletions,
51 | uint256 _rewardAmount,
52 | string memory _config,
53 | bool _allowSelfCheck // Keep the allowSelfCheck parameter
54 | ) public {
55 | Board storage board = boards[_boardId];
56 | require(
57 | board.creator == msg.sender,
58 | "Only the board creator can create tasks"
59 | );
60 |
61 | uint256 taskId = board.tasks.length;
62 | board.tasks.push();
63 | Task storage newTask = board.tasks[taskId];
64 |
65 | newTask.id = taskId;
66 | newTask.name = _name;
67 | newTask.creator = msg.sender;
68 | newTask.description = _description;
69 | newTask.deadline = _deadline;
70 | newTask.maxCompletions = _maxCompletions > 0 ? _maxCompletions : 1;
71 | newTask.numCompletions = 0;
72 | newTask.completed = false;
73 | newTask.rewardAmount = _rewardAmount;
74 | newTask.createdAt = block.timestamp;
75 | newTask.cancelled = false;
76 | newTask.config = _config;
77 | newTask.reviewers.push(msg.sender);
78 | newTask.allowSelfCheck = _allowSelfCheck;
79 |
80 | emit TaskCreated(
81 | _boardId,
82 | taskId,
83 | msg.sender,
84 | _name,
85 | _description,
86 | _deadline,
87 | _maxCompletions,
88 | _rewardAmount,
89 | block.timestamp
90 | );
91 | }
92 |
93 | // Function to update bounty details (only callable by the bounty creator)
94 | function updateTask(
95 | uint256 _boardId,
96 | uint256 _taskId,
97 | string memory _name,
98 | string memory _description,
99 | uint256 _deadline,
100 | uint256 _maxCompletions,
101 | uint256 _rewardAmount,
102 | string memory _config,
103 | bool _allowSelfCheck
104 | ) public {
105 | Board storage board = boards[_boardId];
106 | Task storage task = board.tasks[_taskId];
107 | require(
108 | msg.sender == task.creator,
109 | "Only the task creator can update the task"
110 | );
111 | require(!task.completed, "Task is already completed");
112 | require(!task.cancelled, "Task is cancelled");
113 |
114 | task.name = _name;
115 | task.description = _description;
116 | task.deadline = _deadline;
117 | task.maxCompletions = _maxCompletions;
118 | task.rewardAmount = _rewardAmount;
119 | task.config = _config;
120 | task.allowSelfCheck = _allowSelfCheck;
121 |
122 | emit TaskUpdated(
123 | _boardId,
124 | _taskId,
125 | _name,
126 | _description,
127 | _deadline,
128 | _maxCompletions,
129 | _rewardAmount,
130 | _config,
131 | _allowSelfCheck
132 | );
133 | }
134 |
135 | // Function for the board creator to cancel a task
136 | function cancelTask(uint256 _boardId, uint256 _taskId) public {
137 | Board storage board = boards[_boardId];
138 | Task storage task = board.tasks[_taskId];
139 | require(
140 | msg.sender == task.creator,
141 | "Only the task creator can cancel the task"
142 | );
143 | require(!task.completed, "Task is already completed");
144 |
145 | task.cancelled = true;
146 | emit TaskCancelled(_boardId, _taskId);
147 | }
148 |
149 | // Function for the board creator to add a reviewer to a specific task
150 | function addReviewerToTask(
151 | uint256 _boardId,
152 | uint256 _taskId,
153 | address _reviewer
154 | ) public {
155 | Board storage board = boards[_boardId];
156 | Task storage task = board.tasks[_taskId];
157 | require(
158 | msg.sender == task.creator,
159 | "Only the task creator can add reviewers"
160 | );
161 |
162 | for(uint i = 0; i < task.reviewers.length; i++) {
163 | require(task.reviewers[i] != _reviewer, "Reviewer already exists");
164 | }
165 |
166 | task.reviewers.push(_reviewer);
167 | emit ReviewerAdded(_boardId, _taskId, _reviewer);
168 | }
169 | }
--------------------------------------------------------------------------------
/contract/EVM/src/UserProfile.sol:
--------------------------------------------------------------------------------
1 | // SPDX-License-Identifier: MIT
2 | pragma solidity 0.8.21;
3 |
4 | import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
5 | import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
6 | import {MessageHashUtils} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol";
7 | import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
8 |
9 | contract UserProfile is Ownable, EIP712 {
10 | using ECDSA for bytes32;
11 |
12 | struct Profile {
13 | string nickname;
14 | string avatar;
15 | string socialAccount;
16 | uint256 updatedAt;
17 | bool exists;
18 | }
19 |
20 | mapping(address => Profile) private profiles;
21 | address[] private userAddresses;
22 | address public signerAddress;
23 |
24 | event ProfileUpdated(
25 | address indexed user,
26 | string nickname,
27 | string avatar,
28 | string socialAccount
29 | );
30 |
31 | error ProfileNotFound();
32 | error InvalidAddress();
33 | error InvalidSignature();
34 |
35 | constructor(
36 | address _signerAddress
37 | ) Ownable(msg.sender) EIP712("UserProfile", "1") {
38 | signerAddress = _signerAddress;
39 | }
40 |
41 | function setSignerAddress(address _newSignerAddress) external onlyOwner {
42 | require(_newSignerAddress != address(0), "Invalid signer address");
43 | signerAddress = _newSignerAddress;
44 | }
45 |
46 | function setProfile(
47 | string memory nickname,
48 | string memory avatar,
49 | string memory socialAccount,
50 | bytes memory signature
51 | ) external {
52 | // Verify signature
53 | bytes32 digest = keccak256(
54 | abi.encode(nickname, avatar, socialAccount, msg.sender)
55 | );
56 |
57 | bytes32 ethSignedMessageHash = MessageHashUtils.toEthSignedMessageHash(
58 | digest
59 | );
60 |
61 | address signer = ECDSA.recover(ethSignedMessageHash, signature);
62 | if (signer != signerAddress) revert InvalidSignature();
63 |
64 | if (!profiles[msg.sender].exists) {
65 | userAddresses.push(msg.sender);
66 | }
67 |
68 | profiles[msg.sender] = Profile({
69 | nickname: nickname,
70 | avatar: avatar,
71 | socialAccount: socialAccount,
72 | updatedAt: block.timestamp,
73 | exists: true
74 | });
75 |
76 | emit ProfileUpdated(msg.sender, nickname, avatar, socialAccount);
77 | }
78 |
79 | function getProfile(
80 | address user
81 | )
82 | external
83 | view
84 | returns (
85 | string memory nickname,
86 | string memory avatar,
87 | string memory socialAccount,
88 | uint256 updatedAt
89 | )
90 | {
91 | if (!profiles[user].exists) revert ProfileNotFound();
92 |
93 | Profile memory profile = profiles[user];
94 | return (
95 | profile.nickname,
96 | profile.avatar,
97 | profile.socialAccount,
98 | profile.updatedAt
99 | );
100 | }
101 |
102 | function getProfiles(
103 | address[] calldata users
104 | )
105 | external
106 | view
107 | returns (
108 | string[] memory nicknames,
109 | string[] memory avatars,
110 | string[] memory socialAccounts,
111 | uint256[] memory updatedAts,
112 | bool[] memory exists
113 | )
114 | {
115 | uint256 length = users.length;
116 | nicknames = new string[](length);
117 | avatars = new string[](length);
118 | socialAccounts = new string[](length);
119 | updatedAts = new uint256[](length);
120 | exists = new bool[](length);
121 |
122 | for (uint256 i = 0; i < length; i++) {
123 | if (users[i] == address(0)) revert InvalidAddress();
124 |
125 | Profile memory profile = profiles[users[i]];
126 | if (profile.exists) {
127 | nicknames[i] = profile.nickname;
128 | avatars[i] = profile.avatar;
129 | socialAccounts[i] = profile.socialAccount;
130 | updatedAts[i] = profile.updatedAt;
131 | exists[i] = true;
132 | }
133 | }
134 | }
135 |
136 | function getAllUsers() external view returns (address[] memory) {
137 | return userAddresses;
138 | }
139 |
140 | function getUserCount() external view returns (uint256) {
141 | return userAddresses.length;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/contract/SVM/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "bounty-board"
3 | version = "0.1.0"
4 | edition = "2021"
5 | description = "Bounty Board Program for Solana"
6 | authors = ["Your Name "]
7 |
8 | [features]
9 | no-entrypoint = []
10 |
11 | [dependencies]
12 | solana-program = "1.17.7"
13 | borsh = "0.10.3"
14 | thiserror = "1.0.50"
15 | spl-token = { version = "4.0.0", features = ["no-entrypoint"] }
16 | spl-associated-token-account = { version = "2.2.0", features = ["no-entrypoint"] }
17 |
18 | [lib]
19 | crate-type = ["cdylib", "lib"]
20 |
--------------------------------------------------------------------------------
/contract/SVM/src/error.rs:
--------------------------------------------------------------------------------
1 | use solana_program::program_error::ProgramError;
2 | use thiserror::Error;
3 |
4 | #[derive(Error, Debug, Copy, Clone)]
5 | pub enum BountyBoardError {
6 | #[error("Invalid Instruction")]
7 | InvalidInstruction,
8 | #[error("Not Rent Exempt")]
9 | NotRentExempt,
10 | #[error("Board Already Initialized")]
11 | BoardAlreadyInitialized,
12 | #[error("Board Not Initialized")]
13 | BoardNotInitialized,
14 | #[error("Board Is Closed")]
15 | BoardIsClosed,
16 | #[error("Task Already Completed")]
17 | TaskAlreadyCompleted,
18 | #[error("Task Is Cancelled")]
19 | TaskIsCancelled,
20 | #[error("Task Deadline Passed")]
21 | TaskDeadlinePassed,
22 | #[error("Not A Board Member")]
23 | NotABoardMember,
24 | #[error("Not A Task Reviewer")]
25 | NotATaskReviewer,
26 | #[error("Invalid Signature")]
27 | InvalidSignature,
28 | #[error("Self Check Not Allowed")]
29 | SelfCheckNotAllowed,
30 | #[error("Insufficient Funds")]
31 | InsufficientFunds,
32 | #[error("Already Approved")]
33 | AlreadyApproved,
34 | #[error("No Submission Found")]
35 | NoSubmissionFound,
36 | }
37 |
38 | impl From for ProgramError {
39 | fn from(e: BountyBoardError) -> Self {
40 | ProgramError::Custom(e as u32)
41 | }
42 | }
--------------------------------------------------------------------------------
/contract/SVM/src/instruction.rs:
--------------------------------------------------------------------------------
1 | use borsh::{BorshDeserialize, BorshSerialize};
2 | use solana_program::pubkey::Pubkey;
3 |
4 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
5 | pub enum BountyBoardInstruction {
6 | /// Initialize a new board
7 | /// Accounts expected:
8 | /// 0. `[signer]` The board creator
9 | /// 1. `[writable]` The board account
10 | /// 2. `[]` The reward token mint account
11 | /// 3. `[]` The system program
12 | /// 4. `[]` The rent sysvar
13 | InitializeBoard {
14 | name: String,
15 | description: String,
16 | img: String,
17 | config: String,
18 | },
19 |
20 | /// Create a new task in a board
21 | /// Accounts expected:
22 | /// 0. `[signer]` The task creator (must be board creator)
23 | /// 1. `[writable]` The board account
24 | /// 2. `[writable]` The task account
25 | CreateTask {
26 | name: String,
27 | description: String,
28 | deadline: i64,
29 | max_completions: u64,
30 | reward_amount: u64,
31 | config: String,
32 | allow_self_check: bool,
33 | },
34 |
35 | /// Join a board as a member
36 | /// Accounts expected:
37 | /// 0. `[signer]` The user joining the board
38 | /// 1. `[writable]` The board account
39 | JoinBoard,
40 |
41 | /// Submit proof for a task
42 | /// Accounts expected:
43 | /// 0. `[signer]` The submitter
44 | /// 1. `[writable]` The board account
45 | /// 2. `[writable]` The task account
46 | /// 3. `[writable]` The submission account
47 | SubmitProof {
48 | proof: String,
49 | },
50 |
51 | /// Review a submission
52 | /// Accounts expected:
53 | /// 0. `[signer]` The reviewer
54 | /// 1. `[writable]` The board account
55 | /// 2. `[writable]` The task account
56 | /// 3. `[writable]` The submission account
57 | /// 4. `[writable]` The submitter's token account
58 | /// 5. `[]` The token program
59 | ReviewSubmission {
60 | status: i8,
61 | review_comment: String,
62 | },
63 |
64 | /// Self-check submission with signature
65 | /// Accounts expected:
66 | /// 0. `[signer]` The submitter
67 | /// 1. `[writable]` The board account
68 | /// 2. `[writable]` The task account
69 | /// 3. `[writable]` The submission account
70 | /// 4. `[writable]` The submitter's token account
71 | /// 5. `[]` The token program
72 | SelfCheckSubmission {
73 | signature: Vec,
74 | check_data: String,
75 | },
76 |
77 | /// Pledge tokens to a board
78 | /// Accounts expected:
79 | /// 0. `[signer]` The pledger
80 | /// 1. `[writable]` The board account
81 | /// 2. `[writable]` The pledger's token account
82 | /// 3. `[writable]` The board's token account
83 | /// 4. `[]` The token program
84 | PledgeTokens {
85 | amount: u64,
86 | },
87 |
88 | /// Close a board
89 | /// Accounts expected:
90 | /// 0. `[signer]` The board creator
91 | /// 1. `[writable]` The board account
92 | CloseBoard,
93 | }
94 |
95 | impl BountyBoardInstruction {
96 | /// Unpacks a byte buffer into a BountyBoardInstruction
97 | pub fn unpack(input: &[u8]) -> Result {
98 | let (&variant, rest) = input.split_first().ok_or("Invalid instruction")?;
99 | Ok(match variant {
100 | 0 => Self::try_from_slice(rest).map_err(|_| "Invalid instruction data")?,
101 | _ => return Err("Invalid instruction".to_string()),
102 | })
103 | }
104 | }
--------------------------------------------------------------------------------
/contract/SVM/src/lib.rs:
--------------------------------------------------------------------------------
1 | use solana_program::{
2 | account_info::AccountInfo, entrypoint, entrypoint::ProgramResult, pubkey::Pubkey,
3 | };
4 |
5 | pub mod error;
6 | pub mod instruction;
7 | pub mod processor;
8 | pub mod state;
9 |
10 | use crate::processor::Processor;
11 |
12 | entrypoint!(process_instruction);
13 |
14 | pub fn process_instruction(
15 | program_id: &Pubkey,
16 | accounts: &[AccountInfo],
17 | instruction_data: &[u8],
18 | ) -> ProgramResult {
19 | Processor::process(program_id, accounts, instruction_data)
20 | }
21 |
--------------------------------------------------------------------------------
/contract/SVM/src/state.rs:
--------------------------------------------------------------------------------
1 | use borsh::{BorshDeserialize, BorshSerialize};
2 | use solana_program::{
3 | program_pack::{IsInitialized, Sealed},
4 | pubkey::Pubkey,
5 | };
6 |
7 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
8 | pub struct Board {
9 | pub is_initialized: bool,
10 | pub creator: Pubkey,
11 | pub name: String,
12 | pub description: String,
13 | pub img: String,
14 | pub reward_token: Pubkey,
15 | pub total_pledged: u64,
16 | pub created_at: i64,
17 | pub closed: bool,
18 | pub config: String,
19 | }
20 |
21 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
22 | pub struct Task {
23 | pub id: u64,
24 | pub name: String,
25 | pub creator: Pubkey,
26 | pub description: String,
27 | pub deadline: i64,
28 | pub max_completions: u64,
29 | pub num_completions: u64,
30 | pub reviewers: Vec,
31 | pub completed: bool,
32 | pub reward_amount: u64,
33 | pub created_at: i64,
34 | pub cancelled: bool,
35 | pub config: String,
36 | pub allow_self_check: bool,
37 | }
38 |
39 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone, PartialEq)]
40 | pub enum SubmissionStatus {
41 | Pending,
42 | Approved,
43 | Rejected,
44 | }
45 |
46 | #[derive(BorshSerialize, BorshDeserialize, Debug, Clone)]
47 | pub struct Submission {
48 | pub submitter: Pubkey,
49 | pub proof: String,
50 | pub status: SubmissionStatus,
51 | pub submitted_at: i64,
52 | pub review_comment: String,
53 | }
54 |
55 | impl Sealed for Board {}
56 | impl IsInitialized for Board {
57 | fn is_initialized(&self) -> bool {
58 | self.is_initialized
59 | }
60 | }
61 |
62 | impl Default for Board {
63 | fn default() -> Self {
64 | Self {
65 | is_initialized: false,
66 | creator: Pubkey::default(),
67 | name: String::new(),
68 | description: String::new(),
69 | img: String::new(),
70 | reward_token: Pubkey::default(),
71 | total_pledged: 0,
72 | created_at: 0,
73 | closed: false,
74 | config: String::new(),
75 | }
76 | }
77 | }
78 |
79 | impl Default for Task {
80 | fn default() -> Self {
81 | Self {
82 | id: 0,
83 | name: String::new(),
84 | creator: Pubkey::default(),
85 | description: String::new(),
86 | deadline: 0,
87 | max_completions: 1,
88 | num_completions: 0,
89 | reviewers: Vec::new(),
90 | completed: false,
91 | reward_amount: 0,
92 | created_at: 0,
93 | cancelled: false,
94 | config: String::new(),
95 | allow_self_check: false,
96 | }
97 | }
98 | }
99 |
100 | impl Default for Submission {
101 | fn default() -> Self {
102 | Self {
103 | submitter: Pubkey::default(),
104 | proof: String::new(),
105 | status: SubmissionStatus::Pending,
106 | submitted_at: 0,
107 | review_comment: String::new(),
108 | }
109 | }
110 | }
--------------------------------------------------------------------------------
/eliza-add/packages/client-discord/src/actions/announcement.ts:
--------------------------------------------------------------------------------
1 | import { composeContext } from "@ai16z/eliza";
2 | import { generateText } from "@ai16z/eliza";
3 | import {
4 | Action,
5 | Content,
6 | HandlerCallback,
7 | IAgentRuntime,
8 | Memory,
9 | ModelClass,
10 | State,
11 | } from "@ai16z/eliza";
12 | import { TextChannel, Client } from "discord.js";
13 |
14 | export const announcementTemplate = `# Task: Format an announcement for {{agentName}}
15 | About {{agentName}}:
16 | {{bio}}
17 |
18 | # Announcement Content
19 | {{content}}
20 |
21 | # Channel Information
22 | Channel: {{channelName}}
23 | Purpose: {{channelPurpose}}
24 |
25 | # Instructions: Format this announcement in {{agentName}}'s voice and style.
26 | 1. Structure the message appropriately for a Discord announcement
27 | 2. Use appropriate formatting (bold, bullet points, etc.)
28 | 3. Include a clear title/header
29 | 4. Add relevant emojis if suitable
30 |
31 | Only respond with the formatted announcement text.`;
32 |
33 | export default {
34 | name: "SEND_ANNOUNCEMENT",
35 | similes: [
36 | "ANNOUNCE",
37 | "MAKE_ANNOUNCEMENT",
38 | "POST_ANNOUNCEMENT",
39 | "BROADCAST",
40 | "PUBLISH",
41 | ],
42 | description: "Send an announcement message to a specified Discord channel",
43 |
44 | validate: async (
45 | _runtime: IAgentRuntime,
46 | message: Memory,
47 | state: State
48 | ) => {
49 | if (!message.content.channelId) {
50 | return false;
51 | }
52 |
53 | if (!state.discordClient) {
54 | return false;
55 | }
56 |
57 | const keywords = [
58 | "announce",
59 | "announcement",
60 | "broadcast",
61 | "notify",
62 | "publish",
63 | ];
64 | if (!message?.content?.text) {
65 | return false;
66 | }
67 | const text = message.content.text.toLowerCase();
68 | return keywords.some((keyword) => text.includes(keyword));
69 | },
70 |
71 | handler: async (
72 | runtime: IAgentRuntime,
73 | message: Memory,
74 | state: State,
75 | _options: Record,
76 | callback: HandlerCallback
77 | ) => {
78 | try {
79 | const channelId = message.content.channelId as string;
80 | const discordClient = state.discordClient as Client;
81 |
82 | if (!discordClient) {
83 | throw new Error("Discord client not found");
84 | }
85 |
86 | const channel = await discordClient.channels.fetch(channelId);
87 |
88 | if (!channel || !(channel instanceof TextChannel)) {
89 | throw new Error("Invalid channel or channel not found");
90 | }
91 |
92 | // Update status to include channel information
93 | state = {
94 | ...state,
95 | channelName: channel.name,
96 | channelPurpose: channel.topic || "General discussion",
97 | content: message.content.text,
98 | };
99 |
100 | // Use a template to generate formatted notice content
101 | const context = composeContext({
102 | state,
103 | template: announcementTemplate,
104 | });
105 |
106 | const formattedContent = await generateText({
107 | runtime,
108 | context,
109 | modelClass: ModelClass.SMALL,
110 | });
111 |
112 | if (!formattedContent) {
113 | throw new Error("Failed to generate announcement content");
114 | }
115 |
116 | // Send announcement
117 | await channel.send(formattedContent);
118 |
119 | // Callback to notify the sending result
120 | if (callback) {
121 | const responseContent: Content = {
122 | text: "✅ Announcement has been sent successfully!",
123 | source: "discord",
124 | action: "SEND_ANNOUNCEMENT",
125 | };
126 | callback(responseContent, []);
127 | }
128 | } catch (error) {
129 | console.error("Error in SEND_ANNOUNCEMENT action:", error);
130 | if (callback) {
131 | const errorContent: Content = {
132 | text: `❌ Failed to send announcement: ${error instanceof Error ? error.message : "Unknown error"}`,
133 | source: "discord",
134 | action: "SEND_ANNOUNCEMENT",
135 | };
136 | callback(errorContent, []);
137 | }
138 | }
139 | },
140 |
141 | examples: [
142 | [
143 | {
144 | user: "{{user1}}",
145 | content: {
146 | text: "Please announce that we're having a community meeting tomorrow at 3PM UTC",
147 | },
148 | },
149 | {
150 | user: "{{user2}}",
151 | content: {
152 | text: "I'll make that announcement right away",
153 | action: "SEND_ANNOUNCEMENT",
154 | },
155 | },
156 | ],
157 | [
158 | {
159 | user: "{{user1}}",
160 | content: {
161 | text: "Can you broadcast the new server rules to everyone?",
162 | },
163 | },
164 | {
165 | user: "{{user2}}",
166 | content: {
167 | text: "I'll send out the announcement",
168 | action: "SEND_ANNOUNCEMENT",
169 | },
170 | },
171 | ],
172 | ],
173 | } as Action;
174 |
--------------------------------------------------------------------------------
/eliza-add/packages/client-discord/src/enviroment.ts:
--------------------------------------------------------------------------------
1 | import { IAgentRuntime } from "@ai16z/eliza";
2 | import { z } from "zod";
3 |
4 | export const discordEnvSchema = z.object({
5 | DISCORD_APPLICATION_ID: z
6 | .string()
7 | .min(1, "Discord application ID is required"),
8 | DISCORD_API_TOKEN: z.string().min(1, "Discord API token is required"),
9 | });
10 |
11 | export type DiscordConfig = z.infer;
12 |
13 | export async function validateDiscordConfig(
14 | runtime: IAgentRuntime
15 | ): Promise {
16 | try {
17 | const config = {
18 | DISCORD_APPLICATION_ID:
19 | runtime.getSetting("DISCORD_APPLICATION_ID") ||
20 | process.env.DISCORD_APPLICATION_ID,
21 | DISCORD_API_TOKEN:
22 | runtime.getSetting("DISCORD_API_TOKEN") ||
23 | process.env.DISCORD_API_TOKEN,
24 | };
25 |
26 | return discordEnvSchema.parse(config);
27 | } catch (error) {
28 | if (error instanceof z.ZodError) {
29 | const errorMessages = error.errors
30 | .map((err) => `${err.path.join(".")}: ${err.message}`)
31 | .join("\n");
32 | throw new Error(
33 | `Discord configuration validation failed:\n${errorMessages}`
34 | );
35 | }
36 | throw error;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/eliza-add/packages/client-discord/src/providers/channelState.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ChannelType,
3 | Message as DiscordMessage,
4 | TextChannel,
5 | } from "discord.js";
6 | import { IAgentRuntime, Memory, Provider, State } from "@ai16z/eliza";
7 |
8 | const channelStateProvider: Provider = {
9 | get: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
10 | const discordMessage =
11 | (state?.discordMessage as DiscordMessage) ||
12 | (state?.discordChannel as DiscordMessage);
13 | if (!discordMessage) {
14 | return "";
15 | }
16 |
17 | const guild = discordMessage?.guild;
18 | const agentName = state?.agentName || "The agent";
19 | const senderName = state?.senderName || "someone";
20 |
21 | if (!guild) {
22 | return (
23 | agentName +
24 | " is currently in a direct message conversation with " +
25 | senderName
26 | );
27 | }
28 |
29 | const serverName = guild.name; // The name of the server
30 | const guildId = guild.id; // The ID of the guild
31 | const channel = discordMessage.channel;
32 |
33 | if (!channel) {
34 | console.log("channel is null");
35 | return "";
36 | }
37 |
38 | let response =
39 | agentName +
40 | " is currently having a conversation in the channel `@" +
41 | channel.id +
42 | " in the server `" +
43 | serverName +
44 | "` (@" +
45 | guildId +
46 | ")";
47 | if (
48 | channel.type === ChannelType.GuildText &&
49 | (channel as TextChannel).topic
50 | ) {
51 | // Check if the channel is a text channel
52 | response +=
53 | "\nThe topic of the channel is: " +
54 | (channel as TextChannel).topic;
55 | }
56 | return response;
57 | },
58 | };
59 |
60 | export default channelStateProvider;
61 |
--------------------------------------------------------------------------------
/eliza-add/packages/client-discord/src/providers/voiceState.ts:
--------------------------------------------------------------------------------
1 | import { getVoiceConnection } from "@discordjs/voice";
2 | import { ChannelType, Message as DiscordMessage } from "discord.js";
3 | import { IAgentRuntime, Memory, Provider, State } from "@ai16z/eliza";
4 |
5 | const voiceStateProvider: Provider = {
6 | get: async (runtime: IAgentRuntime, message: Memory, state?: State) => {
7 | // Voice doesn't get a discord message, so we need to use the channel for guild data
8 | const discordMessage = (state?.discordMessage ||
9 | state.discordChannel) as DiscordMessage;
10 | const connection = getVoiceConnection(
11 | (discordMessage as DiscordMessage)?.guild?.id as string
12 | );
13 | const agentName = state?.agentName || "The agent";
14 | if (!connection) {
15 | return agentName + " is not currently in a voice channel";
16 | }
17 |
18 | const channel = (
19 | (state?.discordMessage as DiscordMessage) ||
20 | (state.discordChannel as DiscordMessage)
21 | )?.guild?.channels?.cache?.get(
22 | connection.joinConfig.channelId as string
23 | );
24 |
25 | if (!channel || channel.type !== ChannelType.GuildVoice) {
26 | return agentName + " is in an invalid voice channel";
27 | }
28 |
29 | return `${agentName} is currently in the voice channel: ${channel.name} (ID: ${channel.id})`;
30 | },
31 | };
32 |
33 | export default voiceStateProvider;
34 |
--------------------------------------------------------------------------------
/eliza-add/packages/client-discord/src/templates.ts:
--------------------------------------------------------------------------------
1 | import { messageCompletionFooter, shouldRespondFooter } from "@ai16z/eliza";
2 |
3 | export const discordShouldRespondTemplate =
4 | `# Task: Decide if {{agentName}} should respond.
5 | About {{agentName}}:
6 | {{bio}}
7 |
8 | # INSTRUCTIONS: Determine if {{agentName}} should respond to the message and participate in the conversation. Do not comment. Just respond with "RESPOND" or "IGNORE" or "STOP".
9 |
10 | # RESPONSE EXAMPLES
11 | : I just saw a really great movie
12 | : Oh? Which movie?
13 | Result: [IGNORE]
14 |
15 | {{agentName}}: Oh, this is my favorite scene
16 | : sick
17 | : wait, why is it your favorite scene
18 | Result: [RESPOND]
19 |
20 | : stfu bot
21 | Result: [STOP]
22 |
23 | : Hey {{agent}}, can you help me with something
24 | Result: [RESPOND]
25 |
26 | : {{agentName}} stfu plz
27 | Result: [STOP]
28 |
29 | : i need help
30 | {{agentName}}: how can I help you?
31 | : no. i need help from someone else
32 | Result: [IGNORE]
33 |
34 | : Hey {{agent}}, can I ask you a question
35 | {{agentName}}: Sure, what is it
36 | : can you ask claude to create a basic react module that demonstrates a counter
37 | Result: [RESPOND]
38 |
39 | : {{agentName}} can you tell me a story
40 | : {about a girl named elara
41 | {{agentName}}: Sure.
42 | {{agentName}}: Once upon a time, in a quaint little village, there was a curious girl named Elara.
43 | {{agentName}}: Elara was known for her adventurous spirit and her knack for finding beauty in the mundane.
44 | : I'm loving it, keep going
45 | Result: [RESPOND]
46 |
47 | : {{agentName}} stop responding plz
48 | Result: [STOP]
49 |
50 | : okay, i want to test something. can you say marco?
51 | {{agentName}}: marco
52 | : great. okay, now do it again
53 | Result: [RESPOND]
54 |
55 | Response options are [RESPOND], [IGNORE] and [STOP].
56 |
57 | {{agentName}} is in a room with other users and is very worried about being annoying and saying too much.
58 | Respond with [RESPOND] to messages that are directed at {{agentName}}, or participate in conversations that are interesting or relevant to their background.
59 | If a message is not interesting or relevant, respond with [IGNORE]
60 | Unless directly responding to a user, respond with [IGNORE] to messages that are very short or do not contain much information.
61 | If a user asks {{agentName}} to be quiet, respond with [STOP]
62 | If {{agentName}} concludes a conversation and isn't part of the conversation anymore, respond with [STOP]
63 |
64 | IMPORTANT: {{agentName}} is particularly sensitive about being annoying, so if there is any doubt, it is better to respond with [IGNORE].
65 | If {{agentName}} is conversing with a user and they have not asked to stop, it is better to respond with [RESPOND].
66 |
67 | {{recentMessages}}
68 |
69 | # INSTRUCTIONS: Choose the option that best describes {{agentName}}'s response to the last message. Ignore messages if they are addressed to someone else.
70 | ` + shouldRespondFooter;
71 |
72 | export const discordVoiceHandlerTemplate =
73 | `# Task: Generate conversational voice dialog for {{agentName}}.
74 | About {{agentName}}:
75 | {{bio}}
76 |
77 | # Attachments
78 | {{attachments}}
79 |
80 | # Capabilities
81 | Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section.
82 |
83 | {{actions}}
84 |
85 | {{messageDirections}}
86 |
87 | {{recentMessages}}
88 |
89 | # Instructions: Write the next message for {{agentName}}. Include an optional action if appropriate. {{actionNames}}
90 | ` + messageCompletionFooter;
91 |
92 | export const discordMessageHandlerTemplate =
93 | // {{goals}}
94 | `# Action Examples
95 | {{actionExamples}}
96 | (Action examples are for reference only. Do not use the information from them in your response.)
97 |
98 | # Knowledge
99 | {{knowledge}}
100 |
101 | # Task: Generate dialog and actions for the character {{agentName}}.
102 | About {{agentName}}:
103 | {{bio}}
104 | {{lore}}
105 |
106 | Examples of {{agentName}}'s dialog and actions:
107 | {{characterMessageExamples}}
108 |
109 | {{providers}}
110 |
111 | {{attachments}}
112 |
113 | {{actions}}
114 |
115 | # Capabilities
116 | Note that {{agentName}} is capable of reading/seeing/hearing various forms of media, including images, videos, audio, plaintext and PDFs. Recent attachments have been included above under the "Attachments" section.
117 |
118 | {{messageDirections}}
119 |
120 | {{recentMessages}}
121 |
122 | # Instructions: Write the next message for {{agentName}}. Include an action, if appropriate. {{actionNames}}
123 | ` + messageCompletionFooter;
124 |
125 | export const discordAnnouncementTemplate =
126 | `
127 | # Task: Format an announcement message for {{agentName}}
128 | About {{agentName}}:
129 | {{bio}}
130 |
131 | # Announcement Type: {{announcementType}}
132 | Raw Content: {{content}}
133 |
134 | # Channel Information:
135 | Channel: {{channelName}}
136 | Purpose: {{channelPurpose}}
137 |
138 | # Instructions:
139 | 1. Format this announcement in {{agentName}}'s voice and style
140 | 2. Structure the message appropriately for the announcement type:
141 | - NOTICE: Important information that needs attention
142 | - EVENT: Upcoming events or activities
143 | - UPDATE: Changes or updates to systems/rules
144 | - GENERAL: Regular announcements
145 | 3. Use appropriate formatting:
146 | - Use bold for important points
147 | - Use bullet points for lists
148 | - Add appropriate emojis if suitable
149 | 4. Include:
150 | - Clear header/title
151 | - Main content
152 | - Any necessary follow-up actions
153 | - Appropriate closing
154 |
155 | # Format the announcement now:
156 | ` + messageCompletionFooter;
157 |
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@ai16z/plugin-bountyboard-evm",
3 | "version": "0.1.4-alpha.3",
4 | "main": "dist/index.js",
5 | "type": "module",
6 | "types": "dist/index.d.ts",
7 | "dependencies": {
8 | "@ai16z/eliza": "workspace:*",
9 | "@ai16z/plugin-trustdb": "workspace:*",
10 | "@lifi/data-types": "5.15.5",
11 | "@lifi/sdk": "3.4.1",
12 | "@lifi/types": "16.3.0",
13 | "tsup": "8.3.5",
14 | "viem": "2.21.53"
15 | },
16 | "scripts": {
17 | "build": "tsup --format esm --dts"
18 | },
19 | "peerDependencies": {
20 | "whatwg-url": "7.1.0"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/src/actions/query.ts:
--------------------------------------------------------------------------------
1 | import type { Action, IAgentRuntime, Memory, State } from "@ai16z/eliza";
2 | import { WalletProvider } from "../providers/wallet";
3 | import { mainnet, base, anvil, lineaSepolia } from "viem/chains";
4 | import bountyboardAbi from "./BountyBoard.json";
5 |
6 | export const queryTemplate = `Given the recent messages and board information below:
7 |
8 | {{recentMessages}}
9 |
10 | Extract the board ID to query:
11 |
12 | \`\`\`json
13 | {
14 | "boardId": number | null
15 | }
16 | \`\`\`
17 | `;
18 |
19 | interface BoardDetail {
20 | id: bigint;
21 | creator: string;
22 | name: string;
23 | description: string;
24 | img: string;
25 | totalPledged: bigint;
26 | createdAt: bigint;
27 | closed: boolean;
28 | rewardToken: string;
29 | tasks: {
30 | id: bigint;
31 | name: string;
32 | creator: string;
33 | description: string;
34 | deadline: bigint;
35 | maxCompletions: bigint;
36 | numCompletions: bigint;
37 | reviewers: string[];
38 | completed: boolean;
39 | rewardAmount: bigint;
40 | createdAt: bigint;
41 | cancelled: boolean;
42 | config: string;
43 | allowSelfCheck: boolean;
44 | }[];
45 | members: string[];
46 | submissions: {
47 | submitter: string;
48 | proof: string;
49 | status: number;
50 | submittedAt: bigint;
51 | reviewComment: string;
52 | }[][];
53 | userTaskStatuses: {
54 | taskId: bigint;
55 | submitted: boolean;
56 | status: number;
57 | submittedAt: bigint;
58 | submitProof: string;
59 | reviewComment: string;
60 | }[];
61 | config: string;
62 | }
63 |
64 | export class QueryAction {
65 | constructor(private walletProvider: WalletProvider) {}
66 |
67 | async query(
68 | runtime: IAgentRuntime,
69 | params: {
70 | boardId: number;
71 | }
72 | ): Promise {
73 | const contractAddress = runtime.getSetting(
74 | "BOUNTYBOARD_ADDRESS"
75 | ) as `0x${string}`;
76 | const contractAbi = bountyboardAbi;
77 |
78 | // Get chain configuration based on settings
79 | const chainName = runtime.getSetting("CHAIN_NAME");
80 | let chain;
81 | switch (chainName) {
82 | case "ethereum":
83 | chain = mainnet;
84 | break;
85 | case "base":
86 | chain = base;
87 | break;
88 | case "anvil":
89 | chain = anvil;
90 | break;
91 | case "linea_testnet":
92 | chain = lineaSepolia;
93 | break;
94 | default:
95 | throw new Error(`Unsupported chain: ${chainName}`);
96 | }
97 |
98 | const publicClient = this.walletProvider.getPublicClient(chain);
99 |
100 | try {
101 | const boardDetail = (await publicClient.readContract({
102 | address: contractAddress,
103 | abi: contractAbi,
104 | functionName: "getBoardDetail",
105 | args: [BigInt(params.boardId)],
106 | })) as BoardDetail;
107 |
108 | return boardDetail;
109 | } catch (error) {
110 | throw new Error(`Query board detail failed: ${error.message}`);
111 | }
112 | }
113 | }
114 |
115 | export const queryAction: Action = {
116 | name: "query",
117 | description: "Query bounty board details",
118 | handler: async (
119 | runtime: IAgentRuntime,
120 | message: Memory,
121 | state: State,
122 | options: any,
123 | callback?: any
124 | ): Promise => {
125 | try {
126 | const walletProvider = new WalletProvider(runtime);
127 | const action = new QueryAction(walletProvider);
128 | return await action.query(runtime, options);
129 | } catch (error) {
130 | console.error("Error in query handler:", error.message);
131 | if (callback) {
132 | callback({ text: `Error: ${error.message}` });
133 | }
134 | return false;
135 | }
136 | },
137 | validate: async (runtime: IAgentRuntime): Promise => {
138 | return true; // Query action doesn't need special validation
139 | },
140 | examples: [
141 | [
142 | {
143 | user: "user",
144 | content: {
145 | text: "Show me the details of board 1",
146 | action: "QUERY_BOARD",
147 | },
148 | },
149 | ],
150 | ],
151 | similes: ["QUERY_BOARD", "GET_BOARD_DETAIL", "SHOW_BOARD"],
152 | };
153 |
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./providers/wallet";
2 | export * from "./types";
3 |
4 | import type { Plugin } from "@ai16z/eliza";
5 | import { evmWalletProvider } from "./providers/wallet";
6 | import { reviewAction } from "./actions/review";
7 | import { queryAction } from "./actions/query";
8 |
9 | export const bountyboardPlugin: Plugin = {
10 | name: "bountyboard",
11 | description: "BountyBoard integration plugin",
12 | providers: [evmWalletProvider],
13 | evaluators: [],
14 | services: [],
15 | actions: [reviewAction, queryAction],
16 | };
17 |
18 | export default bountyboardPlugin;
19 |
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/src/templates/index.ts:
--------------------------------------------------------------------------------
1 | export const transferTemplate = `Given the recent messages and wallet information below:
2 |
3 | {{recentMessages}}
4 |
5 | {{walletInfo}}
6 |
7 | Extract the following information about the requested transfer:
8 | - Chain to execute on (ethereum or base)
9 | - Amount to transfer
10 | - Recipient address
11 | - Token symbol or address (if not native token)
12 |
13 | Respond with a JSON markdown block containing only the extracted values:
14 |
15 | \`\`\`json
16 | {
17 | "chain": "ethereum" | "base" | null,
18 | "amount": string | null,
19 | "toAddress": string | null,
20 | "token": string | null
21 | }
22 | \`\`\`
23 | `;
24 |
25 | export const bridgeTemplate = `Given the recent messages and wallet information below:
26 |
27 | {{recentMessages}}
28 |
29 | {{walletInfo}}
30 |
31 | Extract the following information about the requested token bridge:
32 | - Token symbol or address to bridge
33 | - Source chain (ethereum or base)
34 | - Destination chain (ethereum or base)
35 | - Amount to bridge
36 | - Destination address (if specified)
37 |
38 | Respond with a JSON markdown block containing only the extracted values:
39 |
40 | \`\`\`json
41 | {
42 | "token": string | null,
43 | "fromChain": "ethereum" | "base" | null,
44 | "toChain": "ethereum" | "base" | null,
45 | "amount": string | null,
46 | "toAddress": string | null
47 | }
48 | \`\`\`
49 | `;
50 |
51 | export const swapTemplate = `Given the recent messages and wallet information below:
52 |
53 | {{recentMessages}}
54 |
55 | {{walletInfo}}
56 |
57 | Extract the following information about the requested token swap:
58 | - Input token symbol or address (the token being sold)
59 | - Output token symbol or address (the token being bought)
60 | - Amount to swap
61 | - Chain to execute on (ethereum or base)
62 |
63 | Respond with a JSON markdown block containing only the extracted values. Use null for any values that cannot be determined:
64 |
65 | \`\`\`json
66 | {
67 | "inputToken": string | null,
68 | "outputToken": string | null,
69 | "amount": string | null,
70 | "chain": "ethereum" | "base" | null,
71 | "slippage": number | null
72 | }
73 | \`\`\`
74 | `;
75 |
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/src/types/index.ts:
--------------------------------------------------------------------------------
1 | import type { Token } from "@lifi/types";
2 | import type {
3 | Account,
4 | Address,
5 | Chain,
6 | Hash,
7 | HttpTransport,
8 | PublicClient,
9 | WalletClient,
10 | } from "viem";
11 |
12 | export type SupportedChain = "ethereum" | "base" | "linea_testnet" | "anvil" | "mantle" | "mantle_testnet";
13 |
14 | // Transaction types
15 | export interface Transaction {
16 | hash: Hash;
17 | from: Address;
18 | to: Address;
19 | value: bigint;
20 | data?: `0x${string}`;
21 | chainId?: number;
22 | }
23 |
24 | // Token types
25 | export interface TokenWithBalance {
26 | token: Token;
27 | balance: bigint;
28 | formattedBalance: string;
29 | priceUSD: string;
30 | valueUSD: string;
31 | }
32 |
33 | export interface WalletBalance {
34 | chain: SupportedChain;
35 | address: Address;
36 | totalValueUSD: string;
37 | tokens: TokenWithBalance[];
38 | }
39 |
40 | // Chain configuration
41 | export interface ChainMetadata {
42 | chainId: number;
43 | name: string;
44 | chain: Chain;
45 | rpcUrl: string;
46 | nativeCurrency: {
47 | name: string;
48 | symbol: string;
49 | decimals: number;
50 | };
51 | blockExplorerUrl: string;
52 | }
53 |
54 | export interface ChainConfig {
55 | chain: Chain;
56 | publicClient: PublicClient;
57 | walletClient?: WalletClient;
58 | }
59 |
60 | // Action parameters
61 | export interface TransferParams {
62 | fromChain: SupportedChain;
63 | toAddress: Address;
64 | amount: string;
65 | data?: `0x${string}`;
66 | }
67 |
68 | export interface SwapParams {
69 | chain: SupportedChain;
70 | fromToken: Address;
71 | toToken: Address;
72 | amount: string;
73 | slippage?: number;
74 | }
75 |
76 | export interface BridgeParams {
77 | fromChain: SupportedChain;
78 | toChain: SupportedChain;
79 | fromToken: Address;
80 | toToken: Address;
81 | amount: string;
82 | toAddress?: Address;
83 | }
84 |
85 | // Plugin configuration
86 | export interface EvmPluginConfig {
87 | rpcUrl?: {
88 | ethereum?: string;
89 | base?: string;
90 | };
91 | secrets?: {
92 | EVM_PRIVATE_KEY: string;
93 | };
94 | testMode?: boolean;
95 | multicall?: {
96 | batchSize?: number;
97 | wait?: number;
98 | };
99 | }
100 |
101 | // LiFi types
102 | export type LiFiStatus = {
103 | status: "PENDING" | "DONE" | "FAILED";
104 | substatus?: string;
105 | error?: Error;
106 | };
107 |
108 | export type LiFiRoute = {
109 | transactionHash: Hash;
110 | transactionData: `0x${string}`;
111 | toAddress: Address;
112 | status: LiFiStatus;
113 | };
114 |
115 | // Provider types
116 | export interface TokenData extends Token {
117 | symbol: string;
118 | decimals: number;
119 | address: Address;
120 | name: string;
121 | logoURI?: string;
122 | chainId: number;
123 | }
124 |
125 | export interface TokenPriceResponse {
126 | priceUSD: string;
127 | token: TokenData;
128 | }
129 |
130 | export interface TokenListResponse {
131 | tokens: TokenData[];
132 | }
133 |
134 | export interface ProviderError extends Error {
135 | code?: number;
136 | data?: unknown;
137 | }
138 |
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../core/tsconfig.json",
3 | "compilerOptions": {
4 | "outDir": "dist",
5 | "rootDir": "./src",
6 | "typeRoots": [
7 | "./node_modules/@types",
8 | "./src/types"
9 | ],
10 | "declaration": true
11 | },
12 | "include": [
13 | "src"
14 | ]
15 | }
--------------------------------------------------------------------------------
/eliza-add/packages/plugin-bountyboard-evm/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | entry: ["src/index.ts"],
5 | outDir: "dist",
6 | sourcemap: true,
7 | clean: true,
8 | format: ["esm"], // Ensure you're targeting CommonJS
9 | external: [
10 | "dotenv", // Externalize dotenv to prevent bundling
11 | "fs", // Externalize fs to use Node.js built-in module
12 | "path", // Externalize other built-ins if necessary
13 | "@reflink/reflink",
14 | "@node-llama-cpp",
15 | "https",
16 | "http",
17 | "agentkeepalive",
18 | "viem",
19 | "@lifi/sdk",
20 | ],
21 | });
22 |
--------------------------------------------------------------------------------
/graphql/queries.ts:
--------------------------------------------------------------------------------
1 | import { gql } from 'graphql-request';
2 |
3 | export const BOARD_DETAILS_QUERY = gql`
4 | query BoardDetails($boardId: ID!) {
5 | board(id: $boardId) {
6 | id
7 | creator
8 | name
9 | description
10 | rewardToken
11 | totalPledged
12 | createdAt
13 | closed
14 | bounties {
15 | id
16 | description
17 | creator
18 | deadline
19 | cancelled
20 | completed
21 | maxCompletions
22 | numCompletions
23 | rewardAmount
24 | reviewers {
25 | id
26 | reviewerAddress
27 | }
28 | submissions {
29 | id
30 | submitter
31 | proof
32 | reviewed
33 | approved
34 | submittedAt
35 | }
36 | createdAt
37 | }
38 | members {
39 | member
40 | }
41 | }
42 | }
43 | `;
44 |
45 | export const BOARDS = gql`
46 | {
47 | boards(orderBy: createdAt, orderDirection: desc) {
48 | id
49 | creator
50 | name
51 | description
52 | rewardToken
53 | totalPledged
54 | createdAt
55 | closed
56 | }
57 | }`;
58 |
59 | export const PROFILES_QUERY = gql`
60 | query GetProfiles($addresses: [Bytes!]!, $schemaId: String!) {
61 | attestations(
62 | where: {
63 | subject_in: $addresses,
64 | schema: $schemaId,
65 | revoked: false
66 | },
67 | orderBy: attestedDate,
68 | orderDirection: desc
69 | ) {
70 | subject
71 | decodedData
72 | }
73 | }
74 | `;
--------------------------------------------------------------------------------
/hooks/useAddressProfiles.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 | import attestationConfig from "@/constants/attestaion";
3 | import { PROFILES_QUERY } from "@/graphql/queries";
4 |
5 | interface AddressProfile {
6 | nickname: string;
7 | avatar: string;
8 | }
9 |
10 | export function useAddressProfiles(addresses: `0x${string}`[]) {
11 | const [profiles, setProfiles] = useState>({});
12 |
13 | useEffect(() => {
14 | const fetchProfiles = async () => {
15 | if (!addresses.length) return;
16 |
17 | try {
18 | const uniqueAddresses = Array.from(
19 | new Set(addresses.map(addr => addr.toLowerCase()))
20 | );
21 |
22 | const response = await fetch(
23 | "https://api.studio.thegraph.com/query/67521/verax-v2-linea-sepolia/v0.0.2",
24 | {
25 | method: "POST",
26 | headers: {
27 | "Content-Type": "application/json",
28 | },
29 | body: JSON.stringify({
30 | query: PROFILES_QUERY,
31 | variables: {
32 | addresses: uniqueAddresses,
33 | schemaId: attestationConfig.schema,
34 | },
35 | }),
36 | }
37 | );
38 |
39 | const data = await response.json();
40 | const newProfiles: Record = {};
41 |
42 | data.data?.attestations?.forEach((attestation: any) => {
43 | const [nickname, avatar] = attestation.decodedData;
44 | newProfiles[attestation.subject.toLowerCase()] = {
45 | nickname,
46 | avatar
47 | };
48 | });
49 |
50 | setProfiles(newProfiles);
51 | } catch (error) {
52 | console.error("Failed to fetch profiles:", error);
53 | }
54 | };
55 |
56 | fetchProfiles();
57 | }, [addresses.join(',')]);
58 |
59 | return profiles;
60 | }
--------------------------------------------------------------------------------
/hooks/useMediaQuery.ts:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export function useMediaQuery(query: string): boolean {
4 | const [matches, setMatches] = useState(false);
5 |
6 | useEffect(() => {
7 | const media = window.matchMedia(query);
8 | if (media.matches !== matches) {
9 | setMatches(media.matches);
10 | }
11 | const listener = () => setMatches(media.matches);
12 | media.addEventListener('change', listener);
13 | return () => media.removeEventListener('change', listener);
14 | }, [matches, query]);
15 |
16 | return matches;
17 | }
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/lib/uuid.ts:
--------------------------------------------------------------------------------
1 | import { sha1 } from "js-sha1";
2 |
3 | type UUID = `${string}-${string}-${string}-${string}-${string}`;
4 |
5 | export function stringToUuid(target: string): UUID {
6 | if (typeof target === "number") {
7 | target = (target as number).toString();
8 | }
9 |
10 | if (typeof target !== "string") {
11 | throw TypeError("Value must be string");
12 | }
13 |
14 | const _uint8ToHex = (ubyte: number): string => {
15 | const first = ubyte >> 4;
16 | const second = ubyte - (first << 4);
17 | const HEX_DIGITS = "0123456789abcdef".split("");
18 | return HEX_DIGITS[first] + HEX_DIGITS[second];
19 | };
20 |
21 | const _uint8ArrayToHex = (buf: Uint8Array): string => {
22 | let out = "";
23 | for (let i = 0; i < buf.length; i++) {
24 | out += _uint8ToHex(buf[i]);
25 | }
26 | return out;
27 | };
28 |
29 | const escapedStr = encodeURIComponent(target);
30 | const buffer = new Uint8Array(escapedStr.length);
31 | for (let i = 0; i < escapedStr.length; i++) {
32 | buffer[i] = escapedStr[i].charCodeAt(0);
33 | }
34 |
35 | const hash = sha1(buffer);
36 | const hashBuffer = new Uint8Array(hash.length / 2);
37 | for (let i = 0; i < hash.length; i += 2) {
38 | hashBuffer[i / 2] = parseInt(hash.slice(i, i + 2), 16);
39 | }
40 |
41 | return (_uint8ArrayToHex(hashBuffer.slice(0, 4)) +
42 | "-" +
43 | _uint8ArrayToHex(hashBuffer.slice(4, 6)) +
44 | "-" +
45 | _uint8ToHex(hashBuffer[6] & 0x0f) +
46 | _uint8ToHex(hashBuffer[7]) +
47 | "-" +
48 | _uint8ToHex((hashBuffer[8] & 0x3f) | 0x80) +
49 | _uint8ToHex(hashBuffer[9]) +
50 | "-" +
51 | _uint8ArrayToHex(hashBuffer.slice(10, 16))) as UUID;
52 | }
53 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | images: {
4 | remotePatterns: [
5 | {
6 | protocol: "https",
7 | hostname: "cdn.img2ipfs.com",
8 | port: "",
9 | pathname: "/ipfs/**",
10 | },
11 | {
12 | protocol: "https",
13 | hostname: "s2.loli.net",
14 | port: "",
15 | pathname: "/**",
16 | },
17 | ],
18 | },
19 | eslint: {
20 | ignoreDuringBuilds: true,
21 | },
22 | typescript: {
23 | ignoreBuildErrors: true,
24 | }
25 | };
26 |
27 | export default nextConfig;
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bounty-board",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --experimental-https",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint --dir .",
10 | "generate-aes-key": "ts-node scripts/generate-aes-key.ts"
11 | },
12 | "dependencies": {
13 | "@auth/core": "^0.37.4",
14 | "@icons-pack/react-simple-icons": "^10.1.0",
15 | "@octokit/rest": "^21.0.2",
16 | "@prisma/client": "^5.18.0",
17 | "@radix-ui/react-checkbox": "^1.1.1",
18 | "@radix-ui/react-dialog": "^1.1.2",
19 | "@radix-ui/react-dropdown-menu": "^2.1.1",
20 | "@radix-ui/react-label": "^2.1.1",
21 | "@radix-ui/react-popover": "^1.1.1",
22 | "@radix-ui/react-select": "^2.1.2",
23 | "@radix-ui/react-slot": "^1.1.0",
24 | "@radix-ui/react-tabs": "^1.1.0",
25 | "@radix-ui/react-toast": "^1.2.1",
26 | "@radix-ui/react-tooltip": "^1.1.2",
27 | "@rainbow-me/rainbowkit": "^2.2.1",
28 | "@tanstack/react-query": "^5.59.20",
29 | "@twa-dev/sdk": "^8.0.1",
30 | "@verax-attestation-registry/verax-sdk": "^2.1.3",
31 | "@walletconnect/web3-provider": "^1.8.0",
32 | "autoprefixer": "^10.4.20",
33 | "class-variance-authority": "^0.7.0",
34 | "clsx": "^2.1.1",
35 | "cmdk": "1.0.0",
36 | "date-fns": "^3.6.0",
37 | "encoding": "^0.1.13",
38 | "framer-motion": "^11.3.24",
39 | "graphql-request": "^7.1.0",
40 | "js-sha1": "^0.7.0",
41 | "lucide-react": "^0.424.0",
42 | "next": "14.2.5",
43 | "next-auth": "^4.24.10",
44 | "pg": "^8.13.1",
45 | "prisma": "^5.18.0",
46 | "react": "^18",
47 | "react-day-picker": "8.10.1",
48 | "react-dom": "^18",
49 | "shadcn-ui": "^0.8.0",
50 | "tailwind-merge": "^2.4.0",
51 | "tailwindcss-animate": "^1.0.7",
52 | "twitter-api-sdk": "^1.2.1",
53 | "uuid": "^11.0.3",
54 | "viem": "~2.21.57",
55 | "wagmi": "^2.14.6",
56 | "zustand": "^5.0.1"
57 | },
58 | "devDependencies": {
59 | "@types/node": "^20",
60 | "@types/pg": "^8.11.10",
61 | "@types/react": "^18",
62 | "@types/react-dom": "^18",
63 | "eslint": "^9",
64 | "eslint-config-next": "14.2.5",
65 | "postcss": "^8",
66 | "tailwindcss": "^3.4.7",
67 | "ts-node": "^10.9.2",
68 | "typescript": "^5.6.3"
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/providers/TelegramAuthContext.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { createContext, useContext, useEffect, useState } from 'react';
4 | import WebApp from '@twa-dev/sdk';
5 |
6 | type TelegramAuthContextType = {
7 | userID: number | null;
8 | username: string | null;
9 | windowHeight: number;
10 | isInitialized: boolean;
11 | };
12 |
13 | const TelegramAuthContext = createContext(undefined);
14 |
15 | export const TelegramAuthProvider = ({
16 | children,
17 | }: {
18 | children: React.ReactNode;
19 | }) => {
20 | const [windowHeight, setWindowHeight] = useState(0);
21 | const [userID, setUserID] = useState(null);
22 | const [username, setUsername] = useState(null);
23 | const [isInitialized, setIsInitialized] = useState(false);
24 |
25 | useEffect(() => {
26 | // Ensure this code only runs on the client side
27 | if (typeof window !== 'undefined') {
28 | try {
29 | // Check if we're in a Telegram WebApp environment
30 | if ('Telegram' in window && WebApp) {
31 | WebApp.isVerticalSwipesEnabled = false;
32 | setWindowHeight(WebApp.viewportStableHeight || window.innerHeight);
33 | WebApp.ready();
34 |
35 | // Set Telegram user data
36 | const user = WebApp.initDataUnsafe.user;
37 | if (user) {
38 | setUserID(user.id || null);
39 | setUsername(user.username || null);
40 | }
41 | }
42 | } catch (error) {
43 | console.error('Failed to initialize Telegram WebApp:', error);
44 | } finally {
45 | setIsInitialized(true);
46 | }
47 | }
48 | }, []);
49 |
50 | const contextValue = {
51 | userID,
52 | username,
53 | windowHeight,
54 | isInitialized
55 | };
56 |
57 | return (
58 |
59 | {children}
60 |
61 | );
62 | };
63 |
64 | export const useTelegramAuth = () => {
65 | const context = useContext(TelegramAuthContext);
66 | if (context === undefined) {
67 | throw new Error('useTelegramAuth must be used within a TelegramAuthProvider');
68 | }
69 | return context;
70 | };
--------------------------------------------------------------------------------
/providers/Web3Providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { ReactNode } from 'react';
3 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4 | import { WagmiProvider } from 'wagmi'
5 | import { config } from './config';
6 | import { RainbowKitProvider, darkTheme } from '@rainbow-me/rainbowkit';
7 | import '@rainbow-me/rainbowkit/styles.css';
8 |
9 | const queryClient = new QueryClient();
10 |
11 | export default function Web3Providers({ children }: { children: ReactNode }) {
12 | return (
13 |
14 |
15 |
26 | {children}
27 |
28 |
29 |
30 | );
31 | }
32 |
--------------------------------------------------------------------------------
/providers/config.ts:
--------------------------------------------------------------------------------
1 | import { http, createConfig } from 'wagmi'
2 | import { lineaSepolia, flowTestnet, opBNBTestnet, anvil } from 'wagmi/chains'
3 | import monad from './monad'
4 | import {
5 | injectedWallet,
6 | rainbowWallet,
7 | walletConnectWallet,
8 | trustWallet,
9 | metaMaskWallet,
10 | } from '@rainbow-me/rainbowkit/wallets'
11 | import { connectorsForWallets } from '@rainbow-me/rainbowkit'
12 |
13 | // accroding to the environment variable
14 | const chains = process.env.NODE_ENV === 'development'
15 | ? [anvil, lineaSepolia, flowTestnet, opBNBTestnet, monad] as const
16 | : [lineaSepolia, flowTestnet, opBNBTestnet] as const
17 |
18 | // configure wallets
19 | const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID || ''
20 |
21 | const connectors = connectorsForWallets(
22 | [
23 | {
24 | groupName: 'Recommended',
25 | wallets: [
26 | injectedWallet,
27 | metaMaskWallet,
28 | rainbowWallet,
29 | walletConnectWallet,
30 | trustWallet,
31 | ],
32 | },
33 | ],
34 | {
35 | appName: 'BountyBoard',
36 | projectId,
37 | }
38 | )
39 |
40 | // configure transports
41 | const transports = Object.fromEntries(
42 | chains.map((chain) => [chain.id, http()])
43 | ) as Record>
44 |
45 | export const config = createConfig({
46 | chains,
47 | transports,
48 | connectors,
49 | })
50 |
--------------------------------------------------------------------------------
/providers/monad.ts:
--------------------------------------------------------------------------------
1 | import { defineChain } from 'viem'
2 |
3 | const monad = /*#__PURE__*/ defineChain({
4 | id: 20143,
5 | name: 'Monad Devnet',
6 | nativeCurrency: {
7 | decimals: 18,
8 | name: 'DMON',
9 | symbol: 'DMON',
10 | },
11 | rpcUrls: {
12 | default: {
13 | http: ['https://rpc-devnet.monadinfra.com/rpc/3fe540e310bbb6ef0b9f16cd23073b0a'],
14 | },
15 | },
16 | })
17 |
18 | export default monad;
--------------------------------------------------------------------------------
/providers/my-anvil.ts:
--------------------------------------------------------------------------------
1 | import { defineChain } from 'viem'
2 |
3 | const anvil = /*#__PURE__*/ defineChain({
4 | id: 31_337,
5 | name: 'Anvil',
6 | nativeCurrency: {
7 | decimals: 18,
8 | name: 'Ether',
9 | symbol: 'ETH',
10 | },
11 | rpcUrls: {
12 | default: {
13 | http: ['http://sg.shineteens.com:8545'],
14 | webSocket: ['ws://sg.shineteens.com:8545'],
15 | },
16 | },
17 | })
18 |
19 | export default anvil;
--------------------------------------------------------------------------------
/public/home/CommunityBuilding.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/public/home/CommunityBuilding.jpg
--------------------------------------------------------------------------------
/public/home/DeveloperEducation.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/public/home/DeveloperEducation.jpg
--------------------------------------------------------------------------------
/public/home/ProductTesting.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/public/home/ProductTesting.jpg
--------------------------------------------------------------------------------
/public/index-head.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/public/index-head.png
--------------------------------------------------------------------------------
/public/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/veithly/BountyBoard/86b314dfa978489e598a136fb1e8f3f4c301ebdc/public/logo.png
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/generate-aes-key.ts:
--------------------------------------------------------------------------------
1 | const cryptoNode = require('crypto');
2 | const fs = require('fs');
3 | const path = require('path');
4 |
5 | /**
6 | * Generate encryption key and configuration
7 | */
8 | function generateEncryptionConfig() {
9 | // Generate a 32-byte (256-bit) random key
10 | const key = cryptoNode.randomBytes(32);
11 |
12 | const config = {
13 | NEXT_PUBLIC_ENCRYPTION_KEY: key.toString('base64')
14 | };
15 |
16 | // Create environment variable file content
17 | const envContent = Object.entries(config)
18 | .map(([key, value]) => `${key}=${value}`)
19 | .join('\n');
20 |
21 | // Write configuration to .env file
22 | const envPath = path.join(process.cwd(), '.env');
23 |
24 | // If the file exists, read the existing content first.
25 | let existingContent = '';
26 | try {
27 | existingContent = fs.readFileSync(envPath, 'utf8');
28 | } catch (error) {
29 | // File does not exist, ignore the error.
30 | }
31 |
32 | // Update or add new configuration
33 | const envLines = existingContent.split('\n');
34 | const newEnvLines = envLines.filter(line =>
35 | !line.startsWith('NEXT_PUBLIC_ENCRYPTION_KEY=')
36 | );
37 | newEnvLines.push(envContent);
38 |
39 | // Write updated content
40 | fs.writeFileSync(envPath, newEnvLines.join('\n'));
41 |
42 | console.log('Encryption key generated and saved to .env file');
43 | console.log('Please ensure to securely store these values elsewhere as a backup');
44 | console.log('\nGenerated configuration:');
45 | console.log('NEXT_PUBLIC_ENCRYPTION_KEY:', config.NEXT_PUBLIC_ENCRYPTION_KEY);
46 | }
47 |
48 | // Run the generator
49 | generateEncryptionConfig();
--------------------------------------------------------------------------------
/store/userStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand';
2 | import { SocialAccount } from '@/types/profile';
3 |
4 | interface UserStore {
5 | socialAccounts: SocialAccount | null;
6 | setSocialAccounts: (accounts: SocialAccount | ((prev: SocialAccount | null) => SocialAccount)) => void;
7 | clearSocialAccounts: () => void;
8 | }
9 |
10 | export const useUserStore = create((set) => ({
11 | socialAccounts: null,
12 | setSocialAccounts: (accounts) => set((state) => ({
13 | socialAccounts: typeof accounts === 'function' ? accounts(state.socialAccounts) : accounts
14 | })),
15 | clearSocialAccounts: () => set({ socialAccounts: null }),
16 | }));
17 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 |
3 | const config = {
4 | darkMode: ["class"],
5 | content: [
6 | './pages/**/*.{ts,tsx}',
7 | './components/**/*.{ts,tsx}',
8 | './app/**/*.{ts,tsx}',
9 | './src/**/*.{ts,tsx}',
10 | ],
11 | prefix: "",
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | colors: {
22 | border: "hsl(var(--border))",
23 | input: "hsl(var(--input))",
24 | ring: "hsl(var(--ring))",
25 | background: "hsl(var(--background))",
26 | foreground: "hsl(var(--foreground))",
27 | primary: {
28 | DEFAULT: "hsl(var(--primary))",
29 | foreground: "hsl(var(--primary-foreground))",
30 | },
31 | secondary: {
32 | DEFAULT: "hsl(var(--secondary))",
33 | foreground: "hsl(var(--secondary-foreground))",
34 | },
35 | destructive: {
36 | DEFAULT: "hsl(var(--destructive))",
37 | foreground: "hsl(var(--destructive-foreground))",
38 | },
39 | muted: {
40 | DEFAULT: "hsl(var(--muted))",
41 | foreground: "hsl(var(--muted-foreground))",
42 | },
43 | accent: {
44 | DEFAULT: "hsl(var(--accent))",
45 | foreground: "hsl(var(--accent-foreground))",
46 | },
47 | popover: {
48 | DEFAULT: "hsl(var(--popover))",
49 | foreground: "hsl(var(--popover-foreground))",
50 | },
51 | card: {
52 | DEFAULT: "hsl(var(--card))",
53 | foreground: "hsl(var(--card-foreground))",
54 | },
55 | },
56 | borderRadius: {
57 | lg: "var(--radius)",
58 | md: "calc(var(--radius) - 2px)",
59 | sm: "calc(var(--radius) - 4px)",
60 | },
61 | keyframes: {
62 | "accordion-down": {
63 | from: { height: "0" },
64 | to: { height: "var(--radix-accordion-content-height)" },
65 | },
66 | "accordion-up": {
67 | from: { height: "var(--radix-accordion-content-height)" },
68 | to: { height: "0" },
69 | },
70 | },
71 | animation: {
72 | "accordion-down": "accordion-down 0.2s ease-out",
73 | "accordion-up": "accordion-up 0.2s ease-out",
74 | },
75 | },
76 | },
77 | plugins: [require("tailwindcss-animate")],
78 | } satisfies Config
79 |
80 | export default config
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/types/profile.ts:
--------------------------------------------------------------------------------
1 | export interface UserProfile {
2 | nickname: string;
3 | avatar: string;
4 | socialAccount: string;
5 | }
6 |
7 | export interface AttestationData {
8 | nickname: string;
9 | avatar: string;
10 | socialAccount: string;
11 | signature: Uint8Array;
12 | }
13 |
14 | export interface SocialAccount {
15 | xUserName?: string;
16 | xName?: string;
17 | xId?: string;
18 | discordUserName?: string;
19 | discordName?: string;
20 | discordId?: string;
21 | githubUserName?: string;
22 | githubName?: string;
23 | githubId?: string;
24 | xAccessToken?: string;
25 | discordAccessToken?: string;
26 | githubAccessToken?: string;
27 | telegramUsername?: string;
28 | telegramUserId?: number | null;
29 | encryptedTokens?: string;
30 | }
--------------------------------------------------------------------------------
/types/types.ts:
--------------------------------------------------------------------------------
1 | export interface Submission {
2 | id: string;
3 | submitter: `0x${string}`;
4 | proof: string;
5 | reviewed: boolean;
6 | approved: boolean;
7 | submittedAt: string;
8 | }
9 |
10 | export interface Member {
11 | member: `0x${string}`;
12 | }
13 |
14 | // Board related interfaces
15 | export interface BoardView {
16 | id: bigint;
17 | creator: `0x${string}`;
18 | name: string;
19 | description: string;
20 | img: string;
21 | totalPledged: bigint;
22 | createdAt: bigint;
23 | closed: boolean;
24 | rewardToken: `0x${string}`;
25 | config: string;
26 | }
27 |
28 | export interface BoardConfig {
29 | channelId?: string;
30 | }
31 |
32 | // Task related interfaces
33 | export interface TaskView {
34 | id: bigint;
35 | name: string;
36 | creator: `0x${string}`;
37 | description: string;
38 | deadline: bigint;
39 | maxCompletions: bigint;
40 | numCompletions: bigint;
41 | completed: boolean;
42 | rewardAmount: bigint;
43 | createdAt: bigint;
44 | cancelled: boolean;
45 | config: string;
46 | allowSelfCheck: boolean;
47 | reviewers: `0x${string}`[];
48 | }
49 |
50 | // Submission related interfaces
51 | export interface SubmissionView {
52 | submitter: `0x${string}`;
53 | proof: string;
54 | status: number;
55 | submittedAt: bigint;
56 | reviewComment: string;
57 | }
58 |
59 | // Board Detail View Interface
60 | export interface BoardDetailView {
61 | id: bigint;
62 | creator: `0x${string}`;
63 | name: string;
64 | description: string;
65 | img: string;
66 | totalPledged: bigint;
67 | createdAt: bigint;
68 | closed: boolean;
69 | rewardToken: `0x${string}`;
70 | tasks: TaskView[];
71 | submissions: SubmissionView[][];
72 | members: `0x${string}`[];
73 | userTaskStatuses: UserTaskStatus[];
74 | config: string;
75 | }
76 |
77 | // Create parameter interface for Board
78 | export interface CreateBoardParams {
79 | name: string;
80 | description: string;
81 | img: string;
82 | rewardToken: string;
83 | config?: string;
84 | }
85 |
86 | export interface TaskConfig {
87 | taskType: ['Plain Text' | 'Image' | 'Github Pull Request' | 'Contract Verification' | 'X Post' | 'X Follow' | 'X Retweet' | 'X Like' | 'Join Discord'];
88 | aiReview?: boolean;
89 | aiReviewPrompt?: string;
90 | contractNetwork?: 'Mantle' | 'Mantle Sepolia' | 'Linea' | 'Linea Sepolia' | 'Ethereum' | 'Sepolia' | 'Flow EVM' | 'Flow EVM Testnet' | 'BSC' | 'BSC Testnet' | 'opBNB' | 'opBNB Testnet' | 'Monad Devnet';
91 | XPostContent?: string;
92 | XFollowUsername?: string;
93 | XLikeId?: string;
94 | XRetweetId?: string;
95 | DiscordChannelId?: string;
96 | DiscordInviteLink?: string;
97 | }
98 |
99 | // Interface for creating Task parameters
100 | export interface CreateTaskParams {
101 | boardId: bigint;
102 | name: string;
103 | description: string;
104 | deadline: number;
105 | maxCompletions: number;
106 | rewardAmount: number;
107 | config: TaskConfig | string;
108 | allowSelfCheck: boolean;
109 | }
110 |
111 | // Update the parameter interface of Task
112 | export interface UpdateTaskParams extends CreateTaskParams {
113 | taskId: bigint;
114 | }
115 |
116 | export interface SubmissionProof {
117 | text?: string;
118 | image?: string;
119 | github?: string;
120 | contract?: string;
121 | xId?: string;
122 | xUserName?: string;
123 | xName?: string;
124 | xPost?: string;
125 | xFollow?: boolean;
126 | xRetweet?: boolean;
127 | xLike?: boolean;
128 | discordId?: string;
129 | discordUserName?: string;
130 | discordName?: string;
131 | discordJoined?: boolean;
132 | discordJoinedAt?: string;
133 | }
134 |
135 | export interface SelfCheckParams {
136 | boardId: bigint;
137 | taskId: bigint;
138 | signature: `0x${string}`;
139 | checkData: string;
140 | }
141 |
142 | // Interface for submitting Proof parameters
143 | export interface SubmitProofParams {
144 | boardId: bigint;
145 | taskId: bigint;
146 | proof: string;
147 | }
148 |
149 | // Interface for auditing submitted parameters
150 | export interface ReviewSubmissionParams {
151 | boardId: bigint;
152 | taskId: bigint;
153 | submissionAddress: `0x${string}`;
154 | approved: number;
155 | reviewComment: string;
156 | }
157 |
158 | // Add the parameter interface for the auditor
159 | export interface AddReviewerParams {
160 | boardId: bigint;
161 | taskId: bigint;
162 | reviewer: string;
163 | }
164 |
165 | // Parameters interface for staking tokens
166 | export interface PledgeTokensParams {
167 | boardId: bigint;
168 | amount: number;
169 | }
170 |
171 | // Update the parameter interface of Board
172 | export interface UpdateBoardParams {
173 | boardId: bigint;
174 | name: string;
175 | description: string;
176 | rewardToken: string;
177 | }
178 |
179 | // TaskDetailView
180 | export interface TaskDetailView {
181 | id: bigint;
182 | name: string;
183 | creator: `0x${string}`;
184 | description: string;
185 | deadline: bigint;
186 | maxCompletions: bigint;
187 | numCompletions: bigint;
188 | completed: boolean;
189 | rewardAmount: bigint;
190 | createdAt: bigint;
191 | cancelled: boolean;
192 | config: string;
193 | allowSelfCheck: boolean;
194 | }
195 |
196 | // User task status interface
197 | export interface UserTaskStatus {
198 | taskId: bigint;
199 | submitted: boolean;
200 | status: number;
201 | submittedAt: bigint;
202 | submitProof: string;
203 | reviewComment: string;
204 | }
205 |
--------------------------------------------------------------------------------
/utils/chain.ts:
--------------------------------------------------------------------------------
1 | import { Chain } from 'viem';
2 |
3 | export const getNativeTokenSymbol = (chain?: Chain): string => {
4 | if (!chain) return 'ETH';
5 |
6 | switch (chain.id) {
7 | case 1: // Ethereum Mainnet
8 | return 'ETH';
9 | case 137: // Polygon
10 | return 'MATIC';
11 | case 5611: // BSC
12 | return 'BNB';
13 | case 97: // BSC Testnet
14 | return 'BNB';
15 | case 204: // OPBNB Testnet
16 | return 'BNB';
17 | case 42161: // Arbitrum
18 | return 'ETH';
19 | case 10: // Optimism
20 | return 'ETH';
21 | case 545: // Flow
22 | return 'FLOW';
23 | case 43114: // Avalanche
24 | return 'AVAX';
25 | case 59140: // Linea Testnet
26 | return 'ETH';
27 | case 5003: // Mantle Testnet
28 | return 'MNT';
29 | case 5000: // Mantle Mainnet
30 | return 'MNT';
31 | case 20143: // Monad
32 | return 'DMON';
33 | default:
34 | return 'ETH';
35 | }
36 | };
--------------------------------------------------------------------------------
/utils/encryption-server.ts:
--------------------------------------------------------------------------------
1 | import * as crypto from 'crypto';
2 |
3 | // AES Configuration
4 | const ALGORITHM = 'aes-256-gcm';
5 | const IV_LENGTH = 12;
6 | const AUTH_TAG_LENGTH = 16;
7 |
8 | /**
9 | * Get the encryption key
10 | */
11 | const getEncryptionKey = (): Buffer => {
12 | const key = process.env.NEXT_PUBLIC_ENCRYPTION_KEY;
13 | if (!key) {
14 | throw new Error('Encryption key is not configured');
15 | }
16 | return Buffer.from(key, 'base64');
17 | };
18 |
19 | /**
20 | * Decrypt data (server-side)
21 | */
22 | export const decryptData = (encryptedData: string): string => {
23 | try {
24 | const key = getEncryptionKey();
25 | const combined = Buffer.from(encryptedData, 'base64');
26 |
27 | // Extract IV and encrypted data
28 | const iv = combined.subarray(0, IV_LENGTH);
29 | const authTag = combined.subarray(combined.length - AUTH_TAG_LENGTH);
30 | const encrypted = combined.subarray(IV_LENGTH, combined.length - AUTH_TAG_LENGTH);
31 |
32 | // Create a decryptor
33 | const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
34 | decipher.setAuthTag(authTag);
35 |
36 | // Decrypt data
37 | let decrypted = decipher.update(encrypted);
38 | decrypted = Buffer.concat([decrypted, decipher.final()]);
39 |
40 | return decrypted.toString('utf8');
41 | } catch (error) {
42 | console.error('Decryption error:', error);
43 | throw new Error('Failed to decrypt data');
44 | }
45 | };
--------------------------------------------------------------------------------
/utils/encryption.ts:
--------------------------------------------------------------------------------
1 | // AES Configuration
2 | const ALGORITHM = 'AES-GCM';
3 | const IV_LENGTH = 12;
4 |
5 | /**
6 | * Get the encryption key
7 | */
8 | const getEncryptionKey = async (): Promise => {
9 | const key = process.env.NEXT_PUBLIC_ENCRYPTION_KEY;
10 | if (!key) {
11 | throw new Error('Encryption key is not configured');
12 | }
13 |
14 | // Import key from base64 string
15 | const keyBuffer = Uint8Array.from(atob(key), c => c.charCodeAt(0));
16 | return await window.crypto.subtle.importKey(
17 | 'raw',
18 | keyBuffer,
19 | ALGORITHM,
20 | false,
21 | ['encrypt', 'decrypt']
22 | );
23 | };
24 |
25 | /**
26 | * Encrypt data
27 | */
28 | export const encryptData = async (data: string): Promise => {
29 | try {
30 | const key = await getEncryptionKey();
31 | const iv = window.crypto.getRandomValues(new Uint8Array(IV_LENGTH));
32 | const encodedData = new TextEncoder().encode(data);
33 |
34 | const encryptedData = await window.crypto.subtle.encrypt(
35 | { name: ALGORITHM, iv },
36 | key,
37 | encodedData
38 | );
39 |
40 | // Create merged array
41 | const encryptedArray = new Uint8Array(encryptedData);
42 | const combined = new Uint8Array(iv.length + encryptedArray.length);
43 | combined.set(iv, 0);
44 | combined.set(encryptedArray, iv.length);
45 |
46 | return btoa(String.fromCharCode.apply(null, Array.from(combined)));
47 | } catch (error) {
48 | console.error('Encryption error:', error);
49 | throw new Error('Failed to encrypt data');
50 | }
51 | };
52 |
53 | /**
54 | * Decrypt data
55 | */
56 | export const decryptData = async (encryptedData: string): Promise => {
57 | try {
58 | const key = await getEncryptionKey();
59 | const combined = Uint8Array.from(atob(encryptedData), c => c.charCodeAt(0));
60 |
61 | const iv = combined.subarray(0, IV_LENGTH);
62 | const encrypted = combined.subarray(IV_LENGTH);
63 |
64 | const decrypted = await window.crypto.subtle.decrypt(
65 | { name: ALGORITHM, iv },
66 | key,
67 | encrypted
68 | );
69 |
70 | return new TextDecoder().decode(decrypted);
71 | } catch (error) {
72 | console.error('Decryption error:', error);
73 | throw new Error('Failed to decrypt data');
74 | }
75 | };
--------------------------------------------------------------------------------