├── .npmrc ├── .vscode └── settings.json ├── styles └── globals.css ├── public ├── favicon.ico ├── vercel.svg └── next.svg ├── next.config.js ├── postcss.config.js ├── types ├── index.ts ├── inscription.ts └── offer.ts ├── .env ├── .eslintignore ├── config ├── fonts.ts ├── site.ts └── table.ts ├── components ├── counter.tsx ├── primitives.ts ├── notification.tsx ├── connect.tsx ├── home │ └── listedinscriptions.tsx ├── navbar.tsx ├── incriptioncard.tsx ├── theme-switch.tsx └── icons.tsx ├── utils ├── offer.ts └── toast.ts ├── .gitignore ├── tailwind.config.js ├── app ├── page.tsx ├── error.tsx ├── layout.tsx └── inscription │ └── [slug] │ └── page.tsx ├── api ├── unisat.ts ├── list.ts ├── offer.ts └── inscription.ts ├── contexts ├── providers.tsx └── connectioncontext.tsx ├── tsconfig.json ├── LICENSE ├── package.json ├── .eslintrc.json └── README.md /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gigi0500/BRC-2.0-marketplace/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {} 3 | 4 | module.exports = nextConfig 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /types/index.ts: -------------------------------------------------------------------------------- 1 | import { SVGProps } from "react"; 2 | 3 | export type IconSvgProps = SVGProps & { 4 | size?: number; 5 | }; 6 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_NETWORK=testnet 2 | NEXT_PUBLIC_UNISAT_API_KEY=cd5ea3512126785b5f69d00f482b2f51e74694d6eb27d4fb8bf20cc59f45b500 3 | NEXT_PUBLIC_BACEEND_URL=http://localhost:8080 4 | NEXT_PUBLIC_MARKETPLACE_FEE=5 5 | -------------------------------------------------------------------------------- /types/inscription.ts: -------------------------------------------------------------------------------- 1 | export interface IInscription { 2 | address: string; 3 | pubkey: string; 4 | inscriptionId: string; 5 | inscriptionNumber: number; 6 | content: string; 7 | price: number; 8 | tokenTicker: string; 9 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .now/* 2 | *.css 3 | .changeset 4 | dist 5 | esm/* 6 | public/* 7 | tests/* 8 | scripts/* 9 | *.config.js 10 | .DS_Store 11 | node_modules 12 | coverage 13 | .next 14 | build 15 | !.commitlintrc.cjs 16 | !.lintstagedrc.cjs 17 | !jest.config.js 18 | !plopfile.js 19 | !react-shim.js 20 | !tsup.config.ts -------------------------------------------------------------------------------- /config/fonts.ts: -------------------------------------------------------------------------------- 1 | import { Fira_Code as FontMono, Inter as FontSans } from "next/font/google"; 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-sans", 6 | }); 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ["latin"], 10 | variable: "--font-mono", 11 | }); 12 | -------------------------------------------------------------------------------- /config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig; 2 | 3 | export const siteConfig = { 4 | name: "BRC20-Marketplace", 5 | description: "You can buy Ordinals using BRC20 tokens.", 6 | navItems: [ 7 | { 8 | label: "Home", 9 | href: "/", 10 | }, 11 | ], 12 | }; 13 | 14 | export const marketplace_fee = 5; 15 | -------------------------------------------------------------------------------- /components/counter.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import { Button } from "@nextui-org/button"; 5 | 6 | export const Counter = () => { 7 | const [count, setCount] = useState(0); 8 | 9 | return ( 10 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /utils/offer.ts: -------------------------------------------------------------------------------- 1 | import { IOffer } from "@/types/offer" 2 | import { IOfferForTable } from "@/types/offer"; 3 | 4 | export const getOfferDataForTable = (offers: any): IOfferForTable[] => { 5 | const res: IOfferForTable[] = offers.map((offer: any) =>{ 6 | return { 7 | key: offer. _id, 8 | price: offer.price, 9 | token: offer.tokenTicker, 10 | from: offer.buyerAddress, 11 | status: offer.status, 12 | }; 13 | }); 14 | 15 | return res; 16 | } -------------------------------------------------------------------------------- /.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 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | import {nextui} from '@nextui-org/theme' 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | content: [ 6 | './components/**/*.{js,ts,jsx,tsx,mdx}', 7 | './app/**/*.{js,ts,jsx,tsx,mdx}', 8 | './node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}' 9 | ], 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | sans: ["var(--font-sans)"], 14 | mono: ["var(--font-geist-mono)"], 15 | }, 16 | }, 17 | }, 18 | darkMode: "class", 19 | plugins: [nextui()], 20 | } 21 | -------------------------------------------------------------------------------- /types/offer.ts: -------------------------------------------------------------------------------- 1 | export interface IOffer { 2 | _id: string; 3 | inscriptionId: string; 4 | sellerAddress: string; 5 | buyerAddress: string; 6 | price: number; 7 | tokenTicker: string; 8 | psbt: string; 9 | status: number; 10 | buyerSignedPsbt: string; 11 | } 12 | 13 | export interface IOfferForTable { 14 | key: string; 15 | price: number; 16 | token: string; 17 | from: string; 18 | status: number; 19 | } 20 | 21 | export interface IOfferForTableMe { 22 | key: string; 23 | price: number; 24 | token: string; 25 | from: string; 26 | action: string; 27 | } -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@nextui-org/link"; 2 | import { Snippet } from "@nextui-org/snippet"; 3 | import { Code } from "@nextui-org/code"; 4 | import { button as buttonStyles } from "@nextui-org/theme"; 5 | 6 | import { siteConfig } from "@/config/site"; 7 | import { title, subtitle } from "@/components/primitives"; 8 | import { GithubIcon } from "@/components/icons"; 9 | import ListedInscriptions from "@/components/home/listedinscriptions"; 10 | 11 | export default function Home() { 12 | return ( 13 |
14 | 15 |
16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /app/error.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | 5 | export default function Error({ 6 | error, 7 | reset, 8 | }: { 9 | error: Error; 10 | reset: () => void; 11 | }) { 12 | useEffect(() => { 13 | // Log the error to an error reporting service 14 | /* eslint-disable no-console */ 15 | console.error(error); 16 | }, [error]); 17 | 18 | return ( 19 |
20 |

Something went wrong!

21 | 29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /config/table.ts: -------------------------------------------------------------------------------- 1 | export const offerTableColumns = [ 2 | { 3 | key: "price", 4 | label: "PRICE", 5 | }, 6 | { 7 | key: "token", 8 | label: "TOKEN", 9 | }, 10 | { 11 | key: "from", 12 | label: "FROM", 13 | }, 14 | { 15 | key: "status", 16 | label: "STATUS", 17 | }, 18 | ]; 19 | 20 | export const offerTableColumnsMe = [ 21 | { 22 | key: "price", 23 | label: "PRICE", 24 | }, 25 | { 26 | key: "token", 27 | label: "TOKEN", 28 | }, 29 | { 30 | key: "from", 31 | label: "FROM", 32 | }, 33 | { 34 | key: "status", 35 | label: "STATUS", 36 | }, 37 | { 38 | key: "action", 39 | label: "ACTION", 40 | }, 41 | 42 | ]; 43 | -------------------------------------------------------------------------------- /api/unisat.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | 3 | const unisat_api_key = process.env.NEXT_PUBLIC_UNISAT_API_KEY; 4 | 5 | export const getTokenBalanceByAddressTicker = async ( 6 | address: string, 7 | ticker: string 8 | ) => { 9 | const url = `https://open-api-testnet.unisat.io/v1/indexer/address/${address}/brc20/${ticker}/info`; 10 | const config = { 11 | headers: { 12 | Authorization: `Bearer ${unisat_api_key}`, 13 | }, 14 | }; 15 | 16 | try { 17 | const res = await axios.get(url, config); 18 | if (res.data.msg && res.data.msg == "ok") return res.data.data.availableBalance; 19 | return 0; 20 | } catch (error) { 21 | console.log("Balance Error => ", error); 22 | return 0; 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /contexts/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { NextUIProvider } from "@nextui-org/system"; 5 | import { useRouter } from "next/navigation"; 6 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 7 | import { ThemeProviderProps } from "next-themes/dist/types"; 8 | 9 | export interface ProvidersProps { 10 | children: React.ReactNode; 11 | themeProps?: ThemeProviderProps; 12 | } 13 | 14 | export function Providers({ children, themeProps }: ProvidersProps) { 15 | const router = useRouter(); 16 | 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /api/list.ts: -------------------------------------------------------------------------------- 1 | import { IInscription } from "@/types/inscription"; 2 | import axios, { AxiosError } from "axios"; 3 | 4 | const unisat_api_key = process.env.NEXT_PUBLIC_UNISAT_API_KEY; 5 | const backend_api_base_url = process.env.NEXT_PUBLIC_BACEEND_URL; 6 | 7 | export const listInscription = async (inscription: IInscription) => { 8 | const url = `${backend_api_base_url}/api/inscription/list`; 9 | 10 | try { 11 | await axios.post(url, inscription) 12 | return true; 13 | } catch (error) { 14 | return false; 15 | } 16 | } 17 | 18 | export const unlistInscription = async (inscriptionId: string) => { 19 | const url = `${backend_api_base_url}/api/inscription/unlist`; 20 | try { 21 | await axios.post(url, {inscriptionId}) 22 | return true; 23 | } catch (error) { 24 | return false; 25 | } 26 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Next UI 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. 22 | -------------------------------------------------------------------------------- /utils/toast.ts: -------------------------------------------------------------------------------- 1 | import { toast } from "react-toastify"; 2 | 3 | export const toaster = (type: string, msg: string) => { 4 | switch (type) { 5 | case "success": 6 | toast.success(msg, { 7 | position: "top-right", 8 | autoClose: false, 9 | hideProgressBar: false, 10 | closeOnClick: false, 11 | pauseOnHover: true, 12 | draggable: true, 13 | progress: undefined, 14 | theme: "dark", 15 | }); 16 | break; 17 | case "error": 18 | toast.error(msg, { 19 | position: "top-right", 20 | autoClose: 5000, 21 | hideProgressBar: false, 22 | closeOnClick: true, 23 | pauseOnHover: true, 24 | draggable: true, 25 | progress: undefined, 26 | theme: "dark", 27 | }); 28 | break; 29 | case "info": 30 | toast.info(msg, { 31 | position: "top-right", 32 | autoClose: 5000, 33 | hideProgressBar: false, 34 | closeOnClick: true, 35 | pauseOnHover: true, 36 | draggable: true, 37 | progress: undefined, 38 | theme: "dark", 39 | }); 40 | break; 41 | default: 42 | break; 43 | } 44 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/primitives.ts: -------------------------------------------------------------------------------- 1 | import { tv } from "tailwind-variants"; 2 | 3 | export const title = tv({ 4 | base: "tracking-tight inline font-semibold", 5 | variants: { 6 | color: { 7 | violet: "from-[#FF1CF7] to-[#b249f8]", 8 | yellow: "from-[#FF705B] to-[#FFB457]", 9 | blue: "from-[#5EA2EF] to-[#0072F5]", 10 | cyan: "from-[#00b7fa] to-[#01cfea]", 11 | green: "from-[#6FEE8D] to-[#17c964]", 12 | pink: "from-[#FF72E1] to-[#F54C7A]", 13 | foreground: "dark:from-[#FFFFFF] dark:to-[#4B4B4B]", 14 | }, 15 | size: { 16 | sm: "text-3xl lg:text-4xl", 17 | md: "text-[2.3rem] lg:text-5xl leading-9", 18 | lg: "text-4xl lg:text-6xl", 19 | }, 20 | fullWidth: { 21 | true: "w-full block", 22 | }, 23 | }, 24 | defaultVariants: { 25 | size: "md", 26 | }, 27 | compoundVariants: [ 28 | { 29 | color: [ 30 | "violet", 31 | "yellow", 32 | "blue", 33 | "cyan", 34 | "green", 35 | "pink", 36 | "foreground", 37 | ], 38 | class: "bg-clip-text text-transparent bg-gradient-to-b", 39 | }, 40 | ], 41 | }); 42 | 43 | export const subtitle = tv({ 44 | base: "w-full md:w-1/2 my-2 text-lg lg:text-xl text-default-600 block max-w-full", 45 | variants: { 46 | fullWidth: { 47 | true: "!w-full", 48 | }, 49 | }, 50 | defaultVariants: { 51 | fullWidth: true, 52 | }, 53 | }); 54 | -------------------------------------------------------------------------------- /components/notification.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | 3 | interface NotificationProps { 4 | name: string; 5 | action: string; 6 | action_occurred: string; 7 | sub_action?: string; 8 | id: number; 9 | className: string; 10 | isRead: boolean; 11 | imagePath: string; 12 | subImagePath: string; 13 | 14 | handleClick: (id: number) => void; 15 | } 16 | 17 | export const Notification = (props: NotificationProps) => { 18 | const { 19 | name, 20 | action, 21 | action_occurred, 22 | sub_action, 23 | isRead, 24 | id, 25 | imagePath, 26 | subImagePath, 27 | handleClick, 28 | } = props; 29 | 30 | return ( 31 |
handleClick(id)} 33 | className={isRead ? "notification-wrapper read" : "notification-wrapper"} 34 | > 35 | {name} 36 |
37 | {" "} 38 |
39 | {name}{" "} 40 | {action}{" "} 41 | {!isRead ? : null} 42 |
43 |
{action_occurred}
44 | {sub_action ?
{sub_action}
: null} 45 |
46 | {subImagePath ? ( 47 | {name} 54 | ) : null} 55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /components/connect.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useContext } from "react"; 4 | import { 5 | User, 6 | Dropdown, 7 | DropdownTrigger, 8 | DropdownMenu, 9 | DropdownItem, 10 | Button, 11 | } from "@nextui-org/react"; 12 | 13 | import { ConnectionContext } from "@/contexts/connectioncontext"; 14 | 15 | 16 | const Connect = () => { 17 | const { 18 | isConnected, 19 | currentAccount, 20 | balance, 21 | connectWallet, 22 | disconnectWallet, 23 | network, 24 | } = useContext(ConnectionContext); 25 | 26 | const copyToClipboard = (text: string) => { 27 | navigator.clipboard.writeText(text).then(() => { 28 | // message.success('Address copied to clipboard'); 29 | }); 30 | }; 31 | 32 | return ( 33 |
34 | {isConnected ? ( 35 | 36 | 37 | 44 | 45 | 46 | {currentAccount} 47 | Disconnect 48 | 49 | 50 | ) : ( 51 | 52 | )} 53 |
54 | ); 55 | }; 56 | 57 | export default Connect; 58 | -------------------------------------------------------------------------------- /components/home/listedinscriptions.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useContext } from "react"; 4 | import { Switch } from "@nextui-org/switch"; 5 | import { getListedInscriptions, getInscriptions } from "@/api/inscription"; 6 | import InscriptionCard from "../incriptioncard"; 7 | import { IInscription } from "@/types/inscription"; 8 | import { ConnectionContext } from "@/contexts/connectioncontext"; 9 | 10 | const ListedInscriptions = () => { 11 | const { currentAccount } = useContext(ConnectionContext); 12 | const [inscriptions, setInscriptions] = useState([]); 13 | const [isMine, setIsMine] = React.useState(false); 14 | 15 | useEffect(() => { 16 | getAllInscriptions(); 17 | }, []); 18 | 19 | useEffect(() => { 20 | getAllInscriptions(); 21 | }, [isMine]); 22 | 23 | const getAllInscriptions = async () => { 24 | let inscriptions: IInscription[] = []; 25 | if (isMine) inscriptions = await getInscriptions(currentAccount); 26 | else inscriptions = await getListedInscriptions(); 27 | 28 | console.log("xxxxx =>", inscriptions); 29 | setInscriptions(inscriptions); 30 | }; 31 | 32 | return ( 33 |
34 |
35 | 36 | Mine 37 | 38 |
39 |
40 | {inscriptions.map((inscription) => ( 41 | 42 | ))} 43 |
44 |
45 | ); 46 | }; 47 | 48 | export default ListedInscriptions; 49 | -------------------------------------------------------------------------------- /components/navbar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | User, 3 | link as linkStyles, 4 | Input, 5 | Link, 6 | Kbd, 7 | Button, 8 | Navbar as NextUINavbar, 9 | NavbarContent, 10 | NavbarMenu, 11 | NavbarMenuToggle, 12 | NavbarBrand, 13 | NavbarItem, 14 | NavbarMenuItem, 15 | } from "@nextui-org/react"; 16 | import NextLink from "next/link"; 17 | import clsx from "clsx"; 18 | import { siteConfig } from "@/config/site"; 19 | import { SearchIcon, Logo } from "@/components/icons"; 20 | import Connect from "./connect"; 21 | 22 | export const Navbar = () => { 23 | 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 |

ACME

31 |
32 |
33 |
    34 | {siteConfig.navItems.map((item) => ( 35 | 36 | 44 | {item.label} 45 | 46 | 47 | ))} 48 |
49 |
50 | 51 | 52 | 53 | 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-app-template", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint . --ext .ts,.tsx -c .eslintrc.json --fix" 10 | }, 11 | "dependencies": { 12 | "@nextui-org/button": "^2.0.31", 13 | "@nextui-org/code": "^2.0.27", 14 | "@nextui-org/input": "^2.1.21", 15 | "@nextui-org/kbd": "^2.0.28", 16 | "@nextui-org/link": "^2.0.29", 17 | "@nextui-org/navbar": "^2.0.30", 18 | "@nextui-org/react": "^2.3.6", 19 | "@nextui-org/snippet": "^2.0.35", 20 | "@nextui-org/switch": "^2.0.28", 21 | "@nextui-org/system": "2.1.0", 22 | "@nextui-org/theme": "2.2.0", 23 | "@react-aria/ssr": "^3.9.2", 24 | "@react-aria/visually-hidden": "^3.8.10", 25 | "@types/node": "20.5.7", 26 | "@types/react": "18.3.2", 27 | "@types/react-dom": "18.3.0", 28 | "@typescript-eslint/eslint-plugin": "^7.10.0", 29 | "@typescript-eslint/parser": "^7.10.0", 30 | "autoprefixer": "10.4.19", 31 | "axios": "^1.7.2", 32 | "bitcoinjs-lib": "^6.1.5", 33 | "clsx": "^2.0.0", 34 | "dotenv": "^16.4.5", 35 | "eslint": "^8.56.0", 36 | "eslint-config-next": "14.2.1", 37 | "eslint-config-prettier": "^8.2.0", 38 | "eslint-plugin-import": "^2.26.0", 39 | "eslint-plugin-jsx-a11y": "^6.4.1", 40 | "eslint-plugin-node": "^11.1.0", 41 | "eslint-plugin-prettier": "^5.1.3", 42 | "eslint-plugin-react": "^7.23.2", 43 | "eslint-plugin-react-hooks": "^4.6.0", 44 | "eslint-plugin-unused-imports": "^3.2.0", 45 | "framer-motion": "^11.1.1", 46 | "intl-messageformat": "^10.5.0", 47 | "next": "14.2.3", 48 | "next-themes": "^0.2.1", 49 | "postcss": "8.4.38", 50 | "react": "18.3.1", 51 | "react-dom": "18.3.1", 52 | "react-hot-toast": "^2.4.1", 53 | "react-toastify": "^10.0.5", 54 | "tailwind-variants": "^0.1.20", 55 | "tailwindcss": "3.4.3", 56 | "typescript": "5.0.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/incriptioncard.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useContext } from "react"; 4 | import { 5 | Card, 6 | CardHeader, 7 | CardBody, 8 | CardFooter, 9 | Image, 10 | Divider, 11 | Chip, 12 | } from "@nextui-org/react"; 13 | import { ConnectionContext } from "@/contexts/connectioncontext"; 14 | import { useRouter } from "next/navigation"; 15 | import { IInscription } from "@/types/inscription"; 16 | 17 | const InscriptionCard = ({ inscription }: { inscription: IInscription }) => { 18 | const { currentAccount } = useContext(ConnectionContext); 19 | 20 | const router = useRouter(); 21 | 22 | useEffect(() => {}, []); 23 | 24 | return ( 25 | { 29 | router.push(`../inscription/${inscription.inscriptionId}`); 30 | }} 31 | > 32 | 33 |

#{inscription.inscriptionNumber}

34 |
35 | 36 | 37 | Card background 43 | 44 | 45 | 46 |
47 | {(currentAccount === inscription.address && inscription.price == 0) && ( 48 | Unlisted 49 | )} 50 | {inscription.price != 0 && ( 51 | <> 52 |

{inscription.price}

53 |

54 | {inscription.tokenTicker} 55 |

56 | 57 | )} 58 |
59 |
60 |
61 | ); 62 | }; 63 | 64 | export default InscriptionCard; 65 | -------------------------------------------------------------------------------- /components/theme-switch.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { FC } from "react"; 4 | import { VisuallyHidden } from "@react-aria/visually-hidden"; 5 | import { SwitchProps, useSwitch } from "@nextui-org/switch"; 6 | import { useTheme } from "next-themes"; 7 | import { useIsSSR } from "@react-aria/ssr"; 8 | import clsx from "clsx"; 9 | 10 | import { SunFilledIcon, MoonFilledIcon } from "@/components/icons"; 11 | 12 | export interface ThemeSwitchProps { 13 | className?: string; 14 | classNames?: SwitchProps["classNames"]; 15 | } 16 | 17 | export const ThemeSwitch: FC = ({ 18 | className, 19 | classNames, 20 | }) => { 21 | const { theme, setTheme } = useTheme(); 22 | const isSSR = useIsSSR(); 23 | 24 | const onChange = () => { 25 | theme === "light" ? setTheme("dark") : setTheme("light"); 26 | }; 27 | 28 | const { 29 | Component, 30 | slots, 31 | isSelected, 32 | getBaseProps, 33 | getInputProps, 34 | getWrapperProps, 35 | } = useSwitch({ 36 | isSelected: theme === "light" || isSSR, 37 | "aria-label": `Switch to ${theme === "light" || isSSR ? "dark" : "light"} mode`, 38 | onChange, 39 | }); 40 | 41 | return ( 42 | 51 | 52 | 53 | 54 |
73 | {!isSelected || isSSR ? ( 74 | 75 | ) : ( 76 | 77 | )} 78 |
79 |
80 | ); 81 | }; 82 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | import { Metadata, Viewport } from "next"; 3 | import { Link } from "@nextui-org/link"; 4 | 5 | import clsx from "clsx"; 6 | import { Providers } from "@/contexts/providers"; 7 | import ConnectionProvider from "@/contexts/connectioncontext"; 8 | 9 | import { siteConfig } from "@/config/site"; 10 | import { fontSans } from "@/config/fonts"; 11 | import { Navbar } from "@/components/navbar"; 12 | 13 | 14 | export const metadata: Metadata = { 15 | title: { 16 | default: siteConfig.name, 17 | template: `%s - ${siteConfig.name}`, 18 | }, 19 | description: siteConfig.description, 20 | icons: { 21 | icon: "/favicon.ico", 22 | }, 23 | }; 24 | 25 | export const viewport: Viewport = { 26 | themeColor: [ 27 | { media: "(prefers-color-scheme: light)", color: "white" }, 28 | { media: "(prefers-color-scheme: dark)", color: "black" }, 29 | ], 30 | }; 31 | 32 | export default function RootLayout({ 33 | children, 34 | }: { 35 | children: React.ReactNode; 36 | }) { 37 | return ( 38 | 39 | 40 | 46 | 47 | 48 |
49 | 50 |
51 | {children} 52 |
53 |
54 | 60 | Powered by 61 |

NextUI

62 | 63 |
64 |
65 |
66 |
67 | 68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /api/offer.ts: -------------------------------------------------------------------------------- 1 | import { IOffer } from "@/types/offer"; 2 | import axios, { AxiosError } from "axios"; 3 | 4 | const unisat_api_key = process.env.NEXT_PUBLIC_UNISAT_API_KEY; 5 | const backend_api_base_url = process.env.NEXT_PUBLIC_BACEEND_URL; 6 | 7 | 8 | export const getOffersByInscriptionId = async (inscriptionId: string) => { 9 | const url = `${backend_api_base_url}/api/offer/all${inscriptionId}`; 10 | try { 11 | const res = await axios.get(url); 12 | return res.data.offers; 13 | } catch (error) { 14 | return []; 15 | } 16 | }; 17 | 18 | export const requestOffer = async ( 19 | offer: IOffer 20 | ) => { 21 | const url = `${backend_api_base_url}/api/offer/add`; 22 | try { 23 | const res = await axios.post(url, offer); 24 | return true; 25 | } catch (error) { 26 | return false; 27 | } 28 | }; 29 | 30 | export const requestPsbt = async ( 31 | inscriptionId: string, 32 | brcInscriptionId: string, 33 | fee_brcInscriptionId: string, 34 | buyerPubkey: string, 35 | buyerAddress: string, 36 | sellerPubkey: string, 37 | sellerAddress: string 38 | ) => { 39 | const url = `${backend_api_base_url}/api/offer/psbt`; 40 | 41 | try { 42 | const res = await axios.post(url, { 43 | inscriptionId: inscriptionId, 44 | brcInscriptionId: brcInscriptionId, 45 | fee_brcInscriptionId: fee_brcInscriptionId, 46 | buyerPubkey: buyerPubkey, 47 | buyerAddress: buyerAddress, 48 | sellerPubkey: sellerPubkey, 49 | sellerAddress: sellerAddress 50 | }); 51 | console.log("psbt =>", res.data.psbt); 52 | return res.data.psbt; 53 | } catch(error) { 54 | return ""; 55 | } 56 | } 57 | 58 | export const acceptOffer = async (inscriptionId: string, psbt: string, buyerSignedPsbt: string ,signedPsbt: string) => { 59 | const url = `${backend_api_base_url}/api/offer/accept`; 60 | try { 61 | await axios.post(url, { 62 | inscriptionId: inscriptionId, 63 | psbt: psbt, 64 | buyerSignedPsbt: buyerSignedPsbt, 65 | signedPsbt: signedPsbt 66 | }); 67 | return true; 68 | } catch (error) { 69 | return false; 70 | } 71 | }; 72 | 73 | export const rejectOffer = async (id: string) => { 74 | const url = `${backend_api_base_url}/api/offer/remove/${id}`; 75 | try { 76 | await axios.get(url); 77 | return true; 78 | } catch (error) { 79 | return false; 80 | } 81 | }; 82 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc.json", 3 | "env": { 4 | "browser": false, 5 | "es2021": true, 6 | "node": true 7 | }, 8 | "extends": [ 9 | "plugin:react/recommended", 10 | "plugin:prettier/recommended", 11 | "plugin:react-hooks/recommended", 12 | "plugin:jsx-a11y/recommended" 13 | ], 14 | "plugins": ["react", "unused-imports", "import", "@typescript-eslint", "jsx-a11y", "prettier"], 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 12, 21 | "sourceType": "module" 22 | }, 23 | "settings": { 24 | "react": { 25 | "version": "detect" 26 | } 27 | }, 28 | "rules": { 29 | "no-console": "warn", 30 | "react/prop-types": "off", 31 | "react/jsx-uses-react": "off", 32 | "react/react-in-jsx-scope": "off", 33 | "react-hooks/exhaustive-deps": "off", 34 | "jsx-a11y/click-events-have-key-events": "warn", 35 | "jsx-a11y/interactive-supports-focus": "warn", 36 | "prettier/prettier": "warn", 37 | "no-unused-vars": "off", 38 | "unused-imports/no-unused-vars": "off", 39 | "unused-imports/no-unused-imports": "warn", 40 | "@typescript-eslint/no-unused-vars": [ 41 | "warn", 42 | { 43 | "args": "after-used", 44 | "ignoreRestSiblings": false, 45 | "argsIgnorePattern": "^_.*?$" 46 | } 47 | ], 48 | "import/order": [ 49 | "warn", 50 | { 51 | "groups": [ 52 | "type", 53 | "builtin", 54 | "object", 55 | "external", 56 | "internal", 57 | "parent", 58 | "sibling", 59 | "index" 60 | ], 61 | "pathGroups": [ 62 | { 63 | "pattern": "~/**", 64 | "group": "external", 65 | "position": "after" 66 | } 67 | ], 68 | "newlines-between": "always" 69 | } 70 | ], 71 | "react/self-closing-comp": "warn", 72 | "react/jsx-sort-props": [ 73 | "warn", 74 | { 75 | "callbacksLast": true, 76 | "shorthandFirst": true, 77 | "noSortAlphabetically": false, 78 | "reservedFirst": true 79 | } 80 | ], 81 | "padding-line-between-statements": [ 82 | "warn", 83 | {"blankLine": "always", "prev": "*", "next": "return"}, 84 | {"blankLine": "always", "prev": ["const", "let", "var"], "next": "*"}, 85 | { 86 | "blankLine": "any", 87 | "prev": ["const", "let", "var"], 88 | "next": ["const", "let", "var"] 89 | } 90 | ] 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🌐 BRC 2.0 Future Marketplace 🚀 2 | 3 | Welcome to **BRC 2.0 Marketplace**, the **next-generation platform** for seamless trading, minting, and managing BRC20 tokens on the blockchain. Whether you're an investor, developer, or enthusiast, we've got you covered with cutting-edge features and a robust, user-friendly interface. 4 | 5 | --- 6 | 7 | ## 📞 Contact Us 8 | 9 | For inquiries or support, feel free to reach out: 10 | ### **Telegram**: [Gigi](https://t.me/gigi0500) 11 | 12 | --- 13 | 14 | ## 🌟 Key Features 15 | 16 | ### ⚡ **Instant Token Trading** 17 | - Buy and sell BRC20 tokens in real-time. 18 | - Experience fast transaction processing and minimal fees. 19 | 20 | ### 🎨 **Token Minting** 21 | - Easily create your own BRC20 tokens with a few clicks. 22 | - Customize token attributes to match your unique vision. 23 | 24 | ### 📈 **Analytics Dashboard** 25 | - Real-time market trends, trading volume, and token insights. 26 | - Track performance and make informed decisions. 27 | 28 | ### 🔒 **Secure Platform** 29 | - Built with the latest security standards. 30 | - Multi-layer encryption and wallet integration for safe trading. 31 | 32 | ### 🛠️ **Developer-Friendly** 33 | - Open APIs and detailed documentation for seamless integration. 34 | - Customizable SDKs to extend functionality for your projects. 35 | 36 | --- 37 | 38 | ## 🛑 Why Choose BRC20 Marketplace? 39 | 40 | 1. **Investor Confidence**: Tailored for growth and scalability, ensuring a solid ROI for investors. 41 | 2. **Client Satisfaction**: A sleek, intuitive interface designed with user experience in mind. 42 | 3. **Developer Flexibility**: Built for collaboration, customization, and innovation. 43 | 44 | --- 45 | 46 | ## 📜 Tech Stack 47 | 48 | - **Blockchain**: Bitcoin Layer1, BRC20 Protocol 49 | - **Frontend**: React.js, Next.js, Bitcoinjs-lib, Runelib 50 | - **Backend**: Node.js, Express, Bitcoinjs-lib, Runelib 51 | - **Database**: MongoDB 52 | - **Wallet Integration**: MetaMask, WalletConnect 53 | - **Hosting**: AWS, IPFS (for decentralized asset storage) 54 | 55 | --- 56 | 57 | ## 🚀 Get Started 58 | 59 | ### 🔧 Prerequisites 60 | 1. Install [Node.js](https://nodejs.org/). 61 | 2. Set up a BRC20-compatible wallet (e.g., MetaMask). 62 | 63 | ### 🛠️ Installation 64 | 1. Clone the repository: 65 | ```bash 66 | git clone https://github.com/btcwhiz/brc20-marketplace.git 67 | cd brc20-marketplace 68 | ``` 69 | 2. Install dependencies: 70 | ```bash 71 | npm install 72 | ``` 73 | 3. Start the development server: 74 | ```bash 75 | npm run dev 76 | ``` 77 | 78 | 4. Open your browser at `http://localhost:3000`. 79 | 80 | --- 81 | 82 | ### 🙌 Join the Revolution 83 | Empowering the **BRC20 ecosystem** with unparalleled trading experiences. Get started today and be part of the future! 🌟 84 | 85 | -------------------------------------------------------------------------------- /api/inscription.ts: -------------------------------------------------------------------------------- 1 | import { IInscription } from "@/types/inscription"; 2 | import axios, { AxiosError } from "axios"; 3 | 4 | const unisat_api_key = process.env.NEXT_PUBLIC_UNISAT_API_KEY; 5 | const backend_api_base_url = process.env.NEXT_PUBLIC_BACEEND_URL; 6 | 7 | const fetchContentData = async (contentUrl: string): Promise => { 8 | try { 9 | const response = await axios.get(contentUrl); 10 | return response.data; 11 | } catch (error) { 12 | console.error("Error fetching content data:", error); 13 | return null; 14 | } 15 | }; 16 | 17 | export const getInscriptions = async ( 18 | address: string 19 | ): Promise => { 20 | const initialResponse = await window.unisat.getInscriptions(0, 100); 21 | 22 | const res: IInscription[] = []; 23 | for (const inscription of initialResponse.list) { 24 | const contentData = await fetchContentData(inscription.content); 25 | if (contentData && !contentData.tick) { 26 | res.push({ 27 | address: address, 28 | pubkey: "", 29 | inscriptionId: inscription.inscriptionId, 30 | inscriptionNumber: inscription.inscriptionNumber, 31 | content: inscription.content, 32 | price: 0, 33 | tokenTicker: "TSNT", 34 | }); 35 | } 36 | } 37 | 38 | const listedInscriptions: IInscription[] = 39 | await getListedInscriptionsByAddress(address); 40 | 41 | listedInscriptions.forEach((inscription) => { 42 | for (let i = 0; i < res.length; i++) { 43 | if (res[i].inscriptionId == inscription.inscriptionId) 44 | res[i].price = inscription.price; 45 | } 46 | }); 47 | 48 | return res; 49 | }; 50 | 51 | export const getListedInscriptionsByAddress = async ( 52 | address: string 53 | ): Promise => { 54 | const url = `${backend_api_base_url}/api/inscription/address/${address}`; 55 | try { 56 | const response = await axios.get(url); 57 | if (response.data.success) { 58 | return response.data.inscriptions; 59 | } 60 | return []; 61 | } catch (error) { 62 | console.log("Error fetching content data =>", error); 63 | return []; 64 | } 65 | }; 66 | 67 | export const getListedInscriptions = async (): Promise => { 68 | const url = `${backend_api_base_url}/api/inscription/all`; 69 | 70 | try { 71 | const res = await axios.get(url); 72 | return res.data.inscriptions; 73 | } catch (error) { 74 | console.log("Get all listed inscription error."); 75 | return []; 76 | } 77 | }; 78 | 79 | export const getInscriptionById = async (id: string, address: string) => { 80 | let inscription: IInscription = { 81 | address: "", 82 | pubkey: "", 83 | inscriptionId: "", 84 | inscriptionNumber: 0, 85 | content: "", 86 | price: 0, 87 | tokenTicker: "", 88 | }; 89 | 90 | const url = `${backend_api_base_url}/api/inscription/inscriptionid/${id}`; 91 | 92 | try { 93 | const response = await axios.get(url); 94 | if (response.data.success) { 95 | inscription = response.data.inscription; 96 | } 97 | } catch (error: any) { 98 | console.log("Error fetching content error:", error.response.data.error); 99 | } 100 | 101 | 102 | if (inscription.inscriptionId === "") { 103 | const inscriptions = await getInscriptions(address); 104 | inscriptions.forEach((item) => { 105 | if (item.inscriptionId === id) inscription = item; 106 | }); 107 | } 108 | 109 | return inscription; 110 | }; 111 | -------------------------------------------------------------------------------- /contexts/connectioncontext.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { createContext, useState, useEffect, ReactNode } from "react"; 4 | 5 | declare global { 6 | interface Window { 7 | unisat: any; 8 | } 9 | } 10 | 11 | interface Balance { 12 | confirmed: number; 13 | unconfirmed: number; 14 | total: number; 15 | } 16 | 17 | interface ConnectionContextProps { 18 | isUnisatInstalled: boolean; 19 | isConnected: boolean; 20 | balance: Balance; 21 | currentAccount: string; 22 | pubkey: string; 23 | network: string; 24 | connectWallet: () => Promise; 25 | disconnectWallet: () => void; 26 | switchNetwork: () => Promise; 27 | } 28 | 29 | export const ConnectionContext = createContext( 30 | {} as ConnectionContextProps 31 | ); 32 | 33 | interface ConnectionProviderProps { 34 | children: ReactNode; 35 | } 36 | 37 | export const ConnectionProvider: React.FC = ({ 38 | children, 39 | }) => { 40 | const [isUnisatInstalled, setIsUnisatInstalled] = useState(false); 41 | const [isConnected, setIsConnected] = useState(false); 42 | const [balance, setBalance] = useState({ 43 | confirmed: 0, 44 | unconfirmed: 0, 45 | total: 0, 46 | }); 47 | const [currentAccount, setCurrentAccount] = useState(""); 48 | const [network, setNetwork] = useState("livenet"); // Default network 49 | const [pubkey, setPubkey] = useState(""); 50 | 51 | useEffect(() => { 52 | const checkUnisat = async () => { 53 | const unisat = window.unisat; 54 | if (unisat) { 55 | setIsUnisatInstalled(true); 56 | try { 57 | // Check the current network 58 | const currentNetwork = await unisat.getNetwork(); 59 | setNetwork(currentNetwork); 60 | 61 | // Check for accounts and balance 62 | const accounts = await unisat.getAccounts(); 63 | if (accounts.length > 0) { 64 | setIsConnected(true); 65 | setCurrentAccount(accounts[0]); 66 | const balance = await unisat.getBalance(accounts[0]); 67 | setBalance(balance); 68 | const pubkey = await unisat.getPublicKey(accounts[0]); 69 | setPubkey(pubkey); 70 | } 71 | } catch (error) { 72 | console.error("Error checking Unisat status:", error); 73 | // message.error('Could not check Unisat status'); 74 | } 75 | } 76 | }; 77 | 78 | checkUnisat(); 79 | }, []); 80 | 81 | const connectWallet = async () => { 82 | const unisat = window.unisat; 83 | if (unisat) { 84 | try { 85 | const accounts = await unisat.requestAccounts(); 86 | if (accounts.length > 0) { 87 | setIsConnected(true); 88 | setCurrentAccount(accounts[0]); 89 | const newBalance = await unisat.getBalance(accounts[0]); 90 | setBalance(newBalance); 91 | const pubkey = await unisat.getPublicKey(accounts[0]); 92 | setPubkey(pubkey); 93 | } 94 | } catch (error) { 95 | console.error("Error connecting to Unisat:", error); 96 | // message.error('Could not connect to Unisat'); 97 | } 98 | } 99 | }; 100 | 101 | const disconnectWallet = () => { 102 | setIsConnected(false); 103 | setCurrentAccount(""); 104 | setBalance({ confirmed: 0, unconfirmed: 0, total: 0 }); 105 | }; 106 | 107 | const switchNetwork = async () => { 108 | const unisat = window.unisat; 109 | if (unisat) { 110 | try { 111 | const newNetwork = network === "livenet" ? "testnet" : "livenet"; 112 | await unisat.switchNetwork(newNetwork); 113 | setNetwork(newNetwork); 114 | 115 | // After switching the network, refresh the account details 116 | const accounts = await unisat.getAccounts(); 117 | if (accounts.length > 0) { 118 | setCurrentAccount(accounts[0]); 119 | const newBalance = await unisat.getBalance(accounts[0]); 120 | setBalance(newBalance); 121 | const pubkey = await unisat.getPublicKey(accounts[0]); 122 | setPubkey(pubkey); 123 | } else { 124 | // Handle the case where no accounts are found after the network switch 125 | setIsConnected(false); 126 | setCurrentAccount(""); 127 | setBalance({ confirmed: 0, unconfirmed: 0, total: 0 }); 128 | } 129 | } catch (error) { 130 | console.error("Error switching network:", error); 131 | // message.error('Could not switch network'); 132 | } 133 | } 134 | }; 135 | 136 | return ( 137 | 150 | {children} 151 | 152 | ); 153 | }; 154 | 155 | export default ConnectionProvider; 156 | -------------------------------------------------------------------------------- /components/icons.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { IconSvgProps } from "@/types"; 4 | 5 | export const Logo: React.FC = ({ 6 | size = 36, 7 | width, 8 | height, 9 | ...props 10 | }) => ( 11 | 18 | 24 | 25 | ); 26 | 27 | export const DiscordIcon: React.FC = ({ 28 | size = 24, 29 | width, 30 | height, 31 | ...props 32 | }) => { 33 | return ( 34 | 40 | 44 | 45 | ); 46 | }; 47 | 48 | export const TwitterIcon: React.FC = ({ 49 | size = 24, 50 | width, 51 | height, 52 | ...props 53 | }) => { 54 | return ( 55 | 61 | 65 | 66 | ); 67 | }; 68 | 69 | export const GithubIcon: React.FC = ({ 70 | size = 24, 71 | width, 72 | height, 73 | ...props 74 | }) => { 75 | return ( 76 | 82 | 88 | 89 | ); 90 | }; 91 | 92 | export const MoonFilledIcon = ({ 93 | size = 24, 94 | width, 95 | height, 96 | ...props 97 | }: IconSvgProps) => ( 98 | 112 | ); 113 | 114 | export const SunFilledIcon = ({ 115 | size = 24, 116 | width, 117 | height, 118 | ...props 119 | }: IconSvgProps) => ( 120 | 134 | ); 135 | 136 | export const HeartFilledIcon = ({ 137 | size = 24, 138 | width, 139 | height, 140 | ...props 141 | }: IconSvgProps) => ( 142 | 159 | ); 160 | 161 | export const SearchIcon = (props: IconSvgProps) => ( 162 | 187 | ); 188 | 189 | export const NextUILogo: React.FC = (props) => { 190 | const { width, height = 40 } = props; 191 | 192 | return ( 193 | 201 | 205 | 209 | 213 | 214 | ); 215 | }; 216 | -------------------------------------------------------------------------------- /app/inscription/[slug]/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useState, useEffect, useContext } from "react"; 4 | import { usePathname, redirect } from "next/navigation"; 5 | import { 6 | Image, 7 | Card, 8 | CardBody, 9 | Button, 10 | ButtonGroup, 11 | Table, 12 | TableHeader, 13 | TableColumn, 14 | TableBody, 15 | TableRow, 16 | TableCell, 17 | getKeyValue, 18 | Modal, 19 | ModalContent, 20 | ModalHeader, 21 | ModalBody, 22 | ModalFooter, 23 | Input, 24 | useDisclosure, 25 | Divider, 26 | } from "@nextui-org/react"; 27 | import toast, { Toaster } from "react-hot-toast"; 28 | 29 | import { getInscriptionById } from "@/api/inscription"; 30 | import { 31 | getOffersByInscriptionId, 32 | requestOffer, 33 | requestPsbt, 34 | rejectOffer, 35 | acceptOffer, 36 | } from "@/api/offer"; 37 | import { IInscription } from "@/types/inscription"; 38 | import { IOffer, IOfferForTable, IOfferForTableMe } from "@/types/offer"; 39 | import { offerTableColumns, offerTableColumnsMe } from "@/config/table"; 40 | import { getOfferDataForTable } from "@/utils/offer"; 41 | import { ConnectionContext } from "@/contexts/connectioncontext"; 42 | import { getTokenBalanceByAddressTicker } from "@/api/unisat"; 43 | import { unlistInscription, listInscription } from "@/api/list"; 44 | import { marketplace_fee } from "@/config/site"; 45 | 46 | const page = () => { 47 | const pathname = usePathname(); 48 | const { currentAccount, pubkey } = useContext(ConnectionContext); 49 | const [inscription, setInscription] = useState({ 50 | address: "", 51 | pubkey: "", 52 | inscriptionId: "", 53 | inscriptionNumber: 0, 54 | content: "", 55 | price: 0, 56 | tokenTicker: "", 57 | }); 58 | const [offers, setOffers] = useState([]); 59 | const [offerList, setOfferList] = useState([]); 60 | const [bestOffer, setBestOffer] = useState({ 61 | key: "", 62 | price: 0, 63 | token: "", 64 | from: "", 65 | status: 0, 66 | }); 67 | const [isListed, setIsListed] = useState(true); 68 | const [tokenBalance, setTokenBalance] = useState(0); 69 | const [offerValue, setOfferValue] = useState(0); 70 | const { isOpen, onOpen, onClose, onOpenChange } = useDisclosure(); 71 | 72 | useEffect(() => { 73 | initData(); 74 | }, []); 75 | 76 | useEffect(() => { 77 | initData(); 78 | }, [currentAccount]); 79 | 80 | const initData = async () => { 81 | await getInscription(); 82 | await getTokenBalance(); 83 | await getOffers(); 84 | }; 85 | 86 | const getTokenBalance = async () => { 87 | if (!currentAccount) return; 88 | const res = await getTokenBalanceByAddressTicker(currentAccount, "TSNT"); 89 | setTokenBalance(res * 1); 90 | }; 91 | 92 | const getInscription = async () => { 93 | const inscriptionId = pathname.split("/").pop(); 94 | const res = await getInscriptionById( 95 | inscriptionId as string, 96 | currentAccount 97 | ); 98 | 99 | if (res.price !== 0) setIsListed(true); 100 | else setIsListed(false); 101 | 102 | setInscription({ 103 | address: res.address, 104 | pubkey: res.pubkey, 105 | inscriptionId: res.inscriptionId, 106 | inscriptionNumber: res.inscriptionNumber, 107 | content: res.content, 108 | price: res.price, 109 | tokenTicker: res.tokenTicker, 110 | }); 111 | }; 112 | 113 | const getOffers = async () => { 114 | let res = await getOffersByInscriptionId(pathname); 115 | setOfferList(res); 116 | 117 | const resForTable: IOfferForTable[] = getOfferDataForTable(res); 118 | 119 | if (resForTable.length != 0) { 120 | let tempBestOffer = resForTable.at(0); 121 | resForTable.forEach((offer) => { 122 | if (tempBestOffer?.price && tempBestOffer?.price < offer.price) 123 | tempBestOffer = offer; 124 | }); 125 | if (tempBestOffer) { 126 | setBestOffer(tempBestOffer); 127 | } 128 | } 129 | setOffers(resForTable); 130 | }; 131 | 132 | const makeOffer = async () => { 133 | if (offerValue > tokenBalance) { 134 | toast.error( 135 | "You don't have enough funds to complete the purchase. Please deposit or convert your funds." 136 | ); 137 | return; 138 | } 139 | let fee_brcInscription: any = {}, 140 | brcInscription: any = {}; 141 | try { 142 | fee_brcInscription = await window.unisat.inscribeTransfer( 143 | "tsnt", 144 | Math.round((offerValue * marketplace_fee) / 100).toString() 145 | ); 146 | 147 | brcInscription = await window.unisat.inscribeTransfer( 148 | "tsnt", 149 | Math.round((offerValue * (100 - marketplace_fee)) / 100).toString() 150 | ); 151 | } catch (error) { 152 | toast.error("Error occured while inscribing your brc20 tokens!"); 153 | return; 154 | } 155 | 156 | const psbtStr: any = await requestPsbt( 157 | inscription.inscriptionId, 158 | brcInscription.inscriptionId, 159 | fee_brcInscription.inscriptionId, 160 | pubkey, 161 | currentAccount, 162 | inscription.pubkey, 163 | inscription.address 164 | ); 165 | 166 | if (psbtStr == "") { 167 | toast.error("Error occured while request psbt."); 168 | return; 169 | } 170 | // const psbt = Bitcoin.Psbt.fromHex(psbtStr); 171 | // console.log("psbt => ", psbt); 172 | let signedPsbt: any; 173 | try { 174 | signedPsbt = await window.unisat.signPsbt(psbtStr); 175 | } catch (error) { 176 | toast.error("Error occured while sign transaction."); 177 | return; 178 | } 179 | 180 | let offerFlag: any; 181 | try { 182 | offerFlag = await requestOffer({ 183 | _id: "", 184 | inscriptionId: inscription.inscriptionId, 185 | sellerAddress: inscription.address, 186 | buyerAddress: currentAccount, 187 | price: offerValue, 188 | tokenTicker: "tsnt", 189 | psbt: psbtStr, 190 | status: 1, 191 | buyerSignedPsbt: signedPsbt, 192 | }); 193 | if (offerFlag) { 194 | toast.success("Successfully offered."); 195 | onClose(); 196 | initData(); 197 | } 198 | } catch (error) { 199 | toast.error("Error occured while request offer."); 200 | } 201 | }; 202 | 203 | const requestList = async () => { 204 | const res = await listInscription({ 205 | address: currentAccount, 206 | pubkey: pubkey, 207 | inscriptionId: inscription.inscriptionId, 208 | inscriptionNumber: inscription.inscriptionNumber, 209 | content: inscription.content, 210 | price: offerValue, 211 | tokenTicker: inscription.tokenTicker, 212 | }); 213 | 214 | setInscription({ 215 | address: inscription.address, 216 | pubkey: inscription.pubkey, 217 | inscriptionId: inscription.inscriptionId, 218 | inscriptionNumber: inscription.inscriptionNumber, 219 | content: inscription.content, 220 | price: offerValue, 221 | tokenTicker: inscription.tokenTicker, 222 | }); 223 | 224 | if (res) { 225 | toast.success("Successfully listed"); 226 | onClose(); 227 | setIsListed(true); 228 | } 229 | }; 230 | 231 | const handleUnlist = async () => { 232 | const res = await unlistInscription(inscription.inscriptionId); 233 | if (res) { 234 | toast.success("Successfully unlisted!"); 235 | setIsListed(false); 236 | setInscription({ 237 | address: inscription.address, 238 | pubkey: inscription.pubkey, 239 | inscriptionId: inscription.inscriptionId, 240 | inscriptionNumber: inscription.inscriptionNumber, 241 | content: inscription.content, 242 | price: 0, 243 | tokenTicker: inscription.tokenTicker, 244 | }); 245 | } else { 246 | toast.success("Error occured while unlisting!"); 247 | } 248 | onClose(); 249 | }; 250 | 251 | const handleOfferOrList = () => { 252 | if (isListed) { 253 | makeOffer(); 254 | } else requestList(); 255 | }; 256 | 257 | const handleAccept = async (offerId: string) => { 258 | let inscriptionId = "", 259 | psbt = "", 260 | buyerSignedPsbt = ""; 261 | offerList.forEach((offer) => { 262 | if (offer._id == offerId) { 263 | inscriptionId = offer.inscriptionId; 264 | psbt = offer.psbt; 265 | buyerSignedPsbt = offer.buyerSignedPsbt; 266 | } 267 | }); 268 | 269 | let signedPsbt = ""; 270 | try { 271 | signedPsbt = await window.unisat.signPsbt(buyerSignedPsbt); 272 | 273 | await acceptOffer(inscriptionId, psbt, buyerSignedPsbt, signedPsbt); 274 | toast.success( 275 | "Congratelation. Your ordinal was sold out. Please wait a minute and check your wallet." 276 | ); 277 | redirect("/"); 278 | } catch (error) { 279 | } 280 | }; 281 | 282 | const handleReject = async (offerId: string) => { 283 | const res = await rejectOffer(offerId); 284 | if (!res) { 285 | toast.error("Error occured while reject the offer."); 286 | return; 287 | } 288 | initData(); 289 | }; 290 | 291 | return ( 292 |
293 | 298 | 299 |
300 |
301 | Inscription Content 308 |
309 | {} 310 |
311 |

312 | {"inscriptionID : " + 313 | inscription.inscriptionId.substring(0, 9) + 314 | "..."} 315 |

316 | {(currentAccount != inscription.address || isListed) && ( 317 |

318 | {"Price : " + 319 | inscription.price + 320 | " " + 321 | inscription.tokenTicker} 322 |

323 | )} 324 | 325 | {currentAccount != inscription.address && ( 326 | 329 | )} 330 | 331 | {currentAccount === inscription.address && isListed && ( 332 | 333 | 340 | 341 | )} 342 | 343 | {currentAccount === inscription.address && !isListed && ( 344 | 345 | 348 | 349 | )} 350 | {currentAccount != inscription.address && ( 351 | 352 | 353 | {offerTableColumns.map((column) => ( 354 | {column.label} 355 | ))} 356 | 357 | 358 | {offers.map((offer) => ( 359 | 360 | {(columnKey) => ( 361 | 362 | {columnKey === "from" 363 | ? getKeyValue(offer, columnKey).substring(0, 9) + 364 | "..." 365 | : columnKey === "status" 366 | ? getKeyValue(offer, columnKey) == 1 367 | ? "Requested" 368 | : "Rejected" 369 | : getKeyValue(offer, columnKey)} 370 | 371 | )} 372 | 373 | ))} 374 | 375 |
376 | )} 377 | {currentAccount === inscription.address && isListed && ( 378 | 379 | 380 | {offerTableColumnsMe.map((column) => ( 381 | {column.label} 382 | ))} 383 | 384 | 385 | {offers.map((offer) => ( 386 | 387 | {(columnKey) => ( 388 | 389 | {columnKey === "action" && 390 | (getKeyValue("status", columnKey) === 2 ? ( 391 | "" 392 | ) : ( 393 | 394 | 402 | 409 | 410 | ))} 411 | {/* {(columnKey === "action" && getKeyValue("status", columnKey) == 2) && ""} */} 412 | {columnKey === "from" && 413 | getKeyValue(offer, columnKey).substring(0, 9) + 414 | "..."} 415 | {columnKey === "status" && 416 | (getKeyValue(offer, columnKey) == 1 417 | ? "Requested" 418 | : "Rejected")} 419 | {(columnKey === "price" || columnKey === "token") && 420 | getKeyValue(offer, columnKey)} 421 | 422 | )} 423 | 424 | ))} 425 | 426 |
427 | )} 428 |
429 |
430 |
431 |
432 |
433 | 451 |
452 | 453 | 454 | 455 | {(onClose) => ( 456 | <> 457 | 458 | {inscription.address !== currentAccount && "Make Offer"} 459 | {inscription.address === currentAccount && 460 | !isListed && 461 | "List Inscription"} 462 | 463 | 464 | 465 |

466 | {"inscriptionID : " + 467 | inscription.inscriptionId.substring(0, 10) + 468 | "..."} 469 |

470 | {inscription.address !== currentAccount && ( 471 |

472 | {"Price : " + 473 | inscription.price + 474 | " " + 475 | inscription.tokenTicker} 476 |

477 | )} 478 | {bestOffer.price !== 0 && ( 479 | <> 480 | 481 |

Best Offer

482 |

{bestOffer.price + " TSNT"}

483 | 484 | )} 485 | 486 | 487 | {currentAccount !== inscription.address &&

Your Offer

} 488 | setOfferValue(parseInt(e.target.value))} 492 | endContent={ 493 |
494 | TSNT 495 |
496 | } 497 | /> 498 | {currentAccount !== inscription.address && ( 499 | <> 500 | 501 |

Your Balance

502 |

{tokenBalance} TSNT

503 | 504 | )} 505 |
506 | 507 | 508 | 511 | 512 | 513 | 514 | )} 515 |
516 |
517 |
518 | ); 519 | }; 520 | 521 | export default page; 522 | --------------------------------------------------------------------------------