├── .eslintrc.json ├── styles ├── TokenInput.module.css ├── globals.css ├── Footer.module.css ├── NFTConsiderationViewer.module.css ├── FooterSection.module.css ├── About.module.css ├── Home.module.css ├── NavBar.module.css ├── Listings.module.css ├── Listing.module.css ├── Explorer.module.css ├── SubtitleSection.module.css ├── Create.module.css ├── TokenSelection.module.css ├── ListingCard.module.css ├── TitleSection.module.css └── ListingsSection.module.css ├── public ├── assets │ ├── ape.png │ ├── bobu.png │ ├── weth.png │ ├── azuki.png │ ├── coven.png │ ├── doggo.png │ ├── doodle.png │ ├── ellipse.png │ ├── poggo.jpg │ ├── seaport.png │ ├── nftplaceholder.jpg │ ├── seaport_colored.png │ ├── eth.svg │ └── logo.svg ├── vercel.svg └── favicon.ico ├── .env.example ├── next-env.d.ts ├── utils ├── abridgeAddress.tsx └── createItem.tsx ├── next.config.js ├── components ├── withTransition.tsx ├── NavBar.tsx ├── SubtitleSection.tsx ├── web3 │ ├── ENSGreeter.tsx │ ├── CurrencyViewer.tsx │ ├── NFTViewer.tsx │ └── NFTConsiderationViewer.tsx ├── ListingsSection.tsx ├── FooterSection.tsx ├── Sidebar.tsx ├── TitleSection.tsx ├── ListingCard.tsx └── TokenSelection.tsx ├── .gitignore ├── pages ├── _document.tsx ├── api │ ├── Fulfillment.md │ ├── sky-orders.ts │ ├── sky-orders │ │ └── [orderId].ts │ ├── orders.ts │ ├── orders │ │ └── [orderId].ts │ ├── README.md │ ├── filecoin-orders.ts │ ├── sky-relatedOrders │ │ └── [addressParam].ts │ └── relatedOrders │ │ └── [addressParam].ts ├── profile.tsx ├── index.tsx ├── listings.tsx ├── _app.tsx ├── explorer.tsx ├── about.tsx ├── create.tsx └── listings │ └── [listingId].tsx ├── tsconfig.json ├── types └── tokenTypes.tsx ├── .github └── workflows │ └── skynet.yaml ├── LICENSE ├── package.json ├── constants └── pinnedCollections.ts └── README.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /styles/TokenInput.module.css: -------------------------------------------------------------------------------- 1 | .title { 2 | margin: 0; 3 | line-height: 1.15; 4 | font-size: 2rem; 5 | } -------------------------------------------------------------------------------- /public/assets/ape.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/ape.png -------------------------------------------------------------------------------- /public/assets/bobu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/bobu.png -------------------------------------------------------------------------------- /public/assets/weth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/weth.png -------------------------------------------------------------------------------- /public/assets/azuki.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/azuki.png -------------------------------------------------------------------------------- /public/assets/coven.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/coven.png -------------------------------------------------------------------------------- /public/assets/doggo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/doggo.png -------------------------------------------------------------------------------- /public/assets/doodle.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/doodle.png -------------------------------------------------------------------------------- /public/assets/ellipse.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/ellipse.png -------------------------------------------------------------------------------- /public/assets/poggo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/poggo.jpg -------------------------------------------------------------------------------- /public/assets/seaport.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/seaport.png -------------------------------------------------------------------------------- /public/assets/nftplaceholder.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/nftplaceholder.jpg -------------------------------------------------------------------------------- /public/assets/seaport_colored.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tsukiji-seaport-protocol/tsukiji/HEAD/public/assets/seaport_colored.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | OPENSEA_API_KEY= 2 | INFURA_KEY= 3 | FIREBASE_PRIVATE_KEY= 4 | NEXT_PUBLIC_FIREBASE_PROJECT_ID= 5 | FIREBASE_CLIENT_EMAIL= 6 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /utils/abridgeAddress.tsx: -------------------------------------------------------------------------------- 1 | export function abridgeAddress(address?: string) { 2 | if (!address) return address; 3 | const l = address.length; 4 | if (l < 20) return address; 5 | return `${address.substring(0, 6)}...${address.substring(l - 4, l)}`; 6 | } 7 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | domains: ["openseauserdata.com"], 6 | loader: "imgix", 7 | path: "", 8 | }, 9 | }; 10 | 11 | module.exports = nextConfig; 12 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | padding: 0; 4 | margin: 0; 5 | box-sizing: border-box; 6 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, 7 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif; 8 | overflow-x: hidden; 9 | } 10 | 11 | a { 12 | color: inherit; 13 | text-decoration: none; 14 | } 15 | 16 | * { 17 | box-sizing: border-box; 18 | } 19 | -------------------------------------------------------------------------------- /components/withTransition.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | import { motion } from "framer-motion"; 3 | 4 | export default function withTransition(Component: React.ComponentType) { 5 | return (props: T) => ( 6 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /styles/Footer.module.css: -------------------------------------------------------------------------------- 1 | .footer { 2 | display: flex; 3 | flex-direction: column; 4 | flex: 1; 5 | padding: 2rem 0; 6 | justify-content: center; 7 | align-items: center; 8 | background-color: #000000; 9 | color: #ffffff; 10 | font-family: "Inter"; 11 | position: absolute; 12 | bottom: 0; 13 | width: 100%; 14 | } 15 | 16 | .footer a { 17 | display: flex; 18 | justify-content: center; 19 | align-items: center; 20 | flex-grow: 1; 21 | } 22 | 23 | .socials { 24 | margin-top: 1rem; 25 | } 26 | -------------------------------------------------------------------------------- /public/assets/eth.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | 5 | # credentials 6 | creds.json 7 | 8 | # dependencies 9 | /node_modules 10 | /.pnp 11 | .pnp.js 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | .env*.local 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /pages/api/Fulfillment.md: -------------------------------------------------------------------------------- 1 | # Fulfillment 2 | In the spirit of OpenSea, Tsukiji serves as an open port for users to come with what they have, and leave with what they want. As such, we would like to expose an API, or an on-prem piece of software, that outside/third parties could use to help contribute to the flywheel that is having high velocity sales. 3 | 4 | ## BYO fulfillment: 5 | Tsukiji provides: 6 | - Collection of `offers` and `considerations` with all specified terms and metadata 7 | - An SDK that enables any party to serve as matchmaker and fulfill orders 8 | 9 | You provide: 10 | - Fulfillment: kickstart a flywheel that benefits token traders, community members, and you the fulfiller! 11 | -------------------------------------------------------------------------------- /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 | "baseUrl": ".", 18 | "paths": { 19 | "@components/*": ["components/*"], 20 | "@styles/*": ["styles/*"], 21 | "@utils/*": ["utils/*"], 22 | "@data/*": ["data/*"], 23 | "@constants/*": ["constants/*"] 24 | } 25 | }, 26 | 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /pages/profile.tsx: -------------------------------------------------------------------------------- 1 | import { ENSGreeter } from "@components/web3/ENSGreeter"; 2 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 3 | import { NextPage } from "next"; 4 | import { useAccount } from "wagmi"; 5 | import styles from "@styles/Home.module.css"; 6 | 7 | const Profile: NextPage = () => { 8 | const { data: accountData, isError: accountIsError, isLoading: accountIsLoading } = useAccount({ 9 | }); 10 | 11 | return ( 12 |
13 |
14 |
15 | 16 |
17 | {accountData?.address && } 18 | {!accountData?.address &&

Please connect wallet

} 19 |
20 |
21 | ) 22 | } 23 | 24 | export default Profile; 25 | -------------------------------------------------------------------------------- /components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import { Link, Image, useDisclosure } from "@chakra-ui/react"; 2 | import { ConnectButton } from "@rainbow-me/rainbowkit"; 3 | import styles from "@styles/NavBar.module.css"; 4 | import { HamburgerIcon } from "@chakra-ui/icons"; 5 | import { Sidebar } from "./Sidebar"; 6 | 7 | export const NavBar = () => { 8 | const { isOpen, onOpen, onClose } = useDisclosure(); 9 | 10 | return ( 11 |
12 | 13 | logo 14 | 15 | 16 |
17 | 18 | 24 |
25 | 26 |
27 | ); 28 | }; 29 | -------------------------------------------------------------------------------- /types/tokenTypes.tsx: -------------------------------------------------------------------------------- 1 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 2 | import { 3 | ConsiderationInputItem, 4 | CreateInputItem, 5 | OrderWithCounter, 6 | } from "@opensea/seaport-js/lib/types"; 7 | 8 | export type InputItem = OfferItem | ConsiderationItem; 9 | 10 | export type OfferItem = { 11 | type: ItemType; 12 | inputItem: CreateInputItem; 13 | name: string; 14 | image_url: string; 15 | token_id: string; 16 | address?: string; 17 | collectionName: string; 18 | symbol: string; 19 | }; 20 | 21 | export type ConsiderationItem = { 22 | type: ItemType; 23 | inputItem: ConsiderationInputItem; 24 | name: string; 25 | image_url: string; 26 | token_id: string; 27 | address?: string; 28 | collectionName: string; 29 | symbol: string; 30 | }; 31 | 32 | export type OrderWithMetadata = { 33 | id: string; 34 | order: OrderWithCounter; 35 | offers: OfferItem[]; 36 | considerations: ConsiderationItem[]; 37 | }; 38 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type { NextPage } from "next"; 2 | import styles from "../styles/Home.module.css"; 3 | import { useAccount } from "wagmi"; 4 | import { NavBar } from "@components/NavBar"; 5 | import { TitleSection } from "@components/TitleSection"; 6 | import { SubtitleSection } from "@components/SubtitleSection"; 7 | import { ListingsSection } from "@components/ListingsSection"; 8 | import { FooterSection } from "@components/FooterSection"; 9 | import withTransition from "@components/withTransition"; 10 | 11 | const Home: NextPage = () => { 12 | const { data: accountData } = useAccount(); 13 | 14 | return ( 15 |
16 | 17 |
18 | 19 | 20 | 21 | 22 |
23 |
24 | ); 25 | }; 26 | 27 | export default withTransition(Home); 28 | -------------------------------------------------------------------------------- /.github/workflows/skynet.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | # Controls when the workflow will run 4 | on: 5 | # Triggers the workflow on push or pull request events but only for the main branch 6 | push: 7 | branches: [main] 8 | pull_request: 9 | branches: [main] 10 | 11 | # Allows you to run this workflow manually from the Actions tab 12 | workflow_dispatch: 13 | 14 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 15 | jobs: 16 | # This workflow contains a single job called "build" 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: actions/setup-node@v2 23 | with: 24 | node-version: 16.x 25 | 26 | - name: Install dependencies 27 | run: npm install 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Deploy to Skynet 33 | uses: SkynetLabs/deploy-to-skynet-action@v2 34 | with: 35 | upload-dir: out 36 | github-token: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 tsukiji 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 | -------------------------------------------------------------------------------- /components/SubtitleSection.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, VStack, Image, Text, Box } from "@chakra-ui/react"; 2 | import styles from "@styles/SubtitleSection.module.css"; 3 | import { chakra } from "@chakra-ui/react"; 4 | import { motion, isValidMotionProp } from "framer-motion"; 5 | import Link from "next/link"; 6 | 7 | const ChakraBox = chakra(motion.div, { 8 | shouldForwardProp: (prop) => isValidMotionProp(prop) || prop === "children", 9 | }); 10 | 11 | export const SubtitleSection = () => { 12 | return ( 13 |
14 |
15 | 16 |

17 | Enabling new primitives for NFT trading 18 |

19 | 20 | Access features such as bartering or batch exchanging NFTs on the 21 | Tsukiji platform. 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |
30 | ); 31 | }; 32 | -------------------------------------------------------------------------------- /styles/NFTConsiderationViewer.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | display: flex; 3 | } 4 | 5 | .collectionGrid { 6 | height: 540px; 7 | overflow-x: hidden; 8 | overflow-y: scroll; 9 | align-items: flex-start; 10 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 11 | scrollbar-width: none; /* Firefox */ 12 | width: 100%; 13 | } 14 | 15 | .collectionGrid::-webkit-scrollbar { 16 | width: 0; /* Remove scrollbar space */ 17 | background: transparent; /* Optional: just make scrollbar invisible */ 18 | } 19 | 20 | .tokenGrid { 21 | height: 540px; 22 | overflow-x: hidden; 23 | overflow-y: scroll; 24 | align-items: flex-start; 25 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 26 | scrollbar-width: none; /* Firefox */ 27 | width: 100%; 28 | } 29 | 30 | .tokenGrid::-webkit-scrollbar { 31 | width: 0; /* Remove scrollbar space */ 32 | background: transparent; /* Optional: just make scrollbar invisible */ 33 | } 34 | 35 | .collectionRow { 36 | color: rgba(255, 255, 255, 0.7); 37 | padding: 1rem; 38 | transition: 0.3s; 39 | cursor: pointer; 40 | } 41 | 42 | .collectionRow:hover { 43 | color: rgba(255, 255, 255, 1); 44 | } 45 | 46 | .selectedCollection { 47 | color: rgba(255, 255, 255, 1); 48 | text-decoration: underline; 49 | } 50 | -------------------------------------------------------------------------------- /styles/FooterSection.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 3rem; 3 | border-top: 1px solid rgba(255, 255, 255, 0.1); 4 | position: relative; 5 | width: 100%; 6 | } 7 | 8 | .header { 9 | font-family: "Inter"; 10 | font-style: normal; 11 | font-weight: 700; 12 | font-size: 40px; 13 | text-align: center; 14 | 15 | color: #ffffff; 16 | } 17 | 18 | .subheader { 19 | color: rgba(255, 255, 255, 0.8); 20 | padding-left: 0.3rem; 21 | font-size: 1.2rem; 22 | } 23 | 24 | .subheaderLink { 25 | color: rgba(255, 255, 255); 26 | opacity: 0.7; 27 | padding-left: 0.3rem; 28 | font-size: 1.2rem; 29 | transition: 0.3s; 30 | } 31 | 32 | .subheaderLink:hover { 33 | opacity: 1; 34 | } 35 | 36 | .centerEllipse { 37 | left: 50%; 38 | transform: translateX(-50%); 39 | top: -700px; 40 | width: 1000px; 41 | height: 1000px; 42 | background-image: url(/assets/ellipse.png); 43 | background-size: contain; 44 | background-repeat: no-repeat; 45 | position: absolute; 46 | pointer-events: none; 47 | opacity: 0.3; 48 | } 49 | 50 | .content { 51 | width: 100%; 52 | justify-content: space-between; 53 | } 54 | 55 | .footerSubsection { 56 | display: flex; 57 | flex-direction: column; 58 | align-items: flex-start !important; 59 | padding: 0 1rem; 60 | } 61 | -------------------------------------------------------------------------------- /styles/About.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | background-color: #000000; 4 | } 5 | 6 | .content { 7 | padding-top: 4rem; 8 | } 9 | 10 | .contentSection { 11 | padding-top: 3rem; 12 | } 13 | 14 | .header { 15 | font-family: "Inter"; 16 | font-style: normal; 17 | font-weight: 700; 18 | font-size: 30px; 19 | line-height: 35px; 20 | width: 600px; 21 | text-align: center; 22 | color: #ffffff; 23 | } 24 | 25 | .header .italic { 26 | font-style: italic; 27 | } 28 | 29 | .subheader { 30 | font-family: "Inter"; 31 | color: rgb(255, 255, 255, 0.9); 32 | font-size: 18px; 33 | max-width: 1000px; 34 | padding-top: 1rem; 35 | } 36 | 37 | .subheader .italic { 38 | font-style: italic; 39 | } 40 | 41 | .leftEllipse { 42 | top: -295px; 43 | left: -700px; 44 | width: 1000px; 45 | height: 1000px; 46 | background-image: url(/assets/ellipse.png); 47 | background-size: contain; 48 | background-repeat: no-repeat; 49 | position: absolute; 50 | pointer-events: none; 51 | opacity: 0.3; 52 | } 53 | 54 | .rightEllipse { 55 | bottom: -400px; 56 | right: -650px; 57 | width: 1000px; 58 | height: 1000px; 59 | background-image: url(/assets/ellipse.png); 60 | background-size: contain; 61 | background-repeat: no-repeat; 62 | position: absolute; 63 | pointer-events: none; 64 | opacity: 0.3; 65 | } 66 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 | h(    2 |         3 |       4 |        5 |       -------------------------------------------------------------------------------- /pages/api/sky-orders.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | const { SkynetClient, genKeyPairFromSeed } = require("@skynetlabs/skynet-nodejs"); 4 | 5 | const client = new SkynetClient("", { skynetApiKey: process.env.SKYNET_API_KEY }); 6 | const { publicKey, privateKey } = genKeyPairFromSeed(process.env.SKYNET_SEED || ''); 7 | 8 | // This handler supports both GET and POST requests. 9 | // GET will return all orders. 10 | // POST will attempt to create a new order and return the resulting ID. 11 | export default async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | const dataKey = "orders"; 16 | if (req.method === "GET") { 17 | try { 18 | const { data, dataLink } = await client.db.getJSON(publicKey, dataKey); 19 | res.status(200).json({ data: data, dataLink: dataLink }); 20 | } catch (error) { 21 | res.status(400).json({ message: 'sadge', error: error }); 22 | } 23 | } else if (req.method === "POST") { 24 | try { 25 | await client.db.setJSON(privateKey, dataKey, req.body); 26 | res.status(200).json({ message: 'success!' }); 27 | } catch (error) { 28 | console.log(error); 29 | res.status(400).json({ message: 'sadge', error: error, attempt: req.body }); 30 | } 31 | } else { 32 | res.status(400).json("Unable to handle request"); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsukiji", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build && next export", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@chakra-ui/icons": "^2.0.2", 13 | "@chakra-ui/react": "^2.2.1", 14 | "@emotion/react": "^11.9.3", 15 | "@emotion/styled": "^11.9.3", 16 | "@opensea/seaport-js": "^1.0.2", 17 | "@skynetlabs/skynet-nodejs": "^2.6.0", 18 | "add": "^2.0.6", 19 | "crc-32": "^1.2.2", 20 | "ethers": "^5.6.9", 21 | "framer-motion": "^6.3.16", 22 | "lodash.merge": "^4.6.2", 23 | "next": "12.1.6", 24 | "react": "18.2.0", 25 | "react-dom": "18.2.0", 26 | "react-icons": "^4.4.0", 27 | "react-query": "^3.39.1", 28 | "short-unique-id": "^4.4.4", 29 | "skynet-js": "^4.3.0", 30 | "wagmi": "^0.4.12", 31 | "web3.storage": "^4.3.0" 32 | }, 33 | "devDependencies": { 34 | "@rainbow-me/rainbowkit": "^0.3.4", 35 | "@types/lodash.merge": "^4.6.7", 36 | "@types/node": "18.0.0", 37 | "@types/react": "18.0.14", 38 | "@types/react-dom": "18.0.5", 39 | "dotenv": "^16.0.1", 40 | "eslint": "8.18.0", 41 | "eslint-config-next": "12.1.6", 42 | "ethers": "^5.5.4", 43 | "firebase-admin": "^11.0.0", 44 | "typescript": "4.7.4", 45 | "web3": "^1.7.4", 46 | "webpack": "^5.73.0", 47 | "webpack-cli": "^4.10.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /pages/api/sky-orders/[orderId].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | const { SkynetClient, genKeyPairFromSeed } = require("@skynetlabs/skynet-nodejs"); 4 | 5 | const client = new SkynetClient(); 6 | const { publicKey, privateKey } = genKeyPairFromSeed(process.env.SKYNET_SEED || ''); 7 | 8 | // This handler supports both GET and PUT requests. 9 | // GET will return the order associated with the specified ID. 10 | // PUT will attempt to update the order associated with the specified ID. 11 | export default async function handler( 12 | req: NextApiRequest, 13 | res: NextApiResponse 14 | ) { 15 | const dataKey = "orders"; 16 | 17 | if (req.method === 'GET') { 18 | try { 19 | const { data } = await client.db.getJSON(publicKey, dataKey); 20 | data.map((entry: any) => { 21 | if (entry.contains(req.query.orderId)) { 22 | return entry; 23 | } 24 | }) 25 | if (data.length < 1) { 26 | res.status(404).json(`Order with ID ${req.query.orderId} not found`); 27 | return; 28 | }; 29 | res.status(200).json({ data: data }); 30 | } catch (error) { 31 | res.status(400).json({ message: 'sadge', error: error }); 32 | } 33 | } else if (req.method === 'PUT') { 34 | // update specific order 35 | // TODO: make work with skynet 36 | res.status(400).json('Unable to handle request'); 37 | } else { 38 | 39 | res.status(400).json('Unable to handle request'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /pages/api/orders.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | const { initializeApp, cert } = require("firebase-admin/app"); 4 | const { 5 | getFirestore, 6 | Timestamp, 7 | FieldValue, 8 | } = require("firebase-admin/firestore"); 9 | const admin = require("firebase-admin"); 10 | 11 | if (process.env.FIREBASE_PRIVATE_KEY && admin.apps.length === 0) { 12 | initializeApp({ 13 | credential: cert({ 14 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 15 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 16 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"), 17 | }), 18 | }); 19 | } 20 | 21 | const db = getFirestore(); 22 | 23 | // This handler supports both GET and POST requests. 24 | // GET will return all orders. 25 | // POST will attempt to create a new order and return the resulting ID. 26 | export default async function handler( 27 | req: NextApiRequest, 28 | res: NextApiResponse 29 | ) { 30 | if (req.method === "GET") { 31 | // fetch all orders 32 | const orders = await db.collection("orders").get(); 33 | const ordersData = orders.docs.map((order: any) => ({ 34 | id: order.id, 35 | ...order.data(), 36 | })); 37 | 38 | res.status(200).json(ordersData); 39 | } else if (req.method === "POST") { 40 | // add new order with an autogenerated ID 41 | // TODO: do some validation 42 | const doc = await db.collection("orders").add(req.body); 43 | 44 | res.status(200).json(doc.id); 45 | } else { 46 | res.status(400).json("Unable to handle request"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/web3/ENSGreeter.tsx: -------------------------------------------------------------------------------- 1 | import { Image, Stack } from "@chakra-ui/react"; 2 | import { abridgeAddress } from "@utils/abridgeAddress"; 3 | import { useEffect, useState } from "react"; 4 | import { useEnsAvatar, useEnsName } from "wagmi"; 5 | import styles from "@styles/Home.module.css"; 6 | 7 | interface ENSGreeterProps { 8 | account: string; 9 | } 10 | 11 | export const ENSGreeter = ({ account }: ENSGreeterProps) => { 12 | const { data: ENSData, isError: ENSisError, isLoading: ENSIsLoading } = useEnsName({ 13 | address: account, 14 | onSuccess(data) { 15 | console.log('Success ENS data', data) 16 | }, 17 | }) 18 | 19 | const { data: ENSAvatarData } = useEnsAvatar({ 20 | addressOrName: account, 21 | onSuccess(data) { 22 | console.log('Success ENS avatar', data) 23 | }, 24 | }) 25 | 26 | const [displayName, setDisplayName] = useState(''); 27 | 28 | useEffect(() => { 29 | if (!account) { return; } 30 | const abridgedAddr = abridgeAddress(account); 31 | if (ENSData) { 32 | setDisplayName(ENSData); 33 | } else { 34 | if (abridgedAddr) { 35 | setDisplayName(abridgedAddr); 36 | } else { 37 | setDisplayName(''); 38 | } 39 | } 40 | 41 | }, [account, ENSData, ENSIsLoading, ENSisError]) 42 | return (<> 43 | 44 |

45 | Tsukiji 46 |

47 |
Hello {displayName}!
48 | {ENSAvatarData && ENS Avatar <>} 53 | />} 54 |
55 | ) 56 | }; -------------------------------------------------------------------------------- /pages/api/orders/[orderId].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | const { initializeApp, cert } = require("firebase-admin/app"); 4 | const { getFirestore } = require("firebase-admin/firestore"); 5 | const admin = require("firebase-admin"); 6 | 7 | if (process.env.FIREBASE_PRIVATE_KEY && admin.apps.length === 0) { 8 | initializeApp({ 9 | credential: cert({ 10 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 11 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 12 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"), 13 | }), 14 | }); 15 | } 16 | const db = getFirestore(); 17 | 18 | // This handler supports both GET and PUT requests. 19 | // GET will return the order associated with the specified ID. 20 | // PUT will attempt to update the order associated with the specified ID. 21 | export default async function handler( 22 | req: NextApiRequest, 23 | res: NextApiResponse 24 | ) { 25 | if (req.method === "GET") { 26 | // fetch specific order by firebase ID 27 | // e.g. Fp6DysftOikKClem31WJ 28 | // const { pid } = req.query; 29 | const ref = db.collection("orders").doc(req.query.orderId); 30 | const doc = await ref.get(); 31 | 32 | if (!doc.exists) { 33 | res.status(404).json(`Order with ID ${req.query.orderId} not found`); 34 | } 35 | 36 | res.status(200).json(doc.data()); 37 | } else if (req.method === "PUT") { 38 | // update specific order 39 | // TODO: do some validation 40 | const doc = await db 41 | .collection("orders") 42 | .doc(req.query.orderId) 43 | .set(req.body); 44 | 45 | res.status(200).json(doc.id); 46 | } else { 47 | res.status(400).json("Unable to handle request"); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /utils/createItem.tsx: -------------------------------------------------------------------------------- 1 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 2 | import { OfferItem, ConsiderationItem } from "types/tokenTypes"; 3 | import { CurrencyItem, CreateInputItem } from "@opensea/seaport-js/lib/types"; 4 | 5 | export const createOfferItem = ( 6 | type: ItemType, 7 | name: string, 8 | imageUrl: string, 9 | symbol: string, 10 | amount: string, 11 | contractAddress?: string, 12 | collectionName: string = "", 13 | tokenId: string = "" 14 | ): OfferItem => { 15 | let inputItem: CreateInputItem; 16 | if (type !== ItemType.ERC721) { 17 | inputItem = { 18 | token: contractAddress ?? undefined, 19 | amount, 20 | } as CurrencyItem; 21 | } else { 22 | inputItem = { 23 | // @ts-ignore 24 | itemType: type, 25 | token: contractAddress, 26 | identifier: tokenId, 27 | }; 28 | } 29 | return { 30 | type, 31 | inputItem, 32 | name, 33 | collectionName, 34 | image_url: imageUrl, 35 | token_id: tokenId, 36 | address: contractAddress, 37 | symbol: symbol, 38 | }; 39 | }; 40 | 41 | export const createConsiderationItem = ( 42 | type: ItemType, 43 | name: string, 44 | imageUrl: string, 45 | symbol: string, 46 | amount: string, 47 | recipient: string, 48 | contractAddress?: string, 49 | collectionName: string = "", 50 | tokenId: string = "" 51 | ): ConsiderationItem => { 52 | let inputItem: CreateInputItem; 53 | if (type !== ItemType.ERC721) { 54 | inputItem = { 55 | token: contractAddress, 56 | amount, 57 | } as CurrencyItem; 58 | } else { 59 | inputItem = { 60 | // @ts-ignore 61 | itemType: type, 62 | token: contractAddress, 63 | identifier: tokenId, 64 | recipient, 65 | }; 66 | } 67 | return { 68 | type, 69 | inputItem, 70 | name, 71 | collectionName, 72 | image_url: imageUrl, 73 | token_id: tokenId, 74 | address: contractAddress, 75 | symbol: symbol, 76 | }; 77 | }; 78 | -------------------------------------------------------------------------------- /pages/api/README.md: -------------------------------------------------------------------------------- 1 | # API README 2 | 3 | ## API Overview 4 | URI: `/api/orders` 5 | - `GET` returns all orders 6 | - `POST` writes a provided order to DB (via `req.body`) 7 | 8 | URI: `/api/orders/:orderId` 9 | - `GET` returns specified order 10 | - `PUT` updates specified order in DB (via `req.body`) 11 | 12 | URI: `/api/relatedOrders/:addressParam` 13 | - `GET` returns orders related to address param 14 | 15 | 16 | ## Order Schema 17 | ``` 18 | { 19 | "type": type string (e.g. 'offer' or 'consideration'), 20 | "items":[ 21 | { 22 | "quantity": type string (using numbers could lead to overflow), 23 | "contractAddress": type string: contract address for respective token, null for native eth", 24 | "symbol": type string (e.g. 'MAYC'), 25 | "type": type string (e.g. 'erc721'), 26 | "status": type string (e.g. 'open', 'filled', 'partially filled') 27 | } 28 | ] 29 | } 30 | ``` 31 | 32 | ## Skynet Data 33 | 34 | URI: `/api/sky-orders` 35 | - `GET` returns orders 36 | - `PUT` updates orders in Skynet File (via `req.body`) 37 | 38 | URI: `/api/orders/sky-relatedOrders/:addressParam` 39 | - `GET` returns orders related to address param 40 | 41 | ## Filecoin Data 42 | 43 | URI: `/api/filecoin-orders` 44 | - `GET` returns orders 45 | - `PUT` uploads and pins orders in IPFS (via `req.body`) 46 | 47 | ## Raw Order Schema 48 | ``` 49 | type CreateOrderInput = { 50 | conduitKey?: string; 51 | zone?: string; 52 | startTime?: string; 53 | endTime?: string; 54 | offer: readonly CreateInputItem[]; 55 | consideration: readonly ConsiderationInputItem[]; 56 | counter?: number; 57 | fees?: readonly Fee[]; 58 | allowPartialFills?: boolean; 59 | restrictedByZone?: boolean; 60 | useProxy?: boolean; 61 | salt?: string; 62 | }; 63 | ``` 64 | 65 | 66 | ## Test API Calls 67 | Get orders 68 | - [tsukiji.vercel.app/api/orders](https://tsukiji.vercel.app/api/orders) 69 | - [https://tsukiji.vercel.app/api/sky-orders](https://tsukiji.vercel.app/api/sky-orders) 70 | - [tsukiji.vercel.app/api/filecoin-orders](https://tsukiji.vercel.app/api/filecoin-orders) 71 | 72 | -------------------------------------------------------------------------------- /components/web3/CurrencyViewer.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, Image, Text, VStack } from "@chakra-ui/react"; 2 | import { NumberInput, NumberInputField } from "@chakra-ui/react"; 3 | 4 | interface CurrencyViewerProps { 5 | isWETH: boolean; 6 | currencyAmount: string; 7 | handleCurrencyInput: (value: string) => void; 8 | errorMessage: string; 9 | } 10 | 11 | export const CurrencyViewer = ({ 12 | isWETH, 13 | currencyAmount, 14 | handleCurrencyInput, 15 | errorMessage, 16 | }: CurrencyViewerProps) => { 17 | return ( 18 | <> 19 | 20 | 21 | {!isWETH ? ( 22 | 23 | ethereum logo 29 | 30 | ETH 31 | 32 | 33 | ) : ( 34 | 35 | wrapped ethereum logo 42 | 43 | WETH 44 | 45 | 46 | )} 47 | 61 | 62 | 63 | 64 | {errorMessage && {errorMessage}} 65 | 66 | 67 | ); 68 | }; 69 | -------------------------------------------------------------------------------- /styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | margin: 0; 3 | width: 100%; 4 | height: 100%; 5 | box-sizing: border-box; 6 | background-color: #000000; 7 | overflow: hidden; 8 | } 9 | 10 | .header { 11 | top: 10rem; 12 | color: #ffffff; 13 | font-weight: 500; 14 | font-size: 2rem; 15 | padding-bottom: 2rem; 16 | text-align: center; 17 | } 18 | 19 | .main { 20 | min-height: 80vh; 21 | padding: 4rem 0; 22 | flex: 1; 23 | display: flex; 24 | flex-direction: column; 25 | justify-content: center; 26 | align-items: center; 27 | } 28 | 29 | .footer { 30 | display: flex; 31 | flex: 1; 32 | padding: 2rem 0; 33 | border-top: 1px solid #eaeaea; 34 | justify-content: center; 35 | align-items: center; 36 | } 37 | 38 | .footer a { 39 | display: flex; 40 | justify-content: center; 41 | align-items: center; 42 | flex-grow: 1; 43 | } 44 | 45 | .title a { 46 | color: #0070f3; 47 | text-decoration: none; 48 | } 49 | 50 | .title a:hover, 51 | .title a:focus, 52 | .title a:active { 53 | text-decoration: underline; 54 | } 55 | 56 | .title { 57 | margin: 0; 58 | line-height: 1.15; 59 | font-size: 4rem; 60 | } 61 | 62 | .title, 63 | .description { 64 | text-align: center; 65 | } 66 | 67 | .description { 68 | margin: 4rem 0; 69 | line-height: 1.5; 70 | font-size: 1.5rem; 71 | } 72 | 73 | .code { 74 | background: #fafafa; 75 | border-radius: 5px; 76 | padding: 0.75rem; 77 | font-size: 1.1rem; 78 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 79 | Bitstream Vera Sans Mono, Courier New, monospace; 80 | } 81 | 82 | .grid { 83 | display: flex; 84 | align-items: center; 85 | justify-content: center; 86 | flex-wrap: wrap; 87 | max-width: 800px; 88 | } 89 | 90 | .logo { 91 | height: 1em; 92 | margin-left: 0.5rem; 93 | } 94 | 95 | .connectButton { 96 | position: absolute; 97 | top: 20px; 98 | right: 20px; 99 | } 100 | 101 | @media (max-width: 600px) { 102 | .grid { 103 | width: 100%; 104 | flex-direction: column; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /pages/listings.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/Listings.module.css"; 2 | import { useEffect, useState } from "react"; 3 | import { ListingCard } from "../components/ListingCard"; 4 | import { Box, SimpleGrid, Spinner } from "@chakra-ui/react"; 5 | import { OrderWithMetadata } from "types/tokenTypes"; 6 | import { NavBar } from "@components/NavBar"; 7 | import withTransition from "@components/withTransition"; 8 | 9 | type ListingsProps = { 10 | address?: string; 11 | }; 12 | 13 | const Listings = ({ address }: ListingsProps) => { 14 | const [listings, setListings] = useState([]); 15 | const [isLoading, setIsLoading] = useState(false); 16 | 17 | useEffect(() => { 18 | setIsLoading(true); 19 | const fetchOrders = async () => { 20 | try { 21 | const response = await fetch("/api/orders", { 22 | method: "GET", 23 | headers: { 24 | "content-type": "application/json", 25 | }, 26 | }); 27 | const data = await response.json(); 28 | 29 | const filteredData: OrderWithMetadata[] = data.filter( 30 | (listing: OrderWithMetadata) => listing.hasOwnProperty("order") 31 | ); 32 | 33 | setListings(filteredData); 34 | setIsLoading(false); 35 | } catch (err) { 36 | console.log("Error request: ", err); 37 | setIsLoading(false); 38 | } 39 | }; 40 | fetchOrders(); 41 | }, [address]); 42 | 43 | return ( 44 |
45 | 46 |
47 |
ALL OPEN LISTINGS
48 | {isLoading ? ( 49 | 50 | 51 | 52 | ) : ( 53 |
54 | 55 | {listings.map((listing, idx) => ( 56 | 57 | ))} 58 | 59 |
60 | )} 61 |
62 |
63 | ); 64 | }; 65 | 66 | export default withTransition(Listings); 67 | -------------------------------------------------------------------------------- /styles/NavBar.module.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | font-family: "Inter"; 3 | display: flex; 4 | color: #ffffff; 5 | position: fixed; 6 | right: 0; 7 | width: 100%; 8 | z-index: 2; 9 | align-items: center; 10 | justify-content: space-between; 11 | background-size: cover; 12 | box-sizing: border-box; 13 | padding: 0.5rem 2rem; 14 | } 15 | 16 | .homeButton { 17 | color: #ffffff; 18 | width: 100px; 19 | padding-top: 0.3rem; 20 | } 21 | 22 | .navbarRightSection { 23 | display: flex; 24 | } 25 | 26 | .hamburgerButton { 27 | margin-left: 10px; 28 | margin-top: 7px; 29 | opacity: 0.8; 30 | transition: opacity 0.3s; 31 | } 32 | 33 | .hamburgerButton:hover { 34 | opacity: 1; 35 | } 36 | 37 | .drawerBody { 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: flex-start; 41 | align-items: flex-start; 42 | } 43 | 44 | .drawerButton { 45 | background-color: transparent !important; 46 | font-size: 1.2rem !important; 47 | color: #ffffff; 48 | margin-top: 10px; 49 | margin-bottom: 10px; 50 | } 51 | 52 | .drawerButton { 53 | position: relative; 54 | color: white; 55 | text-decoration: none; 56 | padding: 0 0 0 0 !important; 57 | margin-left: 1rem; 58 | } 59 | 60 | .drawerButton:hover { 61 | color: white; 62 | } 63 | 64 | .drawerButton::before { 65 | content: ""; 66 | position: absolute; 67 | display: block; 68 | width: 100%; 69 | height: 2px; 70 | bottom: 0; 71 | left: 0; 72 | background-color: white; 73 | transform: scaleX(0); 74 | transition: transform 0.3s ease; 75 | } 76 | 77 | .drawerButton:hover::before { 78 | transform: scaleX(1); 79 | } 80 | 81 | .drawerFooter { 82 | display: flex; 83 | flex-direction: column; 84 | color: white; 85 | justify-content: flex-start; 86 | align-items: flex-start !important; 87 | } 88 | 89 | .drawerFooterLabel { 90 | padding: 0.5rem 0; 91 | width: 300px; 92 | opacity: 0.3; 93 | } 94 | 95 | .drawerFooterButton { 96 | background-color: transparent !important; 97 | color: #ffffff; 98 | width: 120px; 99 | padding-left: 0 !important; 100 | justify-content: flex-start !important; 101 | opacity: 0.3; 102 | } 103 | 104 | .drawerFooterButton:hover { 105 | opacity: 0.7; 106 | } 107 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import type { AppProps } from "next/app"; 3 | import { QueryClient, QueryClientProvider } from "react-query"; 4 | import { useEffect, useState } from "react"; 5 | import { AnimatePresence } from "framer-motion"; 6 | 7 | // rainbow + wagmi 8 | import "@rainbow-me/rainbowkit/styles.css"; 9 | import { 10 | getDefaultWallets, 11 | darkTheme, 12 | RainbowKitProvider, 13 | Theme, 14 | } from "@rainbow-me/rainbowkit"; 15 | import { chain, configureChains, createClient, WagmiConfig } from "wagmi"; 16 | import merge from "lodash.merge"; 17 | import { jsonRpcProvider } from "wagmi/providers/jsonRpc"; 18 | import { ChakraProvider } from "@chakra-ui/react"; 19 | import { NavBar } from "@components/NavBar"; 20 | 21 | const { chains, provider } = configureChains( 22 | [chain.rinkeby, chain.mainnet, chain.polygon], 23 | [ 24 | jsonRpcProvider({ 25 | rpc: (chain) => ({ 26 | // http: chain.rpcUrls.default, 27 | http: "https://eth-rinkeby.alchemyapi.io/v2/TrV45RvXdr025n8XUskersaTbPmVzVfo", 28 | }), 29 | }), 30 | ] 31 | ); 32 | 33 | const { connectors } = getDefaultWallets({ 34 | appName: "tsukiji", 35 | chains, 36 | }); 37 | 38 | const wagmiClient = createClient({ 39 | autoConnect: true, 40 | connectors, 41 | provider, 42 | }); 43 | 44 | // rainbow theme 45 | const customTheme = merge(darkTheme(), { 46 | colors: { 47 | accentColor: "#000000", 48 | }, 49 | } as Theme); 50 | 51 | const queryClient = new QueryClient(); 52 | 53 | function MyApp({ Component, pageProps, router }: AppProps) { 54 | const [mounted, setMounted] = useState(false); 55 | 56 | // prevent hydration UI bug: https://blog.saeloun.com/2021/12/16/hydration.html 57 | useEffect(() => setMounted(true), []); 58 | if (!mounted) return null; 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | ); 73 | } 74 | 75 | export default MyApp; 76 | -------------------------------------------------------------------------------- /components/ListingsSection.tsx: -------------------------------------------------------------------------------- 1 | import styles from "../styles/ListingsSection.module.css"; 2 | import { useEffect, useState } from "react"; 3 | import { ListingCard } from "./ListingCard"; 4 | import { Box, Button, SimpleGrid, Spinner, VStack } from "@chakra-ui/react"; 5 | import Link from "next/link"; 6 | import { OrderWithMetadata } from "types/tokenTypes"; 7 | 8 | type ListingsSectionProps = { 9 | address?: string; 10 | }; 11 | 12 | export const ListingsSection = ({ address }: ListingsSectionProps) => { 13 | const [listings, setListings] = useState([]); 14 | const [isLoading, setIsLoading] = useState(false); 15 | 16 | useEffect(() => { 17 | setIsLoading(true); 18 | const fetchOrders = async () => { 19 | try { 20 | const response = await fetch("/api/orders", { 21 | method: "GET", 22 | headers: { 23 | "content-type": "application/json", 24 | }, 25 | }); 26 | const data = await response.json(); 27 | 28 | const filteredData: OrderWithMetadata[] = data.filter( 29 | (listing: OrderWithMetadata) => listing.hasOwnProperty("order") 30 | ); 31 | 32 | setListings(filteredData); 33 | setIsLoading(false); 34 | } catch (err) { 35 | console.log("Error request: ", err); 36 | setIsLoading(false); 37 | } 38 | }; 39 | fetchOrders(); 40 | }, [address]); 41 | 42 | return ( 43 | 44 |
45 |
46 |
Recent Listings
47 | {isLoading ? ( 48 | 49 | 50 | 51 | ) : ( 52 |
53 | 54 | {listings.slice(0, 2).map((listing, idx) => ( 55 | 56 | ))} 57 | 58 |
59 | )} 60 | 61 | 62 | 65 | 66 | 67 | 68 | ); 69 | }; 70 | -------------------------------------------------------------------------------- /styles/Listings.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | background-color: #000000; 4 | } 5 | 6 | .header { 7 | top: 10rem; 8 | color: #ffffff; 9 | font-weight: 500; 10 | font-size: 2rem; 11 | padding-bottom: 2rem; 12 | text-align: center; 13 | } 14 | 15 | .main { 16 | min-height: 80vh; 17 | padding: 4rem 0; 18 | flex: 1; 19 | display: flex; 20 | flex-direction: column; 21 | justify-content: center; 22 | align-items: center; 23 | } 24 | 25 | .footer { 26 | display: flex; 27 | flex: 1; 28 | padding: 2rem 0; 29 | border-top: 1px solid #eaeaea; 30 | justify-content: center; 31 | align-items: center; 32 | } 33 | 34 | .footer a { 35 | display: flex; 36 | justify-content: center; 37 | align-items: center; 38 | flex-grow: 1; 39 | } 40 | 41 | .title a { 42 | color: #0070f3; 43 | text-decoration: none; 44 | } 45 | 46 | .title a:hover, 47 | .title a:focus, 48 | .title a:active { 49 | text-decoration: underline; 50 | } 51 | 52 | .title { 53 | margin: 0; 54 | line-height: 1.15; 55 | font-size: 4rem; 56 | } 57 | 58 | .title, 59 | .description { 60 | text-align: center; 61 | } 62 | 63 | .description { 64 | margin: 4rem 0; 65 | line-height: 1.5; 66 | font-size: 1.5rem; 67 | } 68 | 69 | .code { 70 | background: #fafafa; 71 | border-radius: 5px; 72 | padding: 0.75rem; 73 | font-size: 1.1rem; 74 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 75 | Bitstream Vera Sans Mono, Courier New, monospace; 76 | } 77 | 78 | .grid { 79 | display: flex; 80 | align-items: center; 81 | justify-content: center; 82 | flex-wrap: wrap; 83 | max-width: 800px; 84 | } 85 | 86 | .logo { 87 | height: 1em; 88 | margin-left: 0.5rem; 89 | } 90 | 91 | .connectButton { 92 | position: absolute; 93 | top: 20px; 94 | right: 20px; 95 | } 96 | 97 | @media (max-width: 600px) { 98 | .grid { 99 | width: 100%; 100 | flex-direction: column; 101 | } 102 | } 103 | 104 | .viewListingButton { 105 | background: rgba(255, 255, 255, 0.1); 106 | border: 1px solid rgba(255, 255, 255, 0.5); 107 | border-radius: 10px; 108 | padding: 1rem; 109 | 110 | font-family: "Inter"; 111 | font-style: normal; 112 | font-weight: 400; 113 | font-size: 14px; 114 | line-height: 17px; 115 | 116 | color: #ffffff; 117 | } 118 | -------------------------------------------------------------------------------- /pages/api/filecoin-orders.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | const { Web3Storage, Blob, File } = require('web3.storage') 4 | 5 | const { 6 | initializeApp, 7 | cert, 8 | } = require("firebase-admin/app"); 9 | const { 10 | getFirestore, 11 | } = require("firebase-admin/firestore"); 12 | const admin = require('firebase-admin'); 13 | 14 | if (process.env.FIREBASE_PRIVATE_KEY && admin.apps.length === 0) { 15 | initializeApp({ 16 | credential: cert({ 17 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 18 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 19 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, '\n') 20 | }) 21 | }); 22 | } 23 | 24 | const token: string = process.env.FILECOIN_API_KEY || ''; 25 | const storage = new Web3Storage({ token }); 26 | 27 | const db = getFirestore(); 28 | 29 | // This handler supports both GET and POST requests. 30 | // GET will return all orders. 31 | // POST will attempt to create a new order and return the resulting ID. 32 | export default async function handler( 33 | req: NextApiRequest, 34 | res: NextApiResponse 35 | ) { 36 | if (req.method === "GET") { 37 | try { 38 | const doc = await db.collection('utils').doc("filecoin").get(); 39 | const { cid } = doc.data(); 40 | if (!cid) { 41 | res.status(400).json({ message: 'sadge couldn\'t retrieve CID' }); 42 | return; 43 | } 44 | const data = await storage.get(cid); 45 | const files = await data.files(); // Web3File[] 46 | 47 | res.status(200).json({ cid: cid, data: files, file: `https://gateway.pinata.cloud/ipfs/${cid}/orders.json` }); 48 | } catch (error) { 49 | res.status(400).json({ message: 'sadge', error: error }); 50 | } 51 | } else if (req.method === "POST") { 52 | try { 53 | const blob = new Blob([JSON.stringify(req.body)], { type: 'application/json' }) 54 | const orders = [new File([blob], 'orders.json')]; 55 | const cid = await storage.put(orders) 56 | // store cid in firestore 57 | await db.collection('utils').doc("filecoin").set({ cid }); 58 | res.status(200).json({ message: 'success!', cid: cid }); 59 | } catch (error) { 60 | console.log(error); 61 | res.status(400).json({ message: 'sadge', error: error, attempt: req.body }); 62 | } 63 | } else { 64 | res.status(400).json("Unable to handle request"); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /public/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 築 地 市 場 -------------------------------------------------------------------------------- /components/FooterSection.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, VStack, Text, Spacer } from "@chakra-ui/react"; 2 | import styles from "@styles/FooterSection.module.css"; 3 | 4 | export const FooterSection = () => { 5 | return ( 6 |
7 |
8 | 9 | 10 |

Made With ❤️ for Web3

11 | No Rights Reserved. 12 | 13 | Tsukiji 2022 14 | 15 |
16 | 17 | 18 |

Docs

19 | 24 | Github 25 | 26 | 31 | Seaport 32 | 33 | 38 | ETHGlobal 39 | 40 |
41 | 42 | 43 |

Contact

44 | 49 | @jeongminc_ 50 | 51 | 56 | @andrewkjmin 57 | 58 | 63 | @straightupjac 64 | 65 |
66 |
67 |
68 |
69 | ); 70 | }; 71 | -------------------------------------------------------------------------------- /styles/Listing.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | background-color: #000000; 4 | } 5 | 6 | .addAssetButton { 7 | width: 200px; 8 | 9 | background: rgba(255, 255, 255, 0.1); 10 | border: 1px solid rgba(255, 255, 255, 0.5); 11 | border-radius: 10px; 12 | 13 | color: white; 14 | } 15 | 16 | .title { 17 | font-family: "Inter"; 18 | font-style: normal; 19 | font-weight: 500; 20 | font-size: 20px; 21 | line-height: 24px; 22 | padding-top: 1rem; 23 | 24 | color: #ffffff; 25 | } 26 | 27 | .subtitle { 28 | font-family: "Inter"; 29 | font-style: normal; 30 | font-weight: 500; 31 | font-size: 15px; 32 | line-height: 24px; 33 | padding-top: 1rem; 34 | 35 | color: #ffffff; 36 | } 37 | 38 | .itemBundleContainer { 39 | align-items: flex-start !important; 40 | } 41 | 42 | .itemListContainer { 43 | width: 800px; 44 | border: 1px solid rgba(255, 255, 255, 0.5); 45 | border-radius: 10px; 46 | justify-content: flex-start; 47 | overflow-y: hidden; 48 | overflow-x: scroll; 49 | padding: 1rem; 50 | } 51 | 52 | .itemContainer { 53 | overflow: hidden; 54 | } 55 | 56 | .itemImageCard { 57 | width: 130px; 58 | height: 130px; 59 | object-fit: cover; 60 | } 61 | 62 | .itemTextCard { 63 | display: flex; 64 | width: 130px; 65 | height: 30px; 66 | justify-content: center; 67 | color: white; 68 | padding: 0.5rem; 69 | font-size: small; 70 | overflow: hidden; 71 | } 72 | 73 | .listItemQuantity { 74 | font-family: "Inter"; 75 | font-style: normal; 76 | font-weight: 400; 77 | line-height: 24px; 78 | 79 | color: #ffffff; 80 | } 81 | 82 | .listItemRemoveIcon { 83 | opacity: 0.5; 84 | } 85 | 86 | .light { 87 | background: rgba(255, 255, 255, 0.1); 88 | } 89 | 90 | .listingContent { 91 | padding-top: 10rem; 92 | } 93 | 94 | .listingContainer { 95 | display: flex; 96 | flex-direction: column; 97 | margin: auto; 98 | width: 1000px; 99 | padding-top: 1rem; 100 | border: 1px solid rgba(255, 255, 255, 0.3); 101 | border-radius: 10px; 102 | align-items: center; 103 | } 104 | 105 | .modalContent { 106 | width: 1097px; 107 | height: 690px; 108 | 109 | background: #000000 !important; 110 | border: 1px solid rgba(255, 255, 255, 0.5); 111 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 112 | border-radius: 20px; 113 | } 114 | 115 | .modalOverlay { 116 | background: rgba(0, 0, 0, 0.5); 117 | backdrop-filter: blur(8px); 118 | /* Note: backdrop-filter has minimal browser support */ 119 | } 120 | 121 | .tab[aria-selected="true"] { 122 | color: white !important; 123 | } 124 | 125 | .tab[aria-selected="false"] { 126 | border-color: grey !important; 127 | color: grey; 128 | } 129 | -------------------------------------------------------------------------------- /components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Drawer, 3 | DrawerBody, 4 | DrawerFooter, 5 | DrawerOverlay, 6 | DrawerContent, 7 | DrawerCloseButton, 8 | Button, 9 | IconButton, 10 | } from "@chakra-ui/react"; 11 | import styles from "@styles/NavBar.module.css"; 12 | import Link from "next/link"; 13 | import { FaGithub } from "react-icons/fa"; 14 | 15 | type SidebarProps = { 16 | isOpen: boolean; 17 | onClose: () => void; 18 | }; 19 | 20 | export const Sidebar = ({ isOpen, onClose }: SidebarProps) => { 21 | return ( 22 | 23 | 24 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
Made With ❤️ By
48 | 57 | 66 | 75 | 80 | } 86 | /> 87 | 88 |
89 |
90 |
91 | ); 92 | }; 93 | -------------------------------------------------------------------------------- /constants/pinnedCollections.ts: -------------------------------------------------------------------------------- 1 | const Azuki = { 2 | name: "Azuki", 3 | image_url: 4 | "https://lh3.googleusercontent.com/H8jOCJuQokNqGBpkBN5wk1oZwO7LM8bNnrHCaekV2nKjnCqw6UB5oaH8XyNeBDj6bA_n1mjejzhFQUP3O1NfjFLHr3FOaeHcTOOT=s168", 5 | address: "0xae6Bbc3C94D1A6C086AF6a9774B4434A58C793bf", 6 | }; 7 | const BAYC = { 8 | name: "Bored Ape Yacht Club", 9 | image_url: 10 | "https://lh3.googleusercontent.com/Ju9CkWtV-1Okvf45wo8UctR-M9He2PjILP0oOvxE89AyiPPGtrR3gysu1Zgy0hjd2xKIgjJJtWIc0ybj4Vd7wv8t3pxDGHoJBzDB=s168", 11 | address: "0xF865E7c71432EE1c57047f6280c2de39214b5b7A", 12 | }; 13 | const CoolCats = { 14 | name: "Cool Cats", 15 | image_url: 16 | "https://lh3.googleusercontent.com/LIov33kogXOK4XZd2ESj29sqm_Hww5JSdO7AFn5wjt8xgnJJ0UpNV9yITqxra3s_LMEW1AnnrgOVB_hDpjJRA1uF4skI5Sdi_9rULi8=s168", 17 | address: "0x3dAd63203f1A62724DAcb6A473fE9AE042e2ecc3", 18 | }; 19 | const MAYC = { 20 | name: "Mutant Ape Yacht Club", 21 | image_url: 22 | "https://lh3.googleusercontent.com/lHexKRMpw-aoSyB1WdFBff5yfANLReFxHzt1DOj_sg7mS14yARpuvYcUtsyyx-Nkpk6WTcUPFoG53VnLJezYi8hAs0OxNZwlw6Y-dmI=s168", 23 | address: "0xf0d554b751fE43f1A80dd693A30583f041bAc3A5", 24 | }; 25 | const Doodles = { 26 | name: "Doodles", 27 | image_url: 28 | "https://lh3.googleusercontent.com/7B0qai02OdHA8P_EOVK672qUliyjQdQDGNrACxs7WnTgZAkJa_wWURnIFKeOh5VTf8cfTqW3wQpozGedaC9mteKphEOtztls02RlWQ=s168", 29 | address: "0xF6e19DBFdc5a86648c6174c860964F901712c1C4", 30 | }; 31 | const Goblintown = { 32 | name: "Goblintown", 33 | image_url: 34 | "https://lh3.googleusercontent.com/cb_wdEAmvry_noTfeuQzhqKpghhZWQ_sEhuGS9swM03UM8QMEVJrndu0ZRdLFgGVqEPeCUzOHGTUllxug9U3xdvt0bES6VFdkRCKPqg=s168", 35 | address: "0x16fF7dca5A520841e646AF9C927F32F56419c16c", 36 | }; 37 | const Meebits = { 38 | name: "Meebits", 39 | image_url: 40 | "https://lh3.googleusercontent.com/d784iHHbqQFVH1XYD6HoT4u3y_Fsu_9FZUltWjnOzoYv7qqB5dLUqpGyHBd8Gq3h4mykK5Enj8pxqOUorgD2PfIWcVj9ugvu8l0=s0", 41 | address: "0x2fCEb846CFAbd8e26B63256aEd5029F7365af714", 42 | }; 43 | const WorldOfWomen = { 44 | name: "World of Women", 45 | image_url: 46 | "https://lh3.googleusercontent.com/EFAQpIktMBU5SU0TqSdPWZ4byHr3hFirL_mATsR8KWhM5z-GJljX8E73V933lkyKgv2SAFlfRRjGsWvWbQQmJAwu3F2FDXVa1C9F=s168", 47 | address: "0x65d5e1e27159d6758982ac6d2952099D364a33E0", 48 | }; 49 | const CloneX = { 50 | name: "CloneX", 51 | image_url: 52 | "https://lh3.googleusercontent.com/XN0XuD8Uh3jyRWNtPTFeXJg_ht8m5ofDx6aHklOiy4amhFuWUa0JaR6It49AH8tlnYS386Q0TW_-Lmedn0UET_ko1a3CbJGeu5iHMg=s168", 53 | address: "0x45F59541c942CC7cc2319785c9d39F9b1DF35013", 54 | }; 55 | const Coven = { 56 | name: "Crypto Coven", 57 | image_url: 58 | "https://lh3.googleusercontent.com/E8MVasG7noxC0Fa_duhnexc2xze1PzT1jzyeaHsytOC4722C2Zeo7EhUR8-T6mSem9-4XE5ylrCtoAsceZ_lXez_kTaMufV5pfLc3Fk=s0", 59 | address: "0xA6C71b373E6c6daAb5041B26a9D94EbD6D288A81", 60 | }; 61 | 62 | export const pinnedCollections = [ 63 | Azuki, 64 | BAYC, 65 | CoolCats, 66 | MAYC, 67 | Doodles, 68 | Goblintown, 69 | Meebits, 70 | WorldOfWomen, 71 | CloneX, 72 | Coven, 73 | ]; 74 | -------------------------------------------------------------------------------- /styles/Explorer.module.css: -------------------------------------------------------------------------------- 1 | .addAssetButton { 2 | width: 180px; 3 | height: 57px; 4 | 5 | background: rgba(255, 255, 255, 0.1); 6 | border: 1px solid rgba(255, 255, 255, 0.5); 7 | border-radius: 10px; 8 | 9 | color: white; 10 | } 11 | 12 | .itemListHeader { 13 | display: flex; 14 | width: 100%; 15 | justify-content: space-between; 16 | padding: 0 3rem; 17 | } 18 | 19 | .listItemOfferer { 20 | width: 100px; 21 | } 22 | 23 | .title { 24 | font-family: "Inter"; 25 | font-style: normal; 26 | font-weight: 500; 27 | font-size: 20px; 28 | line-height: 24px; 29 | padding-top: 1rem; 30 | 31 | color: #ffffff; 32 | } 33 | 34 | .itemListContainer { 35 | width: 1200px; 36 | height: 1000px; 37 | left: 137px; 38 | top: 328px; 39 | 40 | margin: 1rem; 41 | 42 | display: flex; 43 | flex-direction: column; 44 | 45 | border: 1px solid rgba(255, 255, 255, 0.2); 46 | overflow: scroll; 47 | } 48 | 49 | .listItemContainer { 50 | display: flex; 51 | justify-content: space-between; 52 | width: 100%; 53 | padding: 0.5rem 2rem; 54 | color: white; 55 | } 56 | 57 | .listItemImage { 58 | width: 38px; 59 | height: 38px; 60 | left: 171px; 61 | top: 344px; 62 | 63 | margin-right: 1rem; 64 | 65 | border-radius: 50%; 66 | } 67 | 68 | .listItemLabel { 69 | display: flex; 70 | flex-direction: column; 71 | flex-grow: 2; 72 | align-items: flex-start !important; 73 | } 74 | 75 | .listItemTitle { 76 | color: white; 77 | } 78 | 79 | .listItemSubtitle { 80 | font-family: "Inter"; 81 | font-style: normal; 82 | font-weight: 400; 83 | font-size: 14px; 84 | margin-top: 0 !important; 85 | 86 | color: rgba(255, 255, 255, 0.5); 87 | } 88 | 89 | .listItemQuantity { 90 | font-family: "Inter"; 91 | font-style: normal; 92 | font-weight: 400; 93 | line-height: 24px; 94 | 95 | color: #ffffff; 96 | } 97 | 98 | .listItemRemoveIcon { 99 | opacity: 0.5; 100 | } 101 | 102 | .light { 103 | background: rgba(255, 255, 255, 0.1); 104 | } 105 | 106 | .tokenSelectionContainer { 107 | display: flex; 108 | margin: auto; 109 | margin-top: 2rem; 110 | flex-direction: column; 111 | width: 1500px; 112 | height: 800px; 113 | left: 78px; 114 | top: 255px; 115 | 116 | padding: 1rem; 117 | 118 | align-items: center; 119 | 120 | border: 1px solid #dddddd; 121 | border-radius: 10px; 122 | } 123 | 124 | .modalContent { 125 | width: 1097px; 126 | height: 690px; 127 | 128 | background: #000000 !important; 129 | border: 1px solid rgba(255, 255, 255, 0.5); 130 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 131 | border-radius: 20px; 132 | } 133 | 134 | .modalOverlay { 135 | background: rgba(0, 0, 0, 0.5); 136 | backdrop-filter: blur(8px); 137 | /* Note: backdrop-filter has minimal browser support */ 138 | } 139 | 140 | .tab[aria-selected="true"] { 141 | color: white !important; 142 | } 143 | 144 | .tab[aria-selected="false"] { 145 | border-color: grey !important; 146 | color: grey; 147 | } 148 | -------------------------------------------------------------------------------- /pages/explorer.tsx: -------------------------------------------------------------------------------- 1 | import { Button, HStack, VStack, IconButton } from "@chakra-ui/react"; 2 | import { useEffect, useState } from "react"; 3 | import styles from "@styles/Explorer.module.css"; 4 | import { OrderWithCounter } from "@opensea/seaport-js/lib/types"; 5 | import { abridgeAddress } from "@utils/abridgeAddress"; 6 | 7 | const OrderExplorer = () => { 8 | const [orders, setOrders] = useState([]); 9 | 10 | useEffect(() => { 11 | const fetchOrders = async () => { 12 | try { 13 | const response = await fetch(`/api/orders`, { 14 | method: "GET", 15 | headers: { 16 | "content-type": "application/json", 17 | }, 18 | }); 19 | const data = await response.json(); 20 | 21 | setOrders(data); 22 | } catch (err) { 23 | console.log("Error request: ", err); 24 | } 25 | }; 26 | fetchOrders(); 27 | }, []); 28 | 29 | return ( 30 |
31 |

TSUKIJI ORDER EXPLORER

32 | 33 | 34 | 35 |
Offerer
36 |
Start Time
37 |
# of Offers
38 |
# of Considerations
39 |
Estimated Value
40 |
Payload Signature
41 |
42 | {orders.map((order, idx) => ( 43 | 48 | ))} 49 |
50 | 51 | 54 |
55 | ); 56 | }; 57 | 58 | type ListItemProps = { 59 | item: OrderWithCounter; 60 | isLight: boolean; 61 | }; 62 | 63 | const ListItem = ({ item, isLight }: ListItemProps) => { 64 | return ( 65 | 68 | {/* */} 69 |
70 | {abridgeAddress(item.parameters.offerer)} 71 |
72 |
73 | {item.parameters.startTime.toString()} 74 |
75 |
{item.parameters.offer.length}
76 |
77 | {item.parameters.consideration.length} 78 |
79 |
80 | {`$${(Math.random() * 3000 + 9000).toFixed(2)}`} 81 |
82 |
83 | {abridgeAddress(item.signature)} 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default OrderExplorer; 90 | -------------------------------------------------------------------------------- /styles/SubtitleSection.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: black; 3 | overflow: hidden; 4 | width: 100%; 5 | position: relative; 6 | border-top: 1px solid rgba(255, 255, 255, 0.1); 7 | } 8 | 9 | .centerEllipse { 10 | left: 50%; 11 | transform: translateX(-50%); 12 | bottom: -715px; 13 | width: 1000px; 14 | height: 1000px; 15 | background-image: url(/assets/ellipse.png); 16 | background-size: contain; 17 | background-repeat: no-repeat; 18 | position: absolute; 19 | pointer-events: none; 20 | opacity: 0.3; 21 | } 22 | 23 | .content { 24 | width: 1440px; 25 | max-width: 100%; 26 | margin: 0px auto; 27 | padding: 0px 20px; 28 | margin: 100px auto; 29 | justify-content: center; 30 | align-items: center; 31 | height: 300px; 32 | width: 1280px; 33 | max-width: 100%; 34 | } 35 | 36 | .header { 37 | font-family: "Inter"; 38 | font-style: normal; 39 | font-weight: 700; 40 | font-size: 40px; 41 | line-height: 45px; 42 | text-align: center; 43 | 44 | color: #ffffff; 45 | } 46 | 47 | .subheader { 48 | color: rgb(255, 255, 255, 0.7); 49 | font-size: 20px; 50 | } 51 | 52 | .seaportLogo { 53 | width: 400px; 54 | opacity: 0.6; 55 | -webkit-filter: drop-shadow(2px 2px 10px rgb(255, 255, 255, 0.7)); 56 | filter: drop-shadow(0px 2px 50px rgb(255, 255, 255, 0.7)); 57 | } 58 | 59 | .createButton { 60 | padding: 1rem; 61 | 62 | font-family: "Inter"; 63 | font-style: normal; 64 | font-weight: 600; 65 | font-size: 18px; 66 | line-height: 17px; 67 | 68 | width: 220px; 69 | height: 50px; 70 | border: none; 71 | outline: none; 72 | color: #fff; 73 | background: #111; 74 | cursor: pointer; 75 | position: relative; 76 | z-index: 0; 77 | border-radius: 10px; 78 | } 79 | 80 | /* credits to https://codepen.io/kocsten */ 81 | .createButton:before { 82 | content: ""; 83 | background: linear-gradient( 84 | 45deg, 85 | #ff0000, 86 | #ff7300, 87 | #fffb00, 88 | #48ff00, 89 | #00ffd5, 90 | #002bff, 91 | #7a00ff, 92 | #ff00c8, 93 | #ff0000 94 | ); 95 | position: absolute; 96 | top: -2px; 97 | left: -2px; 98 | background-size: 400%; 99 | z-index: -1; 100 | filter: blur(5px); 101 | width: calc(100% + 4px); 102 | height: calc(100% + 4px); 103 | animation: glowing 20s linear infinite; 104 | opacity: 0; 105 | transition: opacity 0.3s ease-in-out; 106 | border-radius: 10px; 107 | } 108 | 109 | .createButton:active { 110 | color: #000; 111 | } 112 | 113 | .createButton:active:after { 114 | background: transparent; 115 | } 116 | 117 | .createButton:hover:before { 118 | opacity: 1; 119 | } 120 | 121 | .createButton:after { 122 | z-index: -1; 123 | content: ""; 124 | position: absolute; 125 | width: 100%; 126 | height: 100%; 127 | background: #111; 128 | left: 0; 129 | top: 0; 130 | border-radius: 10px; 131 | } 132 | 133 | @keyframes glowing { 134 | 0% { 135 | background-position: 0 0; 136 | } 137 | 50% { 138 | background-position: 400% 0; 139 | } 140 | 100% { 141 | background-position: 0 0; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /styles/Create.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | background-color: #000000; 4 | } 5 | 6 | .main { 7 | min-height: 100vh; 8 | padding: 4rem 0; 9 | flex: 1; 10 | display: flex; 11 | flex-direction: column; 12 | justify-content: center; 13 | align-items: center; 14 | } 15 | 16 | .header { 17 | top: 10rem; 18 | color: #ffffff; 19 | font-weight: 500; 20 | font-size: 2rem; 21 | padding-bottom: 3rem; 22 | } 23 | 24 | .tipContainer { 25 | width: 365px; 26 | height: 82px; 27 | left: 78px; 28 | top: 863px; 29 | 30 | border: 1px solid rgba(255, 255, 255, 0.5); 31 | border-radius: 10px; 32 | } 33 | 34 | .bottomContainer { 35 | display: flex; 36 | padding-top: 1rem; 37 | align-items: flex-start; 38 | justify-content: flex-start; 39 | } 40 | 41 | .footer { 42 | display: flex; 43 | flex: 1; 44 | padding: 2rem 0; 45 | border-top: 1px solid #eaeaea; 46 | justify-content: center; 47 | align-items: center; 48 | } 49 | 50 | .footer a { 51 | display: flex; 52 | justify-content: center; 53 | align-items: center; 54 | flex-grow: 1; 55 | } 56 | 57 | .title a { 58 | color: #0070f3; 59 | text-decoration: none; 60 | } 61 | 62 | .title a:hover, 63 | .title a:focus, 64 | .title a:active { 65 | text-decoration: underline; 66 | } 67 | 68 | .title { 69 | width: 125px; 70 | height: 24px; 71 | left: 378px; 72 | top: 283px; 73 | 74 | font-family: "Inter"; 75 | font-style: normal; 76 | font-weight: 500; 77 | font-size: 20px; 78 | line-height: 24px; 79 | 80 | color: #ffffff; 81 | } 82 | 83 | .title, 84 | .description { 85 | text-align: center; 86 | } 87 | 88 | .description { 89 | margin: 4rem 0; 90 | line-height: 1.5; 91 | font-size: 1.5rem; 92 | } 93 | 94 | .code { 95 | background: #fafafa; 96 | border-radius: 5px; 97 | padding: 0.75rem; 98 | font-size: 1.1rem; 99 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 100 | Bitstream Vera Sans Mono, Courier New, monospace; 101 | } 102 | 103 | .grid { 104 | display: flex; 105 | align-items: center; 106 | justify-content: center; 107 | flex-wrap: wrap; 108 | max-width: 800px; 109 | } 110 | 111 | .logo { 112 | height: 1em; 113 | margin-left: 0.5rem; 114 | } 115 | 116 | .connectButton { 117 | position: absolute; 118 | top: 20px; 119 | right: 20px; 120 | } 121 | 122 | .confirmListingButton { 123 | width: 286px; 124 | height: 70px !important; 125 | 126 | background: rgba(255, 255, 255, 0.1); 127 | border: 1px solid #ffffff; 128 | border-radius: 10px; 129 | } 130 | 131 | @media (max-width: 600px) { 132 | .grid { 133 | width: 100%; 134 | flex-direction: column; 135 | } 136 | } 137 | 138 | .selector select { 139 | color: rgba(255, 255, 255, 0.7); /*hide original SELECT element: */ 140 | width: 200px; 141 | height: 35px; 142 | 143 | border: 1px solid rgba(255, 255, 255, 0.5); 144 | border-radius: 10px; 145 | } 146 | 147 | .currencySwitch { 148 | padding-top: 2px; 149 | height: 35px; 150 | } 151 | 152 | .input { 153 | color: rgba(255, 255, 255, 0.7); 154 | width: 365px; 155 | height: 65px; 156 | 157 | border-color: rgba(255, 255, 255, 0.6); 158 | 159 | border-radius: 10px; 160 | } 161 | 162 | .input input[type="text"] { 163 | height: 4rem; 164 | border-color: rgba(255, 255, 255, 0.6); 165 | } 166 | -------------------------------------------------------------------------------- /styles/TokenSelection.module.css: -------------------------------------------------------------------------------- 1 | .addAssetButton { 2 | width: 180px; 3 | height: 57px; 4 | 5 | background: rgba(255, 255, 255, 0.1); 6 | border: 1px solid rgba(255, 255, 255, 0.5); 7 | border-radius: 10px; 8 | 9 | color: white; 10 | } 11 | 12 | .title { 13 | font-family: "Inter"; 14 | font-style: normal; 15 | font-weight: 500; 16 | font-size: 20px; 17 | line-height: 24px; 18 | padding-top: 1rem; 19 | 20 | color: #ffffff; 21 | } 22 | 23 | .itemListContainer { 24 | width: 621px; 25 | height: 351px; 26 | left: 137px; 27 | top: 328px; 28 | 29 | margin: 1rem; 30 | 31 | border: 1px solid rgba(255, 255, 255, 0.2); 32 | overflow: scroll; 33 | } 34 | 35 | .listItemContainer { 36 | display: flex; 37 | justify-content: space-between; 38 | width: 100%; 39 | padding: 0.5rem 2rem; 40 | color: white; 41 | } 42 | 43 | .listItemImage { 44 | width: 38px; 45 | height: 38px; 46 | left: 171px; 47 | top: 344px; 48 | 49 | margin-right: 1rem; 50 | 51 | border-radius: 50%; 52 | } 53 | 54 | .listItemLabel { 55 | display: flex; 56 | flex-direction: column; 57 | flex-grow: 2; 58 | align-items: flex-start !important; 59 | } 60 | 61 | .listItemTitle { 62 | color: white; 63 | } 64 | 65 | .listItemSubtitle { 66 | font-family: "Inter"; 67 | font-style: normal; 68 | font-weight: 400; 69 | font-size: 14px; 70 | margin-top: 0 !important; 71 | 72 | color: rgba(255, 255, 255, 0.5); 73 | } 74 | 75 | .listItemQuantity { 76 | font-family: "Inter"; 77 | font-style: normal; 78 | font-weight: 400; 79 | line-height: 24px; 80 | 81 | color: #ffffff; 82 | } 83 | 84 | .listItemRemoveIcon { 85 | opacity: 0.5; 86 | } 87 | 88 | .light { 89 | background: rgba(255, 255, 255, 0.1); 90 | } 91 | 92 | .tokenSelectionContainer { 93 | display: flex; 94 | flex-direction: column; 95 | width: 733px; 96 | height: 513px; 97 | left: 78px; 98 | top: 255px; 99 | 100 | padding: 1rem; 101 | 102 | align-items: center; 103 | 104 | border: 1px solid #dddddd; 105 | border-radius: 10px; 106 | } 107 | 108 | .modalContent { 109 | width: 1097px; 110 | height: 690px; 111 | 112 | background: #000000 !important; 113 | border: 1px solid rgba(255, 255, 255, 0.5); 114 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 115 | border-radius: 20px; 116 | } 117 | 118 | .modalOverlay { 119 | background: rgba(0, 0, 0, 0.5); 120 | backdrop-filter: blur(8px); 121 | /* Note: backdrop-filter has minimal browser support */ 122 | } 123 | 124 | .modalBody { 125 | overflow: hidden; 126 | } 127 | 128 | .tab[aria-selected="true"] { 129 | color: white !important; 130 | transition: .3s; 131 | } 132 | 133 | .tab[aria-selected="false"] { 134 | border-color: grey !important; 135 | color: grey; 136 | transition: .3s; 137 | } 138 | 139 | .tab:active { 140 | background-color: black !important; 141 | } 142 | 143 | .grid { 144 | height: 540px; 145 | overflow-x: hidden; 146 | overflow-y: scroll; 147 | -ms-overflow-style: none; /* Internet Explorer 10+ */ 148 | scrollbar-width: none; /* Firefox */ 149 | } 150 | 151 | .grid::-webkit-scrollbar { 152 | width: 0; /* Remove scrollbar space */ 153 | background: transparent; /* Optional: just make scrollbar invisible */ 154 | } 155 | 156 | .tabPanel { 157 | overflow: hidden; 158 | } 159 | 160 | .noTokenMessage { 161 | width: 100%; 162 | text-align: center; 163 | color: white; 164 | } 165 | 166 | .noResultMessage { 167 | width: 100%; 168 | text-align: center; 169 | color: white; 170 | } -------------------------------------------------------------------------------- /styles/ListingCard.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | box-sizing: border-box; 3 | 4 | width: 600px; 5 | 6 | border: 1px solid rgba(255, 255, 255, 0.4); 7 | border-radius: 10px; 8 | padding: 1.5rem 2rem; 9 | display: flex; 10 | justify-content: center; 11 | align-items: center; 12 | 13 | background-color: #000000; 14 | } 15 | 16 | .offerHeader { 17 | width: 100%; 18 | 19 | font-family: "Inter"; 20 | font-style: normal; 21 | font-weight: 500; 22 | font-size: 20px; 23 | line-height: 24px; 24 | 25 | display: flex; 26 | justify-content: space-between; 27 | color: #ffffff; 28 | } 29 | 30 | .offerHeaderButton { 31 | width: 156px; 32 | height: 38px; 33 | 34 | background: rgba(255, 255, 255, 0.1); 35 | border: 1px solid rgba(255, 255, 255, 0.5); 36 | border-radius: 10px; 37 | 38 | font-family: "Inter"; 39 | font-style: normal; 40 | font-weight: 400; 41 | font-size: 16px; 42 | line-height: 17px; 43 | 44 | color: #ffffff; 45 | 46 | opacity: 0.7; 47 | 48 | transition: 0.3s; 49 | } 50 | 51 | .offerHeaderButton:hover { 52 | opacity: 1; 53 | } 54 | 55 | .offerTextContainer { 56 | max-height: 172px; 57 | width: 100px; 58 | padding-left: 0.5rem; 59 | overflow-y: scroll; 60 | } 61 | 62 | .offerCollectionLabel { 63 | display: flex; 64 | width: 100%; 65 | justify-content: flex-start; 66 | align-items: center; 67 | } 68 | 69 | .offerCollectionLabelImg { 70 | width: 32px; 71 | height: 32px; 72 | left: 625px; 73 | top: 345px; 74 | 75 | border-radius: 10px; 76 | } 77 | 78 | .offerCollectionLabelText { 79 | font-family: "Inter"; 80 | font-style: normal; 81 | font-weight: 300; 82 | font-size: 17px; 83 | line-height: 20px; 84 | 85 | color: rgba(255, 255, 255, 0.9); 86 | } 87 | 88 | .offerContainer { 89 | box-sizing: border-box; 90 | 91 | width: 570px; 92 | height: 172px; 93 | left: 115px; 94 | top: 277px; 95 | 96 | border: 1px solid rgba(255, 255, 255, 0.2); 97 | border-radius: 5px; 98 | padding: 1rem; 99 | } 100 | 101 | .offerImageCard { 102 | width: 140px; 103 | height: 140px; 104 | 105 | border-radius: 10px; 106 | } 107 | 108 | .offerImageContainer { 109 | width: 450px; 110 | overflow-y: hidden; 111 | overflow-x: scroll; 112 | } 113 | 114 | .offerImageContainer::-webkit-scrollbar { 115 | width: 0; /* Remove scrollbar space */ 116 | background: transparent; /* Optional: just make scrollbar invisible */ 117 | } 118 | 119 | .considerationHeader { 120 | width: 100%; 121 | 122 | font-family: "Inter"; 123 | font-style: normal; 124 | font-weight: 500; 125 | font-size: 20px; 126 | line-height: 24px; 127 | 128 | color: #ffffff; 129 | padding-top: 1rem; 130 | } 131 | 132 | .considerationContainer { 133 | box-sizing: border-box; 134 | 135 | width: 570px; 136 | 137 | border: 1px solid rgba(255, 255, 255, 0.2); 138 | border-radius: 5px; 139 | padding: 1rem; 140 | } 141 | 142 | .considerationItem { 143 | box-sizing: border-box; 144 | 145 | display: flex; 146 | justify-content: center; 147 | align-items: center; 148 | 149 | width: 164px; 150 | height: 74px; 151 | 152 | border: 1px solid rgba(255, 255, 255, 0.5); 153 | border-radius: 10px; 154 | } 155 | 156 | .considerationItemText { 157 | font-family: "Inter"; 158 | font-style: normal; 159 | font-weight: 400; 160 | font-size: 20px; 161 | line-height: 24px; 162 | 163 | color: #ffffff; 164 | } 165 | 166 | .considerationItemImg { 167 | width: 40px; 168 | height: 40px; 169 | 170 | padding-right: 1rem; 171 | border-radius: 10px; 172 | } 173 | -------------------------------------------------------------------------------- /styles/TitleSection.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | background-color: black; 3 | overflow: hidden; 4 | width: 100%; 5 | position: relative; 6 | border-top: 1px solid rgba(255, 255, 255, 0.1); 7 | } 8 | 9 | .leftEllipse { 10 | top: -295px; 11 | left: -700px; 12 | width: 1000px; 13 | height: 1000px; 14 | background-image: url(/assets/ellipse.png); 15 | background-size: contain; 16 | background-repeat: no-repeat; 17 | position: absolute; 18 | pointer-events: none; 19 | opacity: 0.3; 20 | } 21 | 22 | .rightEllipse { 23 | bottom: -400px; 24 | right: -650px; 25 | width: 1000px; 26 | height: 1000px; 27 | background-image: url(/assets/ellipse.png); 28 | background-size: contain; 29 | background-repeat: no-repeat; 30 | position: absolute; 31 | pointer-events: none; 32 | opacity: 0.3; 33 | } 34 | 35 | .content { 36 | margin: 100px auto; 37 | justify-content: space-between; 38 | align-items: center; 39 | height: 500px; 40 | width: 1200px; 41 | max-width: 100%; 42 | position: relative; 43 | } 44 | 45 | .header { 46 | font-family: "Inter"; 47 | font-style: normal; 48 | font-weight: 700; 49 | font-size: 60px; 50 | line-height: 65px; 51 | width: 600px; 52 | text-align: center; 53 | 54 | color: #ffffff; 55 | } 56 | 57 | .subheader { 58 | color: rgb(255, 255, 255, 0.7); 59 | font-size: 20px; 60 | } 61 | 62 | .animationStack { 63 | display: flex; 64 | justify-content: flex-start !important; 65 | padding-right: 6rem; 66 | } 67 | 68 | .seaportLogo { 69 | width: 400px; 70 | opacity: 0.4; 71 | -webkit-filter: drop-shadow(2px 2px 10px rgb(255, 255, 255, 0.7)); 72 | filter: drop-shadow(0px 2px 50px rgb(255, 255, 255, 0.7)); 73 | } 74 | 75 | .movingImage { 76 | width: 100px; 77 | height: 100px; 78 | border-radius: 50%; 79 | opacity: 0.9; 80 | -webkit-filter: drop-shadow(2px 2px 20px rgb(255, 255, 255, 0.3)); 81 | filter: drop-shadow(0px 2px 20px rgb(255, 255, 255, 0.3)); 82 | } 83 | 84 | .bobu { 85 | position: absolute; 86 | left: 50px; 87 | bottom: 50px; 88 | } 89 | 90 | .ape { 91 | position: absolute; 92 | left: 50px; 93 | bottom: 50px; 94 | } 95 | 96 | .doodle { 97 | filter: brightness(90%); 98 | } 99 | 100 | .offerImages { 101 | position: relative; 102 | display: flex; 103 | } 104 | 105 | .svg { 106 | z-index: 1; 107 | width: 50vw; 108 | max-width: 40rem; 109 | height: auto; 110 | position: absolute; 111 | } 112 | 113 | .logo, 114 | .sparkles { 115 | animation: logoReveal 7s infinite ease-in-out; 116 | } 117 | 118 | @keyframes logoReveal { 119 | 0% { 120 | opacity: 0; 121 | } 122 | 10% { 123 | opacity: 0; 124 | } 125 | 25% { 126 | opacity: 1; 127 | } 128 | 90% { 129 | opacity: 1; 130 | } 131 | 100% { 132 | opacity: 0; 133 | } 134 | } 135 | 136 | .sparkles path { 137 | fill: white; 138 | transform-origin: 50% 50%; 139 | transform-box: fill-box; 140 | animation: sparkle var(--duration) var(--delay) infinite ease-in-out; 141 | } 142 | 143 | @keyframes sparkle { 144 | 0% { 145 | transform: scale(0); 146 | } 147 | 50% { 148 | transform: scale(0); 149 | } 150 | 70% { 151 | transform: scale(-1, 0); 152 | } 153 | 80% { 154 | transform: scale(1); 155 | } 156 | 100% { 157 | transform: scale(0); 158 | } 159 | } 160 | 161 | .sparkle1 { 162 | --duration: 2s; 163 | --delay: 0s; 164 | } 165 | .sparkle2 { 166 | --duration: 1.5s; 167 | --delay: 0.9s; 168 | } 169 | .sparkle3 { 170 | --duration: 1.7s; 171 | --delay: 0.4s; 172 | } 173 | .sparkle4 { 174 | --duration: 2.1s; 175 | --delay: 1.1s; 176 | } 177 | -------------------------------------------------------------------------------- /styles/ListingsSection.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 3rem; 3 | background-color: black; 4 | border-top: 1px solid rgba(255, 255, 255, 0.1); 5 | z-index: 10; 6 | width: 100%; 7 | position: relative; 8 | } 9 | 10 | .leftEllipse { 11 | top: -295px; 12 | left: -700px; 13 | width: 1000px; 14 | height: 1000px; 15 | background-image: url(/assets/ellipse.png); 16 | background-size: contain; 17 | background-repeat: no-repeat; 18 | position: absolute; 19 | pointer-events: none; 20 | opacity: 0.3; 21 | } 22 | 23 | .rightEllipse { 24 | bottom: -400px; 25 | right: -650px; 26 | width: 1000px; 27 | height: 1000px; 28 | background-image: url(/assets/ellipse.png); 29 | background-size: contain; 30 | background-repeat: no-repeat; 31 | position: absolute; 32 | pointer-events: none; 33 | opacity: 0.3; 34 | } 35 | 36 | .header { 37 | font-family: "Inter"; 38 | font-style: normal; 39 | font-weight: 700; 40 | font-size: 40px; 41 | text-align: center; 42 | 43 | color: #ffffff; 44 | } 45 | 46 | .main { 47 | min-height: 80vh; 48 | padding: 4rem 0; 49 | flex: 1; 50 | display: flex; 51 | flex-direction: column; 52 | justify-content: center; 53 | align-items: center; 54 | } 55 | 56 | .footer { 57 | display: flex; 58 | flex: 1; 59 | padding: 2rem 0; 60 | border-top: 1px solid #eaeaea; 61 | justify-content: center; 62 | align-items: center; 63 | } 64 | 65 | .footer a { 66 | display: flex; 67 | justify-content: center; 68 | align-items: center; 69 | flex-grow: 1; 70 | } 71 | 72 | .title a { 73 | color: #0070f3; 74 | text-decoration: none; 75 | } 76 | 77 | .title a:hover, 78 | .title a:focus, 79 | .title a:active { 80 | text-decoration: underline; 81 | } 82 | 83 | .title { 84 | margin: 0; 85 | line-height: 1.15; 86 | font-size: 4rem; 87 | } 88 | 89 | .title, 90 | .description { 91 | text-align: center; 92 | } 93 | 94 | .description { 95 | margin: 4rem 0; 96 | line-height: 1.5; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .code { 101 | background: #fafafa; 102 | border-radius: 5px; 103 | padding: 0.75rem; 104 | font-size: 1.1rem; 105 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, 106 | Bitstream Vera Sans Mono, Courier New, monospace; 107 | } 108 | 109 | .grid { 110 | display: flex; 111 | align-items: center; 112 | justify-content: center; 113 | flex-wrap: wrap; 114 | max-width: 800px; 115 | } 116 | 117 | .logo { 118 | height: 1em; 119 | margin-left: 0.5rem; 120 | } 121 | 122 | .connectButton { 123 | position: absolute; 124 | top: 20px; 125 | right: 20px; 126 | } 127 | 128 | @media (max-width: 600px) { 129 | .grid { 130 | width: 100%; 131 | flex-direction: column; 132 | } 133 | } 134 | 135 | .viewListingButton { 136 | padding: 1rem; 137 | 138 | font-family: "Inter"; 139 | font-style: normal; 140 | font-weight: 600; 141 | font-size: 18px; 142 | line-height: 17px; 143 | 144 | width: 220px; 145 | height: 50px; 146 | border: none; 147 | outline: none; 148 | color: #fff; 149 | background: #111; 150 | cursor: pointer; 151 | position: relative; 152 | z-index: 0; 153 | border-radius: 10px; 154 | } 155 | 156 | /* credits to https://codepen.io/kocsten */ 157 | .viewListingButton:before { 158 | content: ""; 159 | background: linear-gradient( 160 | 45deg, 161 | #ff0000, 162 | #ff7300, 163 | #fffb00, 164 | #48ff00, 165 | #00ffd5, 166 | #002bff, 167 | #7a00ff, 168 | #ff00c8, 169 | #ff0000 170 | ); 171 | position: absolute; 172 | top: -2px; 173 | left: -2px; 174 | background-size: 400%; 175 | z-index: -1; 176 | filter: blur(5px); 177 | width: calc(100% + 4px); 178 | height: calc(100% + 4px); 179 | animation: glowing 20s linear infinite; 180 | opacity: 0; 181 | transition: opacity 0.3s ease-in-out; 182 | border-radius: 10px; 183 | } 184 | 185 | .viewListingButton:active { 186 | color: #000; 187 | } 188 | 189 | .viewListingButton:active:after { 190 | background: transparent; 191 | } 192 | 193 | .viewListingButton:hover:before { 194 | opacity: 1; 195 | } 196 | 197 | .viewListingButton:after { 198 | z-index: -1; 199 | content: ""; 200 | position: absolute; 201 | width: 100%; 202 | height: 100%; 203 | background: #111; 204 | left: 0; 205 | top: 0; 206 | border-radius: 10px; 207 | } 208 | 209 | @keyframes glowing { 210 | 0% { 211 | background-position: 0 0; 212 | } 213 | 50% { 214 | background-position: 400% 0; 215 | } 216 | 100% { 217 | background-position: 0 0; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /pages/about.tsx: -------------------------------------------------------------------------------- 1 | import styles from "@styles/About.module.css"; 2 | import { Text, VStack } from "@chakra-ui/react"; 3 | import { NavBar } from "@components/NavBar"; 4 | import withTransition from "@components/withTransition"; 5 | 6 | const About = () => { 7 | return ( 8 | 9 | 10 |
11 |
12 | 13 | 14 |

15 | Why the name Tsukiji 16 |

17 | 18 | Inspired by the concept of a "seaport", we asked ourselves 19 | what an experience exchanging goods would look like at a real-life 20 | seaside port. The vision that excited us most was that of a bustling 21 | local fish market, where we imagined locals bartering for goods and 22 | shouting bids on freshly caught fish. We wanted to build a platform 23 | that reflected the energy of such a place, but with exchanging NFTs. 24 | So as an homage to what was once the world's largest fish 25 | market - the Tsukiji Fish Market in Tokyo, Japan - we named our 26 | project Tsukiji. 27 | 28 |
29 | 30 |

How it works

31 | 32 | Based on the Seaport Protocol, we enable users to create orders that 33 | comprise of offers and considerations, where offers are what the 34 | user is willing to trade in exchange for considerations, which are 35 | what the user wants in return. All users are able to view open 36 | orders and fulfill them if they can provide the other side of the 37 | trade. These offers or considerations can be a combination of 38 | various token types - ERC20, ERC721, ERC1155.

39 |

40 | 41 | *Tsukiji currently only supports the basic order type and 42 | fulfillment feature from the Seaport Protocol, which excludes 43 | partial order fills, ERC1155 tokens, and third-party order 44 | matching. 45 | 46 |
47 |
48 | 49 |

Applications

50 | 51 | Some interesting applications of an order aggregator like Tsukiji 52 | could be in web3 gaming. Gaming is an industry where the trading of 53 | in-game assets has always been a core feature of. For web3 games 54 | where in-game assets are representable as NFTs, one could imagine 55 | the utility of having an aggregated order book of listings that 56 | indicate the supply and demand of assets that users can fulfill for 57 | themselves or match on behalf of others. If the game creates its own 58 | matching algorithm and instantiates a fee for each exchange, this 59 | could quickly become a revenue-generating feature for the game that 60 | also allows for interoperability across games within the same 61 | network. Tsukiji as an order aggregator and Seaport as a trading 62 | settlement layer could be a powerful combination to incorporate into 63 | such games. 64 | 65 |
66 | 67 |

Origin Story

68 | 69 | This application was built as an entry to the ETH NY 2022 Hackathon. 70 | Although we only had three days to hack on the project from scratch, 71 | it was a tremendous learning experience that we highly recommend for 72 | other builders in the web3 space. We were lucky enough to have this 73 | project chosen as finalists for the hackathon and would love to see 74 | this project continue moving forward with the help of the community. 75 | If you are interested in contributing to this project or helping 76 | shape its direction, please reach out to us:) 77 | 78 |
79 |
80 | 81 | ); 82 | }; 83 | 84 | export default withTransition(About); 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tsukiji: Seaport Protocol Order Aggregator 2 | 3 | Tsukiji is built on the Seaport Protocol. We are building the next-generation NFT marketplace. Create orders and asks. We aggregate and matchers are incentivized to match orders. 4 | 5 | ## Two-sided marketplace 6 | 7 | The web app serves consumers - users are able to create orders and view open orders. The API serves fulfillers and incentivized community members that help match all the open orders. Consumers submit a tip amount upon creating the order to incentivize their orders to be matched. Fulfillers receive the tip amount for their work in aggregating and matching orders. 8 | 9 | WEB APP (offerers): 10 | 11 | 1. Aggregate user orders (e.g. createOrder(orderParams) returns bool) 12 | 2. Showcase orders relevant to users (i.e. an explore page) 13 | 14 | API / SDK (fulfillers): 15 | 16 | 1. Endpoint that returns orders (e.g. getOrders() returns Orders[]) 17 | 2. Endpoint to fulfill orders (e.g. fulfillOrders(Orders[]) returns bool) 18 | 19 | Prototype: API to facilitate order creation and submission on the Seaport Protocol 20 | 21 | 1. Aggregate user orders (e.g. `createOrder(orderParams) returns bool`) 22 | 2. Create endpoint that returns orders (e.g. `getOrders() returns Orders[]`) 23 | 3. Create endpoint to fulfill orders (e.g. `fulfillOrders(Orders[]) returns bool`) 24 | 25 | ## Seaport Alternative Chain Deployments 26 | 27 | Source Repo: [`0xgoretex/seaport`](https://github.com/0xgoretex/seaport) 28 | 29 | Seaport Protocol was previously only available on ETH Mainnet and Rinkeby but to facilitate some of our project needs, we launched it on Polygon and Kovan. This is now available for the convenience of future developers to test on these lower-cost/free networks. 30 | 31 | | **Network** | **Seaport 1.1** | **Conduit Controller** | 32 | | -------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | 33 | | Rinkeby | [0x8644e0f67c55a8db5d89D92371ED842fff16A5c5](https://rinkeby.etherscan.io/address/0x8644e0f67c55a8db5d89D92371ED842fff16A5c5) | [0xBf320C8539386d7eEc20C547F4d0456354a9f2c5](https://rinkeby.etherscan.io/address/0xBf320C8539386d7eEc20C547F4d0456354a9f2c5) | 34 | | Optimism Kovan | [0x5d603b86fA18de1e6a95a2f890cE1aEf2f641839](https://kovan-optimistic.etherscan.io/address/0x5d603b86fA18de1e6a95a2f890cE1aEf2f641839) | [0xB94Ad2559dCC53Da3a51B9f62eB9254fd0fB389D](https://kovan-optimistic.etherscan.io/address/0xB94Ad2559dCC53Da3a51B9f62eB9254fd0fB389D) | 35 | | Polygon Mumbai | [0x5d603b86fA18de1e6a95a2f890cE1aEf2f641839](https://mumbai.polygonscan.com/address/0x5d603b86fA18de1e6a95a2f890cE1aEf2f641839) | [0xB94Ad2559dCC53Da3a51B9f62eB9254fd0fB389D](https://mumbai.polygonscan.com/address/0xB94Ad2559dCC53Da3a51B9f62eB9254fd0fB389D) | 36 | 37 | ## Tech Stack 38 | 39 | Tsukiji is built on top of the Seaport Protocol, a protocol created by Opensea. Our Next.js front-end is hosted on Skynet and Vercel. Our live demo link is on Vercel as our automated SkyNet deploys would require us to continuously update the links as we are rapidly iterating this weekend. However, in our repository, we have set up automated deploys to Skynet to switch over to in the future. 40 | 41 | To ensure maximum data availability for fulfillers, we store order data in Skynet and Filecoin. We expose APIs for developers to choose which decentralized storage system to query. 42 | 43 | ## API Docs 44 | 45 | [`API Docs`](pages/api) 46 | 47 | ## License 48 | 49 | [MIT License](LICENSE) 50 | 51 | Implementation only applies to fulfillBasicOrder logic. 52 | 53 | Concerns: 54 | 55 | - liquidity of NFTs on the market 56 | 57 | Further explorations: 58 | 59 | - criteria-based offers 60 | - fulfillment of advanced orders 61 | 62 | ### Allowlisted Collections on the Exchange: 63 | 64 | - AZUKI: 0xae6Bbc3C94D1A6C086AF6a9774B4434A58C793bf 65 | - BAYC: 0xF865E7c71432EE1c57047f6280c2de39214b5b7A 66 | - COOL: 0x3dAd63203f1A62724DAcb6A473fE9AE042e2ecc3 67 | - MAYC: 0xf0d554b751fE43f1A80dd693A30583f041bAc3A5 68 | - Doodles: 0xF6e19DBFdc5a86648c6174c860964F901712c1C4 69 | - Goblintown: 0x16fF7dca5A520841e646AF9C927F32F56419c16c 70 | - Meebits: 0x2fCEb846CFAbd8e26B63256aEd5029F7365af714 71 | - World of Women: 0x65d5e1e27159d6758982ac6d2952099D364a33E0 72 | - CloneX: 0x45F59541c942CC7cc2319785c9d39F9b1DF35013 73 | - Coven: 0xA6C71b373E6c6daAb5041B26a9D94EbD6D288A81 74 | 75 | Feel free to call `mintPublicSale` on these contracts via Etherscan to free mint some dummy NFTs to play around with on the exchange web app. 76 | -------------------------------------------------------------------------------- /components/TitleSection.tsx: -------------------------------------------------------------------------------- 1 | import { HStack, VStack, Image, Text, Box, Spacer } from "@chakra-ui/react"; 2 | import styles from "@styles/TitleSection.module.css"; 3 | import { chakra } from "@chakra-ui/react"; 4 | import { motion, isValidMotionProp } from "framer-motion"; 5 | 6 | const ChakraBox = chakra(motion.div, { 7 | shouldForwardProp: (prop) => isValidMotionProp(prop) || prop === "children", 8 | }); 9 | 10 | export const TitleSection = () => { 11 | return ( 12 |
13 |
14 |
15 | 16 | 17 |

Next-Generation NFT Exchange

18 | 19 | Built on top of Opensea's Seaport Protocol 20 | 21 |
22 | 23 | 37 | seaport logo 42 | 43 | 60 |
61 | bobu nft 66 | coven nft 71 |
72 |
73 | 90 |
91 | bobu nft 96 | coven nft 101 |
102 |
103 | 108 | 109 | 113 | 117 | 121 | 125 | 126 | 127 |
128 |
129 |
130 | ); 131 | }; 132 | -------------------------------------------------------------------------------- /components/ListingCard.tsx: -------------------------------------------------------------------------------- 1 | import { Box, HStack, VStack } from "@chakra-ui/react"; 2 | import Link from "next/link"; 3 | import styles from "@styles/ListingCard.module.css"; 4 | import { Image } from "@chakra-ui/react"; 5 | import { abridgeAddress } from "@utils/abridgeAddress"; 6 | import { OfferItem, OrderWithMetadata } from "types/tokenTypes"; 7 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 8 | import { formatEther } from "ethers/lib/utils"; 9 | 10 | type ListingCardProps = { 11 | listing: OrderWithMetadata; 12 | }; 13 | 14 | type ItemData = { 15 | count: number; 16 | tokenIds: number[]; 17 | symbol: string; 18 | }; 19 | 20 | type OfferData = Map; 21 | 22 | export const ListingCard = ({ listing }: ListingCardProps) => { 23 | const offersMap = listing.offers.reduce((map: OfferData, item: OfferItem) => { 24 | if (!item.address) return map; 25 | if (!map.has(item.address)) { 26 | if (item.type !== ItemType.ERC721) { 27 | return map.set(item.address ?? "ethereum", { 28 | count: Number( 29 | formatEther( 30 | "amount" in item.inputItem ? item.inputItem.amount! : "0" 31 | ) 32 | ), 33 | tokenIds: [Number(item.token_id)], 34 | symbol: item.symbol, 35 | }); 36 | } else { 37 | return map.set(item.address, { 38 | count: 1, 39 | tokenIds: [Number(item.token_id)], 40 | symbol: item.symbol, 41 | }); 42 | } 43 | } 44 | 45 | const data = map.get(item.address); 46 | const { count, tokenIds, symbol } = data!; 47 | 48 | return map.set(item.address, { 49 | count: count + 1, 50 | tokenIds: [...tokenIds, Number(item.token_id)], 51 | symbol, 52 | }); 53 | }, new Map()); 54 | 55 | const considerationsMap = listing.considerations.reduce( 56 | (map: OfferData, item: OfferItem) => { 57 | if (!item.address) return map; 58 | if (!map.has(item.address)) { 59 | if (item.type !== ItemType.ERC721) { 60 | return map.set(item.address ?? "ethereum", { 61 | count: Number( 62 | formatEther( 63 | "amount" in item.inputItem ? item.inputItem.amount! : "0" 64 | ) 65 | ), 66 | tokenIds: [Number(item.token_id)], 67 | symbol: item.symbol, 68 | }); 69 | } else { 70 | return map.set(item.address, { 71 | count: 1, 72 | tokenIds: [Number(item.token_id)], 73 | symbol: item.symbol, 74 | }); 75 | } 76 | } 77 | 78 | const data = map.get(item.address); 79 | const { count, tokenIds, symbol } = data!; 80 | 81 | return map.set(item.address, { 82 | count: count + 1, 83 | tokenIds: [...tokenIds, Number(item.token_id)], 84 | symbol, 85 | }); 86 | }, 87 | new Map() 88 | ); 89 | 90 | return ( 91 | <> 92 | 93 | 94 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | {listing.offers.map(({ name, image_url, address, token_id }) => ( 111 | {`Image 117 | ))} 118 | 119 | 120 | {Array.from(offersMap.entries()).map( 121 | ([address, data]: [string, ItemData]) => ( 122 | 126 |
{`${data.count} ${data.symbol}`}
129 |
130 | ) 131 | )} 132 |
133 |
134 | 135 |
In Exchange For
136 |
137 | 138 | {Array.from(considerationsMap.entries()).map( 139 | ([address, data]: [string, ItemData]) => ( 140 |
144 |
{`${data.count} ${data.symbol}`}
147 |
148 | ) 149 | )} 150 |
151 |
152 | 153 | ); 154 | }; 155 | -------------------------------------------------------------------------------- /pages/create.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Select, 3 | Box, 4 | Button, 5 | HStack, 6 | Spacer, 7 | Text, 8 | VStack, 9 | } from "@chakra-ui/react"; 10 | import type { NextPage } from "next"; 11 | import styles from "../styles/Create.module.css"; 12 | import { 13 | CreateOrderInput, 14 | OrderWithCounter, 15 | } from "@opensea/seaport-js/lib/types"; 16 | import { Seaport } from "@opensea/seaport-js"; 17 | import { useAccount } from "wagmi"; 18 | import { providers } from "ethers"; 19 | import { useCallback, useState } from "react"; 20 | import { 21 | OfferItem, 22 | ConsiderationItem, 23 | OrderWithMetadata, 24 | } from "types/tokenTypes"; 25 | import { NavBar } from "../components/NavBar"; 26 | import { TokenSelection } from "@components/TokenSelection"; 27 | import { Switch } from "@chakra-ui/react"; 28 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 29 | import withTransition from "@components/withTransition"; 30 | import CRC32 from "crc-32"; 31 | 32 | const Create: NextPage = () => { 33 | const { data: accountData } = useAccount(); 34 | 35 | const [order, setOrder] = useState(); 36 | const [offerItems, setOfferItems] = useState([]); 37 | 38 | const [considerationItems, setConsiderationItems] = useState< 39 | ConsiderationItem[] 40 | >([]); 41 | 42 | const [duration, setDuration] = useState(0); 43 | 44 | const [loading, setLoading] = useState(false); 45 | const [txnsuccess, setTxnSuccess] = useState(false); 46 | 47 | const [isWETH, setIsWETH] = useState(false); 48 | 49 | const ethersProvider = new providers.Web3Provider( 50 | window.ethereum as providers.ExternalProvider 51 | ); 52 | 53 | const seaport = new Seaport(ethersProvider as any); 54 | 55 | const createSeaportOrder = async () => { 56 | if (!accountData) throw Error("No address found"); 57 | setLoading(true); 58 | 59 | const orderParams: CreateOrderInput = { 60 | offer: offerItems.map((item) => item.inputItem), 61 | consideration: considerationItems.map((item) => item.inputItem), 62 | endTime: duration 63 | ? (Math.floor(Date.now() / 1000) + duration).toString() 64 | : undefined, 65 | }; 66 | 67 | const { executeAllActions } = await seaport?.createOrder( 68 | orderParams, 69 | accountData?.address 70 | ); 71 | 72 | const res = await executeAllActions(); 73 | setOrder(res); 74 | 75 | const orderToSave: OrderWithMetadata = { 76 | id: CRC32.str(res.signature).toString(), 77 | order: res, 78 | offers: offerItems, 79 | considerations: considerationItems, 80 | }; 81 | 82 | await saveOrder(orderToSave); 83 | setTxnSuccess(true); 84 | setLoading(false); 85 | }; 86 | 87 | const saveOrder = useCallback(async (order: OrderWithMetadata) => { 88 | try { 89 | const response = await fetch("/api/orders", { 90 | method: "POST", 91 | body: JSON.stringify(order), 92 | headers: { 93 | "content-type": "application/json", 94 | }, 95 | }); 96 | const data = await response.json(); 97 | } catch (err) { 98 | console.log("Error request: ", err); 99 | } 100 | }, []); 101 | 102 | const handleSelectDuration = (e: any) => { 103 | e.preventDefault(); 104 | setDuration(Number(e.target.value)); 105 | }; 106 | 107 | const handleCurrencySwitch = (e: any) => { 108 | const wethSelected = e.target.checked; 109 | 110 | // remove ETH from order if WETH selected 111 | if (wethSelected) { 112 | const newOfferItems = offerItems.filter( 113 | ({ type }) => type !== ItemType.NATIVE 114 | ); 115 | const newConsiderationItems = considerationItems.filter( 116 | ({ type }) => type !== ItemType.NATIVE 117 | ); 118 | setOfferItems(newOfferItems); 119 | setConsiderationItems(newConsiderationItems); 120 | } 121 | 122 | // remove WETH from order if ETH selected 123 | if (!wethSelected) { 124 | const newOfferItems = offerItems.filter( 125 | ({ type }) => type !== ItemType.ERC20 126 | ); 127 | const newConsiderationItems = considerationItems.filter( 128 | ({ type }) => type !== ItemType.ERC20 129 | ); 130 | setOfferItems(newOfferItems); 131 | setConsiderationItems(newConsiderationItems); 132 | } 133 | 134 | setIsWETH(wethSelected); 135 | }; 136 | 137 | return ( 138 |
139 | 140 |
141 |
CREATE LISTING
142 | {!accountData?.address ? ( 143 | 144 | Please connect your wallet to get started. 145 | 146 | ) : ( 147 | <> 148 | 149 | 157 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | Duration 172 | 173 |
174 | 184 |
185 |
186 | 187 | 188 | 189 | Use WETH 190 | 191 | 197 | 198 |
199 | 200 | 201 | 202 | 203 | 216 | {txnsuccess && ( 217 |
218 | YOUR LISTING WAS SUCCESSFUL! 219 |
220 | )} 221 |
222 |
223 | 224 | )} 225 |
226 |
227 | ); 228 | }; 229 | 230 | export default withTransition(Create); 231 | -------------------------------------------------------------------------------- /pages/listings/[listingId].tsx: -------------------------------------------------------------------------------- 1 | import { Box, Button, HStack, Text, VStack, Image } from "@chakra-ui/react"; 2 | import { ArrowUpDownIcon } from "@chakra-ui/icons"; 3 | import { useEffect, useState } from "react"; 4 | import { OrderWithMetadata } from "types/tokenTypes"; 5 | import styles from "@styles/Listing.module.css"; 6 | import { abridgeAddress } from "@utils/abridgeAddress"; 7 | import { Spinner } from "@chakra-ui/react"; 8 | import { providers } from "ethers"; 9 | import { Seaport } from "@opensea/seaport-js"; 10 | import { useAccount } from "wagmi"; 11 | import { useRouter } from "next/router"; 12 | import { NavBar } from "@components/NavBar"; 13 | import withTransition from "@components/withTransition"; 14 | import { OrderWithCounter } from "@opensea/seaport-js/lib/types"; 15 | 16 | const Listing = () => { 17 | const { data: accountData, isError } = useAccount(); 18 | 19 | const router = useRouter(); 20 | const { listingId } = router.query; 21 | const [listing, setListing] = useState(); 22 | const [isLoading, setIsLoading] = useState(false); 23 | const [fulfillmentLoading, setFulfillmentLoading] = useState(false); 24 | const [errorMessage, setErrorMessage] = useState(""); 25 | 26 | const order = listing?.order; 27 | const offers = listing?.offers ?? []; 28 | const considerations = listing?.considerations ?? []; 29 | 30 | useEffect(() => { 31 | setIsLoading(true); 32 | if (!listingId) { 33 | return; 34 | } 35 | const fetchListing = async () => { 36 | try { 37 | const response = await fetch(`/api/orders/${listingId}`, { 38 | method: "GET", 39 | headers: { 40 | "content-type": "application/json", 41 | }, 42 | }); 43 | const data = await response.json(); 44 | 45 | setListing(data); 46 | setIsLoading(false); 47 | } catch (err) { 48 | console.log("Error request: ", err); 49 | } 50 | }; 51 | fetchListing(); 52 | }, [listingId]); 53 | 54 | const ethersProvider = new providers.Web3Provider( 55 | window.ethereum as providers.ExternalProvider 56 | ); 57 | 58 | const seaport = new Seaport(ethersProvider as any); 59 | 60 | const fulfillSeaportOrder = async () => { 61 | if (!accountData) throw Error("No address found"); 62 | if (!order) throw Error("No order found"); 63 | setErrorMessage(""); 64 | setFulfillmentLoading(true); 65 | 66 | try { 67 | let transactionHash: string; 68 | 69 | const { executeAllActions } = await seaport.fulfillOrder({ 70 | order: order, 71 | accountAddress: accountData.address, 72 | }); 73 | 74 | const transaction = await executeAllActions(); 75 | setFulfillmentLoading(false); 76 | transactionHash = transaction.hash; 77 | return transactionHash; 78 | } catch (err) { 79 | const error = err as Error; 80 | setErrorMessage(error.message); 81 | setFulfillmentLoading(false); 82 | } 83 | setFulfillmentLoading(false); 84 | }; 85 | 86 | function getLink(token_id: string, address?: string) { 87 | if (!address) { 88 | return "https://rinkeby.etherscan.io/"; 89 | } else if (address && !token_id) { 90 | return `https://rinkeby.etherscan.io/token/${address}`; 91 | } else 92 | return `https://testnets.opensea.io/assets/rinkeby/${address}/${token_id}`; 93 | } 94 | 95 | return ( 96 |
97 | 98 |
99 |
100 | {isLoading ? ( 101 | 102 | 103 | 104 | ) : ( 105 | 106 |

107 | OFFER TO EXCHANGE BY{" "} 108 | 113 | {abridgeAddress(order?.parameters.offerer)} 114 | 115 |

116 | 117 | 118 |

YOU WILL PROVIDE:

119 | 120 | {considerations?.map( 121 | ({ address, image_url, name, symbol, token_id }, idx) => ( 122 | 128 |
129 | {name} 134 |
135 | {name === "Wrapped Ethereum" 136 | ? "WETH" 137 | : name 138 | ? name 139 | : `${symbol} #${token_id}`} 140 |
141 |
142 |
143 | ) 144 | )} 145 |
146 |
147 | 148 | 149 | 150 | 151 | 152 | 153 |

IN EXCHANGE FOR:

154 | 155 | {offers?.map( 156 | ({ address, image_url, name, symbol, token_id }, idx) => ( 157 | 163 |
167 | {name} 172 |
173 | {name === "Wrapped Ethereum" 174 | ? "WETH" 175 | : name 176 | ? name 177 | : `${symbol} #${token_id}`} 178 |
179 |
180 |
181 | ) 182 | )} 183 |
184 |
185 | 186 | 187 | 198 | {errorMessage && {errorMessage}} 199 | 200 |
201 | )} 202 |
203 |
204 |
205 | ); 206 | }; 207 | 208 | export default withTransition(Listing); 209 | -------------------------------------------------------------------------------- /components/web3/NFTViewer.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Image, SimpleGrid, Spinner, VStack } from "@chakra-ui/react"; 2 | import { abridgeAddress } from "@utils/abridgeAddress"; 3 | import { useEffect, useState } from "react"; 4 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 5 | import { InputItem } from "types/tokenTypes"; 6 | import styles from "@styles/TokenSelection.module.css"; 7 | import { createOfferItem, createConsiderationItem } from "@utils/createItem"; 8 | 9 | interface ImageSelectProps { 10 | imageUrl: string; 11 | name: string; 12 | } 13 | 14 | const ImageSelect = ({ imageUrl, name }: ImageSelectProps) => { 15 | return ( 16 | <> 17 | {imageUrl && ( 18 | {name} 26 | )} 27 | {!imageUrl && ( 28 | nft placeholder 36 | )} 37 | 38 | ); 39 | }; 40 | 41 | interface NFTViewerCardProps { 42 | imageUrl: string; 43 | name: string; 44 | tokenId: string; 45 | contractAddress: string; 46 | collectionName: string; 47 | items: InputItem[]; 48 | setItems: ( 49 | tokens: InputItem[] | ((prevState: InputItem[]) => InputItem[]) 50 | ) => void; 51 | isOffer: boolean; 52 | symbol: string; 53 | recipient?: string; 54 | } 55 | 56 | const NFTViewerCard = ({ 57 | imageUrl, 58 | name, 59 | tokenId, 60 | contractAddress, 61 | collectionName, 62 | items, 63 | setItems, 64 | isOffer, 65 | symbol, 66 | recipient = "", 67 | }: NFTViewerCardProps) => { 68 | const selected = !!items.find( 69 | (token) => 70 | `${token.address}-${token.token_id}` === `${contractAddress}-${tokenId}` 71 | ); 72 | 73 | const selectNFT = () => { 74 | if (selected) { 75 | // remove if already selected 76 | setItems((prev: InputItem[]) => 77 | prev.filter( 78 | ({ address, token_id }: InputItem) => 79 | `${address}-${token_id}` != `${contractAddress}-${tokenId}` 80 | ) 81 | ); 82 | } else { 83 | // select if not 84 | setItems((prev: InputItem[]) => [ 85 | ...prev, 86 | isOffer 87 | ? createOfferItem( 88 | ItemType.ERC721, 89 | name, 90 | imageUrl, 91 | symbol, 92 | "1", 93 | contractAddress, 94 | collectionName, 95 | tokenId 96 | ) 97 | : createConsiderationItem( 98 | ItemType.ERC721, 99 | name, 100 | imageUrl, 101 | symbol, 102 | "1", 103 | recipient, 104 | contractAddress, 105 | collectionName, 106 | tokenId 107 | ), 108 | ]); 109 | } 110 | }; 111 | 112 | return ( 113 | 122 | 123 | 124 |
125 | {name ? name : `${symbol} #${tokenId}`} 126 |
127 |
128 |
129 | ); 130 | }; 131 | 132 | interface NFTViewerProps { 133 | items: InputItem[]; 134 | setItems: ( 135 | tokens: InputItem[] | ((prevState: InputItem[]) => InputItem[]) 136 | ) => void; 137 | isOffer: boolean; 138 | searchText: string; 139 | account: string; 140 | } 141 | 142 | export const NFTViewer = ({ 143 | items, 144 | setItems, 145 | isOffer, 146 | searchText, 147 | account, 148 | }: NFTViewerProps) => { 149 | const [fetchedTokens, setFetchedTokens] = useState([]); 150 | const [isLoading, setIsLoading] = useState(false); 151 | 152 | const filteredTokens = fetchedTokens.filter((token) => 153 | token.collection.name.toLowerCase().includes(searchText.toLowerCase()) 154 | ); 155 | 156 | useEffect( 157 | function fetchData() { 158 | setIsLoading(true); 159 | 160 | const requestHeaders: HeadersInit = { 161 | Accept: "application/json", 162 | "X-API-KEY": process.env.NEXT_PUBLIC_OPENSEA_API_KEY ?? "", 163 | }; 164 | 165 | const requestOptions: RequestInit = { 166 | method: "GET", 167 | headers: requestHeaders, 168 | }; 169 | 170 | const fetchData = async () => { 171 | try { 172 | const response = await fetch( 173 | `https://testnets-api.opensea.io/api/v1/assets?owner=${account}&limit=200&include_orders=false`, 174 | requestOptions 175 | ); 176 | const { assets } = await response.json(); 177 | setFetchedTokens(assets); 178 | } catch (err) { 179 | console.log(`Error fetching assets from Opensea: ${err}`); 180 | return new Error(`Error fetching assets from Opensea: ${err}`); 181 | } 182 | setIsLoading(false); 183 | }; 184 | 185 | fetchData(); 186 | }, 187 | [account, isOffer] 188 | ); 189 | 190 | return ( 191 |
192 | {fetchedTokens.length === 0 && ( 193 | 194 | {"Oops, looks like you don't own any tokens."} 195 | 196 | )} 197 | {isLoading ? ( 198 | 199 | 200 | 201 | ) : ( 202 | fetchedTokens && ( 203 | 204 | {isOffer 205 | ? filteredTokens.map( 206 | ({ name, image_url, token_id, asset_contract }) => ( 207 | 222 | ) 223 | ) 224 | : filteredTokens.map( 225 | ({ 226 | name, 227 | image_url, 228 | token_id, 229 | address, 230 | collectionName, 231 | symbol, 232 | }) => ( 233 | 247 | ) 248 | )} 249 | 250 | ) 251 | )} 252 | {searchText && filteredTokens.length === 0 && ( 253 | 254 | { 255 | "Sorry, we couldn't find any tokens in your wallet for your search :(" 256 | } 257 | 258 | )} 259 |
260 | ); 261 | }; 262 | -------------------------------------------------------------------------------- /pages/api/sky-relatedOrders/[addressParam].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { ethers } from "ethers"; 4 | import dotenv from "dotenv"; 5 | dotenv.config(); 6 | 7 | const { 8 | SkynetClient, 9 | genKeyPairFromSeed, 10 | } = require("@skynetlabs/skynet-nodejs"); 11 | 12 | const client = new SkynetClient(); 13 | const { publicKey, privateKey } = genKeyPairFromSeed( 14 | process.env.SKYNET_SEED || "" 15 | ); 16 | 17 | // initialize web3 provider; converge on using one of ethers and web3 later 18 | const Web3 = require("web3"); 19 | const providerUri = `https://eth-mainnet.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}/`; 20 | const provider = new Web3.providers.HttpProvider(providerUri); 21 | const web3 = new Web3(provider); 22 | 23 | // whitelisted ERC721 contracts 24 | const erc721Contracts: Record> = { 25 | mainnet: { 26 | azuki: "0xED5AF388653567Af2F388E6224dC7C4b3241C544", 27 | bayc: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", 28 | mayc: "0x60E4d786628Fea6478F785A6d7e704777c86a7c6", 29 | moonbirds: "0x23581767a106ae21c074b2276D25e5C3e136a68b", 30 | doodles: "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e", 31 | // 'punks': '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB', // punks might be too complicated to deal with for this hackathon 32 | }, 33 | rinkeby: { 34 | // can deploy some dummies later 35 | }, 36 | }; 37 | // whitelisted ERC20 contracts 38 | const erc20Contracts: Record> = { 39 | mainnet: { 40 | usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 41 | weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 42 | // 'steth': '', 43 | // 'eth': '' 44 | }, 45 | rinkeby: { 46 | // can deploy some dummies later 47 | }, 48 | }; 49 | // whitelisted ERC1155 contracts 50 | 51 | /* #### ROUTE HANDLER #### */ 52 | // This handler supports GET requests. 53 | // It expects an associated address for which to return relevant orders. 54 | export default async function handler( 55 | req: NextApiRequest, 56 | res: NextApiResponse 57 | ) { 58 | const dataKey = "orders"; 59 | if (req.method === "GET") { 60 | // fetch all orders 61 | const { addressParam } = req.query; 62 | 63 | if (Array.isArray(addressParam)) { 64 | res 65 | .status(400) 66 | .json( 67 | `Invalid param: expecting single string, got array: ${addressParam}` 68 | ); 69 | } 70 | 71 | const address = addressParam as string; 72 | 73 | if (!ethers.utils.isAddress(address)) { 74 | res.status(400).json(`Invalid address: ${address}`); 75 | } 76 | 77 | const tokens = await fetchRelevantTokens(address); 78 | 79 | // Inefficient query but it's all g 80 | const { data: snapshot } = await client.db.getJSON(publicKey, dataKey); 81 | let relevantOrders: any = []; 82 | 83 | // Loop through all existing orders and wallet's items (clearly O(m * n)) 84 | // This jawn could be optimized by just throwing stuff into a hash map to go to O(m + n) but meh rn 85 | // Type-agnostic for for-loops, but you can't break/continue out of forEach loops. 86 | for (let i = 0; i < snapshot.docs.length; i++) { 87 | let docId = snapshot.docs[i].id; 88 | let doc = snapshot.docs[i].data(); 89 | 90 | // items[] is stored weirdly in firestore; it's saved as an object, so all we want is the values of that object 91 | let docItems: any = Object.values(doc.items); 92 | 93 | for (let j = 0; j < docItems.length; j++) { 94 | for (let k = 0; k < tokens.length; k++) { 95 | // we got a match! 96 | if (tokens[k].contractAddress === docItems[j].contractAddress) { 97 | relevantOrders.push({ 98 | _id: docId, 99 | ...doc, 100 | }); 101 | 102 | // if there's a match, no need to continue going through other tokens since we know 103 | // the current order (document) is relevant 104 | break; 105 | } 106 | } 107 | } 108 | } 109 | 110 | res.status(200).json(relevantOrders); 111 | } else { 112 | res.status(400).json("Unable to handle request"); 113 | } 114 | } 115 | 116 | // fetchRelevantOrders will return all relevant (whitelisted) tokens belonging to an address. 117 | // Optimizations: batch calls; use indexers to prevent having to make live queries. 118 | const fetchRelevantTokens = async (address: string) => { 119 | const tokens = []; 120 | 121 | for (const erc20 in erc20Contracts["mainnet"]) { 122 | const contract: string = erc20Contracts["mainnet"][erc20]; 123 | if (contract.length == 0) { 124 | continue; 125 | } 126 | 127 | const balanceObj = await getERC20TokenBalance(contract, address); 128 | if (parseInt(balanceObj["balance"], 10) !== 0) tokens.push(balanceObj); 129 | } 130 | 131 | for (const erc721 in erc721Contracts["mainnet"]) { 132 | const contract: string = erc721Contracts["mainnet"][erc721]; 133 | if (contract.length == 0) { 134 | continue; 135 | } 136 | 137 | const balanceObj = await getERC721Tokens(contract, address); 138 | if (parseInt(balanceObj["balance"], 10) !== 0) tokens.push(balanceObj); 139 | } 140 | 141 | return tokens; 142 | }; 143 | 144 | /* #### HELPER FUNCTIONS BELOW #### */ 145 | 146 | // returns ERC20 token balance given token address + wallet address 147 | const getERC20TokenBalance = async ( 148 | contractAddress: string, 149 | walletAddress: string 150 | ): Promise => { 151 | // Get ERC20 Token contract instance 152 | let contract = new web3.eth.Contract(minERC20ABI, contractAddress); 153 | 154 | let balance = 0; 155 | 156 | await contract.methods 157 | .balanceOf(walletAddress) 158 | .call() 159 | .then((bal: any) => { 160 | balance = bal; 161 | }); 162 | 163 | let decimals = 0; 164 | 165 | await contract.methods 166 | .decimals() 167 | .call() 168 | .then((dec: any) => { 169 | decimals = dec; 170 | }); 171 | 172 | let symbol = ""; 173 | 174 | await contract.methods 175 | .symbol() 176 | .call() 177 | .then((sym: any) => { 178 | symbol = sym; 179 | }); 180 | 181 | return { 182 | type: "ERC20", 183 | contractAddress, 184 | balance, 185 | wholeBalance: (balance / 10 ** decimals).toString(), 186 | decimals, 187 | symbol, 188 | }; 189 | }; 190 | 191 | // returns ERC20 token balance given token address + wallet address 192 | const getERC721Tokens = async ( 193 | contractAddress: string, 194 | walletAddress: string 195 | ): Promise => { 196 | // Get ERC721 Token contract instance 197 | let contract = new web3.eth.Contract(minERC721ABI, contractAddress); 198 | 199 | // would be cool to batch these requests 200 | // should be able to handle larger numbers as well 201 | let balance = 0; 202 | 203 | await contract.methods 204 | .balanceOf(walletAddress) 205 | .call() 206 | .then((bal: any) => { 207 | balance = bal; 208 | }); 209 | 210 | let symbol = ""; 211 | 212 | await contract.methods 213 | .symbol() 214 | .call() 215 | .then((sym: any) => { 216 | symbol = sym; 217 | }); 218 | 219 | let tokenIds = []; 220 | 221 | for (let i = 0; i < balance; i++) { 222 | let tokenId = await contract.methods 223 | .tokenOfOwnerByIndex(walletAddress, i) 224 | .call() 225 | .then((id: any) => { 226 | return id; 227 | }); 228 | 229 | tokenIds.push(tokenId); 230 | } 231 | 232 | return { 233 | type: "ERC721", 234 | contractAddress, 235 | balance: balance, 236 | tokens: tokenIds, 237 | symbol, 238 | }; 239 | }; 240 | 241 | // The minimum ABI to get ERC20 Token details 242 | let minERC20ABI = [ 243 | // balanceOf 244 | { 245 | constant: true, 246 | inputs: [{ name: "_owner", type: "address" }], 247 | name: "balanceOf", 248 | outputs: [{ name: "balance", type: "uint256" }], 249 | type: "function", 250 | }, 251 | // decimals 252 | { 253 | constant: true, 254 | inputs: [], 255 | name: "decimals", 256 | outputs: [{ name: "", type: "uint8" }], 257 | type: "function", 258 | }, 259 | // symbol 260 | { 261 | constant: true, 262 | inputs: [], 263 | name: "symbol", 264 | outputs: [{ name: "", type: "string" }], 265 | type: "function", 266 | }, 267 | ]; 268 | 269 | // The minimum ABI to get ERC20 Token details 270 | let minERC721ABI = [ 271 | // balanceOf 272 | { 273 | constant: true, 274 | inputs: [{ name: "_owner", type: "address" }], 275 | name: "balanceOf", 276 | outputs: [{ name: "balance", type: "uint256" }], 277 | type: "function", 278 | }, 279 | // symbol 280 | { 281 | constant: true, 282 | inputs: [], 283 | name: "symbol", 284 | outputs: [{ name: "", type: "string" }], 285 | type: "function", 286 | }, 287 | // tokenOfOwnerByIndex 288 | { 289 | constant: true, 290 | inputs: [ 291 | { 292 | internalType: "address", 293 | name: "owner", 294 | type: "address", 295 | }, 296 | { 297 | internalType: "uint256", 298 | name: "index", 299 | type: "uint256", 300 | }, 301 | ], 302 | name: "tokenOfOwnerByIndex", 303 | outputs: [ 304 | { 305 | internalType: "uint256", 306 | name: "", 307 | type: "uint256", 308 | }, 309 | ], 310 | type: "function", 311 | }, 312 | ]; 313 | -------------------------------------------------------------------------------- /components/TokenSelection.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | HStack, 4 | Modal, 5 | ModalBody, 6 | ModalContent, 7 | ModalFooter, 8 | ModalOverlay, 9 | Text, 10 | useDisclosure, 11 | VStack, 12 | Image, 13 | IconButton, 14 | } from "@chakra-ui/react"; 15 | import { SmallCloseIcon } from "@chakra-ui/icons"; 16 | import { Tabs, TabList, TabPanels, Tab, TabPanel } from "@chakra-ui/react"; 17 | import { NFTViewer } from "./web3/NFTViewer"; 18 | import { useState } from "react"; 19 | import { CurrencyViewer } from "./web3/CurrencyViewer"; 20 | import { InputItem } from "types/tokenTypes"; 21 | import styles from "@styles/TokenSelection.module.css"; 22 | import { Input } from "@chakra-ui/react"; 23 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 24 | import { createConsiderationItem, createOfferItem } from "@utils/createItem"; 25 | import { useBalance } from "wagmi"; 26 | import { NFTConsiderationViewer } from "./web3/NFTConsiderationViewer"; 27 | import { formatEther, parseEther } from "ethers/lib/utils"; 28 | 29 | const WETH_ADDRESS = "0xc778417E063141139Fce010982780140Aa0cD5Ab"; 30 | 31 | interface TokenSelectionProps { 32 | title: string; 33 | isOffer: boolean; 34 | setItems: ( 35 | value: InputItem[] | ((prevState: InputItem[]) => InputItem[]) 36 | ) => void; 37 | items: InputItem[]; 38 | account: string; 39 | isWETH: boolean; 40 | } 41 | 42 | const TokenSelection = ({ 43 | title, 44 | isOffer, 45 | setItems, 46 | items, 47 | account, 48 | isWETH, 49 | }: TokenSelectionProps) => { 50 | const { data: ethData, isSuccess: isEthSuccess } = useBalance({ 51 | addressOrName: account, 52 | }); 53 | 54 | const { data: wethData, isSuccess: isWethSuccess } = useBalance({ 55 | addressOrName: account, 56 | token: WETH_ADDRESS, 57 | }); 58 | 59 | const [tab, setTab] = useState("ERC721"); 60 | const [searchText, setSearchText] = useState(""); 61 | 62 | const [currencyAmount, setCurrencyAmount] = useState("0"); 63 | const [errorMessage, setErrorMessage] = useState(""); 64 | 65 | const handleCurrencyInput = (value: string) => { 66 | if (isOffer && isEthSuccess && Number(ethData!.formatted) < Number(value)) { 67 | setErrorMessage("Insufficient Funds: ETH"); 68 | } else { 69 | setErrorMessage(""); 70 | } 71 | setCurrencyAmount(value); 72 | }; 73 | 74 | const { 75 | isOpen, 76 | onOpen: openAddTokenModal, 77 | onClose: closeAddTokenModal, 78 | } = useDisclosure(); 79 | 80 | const loadTokens = () => { 81 | if (Number(currencyAmount)) { 82 | let CurrencyItem: InputItem; 83 | 84 | if (!isWETH) { 85 | const newItems = items.filter(({ type }) => type !== ItemType.NATIVE); 86 | 87 | const formattedAmount = parseEther(currencyAmount).toString(); 88 | 89 | if (isOffer) { 90 | CurrencyItem = createOfferItem( 91 | ItemType.NATIVE, 92 | "Ethereum", 93 | "assets/eth.svg", 94 | "ETH", 95 | formattedAmount 96 | ); 97 | } else { 98 | CurrencyItem = createConsiderationItem( 99 | ItemType.NATIVE, 100 | "Ethereum", 101 | "assets/eth.svg", 102 | "ETH", 103 | formattedAmount, 104 | account 105 | ); 106 | } 107 | 108 | setItems([...newItems, CurrencyItem]); 109 | } else { 110 | const newItems = items.filter(({ type }) => type !== ItemType.ERC20); 111 | 112 | const formattedAmount = parseEther(currencyAmount).toString(); 113 | 114 | if (isOffer) { 115 | CurrencyItem = createOfferItem( 116 | ItemType.ERC20, 117 | "Wrapped Ethereum", 118 | "/assets/weth.png", 119 | "WETH", 120 | formattedAmount, 121 | WETH_ADDRESS 122 | ); 123 | } else { 124 | CurrencyItem = createConsiderationItem( 125 | ItemType.ERC20, 126 | "Wrapped Ethereum", 127 | "/assets/weth.png", 128 | "WETH", 129 | formattedAmount, 130 | account, 131 | WETH_ADDRESS 132 | ); 133 | } 134 | setItems([...newItems, CurrencyItem]); 135 | } 136 | } 137 | closeAddTokenModal(); 138 | }; 139 | 140 | const openModal = () => { 141 | setSearchText(""); 142 | setCurrencyAmount(""); 143 | setTab("ERC721"); 144 | openAddTokenModal(); 145 | }; 146 | 147 | const removeItem = (address: string, tokenId: string) => { 148 | const filteredItems = items.filter( 149 | (token) => 150 | `${token.address}-${token.token_id}` !== `${address}-${tokenId}` 151 | ); 152 | setItems(filteredItems); 153 | }; 154 | 155 | const handleSearch = (e: any) => { 156 | e.preventDefault(); 157 | setSearchText(e.target.value); 158 | }; 159 | 160 | return ( 161 | <> 162 |
163 |

{title}

164 | 165 | 166 | {items.map((item, idx) => ( 167 | 173 | ))} 174 | 175 | 176 | 184 |
185 | 186 | {/* Popup Modal */} 187 | 188 | 189 | 190 | 191 | 192 | 193 | setTab("ERC721")}> 194 | ERC721 195 | 196 | setTab("ERC20")}> 197 | Currency 198 | 199 | 200 | 201 | 202 | {isOffer ? ( 203 | 210 | ) : ( 211 | 218 | )} 219 | 220 | 221 | 229 | 230 | 231 | 232 | 233 | 234 | {tab === "ERC721" && ( 235 | 245 | )} 246 | 251 | {tab === "ERC721" && `${items.length} NFTs selected`} 252 | 253 | 262 | 263 | 264 | 265 | 266 | ); 267 | }; 268 | 269 | type ListItemProps = { 270 | item: InputItem; 271 | isLight: boolean; 272 | removeItem: (address: string, tokenId: string) => void; 273 | }; 274 | 275 | const ListItem = ({ item, isLight, removeItem }: ListItemProps) => { 276 | const isNFT = item.type === ItemType.ERC721; 277 | 278 | const { name, collectionName, address, token_id, inputItem, symbol } = item; 279 | 280 | return ( 281 | 284 | nft placeholder 291 | 292 |
293 | {isNFT ? collectionName : name} 294 |
295 | 296 |
297 | {isNFT ? `Token ID: ${token_id}` : ""} 298 |
299 |
300 | 301 |
302 | {isNFT 303 | ? `1 ${symbol}` 304 | : `${formatEther( 305 | "amount" in inputItem ? inputItem.amount! : "" 306 | )} ${symbol}`} 307 |
308 | 309 | } 314 | onClick={() => removeItem(address ?? "", token_id)} 315 | /> 316 |
317 | ); 318 | }; 319 | 320 | export { TokenSelection }; 321 | -------------------------------------------------------------------------------- /pages/api/relatedOrders/[addressParam].ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import { ethers } from "ethers"; 4 | import dotenv from "dotenv"; 5 | dotenv.config(); 6 | 7 | const { 8 | initializeApp, 9 | applicationDefault, 10 | cert, 11 | } = require("firebase-admin/app"); 12 | const { 13 | getFirestore, 14 | Timestamp, 15 | FieldValue, 16 | } = require("firebase-admin/firestore"); 17 | 18 | const network = "rinkeby"; // or mainnet 19 | 20 | // initialize web3 provider; converge on using one of ethers and web3 later 21 | const Web3 = require("web3"); 22 | const providerUri = `https://eth-${network}.alchemyapi.io/v2/${process.env.ALCHEMY_KEY}/`; 23 | const provider = new Web3.providers.HttpProvider(providerUri); 24 | const web3 = new Web3(provider); 25 | const admin = require("firebase-admin"); 26 | 27 | if (process.env.FIREBASE_PRIVATE_KEY && admin.apps.length === 0) { 28 | initializeApp({ 29 | credential: cert({ 30 | projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID, 31 | clientEmail: process.env.FIREBASE_CLIENT_EMAIL, 32 | privateKey: process.env.FIREBASE_PRIVATE_KEY.replace(/\\n/g, "\n"), 33 | }), 34 | }); 35 | } 36 | 37 | const db = getFirestore(); 38 | 39 | // sample mainnet address: 0xF56A5dd899dD95F39f5E01488BDdCbaBa64B8d45 40 | // sample testnet address: 0x17e547d79C04D01E49fEa275Cf32ba06554f9dF7 41 | // whitelisted ERC721 contracts 42 | const erc721Contracts: Record> = { 43 | mainnet: { 44 | azuki: "0xED5AF388653567Af2F388E6224dC7C4b3241C544", 45 | bayc: "0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D", 46 | mayc: "0x60E4d786628Fea6478F785A6d7e704777c86a7c6", 47 | moonbirds: "0x23581767a106ae21c074b2276D25e5C3e136a68b", 48 | doodles: "0x8a90CAb2b38dba80c64b7734e58Ee1dB38B8992e", 49 | // 'punks': '0xb47e3cd837dDF8e4c57F05d70Ab865de6e193BBB', // punks might be too complicated to deal with for this hackathon 50 | }, 51 | rinkeby: { 52 | sznouns1: "0xc3A949D2798e13cE845bDd21Bf639A28548faf30", 53 | sznouns2: "0x63f9e083a76e396c45b5f6fce41e6a91ea0a1400", 54 | }, 55 | }; 56 | // whitelisted ERC20 contracts 57 | const erc20Contracts: Record> = { 58 | mainnet: { 59 | usdc: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 60 | weth: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 61 | // 'steth': '', 62 | // 'eth': '' 63 | }, 64 | rinkeby: { 65 | // put something here later 66 | }, 67 | }; 68 | // whitelisted ERC1155 contracts 69 | 70 | /* #### ROUTE HANDLER #### */ 71 | // This handler supports GET requests. 72 | // It expects an associated address for which to return relevant orders. 73 | export default async function handler( 74 | req: NextApiRequest, 75 | res: NextApiResponse 76 | ) { 77 | if (req.method === "GET") { 78 | // fetch all orders 79 | const { addressParam } = req.query; 80 | 81 | if (Array.isArray(addressParam)) { 82 | res 83 | .status(400) 84 | .json( 85 | `Invalid param: expecting single string, got array: ${addressParam}` 86 | ); 87 | } 88 | 89 | const address = addressParam as string; 90 | 91 | if (!ethers.utils.isAddress(address)) { 92 | res.status(400).json(`Invalid address: ${address}`); 93 | } 94 | 95 | const tokens = await fetchRelevantTokens(address); 96 | 97 | // Inefficient query but it's all g 98 | const snapshot = await db.collection("orders").get(); 99 | let relevantOrders: any = []; 100 | 101 | // Loop through all existing orders and wallet's items (clearly O(m * n)) 102 | // This jawn could be optimized by just throwing stuff into a hash map to go to O(m + n) but meh rn 103 | // Type-agnostic for for-loops, but you can't break/continue out of forEach loops. 104 | // Iterate through the wallet's tokens first because if there are none, we save a query. 105 | for (let i = 0; i < tokens.length; i++) { 106 | for (let j = 0; j < snapshot.docs.length; j++) { 107 | let docId = snapshot.docs[i].id; 108 | let doc = snapshot.docs[i].data(); 109 | 110 | // The offer[] and consideration[] arrays are stored weirdly in firestore. 111 | // They're saved as objects, so all we want is the values of those objects (concatenated). 112 | let docItems: any = Object.values(doc["parameters"]["offer"]).concat( 113 | Object.values(doc["parameters"]["consideration"]) 114 | ); 115 | 116 | for (let k = 0; k < docItems.length; k++) { 117 | if (tokens[i].contractAddress === docItems[k].token) { 118 | // we got a match! 119 | relevantOrders.push({ 120 | _id: docId, 121 | ...doc, 122 | }); 123 | 124 | // if there's a match, no need to continue going through other tokens since we know 125 | // the current order (document) is relevant 126 | break; 127 | } 128 | } 129 | } 130 | } 131 | 132 | res.status(200).json(relevantOrders); 133 | } else { 134 | res.status(400).json("Unable to handle request"); 135 | } 136 | } 137 | 138 | // fetchRelevantOrders will return all relevant (whitelisted) tokens belonging to an address. 139 | // Optimizations: batch calls; use indexers to prevent having to make live queries. 140 | const fetchRelevantTokens = async (address: string) => { 141 | const tokens = []; 142 | 143 | for (const erc20 in erc20Contracts[network]) { 144 | const contract: string = erc20Contracts[network][erc20]; 145 | if (contract.length == 0) { 146 | continue; 147 | } 148 | 149 | const balanceObj = await getERC20TokenBalance(contract, address); 150 | if (parseInt(balanceObj["balance"], 10) !== 0) tokens.push(balanceObj); 151 | } 152 | 153 | for (const erc721 in erc721Contracts[network]) { 154 | const contract: string = erc721Contracts[network][erc721]; 155 | if (contract.length == 0) { 156 | continue; 157 | } 158 | 159 | const balanceObj = await getERC721Tokens(contract, address); 160 | if (parseInt(balanceObj["balance"], 10) !== 0) tokens.push(balanceObj); 161 | } 162 | 163 | return tokens; 164 | }; 165 | 166 | /* #### HELPER FUNCTIONS BELOW #### */ 167 | 168 | // returns ERC20 token balance given token address + wallet address 169 | const getERC20TokenBalance = async ( 170 | contractAddress: string, 171 | walletAddress: string 172 | ): Promise => { 173 | // Get ERC20 Token contract instance 174 | let contract = new web3.eth.Contract(minERC20ABI, contractAddress); 175 | 176 | let balance = 0; 177 | 178 | await contract.methods 179 | .balanceOf(walletAddress) 180 | .call() 181 | .then((bal: any) => { 182 | balance = bal; 183 | }); 184 | 185 | let decimals = 0; 186 | 187 | await contract.methods 188 | .decimals() 189 | .call() 190 | .then((dec: any) => { 191 | decimals = dec; 192 | }); 193 | 194 | let symbol = ""; 195 | 196 | await contract.methods 197 | .symbol() 198 | .call() 199 | .then((sym: any) => { 200 | symbol = sym; 201 | }); 202 | 203 | return { 204 | type: "ERC20", 205 | contractAddress, 206 | balance, 207 | wholeBalance: (balance / 10 ** decimals).toString(), 208 | decimals, 209 | symbol, 210 | }; 211 | }; 212 | 213 | // returns ERC20 token balance given token address + wallet address 214 | const getERC721Tokens = async ( 215 | contractAddress: string, 216 | walletAddress: string 217 | ): Promise => { 218 | // Get ERC721 Token contract instance 219 | let contract = new web3.eth.Contract(minERC721ABI, contractAddress); 220 | 221 | // would be cool to batch these requests 222 | // should be able to handle larger numbers as well 223 | let balance = 0; 224 | 225 | await contract.methods 226 | .balanceOf(walletAddress) 227 | .call() 228 | .then((bal: any) => { 229 | balance = bal; 230 | }); 231 | 232 | let symbol = ""; 233 | 234 | await contract.methods 235 | .symbol() 236 | .call() 237 | .then((sym: any) => { 238 | symbol = sym; 239 | }); 240 | 241 | let tokenIds = []; 242 | 243 | for (let i = 0; i < balance; i++) { 244 | let tokenId = await contract.methods 245 | .tokenOfOwnerByIndex(walletAddress, i) 246 | .call() 247 | .then((id: any) => { 248 | return id; 249 | }); 250 | 251 | tokenIds.push(tokenId); 252 | } 253 | 254 | return { 255 | type: "ERC721", 256 | contractAddress, 257 | balance: balance, 258 | tokens: tokenIds, 259 | symbol, 260 | }; 261 | }; 262 | 263 | // The minimum ABI to get ERC20 Token details 264 | let minERC20ABI = [ 265 | // balanceOf 266 | { 267 | constant: true, 268 | inputs: [{ name: "_owner", type: "address" }], 269 | name: "balanceOf", 270 | outputs: [{ name: "balance", type: "uint256" }], 271 | type: "function", 272 | }, 273 | // decimals 274 | { 275 | constant: true, 276 | inputs: [], 277 | name: "decimals", 278 | outputs: [{ name: "", type: "uint8" }], 279 | type: "function", 280 | }, 281 | // symbol 282 | { 283 | constant: true, 284 | inputs: [], 285 | name: "symbol", 286 | outputs: [{ name: "", type: "string" }], 287 | type: "function", 288 | }, 289 | ]; 290 | 291 | // The minimum ABI to get ERC20 Token details 292 | let minERC721ABI = [ 293 | // balanceOf 294 | { 295 | constant: true, 296 | inputs: [{ name: "_owner", type: "address" }], 297 | name: "balanceOf", 298 | outputs: [{ name: "balance", type: "uint256" }], 299 | type: "function", 300 | }, 301 | // symbol 302 | { 303 | constant: true, 304 | inputs: [], 305 | name: "symbol", 306 | outputs: [{ name: "", type: "string" }], 307 | type: "function", 308 | }, 309 | // tokenOfOwnerByIndex 310 | { 311 | constant: true, 312 | inputs: [ 313 | { 314 | internalType: "address", 315 | name: "owner", 316 | type: "address", 317 | }, 318 | { 319 | internalType: "uint256", 320 | name: "index", 321 | type: "uint256", 322 | }, 323 | ], 324 | name: "tokenOfOwnerByIndex", 325 | outputs: [ 326 | { 327 | internalType: "uint256", 328 | name: "", 329 | type: "uint256", 330 | }, 331 | ], 332 | type: "function", 333 | }, 334 | ]; 335 | -------------------------------------------------------------------------------- /components/web3/NFTConsiderationViewer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | HStack, 4 | Image, 5 | SimpleGrid, 6 | Spinner, 7 | VStack, 8 | Text, 9 | Center, 10 | Divider, 11 | } from "@chakra-ui/react"; 12 | import { useCallback, useEffect, useState } from "react"; 13 | import { ItemType } from "@opensea/seaport-js/lib/constants"; 14 | import { InputItem } from "types/tokenTypes"; 15 | import styles from "@styles/NFTConsiderationViewer.module.css"; 16 | import { createOfferItem, createConsiderationItem } from "@utils/createItem"; 17 | import { pinnedCollections } from "@constants/pinnedCollections"; 18 | 19 | interface NFTViewerCardProps { 20 | imageUrl: string; 21 | name: string; 22 | tokenId: string; 23 | contractAddress: string; 24 | collectionName: string; 25 | items: InputItem[]; 26 | setItems: ( 27 | tokens: InputItem[] | ((prevState: InputItem[]) => InputItem[]) 28 | ) => void; 29 | isOffer: boolean; 30 | symbol: string; 31 | recipient: string; 32 | } 33 | 34 | const NFTViewerCard = ({ 35 | imageUrl, 36 | name, 37 | tokenId, 38 | contractAddress, 39 | collectionName, 40 | items, 41 | setItems, 42 | isOffer, 43 | symbol, 44 | recipient, 45 | }: NFTViewerCardProps) => { 46 | const selected = !!items.find( 47 | (token) => 48 | `${token.address}-${token.token_id}` === `${contractAddress}-${tokenId}` 49 | ); 50 | 51 | const selectNFT = () => { 52 | if (selected) { 53 | // remove if already selected 54 | setItems((prev: InputItem[]) => 55 | prev.filter( 56 | ({ address, token_id }: InputItem) => 57 | `${address}-${token_id}` != `${contractAddress}-${tokenId}` 58 | ) 59 | ); 60 | } else { 61 | // select if not 62 | setItems((prev: InputItem[]) => [ 63 | ...prev, 64 | isOffer 65 | ? createOfferItem( 66 | ItemType.ERC721, 67 | name, 68 | imageUrl, 69 | symbol, 70 | "1", 71 | contractAddress, 72 | collectionName, 73 | tokenId 74 | ) 75 | : createConsiderationItem( 76 | ItemType.ERC721, 77 | name, 78 | imageUrl, 79 | symbol, 80 | "1", 81 | recipient, 82 | contractAddress, 83 | collectionName, 84 | tokenId 85 | ), 86 | ]); 87 | } 88 | }; 89 | 90 | return ( 91 | 100 | 101 | <> 102 | {imageUrl ? ( 103 | {name} 111 | ) : ( 112 | nft placeholder 120 | )} 121 | 122 |
123 | {name ? name : `${symbol} #${tokenId}`} 124 |
125 |
126 |
127 | ); 128 | }; 129 | 130 | interface NFTViewerProps { 131 | items: InputItem[]; 132 | setItems: ( 133 | tokens: InputItem[] | ((prevState: InputItem[]) => InputItem[]) 134 | ) => void; 135 | isOffer: boolean; 136 | searchText: string; 137 | account: string; 138 | } 139 | 140 | export const NFTConsiderationViewer = ({ 141 | items, 142 | setItems, 143 | isOffer, 144 | searchText, 145 | account, 146 | }: NFTViewerProps) => { 147 | const [fetchedCollections, setFetchedCollections] = useState([]); 148 | const [searchedCollections, setSearchedCollections] = useState([]); 149 | const [fetchedTokens, setFetchedTokens] = useState([]); 150 | const [isCollectionsLoading, setIsCollectionsLoading] = 151 | useState(false); 152 | const [isTokensLoading, setIsTokensLoading] = useState(false); 153 | const [selectedCollection, setSelectedCollection] = useState(""); 154 | 155 | useEffect( 156 | function fetchCollections() { 157 | setIsCollectionsLoading(true); 158 | 159 | const requestHeaders: HeadersInit = { 160 | Accept: "application/json", 161 | "X-API-KEY": process.env.NEXT_PUBLIC_OPENSEA_API_KEY ?? "", 162 | }; 163 | 164 | const requestOptions: RequestInit = { 165 | method: "GET", 166 | headers: requestHeaders, 167 | }; 168 | 169 | const fetchData = async () => { 170 | try { 171 | let response; 172 | if (searchText.length > 40) { 173 | response = await fetch( 174 | `https://testnets-api.opensea.io/api/v1/assets?asset_contract_address=${searchText}&order_direction=desc&offset=0&limit=50&include_orders=false`, 175 | requestOptions 176 | ); 177 | if (!response.ok) { 178 | throw new Error(`Error! status: ${response.status}`); 179 | } 180 | const { assets } = await response.json(); 181 | const { asset_contract, image_url } = assets[0]; 182 | asset_contract.image_url = image_url; 183 | setSearchedCollections([asset_contract]); 184 | } else { 185 | response = await fetch( 186 | "https://testnets-api.opensea.io/api/v1/collections?offset=0&limit=300", 187 | requestOptions 188 | ); 189 | if (!response.ok) { 190 | throw new Error(`Error! status: ${response.status}`); 191 | } 192 | const { collections } = await response.json(); 193 | 194 | setFetchedCollections(collections); 195 | } 196 | } catch (err) { 197 | console.log(`Error fetching collections from Opensea: ${err}`); 198 | return new Error(`Error fetching collections from Opensea: ${err}`); 199 | } 200 | setIsCollectionsLoading(false); 201 | }; 202 | 203 | fetchData(); 204 | }, 205 | [searchText] 206 | ); 207 | 208 | const fetchCollectionTokens = useCallback(async (address: string) => { 209 | setIsTokensLoading(true); 210 | const requestHeaders: HeadersInit = { 211 | Accept: "application/json", 212 | "X-API-KEY": process.env.NEXT_PUBLIC_OPENSEA_API_KEY ?? "", 213 | }; 214 | 215 | const requestOptions: RequestInit = { 216 | method: "GET", 217 | headers: requestHeaders, 218 | }; 219 | try { 220 | const response = await fetch( 221 | `https://testnets-api.opensea.io/api/v1/assets?asset_contract_address=${address}&order_direction=desc&offset=0&limit=50&include_orders=false`, 222 | requestOptions 223 | ); 224 | const { assets } = await response.json(); 225 | 226 | if (!response.ok) { 227 | throw new Error(`Error! status: ${response.status}`); 228 | } 229 | 230 | setFetchedTokens(assets); 231 | } catch (err) { 232 | console.log(`Error fetching collections from Opensea: ${err}`); 233 | return new Error(`Error fetching collections from Opensea: ${err}`); 234 | } 235 | setIsTokensLoading(false); 236 | }, []); 237 | 238 | const handleSelectCollection = async (address: string) => { 239 | setSelectedCollection(address); 240 | fetchCollectionTokens(address); 241 | }; 242 | 243 | const filteredCollection = fetchedCollections.filter((coll) => { 244 | if ( 245 | coll.primary_asset_contracts && 246 | coll.primary_asset_contracts[0] && 247 | coll.primary_asset_contracts[0].total_supply !== "0" 248 | ) 249 | return coll; 250 | }); 251 | 252 | // opensea returns duplicate collections which renders DOM errors 253 | const uniqueCollections = filteredCollection.reduce((unique, o) => { 254 | if (!unique.some((obj: any) => obj.name === o.name)) { 255 | unique.push(o); 256 | } 257 | return unique; 258 | }, []); 259 | 260 | const aggregatedCollections = 261 | searchText.length > 40 262 | ? searchedCollections 263 | : [...pinnedCollections, ...uniqueCollections]; 264 | 265 | return ( 266 |
267 |
268 | {isCollectionsLoading ? ( 269 | 270 | 271 | 272 | ) : ( 273 | aggregatedCollections.map((coll) => { 274 | const contractAddress = 275 | coll.primary_asset_contracts && coll.primary_asset_contracts[0] 276 | ? coll.primary_asset_contracts[0].address 277 | : coll.address; 278 | 279 | return ( 280 | handleSelectCollection(contractAddress)} 288 | > 289 | {coll.image_url ? ( 290 | {coll.name} 299 | ) : ( 300 | nft placeholder 309 | )} 310 | {coll.name} 311 | 312 | ); 313 | }) 314 | )} 315 |
316 |
317 | 318 |
319 |
320 | {isTokensLoading ? ( 321 | 322 | 323 | 324 | ) : ( 325 | 331 | {fetchedTokens.map( 332 | ({ name, image_url, token_id, asset_contract }) => ( 333 | 348 | ) 349 | )} 350 | 351 | )} 352 |
353 |
354 | ); 355 | }; 356 | --------------------------------------------------------------------------------