├── market-backend ├── src │ ├── types │ │ ├── structs │ │ │ ├── index.ts │ │ │ ├── market_id.ts │ │ │ └── TokenData.ts │ │ ├── index.ts │ │ └── events │ │ │ ├── index.ts │ │ │ ├── event.ts │ │ │ ├── list_token.ts │ │ │ └── buy_token.ts │ ├── utils │ │ └── delay.ts │ ├── consumers │ │ ├── index.ts │ │ ├── Consumer.ts │ │ ├── BuyEventConsumer.ts │ │ └── ListEventConsumer.ts │ ├── config │ │ ├── constants.ts │ │ └── libs.ts │ ├── State.ts │ ├── EventStream.ts │ └── Execution.ts ├── prisma │ ├── migrations │ │ ├── migration_lock.toml │ │ ├── 20220909181940_tweak_type_of_maximum_supply │ │ │ └── migration.sql │ │ └── 20220905143301_init │ │ │ └── migration.sql │ ├── seed.ts │ └── schema.prisma ├── package.json └── tsconfig.json ├── market-frontend ├── src │ ├── types │ │ ├── index.ts │ │ ├── Token.ts │ │ └── Offer.ts │ ├── hooks │ │ ├── index.ts │ │ ├── useOffers.ts │ │ └── useTokens.ts │ ├── styles │ │ ├── globals.css │ │ └── loading.css │ ├── utils │ │ ├── supabase.ts │ │ ├── aptos.ts │ │ └── nftstorage.ts │ ├── components │ │ ├── Loading.tsx │ │ ├── ModalContext.tsx │ │ ├── NavItem.tsx │ │ ├── TooltipSection.tsx │ │ ├── NavBar.tsx │ │ ├── TokenCard.tsx │ │ ├── ListCard.tsx │ │ ├── AptosConnect.tsx │ │ ├── OfferCard.tsx │ │ └── WalletModal.tsx │ ├── pages │ │ ├── api │ │ │ └── offers.ts │ │ ├── dashboard.tsx │ │ ├── _app.tsx │ │ ├── make-offer.tsx │ │ ├── index.tsx │ │ └── mint.tsx │ └── config │ │ └── constants.ts ├── postcss.config.js ├── public │ ├── favicon.ico │ ├── vercel.svg │ └── logo.svg ├── tailwind.config.js ├── .gitignore ├── tsconfig.json ├── next.config.js ├── package.json ├── README.md ├── scripts │ ├── create_market.mjs │ ├── list_token.mjs │ └── buy_token.mjs └── yarn.lock ├── package.json ├── market-contracts ├── package.json ├── Move.toml └── sources │ └── marketplace.move ├── .gitignore ├── bin └── install-service ├── README.md └── pics └── logo.svg /market-backend/src/types/structs/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./market_id"; 2 | -------------------------------------------------------------------------------- /market-frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Offer"; 2 | export * from "./Token"; 3 | -------------------------------------------------------------------------------- /market-backend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./events"; 2 | export * from "./structs"; 3 | -------------------------------------------------------------------------------- /market-frontend/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./useOffers"; 2 | export * from "./useTokens"; 3 | -------------------------------------------------------------------------------- /market-frontend/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /market-backend/src/types/events/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./list_token"; 2 | export * from "./buy_token"; 3 | export * from "./event"; 4 | -------------------------------------------------------------------------------- /market-backend/src/types/structs/market_id.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface MarketId{ 3 | market_address: string, 4 | market_name: string 5 | } -------------------------------------------------------------------------------- /market-backend/src/utils/delay.ts: -------------------------------------------------------------------------------- 1 | export function delay(ms: number) { 2 | return new Promise( resolve => setTimeout(resolve, ms) ); 3 | } -------------------------------------------------------------------------------- /market-backend/src/consumers/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./BuyEventConsumer" 2 | export * from "./ListEventConsumer" 3 | export * from "./Consumer" -------------------------------------------------------------------------------- /market-frontend/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /market-frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ethdestinybtc/aptos-NFT-marketplacee/HEAD/market-frontend/public/favicon.ico -------------------------------------------------------------------------------- /market-backend/src/types/events/event.ts: -------------------------------------------------------------------------------- 1 | export interface Event{ 2 | key: string; 3 | sequence_number: string; 4 | type: string; 5 | data: T; 6 | } -------------------------------------------------------------------------------- /market-backend/src/config/constants.ts: -------------------------------------------------------------------------------- 1 | import env from "dotenv"; 2 | env.config(); 3 | export const { APTOS_NODE_URL, MARKET_ADDRESS, APTOS_FAUCET_URL } = process.env; 4 | -------------------------------------------------------------------------------- /market-backend/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "market-frontend", 5 | "market-contracts", 6 | "market-backend" 7 | ], 8 | "dependencies": { 9 | "ts-node": "^10.9.1" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /market-frontend/src/utils/supabase.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@supabase/supabase-js"; 2 | import { SUPABASE_KEY, SUPABASE_URL } from "../config/constants"; 3 | 4 | export const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); 5 | -------------------------------------------------------------------------------- /market-backend/src/State.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs"; 2 | 3 | export type State = { 4 | listEventsExecutedSeqNum: bigint; 5 | buyEventsExecutedSeqNum: bigint; 6 | old?: State; 7 | }; 8 | 9 | export type StateFlow = Subject; 10 | -------------------------------------------------------------------------------- /market-frontend/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | export function Loading() { 2 | return ( 3 | 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /market-backend/prisma/migrations/20220909181940_tweak_type_of_maximum_supply/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "tokens" ALTER COLUMN "maximum" DROP NOT NULL, 3 | ALTER COLUMN "maximum" SET DATA TYPE TEXT, 4 | ALTER COLUMN "supply" SET DATA TYPE TEXT; 5 | -------------------------------------------------------------------------------- /market-frontend/src/types/Token.ts: -------------------------------------------------------------------------------- 1 | export interface Token { 2 | propertyVersion: number; 3 | creator: string; 4 | collection: string; 5 | name: string; 6 | description: string; 7 | uri: string; 8 | maximum: number; 9 | supply: number; 10 | } 11 | -------------------------------------------------------------------------------- /market-frontend/src/components/ModalContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from "react"; 2 | 3 | export type ModalState = { walletModal: boolean }; 4 | 5 | export const ModalContext = createContext({ 6 | modalState: { 7 | walletModal: false, 8 | }, 9 | setModalState: (_: ModalState) => {}, 10 | }); 11 | -------------------------------------------------------------------------------- /market-frontend/src/components/NavItem.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | type NavItemProps = { href: string; title: string }; 4 | export function NavItem({ href, title }: NavItemProps) { 5 | return ( 6 |
  • 7 | {title} 8 |
  • 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /market-backend/src/types/events/list_token.ts: -------------------------------------------------------------------------------- 1 | import { TokenId } from "@martiandao/aptos-web3-bip44.js"; 2 | import { MarketId } from "../structs"; 3 | 4 | export interface ListTokenEventData { 5 | market_id: MarketId; 6 | price: string; 7 | token_id: TokenId; 8 | seller: string; 9 | timestamp: string; 10 | offer_id: string; 11 | } 12 | -------------------------------------------------------------------------------- /market-contracts/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "market-contracts", 3 | "version": "0.1.1", 4 | "scripts": { 5 | "move:init": "aptos init", 6 | "move:compile": "aptos move compile --package-dir . --named-addresses $NAME=$ADDRESS", 7 | "move:publish": "aptos move publish --package-dir . --named-addresses $NAME=$ADDRESS" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /market-frontend/src/types/Offer.ts: -------------------------------------------------------------------------------- 1 | import { Token } from "./Token"; 2 | 3 | export type Status = "ongoing" | "finished" | "revoked"; 4 | 5 | export interface Offer { 6 | id: number; 7 | seller: string; 8 | buyer?: string; 9 | price: number; 10 | status: Status; 11 | createAt: Date; 12 | updateAt?: Date; 13 | token: Token; 14 | } 15 | -------------------------------------------------------------------------------- /market-frontend/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx}', 5 | './src/components/**/*.{js,ts,jsx,tsx}', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [require("daisyui")], 11 | daisyui: { 12 | themes: ["light"], 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /market-backend/src/types/events/buy_token.ts: -------------------------------------------------------------------------------- 1 | import { TokenId } from "@martiandao/aptos-web3-bip44.js"; 2 | import { MarketId } from "../structs"; 3 | 4 | export interface BuyTokenEventData { 5 | market_id: MarketId; 6 | price: string; 7 | token_id: TokenId; 8 | seller: string; 9 | buyer: string; 10 | offer_id: string; 11 | timestamp: string; 12 | } 13 | -------------------------------------------------------------------------------- /market-backend/src/config/libs.ts: -------------------------------------------------------------------------------- 1 | import { AptosClient, WalletClient } from "@martiandao/aptos-web3-bip44.js"; 2 | import { PrismaClient } from "@prisma/client"; 3 | import { APTOS_NODE_URL, APTOS_FAUCET_URL } from "./constants"; 4 | 5 | export const aptosClient = new AptosClient(APTOS_NODE_URL!); 6 | export const walletClient = new WalletClient(APTOS_NODE_URL!, APTOS_FAUCET_URL!) 7 | export const prismaClient = new PrismaClient(); -------------------------------------------------------------------------------- /market-backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "market-backend", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "start": "ts-node src/Execution.ts", 7 | "seed": "ts-node prisma/seed.ts" 8 | }, 9 | "license": "MIT", 10 | "dependencies": { 11 | "@martiandao/aptos-web3-bip44.js": "^1.1.21", 12 | "@prisma/client": "^4.3.1", 13 | "prisma": "^4.3.1", 14 | "rxjs": "^7.5.6" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /market-backend/src/types/structs/TokenData.ts: -------------------------------------------------------------------------------- 1 | export interface TokenData { 2 | /** Unique name within this creator's account for this Token's collection */ 3 | collection: string; 4 | 5 | /** Description of Token */ 6 | description: string; 7 | 8 | /** Name of Token */ 9 | name: string; 10 | 11 | /** Optional maximum number of this Token */ 12 | maximum?: string; 13 | 14 | /** Total number of this type of Token */ 15 | supply: string; 16 | 17 | /** URL for additional information / media */ 18 | uri: string; 19 | } 20 | -------------------------------------------------------------------------------- /market-frontend/src/pages/api/offers.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | import { supabase } from "../../utils/supabase"; 3 | 4 | export default async function handler( 5 | req: NextApiRequest, 6 | res: NextApiResponse 7 | ) { 8 | let { data: offers, error } = await supabase 9 | .from("offers") 10 | .select("id,buyer,seller,price,status,createAt,updateAt,token:tokens(*)") 11 | .eq("status", "ongoing"); 12 | if (error) { 13 | return res.status(500).json(error); 14 | } else { 15 | return res.status(200).json(offers); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /market-frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | # aptos 39 | */.aptos/ -------------------------------------------------------------------------------- /market-frontend/src/components/TooltipSection.tsx: -------------------------------------------------------------------------------- 1 | type TooltipSectionProps = { text: string }; 2 | export function TooltipSection({ text }: TooltipSectionProps) { 3 | return ( 4 |
    5 |

    16 | {text} 17 |

    18 |
    19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /market-frontend/src/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const NFT_STORAGE_KEY = process.env.NEXT_PUBLIC_NFT_STORAGE_KEY!; 2 | export const SUPABASE_KEY = process.env.NEXT_PUBLIC_SUPABASE_KEY!; 3 | export const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!; 4 | 5 | export const MARKET_NAME = process.env.NEXT_PUBLIC_MARKET_NAME!; 6 | export const MARKET_ADDRESS = process.env.NEXT_PUBLIC_MARKET_ADDRESS!; 7 | export const MARKET_COINT_TYPE = process.env.NEXT_PUBLIC_MARKET_COIN_TYPE!; 8 | 9 | export const APTOS_NODE_URL = process.env.NEXT_PUBLIC_APTOS_NODE_URL!; 10 | export const APTOS_FAUCET_URL = process.env.NEXT_PUBLIC_APTOS_FAUCET_URL!; -------------------------------------------------------------------------------- /market-backend/prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | async function main() { 4 | const client = new PrismaClient(); 5 | const result = await client.execution.upsert({ 6 | where: { id: 1 }, 7 | update: { 8 | listEventsExecutedSeqNum: -1n, 9 | buyEventsExecutedSeqNum: -1n, 10 | }, 11 | create: { 12 | listEventsExecutedSeqNum: -1n, 13 | buyEventsExecutedSeqNum: -1n, 14 | }, 15 | }); 16 | console.log(result); 17 | } 18 | 19 | main() 20 | .then(() => process.exit(0)) 21 | .catch((error) => { 22 | console.error(error); 23 | process.exit(1); 24 | }); 25 | -------------------------------------------------------------------------------- /market-frontend/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 | }, 18 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "scripts/create_market.mjs"], 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | */node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | .pnpm-debug.log* 28 | 29 | # local env files 30 | .env*.local 31 | .env 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | next-env.d.ts 39 | 40 | # move 41 | */build 42 | 43 | # aptos 44 | */.aptos/ 45 | -------------------------------------------------------------------------------- /bin/install-service: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | APP_NAME=$1 6 | ENV_NAME=$2 7 | 8 | 9 | cd `dirname $0`/.. 10 | app_home=`pwd` 11 | sudo_user=$SUDO_USER 12 | if [ $sudo_user = "" ]; then 13 | sudo_user=$USER 14 | fi 15 | 16 | echo $APP_NAME must be a target of Makefile 17 | 18 | cat < /etc/systemd/system/$APP_NAME.service 19 | [Unit] 20 | Description=$APP_NAME 21 | ConditionPathExists=$app_home 22 | 23 | [Service] 24 | EnvironmentFile=$app_home/.env 25 | WorkingDirectory=$app_home 26 | ExecStart=/usr/bin/env make run 27 | ExecReload=/bin/kill -HUP \$MAINPID 28 | Restart=always 29 | User=$sudo_user 30 | 31 | EOF 32 | 33 | systemctl daemon-reload -------------------------------------------------------------------------------- /market-frontend/src/hooks/useOffers.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | import { Offer } from "../types"; 3 | 4 | export function useOffers(): { 5 | offers: Offer[]; 6 | loading: boolean; 7 | } { 8 | const [offers, updateOffers] = useState([]); 9 | const [loading, setLoading] = useState(true); 10 | useEffect(() => { 11 | const fetchOffers = async () => { 12 | const response = await fetch("/api/offers"); 13 | const offers = (await response.json()).map((i: any) => i as Offer); 14 | updateOffers(offers); 15 | setLoading(false); 16 | }; 17 | fetchOffers(); 18 | }, []); 19 | return { offers, loading }; 20 | } 21 | -------------------------------------------------------------------------------- /market-frontend/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import { NavItem } from "./NavItem"; 3 | import { AptosConnect } from "./AptosConnect"; 4 | 5 | export function NavBar() { 6 | return ( 7 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /market-contracts/Move.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "_1200_dollars_per_hour" 3 | version = "0.1.3" 4 | license = "MIT" 5 | 6 | [addresses] 7 | _1200_dollars_per_hour ="_" 8 | std="0x1" 9 | aptos_framework="0x1" 10 | aptos_std="0x1" 11 | aptos_token="0x3" 12 | 13 | [dependencies] 14 | AptosFramework = { git = "https://github.com/aptos-labs/aptos-core.git", subdir="aptos-move/framework/aptos-framework/", rev="aptos-cli-v0.3.4" } 15 | AptosStdlib = { git = "https://github.com/aptos-labs/aptos-core.git", subdir="aptos-move/framework/aptos-stdlib/", rev="aptos-cli-v0.3.4" } 16 | AptosToken = { git = "https://github.com/aptos-labs/aptos-core.git", subdir="aptos-move/framework/aptos-token/", rev="aptos-cli-v0.3.4" } -------------------------------------------------------------------------------- /market-frontend/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true, 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "*.arweave.net", 9 | }, 10 | ], 11 | domains: [ 12 | "ipfs.io", 13 | "ipfs.filebase.io", 14 | "ipfs.infura.io", 15 | "nftstorage.link", 16 | "aptoslabs.com", 17 | "miro.medium.com", 18 | "www.gitbook.com", 19 | ], 20 | }, 21 | webpack5: true, 22 | webpack: (config) => { 23 | config.resolve.fallback = { fs: false, path: false }; 24 | return config; 25 | }, 26 | }; 27 | 28 | module.exports = nextConfig; 29 | -------------------------------------------------------------------------------- /market-backend/src/EventStream.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs"; 2 | import { MARKET_ADDRESS } from "./config/constants"; 3 | import { aptosClient } from "./config/libs"; 4 | import { State } from "./State"; 5 | import { Event } from "./types/events/event"; 6 | 7 | export function eventStream(): Subject<{ 8 | state: State; 9 | events: Event[]; 10 | }> { 11 | return new Subject<{ state: State; events: Event[] }>(); 12 | } 13 | 14 | export async function events(eventField: string, seqNum: bigint) { 15 | const events = await aptosClient.getEventsByEventHandle( 16 | MARKET_ADDRESS!, 17 | `${MARKET_ADDRESS}::marketplace::MarketEvents`, 18 | eventField, 19 | { start: seqNum + 1n, limit: 100 } 20 | ); 21 | return events.map((e) => e as Event); 22 | } 23 | -------------------------------------------------------------------------------- /market-frontend/src/components/TokenCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | import { Token } from "../types"; 4 | import { TooltipSection } from "./TooltipSection"; 5 | 6 | type CardProps = { data: Token }; 7 | 8 | export function TokenCard({ data: token }: CardProps) { 9 | return ( 10 |
    11 | picture 19 |
    20 |

    {token.name}

    21 | 22 |
    23 |
    24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /market-backend/src/consumers/Consumer.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; 2 | import { State } from "../State"; 3 | import { Event } from "../types/events/event"; 4 | 5 | export interface Consumer { 6 | consumeAll(state: State, events: Event[]): Promise; 7 | consume( 8 | state: State, 9 | event: Event 10 | ): Promise<{ success: boolean; state: State }>; 11 | } 12 | 13 | export function dispatch( 14 | state: State, 15 | events: Event[], 16 | consumer: Consumer 17 | ): Promise { 18 | return consumer.consumeAll(state, events); 19 | } 20 | 21 | export function handleError(e: any) { 22 | if (e instanceof PrismaClientKnownRequestError) { 23 | const msg = `${e.code}: ${JSON.stringify(e.meta)}`; 24 | console.error(msg); 25 | } else { 26 | console.error(e); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /market-frontend/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /market-frontend/src/components/ListCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | import { Token } from "../types"; 4 | import { TooltipSection } from "./TooltipSection"; 5 | 6 | type CardProps = { key: string; data: Token; onClick: any }; 7 | export function ListCard({ data: token, onClick }: CardProps) { 8 | return ( 9 |
    10 | picture 18 |
    19 |

    {token.name}

    20 | 21 |
    22 | 29 |
    30 |
    31 |
    32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /market-frontend/src/components/AptosConnect.tsx: -------------------------------------------------------------------------------- 1 | import { useWallet } from "@manahippo/aptos-wallet-adapter"; 2 | import { useContext } from "react"; 3 | import { ModalContext } from "./ModalContext"; 4 | import { WalletModal } from "./WalletModal"; 5 | 6 | export function AptosConnect() { 7 | const { account } = useWallet(); 8 | const { modalState, setModalState } = useContext(ModalContext); 9 | 10 | return ( 11 | <> 12 | {account?.address ? ( 13 | 24 | ) : ( 25 | 31 | )} 32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /market-frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "market-frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "aptos:create-market": "npx node scripts/create_market.mjs", 10 | "aptos:buy-token": "npx node scripts/buy_token.mjs", 11 | "aptos:list-token": "npx node scripts/list_token.mjs" 12 | }, 13 | "dependencies": { 14 | "@manahippo/aptos-wallet-adapter": "^0.3.6", 15 | "@martiandao/aptos-web3-bip44.js": "^1.2.10", 16 | "@supabase/supabase-js": "^1.35.6", 17 | "@types/mime": "^3.0.1", 18 | "axios": "^0.27.2", 19 | "daisyui": "^2.24.0", 20 | "dotenv": "^16.0.1", 21 | "mime": "^3.0.0", 22 | "mongodb": "^4.9.0", 23 | "next": "^12.3.1-canary.0", 24 | "nft.storage": "^7.0.0", 25 | "react": "18.1.0", 26 | "react-dom": "18.1.0" 27 | }, 28 | "devDependencies": { 29 | "@types/node": "17.0.35", 30 | "@types/react": "18.0.9", 31 | "@types/react-dom": "18.0.5", 32 | "autoprefixer": "^10.4.7", 33 | "postcss": "^8.4.14", 34 | "tailwindcss": "^3.1.2", 35 | "typescript": "4.7.2" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /market-frontend/src/pages/dashboard.tsx: -------------------------------------------------------------------------------- 1 | import { useTokens } from "../hooks"; 2 | import { useRouter } from "next/router"; 3 | import { ListCard } from "../components/ListCard"; 4 | import { useWallet } from "@manahippo/aptos-wallet-adapter"; 5 | import { Loading } from "../components/Loading"; 6 | 7 | export default function Dashboard() { 8 | const { account } = useWallet(); 9 | const { tokens, loading } = useTokens(account); 10 | const router = useRouter(); 11 | 12 | return loading ? ( 13 | 14 | ) : !tokens.length ? ( 15 |

    No NFTs owned

    16 | ) : ( 17 |
    18 |
    19 |

    Items owned

    20 |
    21 | {tokens.map((token, i) => ( 22 | 26 | router.push( 27 | `/make-offer?creator=${token.creator}&name=${token.name}&collection=${token.collection}&description=${token.description}&uri=${token.uri}` 28 | ) 29 | } 30 | /> 31 | ))} 32 |
    33 |
    34 |
    35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /market-frontend/src/utils/aptos.ts: -------------------------------------------------------------------------------- 1 | import { WalletClient } from "@martiandao/aptos-web3-bip44.js"; 2 | import { APTOS_FAUCET_URL, APTOS_NODE_URL } from "../config/constants"; 3 | 4 | const MAX_U64_BIG_INT: bigint = BigInt(2 ** 64) - BigInt(1); 5 | 6 | export function createCollectionPayload( 7 | name: string, 8 | description: string, 9 | uri: string 10 | ) { 11 | return { 12 | type: "entry_function_payload", 13 | function: "0x3::token::create_collection_script", 14 | type_arguments: [], 15 | arguments: [ 16 | name, 17 | description, 18 | uri, 19 | MAX_U64_BIG_INT.toString(), 20 | [false, false, false], 21 | ], 22 | }; 23 | } 24 | 25 | export function createTokenPayload( 26 | collection: string, 27 | name: string, 28 | description: string, 29 | uri: string, 30 | royaltyPayee: string 31 | ) { 32 | return { 33 | type: "entry_function_payload", 34 | function: "0x3::token::create_token_script", 35 | type_arguments: [], 36 | arguments: [ 37 | collection, 38 | name, 39 | description, 40 | "1", 41 | MAX_U64_BIG_INT.toString(), 42 | uri, 43 | royaltyPayee, 44 | "100", 45 | "0", 46 | [false, false, false, false, false], 47 | [], 48 | [], 49 | [], 50 | ], 51 | }; 52 | } 53 | 54 | export const walletClient = new WalletClient(APTOS_NODE_URL, APTOS_FAUCET_URL); 55 | -------------------------------------------------------------------------------- /market-frontend/src/utils/nftstorage.ts: -------------------------------------------------------------------------------- 1 | import { NFTStorage } from "nft.storage"; 2 | import * as fs from "fs/promises"; 3 | import mime from "mime"; 4 | import path from "path"; 5 | import axios from "axios"; 6 | import { NFT_STORAGE_KEY } from "../config/constants"; 7 | 8 | export class NFTStorageClient { 9 | private nftStorage: NFTStorage; 10 | constructor(token: string) { 11 | this.nftStorage = new NFTStorage({ token }); 12 | } 13 | 14 | private async fileFromPath(file: string | File) { 15 | if (file instanceof File) return file; 16 | const content = await fs.readFile(file); 17 | const type = mime.getType(file)!; 18 | return new File([content], path.basename(file), { type }); 19 | } 20 | 21 | private convertGatewayURL(ipfsURL: string) { 22 | if (ipfsURL.startsWith("ipfs:")) 23 | return ( 24 | "https://nftstorage.link/ipfs/" + 25 | new URL(ipfsURL).pathname.replace(/^\/\//, "") 26 | ); 27 | return ipfsURL; 28 | } 29 | 30 | async upload(file: string | File, name: string, description: string) { 31 | const image = await this.fileFromPath(file); 32 | return await this.nftStorage.store({ image, name, description }); 33 | } 34 | 35 | async getImageURL(tokenURL: string) { 36 | let gatewayURL = this.convertGatewayURL(tokenURL); 37 | let image = (await axios.get(gatewayURL)).data.image; 38 | return this.convertGatewayURL(image); 39 | } 40 | } 41 | 42 | export const nftStorage = new NFTStorageClient(NFT_STORAGE_KEY!); -------------------------------------------------------------------------------- /market-frontend/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "../styles/globals.css"; 2 | import "../styles/loading.css"; 3 | import { NavBar } from "../components/NavBar"; 4 | import type { AppProps } from "next/app"; 5 | import { useMemo, useState } from "react"; 6 | import { 7 | FewchaWalletAdapter, 8 | PontemWalletAdapter, 9 | MartianWalletAdapter, 10 | WalletProvider, 11 | AptosWalletAdapter, 12 | } from "@manahippo/aptos-wallet-adapter"; 13 | import { ModalContext, ModalState } from "../components/ModalContext"; 14 | function MyApp({ Component, pageProps }: AppProps) { 15 | const [modalState, setModalState] = useState({ 16 | walletModal: false, 17 | }); 18 | const wallets = useMemo( 19 | () => [ 20 | new AptosWalletAdapter(), 21 | new MartianWalletAdapter(), 22 | new PontemWalletAdapter(), 23 | new FewchaWalletAdapter(), 24 | ], 25 | [] 26 | ); 27 | const modals = useMemo( 28 | () => ({ 29 | modalState, 30 | setModalState: (modalState: ModalState) => { 31 | setModalState(modalState); 32 | }, 33 | }), 34 | [modalState] 35 | ); 36 | 37 | return ( 38 | 39 | 40 |
    41 | 42 | 43 |
    44 |
    45 |
    46 | ); 47 | } 48 | 49 | export default MyApp; 50 | -------------------------------------------------------------------------------- /market-frontend/src/hooks/useTokens.ts: -------------------------------------------------------------------------------- 1 | import { AccountKeys } from "@manahippo/aptos-wallet-adapter"; 2 | import { useEffect, useState } from "react"; 3 | import { Token } from "../types"; 4 | import { walletClient } from "../utils/aptos"; 5 | 6 | export function useTokens(account: AccountKeys | null): { 7 | tokens: Token[]; 8 | loading: boolean; 9 | } { 10 | const [tokens, setTokens] = useState([]); 11 | const [loading, setLoading] = useState(true); 12 | useEffect(() => { 13 | const getTokens = async () => { 14 | const data = await walletClient.getTokenIds( 15 | account!.address!.toString(), 16 | 100, 17 | 0, 18 | 0 19 | ); 20 | const tokens = await Promise.all( 21 | data.tokenIds 22 | .filter((i) => i.difference != 0) 23 | .map(async (i) => { 24 | const token = await walletClient.getToken(i.data); 25 | return { 26 | propertyVersion: i.data.property_version, 27 | creator: i.data.token_data_id.creator, 28 | collection: token.collection, 29 | name: token.name, 30 | description: token.description, 31 | uri: token.uri, 32 | maximum: token.maximum, 33 | supply: token.supply, 34 | }; 35 | }) 36 | ); 37 | setLoading(false); 38 | setTokens(tokens); 39 | }; 40 | if (account?.address) { 41 | getTokens(); 42 | } 43 | }, [account]); 44 | return { tokens, loading }; 45 | } 46 | -------------------------------------------------------------------------------- /market-frontend/README.md: -------------------------------------------------------------------------------- 1 | # Next.js + Tailwind CSS Example 2 | 3 | This example shows how to use [Tailwind CSS](https://tailwindcss.com/) [(v3.0)](https://tailwindcss.com/blog/tailwindcss-v3) with Next.js. It follows the steps outlined in the official [Tailwind docs](https://tailwindcss.com/docs/guides/nextjs). 4 | 5 | ## Deploy your own 6 | 7 | Deploy the example using [Vercel](https://vercel.com?utm_source=github&utm_medium=readme&utm_campaign=next-example) or preview live with [StackBlitz](https://stackblitz.com/github/vercel/next.js/tree/canary/examples/with-tailwindcss) 8 | 9 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https://github.com/vercel/next.js/tree/canary/examples/with-tailwindcss&project-name=with-tailwindcss&repository-name=with-tailwindcss) 10 | 11 | ## How to use 12 | 13 | Execute [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: 14 | 15 | ```bash 16 | npx create-next-app --example with-tailwindcss with-tailwindcss-app 17 | ``` 18 | 19 | ```bash 20 | yarn create next-app --example with-tailwindcss with-tailwindcss-app 21 | ``` 22 | 23 | ```bash 24 | pnpm create next-app --example with-tailwindcss with-tailwindcss-app 25 | ``` 26 | 27 | Deploy it to the cloud with [Vercel](https://vercel.com/new?utm_source=github&utm_medium=readme&utm_campaign=next-example) ([Documentation](https://nextjs.org/docs/deployment)). 28 | -------------------------------------------------------------------------------- /market-frontend/scripts/create_market.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | AptosAccount, 3 | WalletClient, 4 | HexString, 5 | } from "@martiandao/aptos-web3-bip44.js"; 6 | import * as env from "dotenv"; 7 | env.config({ path: `.env.${process.env.NODE_ENV}.local` }); 8 | 9 | const { 10 | NEXT_PUBLIC_APTOS_NODE_URL: APTOS_NODE_URL, 11 | NEXT_PUBLIC_APTOS_FAUCET_URL: APTOS_FAUCET_URL, 12 | NEXT_PUBLIC_WALLET_PRIVATE_KEY: WALLET_PRIVATE_KEY, 13 | NEXT_PUBLIC_MARKET_COIN_TYPE: COIN_TYPE, 14 | NEXT_PUBLIC_MARKET_NAME: MARKET_NAME, 15 | NEXT_PUBLIC_MARKET_FEE_NUMERATOR: FEE_NUMERATOR, 16 | NEXT_PUBLIC_MARKET_INITIAL_FUND: INITIAL_FUND, 17 | } = process.env; 18 | 19 | async function main() { 20 | const client = new WalletClient(APTOS_NODE_URL, APTOS_FAUCET_URL); 21 | const account = new AptosAccount( 22 | HexString.ensure(WALLET_PRIVATE_KEY).toUint8Array() 23 | ); 24 | const payload = { 25 | function: `${account.address()}::marketplace::create_market`, 26 | type_arguments: [COIN_TYPE], 27 | arguments: [ 28 | MARKET_NAME, 29 | +FEE_NUMERATOR, 30 | `${account.address()}`, 31 | +INITIAL_FUND, 32 | ], 33 | }; 34 | const transaction = await client.aptosClient.generateTransaction( 35 | account.address(), 36 | payload, 37 | { gas_unit_price: 100 } 38 | ); 39 | const tx = await client.signAndSubmitTransaction(account, transaction); 40 | const result = await client.aptosClient.waitForTransactionWithResult(tx, { 41 | checkSuccess: true, 42 | }); 43 | console.log(result); 44 | 45 | client.signTransaction 46 | } 47 | 48 | main() 49 | .then(() => process.exit(0)) 50 | .catch((error) => { 51 | console.error(error); 52 | process.exit(1); 53 | }); 54 | -------------------------------------------------------------------------------- /market-backend/prisma/migrations/20220905143301_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateEnum 2 | CREATE TYPE "Status" AS ENUM ('ongoing', 'revoked', 'finished'); 3 | 4 | -- CreateTable 5 | CREATE TABLE "offers" ( 6 | "id" BIGINT NOT NULL, 7 | "tokenId" BIGSERIAL NOT NULL, 8 | "price" DOUBLE PRECISION NOT NULL, 9 | "seller" TEXT NOT NULL, 10 | "buyer" TEXT, 11 | "status" "Status" NOT NULL, 12 | "tokenPropertyVersion" BIGINT NOT NULL, 13 | "tokenCreator" TEXT NOT NULL, 14 | "tokenCollection" TEXT NOT NULL, 15 | "tokenName" TEXT NOT NULL, 16 | "createAt" TIMESTAMP(3) NOT NULL, 17 | "updateAt" TIMESTAMP(3), 18 | 19 | CONSTRAINT "offers_pkey" PRIMARY KEY ("id") 20 | ); 21 | 22 | -- CreateTable 23 | CREATE TABLE "tokens" ( 24 | "id" BIGSERIAL NOT NULL, 25 | "propertyVersion" BIGINT NOT NULL, 26 | "creator" TEXT NOT NULL, 27 | "collection" TEXT NOT NULL, 28 | "name" TEXT NOT NULL, 29 | "uri" TEXT NOT NULL, 30 | "description" TEXT NOT NULL, 31 | "maximum" BIGINT NOT NULL, 32 | "supply" BIGINT NOT NULL, 33 | 34 | CONSTRAINT "tokens_pkey" PRIMARY KEY ("id") 35 | ); 36 | 37 | -- CreateTable 38 | CREATE TABLE "executions" ( 39 | "id" BIGSERIAL NOT NULL, 40 | "listEventsExecutedSeqNum" BIGINT NOT NULL, 41 | "buyEventsExecutedSeqNum" BIGINT NOT NULL, 42 | 43 | CONSTRAINT "executions_pkey" PRIMARY KEY ("id") 44 | ); 45 | 46 | -- CreateIndex 47 | CREATE UNIQUE INDEX "tokens_propertyVersion_creator_collection_name_key" ON "tokens"("propertyVersion", "creator", "collection", "name"); 48 | 49 | -- AddForeignKey 50 | ALTER TABLE "offers" ADD CONSTRAINT "offers_tokenId_fkey" FOREIGN KEY ("tokenId") REFERENCES "tokens"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 51 | -------------------------------------------------------------------------------- /market-frontend/scripts/list_token.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | AptosAccount, 3 | WalletClient, 4 | HexString, 5 | } from "@martiandao/aptos-web3-bip44.js"; 6 | import * as env from "dotenv"; 7 | env.config({ path: `.env.${process.env.NODE_ENV}.local` }); 8 | 9 | const { 10 | NEXT_PUBLIC_APTOS_NODE_URL: APTOS_NODE_URL, 11 | NEXT_PUBLIC_APTOS_FAUCET_URL: APTOS_FAUCET_URL, 12 | NEXT_PUBLIC_ARBITRAGER_PRIVATE_KEY: ARBITRAGER_PRIVATE_KEY, 13 | NEXT_PUBLIC_MARKET_COIN_TYPE: COIN_TYPE, 14 | NEXT_PUBLIC_MARKET_ADDRESS: MARKET_ADDRESS, 15 | NEXT_PUBLIC_MARKET_NAME: MARKET_NAME, 16 | } = process.env; 17 | 18 | async function main() { 19 | const client = new WalletClient(APTOS_NODE_URL, APTOS_FAUCET_URL); 20 | const account = new AptosAccount( 21 | HexString.ensure(ARBITRAGER_PRIVATE_KEY).toUint8Array() 22 | ); 23 | const payload = { 24 | function: `${MARKET_ADDRESS}::marketplace::list_token`, 25 | type_arguments: [COIN_TYPE], 26 | arguments: [ 27 | MARKET_ADDRESS, 28 | MARKET_NAME, 29 | "0xa0153890d8a3c360bc1045b4a9566dcfade479c2a8d2122056186ddc8ad2e2bc", 30 | "cybercat", 31 | "cybercat", 32 | "0", 33 | "10", 34 | ], 35 | }; 36 | const transaction = await client.aptosClient.generateTransaction( 37 | account.address(), 38 | payload, 39 | { gas_unit_price: 100 } 40 | ); 41 | const tx = await client.signAndSubmitTransaction(account, transaction); 42 | const result = await client.aptosClient.waitForTransactionWithResult(tx, { 43 | checkSuccess: true, 44 | }); 45 | console.log(result.vm_status); 46 | } 47 | 48 | main() 49 | .then(() => process.exit(0)) 50 | .catch((error) => { 51 | console.error(error); 52 | process.exit(1); 53 | }); 54 | -------------------------------------------------------------------------------- /market-frontend/scripts/buy_token.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | AptosAccount, 3 | WalletClient, 4 | HexString, 5 | } from "@martiandao/aptos-web3-bip44.js"; 6 | import * as env from "dotenv"; 7 | env.config({ path: `.env.${process.env.NODE_ENV}.local` }); 8 | 9 | const { 10 | NEXT_PUBLIC_APTOS_NODE_URL: APTOS_NODE_URL, 11 | NEXT_PUBLIC_APTOS_FAUCET_URL: APTOS_FAUCET_URL, 12 | NEXT_PUBLIC_ARBITRAGER_PRIVATE_KEY: ARBITRAGER_PRIVATE_KEY, 13 | NEXT_PUBLIC_MARKET_COIN_TYPE: COIN_TYPE, 14 | NEXT_PUBLIC_MARKET_ADDRESS: MARKET_ADDRESS, 15 | NEXT_PUBLIC_MARKET_NAME: MARKET_NAME, 16 | } = process.env; 17 | 18 | async function main() { 19 | const client = new WalletClient(APTOS_NODE_URL, APTOS_FAUCET_URL); 20 | const account = new AptosAccount( 21 | HexString.ensure(ARBITRAGER_PRIVATE_KEY).toUint8Array() 22 | ); 23 | const payload = { 24 | function: `${MARKET_ADDRESS}::marketplace::buy_token`, 25 | type_arguments: [COIN_TYPE], 26 | arguments: [ 27 | MARKET_ADDRESS, 28 | MARKET_NAME, 29 | "0xa0153890d8a3c360bc1045b4a9566dcfade479c2a8d2122056186ddc8ad2e2bc", 30 | "cybercat", 31 | "cybercat", 32 | "0", 33 | "2", 34 | "12", 35 | ], 36 | }; 37 | const transaction = await client.aptosClient.generateTransaction( 38 | account.address(), 39 | payload, 40 | { gas_unit_price: 100 } 41 | ); 42 | const tx = await client.signAndSubmitTransaction(account, transaction); 43 | const result = await client.aptosClient.waitForTransactionWithResult(tx, { 44 | checkSuccess: true, 45 | }); 46 | console.log(result.vm_status); 47 | } 48 | 49 | main() 50 | .then(() => process.exit(0)) 51 | .catch((error) => { 52 | console.error(error); 53 | process.exit(1); 54 | }); 55 | -------------------------------------------------------------------------------- /market-backend/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | enum Status { 14 | ongoing 15 | revoked 16 | finished 17 | } 18 | 19 | model Offer { 20 | id BigInt @id 21 | token Token @relation(fields: [tokenId], references: [id]) 22 | tokenId BigInt @default(autoincrement()) 23 | price Float 24 | seller String 25 | buyer String? 26 | status Status 27 | tokenPropertyVersion BigInt 28 | tokenCreator String 29 | tokenCollection String 30 | tokenName String 31 | createAt DateTime 32 | updateAt DateTime? 33 | 34 | @@map("offers") 35 | } 36 | 37 | model Token { 38 | id BigInt @id @default(autoincrement()) 39 | propertyVersion BigInt 40 | creator String 41 | collection String 42 | name String 43 | uri String 44 | description String 45 | // royaltyPayee String 46 | // royaltyNumerator BigInt 47 | // royaltyDenominator BigInt 48 | // https://github.com/prisma/prisma/issues/14231 49 | maximum String? 50 | supply String 51 | Offer Offer[] 52 | 53 | @@unique([propertyVersion, creator, collection, name]) 54 | @@map("tokens") 55 | } 56 | 57 | model Execution { 58 | id BigInt @id @default(autoincrement()) 59 | listEventsExecutedSeqNum BigInt 60 | buyEventsExecutedSeqNum BigInt 61 | 62 | @@map("executions") 63 | } 64 | -------------------------------------------------------------------------------- /market-frontend/src/components/OfferCard.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import React from "react"; 3 | import { Offer } from "../types"; 4 | import { TooltipSection } from "./TooltipSection"; 5 | 6 | type CardProps = { data: Offer; onClick: any }; 7 | export function OfferCard({ data: offer, onClick }: CardProps) { 8 | return ( 9 |
    10 | picture 18 |
    19 |

    {offer.token.name}

    20 | 21 |
    29 | {"creator: " + offer.token.creator} 30 |
    31 |
    39 | {"seller: " + offer.seller} 40 |
    41 |
    42 | {offer.price} APT 43 |
    44 | 45 |
    46 | 53 |
    54 |
    55 |
    56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /market-frontend/src/pages/make-offer.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { 4 | MARKET_ADDRESS, 5 | MARKET_COINT_TYPE, 6 | MARKET_NAME, 7 | } from "../config/constants"; 8 | import { TokenCard } from "../components/TokenCard"; 9 | import { Token } from "../types"; 10 | import { useWallet } from "@manahippo/aptos-wallet-adapter"; 11 | 12 | export default function MakeOffer() { 13 | const router = useRouter(); 14 | const [loading, setLoading] = useState(false); 15 | const { creator, name, collection, uri, description } = router.query; 16 | const { signAndSubmitTransaction } = useWallet(); 17 | const [price, updatePrice] = useState(""); 18 | 19 | async function makeOffer() { 20 | setLoading(true); 21 | const payload = { 22 | type: "entry_function_payload", 23 | function: `${MARKET_ADDRESS}::marketplace::list_token`, 24 | type_arguments: [MARKET_COINT_TYPE], 25 | arguments: [ 26 | MARKET_ADDRESS, 27 | MARKET_NAME, 28 | creator, 29 | collection, 30 | name, 31 | "0", 32 | price, 33 | ], 34 | }; 35 | await signAndSubmitTransaction(payload, { gas_unit_price: 100 }); 36 | setLoading(false); 37 | router.push("/"); 38 | } 39 | 40 | return ( 41 |
    42 |
    43 | 44 |
    45 |
    46 | updatePrice(e.target.value)} 51 | /> 52 | 61 |
    62 |
    63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /market-frontend/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Offer } from "../types"; 2 | import { useOffers } from "../hooks"; 3 | import { useRouter } from "next/router"; 4 | import { 5 | MARKET_ADDRESS, 6 | MARKET_COINT_TYPE, 7 | MARKET_NAME, 8 | } from "../config/constants"; 9 | import { OfferCard } from "../components/OfferCard"; 10 | import { useWallet } from "@manahippo/aptos-wallet-adapter"; 11 | import { TransactionPayload } from "@martiandao/aptos-web3-bip44.js/dist/generated"; 12 | import { useContext } from "react"; 13 | import { ModalContext } from "../components/ModalContext"; 14 | import { Loading } from "../components/Loading"; 15 | 16 | export default function Home() { 17 | const router = useRouter(); 18 | const { account, signAndSubmitTransaction } = useWallet(); 19 | const { modalState, setModalState } = useContext(ModalContext); 20 | const { offers, loading } = useOffers(); 21 | 22 | async function claimOffer(offer: Offer) { 23 | if (!account) { 24 | setModalState({ ...modalState, walletModal: true }); 25 | return; 26 | } 27 | 28 | const payload: TransactionPayload = { 29 | type: "entry_function_payload", 30 | function: `${MARKET_ADDRESS}::marketplace::buy_token`, 31 | type_arguments: [MARKET_COINT_TYPE], 32 | arguments: [ 33 | MARKET_ADDRESS, 34 | MARKET_NAME, 35 | offer.token.creator, 36 | offer.token.collection, 37 | offer.token.name, 38 | `${offer.token.propertyVersion}`, 39 | `${offer.price}`, 40 | `${offer.id}`, 41 | ], 42 | }; 43 | 44 | await signAndSubmitTransaction(payload, { gas_unit_price: 100 }); 45 | router.push("/dashboard"); 46 | } 47 | 48 | return loading ? ( 49 | 50 | ) : !offers.length ? ( 51 |

    No items in marketplace

    52 | ) : ( 53 |
    54 |
    55 | {offers.map((offer) => ( 56 | claimOffer(offer)} /> 57 | ))} 58 |
    59 |
    60 | ); 61 | } 62 | -------------------------------------------------------------------------------- /market-frontend/src/components/WalletModal.tsx: -------------------------------------------------------------------------------- 1 | import { useWallet, Wallet } from "@manahippo/aptos-wallet-adapter"; 2 | import Image from "next/image"; 3 | import { useContext } from "react"; 4 | import { ModalContext } from "./ModalContext"; 5 | 6 | export function WalletModal() { 7 | const { wallets, connect, account, disconnect } = useWallet(); 8 | const { modalState, setModalState } = useContext(ModalContext); 9 | 10 | async function connectWallet(wallet: Wallet) { 11 | connect(wallet.adapter.name); 12 | setModalState({ ...modalState, walletModal: false }); 13 | } 14 | 15 | function disconnectWallet() { 16 | disconnect(); 17 | setModalState({ ...modalState, walletModal: false }); 18 | } 19 | 20 | function modalBox(content: JSX.Element) { 21 | return ( 22 | <> 23 | 43 | 44 | ); 45 | } 46 | 47 | return account?.address 48 | ? modalBox( 49 | <> 50 |

    57 | Account: {account!.address!.toString()} 58 |

    59 | 62 | 63 | ) 64 | : modalBox( 65 | <> 66 | {wallets.map((wallet: Wallet, i) => { 67 | return ( 68 | 78 | ); 79 | })} 80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /market-backend/src/consumers/BuyEventConsumer.ts: -------------------------------------------------------------------------------- 1 | import { prismaClient } from "../config/libs"; 2 | import { State } from "../State"; 3 | import { BuyTokenEventData, Event } from "../types"; 4 | import { Consumer, handleError } from "./Consumer"; 5 | 6 | export class BuyEventConsumer implements Consumer { 7 | async consumeAll( 8 | state: State, 9 | events: Event[] 10 | ): Promise { 11 | delete state.old; 12 | let newState = state; 13 | newState.old = { ...state }; 14 | for (const event of events) { 15 | const { success, state } = await this.consume(newState, event); 16 | if (success) { 17 | newState.buyEventsExecutedSeqNum = state.buyEventsExecutedSeqNum; 18 | } else { 19 | return newState; 20 | } 21 | } 22 | return newState; 23 | } 24 | 25 | async consume( 26 | state: State, 27 | event: Event 28 | ): Promise<{ success: boolean; state: State }> { 29 | let newState = state; 30 | const data = event.data; 31 | const executedSeqNum = BigInt(event.sequence_number); 32 | const tokenDataId = data.token_id.token_data_id; 33 | 34 | const tokenCollection = tokenDataId.collection; 35 | const tokenCreator = tokenDataId.creator; 36 | const tokenName = tokenDataId.name; 37 | 38 | const tokenPropertyVersion = BigInt(data.token_id.property_version); 39 | const offerId = BigInt(data.offer_id); 40 | const updateAt = new Date(parseInt(data.timestamp) / 1000); 41 | const price = parseFloat(data.price); 42 | const seller = data.seller; 43 | const buyer = data.buyer; 44 | 45 | const updateOffer = prismaClient.offer.update({ 46 | where: { 47 | id: offerId, 48 | }, 49 | data: { 50 | tokenPropertyVersion, 51 | tokenCollection, 52 | tokenCreator, 53 | tokenName, 54 | price, 55 | seller, 56 | buyer, 57 | updateAt, 58 | status: "finished", 59 | }, 60 | }); 61 | 62 | const updateSeqNum = prismaClient.execution.update({ 63 | where: { id: 1 }, 64 | data: { 65 | buyEventsExecutedSeqNum: executedSeqNum, 66 | }, 67 | }); 68 | try { 69 | const [_, state] = await prismaClient.$transaction([ 70 | updateOffer, 71 | updateSeqNum, 72 | ]); 73 | newState.buyEventsExecutedSeqNum = state.buyEventsExecutedSeqNum; 74 | return { success: true, state: newState }; 75 | } catch (e: any) { 76 | handleError(e); 77 | return { success: false, state: newState }; 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /market-backend/src/Execution.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from "rxjs"; 2 | import { prismaClient } from "./config/libs"; 3 | import { BuyEventConsumer, dispatch, ListEventConsumer } from "./consumers"; 4 | import { StateFlow, State } from "./State"; 5 | import { events, eventStream } from "./EventStream"; 6 | import { BuyTokenEventData, ListTokenEventData, Event } from "./types"; 7 | import { delay } from "./utils/delay"; 8 | 9 | async function main() { 10 | const listStateFlow: StateFlow = new Subject(); 11 | const listEventstream = eventStream(); 12 | listStateFlow.subscribe(async (state) => { 13 | await produceListEventsIfStateChange(listEventstream, { ...state }); 14 | }); 15 | listEventstream.subscribe(async ({ state, events }) => { 16 | listStateFlow.next(await dispatch(state, events, new ListEventConsumer())); 17 | }); 18 | listStateFlow.next(await initialState()); 19 | 20 | const buyStateFlow: StateFlow = new Subject(); 21 | const buyEventstream = eventStream(); 22 | buyStateFlow.subscribe(async (state) => { 23 | await produceBuyEventsIfStateChange(buyEventstream, { ...state }); 24 | }); 25 | buyEventstream.subscribe(async ({ state, events }) => { 26 | buyStateFlow.next(await dispatch(state, events, new BuyEventConsumer())); 27 | }); 28 | buyStateFlow.next(await initialState()); 29 | } 30 | 31 | async function produceListEventsIfStateChange( 32 | eventStream: Subject<{ 33 | state: State; 34 | events: Event[]; 35 | }>, 36 | state: State 37 | ) { 38 | if (state.listEventsExecutedSeqNum <= state.old!.listEventsExecutedSeqNum) { 39 | await delay(5000); 40 | } 41 | eventStream.next({ 42 | state, 43 | events: await events( 44 | "list_token_events", 45 | state.listEventsExecutedSeqNum 46 | ), 47 | }); 48 | } 49 | 50 | async function produceBuyEventsIfStateChange( 51 | eventStream: Subject<{ 52 | state: State; 53 | events: Event[]; 54 | }>, 55 | state: State 56 | ) { 57 | if (state.buyEventsExecutedSeqNum <= state.old!.buyEventsExecutedSeqNum) { 58 | await delay(5000); 59 | } 60 | eventStream.next({ 61 | state, 62 | events: await events( 63 | "buy_token_events", 64 | state.buyEventsExecutedSeqNum 65 | ), 66 | }); 67 | } 68 | 69 | async function initialState(): Promise { 70 | const execution = await prismaClient.execution.findUnique({ 71 | where: { id: 1 }, 72 | }); 73 | if (!execution) { 74 | throw new Error("Missing initial state"); 75 | } 76 | return { 77 | old: { 78 | listEventsExecutedSeqNum: -1n, 79 | buyEventsExecutedSeqNum: -1n, 80 | }, 81 | listEventsExecutedSeqNum: execution.listEventsExecutedSeqNum, 82 | buyEventsExecutedSeqNum: execution.buyEventsExecutedSeqNum, 83 | }; 84 | } 85 | 86 | main(); 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Demo NFT marketplace 2 | 3 |

    4 | 5 |

    6 | 7 | A full stack demo NFT marketplace based on aptos devnet. Aiming for clean and cocise code that is easy to understand and also make some automations to speed up full stack development. 8 | 9 | ## Tech stack 10 | 11 | - Lang: Move, Typescript 12 | - Frontend: [Next.js](https://nextjs.org/), [tailwindcss](https://tailwindcss.com), [daisyui](https://daisyui.com) 13 | - Backend: [Supabase](https://supabase.com), [Prisma](https://prisma.io), [Rxjs](https://rxjs.dev/) 14 | 15 | ## Development 16 | 17 | ### Contract 18 | 19 | - Compile move module 20 | 21 | ``` 22 | NAME= ADDRESS=
    yarn workspace market-contracts move:compile 23 | ``` 24 | 25 | - Publish move module 26 | 27 | ``` 28 | NAME= ADDRESS=
    yarn workspace market-contracts move:publish 29 | ``` 30 | 31 | ### Frontend 32 | 33 | - Installation 34 | 35 | ``` 36 | yarn workspace market-frontend install 37 | ``` 38 | 39 | - Config `.env.development.local` 40 | 41 | In order to expose a variable to the browser you have to prefix the variable with `NEXT_PUBLIC_` 42 | 43 | ``` 44 | NEXT_PUBLIC_NFT_STORAGE_KEY= 45 | NEXT_PUBLIC_WALLET_PRIVATE_KEY= 46 | NEXT_PUBLIC_MARKET_ADDRESS=0x64f236ab7ba803a8921c16fa2b9995da51033e3ed2e284e358f0d5431a39c0d0 47 | NEXT_PUBLIC_MARKET_NAME=_1200_dollars_per_hour 48 | NEXT_PUBLIC_MARKET_FEE_NUMERATOR=10 49 | NEXT_PUBLIC_MARKET_INITIAL_FUND=10000 50 | NEXT_PUBLIC_MARKET_COIN_TYPE=0x1::aptos_coin::AptosCoin 51 | NEXT_PUBLIC_APTOS_NODE_URL=https://fullnode.devnet.aptoslabs.com/v1/ 52 | NEXT_PUBLIC_APTOS_FAUCET_URL=https://faucet.devnet.aptoslabs.com/v1/ 53 | NEXT_PUBLIC_SUPABASE_KEY= 54 | NEXT_PUBLIC_SUPABASE_URL= 55 | ``` 56 | 57 | Read more on [https://nextjs.org/docs/basic-features/environment-variables](https://nextjs.org/docs/basic-features/environment-variables) 58 | 59 | - Run script for create market 60 | 61 | ``` 62 | NODE_ENV=development yarn workspace market-frontend aptos:create-market 63 | ``` 64 | 65 | - Run dev 66 | 67 | ``` 68 | yarn workspace market-frontend dev 69 | ``` 70 | 71 | ### Backend 72 | 73 | - Setup 74 | 75 | ``` 76 | // install all the dependencies listed within package.json 77 | yarn workspace market-backend install 78 | 79 | // set up a new Prisma project 80 | yarn workspace market-backend prisma init 81 | ``` 82 | 83 | - Config `.env` 84 | 85 | ``` 86 | DATABASE_URL= 87 | APTOS_NODE_URL=https://fullnode.devnet.aptoslabs.com/v1/ 88 | APTOS_FAUCET_URL=https://faucet.devnet.aptoslabs.com/ 89 | MARKET_ADDRESS= 90 | ``` 91 | 92 | - DB Migration 93 | 94 | ``` 95 | // create migrations from your Prisma schema, apply them to the database 96 | yarn workspace market-backend prisma migrate dev --name 97 | ``` 98 | 99 | - Run 100 | ``` 101 | yarn workspace market-backend start 102 | ``` 103 | -------------------------------------------------------------------------------- /market-backend/src/consumers/ListEventConsumer.ts: -------------------------------------------------------------------------------- 1 | import { Execution } from "@prisma/client"; 2 | import { prismaClient, walletClient } from "../config/libs"; 3 | import { State } from "../State"; 4 | import { ListTokenEventData, Event } from "../types"; 5 | import { TokenData } from "../types/structs/TokenData"; 6 | import { Consumer, handleError } from "./Consumer"; 7 | 8 | export class ListEventConsumer implements Consumer { 9 | async consumeAll( 10 | state: State, 11 | events: Event[] 12 | ): Promise { 13 | delete state.old; 14 | let newState = state; 15 | newState.old = { ...state }; 16 | for (const event of events) { 17 | const { success, state } = await this.consume(newState, event); 18 | if (success) { 19 | newState.listEventsExecutedSeqNum = state.listEventsExecutedSeqNum; 20 | } else { 21 | return newState; 22 | } 23 | } 24 | return newState; 25 | } 26 | async consume( 27 | state: State, 28 | event: Event 29 | ): Promise<{ success: boolean; state: State }> { 30 | let newState = state; 31 | 32 | const transactions = []; 33 | const data = event.data as ListTokenEventData; 34 | const tokenDataId = data.token_id.token_data_id; 35 | 36 | const creator = tokenDataId.creator; 37 | const propertyVersion = BigInt(data.token_id.property_version); 38 | const collection = tokenDataId.collection; 39 | const name = tokenDataId.name; 40 | let token = await prismaClient.token.findUnique({ 41 | where: { 42 | propertyVersion_creator_collection_name: { 43 | propertyVersion, 44 | creator, 45 | collection, 46 | name, 47 | }, 48 | }, 49 | }); 50 | 51 | try { 52 | if (!token) { 53 | const { description, uri, maximum, supply } = 54 | (await walletClient.getToken(data.token_id)) as TokenData; 55 | token = await prismaClient.token.create({ 56 | data: { 57 | creator, 58 | propertyVersion, 59 | collection, 60 | name, 61 | uri, 62 | description, 63 | maximum: maximum, 64 | supply: supply, 65 | }, 66 | }); 67 | } 68 | } catch (e) { 69 | handleError(e); 70 | return { success: false, state: newState }; 71 | } 72 | 73 | const executedSeqNum = BigInt(event.sequence_number); 74 | const createAt = new Date(parseInt(data.timestamp) / 1000); 75 | const offerId = BigInt(data.offer_id); 76 | const price = parseFloat(data.price); 77 | const seller = data.seller; 78 | 79 | transactions.push( 80 | prismaClient.execution.update({ 81 | where: { id: 1 }, 82 | data: { 83 | listEventsExecutedSeqNum: executedSeqNum, 84 | }, 85 | }) 86 | ); 87 | 88 | transactions.push( 89 | prismaClient.offer.create({ 90 | data: { 91 | id: offerId, 92 | // FIXME: use token:{connectOrCreate} 93 | tokenId: token.id, 94 | price, 95 | seller, 96 | status: "ongoing", 97 | tokenPropertyVersion: propertyVersion, 98 | tokenCreator: creator, 99 | tokenCollection: collection, 100 | tokenName: name, 101 | createAt, 102 | }, 103 | }) 104 | ); 105 | 106 | try { 107 | const [execution, _] = await prismaClient.$transaction(transactions); 108 | newState.listEventsExecutedSeqNum = ( 109 | execution as Execution 110 | ).listEventsExecutedSeqNum; 111 | return { success: true, state: newState }; 112 | } catch (e: any) { 113 | handleError(e); 114 | return { success: false, state: newState }; 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /market-frontend/src/pages/mint.tsx: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, useState } from "react"; 2 | import { useRouter } from "next/router"; 3 | import { nftStorage } from "../utils/nftstorage"; 4 | import { useWallet } from "@manahippo/aptos-wallet-adapter"; 5 | import { 6 | createCollectionPayload, 7 | createTokenPayload, 8 | walletClient, 9 | } from "../utils/aptos"; 10 | 11 | export default function Mint() { 12 | const router = useRouter(); 13 | const [loading, setLoading] = useState(false); 14 | const { account, signAndSubmitTransaction } = useWallet(); 15 | const [base64image, setBase64image] = useState(""); 16 | const [formInput, updateFormInput] = useState<{ 17 | collection: string; 18 | name: string; 19 | description: string; 20 | file: File | null; 21 | }>({ 22 | collection: "", 23 | name: "", 24 | description: "", 25 | file: null, 26 | }); 27 | 28 | async function onChange(e: ChangeEvent) { 29 | const file = e.target.files![0]; 30 | updateFormInput({ ...formInput, file: file }); 31 | const reader = new FileReader(); 32 | reader.onload = function (event) { 33 | setBase64image(event.target!.result!.toString()); 34 | }; 35 | reader.readAsDataURL(file); 36 | } 37 | 38 | async function createCollection(address: string, collection: string) { 39 | try { 40 | await walletClient.getCollection(address, collection); 41 | } catch (e) { 42 | await signAndSubmitTransaction( 43 | createCollectionPayload( 44 | collection, 45 | "_1200_dollars_per_hour", 46 | "https://github.com/amovane/aptos-NFT-marketplace" 47 | ), 48 | { gas_unit_price: 100 } 49 | ); 50 | } 51 | } 52 | 53 | async function mintNFT() { 54 | const { collection, name, description, file } = formInput; 55 | if (!account || !collection || !name || !description || !file) return; 56 | setLoading(true); 57 | try { 58 | const address = account!.address!.toString(); 59 | const token = await nftStorage.upload(file, name, description); 60 | const image = await nftStorage.getImageURL(token.url); 61 | 62 | await createCollection(address, collection); 63 | await signAndSubmitTransaction( 64 | createTokenPayload(collection, name, description, image, address), 65 | { gas_unit_price: 100 } 66 | ); 67 | setLoading(false); 68 | router.push("/dashboard"); 69 | } catch (error) { 70 | console.log("Error create NFT: ", error); 71 | } finally { 72 | setLoading(false); 73 | } 74 | } 75 | 76 | return ( 77 |
    78 |
    79 | 83 | updateFormInput({ ...formInput, collection: e.target.value }) 84 | } 85 | /> 86 | 90 | updateFormInput({ ...formInput, name: e.target.value }) 91 | } 92 | /> 93 |