├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── app
├── api
│ ├── assistants
│ │ └── threads
│ │ │ └── messages
│ │ │ └── route.ts
│ ├── auth
│ │ └── [...nextauth]
│ │ │ └── route.ts
│ ├── cron
│ │ └── route.ts
│ ├── deploy-contract
│ │ └── route.ts
│ ├── external-deploy
│ │ └── route.ts
│ ├── ipfs-upload
│ │ └── route.ts
│ ├── og
│ │ └── route.tsx
│ ├── v1
│ │ └── completions
│ │ │ └── route.ts
│ └── verify-contract
│ │ └── route.ts
├── chat
│ └── [id]
│ │ ├── opengraph-image.png
│ │ ├── page.tsx
│ │ └── twitter-image.png
├── globals.css
├── layout.tsx
├── manifest.ts
├── opengraph-image.png
├── page.tsx
├── robots.ts
├── share
│ └── [id]
│ │ └── page.tsx
├── sign-in
│ └── page.tsx
├── sitemap.ts
├── state
│ └── global-store.tsx
└── twitter-image.png
├── auth.ts
├── biome.json
├── bun.lockb
├── components.json
├── components
├── agent-card.tsx
├── chat
│ ├── button-scroll-to-bottom.tsx
│ ├── chat-list.tsx
│ ├── chat-message-actions.tsx
│ ├── chat-message.tsx
│ ├── chat-panel.tsx
│ ├── chat-scroll-anchor.tsx
│ ├── chat.tsx
│ └── prompt-form.tsx
├── connect-button.tsx
├── deploy-contract-button.tsx
├── deploy-tokenscript-button.tsx
├── external-link.tsx
├── footer.tsx
├── header
│ ├── clear-history.tsx
│ ├── header.tsx
│ ├── login-button.tsx
│ ├── settings-drop-down.tsx
│ └── user-menu.tsx
├── landing.tsx
├── markdown.tsx
├── metis-teaser.tsx
├── providers
│ ├── ui-providers.tsx
│ └── web3-provider.tsx
├── sidebar
│ ├── sidebar-actions.tsx
│ ├── sidebar-agents.tsx
│ ├── sidebar-footer.tsx
│ ├── sidebar-item.tsx
│ ├── sidebar-list.tsx
│ └── sidebar.tsx
├── sign-out-button.tsx
└── ui
│ ├── alert-dialog.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── code-block.tsx
│ ├── dialog.tsx
│ ├── dropdown-menu.tsx
│ ├── icons.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── select.tsx
│ ├── separator.tsx
│ ├── sheet.tsx
│ ├── sonner.tsx
│ ├── textarea.tsx
│ └── tooltip.tsx
├── env.ts
├── lib
├── actions
│ ├── ai.ts
│ ├── chat.ts
│ ├── db.ts
│ ├── deploy-contract.ts
│ ├── ipfs.ts
│ ├── solidity
│ │ ├── compile-contract.ts
│ │ ├── deploy-contract.ts
│ │ ├── deploy-tokenscript.ts
│ │ └── verify-contract.ts
│ ├── unstoppable-domains.ts
│ └── verification.ts
├── blockscout.ts
├── config-server.ts
├── config.ts
├── constants.ts
├── contracts
│ ├── contract-utils.ts
│ └── resolve-imports.ts
├── data
│ ├── ipfs.ts
│ ├── kv.ts
│ └── openai.ts
├── hooks
│ ├── use-copy-to-clipboard.tsx
│ ├── use-enter-submit.tsx
│ ├── use-is-client.tsx
│ ├── use-local-storage.ts
│ ├── use-safe-auto-connect.ts
│ ├── use-scroll-to-bottom.tsx
│ ├── use-tokenscript-deploy.ts
│ ├── use-tokenscript-deploy.tsx
│ └── use-wallet-deploy.ts
├── openai.ts
├── rainbowkit.ts
├── solc.d.ts
├── solidity
│ ├── deploy.ts
│ ├── utils.ts
│ └── verification.ts
├── tools.ts
├── types.ts
├── utils.ts
└── viem.ts
├── middleware.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── postcss.config.js
├── public
├── apple-touch-icon.png
├── assets
│ ├── agent-factory.png
│ ├── erc20.png
│ ├── erc721.png
│ ├── metis-logo.png
│ ├── rootstock.png
│ ├── safe-logo.svg
│ ├── tokenscript.png
│ └── web3gpt.png
├── favicon-16x16.png
├── favicon.ico
├── favicon.png
├── logo.svg
├── lotties
│ ├── clock.json
│ ├── globe.json
│ └── puzzle.json
├── manifest.json
├── mantle-logo.jpeg
├── polygon-logo.png
├── web3gpt-logo-beta.svg
└── web3gpt-logo.svg
├── tailwind.config.ts
├── tsconfig.json
└── vercel.json
/.env.example:
--------------------------------------------------------------------------------
1 | AUTH_GITHUB_ID=
2 | AUTH_GITHUB_SECRET=
3 | AUTH_REDIRECT_PROXY_URL=
4 | AUTH_SECRET=
5 | CRON_SECRET=
6 | DEPLOYER_PRIVATE_KEY=
7 | KV_REST_API_READ_ONLY_TOKEN=
8 | KV_REST_API_TOKEN=
9 | KV_REST_API_URL=
10 | KV_URL=
11 | NEXT_PUBLIC_ALCHEMY_API_KEY=
12 | NEXT_PUBLIC_ARBISCAN_API_KEY=
13 | NEXT_PUBLIC_BASESCAN_API_KEY=
14 | NEXT_PUBLIC_BLOCKSCOUT_API_KEY=
15 | NEXT_PUBLIC_ETHERSCAN_API_KEY=
16 | NEXT_PUBLIC_INFURA_API_KEY=
17 | NEXT_PUBLIC_IPFS_GATEWAY=
18 | NEXT_PUBLIC_MANTLESCAN_API_KEY=
19 | NEXT_PUBLIC_OPSCAN_API_KEY=
20 | NEXT_PUBLIC_POLYGONSCAN_API_KEY=
21 | NEXT_PUBLIC_QUICKNODE_API_KEY=
22 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID=
23 | OPENAI_API_KEY=
24 | PINATA_API_KEY=
25 | PINATA_API_SECRET=
26 | PINATA_JWT=
27 | STABILITY_API_KEY=
28 | XAI_API_KEY=
29 | UNKEY_COMPLETIONS_API_ID=
--------------------------------------------------------------------------------
/.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 |
8 | # testing
9 | coverage
10 |
11 | # next.js
12 | .next/
13 | out/
14 | build
15 |
16 | # misc
17 | .DS_Store
18 | *.pem
19 |
20 | # debug
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .pnpm-debug.log*
25 |
26 | # local env files
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 |
32 | # turbo
33 | .turbo
34 |
35 | .contentlayer
36 | .env
37 | .vercel
38 | .vscode
39 | .env*.local
40 |
41 | # tsc
42 | tsconfig.tsbuildinfo
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 W3GPT Corp
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Web3GPT 🚀
2 |
3 | Web3GPT is an AI-powered smart contract development platform that combines Large Language Models (LLMs) with specialized AI agents to streamline blockchain development. Try it live at [w3gpt.ai](https://w3gpt.ai) or check out our [documentation](https://docs.w3gpt.ai).
4 |
5 | 
6 |
7 | ## Key Features 🌟
8 |
9 | - **Multi-Chain Smart Contract Development:** Deploy contracts across multiple EVM-compatible testnets including:
10 | - Arbitrum Sepolia
11 | - Optimism Sepolia
12 | - Base Sepolia
13 | - Metis Sepolia
14 | - Mantle Sepolia
15 | - Polygon Amoy
16 | - Sepolia
17 |
18 | - **Specialized AI Agents:**
19 | - Web3GPT - Core smart contract development agent
20 | - Unstoppable Domains - Domain resolution specialist
21 | - OpenZeppelin 5.0 - Security-focused development using latest OZ libraries
22 | - CTF Agent - Interactive Capture The Flag challenges
23 | - Creator - Custom AI agent creation
24 | - Smart Token - TokenScript-powered token deployment
25 |
26 | - **GitHub Authentication:** Secure login and persistence of your development sessions
27 |
28 | - **Share & Collaborate:** Share your smart contract development conversations with unique shareable URLs
29 |
30 | ## Getting Started 🛠️
31 |
32 | 1. Clone the repository
33 | 2. Configure environment variables (see `.env.example`)
34 | 3. Install dependencies and run the development server
35 |
36 | ```bash
37 | bun install
38 | ```
39 |
40 | ```bash
41 | bun dev
42 | ```
43 |
--------------------------------------------------------------------------------
/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | export const runtime = "edge"
2 |
3 | export { GET, POST } from "@/auth"
4 |
--------------------------------------------------------------------------------
/app/api/cron/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from "next/server"
2 |
3 | import { deleteVerification, getVerifications } from "@/lib/data/kv"
4 | import { checkVerifyStatus, verifyContract } from "@/lib/solidity/verification"
5 |
6 | const PASS_MESSAGE = "Pass - Verified"
7 | const ALREADY_VERIFIED_MESSAGE = "Smart-contract already verified."
8 |
9 | const CRON_SECRET = process.env.CRON_SECRET
10 |
11 | export const GET = async (req: NextRequest) => {
12 | const token = req.headers.get("Authorization")
13 | if (!token) {
14 | return NextResponse.json("Unauthorized", { status: 401 })
15 | }
16 | if (token.replace("Bearer ", "") !== CRON_SECRET) {
17 | return NextResponse.json("Unauthorized", { status: 401 })
18 | }
19 | const verifications = await getVerifications()
20 |
21 | for (const verificationData of verifications) {
22 | try {
23 | const { result: guid } = await verifyContract(verificationData)
24 |
25 | if (guid === ALREADY_VERIFIED_MESSAGE) {
26 | console.log(`${verificationData.viemChain.name} ${verificationData.contractAddress}`)
27 | await deleteVerification(verificationData.deployHash)
28 | continue
29 | }
30 | const verificationStatus = await checkVerifyStatus(guid, verificationData.viemChain)
31 | if (verificationStatus.result === PASS_MESSAGE) {
32 | console.log(`${verificationData.viemChain.name} ${verificationData.contractAddress}`)
33 | await deleteVerification(verificationData.deployHash)
34 | }
35 | } catch (error) {
36 | console.error(error instanceof Error ? error.message : "Unknown error")
37 | }
38 | }
39 |
40 | if (verifications.length > 5) console.error(`Too many verifications in queue: ${verifications.length}`)
41 |
42 | return NextResponse.json({ success: true })
43 | }
44 |
--------------------------------------------------------------------------------
/app/api/deploy-contract/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from "next/server"
2 |
3 | import { deployContract } from "@/lib/actions/solidity/deploy-contract"
4 |
5 | export const runtime = "nodejs"
6 |
7 | export async function POST(req: NextRequest) {
8 | const data = await req.json()
9 | const { chainId, contractName, sourceCode, constructorArgs } = data
10 |
11 | try {
12 | const deployResult = await deployContract({
13 | chainId,
14 | contractName,
15 | sourceCode,
16 | constructorArgs
17 | })
18 | return NextResponse.json(deployResult)
19 | } catch (error) {
20 | return NextResponse.json(`Error in deployContract API ROUTE: ${error}`)
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/app/api/external-deploy/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from "next/server"
2 |
3 | import { parseEther } from "viem"
4 | import { arbitrumSepolia } from "viem/chains"
5 |
6 | import { deployContract } from "@/lib/actions/solidity/deploy-contract"
7 | import { WEB3GPT_API_SECRET } from "@/lib/config-server"
8 |
9 | export const runtime = "nodejs"
10 |
11 | type ContractBuilderParams = {
12 | ownerAddress: string
13 | name?: string
14 | symbol?: string
15 | maxSupply?: number
16 | mintPrice?: number
17 | baseURI?: string
18 | }
19 |
20 | const contractBuilder = ({
21 | ownerAddress,
22 | name = "NFT",
23 | symbol = "NFT",
24 | maxSupply = 1000,
25 | mintPrice = Number(parseEther("0.001")),
26 | baseURI = "ipfs://"
27 | }: ContractBuilderParams) => {
28 | const sourceCode = `// SPDX-License-Identifier: MIT
29 | pragma solidity ^0.8.0;
30 |
31 | import "@openzeppelin/contracts@4.9.3/token/ERC721/extensions/ERC721URIStorage.sol";
32 | import "@openzeppelin/contracts@4.9.3/access/Ownable.sol";
33 | import "@openzeppelin/contracts@4.9.3/utils/Counters.sol";
34 |
35 | contract ConfigurableNFT is ERC721URIStorage, Ownable {
36 | using Counters for Counters.Counter;
37 |
38 | Counters.Counter private _tokenIdCounter;
39 |
40 | uint256 public immutable MAX_SUPPLY;
41 | uint256 public immutable MINT_PRICE;
42 |
43 | string private baseTokenURI;
44 |
45 | constructor(
46 | address owner,
47 | string memory name,
48 | string memory symbol,
49 | uint256 maxSupply,
50 | uint256 mintPrice,
51 | string memory initialBaseURI
52 | ) ERC721(name, symbol) {
53 | MAX_SUPPLY = maxSupply;
54 | MINT_PRICE = mintPrice;
55 | setBaseURI(initialBaseURI);
56 | transferOwnership(owner);
57 | }
58 |
59 | function mintNFT() public payable {
60 | require(_tokenIdCounter.current() < MAX_SUPPLY, "Max supply reached");
61 | require(msg.value >= MINT_PRICE, "Ether sent is not correct");
62 |
63 | uint256 tokenId = _tokenIdCounter.current();
64 | _tokenIdCounter.increment();
65 | _safeMint(msg.sender, tokenId);
66 | _setTokenURI(tokenId, string(abi.encodePacked(baseTokenURI, Strings.toString(tokenId))));
67 | }
68 |
69 | function setBaseURI(string memory newBaseURI) public onlyOwner {
70 | baseTokenURI = newBaseURI;
71 | }
72 |
73 | function _baseURI() internal view override returns (string memory) {
74 | return baseTokenURI;
75 | }
76 |
77 | function withdraw() public onlyOwner {
78 | uint256 balance = address(this).balance;
79 | payable(owner()).transfer(balance);
80 | }
81 | }
82 | `
83 |
84 | const constructorArgs = [ownerAddress, name, symbol, maxSupply?.toString(), mintPrice?.toString(), baseURI]
85 |
86 | return { sourceCode, constructorArgs }
87 | }
88 | export async function POST(req: NextRequest) {
89 | const apiSecret = req.headers.get("web3gpt-api-key")
90 | if (apiSecret !== WEB3GPT_API_SECRET) {
91 | return NextResponse.json({ error: "Unauthorized: invalid web3gpt-api-key" }, { status: 401 })
92 | }
93 | const json = await req.json()
94 |
95 | const { ownerAddress, size, price, baseUri, name, symbol } = json
96 |
97 | if (!ownerAddress) {
98 | return NextResponse.json({ error: "OwnerAddress missing in body" }, { status: 400 })
99 | }
100 |
101 | const { sourceCode, constructorArgs } = contractBuilder({
102 | ownerAddress,
103 | name,
104 | symbol,
105 | maxSupply: size,
106 | mintPrice: price,
107 | baseURI: baseUri
108 | })
109 |
110 | const chainId = arbitrumSepolia.id.toString()
111 |
112 | try {
113 | const deployResult = await deployContract({
114 | chainId,
115 | contractName: "ConfigurableNFT",
116 | sourceCode,
117 | constructorArgs
118 | })
119 | return NextResponse.json(deployResult)
120 | } catch (error) {
121 | return NextResponse.json({ error: `Error in deployContract external: ${error}` }, { status: 500 })
122 | }
123 | }
124 |
125 | export const OPTIONS = async (_req: NextRequest) => {
126 | return NextResponse.json("", {
127 | status: 200
128 | })
129 | }
130 |
--------------------------------------------------------------------------------
/app/api/ipfs-upload/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from "next/server"
2 |
3 | import { ipfsUploadDir } from "@/lib/actions/ipfs"
4 |
5 | export const runtime = "nodejs"
6 |
7 | export async function POST(req: NextRequest) {
8 | const json = await req.json()
9 | const { sources, abi, bytecode, standardJsonInput } = json
10 | try {
11 | const deployResult = await ipfsUploadDir(sources, abi, bytecode, standardJsonInput)
12 |
13 | return NextResponse.json(deployResult)
14 | } catch (error) {
15 | const err = error as Error
16 | console.error(`Error in verifyContract: ${err.message}`)
17 | return NextResponse.json(`Error in verifyContract: ${err.message}`)
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/app/api/v1/completions/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server"
2 |
3 | import { openai } from "@ai-sdk/openai"
4 | import { type NextRequestWithUnkeyContext, withUnkey } from "@unkey/nextjs"
5 | import { track } from "@vercel/analytics/server"
6 | import { generateText, streamText } from "ai"
7 |
8 | const UNKEY_COMPLETIONS_API_ID = process.env.UNKEY_COMPLETIONS_API_ID
9 |
10 | export const maxDuration = 30
11 |
12 | export const POST = withUnkey(
13 | async (req: NextRequestWithUnkeyContext) => {
14 | // Set CORS headers
15 | const headers = {
16 | "Access-Control-Allow-Origin": "*",
17 | "Access-Control-Allow-Methods": "POST",
18 | "Access-Control-Allow-Headers": "Content-Type, Authorization",
19 | }
20 |
21 | // Validate API key
22 | if (!req.unkey?.valid) {
23 | return new NextResponse("unauthorized get api key at https://t.me/w3gptai", {
24 | status: 403,
25 | headers,
26 | })
27 | }
28 |
29 | try {
30 | // Extract request parameters
31 | const { prompt, stream = false, system } = await req.json()
32 |
33 | // Validate prompt
34 | if (!prompt || typeof prompt !== "string") {
35 | return new NextResponse("Prompt is required and must be a string", {
36 | status: 400,
37 | headers,
38 | })
39 | }
40 |
41 | const { keyId = "unknown_key", remaining = 0, ownerId = "unknown_owner" } = req.unkey
42 | console.log("keyId", keyId)
43 | console.log("remaining", remaining)
44 | console.log("ownerId", ownerId)
45 | // Track analytics
46 | track("completions_request", {
47 | apiId: UNKEY_COMPLETIONS_API_ID,
48 | keyId,
49 | ownerId,
50 | remaining,
51 | stream,
52 | })
53 |
54 | // Handle streaming completion
55 | if (stream) {
56 | const result = streamText({
57 | system,
58 | prompt,
59 | model: openai("gpt-4o"),
60 | })
61 |
62 | const response = result.toDataStreamResponse()
63 | // Add CORS headers to stream response
64 | response.headers.set("Access-Control-Allow-Origin", "*")
65 | return response
66 | }
67 |
68 | // Handle non-streaming completion
69 | const { text } = await generateText({
70 | system,
71 | prompt,
72 | model: openai("gpt-4o"),
73 | })
74 |
75 | return NextResponse.json({ text }, { headers })
76 | } catch (error) {
77 | console.error("Completion error:", error)
78 | return new NextResponse("An error occurred while generating the completion", {
79 | status: 500,
80 | headers,
81 | })
82 | }
83 | },
84 | {
85 | handleInvalidKey(req, result) {
86 | console.log("handleInvalidKey", req, result)
87 | return new NextResponse("Unauthorized get api key at https://t.me/w3gptai", {
88 | status: 403,
89 | })
90 | },
91 | disableTelemetry: true,
92 | apiId: UNKEY_COMPLETIONS_API_ID,
93 | },
94 | )
95 |
96 | // Handle CORS preflight requests
97 | export async function OPTIONS() {
98 | return new NextResponse(null, {
99 | status: 200,
100 | headers: {
101 | "Access-Control-Allow-Origin": "*",
102 | "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
103 | "Access-Control-Allow-Headers": "Content-Type, Authorization",
104 | },
105 | })
106 | }
107 |
--------------------------------------------------------------------------------
/app/api/verify-contract/route.ts:
--------------------------------------------------------------------------------
1 | import { type NextRequest, NextResponse } from "next/server"
2 |
3 | import { verifyContract } from "@/lib/actions/solidity/verify-contract"
4 |
5 | export const runtime = "nodejs"
6 |
7 | export async function POST(req: NextRequest) {
8 | const data = await req.json()
9 | const { deployHash, contractAddress, standardJsonInput, encodedConstructorArgs, fileName, contractName, viemChain } =
10 | data
11 |
12 | try {
13 | const deployResult = await verifyContract({
14 | deployHash,
15 | contractAddress,
16 | standardJsonInput,
17 | encodedConstructorArgs,
18 | fileName,
19 | contractName,
20 | viemChain
21 | })
22 | return NextResponse.json(deployResult)
23 | } catch (error) {
24 | return NextResponse.json(`Error in verifyContract: ${error}`)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/chat/[id]/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/app/chat/[id]/opengraph-image.png
--------------------------------------------------------------------------------
/app/chat/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { notFound, redirect } from "next/navigation"
2 |
3 | import { auth } from "@/auth"
4 | import { Chat } from "@/components/chat/chat"
5 | import { DEFAULT_AGENT } from "@/lib/constants"
6 | import { getAgent, getChat } from "@/lib/data/kv"
7 | import { getAiThreadMessages } from "@/lib/data/openai"
8 | import type { NextPageProps } from "@/lib/types"
9 |
10 | export default async function ChatPage({ params, searchParams }: NextPageProps) {
11 | const session = await auth()
12 |
13 | if (!session?.user.id) {
14 | redirect(`/sign-in?next=/chat/${params.id}`)
15 | }
16 |
17 | const chat = await getChat(params.id)
18 |
19 | if (!chat) {
20 | redirect("/")
21 | }
22 |
23 | if (String(chat?.userId) !== session?.user.id) {
24 | notFound()
25 | }
26 |
27 | const agentId = chat.agentId || searchParams?.a
28 | if (typeof agentId !== "string") {
29 | notFound()
30 | }
31 |
32 | const [agent, messages] = await Promise.all([getAgent(agentId), getAiThreadMessages(params.id)])
33 |
34 | return (
35 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/app/chat/[id]/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/app/chat/[id]/twitter-image.png
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 142.1 76.2% 36.3%;
14 | --primary-foreground: 355.7 100% 97.3%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 84.2% 60.2%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 142.1 76.2% 36.3%;
26 | --radius: 0.5rem;
27 | }
28 |
29 | .dark {
30 | --background: 0 0% 0%;
31 | --foreground: 0 0% 95%;
32 | --card: 0 0% 0%;
33 | --card-foreground: 0 0% 95%;
34 | --popover: 0 0% 9%;
35 | --popover-foreground: 0 0% 95%;
36 | --primary: 142.1 70.6% 45.3%;
37 | --primary-foreground: 144.9 80.4% 10%;
38 | --secondary: 240 3.7% 15.9%;
39 | --secondary-foreground: 0 0% 98%;
40 | --muted: 0 0% 15%;
41 | --muted-foreground: 240 5% 64.9%;
42 | --accent: 12 6.5% 15.1%;
43 | --accent-foreground: 0 0% 98%;
44 | --destructive: 0 62.8% 30.6%;
45 | --destructive-foreground: 0 85.7% 97.3%;
46 | --border: 240 3.7% 15.9%;
47 | --input: 240 3.7% 15.9%;
48 | --ring: 142.4 71.8% 29.2%;
49 | }
50 | }
51 |
52 | @layer base {
53 | * {
54 | @apply border-border;
55 | }
56 |
57 | body {
58 | @apply bg-background text-foreground;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata, Viewport } from "next"
2 | import { JetBrains_Mono as FontMono, Inter as FontSans } from "next/font/google"
3 | import { headers } from "next/headers"
4 | import Script from "next/script"
5 | import type { ReactNode } from "react"
6 |
7 | import { Analytics } from "@vercel/analytics/react"
8 | import { ThemeProvider } from "next-themes"
9 | import { cookieToInitialState } from "wagmi"
10 |
11 | import "@/app/globals.css"
12 | import { Header } from "@/components/header/header"
13 | import { Web3Provider } from "@/components/providers/web3-provider"
14 | import { Toaster } from "@/components/ui/sonner"
15 | import { TooltipProvider } from "@/components/ui/tooltip"
16 | import { APP_URL, getWagmiConfig } from "@/lib/config"
17 | import { cn } from "@/lib/utils"
18 |
19 | const fontSans = FontSans({
20 | subsets: ["latin"],
21 | variable: "--font-sans",
22 | })
23 |
24 | const fontMono = FontMono({
25 | subsets: ["latin"],
26 | variable: "--font-mono",
27 | })
28 |
29 | export const metadata: Metadata = {
30 | title: {
31 | default: "Web3GPT",
32 | template: "%s - Web3GPT",
33 | },
34 | description: "Deploy smart contracts, create AI Agents, do more onchain with AI.",
35 | keywords: ["smart contracts", "AI", "web3", "blockchain", "ethereum", "solidity", "development"],
36 | authors: [{ name: "Markeljan" }],
37 | creator: "Markeljan",
38 | publisher: "W3GPT",
39 | robots: {
40 | index: true,
41 | follow: true,
42 | googleBot: {
43 | index: true,
44 | follow: true,
45 | },
46 | },
47 | icons: {
48 | icon: "/logo.svg",
49 | shortcut: "/favicon-16x16.png",
50 | apple: "/apple-touch-icon.png",
51 | },
52 | metadataBase: new URL(APP_URL),
53 | openGraph: {
54 | type: "website",
55 | locale: "en_US",
56 | url: APP_URL,
57 | title: "Web3GPT",
58 | description: "Deploy smart contracts, create AI Agents, do more onchain with AI.",
59 | siteName: "Web3GPT",
60 | images: [
61 | {
62 | url: `${APP_URL}/og-image.png`,
63 | width: 1200,
64 | height: 630,
65 | alt: "Web3GPT",
66 | },
67 | ],
68 | },
69 | twitter: {
70 | card: "summary_large_image",
71 | title: "Web3GPT",
72 | description: "Deploy smart contracts, create AI Agents, do more onchain with AI.",
73 | site: "@w3gptai",
74 | creator: "@0xSoko",
75 | images: [`${APP_URL}/twitter-image.png`],
76 | },
77 | }
78 |
79 | export const viewport: Viewport = {
80 | themeColor: [
81 | { media: "(prefers-color-scheme: light)", color: "white" },
82 | { media: "(prefers-color-scheme: dark)", color: "black" },
83 | ],
84 | }
85 |
86 | export default function Layout({ children }: { children: ReactNode }) {
87 | const initialState = cookieToInitialState(getWagmiConfig(), headers().get("cookie"))
88 |
89 | return (
90 |
91 |
92 |
100 |
101 |
102 |
103 |
104 |
105 | {children}
106 |
107 |
108 |
113 |
114 |
115 |
116 |
117 |
123 |
124 |
125 | )
126 | }
127 |
--------------------------------------------------------------------------------
/app/manifest.ts:
--------------------------------------------------------------------------------
1 | import type { MetadataRoute } from "next"
2 |
3 | export default function manifest(): MetadataRoute.Manifest {
4 | return {
5 | name: "Web3GPT",
6 | short_name: "Web3GPT",
7 | description: "Deploy smart contracts, create AI Agents, do more onchain with AI.",
8 | start_url: "/",
9 | display: "standalone",
10 | background_color: "#000000",
11 | theme_color: "#22DA00",
12 | icons: [
13 | {
14 | src: "/logo.svg",
15 | sizes: "any",
16 | type: "image/x-icon",
17 | },
18 | ],
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/opengraph-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/app/opengraph-image.png
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { auth } from "@/auth"
2 | import { Chat } from "@/components/chat/chat"
3 | import { DEFAULT_AGENT } from "@/lib/constants"
4 | import { getAgent } from "@/lib/data/kv"
5 | import type { NextPageProps } from "@/lib/types"
6 |
7 | export default async function ChatPage({ searchParams }: NextPageProps) {
8 | const agentIdParam = typeof searchParams?.a === "string" ? searchParams.a : null
9 |
10 | const agent = (agentIdParam && (await getAgent(agentIdParam))) || DEFAULT_AGENT
11 | const session = await auth()
12 | const { id, image } = session?.user || {}
13 |
14 | return
15 | }
16 |
--------------------------------------------------------------------------------
/app/robots.ts:
--------------------------------------------------------------------------------
1 | import type { MetadataRoute } from "next"
2 |
3 | import { APP_URL } from "@/lib/config"
4 |
5 | export default function robots(): MetadataRoute.Robots {
6 | return {
7 | rules: {
8 | disallow: ["/api/", "/_next/", "/private/"],
9 | userAgent: "*",
10 | allow: "/",
11 | },
12 | sitemap: `${APP_URL}/sitemap.xml`,
13 | host: APP_URL,
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/app/share/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next"
2 | import { notFound } from "next/navigation"
3 |
4 | import { auth } from "@/auth"
5 | import { AgentCard } from "@/components/agent-card"
6 | import { ChatList } from "@/components/chat/chat-list"
7 | import { Landing } from "@/components/landing"
8 | import { APP_URL } from "@/lib/config"
9 | import { getAgent, getPublishedChat } from "@/lib/data/kv"
10 | import { getAiThreadMessages } from "@/lib/data/openai"
11 | import type { NextPageProps } from "@/lib/types"
12 | import { formatDate } from "@/lib/utils"
13 |
14 | export async function generateMetadata({ params }: NextPageProps) {
15 | const metadata: Metadata = {
16 | title: "Shared Chat",
17 | description: "Deploy smart contracts, create AI Agents, do more onchain with AI.",
18 | openGraph: {
19 | images: [`${APP_URL}/api/og?id=${params.id}&h=630`],
20 | url: `${APP_URL}/share/${params.id}`,
21 | },
22 | twitter: {
23 | card: "summary_large_image",
24 | site: "@w3gptai",
25 | images: [`${APP_URL}/api/og?id=${params.id}&h=675`],
26 | },
27 | }
28 | return metadata
29 | }
30 |
31 | export default async function SharePage({ params, searchParams }: NextPageProps) {
32 | const [session, chat] = await Promise.all([auth(), getPublishedChat(params.id)])
33 | const userId = session?.user.id
34 |
35 | if (!chat || !chat.published) {
36 | notFound()
37 | }
38 | const { title, avatarUrl, agentId = searchParams?.a, createdAt = new Date(), id: chatId = params.id } = chat
39 |
40 | const [agent, messages] = await Promise.all([
41 | typeof agentId === "string" ? getAgent(agentId) : undefined,
42 | getAiThreadMessages(chatId),
43 | ])
44 |
45 | return (
46 | <>
47 |
48 |
49 |
50 |
51 |
{title}
52 |
53 | {formatDate(createdAt)} · {messages.length} messages
54 |
55 |
56 |
57 |
58 | {agent ?
:
}
59 |
60 |
61 | >
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/app/sign-in/page.tsx:
--------------------------------------------------------------------------------
1 | import { redirect } from "next/navigation"
2 |
3 | import { auth } from "@/auth"
4 | import { LoginButton } from "@/components/header/login-button"
5 |
6 | export default async function SignInPage() {
7 | const session = await auth()
8 |
9 | if (session?.user) {
10 | redirect("/")
11 | }
12 | return (
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/app/sitemap.ts:
--------------------------------------------------------------------------------
1 | import type { MetadataRoute } from "next"
2 |
3 | import { APP_URL } from "@/lib/config"
4 |
5 | export default function sitemap(): MetadataRoute.Sitemap {
6 | return [
7 | {
8 | url: APP_URL,
9 | lastModified: new Date(),
10 | changeFrequency: "monthly",
11 | priority: 1,
12 | },
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/app/state/global-store.tsx:
--------------------------------------------------------------------------------
1 | import { create } from "zustand"
2 |
3 | import type { LastDeploymentData } from "@/lib/types"
4 |
5 | interface GlobalState {
6 | tokenScriptViewerUrl?: string | null
7 | setTokenScriptViewerUrl: (tokenScriptViewerUrl: string | null) => void
8 |
9 | completedDeploymentReport: boolean
10 | setCompletedDeploymentReport: (completed: boolean) => void
11 |
12 | readyForTokenScript: boolean
13 | setReadyForTokenScript: (ready: boolean) => void
14 |
15 | isDeploying: boolean
16 | setIsDeploying: (isDeploying: boolean) => void
17 |
18 | // last deployment data
19 | lastDeploymentData?: LastDeploymentData
20 | setLastDeploymentData: (lastDeploymentData: LastDeploymentData) => void
21 | }
22 |
23 | export const useGlobalStore = create((set) => ({
24 | tokenScriptViewerUrl: undefined,
25 | setTokenScriptViewerUrl: (tokenScriptViewerUrl: string | null) => set({ tokenScriptViewerUrl }),
26 |
27 | isDeploying: false,
28 | setIsDeploying: (isDeploying: boolean) => set({ isDeploying }),
29 |
30 | // last deployment data
31 | lastDeploymentData: undefined,
32 | setLastDeploymentData: (lastDeploymentData: LastDeploymentData) => set({ lastDeploymentData }),
33 |
34 | completedDeploymentReport: false,
35 | setCompletedDeploymentReport: (completedDeploymentReport: boolean) => set({ completedDeploymentReport }),
36 |
37 | readyForTokenScript: false,
38 | setReadyForTokenScript: (readyForTokenScript: boolean) => set({ readyForTokenScript }),
39 | }))
40 |
--------------------------------------------------------------------------------
/app/twitter-image.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/app/twitter-image.png
--------------------------------------------------------------------------------
/auth.ts:
--------------------------------------------------------------------------------
1 | import NextAuth, { type DefaultSession } from "next-auth"
2 | import GitHub from "next-auth/providers/github"
3 |
4 | import { storeUser } from "@/lib/data/kv"
5 |
6 | declare module "next-auth" {
7 | interface Session extends DefaultSession {
8 | user: {
9 | id: string
10 | } & DefaultSession["user"]
11 | }
12 | }
13 |
14 | export const {
15 | handlers: { GET, POST },
16 | auth,
17 | signIn,
18 | signOut,
19 | } = NextAuth({
20 | providers: [GitHub],
21 | callbacks: {
22 | async jwt({ token, profile }) {
23 | if (profile?.id) {
24 | const profileId = String(profile.id)
25 | token.id = profileId
26 | const user = {
27 | ...token,
28 | ...profile,
29 | id: profileId,
30 | }
31 | await storeUser(user)
32 | }
33 | return token
34 | },
35 |
36 | session({ session, token }) {
37 | if (token?.id) {
38 | session.user.id = String(token.id)
39 | }
40 | return {
41 | ...session,
42 | user: {
43 | ...session.user,
44 | id: String(token.id),
45 | },
46 | }
47 | },
48 | },
49 | pages: {
50 | signIn: "/sign-in",
51 | },
52 | })
53 |
--------------------------------------------------------------------------------
/biome.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
3 | "files": {
4 | "include": ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
5 | "ignore": ["**/node_modules/**", "**/.next/**"],
6 | "maxSize": 5242880
7 | },
8 | "vcs": {
9 | "enabled": true,
10 | "clientKind": "git",
11 | "defaultBranch": "main",
12 | "useIgnoreFile": true
13 | },
14 | "formatter": {
15 | "enabled": true,
16 | "formatWithErrors": false,
17 | "indentStyle": "space",
18 | "lineWidth": 120
19 | },
20 | "javascript": {
21 | "formatter": {
22 | "enabled": true,
23 | "semicolons": "asNeeded"
24 | }
25 | },
26 | "organizeImports": { "enabled": true },
27 | "linter": {
28 | "enabled": true,
29 | "rules": {
30 | "recommended": true,
31 | "correctness": {
32 | "noUnusedVariables": "warn"
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/bun.lockb:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/bun.lockb
--------------------------------------------------------------------------------
/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": "zinc",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/components/agent-card.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Image from "next/image"
4 | import { useRouter } from "next/navigation"
5 |
6 | import { Button } from "@/components/ui/button"
7 | import { IconCheck, IconCopy, IconRefresh, IconSpinner } from "@/components/ui/icons"
8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
9 | import { APP_URL } from "@/lib/config"
10 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"
11 | import type { Agent } from "@/lib/types"
12 | import { cn } from "@/lib/utils"
13 | import { useTransition } from "react"
14 |
15 | type AgentCardProps = {
16 | agent: Agent
17 | className?: string
18 | setThreadId?: (threadId: string | undefined) => void
19 | }
20 |
21 | export const AgentCard = ({ agent, setThreadId, className }: AgentCardProps) => {
22 | const router = useRouter()
23 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
24 | const [isPending, startTransition] = useTransition()
25 |
26 | return (
27 | <>
28 |
34 |
35 |
43 |
44 |
45 |
{agent.name}
46 |
{agent.description}
47 |
created by {agent.creator}
48 |
49 |
50 | {setThreadId ? (
51 |
52 |
53 |
69 |
70 | New Chat
71 |
72 | ) : null}
73 |
74 |
75 |
84 |
85 | Agent URL
86 |
87 |
88 |
89 |
90 | >
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/components/chat/button-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button, type ButtonProps } from "@/components/ui/button"
4 | import { IconArrowDown } from "@/components/ui/icons"
5 | import { useScrollToBottom } from "@/lib/hooks/use-scroll-to-bottom"
6 | import { cn } from "@/lib/utils"
7 |
8 | export function ButtonScrollToBottom({ className, ...props }: ButtonProps) {
9 | const { isAtBottom, scrollToBottom } = useScrollToBottom()
10 |
11 | if (isAtBottom) {
12 | return null
13 | }
14 |
15 | return (
16 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/chat/chat-list.tsx:
--------------------------------------------------------------------------------
1 | import type { AssistantStatus, Message } from "ai"
2 |
3 | import { ChatMessage } from "@/components/chat/chat-message"
4 | import { Separator } from "@/components/ui/separator"
5 |
6 | export type ChatList = {
7 | messages: Message[]
8 | avatarUrl?: string | null
9 | status?: AssistantStatus
10 | }
11 |
12 | export const ChatList = ({ messages, avatarUrl, status }: ChatList) => {
13 | if (!messages || messages.length === 0) {
14 | return null
15 | }
16 | return (
17 |
18 | {messages
19 | .filter((unfilteredMessage) => unfilteredMessage.role !== "system")
20 | .map((message, index) => (
21 |
22 |
23 | {index < messages.length - 1 && }
24 |
25 | ))}
26 |
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/components/chat/chat-message-actions.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type { Message } from "ai"
4 |
5 | import { Button } from "@/components/ui/button"
6 | import { IconCheck, IconCopy } from "@/components/ui/icons"
7 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"
8 | import { cn } from "@/lib/utils"
9 |
10 | interface ChatMessageActionsProps extends React.ComponentProps<"div"> {
11 | message: Message
12 | }
13 |
14 | export function ChatMessageActions({ message, className, ...props }: ChatMessageActionsProps) {
15 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
16 |
17 | return (
18 |
25 |
29 |
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/components/chat/chat-message.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 |
3 | import { type AssistantStatus, type Message, generateId } from "ai"
4 | import remarkGfm from "remark-gfm"
5 | import remarkMath from "remark-math"
6 |
7 | import { ChatMessageActions } from "@/components/chat/chat-message-actions"
8 | import { MemoizedReactMarkdown } from "@/components/markdown"
9 | import { CodeBlock } from "@/components/ui/code-block"
10 | import { IconUser, IconWeb3GPT } from "@/components/ui/icons"
11 |
12 | export interface ChatMessageProps {
13 | message: Message
14 | status?: AssistantStatus
15 | avatarUrl?: string | null
16 | }
17 |
18 | export function ChatMessage({ message, avatarUrl, status }: ChatMessageProps) {
19 | return (
20 |
21 |
22 | {message.role === "user" ? (
23 | avatarUrl ? (
24 |
25 | ) : (
26 |
27 | )
28 | ) : (
29 |
30 | )}
31 |
32 |
33 | {children}
40 | },
41 | code({ inline, className, children }) {
42 | if (typeof children?.[0] === "string") {
43 | if (children[0] === "▍") {
44 | return ▍
45 | }
46 |
47 | children[0] = children[0].replace("`▍`", "▍")
48 | }
49 |
50 | if (inline) {
51 | return {children}
52 | }
53 |
54 | const match = /language-(\w+)/.exec(className || "")
55 |
56 | return (
57 |
58 | )
59 | },
60 | }}
61 | >
62 | {message.content}
63 |
64 | {status === "in_progress" ? null : }
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/components/chat/chat-panel.tsx:
--------------------------------------------------------------------------------
1 | import type { UseAssistantHelpers } from "@ai-sdk/react"
2 |
3 | import { ButtonScrollToBottom } from "@/components/chat/button-scroll-to-bottom"
4 | import { PromptForm } from "@/components/chat/prompt-form"
5 | import { Button } from "@/components/ui/button"
6 | import { IconSpinner } from "@/components/ui/icons"
7 |
8 | export type ChatPanelProps = Pick
9 |
10 | export function ChatPanel({ status, append, stop, setThreadId }: ChatPanelProps) {
11 | return (
12 |
13 |
14 |
15 |
16 | {status === "in_progress" ? (
17 |
21 | ) : null}
22 |
23 |
26 |
27 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/components/chat/chat-scroll-anchor.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect } from "react"
4 |
5 | import { useInView } from "react-intersection-observer"
6 |
7 | import { useScrollToBottom } from "@/lib/hooks/use-scroll-to-bottom"
8 |
9 | interface ChatScrollAnchorProps {
10 | trackVisibility?: boolean
11 | }
12 |
13 | export function ChatScrollAnchor({ trackVisibility }: ChatScrollAnchorProps) {
14 | const { isAtBottom } = useScrollToBottom()
15 | const { ref, entry, inView } = useInView({
16 | trackVisibility,
17 | delay: 100,
18 | rootMargin: "0px 0px -150px 0px",
19 | })
20 |
21 | useEffect(() => {
22 | if (isAtBottom && trackVisibility && !inView) {
23 | entry?.target.scrollIntoView({
24 | block: "start",
25 | })
26 | }
27 | }, [inView, entry, isAtBottom, trackVisibility])
28 |
29 | return
30 | }
31 |
--------------------------------------------------------------------------------
/components/chat/chat.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useCallback, useEffect, useRef } from "react"
4 |
5 | import { type CreateMessage, type Message, useAssistant } from "@ai-sdk/react"
6 | import { generateId } from "ai"
7 |
8 | import { useGlobalStore } from "@/app/state/global-store"
9 | import { AgentCard } from "@/components/agent-card"
10 | import { ChatList } from "@/components/chat/chat-list"
11 | import { ChatPanel } from "@/components/chat/chat-panel"
12 | import { ChatScrollAnchor } from "@/components/chat/chat-scroll-anchor"
13 | import { Landing } from "@/components/landing"
14 | import { DEFAULT_AGENT, DEFAULT_AGENT_ID, TOKENSCRIPT_AGENT_ID } from "@/lib/constants"
15 | import { useScrollToBottom } from "@/lib/hooks/use-scroll-to-bottom"
16 | import type { Agent } from "@/lib/types"
17 | import { cn } from "@/lib/utils"
18 |
19 | type ChatProps = {
20 | agent: Agent
21 | className?: string
22 | initialThreadId?: string
23 | initialMessages?: Message[]
24 | userId?: string
25 | avatarUrl?: string | null
26 | }
27 |
28 | export const Chat = ({ initialThreadId, initialMessages = [], agent, className, userId, avatarUrl }: ChatProps) => {
29 | const chatRef = useRef(null)
30 | const { scrollToBottom } = useScrollToBottom(chatRef)
31 |
32 | const {
33 | tokenScriptViewerUrl,
34 | lastDeploymentData,
35 | completedDeploymentReport,
36 | setCompletedDeploymentReport,
37 | setTokenScriptViewerUrl,
38 | } = useGlobalStore()
39 | const { messages, status, setThreadId, stop, append, setMessages, threadId } = useAssistant({
40 | threadId: initialThreadId,
41 | api: "/api/assistants/threads/messages",
42 | body: {
43 | assistantId: agent.id || DEFAULT_AGENT.id,
44 | },
45 | })
46 |
47 | const isInProgress = status === "in_progress"
48 | const isTokenScriptAgent = agent.id === TOKENSCRIPT_AGENT_ID
49 | const showLanding = agent.id === DEFAULT_AGENT_ID && !initialThreadId
50 |
51 | const appendSystemMessage = useCallback(
52 | (message: Message | CreateMessage) => {
53 | setMessages((prevMessages) => [
54 | ...prevMessages,
55 | {
56 | ...message,
57 | id: message.id || generateId(),
58 | },
59 | ])
60 | },
61 | [setMessages],
62 | )
63 |
64 | useEffect(() => {
65 | if (threadId && !isInProgress && initialThreadId !== threadId) {
66 | history.pushState(null, "", `/chat/${threadId}`)
67 | }
68 | }, [initialThreadId, isInProgress, threadId])
69 |
70 | useEffect(() => {
71 | if (messages.length === 0 && initialMessages?.length > 0) {
72 | setMessages(initialMessages)
73 | setTimeout(() => {
74 | scrollToBottom()
75 | }, 500)
76 | }
77 | }, [initialMessages, messages, setMessages, scrollToBottom])
78 |
79 | useEffect(() => {
80 | if (isTokenScriptAgent && lastDeploymentData && !completedDeploymentReport && !isInProgress) {
81 | const { ipfsUrl, explorerUrl, contractAddress, transactionHash, walletAddress, chainId } = lastDeploymentData
82 | appendSystemMessage({
83 | role: "system",
84 | content: `User deployed the following TokenScript contract:
85 | ${JSON.stringify({
86 | ipfsUrl,
87 | explorerUrl,
88 | contractAddress,
89 | transactionHash,
90 | walletAddress,
91 | chainId,
92 | })}`,
93 | })
94 | setCompletedDeploymentReport(true)
95 | scrollToBottom()
96 | }
97 | }, [
98 | appendSystemMessage,
99 | setCompletedDeploymentReport,
100 | scrollToBottom,
101 | lastDeploymentData,
102 | isTokenScriptAgent,
103 | completedDeploymentReport,
104 | isInProgress,
105 | ])
106 |
107 | useEffect(() => {
108 | if (tokenScriptViewerUrl && completedDeploymentReport && !isInProgress) {
109 | appendSystemMessage({
110 | role: "system",
111 | content: `User uploaded the TokenScript and updated the scriptURI:
112 | ${JSON.stringify(tokenScriptViewerUrl)}`,
113 | })
114 | setTokenScriptViewerUrl(null)
115 | }
116 | }, [appendSystemMessage, isInProgress, tokenScriptViewerUrl, completedDeploymentReport, setTokenScriptViewerUrl])
117 |
118 | return (
119 | <>
120 |
121 | {showLanding ?
:
}
122 |
123 |
124 |
125 |
126 | >
127 | )
128 | }
129 |
--------------------------------------------------------------------------------
/components/chat/prompt-form.tsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from "next/navigation"
2 | import { useEffect, useRef, useState, useTransition } from "react"
3 |
4 | import type { UseAssistantHelpers } from "@ai-sdk/react"
5 | import Textarea from "react-textarea-autosize"
6 |
7 | import { Button, buttonVariants } from "@/components/ui/button"
8 | import { IconArrowElbow, IconHome, IconSpinner } from "@/components/ui/icons"
9 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
10 | import { useEnterSubmit } from "@/lib/hooks/use-enter-submit"
11 | import { useScrollToBottom } from "@/lib/hooks/use-scroll-to-bottom"
12 | import { cn } from "@/lib/utils"
13 |
14 | type PromptProps = Pick
15 |
16 | export const PromptForm = ({ append, status, setThreadId }: PromptProps) => {
17 | const [input, setInput] = useState("")
18 | const inputRef = useRef(null)
19 | const { formRef, onKeyDown } = useEnterSubmit()
20 | const { scrollToBottom } = useScrollToBottom()
21 | const [isPendingTransition, startTransition] = useTransition()
22 | const router = useRouter()
23 | const isInProgress = status === "in_progress"
24 |
25 | useEffect(() => {
26 | if (inputRef.current) {
27 | inputRef.current.focus()
28 | }
29 | }, [])
30 |
31 | return (
32 |
104 | )
105 | }
106 |
--------------------------------------------------------------------------------
/components/connect-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ConnectButton as RainbowkitConnectButton } from "@rainbow-me/rainbowkit"
4 |
5 | import { useSafeAutoConnect } from "@/lib/hooks/use-safe-auto-connect"
6 |
7 | export function ConnectButton() {
8 | useSafeAutoConnect()
9 |
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/components/deploy-tokenscript-button.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 |
3 | import { useAccount } from "wagmi"
4 |
5 | import { useGlobalStore } from "@/app/state/global-store"
6 | import { Badge } from "@/components/ui/badge"
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogFooter,
13 | DialogHeader,
14 | DialogTitle,
15 | DialogTrigger,
16 | } from "@/components/ui/dialog"
17 | import { IconSpinner } from "@/components/ui/icons"
18 | import { storeTokenScriptDeploymentAction } from "@/lib/actions/deploy-contract"
19 | import { useTokenScriptDeploy } from "@/lib/hooks/use-tokenscript-deploy"
20 |
21 | type DeployContractButtonProps = {
22 | getSourceCode: () => string
23 | }
24 |
25 | export const DeployTokenScriptButton = ({ getSourceCode }: DeployContractButtonProps) => {
26 | const { deploy: deployTokenScript } = useTokenScriptDeploy()
27 | const [explorerUrl, setExplorerUrl] = useState()
28 | const [isErrorDeploying, setIsErrorDeploying] = useState(false)
29 | const [sourceCode, setSourceCode] = useState(getSourceCode())
30 | const { isDeploying, setIsDeploying, setTokenScriptViewerUrl } = useGlobalStore()
31 | const { chainId } = useAccount()
32 | const [isDialogOpen, setIsDialogOpen] = useState(false)
33 |
34 | const handleDeployToIPFS = async () => {
35 | if (!chainId || !sourceCode) return
36 |
37 | setIsDeploying(true)
38 | setIsErrorDeploying(false)
39 |
40 | try {
41 | const deploymentData = await deployTokenScript({ tokenScriptSource: sourceCode })
42 |
43 | if (!deploymentData) throw new Error("Error deploying TokenScript")
44 |
45 | const { cid, txHash, tokenAddress } = deploymentData
46 | const tokenscriptViewerUrl = `https://viewer.tokenscript.org/?chain=${chainId}&contract=${tokenAddress}`
47 |
48 | await storeTokenScriptDeploymentAction({
49 | chainId: chainId.toString(),
50 | deployHash: txHash,
51 | cid,
52 | tokenAddress,
53 | })
54 |
55 | setExplorerUrl(explorerUrl)
56 | setTokenScriptViewerUrl(tokenscriptViewerUrl)
57 |
58 | setIsDialogOpen(false)
59 | } catch (e) {
60 | console.error(e)
61 | setIsErrorDeploying(true)
62 | } finally {
63 | setIsDeploying(false)
64 | }
65 | }
66 |
67 | return (
68 |
69 |
123 |
124 | )
125 | }
126 |
--------------------------------------------------------------------------------
/components/external-link.tsx:
--------------------------------------------------------------------------------
1 | export function ExternalLink({
2 | href,
3 | children,
4 | }: {
5 | href: string
6 | children: React.ReactNode
7 | }) {
8 | return (
9 |
15 | {children}
16 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | import { ExternalLink } from "@/components/external-link"
2 | import { cn } from "@/lib/utils"
3 |
4 | export function FooterText({ className, ...props }: React.ComponentProps<"p">) {
5 | return (
6 |
7 | Made by soko.eth with{" "}
8 | Vercel AI.
9 |
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/components/header/clear-history.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState, useTransition } from "react"
4 |
5 | import {
6 | AlertDialog,
7 | AlertDialogAction,
8 | AlertDialogCancel,
9 | AlertDialogContent,
10 | AlertDialogDescription,
11 | AlertDialogFooter,
12 | AlertDialogHeader,
13 | AlertDialogTitle,
14 | AlertDialogTrigger,
15 | } from "@/components/ui/alert-dialog"
16 | import { Button } from "@/components/ui/button"
17 | import { IconClear, IconSpinner } from "@/components/ui/icons"
18 | import { clearChatsAction } from "@/lib/actions/chat"
19 |
20 | export function ClearHistory() {
21 | const [open, setOpen] = useState(false)
22 | const [isPending, startTransition] = useTransition()
23 |
24 | return (
25 |
26 |
27 |
32 |
33 |
34 |
35 | Are you absolutely sure?
36 |
37 | This will permanently delete your chat history and remove your data from our servers.
38 |
39 |
40 |
41 | Cancel
42 | {
45 | event.preventDefault()
46 | startTransition(async () => {
47 | await clearChatsAction()
48 | setOpen(false)
49 | })
50 | }}
51 | >
52 | {isPending ? : null}
53 | Delete
54 |
55 |
56 |
57 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/components/header/header.tsx:
--------------------------------------------------------------------------------
1 | import Link from "next/link"
2 |
3 | import { auth } from "@/auth"
4 | import { ConnectButton } from "@/components/connect-button"
5 | import { ClearHistory } from "@/components/header/clear-history"
6 | import { LoginButton } from "@/components/header/login-button"
7 | import { SettingsDropDown } from "@/components/header/settings-drop-down"
8 | import { UserMenu } from "@/components/header/user-menu"
9 | import { Sidebar } from "@/components/sidebar/sidebar"
10 | import { SidebarAgents } from "@/components/sidebar/sidebar-agents"
11 | import { SidebarFooter } from "@/components/sidebar/sidebar-footer"
12 | import { SidebarList } from "@/components/sidebar/sidebar-list"
13 | import { Badge } from "@/components/ui/badge"
14 | import { Button } from "@/components/ui/button"
15 | import { IconSeparator } from "@/components/ui/icons"
16 | import { SheetHeader, SheetTitle } from "@/components/ui/sheet"
17 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
18 | import { getChatList } from "@/lib/data/kv"
19 | import { cn } from "@/lib/utils"
20 | import { MetisTeaser } from "@/components/metis-teaser"
21 |
22 | export const Header = async () => {
23 | const chatList = await getChatList()
24 | const session = await auth()
25 |
26 | const user = session?.user
27 |
28 | return (
29 |
81 | )
82 | }
83 |
--------------------------------------------------------------------------------
/components/header/login-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 |
5 | import { signIn } from "next-auth/react"
6 |
7 | import { Button, type ButtonProps } from "@/components/ui/button"
8 | import { IconGitHub, IconSpinner } from "@/components/ui/icons"
9 | import { cn } from "@/lib/utils"
10 |
11 | interface LoginButtonProps extends ButtonProps {
12 | showGithubIcon?: boolean
13 | text?: string
14 | }
15 |
16 | export function LoginButton({
17 | text = "Login with GitHub",
18 | showGithubIcon = true,
19 | className,
20 | ...props
21 | }: LoginButtonProps) {
22 | const [isLoading, setIsLoading] = useState(false)
23 | return (
24 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/components/header/settings-drop-down.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 | import { useTransition } from "react"
5 |
6 | import { useTheme } from "next-themes"
7 |
8 | import { Button, buttonVariants } from "@/components/ui/button"
9 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
10 | import { IconCog, IconGitHub, IconTelegram, IconTwitter } from "@/components/ui/icons"
11 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
12 | import { cn } from "@/lib/utils"
13 |
14 | export function SettingsDropDown() {
15 | const { setTheme } = useTheme()
16 | const [_, startTransition] = useTransition()
17 |
18 | return (
19 |
20 |
21 |
27 |
28 |
29 | {
31 | startTransition(() => {
32 | setTheme("light")
33 | })
34 | }}
35 | >
36 | Light
37 |
38 | {
40 | startTransition(() => {
41 | setTheme("dark")
42 | })
43 | }}
44 | >
45 | Dark
46 |
47 | {
49 | startTransition(() => {
50 | setTheme("system")
51 | })
52 | }}
53 | >
54 | System
55 |
56 |
57 |
58 |
59 |
65 |
66 | Twitter
67 |
68 |
69 | Twitter
70 |
71 |
72 |
73 |
79 |
80 | Telegram
81 |
82 |
83 | Telegram
84 |
85 |
86 |
87 |
93 |
94 | Github
95 |
96 |
97 | Github
98 |
99 |
100 |
101 |
102 | )
103 | }
104 |
--------------------------------------------------------------------------------
/components/header/user-menu.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 |
3 | import type { Session } from "next-auth"
4 |
5 | import { SignOutButton } from "@/components/sign-out-button"
6 | import { Button } from "@/components/ui/button"
7 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
8 |
9 | function getUserInitials(name: string) {
10 | const [firstName, lastName] = name.split(" ")
11 |
12 | return lastName ? `${firstName[0]}${lastName[0]}` : firstName.slice(0, 2)
13 | }
14 |
15 | export const UserMenu = ({ user }: { user: Session["user"] }) => {
16 | return (
17 |
18 |
19 |
20 |
36 |
37 |
38 |
39 | {user?.name}
40 | {user?.email}
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | )
49 | }
50 |
--------------------------------------------------------------------------------
/components/markdown.tsx:
--------------------------------------------------------------------------------
1 | import { type FC, memo } from "react"
2 | import ReactMarkdown, { type Options } from "react-markdown"
3 |
4 | export const MemoizedReactMarkdown: FC = memo(
5 | ReactMarkdown,
6 | (prevProps, nextProps) => prevProps.children === nextProps.children && prevProps.className === nextProps.className,
7 | )
8 |
--------------------------------------------------------------------------------
/components/metis-teaser.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { metisSepolia } from "@/lib/config"
5 | import Link from "next/link"
6 | import { useAccount } from "wagmi"
7 |
8 | export const MetisTeaser = () => {
9 | const { chain } = useAccount()
10 |
11 | if (chain?.id !== metisSepolia.id) {
12 | return null
13 | }
14 |
15 | return (
16 |
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/providers/ui-providers.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { ThemeProvider as NextThemesProvider } from "next-themes"
4 | import type { ThemeProviderProps } from "next-themes/dist/types"
5 |
6 | import { TooltipProvider } from "@/components/ui/tooltip"
7 |
8 | export function Providers({ children, ...props }: ThemeProviderProps) {
9 | return (
10 |
11 | {children}
12 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/components/providers/web3-provider.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useState } from "react"
4 |
5 | import { RainbowKitProvider, darkTheme, lightTheme } from "@rainbow-me/rainbowkit"
6 | import "@rainbow-me/rainbowkit/styles.css"
7 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
8 | import { useTheme } from "next-themes"
9 | import { type State, WagmiProvider } from "wagmi"
10 |
11 | import { getWagmiConfig } from "@/lib/config"
12 | import { connectors } from "@/lib/rainbowkit"
13 |
14 | export function Web3Provider({
15 | children,
16 | initialState,
17 | }: {
18 | children: React.ReactNode
19 | initialState: State | undefined
20 | }) {
21 | const { resolvedTheme } = useTheme()
22 | const [config] = useState(() => getWagmiConfig(connectors))
23 | const [queryClient] = useState(() => new QueryClient())
24 |
25 | return (
26 |
27 |
28 | (
33 |
34 | Web3GPT is an experimental AI tool. Beware of the{" "}
35 | risks{" "}
36 | associated with deploying smart contracts.
37 |
38 | ),
39 | learnMoreUrl: "https://x.com/w3gptai",
40 | }}
41 | theme={
42 | resolvedTheme === "dark"
43 | ? darkTheme({
44 | accentColor: "#21C55E",
45 | accentColorForeground: "black",
46 | })
47 | : lightTheme({
48 | accentColor: "#21C55E",
49 | accentColorForeground: "white",
50 | })
51 | }
52 | >
53 | {children}
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-agents.tsx:
--------------------------------------------------------------------------------
1 | import Image from "next/image"
2 | import Link from "next/link"
3 |
4 | import { Button } from "@/components/ui/button"
5 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
6 | import { AGENTS_ARRAY } from "@/lib/constants"
7 | import { cn } from "@/lib/utils"
8 |
9 | export const SidebarAgents = () => {
10 | return (
11 |
12 | {AGENTS_ARRAY.map((assistant) => (
13 |
14 |
15 |
35 |
36 | {assistant.description}
37 |
38 | ))}
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-footer.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from "@/lib/utils"
2 |
3 | export function SidebarFooter({ children, className, ...props }: React.ComponentProps<"div">) {
4 | return (
5 |
6 | {children}
7 |
8 | )
9 | }
10 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-item.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import Link from "next/link"
4 |
5 | import { buttonVariants } from "@/components/ui/button"
6 | import { IconMessage, IconUsers } from "@/components/ui/icons"
7 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"
8 | import type { DbChatListItem } from "@/lib/types"
9 | import { cn } from "@/lib/utils"
10 |
11 | interface SidebarItemProps {
12 | chat: DbChatListItem
13 | isActive: boolean
14 | children: React.ReactNode
15 | }
16 |
17 | export function SidebarItem({ chat, isActive, children }: SidebarItemProps) {
18 | if (!chat?.id) return null
19 |
20 | return (
21 |
22 |
23 | {chat.published ? (
24 |
25 |
26 |
27 |
28 | This is a published chat.
29 |
30 | ) : (
31 |
32 | )}
33 |
34 |
38 |
39 | {chat.title}
40 |
41 |
42 |
{children}
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar-list.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import { usePathname } from "next/navigation"
3 |
4 | import { SidebarActions } from "@/components/sidebar/sidebar-actions"
5 | import { SidebarItem } from "@/components/sidebar/sidebar-item"
6 | import type { DbChatListItem } from "@/lib/types"
7 |
8 | export interface SidebarListProps {
9 | chatList?: DbChatListItem[] | null
10 | }
11 |
12 | export function SidebarList({ chatList }: SidebarListProps) {
13 | const pathname = usePathname()
14 | const activeChatId = pathname.split("/chat/")[1]
15 |
16 | return (
17 |
18 | {chatList?.length ? (
19 |
20 | {chatList?.map((chat) => (
21 |
22 |
23 |
24 | ))}
25 |
26 | ) : (
27 |
28 |
No chat history found
29 |
30 | )}
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/sidebar/sidebar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { Button } from "@/components/ui/button"
4 | import { IconSidebar } from "@/components/ui/icons"
5 | import { Sheet, SheetContent, SheetTrigger } from "@/components/ui/sheet"
6 |
7 | export type SidebarProps = {
8 | children?: React.ReactNode
9 | }
10 |
11 | export const Sidebar = ({ children }: SidebarProps) => {
12 | return (
13 |
14 |
15 |
19 |
20 | {children}
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/components/sign-out-button.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { signOut } from "next-auth/react"
4 |
5 | export const SignOutButton = () => {
6 | return (
7 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
4 | import * as React from "react"
5 |
6 | import { buttonVariants } from "@/components/ui/button"
7 | import { cn } from "@/lib/utils"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = ({ className, children, ...props }: AlertDialogPrimitive.AlertDialogPortalProps) => (
14 |
15 | {children}
16 |
17 | )
18 | AlertDialogPortal.displayName = AlertDialogPrimitive.Portal.displayName
19 |
20 | const AlertDialogOverlay = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, children, ...props }, ref) => (
24 |
32 | ))
33 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
34 |
35 | const AlertDialogContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
40 |
41 |
49 |
50 | ))
51 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
52 |
53 | const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
54 |
55 | )
56 | AlertDialogHeader.displayName = "AlertDialogHeader"
57 |
58 | const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
59 |
60 | )
61 | AlertDialogFooter.displayName = "AlertDialogFooter"
62 |
63 | const AlertDialogTitle = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, ...props }, ref) => (
67 |
68 | ))
69 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
70 |
71 | const AlertDialogDescription = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
76 | ))
77 | AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName
78 |
79 | const AlertDialogAction = React.forwardRef<
80 | React.ElementRef,
81 | React.ComponentPropsWithoutRef
82 | >(({ className, ...props }, ref) => (
83 |
84 | ))
85 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
86 |
87 | const AlertDialogCancel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => (
91 |
96 | ))
97 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
98 |
99 | export {
100 | AlertDialog,
101 | AlertDialogTrigger,
102 | AlertDialogContent,
103 | AlertDialogHeader,
104 | AlertDialogFooter,
105 | AlertDialogTitle,
106 | AlertDialogDescription,
107 | AlertDialogAction,
108 | AlertDialogCancel,
109 | }
110 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import { type VariantProps, cva } from "class-variance-authority"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const badgeVariants = cva(
6 | "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",
7 | {
8 | variants: {
9 | variant: {
10 | default: "border-transparent bg-primary text-primary-foreground",
11 | secondary: "border-transparent bg-secondary text-secondary-foreground",
12 | destructive: "border-transparent bg-destructive text-destructive-foreground",
13 | outline: "text-foreground",
14 | },
15 | },
16 | defaultVariants: {
17 | variant: "default",
18 | },
19 | },
20 | )
21 |
22 | export interface BadgeProps extends React.HTMLAttributes, VariantProps {}
23 |
24 | function Badge({ className, variant, ...props }: BadgeProps) {
25 | return
26 | }
27 |
28 | export { Badge, badgeVariants }
29 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import { Slot } from "@radix-ui/react-slot"
2 | import { type VariantProps, cva } from "class-variance-authority"
3 | import * as React from "react"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center rounded-md text-sm font-medium shadow 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 shadow-md hover:bg-primary/80",
13 | destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90",
14 | outline: "border border-input hover:bg-accent hover:text-accent-foreground",
15 | secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80",
16 | ghost: "shadow-none hover:bg-accent hover:text-accent-foreground",
17 | link: "text-primary underline-offset-4 shadow-none hover:underline",
18 | },
19 | size: {
20 | default: "h-8 px-4 py-2",
21 | sm: "h-8 rounded-md px-3",
22 | lg: "h-11 rounded-md px-8",
23 | icon: "size-8 p-0",
24 | },
25 | },
26 | defaultVariants: {
27 | variant: "default",
28 | size: "default",
29 | },
30 | },
31 | )
32 |
33 | export interface ButtonProps
34 | extends React.ButtonHTMLAttributes,
35 | VariantProps {
36 | asChild?: boolean
37 | }
38 |
39 | const Button = React.forwardRef(
40 | ({ className, variant, size, asChild = false, ...props }, ref) => {
41 | const Comp = asChild ? Slot : "button"
42 | return
43 | },
44 | )
45 | Button.displayName = "Button"
46 |
47 | export { Button, buttonVariants }
48 |
--------------------------------------------------------------------------------
/components/ui/code-block.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { memo, useCallback, useMemo } from "react"
4 |
5 | import { generateId } from "ai"
6 | import { useTheme } from "next-themes"
7 | import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
8 | import { coldarkCold, coldarkDark } from "react-syntax-highlighter/dist/cjs/styles/prism"
9 |
10 | import { DeployContractButton } from "@/components/deploy-contract-button"
11 | import { DeployTokenScriptButton } from "@/components/deploy-tokenscript-button"
12 | import { Button } from "@/components/ui/button"
13 | import { IconCheck, IconCopy, IconDownload } from "@/components/ui/icons"
14 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard"
15 | import { useIsClient } from "@/lib/hooks/use-is-client"
16 | import { cn } from "@/lib/utils"
17 |
18 | const PROGRAMMING_LANGUAGES: Record = {
19 | javascript: ".js",
20 | python: ".py",
21 | java: ".java",
22 | c: ".c",
23 | cpp: ".cpp",
24 | "c++": ".cpp",
25 | "c#": ".cs",
26 | ruby: ".rb",
27 | php: ".php",
28 | swift: ".swift",
29 | "objective-c": ".m",
30 | kotlin: ".kt",
31 | typescript: ".ts",
32 | go: ".go",
33 | perl: ".pl",
34 | rust: ".rs",
35 | scala: ".scala",
36 | haskell: ".hs",
37 | lua: ".lua",
38 | shell: ".sh",
39 | sql: ".sql",
40 | html: ".html",
41 | css: ".css",
42 | solidity: ".sol",
43 | clarity: ".clar",
44 | tokenscript: ".xml",
45 | tokenscripttsml: ".tsml",
46 | }
47 |
48 | type CodeBlockProps = {
49 | language: string
50 | value: string
51 | }
52 |
53 | export const CodeBlock = memo(({ language, value }: CodeBlockProps) => {
54 | const isClient = useIsClient()
55 | const { resolvedTheme } = useTheme()
56 | const { isCopied, copyToClipboard } = useCopyToClipboard({ timeout: 2000 })
57 |
58 | const isDarkMode = !isClient ? true : resolvedTheme === "dark"
59 |
60 | const memoizedHighlighter = useMemo(() => {
61 | return (
62 |
79 | {value}
80 |
81 | )
82 | }, [value, language, isDarkMode])
83 |
84 | const downloadAsFile = useCallback(() => {
85 | if (typeof window === "undefined") {
86 | return
87 | }
88 | const fileExtension = PROGRAMMING_LANGUAGES[language] || ".file"
89 | const suggestedFileName = `web3gpt-${generateId(6)}${fileExtension}`
90 | const fileName = window.prompt("Enter file name", suggestedFileName)
91 |
92 | if (!fileName) return
93 |
94 | const blob = new Blob([value], { type: "text/plain" })
95 | const url = URL.createObjectURL(blob)
96 | const link = document.createElement("a")
97 | link.download = fileName
98 | link.href = url
99 | link.style.display = "none"
100 | document.body.appendChild(link)
101 | link.click()
102 | document.body.removeChild(link)
103 | URL.revokeObjectURL(url)
104 | }, [value, language])
105 |
106 | const renderDeployButton = useCallback(() => {
107 | if (language === "solidity") {
108 | return value} />
109 | }
110 | if (["tokenscript", "tokenscripttsml", "xml"].includes(language)) {
111 | return value} />
112 | }
113 | return null
114 | }, [language, value])
115 |
116 | return (
117 |
118 |
124 |
{language}
125 |
126 | {renderDeployButton()}
127 |
136 |
145 |
146 |
147 | {memoizedHighlighter}
148 |
149 | )
150 | })
151 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as DialogPrimitive from "@radix-ui/react-dialog"
4 | import * as React from "react"
5 |
6 | import { IconClose } from "@/components/ui/icons"
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = ({ className, children, ...props }: DialogPrimitive.DialogPortalProps) => (
14 |
15 | {children}
16 |
17 | )
18 | DialogPortal.displayName = DialogPrimitive.Portal.displayName
19 |
20 | const DialogOverlay = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
32 | ))
33 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
34 |
35 | const DialogContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, ...props }, ref) => (
39 |
40 |
41 |
49 | {children}
50 |
51 |
52 | Close
53 |
54 |
55 |
56 | ))
57 | DialogContent.displayName = DialogPrimitive.Content.displayName
58 |
59 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
60 |
61 | )
62 | DialogHeader.displayName = "DialogHeader"
63 |
64 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
65 |
66 | )
67 | DialogFooter.displayName = "DialogFooter"
68 |
69 | const DialogTitle = React.forwardRef<
70 | React.ElementRef,
71 | React.ComponentPropsWithoutRef
72 | >(({ className, ...props }, ref) => (
73 |
78 | ))
79 | DialogTitle.displayName = DialogPrimitive.Title.displayName
80 |
81 | const DialogDescription = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, ...props }, ref) => (
85 |
86 | ))
87 | DialogDescription.displayName = DialogPrimitive.Description.displayName
88 |
89 | export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }
90 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const DropdownMenu = DropdownMenuPrimitive.Root
9 |
10 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
11 |
12 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
13 |
14 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
15 |
16 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
17 |
18 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
19 |
20 | const DropdownMenuRadioItem = React.forwardRef<
21 | React.ElementRef,
22 | React.ComponentPropsWithoutRef
23 | >(({ className, ...props }, ref) => (
24 |
32 | ))
33 |
34 | const DropdownMenuSubContent = React.forwardRef<
35 | React.ElementRef,
36 | React.ComponentPropsWithoutRef
37 | >(({ className, ...props }, ref) => (
38 |
46 | ))
47 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
48 |
49 | const DropdownMenuContent = React.forwardRef<
50 | React.ElementRef,
51 | React.ComponentPropsWithoutRef
52 | >(({ className, sideOffset = 4, ...props }, ref) => (
53 |
54 |
63 |
64 | ))
65 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
66 |
67 | const DropdownMenuItem = React.forwardRef<
68 | React.ElementRef,
69 | React.ComponentPropsWithoutRef & {
70 | inset?: boolean
71 | }
72 | >(({ className, inset, ...props }, ref) => (
73 |
82 | ))
83 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
84 |
85 | const DropdownMenuLabel = React.forwardRef<
86 | React.ElementRef,
87 | React.ComponentPropsWithoutRef & {
88 | inset?: boolean
89 | }
90 | >(({ className, inset, ...props }, ref) => (
91 |
96 | ))
97 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
98 |
99 | const DropdownMenuSeparator = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
104 | ))
105 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
106 |
107 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
108 | return
109 | }
110 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
111 |
112 | export {
113 | DropdownMenu,
114 | DropdownMenuTrigger,
115 | DropdownMenuContent,
116 | DropdownMenuItem,
117 | DropdownMenuLabel,
118 | DropdownMenuSeparator,
119 | DropdownMenuShortcut,
120 | DropdownMenuGroup,
121 | DropdownMenuPortal,
122 | DropdownMenuSub,
123 | DropdownMenuSubContent,
124 | DropdownMenuRadioGroup,
125 | DropdownMenuRadioItem,
126 | }
127 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps extends React.InputHTMLAttributes {}
6 |
7 | const Input = React.forwardRef(({ className, type, ...props }, ref) => {
8 | return (
9 |
18 | )
19 | })
20 | Input.displayName = "Input"
21 |
22 | export { Input }
23 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as LabelPrimitive from "@radix-ui/react-label"
4 | import { type VariantProps, cva } from "class-variance-authority"
5 | import * as React from "react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70")
10 |
11 | const Label = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef & VariantProps
14 | >(({ className, ...props }, ref) => (
15 |
16 | ))
17 | Label.displayName = LabelPrimitive.Root.displayName
18 |
19 | export { Label }
20 |
--------------------------------------------------------------------------------
/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SelectPrimitive from "@radix-ui/react-select"
4 | import * as React from "react"
5 |
6 | import { IconCheck, IconChevronUpDown } from "@/components/ui/icons"
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 |
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectContent = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, children, position = "popper", ...props }, ref) => (
39 |
40 |
50 |
57 | {children}
58 |
59 |
60 |
61 | ))
62 | SelectContent.displayName = SelectPrimitive.Content.displayName
63 |
64 | const SelectLabel = React.forwardRef<
65 | React.ElementRef,
66 | React.ComponentPropsWithoutRef
67 | >(({ className, ...props }, ref) => (
68 |
69 | ))
70 | SelectLabel.displayName = SelectPrimitive.Label.displayName
71 |
72 | const SelectItem = React.forwardRef<
73 | React.ElementRef,
74 | React.ComponentPropsWithoutRef
75 | >(({ className, children, ...props }, ref) => (
76 |
84 |
85 |
86 |
87 |
88 |
89 | {children}
90 |
91 | ))
92 | SelectItem.displayName = SelectPrimitive.Item.displayName
93 |
94 | const SelectSeparator = React.forwardRef<
95 | React.ElementRef,
96 | React.ComponentPropsWithoutRef
97 | >(({ className, ...props }, ref) => (
98 |
99 | ))
100 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
101 |
102 | export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator }
103 |
--------------------------------------------------------------------------------
/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SeparatorPrimitive from "@radix-ui/react-separator"
4 | import * as React from "react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, orientation = "horizontal", decorative = true, ...props }, ref) => (
12 |
19 | ))
20 | Separator.displayName = SeparatorPrimitive.Root.displayName
21 |
22 | export { Separator }
23 |
--------------------------------------------------------------------------------
/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as SheetPrimitive from "@radix-ui/react-dialog"
4 | import * as React from "react"
5 |
6 | import { IconClose } from "@/components/ui/icons"
7 | import { cn } from "@/lib/utils"
8 |
9 | const Sheet = SheetPrimitive.Root
10 |
11 | const SheetTrigger = SheetPrimitive.Trigger
12 |
13 | const SheetClose = SheetPrimitive.Close
14 |
15 | const SheetPortal = ({ className, children, ...props }: SheetPrimitive.DialogPortalProps) => (
16 |
17 | {children}
18 |
19 | )
20 | SheetPortal.displayName = SheetPrimitive.Portal.displayName
21 |
22 | const SheetOverlay = React.forwardRef<
23 | React.ElementRef,
24 | React.ComponentPropsWithoutRef
25 | >(({ className, children, ...props }, ref) => (
26 |
34 | ))
35 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
36 |
37 | const SheetContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 | {
45 | e.preventDefault()
46 | }}
47 | className={cn(
48 | "fixed z-50 h-full border-r bg-background p-6 opacity-100 shadow-lg data-[state=closed]:animate-slide-to-left data-[state=open]:animate-slide-from-left",
49 | className,
50 | )}
51 | {...props}
52 | >
53 | {children}
54 |
55 |
56 | Close
57 |
58 |
59 |
60 | ))
61 | SheetContent.displayName = SheetPrimitive.Content.displayName
62 |
63 | const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => (
64 |
65 | )
66 | SheetHeader.displayName = "SheetHeader"
67 |
68 | const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => (
69 |
70 | )
71 | SheetFooter.displayName = "SheetFooter"
72 |
73 | const SheetTitle = React.forwardRef<
74 | React.ElementRef,
75 | React.ComponentPropsWithoutRef
76 | >(({ className, ...props }, ref) => (
77 |
78 | ))
79 | SheetTitle.displayName = SheetPrimitive.Title.displayName
80 |
81 | const SheetDescription = React.forwardRef<
82 | React.ElementRef,
83 | React.ComponentPropsWithoutRef
84 | >(({ className, ...props }, ref) => (
85 |
86 | ))
87 | SheetDescription.displayName = SheetPrimitive.Description.displayName
88 |
89 | export { Sheet, SheetTrigger, SheetClose, SheetContent, SheetHeader, SheetFooter, SheetTitle, SheetDescription }
90 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { resolvedTheme } = useTheme()
10 |
11 | return (
12 |
26 | )
27 | }
28 |
29 | export { Toaster }
30 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps extends React.TextareaHTMLAttributes {}
6 |
7 | const Textarea = React.forwardRef(({ className, ...props }, ref) => {
8 | return (
9 |
17 | )
18 | })
19 | Textarea.displayName = "Textarea"
20 |
21 | export { Textarea }
22 |
--------------------------------------------------------------------------------
/components/ui/tooltip.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { forwardRef } from "react"
4 |
5 | import * as TooltipPrimitive from "@radix-ui/react-tooltip"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const TooltipProvider = TooltipPrimitive.Provider
10 |
11 | const Tooltip = TooltipPrimitive.Root
12 |
13 | const TooltipTrigger = TooltipPrimitive.Trigger
14 |
15 | const TooltipContent = forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, sideOffset = 4, ...props }, ref) => (
19 |
28 | ))
29 | TooltipContent.displayName = TooltipPrimitive.Content.displayName
30 |
31 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
32 |
--------------------------------------------------------------------------------
/env.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod"
2 |
3 | const envSchema = z.object({
4 | NEXT_PUBLIC_ALCHEMY_API_KEY: z.string(),
5 | NEXT_PUBLIC_QUICKNODE_API_KEY: z.string(),
6 | NEXT_PUBLIC_INFURA_API_KEY: z.string(),
7 | NEXT_PUBLIC_IPFS_GATEWAY: z.string(),
8 | NEXT_PUBLIC_BLOCKSCOUT_API_KEY: z.string(),
9 | NEXT_PUBLIC_ETHERSCAN_API_KEY: z.string(),
10 | NEXT_PUBLIC_POLYGONSCAN_API_KEY: z.string(),
11 | NEXT_PUBLIC_BASESCAN_API_KEY: z.string(),
12 | NEXT_PUBLIC_ARBISCAN_API_KEY: z.string(),
13 | NEXT_PUBLIC_OPSCAN_API_KEY: z.string(),
14 | NEXT_PUBLIC_MANTLESCAN_API_KEY: z.string(),
15 | NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID: z.string(),
16 | CRON_SECRET: z.string().min(1),
17 | PINATA_JWT: z.string().min(1),
18 | AUTH_SECRET: z.string().min(1),
19 | DEPLOYER_PRIVATE_KEY: z.string().min(1),
20 | UNKEY_COMPLETIONS_API_ID: z.string().min(1),
21 | })
22 |
23 | envSchema.parse(process.env)
24 |
25 | declare global {
26 | namespace NodeJS {
27 | interface ProcessEnv extends z.infer {}
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/actions/ai.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import type { Message } from "ai"
4 |
5 | import { storeAgent } from "@/lib/actions/db"
6 | import { openai } from "@/lib/openai"
7 | import { TOOL_SCHEMAS, ToolName } from "@/lib/tools"
8 | import type { CreateAgentParams } from "@/lib/types"
9 |
10 | export const createAgent = async ({
11 | name,
12 | userId,
13 | description,
14 | instructions,
15 | creator,
16 | imageUrl
17 | }: CreateAgentParams) => {
18 | try {
19 | const { id } = await openai.beta.assistants.create({
20 | name: name,
21 | model: "gpt-4o-mini",
22 | description: description,
23 | instructions: instructions,
24 | tools: [
25 | TOOL_SCHEMAS[ToolName.DeployContract],
26 | TOOL_SCHEMAS[ToolName.ResolveAddress],
27 | TOOL_SCHEMAS[ToolName.ResolveDomain]
28 | ]
29 | })
30 |
31 | if (!userId) {
32 | throw new Error("Unauthorized")
33 | }
34 |
35 | const res = await storeAgent({
36 | id,
37 | userId,
38 | name,
39 | description,
40 | creator,
41 | imageUrl
42 | })
43 |
44 | if (res?.error) {
45 | return null
46 | }
47 |
48 | return id
49 | } catch (error) {
50 | console.error("Error in createAgent", error)
51 | return null
52 | }
53 | }
54 |
55 | export const getAiThreadMessages = async (threadId: string) => {
56 | const fullMessages = (await openai.beta.threads.messages.list(threadId, { order: "asc" }))?.data
57 |
58 | return fullMessages?.map((message) => {
59 | const { id, content, role, created_at: createdAt } = message
60 | const textContent = content.find((c) => c.type === "text")
61 | const text = textContent?.type === "text" ? textContent.text.value : ""
62 |
63 | return {
64 | id,
65 | content: text,
66 | role,
67 | createdAt: new Date(createdAt * 1000)
68 | } satisfies Message
69 | })
70 | }
71 |
--------------------------------------------------------------------------------
/lib/actions/chat.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { revalidateTag } from "next/cache"
4 |
5 | import { auth } from "@/auth"
6 | import { withUser } from "@/lib/data/kv"
7 | import type { DbChatListItem } from "@/lib/types"
8 | import { kv } from "@vercel/kv"
9 |
10 | export const shareChatAction = withUser(async (chat, userId) => {
11 | if (userId !== String(chat.userId)) {
12 | return
13 | }
14 |
15 | const payload = {
16 | ...chat,
17 | published: true,
18 | }
19 |
20 | await kv.hmset(`chat:${chat.id}`, payload)
21 |
22 | return revalidateTag("chat-list")
23 | })
24 | export const deleteChatAction = withUser(async (id, userId) => {
25 | await Promise.all([kv.del(`chat:${id}`), kv.zrem(`user:chat:${userId}`, `chat:${id}`)])
26 |
27 | return revalidateTag("chat-list")
28 | })
29 |
30 | export async function storeEmailAction(email: string) {
31 | await kv.sadd("emails:list", email)
32 |
33 | const session = await auth()
34 | const userId = session?.user.id
35 |
36 | if (userId) {
37 | await kv.hmset(`user:details:${userId}`, {
38 | email_subscribed: true,
39 | })
40 | }
41 | }
42 |
43 | export const getUserFieldAction = withUser(async (fieldName, userId) => {
44 | const userKey = `user:details:${userId}`
45 | const userDetails = await kv.hgetall<{ [key: string]: string }>(userKey)
46 | return userDetails?.[fieldName] || null
47 | })
48 |
49 | export const clearChatsAction = withUser(async (_, userId) => {
50 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1)
51 | if (!chats.length) {
52 | return
53 | }
54 | const pipeline = kv.pipeline()
55 |
56 | for (const chat of chats) {
57 | pipeline.del(chat)
58 | pipeline.zrem(`user:chat:${userId}`, chat)
59 | }
60 |
61 | await pipeline.exec()
62 |
63 | return revalidateTag("chat-list")
64 | })
65 |
--------------------------------------------------------------------------------
/lib/actions/deploy-contract.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { ipfsUploadDir, ipfsUploadFile } from "@/lib/data/ipfs"
4 | import { withUser } from "@/lib/data/kv"
5 | import { getContractFileName, prepareContractSources } from "@/lib/solidity/utils"
6 | import { ensureHashPrefix } from "@/lib/utils"
7 | import { kv } from "@vercel/kv"
8 | import solc, { type SolcInput, type SolcOutput } from "solc"
9 | import type { Abi } from "viem"
10 |
11 | export async function compileContract({ contractName, sourceCode }: { contractName: string; sourceCode: string }) {
12 | const sources = await prepareContractSources(contractName, sourceCode)
13 | const standardJsonInputString = JSON.stringify({
14 | language: "Solidity",
15 | sources,
16 | settings: {
17 | outputSelection: {
18 | "*": {
19 | "*": ["*"],
20 | },
21 | },
22 | optimizer: {
23 | enabled: true,
24 | runs: 200,
25 | },
26 | },
27 | } satisfies SolcInput)
28 |
29 | const fileName = getContractFileName(contractName)
30 |
31 | const compileOutput: SolcOutput = JSON.parse(solc.compile(standardJsonInputString))
32 |
33 | if (compileOutput.errors) {
34 | const errors = compileOutput.errors.filter((error) => error.severity === "error")
35 | if (errors.length > 0) {
36 | throw new Error(errors[0].formattedMessage)
37 | }
38 | }
39 |
40 | const contract = compileOutput.contracts[fileName][contractName]
41 | const abi = contract.abi
42 | const bytecode = ensureHashPrefix(contract.evm.bytecode.object)
43 |
44 | return {
45 | abi,
46 | bytecode,
47 | standardJsonInput: standardJsonInputString,
48 | sources,
49 | }
50 | }
51 |
52 | export const storeTokenScriptDeploymentAction = withUser<
53 | {
54 | chainId: string
55 | deployHash: string
56 | cid: string
57 | tokenAddress: string
58 | },
59 | void
60 | >(async (data, userId) => {
61 | await kv.hmset(`tokenscript:${data.cid}`, data)
62 |
63 | await kv.zadd(`user:tokenscripts:${userId}`, {
64 | score: Date.now(),
65 | member: `tokenscript:${data.cid}`,
66 | })
67 | })
68 |
69 | export async function ipfsUploadFileAction(fileName: string, fileContent: string | Buffer): Promise {
70 | return await ipfsUploadFile(fileName, fileContent)
71 | }
72 |
73 | export async function ipfsUploadDirAction(
74 | sources: SolcOutput["sources"],
75 | abi: Abi,
76 | bytecode: string,
77 | standardJsonInput: string,
78 | ) {
79 | return await ipfsUploadDir(sources, abi, bytecode, standardJsonInput)
80 | }
81 |
--------------------------------------------------------------------------------
/lib/actions/ipfs.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { type FileObject, PinataSDK } from "pinata"
4 | import type { SolcOutput } from "solc"
5 | import type { Abi } from "viem"
6 |
7 | import { IPFS_GATEWAY } from "@/lib/config"
8 | import { PINATA_JWT } from "@/lib/config-server"
9 |
10 | const pinata = new PinataSDK({
11 | pinataJwt: PINATA_JWT,
12 | pinataGateway: IPFS_GATEWAY
13 | })
14 |
15 | export async function ipfsUploadDir(
16 | sources: SolcOutput["sources"],
17 | abi: Abi,
18 | bytecode: string,
19 | standardJsonInput: string
20 | ): Promise {
21 | try {
22 | const files: FileObject[] = []
23 |
24 | for (const [fileName, { content }] of Object.entries(sources)) {
25 | files.push(new File([content], fileName))
26 | }
27 | files.push(new File([JSON.stringify(abi, null, 2)], "abi.json"))
28 | files.push(new File([bytecode], "bytecode.txt"))
29 | files.push(new File([standardJsonInput], "standardJsonInput.json"))
30 |
31 | const { IpfsHash } = await pinata.upload.fileArray(files, {
32 | cidVersion: 1,
33 | metadata: {
34 | name: "contract"
35 | }
36 | })
37 |
38 | return IpfsHash
39 | } catch (error) {
40 | console.error("Error writing to temporary file:", error)
41 | return null
42 | }
43 | }
44 |
45 | export async function ipfsUploadFile(fileName: string, fileContent: string | Buffer): Promise {
46 | try {
47 | const file = new File([fileContent], fileName)
48 | const { IpfsHash } = await pinata.upload.file(file, {
49 | cidVersion: 1,
50 | metadata: {
51 | name: fileName
52 | }
53 | })
54 |
55 | return IpfsHash
56 | } catch (error) {
57 | console.error("Error writing to temporary file:", error)
58 | return null
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/lib/actions/solidity/compile-contract.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import solc, { type SolcInput, type SolcOutput } from "solc"
4 |
5 | import { getContractFileName, prepareContractSources } from "@/lib/contracts/contract-utils"
6 | import { ensureHashPrefix } from "@/lib/utils"
7 |
8 | export async function compileContract({
9 | contractName,
10 | sourceCode
11 | }: {
12 | contractName: string
13 | sourceCode: string
14 | }) {
15 | const sources = await prepareContractSources(contractName, sourceCode)
16 | const standardJsonInputString = JSON.stringify({
17 | language: "Solidity",
18 | sources,
19 | settings: {
20 | outputSelection: {
21 | "*": {
22 | "*": ["*"]
23 | }
24 | },
25 | optimizer: {
26 | enabled: true,
27 | runs: 200
28 | }
29 | }
30 | } satisfies SolcInput)
31 |
32 | const fileName = getContractFileName(contractName)
33 |
34 | const compileOutput: SolcOutput = JSON.parse(solc.compile(standardJsonInputString))
35 |
36 | if (compileOutput.errors) {
37 | const errors = compileOutput.errors.filter((error) => error.severity === "error")
38 | if (errors.length > 0) {
39 | throw new Error(errors[0].formattedMessage)
40 | }
41 | }
42 |
43 | const contract = compileOutput.contracts[fileName][contractName]
44 | const abi = contract.abi
45 | const bytecode = ensureHashPrefix(contract.evm.bytecode.object)
46 |
47 | return {
48 | abi,
49 | bytecode,
50 | standardJsonInput: standardJsonInputString,
51 | sources
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/lib/actions/solidity/deploy-contract.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { track } from "@vercel/analytics/server"
4 | import { http, type Chain, createWalletClient, encodeDeployData, getCreateAddress, createPublicClient } from "viem"
5 |
6 | import { storeDeployment, storeVerification } from "@/lib/actions/db"
7 | import { ipfsUploadDir } from "@/lib/actions/ipfs"
8 | import { compileContract } from "@/lib/actions/solidity/compile-contract"
9 | import { getContractFileName, getExplorerUrl, getIpfsUrl } from "@/lib/contracts/contract-utils"
10 | import type { DeployContractParams, DeployContractResult, VerifyContractParams } from "@/lib/types"
11 | import { getChainById } from "@/lib/viem"
12 | import { DEPLOYER_ACCOUNT } from "@/lib/config-server"
13 |
14 | export const deployContract = async ({
15 | chainId,
16 | contractName,
17 | sourceCode,
18 | constructorArgs
19 | }: DeployContractParams): Promise => {
20 | const viemChain = getChainById(Number(chainId)) as Chain
21 |
22 | const { abi, bytecode, standardJsonInput, sources } = await compileContract({ contractName, sourceCode })
23 |
24 | const alchemyHttpUrl = viemChain?.rpcUrls?.alchemy?.http[0]
25 | ? `${viemChain.rpcUrls.alchemy.http[0]}/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`
26 | : undefined
27 |
28 | const walletClient = createWalletClient({
29 | account: DEPLOYER_ACCOUNT,
30 | chain: viemChain,
31 | transport: http(alchemyHttpUrl)
32 | })
33 |
34 | const publicClient = createPublicClient({
35 | chain: viemChain,
36 | transport: http(alchemyHttpUrl)
37 | })
38 |
39 | if (!(await walletClient.getAddresses())) {
40 | const error = new Error(`Wallet for chain ${viemChain.name} not available`)
41 | console.error(error)
42 | throw error
43 | }
44 |
45 | const deployerAddress = DEPLOYER_ACCOUNT.address
46 | const nonce = await publicClient.getTransactionCount({ address: deployerAddress })
47 |
48 | const contractAddress = getCreateAddress({
49 | from: deployerAddress,
50 | nonce: BigInt(nonce)
51 | })
52 |
53 | const deployData = encodeDeployData({
54 | abi,
55 | bytecode,
56 | args: constructorArgs
57 | })
58 |
59 | const deployHash = await walletClient.deployContract({
60 | abi,
61 | bytecode,
62 | account: DEPLOYER_ACCOUNT,
63 | args: constructorArgs
64 | })
65 |
66 | const explorerUrl = getExplorerUrl({
67 | viemChain,
68 | hash: contractAddress,
69 | type: "address"
70 | })
71 |
72 | const cid = await ipfsUploadDir(sources, abi, bytecode, standardJsonInput)
73 | if (!cid) {
74 | throw new Error("Error uploading to IPFS")
75 | }
76 |
77 | const ipfsUrl = getIpfsUrl(cid)
78 |
79 | const encodedConstructorArgs = deployData.slice(bytecode?.length) as `0x${string}`
80 | const fileName = getContractFileName(contractName)
81 |
82 | const verifyContractConfig: VerifyContractParams = {
83 | deployHash,
84 | contractAddress,
85 | standardJsonInput,
86 | encodedConstructorArgs,
87 | fileName,
88 | contractName,
89 | viemChain
90 | }
91 |
92 | const deploymentData: DeployContractResult = {
93 | contractAddress,
94 | sourceCode,
95 | explorerUrl,
96 | ipfsUrl,
97 | verifyContractConfig,
98 | abi,
99 | standardJsonInput
100 | }
101 |
102 | await Promise.all([
103 | storeDeployment({
104 | chainId,
105 | deployHash,
106 | contractAddress,
107 | cid
108 | }),
109 | storeVerification(verifyContractConfig),
110 | track("deployed_contract", {
111 | contractName,
112 | explorerUrl,
113 | contractAddress
114 | })
115 | ])
116 |
117 | return deploymentData
118 | }
119 |
--------------------------------------------------------------------------------
/lib/actions/solidity/deploy-tokenscript.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { track } from "@vercel/analytics/server"
4 | import { http, type Chain, createPublicClient, createWalletClient, encodeFunctionData } from "viem"
5 | import { parseAbiItem } from "viem"
6 |
7 | import { storeTokenScriptDeployment } from "@/lib/actions/db"
8 | import { ipfsUploadFile } from "@/lib/actions/ipfs"
9 | import { getExplorerUrl, getIpfsUrl } from "@/lib/contracts/contract-utils"
10 | import type { DeployTokenScriptParams, DeployTokenScriptResult } from "@/lib/types"
11 | import { getChainById } from "@/lib/viem"
12 | import { DEPLOYER_ACCOUNT } from "@/lib/config-server"
13 |
14 | export const deployTokenScript = async ({
15 | chainId,
16 | tokenAddress,
17 | tokenScriptSource,
18 | tokenName,
19 | ensDomain,
20 | includeBurnFunction
21 | }: DeployTokenScriptParams): Promise => {
22 | const viemChain = getChainById(Number(chainId)) as Chain
23 |
24 | const alchemyHttpUrl = viemChain?.rpcUrls?.alchemy?.http[0]
25 | ? `${viemChain.rpcUrls.alchemy.http[0]}/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`
26 | : undefined
27 |
28 | const walletClient = createWalletClient({
29 | account: DEPLOYER_ACCOUNT,
30 | chain: viemChain,
31 | transport: http(alchemyHttpUrl)
32 | })
33 |
34 | const publicClient = createPublicClient({
35 | chain: viemChain,
36 | transport: http(alchemyHttpUrl)
37 | })
38 |
39 | if (!(await walletClient.getAddresses())) {
40 | const error = new Error(`Wallet for chain ${viemChain.name} not available`)
41 | console.error(error)
42 | throw error
43 | }
44 |
45 | // Prepare TokenScript
46 | let updatedTokenScriptSource = tokenScriptSource
47 | .replace(/TOKEN_NAME/g, tokenName)
48 | .replace(/CHAIN_ID/g, chainId)
49 | .replace(/CONTRACT_ADDRESS/g, tokenAddress)
50 |
51 | if (ensDomain) {
52 | updatedTokenScriptSource = updatedTokenScriptSource.replace(/ENS_DOMAIN/g, ensDomain)
53 | }
54 |
55 | if (!includeBurnFunction) {
56 | // Remove burn card if not requested
57 | updatedTokenScriptSource = updatedTokenScriptSource.replace(
58 | //,
59 | ""
60 | )
61 | }
62 |
63 | // Upload TokenScript to IPFS
64 | const cid = await ipfsUploadFile("tokenscript.tsml", updatedTokenScriptSource)
65 | if (!cid) {
66 | throw new Error("Error uploading to IPFS")
67 | }
68 |
69 | const ipfsUrl = getIpfsUrl(cid)
70 | const ipfsRoute = [`ipfs://${cid}`]
71 |
72 | // Prepare transaction to update scriptURI
73 | const setScriptURIAbi = parseAbiItem("function setScriptURI(string[] memory newScriptURI)")
74 | const data = encodeFunctionData({
75 | abi: [setScriptURIAbi],
76 | functionName: "setScriptURI",
77 | args: [ipfsRoute]
78 | })
79 |
80 | // Send transaction
81 | const txHash = await walletClient.sendTransaction({
82 | to: tokenAddress,
83 | data
84 | })
85 |
86 | const explorerUrl = getExplorerUrl({
87 | viemChain,
88 | hash: txHash,
89 | type: "tx"
90 | })
91 |
92 | // Wait for transaction confirmation
93 | const transactionReceipt = await publicClient.waitForTransactionReceipt({
94 | hash: txHash
95 | })
96 |
97 | if (!transactionReceipt) {
98 | throw new Error("Failed to receive transaction confirmation")
99 | }
100 |
101 | // Generate viewer URL
102 | const viewerUrl = `https://viewer.tokenscript.org/?chain=${chainId}&contract=${tokenAddress}`
103 |
104 | const deploymentData: DeployTokenScriptResult = {
105 | txHash,
106 | explorerUrl,
107 | ipfsUrl,
108 | viewerUrl,
109 | tokenName,
110 | ensDomain,
111 | includeBurnFunction
112 | }
113 |
114 | await Promise.all([
115 | storeTokenScriptDeployment({
116 | chainId,
117 | deployHash: txHash,
118 | cid,
119 | tokenAddress
120 | }),
121 | track("deployed_tokenscript", {
122 | tokenAddress,
123 | explorerUrl,
124 | cid
125 | })
126 | ])
127 |
128 | return deploymentData
129 | }
130 |
--------------------------------------------------------------------------------
/lib/actions/solidity/verify-contract.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import type { Chain } from "viem"
4 |
5 | import { DEFAULT_GLOBAL_CONFIG } from "@/lib/config"
6 | import type { VerifyContractParams } from "@/lib/types"
7 | import { getExplorerDetails } from "@/lib/viem"
8 |
9 | export const verifyContract = async ({
10 | contractAddress,
11 | standardJsonInput,
12 | encodedConstructorArgs,
13 | fileName,
14 | contractName,
15 | viemChain
16 | }: VerifyContractParams) => {
17 | const { apiUrl, apiKey } = getExplorerDetails(viemChain)
18 |
19 | const params = new URLSearchParams()
20 | params.append("module", "contract")
21 | params.append("action", "verifysourcecode")
22 | params.append("contractaddress", contractAddress)
23 | params.append("sourceCode", JSON.stringify(standardJsonInput))
24 | params.append("codeformat", "solidity-standard-json-input")
25 | params.append("contractname", `${fileName}:${contractName}`)
26 | params.append("compilerversion", DEFAULT_GLOBAL_CONFIG.compilerVersion)
27 | if (encodedConstructorArgs) {
28 | params.append("constructorArguments", encodedConstructorArgs)
29 | }
30 | params.append("optimization", "1")
31 | params.append("optimizationRuns", "200")
32 | params.append("apikey", apiKey)
33 |
34 | const response = await fetch(apiUrl, {
35 | method: "POST",
36 | headers: {
37 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
38 | },
39 | body: new URLSearchParams(params).toString()
40 | })
41 | if (!response.ok) {
42 | throw new Error(`Explorer API request failed with status ${response.status}`)
43 | }
44 |
45 | const verifyResult = (await response.json()) as { status: string; result: string }
46 |
47 | return verifyResult
48 | }
49 |
50 | export const checkVerifyStatus = async (guid: string, viemChain: Chain) => {
51 | const { apiUrl, apiKey } = getExplorerDetails(viemChain)
52 |
53 | const params = new URLSearchParams()
54 | params.append("apikey", apiKey)
55 | params.append("module", "contract")
56 | params.append("action", "checkverifystatus")
57 | params.append("guid", guid)
58 |
59 | const response = await fetch(apiUrl, {
60 | method: "POST",
61 | headers: {
62 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
63 | },
64 | body: new URLSearchParams(params).toString()
65 | })
66 |
67 | if (!response.ok) {
68 | throw new Error(`Explorer API request failed with status ${response.status}`)
69 | }
70 |
71 | const verificationStatus = (await response.json()) as { status: string; result: string }
72 |
73 | return verificationStatus
74 | }
75 |
--------------------------------------------------------------------------------
/lib/actions/unstoppable-domains.ts:
--------------------------------------------------------------------------------
1 | "server-only"
2 |
3 | import { Resolution } from "@unstoppabledomains/resolution"
4 |
5 | const INFURA_API_KEY = process.env.NEXT_PUBLIC_INFURA_API_KEY
6 |
7 | const resolution = new Resolution({
8 | sourceConfig: {
9 | uns: {
10 | locations: {
11 | Layer1: {
12 | url: `https://mainnet.infura.io/v3/${INFURA_API_KEY}`,
13 | network: "mainnet",
14 | },
15 | Layer2: {
16 | url: `https://polygon-mainnet.infura.io/v3/${INFURA_API_KEY}`,
17 | network: "polygon-mainnet",
18 | },
19 | },
20 | },
21 | ens: {
22 | url: `https://mainnet.infura.io/v3/${INFURA_API_KEY}`,
23 | network: "mainnet",
24 | },
25 | zns: {
26 | url: "https://api.zilliqa.com",
27 | network: "mainnet",
28 | },
29 | },
30 | })
31 |
32 | export const resolveDomain = async (domain: string, ticker = "ETH") => {
33 | const address = await resolution.addr(domain, ticker)
34 | return address
35 | }
36 |
37 | export const resolveAddress = async (address: string) => {
38 | const domain = await resolution.reverse(address)
39 | return domain
40 | }
41 |
--------------------------------------------------------------------------------
/lib/actions/verification.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { kv } from "@vercel/kv"
4 |
5 | import { auth } from "@/auth"
6 | import type { VerifyContractParams } from "@/lib/types"
7 |
8 | export const storeVerificationAction = async (data: VerifyContractParams) => {
9 | await kv.hmset(`verification:${data.deployHash}`, data)
10 | }
11 |
12 | export const storeDeploymentAction = async (data: {
13 | chainId: string
14 | deployHash: string
15 | contractAddress: string
16 | cid: string
17 | }) => {
18 | const session = await auth()
19 | const userId = session?.user?.id || "anon"
20 |
21 | await Promise.all([
22 | kv.hmset(`deployment:${data.cid}`, data),
23 | kv.zadd(`user:deployments:${userId}`, {
24 | score: Date.now(),
25 | member: `deployment:${data.cid}`,
26 | }),
27 | ])
28 | }
29 |
--------------------------------------------------------------------------------
/lib/config-server.ts:
--------------------------------------------------------------------------------
1 | import { privateKeyToAccount } from "viem/accounts"
2 | import { z } from "zod"
3 |
4 | const envSchema = z.object({
5 | WEB3GPT_API_SECRET: z.string().min(1),
6 | CRON_SECRET: z.string().min(1),
7 | INFURA_API_KEY: z.string().min(1),
8 | PINATA_JWT: z.string().min(1)
9 | })
10 |
11 | const env = envSchema.parse({
12 | WEB3GPT_API_SECRET: process.env.WEB3GPT_API_SECRET,
13 | CRON_SECRET: process.env.CRON_SECRET,
14 | INFURA_API_KEY: process.env.INFURA_API_KEY,
15 | PINATA_JWT: process.env.PINATA_JWT
16 | })
17 |
18 | export const { WEB3GPT_API_SECRET, CRON_SECRET, INFURA_API_KEY, PINATA_JWT } = env
19 |
20 | export const DEPLOYER_ACCOUNT = privateKeyToAccount(`0x${process.env.DEPLOYER_PRIVATE_KEY}`)
21 |
--------------------------------------------------------------------------------
/lib/config.ts:
--------------------------------------------------------------------------------
1 | import { DEPLOYMENT_URL } from "vercel-url"
2 | import { type Chain, defineChain } from "viem"
3 | import { arbitrumSepolia, baseSepolia, mantleSepoliaTestnet, optimismSepolia, polygonAmoy, sepolia } from "viem/chains"
4 | import { http, type CreateConnectorFn, cookieStorage, createConfig, createStorage } from "wagmi"
5 |
6 | import { BLOCKSCOUT_URLS } from "@/lib/blockscout"
7 | import type { ChainDetails } from "@/lib/types"
8 |
9 | const ALCHEMY_API_KEY = process.env.NEXT_PUBLIC_ALCHEMY_API_KEY
10 | const BLOCKSCOUT_API_KEY = process.env.NEXT_PUBLIC_BLOCKSCOUT_API_KEY
11 |
12 | export const metisSepolia = {
13 | ...defineChain({
14 | id: 59902,
15 | name: "Metis Sepolia",
16 | nativeCurrency: {
17 | name: "Testnet Metis",
18 | symbol: "sMETIS",
19 | decimals: 18,
20 | },
21 | rpcUrls: {
22 | default: { http: ["https://sepolia.metisdevops.link"], webSocket: ["wss://sepolia-ws.rpc.metisdevops.link"] },
23 | },
24 | blockExplorers: {
25 | default: {
26 | name: "Metis Sepolia Blockscout",
27 | url: "https://sepolia-explorer.metisdevops.link",
28 | apiUrl: "https://sepolia-explorer-api.metisdevops.link/api",
29 | },
30 | },
31 | testnet: true,
32 | sourceId: 11155111,
33 | }),
34 | iconUrl: "/assets/metis-logo.png",
35 | }
36 |
37 | export const APP_URL = DEPLOYMENT_URL
38 | export const DEFAULT_COMPILER_VERSION = "v0.8.28+commit.7893614a"
39 | export const DEFAULT_CHAIN = metisSepolia
40 |
41 | const mantleSepolia = {
42 | ...mantleSepoliaTestnet,
43 | name: "Mantle Sepolia",
44 | iconUrl: "/mantle-logo.jpeg",
45 | }
46 |
47 | const amoy = {
48 | ...polygonAmoy,
49 | iconUrl: "/polygon-logo.png",
50 | }
51 |
52 | export const supportedChains: [Chain, ...Chain[]] = [
53 | arbitrumSepolia,
54 | optimismSepolia,
55 | baseSepolia,
56 | metisSepolia,
57 | mantleSepolia,
58 | amoy,
59 | sepolia,
60 | ]
61 |
62 | export const CHAIN_DETAILS: Record = {
63 | [sepolia.id]: {
64 | rpcUrl: `https://eth-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
65 | explorerUrl: "https://sepolia.etherscan.io",
66 | explorerApiUrl: "https://api-sepolia.etherscan.io/api",
67 | explorerApiKey: process.env.NEXT_PUBLIC_ETHERSCAN_API_KEY,
68 | },
69 | [polygonAmoy.id]: {
70 | rpcUrl: `https://polygon-amoy.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
71 | explorerUrl: "https://amoy.polygonscan.com",
72 | explorerApiUrl: "https://api-amoy.polygonscan.com/api",
73 | explorerApiKey: process.env.NEXT_PUBLIC_POLYGONSCAN_API_KEY,
74 | },
75 | [baseSepolia.id]: {
76 | rpcUrl: `https://base-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
77 | explorerUrl: "https://sepolia.basescan.org",
78 | explorerApiUrl: "https://api-sepolia.basescan.org/api",
79 | explorerApiKey: process.env.NEXT_PUBLIC_BASESCAN_API_KEY,
80 | },
81 | [mantleSepolia.id]: {
82 | rpcUrl: `https://green-few-wish.mantle-sepolia.quiknode.pro/${process.env.NEXT_PUBLIC_QUICKNODE_API_KEY}`,
83 | explorerUrl: "https://sepolia.mantlescan.xyz/",
84 | explorerApiUrl: "https://api-sepolia.mantlescan.xyz/api",
85 | explorerApiKey: process.env.NEXT_PUBLIC_MANTLESCAN_API_KEY,
86 | },
87 | [arbitrumSepolia.id]: {
88 | rpcUrl: `https://arb-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
89 | explorerUrl: "https://sepolia.arbiscan.io",
90 | explorerApiUrl: "https://api-sepolia.arbiscan.io/api",
91 | explorerApiKey: process.env.NEXT_PUBLIC_ARBISCAN_API_KEY,
92 | },
93 | [optimismSepolia.id]: {
94 | rpcUrl: `https://opt-sepolia.g.alchemy.com/v2/${ALCHEMY_API_KEY}`,
95 | explorerUrl: "https://sepolia-optimism.etherscan.io",
96 | explorerApiUrl: "https://api-sepolia-optimistic.etherscan.io/api",
97 | explorerApiKey: process.env.NEXT_PUBLIC_OPSCAN_API_KEY,
98 | },
99 | [metisSepolia.id]: {
100 | rpcUrl: "https://sepolia.metisdevops.link",
101 | explorerUrl: "https://sepolia-explorer.metisdevops.link",
102 | explorerApiUrl: "https://sepolia-explorer-api.metisdevops.link/api",
103 | explorerApiKey: BLOCKSCOUT_API_KEY,
104 | },
105 | }
106 |
107 | const buildApiUrl = (blockscoutUrl: string) => {
108 | if (blockscoutUrl === "https://sepolia-explorer.metisdevops.link") {
109 | return "https://sepolia-explorer-api.metisdevops.link/api"
110 | }
111 | return `${blockscoutUrl}/api`
112 | }
113 |
114 | export const getChainDetails = (viemChain: Chain): ChainDetails => {
115 | const chainId = viemChain.id
116 | const chainDetails = CHAIN_DETAILS[chainId]
117 | const blockscoutUrl = BLOCKSCOUT_URLS[chainId]
118 |
119 | return {
120 | rpcUrl: chainDetails.rpcUrl,
121 | explorerUrl: blockscoutUrl || chainDetails.explorerUrl || viemChain.blockExplorers?.default.url || "",
122 | explorerApiUrl: blockscoutUrl
123 | ? buildApiUrl(blockscoutUrl)
124 | : chainDetails.explorerApiUrl || viemChain.blockExplorers?.default.apiUrl || "",
125 | explorerApiKey: blockscoutUrl ? BLOCKSCOUT_API_KEY : chainDetails.explorerApiKey,
126 | }
127 | }
128 |
129 | export function getChainById(chainId: number): Chain | null {
130 | return supportedChains.find((chain) => chain.id === chainId) || null
131 | }
132 |
133 | export function getWagmiConfig(connectors?: CreateConnectorFn[]) {
134 | return createConfig({
135 | chains: supportedChains,
136 | transports: Object.fromEntries(supportedChains.map((chain) => [[chain.id], http(CHAIN_DETAILS[chain.id].rpcUrl)])),
137 | ssr: true,
138 | storage: createStorage({
139 | storage: cookieStorage,
140 | }),
141 | connectors,
142 | })
143 | }
144 |
--------------------------------------------------------------------------------
/lib/constants.ts:
--------------------------------------------------------------------------------
1 | import type { Agent } from "@/lib/types"
2 |
3 | export const DEFAULT_AGENT_ID = "asst_Tgzrzv0VaSgTRMn8ufAULlZG"
4 | export const TOKENSCRIPT_AGENT_ID = "asst_13kX3wWTUa7Gz9jvFOqnnA77"
5 |
6 | export const DEFAULT_AGENT: Agent = {
7 | id: DEFAULT_AGENT_ID,
8 | userId: "12901349",
9 | name: "Web3GPT",
10 | description: "Develop smart contracts",
11 | creator: "soko.eth",
12 | imageUrl: "/assets/web3gpt.png",
13 | }
14 |
15 | export const AGENTS_ARRAY: Agent[] = [
16 | DEFAULT_AGENT,
17 | {
18 | id: "asst_mv5KGoBLhXXQFiJHpgnopGQQ",
19 | userId: "12901349",
20 | name: "Unstoppable Domains",
21 | description: "Resolve cryptocurrency addresses to domains and vice versa",
22 | creator: "soko.eth",
23 | imageUrl: "https://docs.unstoppabledomains.com/images/logo.png",
24 | },
25 | {
26 | name: "OpenZeppelin 5.0",
27 | userId: "12901349",
28 | creator: "soko.eth",
29 | description:
30 | "Assists users in writing and deploying smart contracts using the OpenZeppelin 5.0 libraries, incorporating the latest features and best practices.",
31 | id: "asst_s66Y7GSbtkCLHMWKylSjqO7g",
32 | imageUrl: "https://www.openzeppelin.com/hubfs/oz-iso.svg",
33 | },
34 | {
35 | name: "CTF Agent",
36 | userId: "12901349",
37 | creator: "soko.eth",
38 | description:
39 | "Learn solidity the fun way by solving interactive challenges. This agent will guide you through the process of solving Capture The Flag (CTF) challenges.",
40 | id: "asst_GfjkcVcwAXzkNE1JBXNfe89q",
41 | imageUrl:
42 | "https://media.licdn.com/dms/image/D5612AQEMTmdASEpqog/article-cover_image-shrink_720_1280/0/1680103178404?e=2147483647&v=beta&t=J6hdKmr-VKTqTyLzO2FR10_mJTdAxzU4QWTQiRrv2fs",
43 | },
44 | {
45 | id: "asst_q1i7mHlBuAbDSrpDQk9f3Egm",
46 | userId: "12901349",
47 | name: "Creator",
48 | description: "Create your own AI agent",
49 | creator: "soko.eth",
50 | imageUrl: "/assets/agent-factory.png",
51 | },
52 | {
53 | id: TOKENSCRIPT_AGENT_ID,
54 | userId: "12689544",
55 | name: "Smart Token",
56 | description: "Create a Smart Token - create and self deploy a token, then power it with a TokenScript",
57 | creator: "61cygni.eth",
58 | imageUrl: "/assets/tokenscript.png",
59 | },
60 | ]
61 |
--------------------------------------------------------------------------------
/lib/contracts/contract-utils.ts:
--------------------------------------------------------------------------------
1 | import type { Chain, Hash } from "viem"
2 |
3 | import { resolveImports } from "@/lib/contracts/resolve-imports"
4 | import { getGatewayUrl } from "@/lib/utils"
5 | import { getExplorerDetails } from "@/lib/viem"
6 |
7 | export async function prepareContractSources(contractName: string, sourceCode: string) {
8 | const fileName = getContractFileName(contractName)
9 |
10 | const handleImportsResult = await resolveImports(sourceCode)
11 |
12 | const sources = {
13 | [fileName]: {
14 | content: handleImportsResult?.sourceCode
15 | },
16 | ...handleImportsResult?.sources
17 | }
18 |
19 | const sourcesKeys = Object.keys(sources)
20 |
21 | for (const sourceKey of sourcesKeys) {
22 | let sourceCode = sources[sourceKey].content
23 | const importStatements = sourceCode.match(/import\s+["'][^"']+["'];/g) || []
24 |
25 | for (const importStatement of importStatements) {
26 | const importPathMatch = importStatement.match(/["']([^"']+)["']/)
27 | if (!importPathMatch) continue
28 |
29 | const importPath = importPathMatch[1]
30 | const fileName = importPath.split("/").pop() || importPath
31 | sourceCode = sourceCode.replace(importStatement, `import "${fileName}";`)
32 | }
33 |
34 | sources[sourceKey].content = sourceCode
35 | }
36 |
37 | return sources
38 | }
39 |
40 | export function getExplorerUrl({
41 | viemChain,
42 | hash,
43 | type
44 | }: {
45 | viemChain: Chain
46 | hash: Hash
47 | type: "tx" | "address"
48 | }) {
49 | const { url } = getExplorerDetails(viemChain)
50 | if (type === "tx") {
51 | return `${url}/tx/${hash}`
52 | }
53 |
54 | return `${url}/address/${hash}`
55 | }
56 |
57 | export function getIpfsUrl(cid: string) {
58 | return getGatewayUrl(cid)
59 | }
60 |
61 | export const getContractFileName = (contractName: string) => `${contractName.replace(/[\/\\:*?"<>|.\s]+$/g, "_")}.sol`
62 |
--------------------------------------------------------------------------------
/lib/contracts/resolve-imports.ts:
--------------------------------------------------------------------------------
1 | // Recursive function to resolve imports in a source code. Fetches the source code of the imports one by one and returns the final source code with all imports resolved and urls / aliases replaced with relative paths.
2 | export async function resolveImports(sourceCode: string, sourcePath?: string) {
3 | const sources: { [fileName: string]: { content: string } } = {}
4 | const importRegex = /import\s+(?:{[^}]+}\s+from\s+)?["']([^"']+)["'];/g
5 | const matches = Array.from(sourceCode.matchAll(importRegex))
6 | let sourceCodeWithImports = sourceCode
7 | for (const match of matches) {
8 | const importPath = match[1]
9 | const { sources: importedSources, sourceCode: mainSourceCode } = await fetchImport(importPath, sourcePath)
10 |
11 | // Merge the imported sources into the main sources object
12 | Object.assign(sources, importedSources)
13 |
14 | let sourceFileName = importPath.split("/").pop() || importPath
15 |
16 | // if sources[sourceFileName] already exists and the content is the same, then skip, otherwise change the sourceFileName to keep the folder structure but still be a relative path
17 | if (sources[sourceFileName] && sources[sourceFileName].content !== mainSourceCode) {
18 | sourceFileName = importPath.split("/").slice(-2).join("/")
19 | }
20 |
21 | sources[sourceFileName] = {
22 | content: mainSourceCode
23 | }
24 | sourceCodeWithImports = sourceCode.replace(match[0], `import "${sourceFileName}";`)
25 | }
26 | return { sources, sourceCode: sourceCodeWithImports }
27 | }
28 |
29 | async function fetchImport(importPath: string, sourcePath?: string) {
30 | // Determine the URL to fetch
31 | let urlToFetch: string
32 | if (importPath[0] === "." && sourcePath) {
33 | // If the import path starts with '.', it's a relative path, so resolve the path
34 | const finalPath = resolveImportPath(importPath, sourcePath)
35 | urlToFetch = finalPath
36 | } else if (importPath[0] !== "@") {
37 | // If the import path starts with anything other than '@', use it directly
38 | urlToFetch = importPath
39 | } else {
40 | // Otherwise, convert the import path to an unpkg URL
41 | urlToFetch = `https://unpkg.com/${importPath}`
42 | }
43 | // Convert GitHub URLs to raw content URLs
44 | if (urlToFetch.includes("github.com")) {
45 | urlToFetch = urlToFetch.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/")
46 | }
47 |
48 | // Fetch the imported file
49 | const response = await fetch(urlToFetch)
50 |
51 | if (!response.ok) {
52 | throw new Error(`HTTP error! status: ${response.status}`)
53 | }
54 |
55 | const importedSource = await response.text()
56 |
57 | // Handle any imports within the fetched source code
58 | const { sources, sourceCode } = await resolveImports(importedSource, urlToFetch)
59 |
60 | return { sources, sourceCode }
61 | }
62 |
63 | /// utility function to handle relative paths
64 | function resolveImportPath(importPath: string, sourcePath: string) {
65 | const importSegments = importPath.split("/")
66 | const sourceSegments = sourcePath.split("/")
67 |
68 | // Remove the last segment (file name) from the source path
69 | sourceSegments.pop()
70 |
71 | // Process each segment of the import path
72 | while (importSegments.length > 0 && importSegments[0] === "..") {
73 | // Remove one directory level for each '..'
74 | sourceSegments.pop()
75 | importSegments.shift()
76 | }
77 |
78 | // Special handling for './'
79 | if (importSegments.length > 0 && importSegments[0] === ".") {
80 | importSegments.shift()
81 | }
82 |
83 | // Reconstruct the final path
84 | return sourceSegments.concat(importSegments).join("/")
85 | }
86 |
--------------------------------------------------------------------------------
/lib/data/ipfs.ts:
--------------------------------------------------------------------------------
1 | "server-only"
2 |
3 | import { type FileObject, PinataSDK } from "pinata"
4 | import type { SolcOutput } from "solc"
5 | import type { Abi } from "viem"
6 |
7 | const pinata = new PinataSDK({
8 | pinataJwt: process.env.PINATA_JWT,
9 | pinataGateway: process.env.NEXT_PUBLIC_IPFS_GATEWAY,
10 | })
11 |
12 | export async function ipfsUploadDir(
13 | sources: SolcOutput["sources"],
14 | abi: Abi,
15 | bytecode: string,
16 | standardJsonInput: string,
17 | ): Promise {
18 | try {
19 | const files: FileObject[] = []
20 |
21 | for (const [fileName, { content }] of Object.entries(sources)) {
22 | files.push(new File([content], fileName))
23 | }
24 | files.push(new File([JSON.stringify(abi, null, 2)], "abi.json"))
25 | files.push(new File([bytecode], "bytecode.txt"))
26 | files.push(new File([standardJsonInput], "standardJsonInput.json"))
27 |
28 | const { IpfsHash } = await pinata.upload.fileArray(files, {
29 | cidVersion: 1,
30 | metadata: {
31 | name: "contract",
32 | },
33 | })
34 |
35 | return IpfsHash
36 | } catch (error) {
37 | console.error("Error writing to temporary file:", error)
38 | return null
39 | }
40 | }
41 |
42 | export async function ipfsUploadFile(fileName: string, fileContent: string | Buffer): Promise {
43 | try {
44 | const file = new File([fileContent], fileName)
45 | const { IpfsHash } = await pinata.upload.file(file, {
46 | cidVersion: 1,
47 | metadata: {
48 | name: fileName,
49 | },
50 | })
51 |
52 | return IpfsHash
53 | } catch (error) {
54 | console.error("Error writing to temporary file:", error)
55 | return null
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/data/kv.ts:
--------------------------------------------------------------------------------
1 | import "server-only"
2 |
3 | import { kv } from "@vercel/kv"
4 | import { unstable_cache as cache, revalidateTag } from "next/cache"
5 |
6 | import { auth } from "@/auth"
7 | import type { Agent, DbChat, DbChatListItem, VerifyContractParams } from "@/lib/types"
8 |
9 | type ActionWithUser = (data: T, userId: string) => Promise
10 |
11 | export const withUser = (action: ActionWithUser) => {
12 | return async (data: T): Promise => {
13 | const session = await auth()
14 | const userId = session?.user?.id
15 | if (!userId) return
16 |
17 | return action(data, userId)
18 | }
19 | }
20 |
21 | export async function storeUser(user: { id: string }) {
22 | const userKey = `user:details:${user.id}`
23 |
24 | await kv.hmset(userKey, user)
25 |
26 | await kv.sadd("users:list", user.id)
27 | }
28 |
29 | export const getChatList = withUser(
30 | cache(
31 | async (_, userId) => {
32 | const chats: string[] = await kv.zrange(`user:chat:${userId}`, 0, -1, { rev: true })
33 | if (!chats.length) {
34 | return []
35 | }
36 | const pipeline = kv.pipeline()
37 |
38 | for (const chat of chats) {
39 | pipeline.hmget(chat, "id", "title", "published", "createdAt", "avatarUrl", "userId")
40 | }
41 | return await pipeline.exec()
42 | },
43 | ["chat-list"],
44 | { revalidate: 3600, tags: ["chat-list"] },
45 | ),
46 | )
47 |
48 | export const getChat = withUser(async (id) => {
49 | return await kv.hgetall(`chat:${id}`)
50 | })
51 |
52 | export async function getPublishedChat(id: string) {
53 | const chat = await kv.hgetall(`chat:${id}`)
54 |
55 | if (!chat?.published) {
56 | return {
57 | ...chat,
58 | messages: [],
59 | }
60 | }
61 |
62 | return chat
63 | }
64 |
65 | export const getAgent = async (id: string) => {
66 | return await kv.hgetall(`agent:${id}`)
67 | }
68 |
69 | // verifications
70 |
71 | export const getVerifications = async () => {
72 | const verifications = await kv.keys("verification:*")
73 | if (!verifications || verifications.length === 0) {
74 | return []
75 | }
76 | const pipeline = kv.pipeline()
77 |
78 | for (const verification of verifications) {
79 | pipeline.hgetall(verification)
80 | }
81 |
82 | if (!pipeline) {
83 | return []
84 | }
85 | return await pipeline.exec()
86 | }
87 |
88 | export const deleteVerification = async (deployHash: string) => {
89 | await kv.del(`verification:${deployHash}`)
90 | }
91 |
92 | export const storeAgent = withUser(async (agent, userId) => {
93 | if (userId !== agent.userId) {
94 | return
95 | }
96 | await Promise.all([kv.hmset(`agent:${agent.id}`, agent), kv.sadd("agents:list", agent.id)])
97 | })
98 |
99 | export const storeChat = withUser<
100 | {
101 | data: DbChat
102 | userId: string
103 | },
104 | void
105 | >(async ({ data, userId }) => {
106 | if (userId !== data.userId) {
107 | return
108 | }
109 |
110 | const payload: DbChat = {
111 | ...data,
112 | userId,
113 | }
114 |
115 | await Promise.all([
116 | kv.hmset(`chat:${data.id}`, payload),
117 | kv.zadd(`user:chat:${userId}`, {
118 | score: data.createdAt,
119 | member: `chat:${data.id}`,
120 | }),
121 | ])
122 |
123 | return revalidateTag("chat-list")
124 | })
125 |
--------------------------------------------------------------------------------
/lib/data/openai.ts:
--------------------------------------------------------------------------------
1 | import "server-only"
2 |
3 | import { storeAgent } from "@/lib/data/kv"
4 | import { TOOL_SCHEMAS, ToolName } from "@/lib/tools"
5 | import type { CreateAgentParams } from "@/lib/types"
6 | import type { Message } from "ai"
7 | import { OpenAI } from "openai"
8 |
9 | export const openai = new OpenAI()
10 |
11 | export const getAiThreadMessages = async (threadId: string) => {
12 | const fullMessages = (await openai.beta.threads.messages.list(threadId, { order: "asc" })).data
13 |
14 | return fullMessages.map((message) => {
15 | const { id, content, role, created_at: createdAt } = message
16 | const textContent = content.find((c) => c.type === "text")
17 | const text = textContent?.type === "text" ? textContent.text.value : ""
18 |
19 | return {
20 | id,
21 | content: text,
22 | role,
23 | createdAt: new Date(createdAt * 1000),
24 | } satisfies Message
25 | })
26 | }
27 |
28 | export const createAgent = async ({
29 | name,
30 | userId,
31 | description,
32 | instructions,
33 | creator,
34 | imageUrl,
35 | }: CreateAgentParams) => {
36 | try {
37 | const { id } = await openai.beta.assistants.create({
38 | name: name,
39 | model: "gpt-4o",
40 | description: description,
41 | instructions: instructions,
42 | tools: [
43 | TOOL_SCHEMAS[ToolName.DeployContract],
44 | TOOL_SCHEMAS[ToolName.ResolveAddress],
45 | TOOL_SCHEMAS[ToolName.ResolveDomain],
46 | ],
47 | })
48 |
49 | if (!userId) {
50 | throw new Error("Unauthorized")
51 | }
52 |
53 | await storeAgent({
54 | id,
55 | userId,
56 | name,
57 | description,
58 | creator,
59 | imageUrl,
60 | })
61 |
62 | return id
63 | } catch (error) {
64 | console.error("Error in createAgent", error)
65 | return null
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/hooks/use-copy-to-clipboard.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 |
3 | import { useIsClient } from "@/lib/hooks/use-is-client"
4 |
5 | export type useCopyToClipboardProps = {
6 | timeout?: number
7 | }
8 |
9 | export function useCopyToClipboard({ timeout = 2000 }: useCopyToClipboardProps) {
10 | const [isCopied, setIsCopied] = useState(false)
11 | const isClient = useIsClient()
12 |
13 | const copyToClipboard = (value: string) => {
14 | if (!isClient || isCopied) {
15 | return
16 | }
17 | navigator.clipboard.writeText(value).then(() => {
18 | setIsCopied(true)
19 |
20 | setTimeout(() => {
21 | setIsCopied(false)
22 | }, timeout)
23 | })
24 | }
25 |
26 | return { isCopied, copyToClipboard }
27 | }
28 |
--------------------------------------------------------------------------------
/lib/hooks/use-enter-submit.tsx:
--------------------------------------------------------------------------------
1 | import { type RefObject, useRef } from "react"
2 |
3 | export function useEnterSubmit(): {
4 | formRef: RefObject
5 | onKeyDown: (event: React.KeyboardEvent) => void
6 | } {
7 | const formRef = useRef(null)
8 |
9 | const handleKeyDown = (event: React.KeyboardEvent): void => {
10 | if (event.key === "Enter" && !event.shiftKey && !event.nativeEvent.isComposing) {
11 | formRef.current?.requestSubmit()
12 | event.preventDefault()
13 | }
14 | }
15 |
16 | return { formRef, onKeyDown: handleKeyDown }
17 | }
18 |
--------------------------------------------------------------------------------
/lib/hooks/use-is-client.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useEffect, useState } from "react"
4 |
5 | export const useIsClient = () => {
6 | const [isClient, setIsClient] = useState(false)
7 |
8 | useEffect(() => setIsClient(true), [])
9 |
10 | return isClient
11 | }
12 |
--------------------------------------------------------------------------------
/lib/hooks/use-local-storage.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react"
2 |
3 | export const useLocalStorage = (key: string, initialValue: T): [T, (value: T) => void] => {
4 | const [storedValue, setStoredValue] = useState(initialValue)
5 |
6 | useEffect(() => {
7 | // Retrieve from localStorage
8 | const item = window.localStorage.getItem(key)
9 | if (item) {
10 | setStoredValue(JSON.parse(item))
11 | }
12 | }, [key])
13 |
14 | const setValue = (value: T) => {
15 | // Save state
16 | setStoredValue(value)
17 | // Save to localStorage
18 | window.localStorage.setItem(key, JSON.stringify(value))
19 | }
20 | return [storedValue, setValue]
21 | }
22 |
--------------------------------------------------------------------------------
/lib/hooks/use-safe-auto-connect.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react"
2 | import { useConnect } from "wagmi"
3 |
4 | const AUTOCONNECTED_CONNECTOR_IDS = ["safe"]
5 |
6 | export const useSafeAutoConnect = () => {
7 | const { connect, connectors } = useConnect()
8 |
9 | useEffect(() => {
10 | for (const connector of AUTOCONNECTED_CONNECTOR_IDS) {
11 | const connectorInstance = connectors.find((c) => c.id === connector && c.ready)
12 |
13 | if (connectorInstance) {
14 | connect({ connector: connectorInstance })
15 | }
16 | }
17 | }, [connect, connectors])
18 | }
19 |
--------------------------------------------------------------------------------
/lib/hooks/use-scroll-to-bottom.tsx:
--------------------------------------------------------------------------------
1 | import { type RefObject, useCallback, useEffect, useState } from "react"
2 |
3 | export function useScrollToBottom(ref?: RefObject, offset = 200) {
4 | const [isAtBottom, setIsAtBottom] = useState(false)
5 |
6 | const checkIfAtBottom = useCallback(() => {
7 | const atBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - offset
8 | setIsAtBottom(atBottom)
9 | }, [offset])
10 |
11 | const scrollToBottom = useCallback(() => {
12 | window.scrollTo({
13 | top: document.body.offsetHeight,
14 | behavior: "smooth",
15 | })
16 | }, [])
17 |
18 | useEffect(() => {
19 | window.addEventListener("scroll", checkIfAtBottom, { passive: true })
20 | checkIfAtBottom()
21 | return () => {
22 | window.removeEventListener("scroll", checkIfAtBottom)
23 | }
24 | }, [checkIfAtBottom])
25 |
26 | useEffect(() => {
27 | if (ref?.current) {
28 | ref.current.scrollTop = ref.current.scrollHeight
29 | scrollToBottom()
30 | }
31 | }, [ref, scrollToBottom])
32 |
33 | return { isAtBottom, scrollToBottom }
34 | }
35 |
--------------------------------------------------------------------------------
/lib/hooks/use-tokenscript-deploy.ts:
--------------------------------------------------------------------------------
1 | import { toast } from "sonner"
2 | import { encodeFunctionData, parseAbiItem } from "viem"
3 | import { useAccount, usePublicClient, useWalletClient } from "wagmi"
4 |
5 | import { useGlobalStore } from "@/app/state/global-store"
6 | import { storeTokenScriptDeployment } from "@/lib/actions/db"
7 | import { ipfsUploadFile } from "@/lib/actions/ipfs"
8 |
9 | export function useWriteToIPFS() {
10 | const { chain: viemChain } = useAccount()
11 | const { data: walletClient } = useWalletClient()
12 | const publicClient = usePublicClient({
13 | chainId: viemChain?.id || 5003
14 | })
15 |
16 | const { lastDeploymentData } = useGlobalStore()
17 |
18 | async function deploy({
19 | tokenScriptSource
20 | }: {
21 | tokenScriptSource: string
22 | }) {
23 | if (!viemChain || !walletClient || !publicClient) {
24 | throw new Error("Provider or wallet not available")
25 | }
26 | if (!lastDeploymentData) {
27 | throw new Error("No token deployment data found")
28 | }
29 |
30 | const deployToast = toast.loading("Deploying TokenScript to IPFS...")
31 |
32 | const tokenAddress = lastDeploymentData.contractAddress
33 | if (walletClient.account.address !== lastDeploymentData.walletAddress) {
34 | toast.error("No deployed token found for the connected wallet")
35 | return
36 | }
37 | const ipfsCid = await ipfsUploadFile("tokenscript.tsml", tokenScriptSource)
38 |
39 | toast.dismiss(deployToast)
40 |
41 | if (ipfsCid === null) {
42 | toast.error("Error uploading to IPFS")
43 | return
44 | }
45 |
46 | toast.success("TokenScript uploaded! Updating contract scriptURI...")
47 |
48 | const ipfsRoute = [`ipfs://${ipfsCid}`]
49 |
50 | const setScriptURIAbi = parseAbiItem("function setScriptURI(string[] memory newScriptURI)")
51 | try {
52 | const data = encodeFunctionData({
53 | abi: [setScriptURIAbi],
54 | functionName: "setScriptURI",
55 | args: [ipfsRoute]
56 | })
57 |
58 | const txHash = await walletClient.sendTransaction({
59 | to: tokenAddress,
60 | data
61 | })
62 |
63 | const transactionReceipt = await publicClient.waitForTransactionReceipt({
64 | hash: txHash
65 | })
66 |
67 | if (!transactionReceipt) {
68 | toast.error("Failed to receive enough confirmations")
69 | return
70 | }
71 | toast.success("Transaction confirmed!")
72 |
73 | const chainId = await walletClient.getChainId()
74 |
75 | await storeTokenScriptDeployment({
76 | chainId: chainId.toString(),
77 | deployHash: txHash,
78 | cid: ipfsCid,
79 | tokenAddress
80 | })
81 |
82 | return `https://viewer.tokenscript.org/?chain=${chainId}&contract=${tokenAddress}`
83 | } catch (error) {
84 | console.error(error)
85 | toast.error("Failed to deploy TokenScript")
86 | return "unable to generate viewer url"
87 | }
88 | }
89 |
90 | return { deploy }
91 | }
92 |
--------------------------------------------------------------------------------
/lib/hooks/use-tokenscript-deploy.tsx:
--------------------------------------------------------------------------------
1 | import { toast } from "sonner"
2 | import { encodeFunctionData, parseAbiItem, publicActions } from "viem"
3 | import { useAccount, useWalletClient } from "wagmi"
4 |
5 | import { useGlobalStore } from "@/app/state/global-store"
6 | import { ipfsUploadFileAction } from "@/lib/actions/deploy-contract"
7 |
8 | export function useTokenScriptDeploy() {
9 | const { chain: viemChain, chainId, address } = useAccount()
10 | const { data } = useWalletClient({
11 | chainId,
12 | })
13 | const walletClient = data?.extend(publicActions)
14 |
15 | const { lastDeploymentData } = useGlobalStore()
16 |
17 | async function deploy({ tokenScriptSource }: { tokenScriptSource: string }): Promise<
18 | | {
19 | txHash: string
20 | cid: string
21 | tokenAddress: string
22 | }
23 | | undefined
24 | > {
25 | if (!viemChain || !walletClient) {
26 | throw new Error("Provider or wallet not available")
27 | }
28 | if (!lastDeploymentData) {
29 | throw new Error("No token deployment data found")
30 | }
31 |
32 | const deployToast = toast.loading("Deploying TokenScript to IPFS...")
33 |
34 | const tokenAddress = lastDeploymentData.contractAddress
35 | if (address !== lastDeploymentData.walletAddress) {
36 | toast.error("Last deployment must be from the connected wallet")
37 | return
38 | }
39 | const cid = await ipfsUploadFileAction("tokenscript.tsml", tokenScriptSource)
40 |
41 | toast.dismiss(deployToast)
42 |
43 | if (!cid) {
44 | toast.error("Error uploading to IPFS")
45 | return
46 | }
47 |
48 | toast.success("TokenScript uploaded! Updating contract scriptURI...")
49 |
50 | const ipfsRoute = [`ipfs://${cid}`]
51 |
52 | const setScriptURIAbi = parseAbiItem("function setScriptURI(string[] memory newScriptURI)")
53 | try {
54 | const data = encodeFunctionData({
55 | abi: [setScriptURIAbi],
56 | functionName: "setScriptURI",
57 | args: [ipfsRoute],
58 | })
59 |
60 | const txHash = await walletClient.sendTransaction({
61 | to: tokenAddress,
62 | data,
63 | })
64 |
65 | const transactionReceipt = await walletClient.waitForTransactionReceipt({
66 | hash: txHash,
67 | })
68 |
69 | if (!transactionReceipt) {
70 | toast.error("Failed to receive enough confirmations")
71 | return
72 | }
73 | toast.success("Transaction confirmed!")
74 |
75 | return {
76 | txHash,
77 | cid,
78 | tokenAddress,
79 | }
80 | } catch (error) {
81 | console.error(error)
82 | toast.error("Failed to deploy TokenScript")
83 | return
84 | }
85 | }
86 |
87 | return { deploy }
88 | }
89 |
--------------------------------------------------------------------------------
/lib/openai.ts:
--------------------------------------------------------------------------------
1 | import { OpenAI } from "openai"
2 |
3 | export const openai = new OpenAI({
4 | apiKey: process.env.OPENAI_API_KEY
5 | })
6 |
--------------------------------------------------------------------------------
/lib/rainbowkit.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { connectorsForWallets } from "@rainbow-me/rainbowkit"
4 | import {
5 | coinbaseWallet,
6 | injectedWallet,
7 | metaMaskWallet,
8 | rainbowWallet,
9 | safeWallet,
10 | walletConnectWallet,
11 | } from "@rainbow-me/rainbowkit/wallets"
12 |
13 | import { APP_URL } from "@/lib/config"
14 |
15 | export const connectors = connectorsForWallets(
16 | [
17 | {
18 | groupName: "Recommended",
19 | wallets: [rainbowWallet, walletConnectWallet, coinbaseWallet, metaMaskWallet, injectedWallet, safeWallet],
20 | },
21 | ],
22 | {
23 | appName: "Web3GPT",
24 | appDescription: "Write and deploy Solidity smart contracts with AI",
25 | appUrl: APP_URL,
26 | appIcon: "/assets/web3gpt.png",
27 | projectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID,
28 | },
29 | )
30 |
--------------------------------------------------------------------------------
/lib/solc.d.ts:
--------------------------------------------------------------------------------
1 | declare module "solc" {
2 | import type { Abi } from "viem"
3 |
4 | export type SolcInputSources = {
5 | [fileName: string]: {
6 | content: string
7 | }
8 | }
9 |
10 | export type SolcInput = {
11 | language: "Solidity"
12 | sources: SolcInputSources
13 | settings: {
14 | optimizer: {
15 | enabled: boolean
16 | runs: number
17 | }
18 | outputSelection: {
19 | "*": {
20 | "*": string[]
21 | }
22 | }
23 | }
24 | }
25 |
26 | export type Bytecode = {
27 | functionDebugData: Record<
28 | string,
29 | {
30 | entryPoint: number | null
31 | id: number | null
32 | parameterSlots: number
33 | returnSlots: number
34 | }
35 | >
36 | generatedSources: Array<{
37 | ast: object
38 | contents: string
39 | id: number
40 | language: string
41 | name: string
42 | }>
43 | linkReferences: Record>>
44 | object: string
45 | opcodes: string
46 | sourceMap: string
47 | }
48 |
49 | export type Contract = {
50 | abi: Abi
51 | devdoc: {
52 | kind: string
53 | methods: Record
54 | version: number
55 | }
56 | evm: {
57 | assembly: string
58 | bytecode: Bytecode
59 | deployedBytecode: Bytecode
60 | gasEstimates: {
61 | creation: {
62 | codeDepositCost: string
63 | executionCost: string
64 | totalCost: string
65 | }
66 | external: Record
67 | }
68 | legacyAssembly: object
69 | methodIdentifiers: Record
70 | }
71 | metadata: string
72 | storageLayout: {
73 | storage: Array<{
74 | astId: number
75 | contract: string
76 | label: string
77 | offset: number
78 | slot: string
79 | type: string
80 | }>
81 | types: Record<
82 | string,
83 | {
84 | encoding: string
85 | label: string
86 | numberOfBytes: string
87 | }
88 | >
89 | }
90 | userdoc: {
91 | kind: string
92 | methods: Record
93 | version: number
94 | }
95 | }
96 |
97 | export type CompilationError = {
98 | component: string
99 | errorCode: string
100 | formattedMessage: string
101 | message: string
102 | severity: "error" | "warning"
103 | sourceLocation?: {
104 | end: number
105 | file: string
106 | start: number
107 | }
108 | type: string
109 | }
110 |
111 | export type SolcOutput = {
112 | contracts: {
113 | [fileName: string]: {
114 | [contractName: string]: Contract
115 | }
116 | }
117 | sources: {
118 | [fileName: string]: {
119 | content: string
120 | }
121 | }
122 | errors?: CompilationError[]
123 | }
124 |
125 | function compile(input: string, readCallback?: (path: string) => { contents: string } | { error: string }): string
126 |
127 | const solc: {
128 | compile: typeof compile
129 | }
130 |
131 | export default solc
132 | }
133 |
--------------------------------------------------------------------------------
/lib/solidity/utils.ts:
--------------------------------------------------------------------------------
1 | // Recursive function to resolve imports in a source code. Fetches the source code of the imports one by one and returns the final source code with all imports resolved and urls / aliases replaced with relative paths.
2 | export async function resolveImports(sourceCode: string, sourcePath?: string) {
3 | const sources: { [fileName: string]: { content: string } } = {}
4 | const importRegex = /import\s+(?:{[^}]+}\s+from\s+)?["']([^"']+)["'];/g
5 | const matches = Array.from(sourceCode.matchAll(importRegex))
6 | let sourceCodeWithImports = sourceCode
7 | for (const match of matches) {
8 | const importPath = match[1]
9 | const { sources: importedSources, sourceCode: mainSourceCode } = await fetchImport(importPath, sourcePath)
10 |
11 | // Merge the imported sources into the main sources object
12 | Object.assign(sources, importedSources)
13 |
14 | let sourceFileName = importPath.split("/").pop() || importPath
15 |
16 | // if sources[sourceFileName] already exists and the content is the same, then skip, otherwise change the sourceFileName to keep the folder structure but still be a relative path
17 | if (sources[sourceFileName] && sources[sourceFileName].content !== mainSourceCode) {
18 | sourceFileName = importPath.split("/").slice(-2).join("/")
19 | }
20 |
21 | sources[sourceFileName] = {
22 | content: mainSourceCode,
23 | }
24 | sourceCodeWithImports = sourceCode.replace(match[0], `import "${sourceFileName}";`)
25 | }
26 | return { sources, sourceCode: sourceCodeWithImports }
27 | }
28 |
29 | async function fetchImport(importPath: string, sourcePath?: string) {
30 | // Determine the URL to fetch
31 | let urlToFetch: string
32 | if (importPath[0] === "." && sourcePath) {
33 | // If the import path starts with '.', it's a relative path, so resolve the path
34 | const finalPath = resolveImportPath(importPath, sourcePath)
35 | urlToFetch = finalPath
36 | } else if (importPath[0] !== "@") {
37 | // If the import path starts with anything other than '@', use it directly
38 | urlToFetch = importPath
39 | } else {
40 | // Otherwise, convert the import path to an unpkg URL
41 | urlToFetch = `https://unpkg.com/${importPath}`
42 | }
43 | // Convert GitHub URLs to raw content URLs
44 | if (urlToFetch.includes("github.com")) {
45 | urlToFetch = urlToFetch.replace("github.com", "raw.githubusercontent.com").replace("/blob/", "/")
46 | }
47 |
48 | // Fetch the imported file
49 | const response = await fetch(urlToFetch)
50 |
51 | if (!response.ok) {
52 | throw new Error(`HTTP error! status: ${response.status}`)
53 | }
54 |
55 | const importedSource = await response.text()
56 |
57 | // Handle any imports within the fetched source code
58 | const { sources, sourceCode } = await resolveImports(importedSource, urlToFetch)
59 |
60 | return { sources, sourceCode }
61 | }
62 |
63 | /// utility function to handle relative paths
64 | function resolveImportPath(importPath: string, sourcePath: string) {
65 | const importSegments = importPath.split("/")
66 | const sourceSegments = sourcePath.split("/")
67 |
68 | // Remove the last segment (file name) from the source path
69 | sourceSegments.pop()
70 |
71 | // Process each segment of the import path
72 | while (importSegments.length > 0 && importSegments[0] === "..") {
73 | // Remove one directory level for each '..'
74 | sourceSegments.pop()
75 | importSegments.shift()
76 | }
77 |
78 | // Special handling for './'
79 | if (importSegments.length > 0 && importSegments[0] === ".") {
80 | importSegments.shift()
81 | }
82 |
83 | // Reconstruct the final path
84 | return sourceSegments.concat(importSegments).join("/")
85 | }
86 |
87 | export const getContractFileName = (contractName: string): string => {
88 | return `${contractName.replace(/[\/\\:*?"<>|.\s]+$/g, "_")}.sol`
89 | }
90 |
91 | export async function prepareContractSources(contractName: string, sourceCode: string) {
92 | const fileName = getContractFileName(contractName)
93 |
94 | const handleImportsResult = await resolveImports(sourceCode)
95 |
96 | const sources = {
97 | [fileName]: {
98 | content: handleImportsResult?.sourceCode,
99 | },
100 | ...handleImportsResult?.sources,
101 | }
102 |
103 | const sourcesKeys = Object.keys(sources)
104 |
105 | for (const sourceKey of sourcesKeys) {
106 | let sourceCode = sources[sourceKey].content
107 | const importStatements = sourceCode.match(/import\s+["'][^"']+["'];/g) || []
108 |
109 | for (const importStatement of importStatements) {
110 | const importPathMatch = importStatement.match(/["']([^"']+)["']/)
111 | if (!importPathMatch) continue
112 |
113 | const importPath = importPathMatch[1]
114 | const fileName = importPath.split("/").pop() || importPath
115 | sourceCode = sourceCode.replace(importStatement, `import "${fileName}";`)
116 | }
117 |
118 | sources[sourceKey].content = sourceCode
119 | }
120 |
121 | return sources
122 | }
123 |
--------------------------------------------------------------------------------
/lib/solidity/verification.ts:
--------------------------------------------------------------------------------
1 | "server-only"
2 |
3 | import type { Chain } from "viem"
4 |
5 | import { DEFAULT_COMPILER_VERSION, getChainDetails } from "@/lib/config"
6 | import type { VerifyContractParams } from "@/lib/types"
7 |
8 | export const verifyContract = async ({
9 | contractAddress,
10 | standardJsonInput,
11 | encodedConstructorArgs,
12 | fileName,
13 | contractName,
14 | viemChain,
15 | }: VerifyContractParams): Promise<{ status: string; result: string }> => {
16 | const { explorerApiUrl, explorerApiKey } = getChainDetails(viemChain)
17 |
18 | const params = new URLSearchParams()
19 | params.append("module", "contract")
20 | params.append("action", "verifysourcecode")
21 | params.append("contractaddress", contractAddress)
22 | params.append("sourceCode", JSON.stringify(standardJsonInput))
23 | params.append("codeformat", "solidity-standard-json-input")
24 | params.append("contractname", `${fileName}:${contractName}`)
25 | params.append("compilerversion", DEFAULT_COMPILER_VERSION)
26 | if (encodedConstructorArgs) {
27 | params.append("constructorArguements", encodedConstructorArgs)
28 | }
29 | params.append("optimizationUsed", "1")
30 | params.append("runs", "200")
31 | params.append("licenseType", "mit")
32 | params.append("apikey", explorerApiKey)
33 |
34 | const response = await fetch(explorerApiUrl, {
35 | method: "POST",
36 | headers: {
37 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
38 | },
39 | body: params.toString(),
40 | })
41 |
42 | if (!response.ok) {
43 | throw new Error(`Explorer API request failed with status ${response.status}`)
44 | }
45 |
46 | return await response.json()
47 | }
48 |
49 | export const checkVerifyStatus = async (
50 | guid: string,
51 | viemChain: Chain,
52 | ): Promise<{ status: string; result: string }> => {
53 | const { explorerApiUrl, explorerApiKey } = getChainDetails(viemChain)
54 |
55 | const params = new URLSearchParams()
56 | params.append("apikey", explorerApiKey)
57 | params.append("module", "contract")
58 | params.append("action", "checkverifystatus")
59 | params.append("guid", guid)
60 |
61 | const response = await fetch(explorerApiUrl, {
62 | method: "POST",
63 | headers: {
64 | "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
65 | },
66 | body: params.toString(),
67 | })
68 |
69 | if (!response.ok) {
70 | throw new Error(`Explorer API request failed with status ${response.status}`)
71 | }
72 |
73 | return await response.json()
74 | }
75 |
--------------------------------------------------------------------------------
/lib/types.ts:
--------------------------------------------------------------------------------
1 | import type { Message } from "ai"
2 | import type { Abi, Chain, Hash } from "viem"
3 |
4 | export type NextPageProps = {
5 | params: { id: string }
6 | searchParams?: { [key: string]: string | string[] | undefined }
7 | }
8 |
9 | export type DbChat = {
10 | id: string
11 | agentId: string
12 | title: string
13 | createdAt: number
14 | userId: string
15 | messages: Message[]
16 | published: boolean
17 | avatarUrl?: string | null
18 | }
19 |
20 | export type DbChatListItem = {
21 | id: string
22 | agentId: string
23 | createdAt: number
24 | title: string
25 | userId: string
26 | published: boolean
27 | }
28 |
29 | export type Agent = {
30 | id: string
31 | userId: string
32 | name: string
33 | description: string
34 | imageUrl: string
35 | creator: string
36 | }
37 |
38 | export type CreateAgentParams = {
39 | name: string
40 | userId: string
41 | description: string
42 | instructions: string
43 | creator: string
44 | imageUrl: string
45 | }
46 |
47 | export type DeployContractParams = {
48 | chainId: string
49 | contractName: string
50 | sourceCode: string
51 | constructorArgs: Array
52 | }
53 |
54 | export type DeployContractResult = {
55 | contractAddress: Hash
56 | sourceCode: string
57 | explorerUrl: string
58 | ipfsUrl: string
59 | verifyContractConfig: VerifyContractParams
60 | abi: Abi
61 | standardJsonInput: string
62 | }
63 |
64 | export type VerifyContractParams = {
65 | deployHash: Hash
66 | contractAddress: Hash
67 | standardJsonInput: string
68 | encodedConstructorArgs: string
69 | fileName: string
70 | contractName: string
71 | viemChain: Omit
72 | }
73 |
74 | export type LastDeploymentData = DeployContractResult & {
75 | walletAddress: Hash
76 | chainId: number
77 | transactionHash: Hash
78 | }
79 |
80 | export type DeployTokenScriptParams = {
81 | chainId: string
82 | tokenAddress: Hash
83 | tokenScriptSource: string
84 | tokenName: string
85 | ensDomain: string
86 | includeBurnFunction: boolean
87 | }
88 |
89 | export type DeployTokenScriptResult = {
90 | txHash: string
91 | explorerUrl: string
92 | ipfsUrl: string
93 | viewerUrl: string
94 | tokenName: string
95 | ensDomain: string
96 | includeBurnFunction: boolean
97 | }
98 |
99 | export type ChainDetails = {
100 | rpcUrl: string
101 | explorerUrl: string
102 | explorerApiUrl: string
103 | explorerApiKey: string
104 | }
105 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 | import type { Chain, Hash } from "viem"
4 |
5 | import { getChainDetails } from "@/lib/config"
6 |
7 | export const cn = (...inputs: ClassValue[]): string => twMerge(clsx(inputs))
8 |
9 | export const formatDate = (input: string | number | Date): string => {
10 | let date: Date
11 | if (typeof input === "number") {
12 | if (input.toString().length === 10) {
13 | date = new Date(input * 1000)
14 | }
15 | }
16 | if (typeof input === "string") {
17 | date = new Date(input)
18 | }
19 | if (input instanceof Date) {
20 | date = input
21 | } else {
22 | date = new Date()
23 | }
24 |
25 | return date.toLocaleDateString("en-US", {
26 | month: "long",
27 | day: "numeric",
28 | year: "numeric",
29 | })
30 | }
31 |
32 | export const isValidEmail = (email: string): boolean => {
33 | const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
34 | return regex.test(email)
35 | }
36 |
37 | export const getIpfsUrl = (cid: string): string => `${process.env.NEXT_PUBLIC_IPFS_GATEWAY}/ipfs/${cid}`
38 |
39 | export function ensureHashPrefix(bytecode: string | Hash): Hash {
40 | return `0x${bytecode.replace(/^0x/, "")}`
41 | }
42 |
43 | export function getExplorerUrl({
44 | viemChain,
45 | hash,
46 | type,
47 | }: {
48 | viemChain: Chain
49 | hash: Hash
50 | type: "tx" | "address"
51 | }): string {
52 | const { explorerUrl } = getChainDetails(viemChain)
53 | if (!explorerUrl) {
54 | console.error(`No explorer URL found for chainId ${viemChain.id}`)
55 | return ""
56 | }
57 | if (type === "tx") return `${explorerUrl}/tx/${hash}`
58 |
59 | return `${explorerUrl}/address/${hash}`
60 | }
61 |
--------------------------------------------------------------------------------
/lib/viem.ts:
--------------------------------------------------------------------------------
1 | import type { Chain } from "viem"
2 | import * as allViemChains from "viem/chains"
3 |
4 | import { BLOCKSCOUT_URLS } from "@/lib/blockscout"
5 |
6 | export function getChainById(chainId: number) {
7 | for (const chain of Object.values(allViemChains)) {
8 | if (chain.id === chainId) {
9 | return chain
10 | }
11 | }
12 |
13 | throw new Error(`Chain with id ${chainId} not found`)
14 | }
15 |
16 | export const FULL_RPC_URLS: Record = {
17 | 11155111: `https://eth-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
18 | 80002: `https://polygon-amoy.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
19 | 84532: `https://base-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`,
20 | 421614: `https://arb-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_API_KEY}`
21 | }
22 |
23 | export const EXPLORER_API_URLS: Record = {
24 | 11155111: "https://api-sepolia.etherscan.io/api",
25 | 80002: "https://api-amoy.polygonscan.com/api",
26 | 84532: "https://api-sepolia.basescan.org/api",
27 | 1: "https://api.etherscan.io/api",
28 | 17000: "https://api-holesky.etherscan.io/api",
29 | 420: "https://api-goerli.optimistic.etherscan.io/api",
30 | 5003: "https://explorer.sepolia.mantle.xyz/api",
31 | 421614: "https://api-sepolia.arbiscan.io/api"
32 | }
33 |
34 | export const EXPLORER_API_KEYS: Record = {
35 | 11155111: `${process.env.ETHEREUM_EXPLORER_API_KEY}`,
36 | 80002: `${process.env.POLYGON_EXPLORER_API_KEY}`,
37 | 84532: `${process.env.BASE_EXPLORER_API_KEY}`,
38 | 1: `${process.env.ETHEREUM_EXPLORER_API_KEY}`,
39 | 17000: `${process.env.ETHEREUM_EXPLORER_API_KEY}`,
40 | 420: `${process.env.OPTIMISM_EXPLORER_API_KEY}`,
41 | 5003: `${process.env.MANTLE_EXPLORER_API_KEY}`,
42 | 421614: `${process.env.ARBITRUM_EXPLORER_API_KEY}`
43 | }
44 |
45 | type ExplorerDetails = {
46 | url: string
47 | apiUrl: string
48 | apiKey: string
49 | }
50 |
51 | export const getExplorerDetails = (viemChain: Chain): ExplorerDetails => {
52 | const blockscoutUrl = BLOCKSCOUT_URLS[viemChain.id]
53 | if (blockscoutUrl) {
54 | return {
55 | url: `${blockscoutUrl}`,
56 | apiUrl: `${blockscoutUrl}/api`,
57 | apiKey: `${process.env.BLOCKSCOUT_API_KEY}`
58 | }
59 | }
60 |
61 | const viemExplorerUrl = viemChain.blockExplorers?.default.url
62 | const viemApiUrl = EXPLORER_API_URLS[viemChain.id]
63 | const viemApiKey = EXPLORER_API_KEYS[viemChain.id]
64 | if (viemExplorerUrl && viemApiUrl && viemApiKey) {
65 | return {
66 | url: viemExplorerUrl,
67 | apiUrl: viemApiUrl,
68 | apiKey: viemApiKey
69 | }
70 | }
71 |
72 | throw new Error(`Unsupported chain or explorer api. Network: ${viemChain.name} ChainId: ${viemChain.id}`)
73 | }
74 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | export { auth as middleware } from "@/auth"
2 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 |
3 | module.exports = {
4 | webpack: (config) => {
5 | config.externals.push("pino-pretty", "lokijs", "encoding")
6 | return config
7 | },
8 | images: {
9 | remotePatterns: [
10 | {
11 | protocol: "https",
12 | hostname: "**"
13 | }
14 | ]
15 | },
16 | async headers() {
17 | return [
18 | {
19 | source: "/:path*",
20 | headers: [
21 | {
22 | key: "Content-Security-Policy",
23 | value: "frame-ancestors 'self' https://app.safe.global https://*.blockscout.com;"
24 | },
25 | {
26 | key: "Access-Control-Allow-Origin",
27 | value: "*"
28 | },
29 | {
30 | key: "Access-Control-Allow-Methods",
31 | value: "GET, OPTIONS"
32 | },
33 | {
34 | key: "Access-Control-Allow-Headers",
35 | value: "X-Requested-With, content-type, Authorization"
36 | }
37 | ]
38 | }
39 | ]
40 | },
41 | }
42 |
43 |
44 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web3gpt",
3 | "version": "0.2.0",
4 | "license": "MIT",
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "preview": "next build && next start",
10 | "lint": "biome check {app,lib,components}",
11 | "lint:fix": "biome check --write {app,lib,components}"
12 | },
13 | "dependencies": {
14 | "@ai-sdk/openai": "^1.0.11",
15 | "@radix-ui/react-alert-dialog": "1.0.4",
16 | "@radix-ui/react-dialog": "1.0.4",
17 | "@radix-ui/react-dropdown-menu": "^2.1.2",
18 | "@radix-ui/react-label": "^2.1.0",
19 | "@radix-ui/react-select": "^1.2.2",
20 | "@radix-ui/react-separator": "^1.1.0",
21 | "@radix-ui/react-slot": "^1.1.0",
22 | "@radix-ui/react-switch": "^1.1.1",
23 | "@radix-ui/react-tooltip": "^1.1.3",
24 | "@rainbow-me/rainbowkit": "^2.2.0",
25 | "@tanstack/react-query": "^5.61.0",
26 | "@unkey/nextjs": "^0.18.3",
27 | "@unstoppabledomains/resolution": "^9.3.2",
28 | "@vercel/analytics": "^1.4.1",
29 | "@vercel/kv": "^2.0.0",
30 | "@vercel/og": "^0.6.4",
31 | "ai": "^4.0.22",
32 | "class-variance-authority": "^0.7.0",
33 | "clsx": "^2.1.1",
34 | "next": "^14.2.18",
35 | "next-auth": "5.0.0-beta.19",
36 | "next-themes": "^0.4.3",
37 | "openai": "^4.77.0",
38 | "pinata": "^0.4.0",
39 | "react": "^18.3.1",
40 | "react-dom": "^18.3.1",
41 | "react-intersection-observer": "^9.13.1",
42 | "react-lottie-player": "^2.1.0",
43 | "react-markdown": "^8.0.7",
44 | "react-syntax-highlighter": "^15.6.1",
45 | "react-textarea-autosize": "^8.5.5",
46 | "remark-gfm": "^3.0.1",
47 | "remark-math": "^5.1.1",
48 | "server-only": "^0.0.1",
49 | "solc": "0.8.28",
50 | "sonner": "^1.7.0",
51 | "vercel-url": "^0.2.4",
52 | "viem": "^2.21.48",
53 | "wagmi": "^2.13.0",
54 | "zustand": "^5.0.1"
55 | },
56 | "devDependencies": {
57 | "@biomejs/biome": "1.9.4",
58 | "@tailwindcss/typography": "^0.5.15",
59 | "@types/node": "^20.17.10",
60 | "@types/react": "^18.3.18",
61 | "@types/react-dom": "^18.3.5",
62 | "@types/react-syntax-highlighter": "^15.5.13",
63 | "autoprefixer": "^10.4.20",
64 | "postcss": "^8.4.49",
65 | "tailwind-merge": "^2.6.0",
66 | "tailwindcss": "^3.4.17",
67 | "tailwindcss-animate": "^1.0.7",
68 | "typescript": "^5.7.2"
69 | },
70 | "trustedDependencies": [
71 | "@biomejs/biome",
72 | "@parcel/watcher"
73 | ]
74 | }
75 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {}
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/public/assets/agent-factory.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/agent-factory.png
--------------------------------------------------------------------------------
/public/assets/erc20.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/erc20.png
--------------------------------------------------------------------------------
/public/assets/erc721.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/erc721.png
--------------------------------------------------------------------------------
/public/assets/metis-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/metis-logo.png
--------------------------------------------------------------------------------
/public/assets/rootstock.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/rootstock.png
--------------------------------------------------------------------------------
/public/assets/safe-logo.svg:
--------------------------------------------------------------------------------
1 |
7 |
--------------------------------------------------------------------------------
/public/assets/tokenscript.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/tokenscript.png
--------------------------------------------------------------------------------
/public/assets/web3gpt.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/assets/web3gpt.png
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/favicon.ico
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/favicon.png
--------------------------------------------------------------------------------
/public/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Web3GPT",
3 | "short_name": "Web3GPT",
4 | "description": "Deploy smart contracts, create AI Agents, do more onchain with AI.",
5 | "start_url": "/",
6 | "display": "standalone",
7 | "background_color": "#000000",
8 | "theme_color": "#22DA00",
9 | "icons": [
10 | {
11 | "src": "/favicon.ico",
12 | "sizes": "any",
13 | "type": "image/x-icon"
14 | }
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/public/mantle-logo.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/mantle-logo.jpeg
--------------------------------------------------------------------------------
/public/polygon-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/web3-gpt/web3gpt/6b60b15f35a7018e89fcdae3bb37493b91a4fa9e/public/polygon-logo.png
--------------------------------------------------------------------------------
/public/web3gpt-logo-beta.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/web3gpt-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss"
2 | import { fontFamily } from "tailwindcss/defaultTheme"
3 | import tailwindAnimate from "tailwindcss-animate"
4 | import typographyPlugin from "@tailwindcss/typography"
5 |
6 | const config: Config = {
7 | darkMode: ["class"],
8 | content: ["./components/**/*.{ts,tsx}", "./app/**/*.{ts,tsx}", "./lib/**/*.{ts,tsx}"],
9 | prefix: "",
10 | theme: {
11 | container: {
12 | center: true,
13 | padding: "2rem",
14 | screens: {
15 | "2xl": "1400px"
16 | }
17 | },
18 | extend: {
19 | fontFamily: {
20 | sans: ["var(--font-sans)", ...fontFamily.sans],
21 | mono: ["var(--font-mono)", ...fontFamily.mono]
22 | },
23 | colors: {
24 | border: "hsl(var(--border))",
25 | input: "hsl(var(--input))",
26 | ring: "hsl(var(--ring))",
27 | background: "hsl(var(--background))",
28 | foreground: "hsl(var(--foreground))",
29 | primary: {
30 | DEFAULT: "hsl(var(--primary))",
31 | foreground: "hsl(var(--primary-foreground))"
32 | },
33 | secondary: {
34 | DEFAULT: "hsl(var(--secondary))",
35 | foreground: "hsl(var(--secondary-foreground))"
36 | },
37 | destructive: {
38 | DEFAULT: "hsl(var(--destructive))",
39 | foreground: "hsl(var(--destructive-foreground))"
40 | },
41 | muted: {
42 | DEFAULT: "hsl(var(--muted))",
43 | foreground: "hsl(var(--muted-foreground))"
44 | },
45 | accent: {
46 | DEFAULT: "hsl(var(--accent))",
47 | foreground: "hsl(var(--accent-foreground))"
48 | },
49 | popover: {
50 | DEFAULT: "hsl(var(--popover))",
51 | foreground: "hsl(var(--popover-foreground))"
52 | },
53 | card: {
54 | DEFAULT: "hsl(var(--card))",
55 | foreground: "hsl(var(--card-foreground))"
56 | }
57 | },
58 | borderRadius: {
59 | lg: "var(--radius)",
60 | md: "calc(var(--radius) - 2px)",
61 | sm: "calc(var(--radius) - 4px)"
62 | },
63 | keyframes: {
64 | "accordion-down": {
65 | from: { height: "0" },
66 | to: { height: "var(--radix-accordion-content-height)" }
67 | },
68 | "accordion-up": {
69 | from: { height: "var(--radix-accordion-content-height)" },
70 | to: { height: "0" }
71 | },
72 | "slide-from-left": {
73 | "0%": {
74 | transform: "translateX(-100%)"
75 | },
76 | "100%": {
77 | transform: "translateX(0)"
78 | }
79 | },
80 | "slide-to-left": {
81 | "0%": {
82 | transform: "translateX(0)"
83 | },
84 | "100%": {
85 | transform: "translateX(-100%)"
86 | }
87 | }
88 | },
89 | animation: {
90 | "slide-from-left": "slide-from-left 0.3s cubic-bezier(0.82, 0.085, 0.395, 0.895)",
91 | "slide-to-left": "slide-to-left 0.25s cubic-bezier(0.82, 0.085, 0.395, 0.895)",
92 | "accordion-down": "accordion-down 0.2s ease-out",
93 | "accordion-up": "accordion-up 0.2s ease-out"
94 | }
95 | }
96 | },
97 | plugins: [tailwindAnimate, typographyPlugin]
98 | }
99 |
100 | export default config
101 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | // Enable latest features
4 | "lib": ["ESNext", "DOM"],
5 | "target": "ESNext",
6 | "module": "ESNext",
7 | "moduleDetection": "force",
8 | "jsx": "preserve",
9 | "allowJs": true,
10 |
11 | // Bundler mode
12 | "moduleResolution": "bundler",
13 | "allowImportingTsExtensions": true,
14 | "verbatimModuleSyntax": true,
15 | "noEmit": true,
16 |
17 | // Best practices
18 | "strict": true,
19 | "skipLibCheck": true,
20 | "noFallthroughCasesInSwitch": true,
21 |
22 | // Some stricter flags
23 | "forceConsistentCasingInFileNames": true,
24 | "noUnusedLocals": false,
25 | "noUnusedParameters": false,
26 | "noPropertyAccessFromIndexSignature": false,
27 | "strictNullChecks": true,
28 |
29 | // other
30 | "incremental": true,
31 | "esModuleInterop": true,
32 | "resolveJsonModule": true,
33 | "isolatedModules": true,
34 | "baseUrl": ".",
35 | "paths": {
36 | "@/*": ["./*"]
37 | },
38 | "plugins": [
39 | {
40 | "name": "next"
41 | }
42 | ]
43 | },
44 | "include": [
45 | "next-env.d.ts",
46 | "next-auth.d.ts",
47 | "**/*.ts",
48 | "**/*.tsx",
49 | ".next/types/**/*.ts",
50 | "components/ui/.d.ts",
51 | "lib/solc.d.ts",
52 | "next.config.js"
53 | ],
54 | "exclude": ["node_modules", ".next"]
55 | }
56 |
--------------------------------------------------------------------------------
/vercel.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": {
3 | "app/api/**/*.ts": {
4 | "maxDuration": 300
5 | }
6 | },
7 | "crons": [
8 | {
9 | "path": "/api/cron",
10 | "schedule": "* * * * *"
11 | }
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------