├── .husky ├── .gitignore └── pre-commit ├── .prettierrc.json ├── .prettierignore ├── src ├── hooks │ ├── login │ │ ├── index.ts │ │ └── login.ts │ ├── sidebar │ │ ├── index.ts │ │ └── sidebar.ts │ ├── tokenList │ │ ├── index.ts │ │ └── tokenlist.ts │ └── outsideAlerter │ │ ├── index.ts │ │ └── outsideAlerter.ts ├── modules │ ├── sol │ │ ├── index.ts │ │ └── sol.ts │ ├── address │ │ ├── index.ts │ │ └── address.ts │ ├── auction-house │ │ ├── index.ts │ │ ├── UpdateAuctionHouse.ts │ │ └── updateAuctionHouse.ts │ ├── ipfs │ │ ├── types.ts │ │ └── client.ts │ └── multi-transaction │ │ └── index.tsx ├── components │ ├── List │ │ ├── index.ts │ │ └── List.tsx │ ├── Modal │ │ ├── index.tsx │ │ └── Modal.tsx │ ├── NftCard │ │ ├── index.ts │ │ └── NftCard.tsx │ ├── Slider │ │ ├── index.ts │ │ └── Slider.tsx │ ├── EmptyTreasuryWalletForm │ │ ├── index.ts │ │ └── EmptyTreasuryWalletForm.tsx │ ├── WalletPortal │ │ ├── index.ts │ │ └── WalletPortal.tsx │ ├── Avatar │ │ └── index.tsx │ ├── DialectNotificationsButton │ │ ├── CloseIcon.tsx │ │ ├── BellIcon.tsx │ │ ├── SettingsIcon.tsx │ │ └── index.tsx │ ├── icons │ │ └── Close.tsx │ ├── Price │ │ └── index.tsx │ ├── SplToken │ │ └── index.tsx │ ├── NftPreview │ │ └── index.tsx │ ├── UploadFile │ │ └── index.tsx │ ├── AdminMenu │ │ └── index.tsx │ ├── Chart │ │ └── index.tsx │ ├── Button │ │ └── index.tsx │ ├── CancelOfferForm │ │ └── index.tsx │ └── AcceptOfferForm │ │ └── index.tsx ├── layouts │ ├── Modal │ │ ├── index.tsx │ │ └── Modal.tsx │ ├── Nft │ │ └── index.ts │ ├── Admin │ │ ├── index.ts │ │ └── AdminLayout.tsx │ ├── Analytics │ │ └── index.ts │ ├── Banner │ │ ├── index.ts │ │ └── BannerLayout.tsx │ └── Basic │ │ ├── index.ts │ │ └── BasicLayout.tsx ├── providers │ └── Viewer │ │ ├── index.ts │ │ └── Viewer.tsx ├── cache.ts ├── pages │ ├── 404.tsx │ ├── _app.tsx │ ├── analytics │ │ └── index.tsx │ ├── creators │ │ ├── [creator] │ │ │ └── analytics.tsx │ │ └── index.tsx │ ├── admin │ │ ├── financials │ │ │ └── edit.tsx │ │ ├── creators │ │ │ └── edit.tsx │ │ └── marketplace │ │ │ └── edit.tsx │ └── nfts │ │ ├── [address].tsx │ │ └── [address] │ │ ├── offers │ │ └── new.tsx │ │ └── listings │ │ └── new.tsx ├── utils │ └── errorCodes.ts ├── client.ts └── styles │ └── globals.css ├── .eslintrc.json ├── .babelrc ├── public ├── favicon.ico ├── images │ ├── analytics_icon.svg │ └── uturn.svg └── vercel.svg ├── .env.development ├── .prettierrc ├── postcss.config.js ├── .env ├── next-env.d.ts ├── templates └── default.conf.template ├── docker-compose.yaml ├── k8s ├── prod │ ├── app │ │ ├── configMap.yaml │ │ └── marketplace-deploy.yaml │ └── argocd │ │ ├── argo-app.yaml │ │ └── argo-proj.yaml ├── dev │ ├── app │ │ ├── configMap.yaml │ │ └── marketplace-deploy.yaml │ └── argocd │ │ ├── argo-app.yaml │ │ └── argo-proj.yaml └── canary │ ├── argocd │ ├── argo-app.yaml │ └── argo-proj.yaml │ └── app │ └── marketplace-deploy.yaml ├── .github └── workflows │ ├── test.yml │ └── main.yml ├── .gitignore ├── tsconfig.json ├── next.config.js ├── tailwind.config.js ├── Dockerfile ├── README.md ├── package.json └── docs └── readme.md /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | public 3 | node_modules 4 | -------------------------------------------------------------------------------- /src/hooks/login/index.ts: -------------------------------------------------------------------------------- 1 | export * from './login' 2 | -------------------------------------------------------------------------------- /src/modules/sol/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sol' 2 | -------------------------------------------------------------------------------- /src/components/List/index.ts: -------------------------------------------------------------------------------- 1 | export * from './List' 2 | -------------------------------------------------------------------------------- /src/components/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Modal' 2 | -------------------------------------------------------------------------------- /src/hooks/sidebar/index.ts: -------------------------------------------------------------------------------- 1 | export * from './sidebar' 2 | -------------------------------------------------------------------------------- /src/layouts/Modal/index.tsx: -------------------------------------------------------------------------------- 1 | export * from './Modal' 2 | -------------------------------------------------------------------------------- /src/layouts/Nft/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NftLayout' 2 | -------------------------------------------------------------------------------- /src/modules/address/index.ts: -------------------------------------------------------------------------------- 1 | export * from './address' 2 | -------------------------------------------------------------------------------- /src/providers/Viewer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Viewer' 2 | -------------------------------------------------------------------------------- /src/components/NftCard/index.ts: -------------------------------------------------------------------------------- 1 | export * from './NftCard' 2 | -------------------------------------------------------------------------------- /src/components/Slider/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Slider' 2 | -------------------------------------------------------------------------------- /src/hooks/tokenList/index.ts: -------------------------------------------------------------------------------- 1 | export * from './tokenlist' 2 | -------------------------------------------------------------------------------- /src/layouts/Admin/index.ts: -------------------------------------------------------------------------------- 1 | export * from './AdminLayout' 2 | -------------------------------------------------------------------------------- /src/layouts/Analytics/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Analytics' 2 | -------------------------------------------------------------------------------- /src/layouts/Banner/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BannerLayout' 2 | -------------------------------------------------------------------------------- /src/layouts/Basic/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BasicLayout' 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /src/hooks/outsideAlerter/index.ts: -------------------------------------------------------------------------------- 1 | export * from './outsideAlerter' 2 | -------------------------------------------------------------------------------- /src/modules/auction-house/index.ts: -------------------------------------------------------------------------------- 1 | export * from './updateAuctionHouse' 2 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["next/babel"], 3 | "plugins": ["graphql-tag"] 4 | } 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /src/components/EmptyTreasuryWalletForm/index.ts: -------------------------------------------------------------------------------- 1 | export * from './EmptyTreasuryWalletForm' 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cutupdev/Solana-NFT-Marketplace/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_GRAPHQL_ENDPOINT="https://graph-test.holaplex.com/v1" 2 | MARKETPLACE_SUBDOMAIN=mpw 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /src/components/WalletPortal/index.ts: -------------------------------------------------------------------------------- 1 | import WalletPortal from './WalletPortal' 2 | 3 | export default WalletPortal 4 | -------------------------------------------------------------------------------- /src/modules/ipfs/types.ts: -------------------------------------------------------------------------------- 1 | export type PinFileResponse = { 2 | uri?: string 3 | name?: string 4 | type?: string 5 | error?: string 6 | } 7 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_SOLANA_ENDPOINT="https://rpc.mainnet.holaplex.tools" 2 | NEXT_PUBLIC_GRAPHQL_ENDPOINT="https://graph-test.holaplex.com/v1" 3 | SUBDOMAIN= 4 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/cache.ts: -------------------------------------------------------------------------------- 1 | import { makeVar } from '@apollo/client' 2 | import { Viewer } from '@holaplex/marketplace-js-sdk' 3 | 4 | export const viewerVar = makeVar(null) 5 | 6 | export const sidebarOpenVar = makeVar(false) 7 | -------------------------------------------------------------------------------- /src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | const NotFound = () => { 2 | return ( 3 |
4 |

Page not found

5 |
6 | ) 7 | } 8 | 9 | export default NotFound 10 | -------------------------------------------------------------------------------- /src/components/Slider/Slider.tsx: -------------------------------------------------------------------------------- 1 | import Carousel, { CarouselProps } from 'react-multi-carousel' 2 | import cx from 'classnames' 3 | 4 | export const Slider = ({ className, ...props }: CarouselProps) => ( 5 | 6 | ) 7 | -------------------------------------------------------------------------------- /templates/default.conf.template: -------------------------------------------------------------------------------- 1 | server { 2 | listen ${PORT}; 3 | 4 | server_name ~^(?\w+)\.${HOSTNAME}$; 5 | 6 | location / { 7 | proxy_set_header X-Holaplex-Subdomain $subdomain; 8 | 9 | proxy_pass http://${PROXY_HOST}:${WEB_PORT}; 10 | } 11 | } -------------------------------------------------------------------------------- /public/images/analytics_icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3.9' 2 | services: 3 | proxy: 4 | volumes: 5 | - ./templates:/etc/nginx/templates 6 | image: nginx 7 | ports: 8 | - 8081:4000 9 | environment: 10 | - HOSTNAME=localhost 11 | - PORT=4000 12 | - WEB_PORT=8080 13 | - PROXY_HOST=host.docker.internal 14 | -------------------------------------------------------------------------------- /k8s/prod/app/configMap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: marketplace-config 5 | data: 6 | NODE_ENV: production 7 | NEXT_PUBLIC_ENVIRONMENT: production 8 | NEXT_PUBLIC_SOLANA_ENDPOINT: https://rpc.mainnet.holaplex.tools 9 | NEXT_PUBLIC_INDEXER_GRAPHQL_URL: https://graph.holaplex.com/v1 10 | -------------------------------------------------------------------------------- /k8s/dev/app/configMap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: marketplace-config 5 | data: 6 | NODE_ENV: development 7 | NEXT_PUBLIC_ENVIRONMENT: development 8 | NEXT_PUBLIC_SOLANA_ENDPOINT: https://rpc.mainnet.holaplex.tools 9 | NEXT_PUBLIC_INDEXER_GRAPHQL_URL: https://graph-test.holaplex.com/v1 10 | -------------------------------------------------------------------------------- /src/components/Avatar/index.tsx: -------------------------------------------------------------------------------- 1 | interface AvatarProps { 2 | name: string 3 | url?: string 4 | } 5 | 6 | const Avatar = ({ name, url }: AvatarProps) => ( 7 |
8 | {url && label} 9 |
{name}
10 |
11 | ) 12 | 13 | export default Avatar 14 | -------------------------------------------------------------------------------- /k8s/prod/argocd/argo-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: prod-marketplace 5 | namespace: argocd 6 | spec: 7 | project: prod-marketplace 8 | source: 9 | repoURL: https://github.com/holaplex/marketplace.git 10 | targetRevision: main 11 | path: k8s/prod/app 12 | destination: 13 | server: https://kubernetes.default.svc 14 | namespace: prod-marketplace 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | branches: [dev, main] 7 | 8 | jobs: 9 | test: 10 | name: All Checks 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | - name: Setup Node 17 | uses: actions/setup-node@v2 18 | with: 19 | node-version: '16' 20 | cache: 'yarn' 21 | - run: yarn install 22 | -------------------------------------------------------------------------------- /src/modules/sol/sol.ts: -------------------------------------------------------------------------------- 1 | import { LAMPORTS_PER_SOL } from '@solana/web3.js' 2 | import { equals } from 'ramda' 3 | 4 | export const toSOL = (lamports: number, precision: number = 5) => { 5 | var multiplier = Math.pow(10, precision) 6 | 7 | return Math.round((lamports / LAMPORTS_PER_SOL) * multiplier) / multiplier 8 | } 9 | 10 | export const NATIVE_MINT_ADDRESS = 'So11111111111111111111111111111111111111112' 11 | 12 | export const isSol = equals(NATIVE_MINT_ADDRESS) 13 | -------------------------------------------------------------------------------- /src/hooks/outsideAlerter/outsideAlerter.ts: -------------------------------------------------------------------------------- 1 | import { RefObject, useEffect } from 'react' 2 | 3 | export const useOutsideAlerter = (ref: RefObject, cb: VoidFunction) => { 4 | useEffect(() => { 5 | const handleClickOutside = (event: Event) => { 6 | if (ref?.current && !ref?.current?.contains(event.target as Node)) { 7 | cb() 8 | } 9 | } 10 | document.addEventListener('mousedown', handleClickOutside) 11 | return () => { 12 | document.removeEventListener('mousedown', handleClickOutside) 13 | } 14 | }, [cb, ref]) 15 | } 16 | -------------------------------------------------------------------------------- /src/hooks/login/login.ts: -------------------------------------------------------------------------------- 1 | import { useWallet } from '@solana/wallet-adapter-react' 2 | import { useWalletModal } from '@solana/wallet-adapter-react-ui' 3 | import { ifElse, always, isNil } from 'ramda' 4 | 5 | export const useLogin = () => { 6 | const { wallet, connect } = useWallet() 7 | 8 | const { setVisible } = useWalletModal() 9 | 10 | const openModal = async () => { 11 | setVisible(true) 12 | 13 | return Promise.resolve() 14 | } 15 | 16 | const onConnect = ifElse(isNil, always(openModal), always(connect))(wallet) 17 | 18 | return onConnect 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | 36 | # typescript 37 | *.tsbuildinfo 38 | -------------------------------------------------------------------------------- /src/components/DialectNotificationsButton/CloseIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | const SvgX = (props: SVGProps) => ( 5 | 13 | 19 | 20 | ) 21 | 22 | export default SvgX 23 | -------------------------------------------------------------------------------- /src/modules/address/address.ts: -------------------------------------------------------------------------------- 1 | import { PublicKey } from '@solana/web3.js' 2 | import { pipe, split, take, join, takeLast, add } from 'ramda' 3 | 4 | export const truncateAddress = pipe( 5 | split(''), 6 | (characters: string[]): string[] => [ 7 | pipe(take(4), join(''))(characters), 8 | pipe(takeLast(4), join(''))(characters), 9 | ], 10 | join('...') 11 | ) 12 | 13 | export const addressAvatar = (publicKey: PublicKey) => { 14 | const gradient = publicKey.toBytes().reduce(add, 0) % 8 15 | return `https://market.holaplex.com/images/gradients/gradient-${ 16 | gradient + 1 17 | }.png` 18 | } 19 | -------------------------------------------------------------------------------- /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 | }, 19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 20 | "exclude": ["node_modules"] 21 | } 22 | -------------------------------------------------------------------------------- /src/components/DialectNotificationsButton/BellIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | const SvgComponent = (props: SVGProps) => ( 5 | 11 | 18 | 19 | ) 20 | 21 | export default SvgComponent 22 | -------------------------------------------------------------------------------- /src/components/icons/Close.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { FC, SVGProps } from 'react' 3 | 4 | type CloseProps = SVGProps & { 5 | color: string 6 | } 7 | 8 | export const Close: FC = ({ color, ...props }) => { 9 | return ( 10 | 17 | 24 | 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /public/images/uturn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/components/DialectNotificationsButton/SettingsIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import { SVGProps } from 'react' 3 | 4 | const SvgComponent = (props: SVGProps) => ( 5 | 12 | 18 | 19 | 20 | 21 | 22 | ) 23 | 24 | export default SvgComponent 25 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | webpack(config) { 4 | config.resolve.fallback = { 5 | ...config.resolve.fallback, // if you miss it, all the other options in fallback, specified 6 | // by next.js will be dropped. Doesn't make much sense, but how it is 7 | fs: false, // the solution 8 | } 9 | 10 | return config 11 | }, 12 | reactStrictMode: false, 13 | env: { 14 | NEXT_PUBLIC_GRAPH_ENDPOINT: process.env.NEXT_PUBLIC_GRAPH_ENDPOINT, 15 | NEXT_PUBLIC_SOLANA_ENDPOINT: process.env.NEXT_PUBLIC_SOLANA_ENDPOINT, 16 | }, 17 | eslint: { 18 | ignoreDuringBuilds: true, 19 | }, 20 | typescript: { 21 | ignoreBuildErrors: true, 22 | }, 23 | experimental: { 24 | outputStandalone: true, 25 | }, 26 | } 27 | -------------------------------------------------------------------------------- /k8s/dev/argocd/argo-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | annotations: 5 | notifications.argoproj.io/subscribe.on-deployed.slack: cicd-monitoring 6 | notifications.argoproj.io/subscribe.on-health-degraded.slack: cicd-monitoring 7 | notifications.argoproj.io/subscribe.on-sync-failed.slack: cicd-monitoring 8 | notifications.argoproj.io/subscribe.on-sync-status-unknown.slack: cicd-monitoring 9 | notifications.argoproj.io/subscribe.on-sync-succeeded.slack: cicd-monitoring 10 | name: stage-marketplace 11 | namespace: argocd 12 | spec: 13 | project: stage-marketplace 14 | source: 15 | repoURL: https://github.com/holaplex/marketplace.git 16 | targetRevision: dev 17 | path: k8s/dev/app 18 | destination: 19 | server: https://kubernetes.default.svc 20 | namespace: stage-marketplace 21 | -------------------------------------------------------------------------------- /src/modules/ipfs/client.ts: -------------------------------------------------------------------------------- 1 | interface FileUploadResponse { 2 | name: string 3 | type: string 4 | uri: string 5 | error?: string 6 | } 7 | 8 | interface IpfsSender { 9 | uploadFile: (file: File) => Promise 10 | } 11 | 12 | const ipfsSDK = { 13 | uploadFile: async (file) => { 14 | const body = new FormData() 15 | body.append(file.name, file, file.name) 16 | try { 17 | const resp = await fetch('https://market.holaplex.com/api/ipfs/upload', { 18 | method: 'POST', 19 | body, 20 | }) 21 | const json = await resp.json() 22 | if (json) { 23 | return json.files[0] as FileUploadResponse 24 | } 25 | } catch (e: any) { 26 | console.error('Could not upload file', e) 27 | throw new Error(e) 28 | } 29 | }, 30 | } as IpfsSender 31 | 32 | export default ipfsSDK 33 | -------------------------------------------------------------------------------- /k8s/canary/argocd/argo-app.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | annotations: 5 | notifications.argoproj.io/subscribe.on-deployed.slack: cicd-monitoring 6 | notifications.argoproj.io/subscribe.on-health-degraded.slack: cicd-monitoring 7 | notifications.argoproj.io/subscribe.on-sync-failed.slack: cicd-monitoring 8 | notifications.argoproj.io/subscribe.on-sync-status-unknown.slack: cicd-monitoring 9 | notifications.argoproj.io/subscribe.on-sync-succeeded.slack: cicd-monitoring 10 | name: canary-marketplace 11 | namespace: argocd 12 | spec: 13 | project: canary-marketplace 14 | source: 15 | repoURL: https://github.com/holaplex/marketplace.git 16 | targetRevision: dev 17 | path: k8s/dev/app 18 | destination: 19 | server: https://kubernetes.default.svc 20 | namespace: canary-marketplace 21 | -------------------------------------------------------------------------------- /src/hooks/tokenList/tokenlist.ts: -------------------------------------------------------------------------------- 1 | import { ENV, TokenInfo, TokenListProvider } from '@solana/spl-token-registry' 2 | import { useEffect, useState } from 'react' 3 | 4 | type TokenMap = Map 5 | 6 | export const useTokenList = (): [TokenMap, boolean] => { 7 | const [tokenMap, setTokenMap] = useState(new Map()) 8 | const [loading, setLoading] = useState(true) 9 | 10 | useEffect(() => { 11 | setLoading(true) 12 | 13 | new TokenListProvider().resolve().then((tokens) => { 14 | const tokenList = tokens.filterByChainId(ENV.MainnetBeta).getList() 15 | 16 | setTokenMap( 17 | tokenList.reduce((map, item) => { 18 | map.set(item.address, item) 19 | return map 20 | }, new Map()) 21 | ) 22 | setLoading(false) 23 | }) 24 | }, [setTokenMap]) 25 | 26 | return [tokenMap, loading] 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Price/index.tsx: -------------------------------------------------------------------------------- 1 | import { isSol } from 'src/modules/sol' 2 | import cx from 'classnames' 3 | import { TokenInfo } from '@solana/spl-token-registry' 4 | import { MarketplaceClient } from '@holaplex/marketplace-js-sdk' 5 | 6 | interface PriceProps { 7 | price: number 8 | token?: TokenInfo 9 | style?: string 10 | } 11 | 12 | const Price = ({ price, token, style }: PriceProps) => ( 13 | <> 14 | {token ? ( 15 |
16 | 21 | {MarketplaceClient.price(price, token)} 22 | 23 | {!isSol(token.address) && ( 24 | {token.symbol} 25 | )} 26 |
27 | ) : ( 28 | {price} 29 | )} 30 | 31 | ) 32 | 33 | export default Price 34 | -------------------------------------------------------------------------------- /src/providers/Viewer/Viewer.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useConnection } from '@solana/wallet-adapter-react' 3 | import { useWallet } from '@solana/wallet-adapter-react' 4 | import { viewerVar } from '../../cache' 5 | 6 | export const ViewerProvider: React.FC = ({ children }) => { 7 | const { connection } = useConnection() 8 | const { publicKey } = useWallet() 9 | 10 | useEffect(() => { 11 | ;(async () => { 12 | if (!publicKey) { 13 | return 14 | } 15 | 16 | try { 17 | const balance = await connection.getBalance(publicKey) 18 | 19 | viewerVar({ 20 | balance, 21 | id: publicKey?.toBase58() as string, 22 | __typename: 'Viewer', 23 | }) 24 | } catch (e) { 25 | console.error(e) 26 | return null 27 | } 28 | })() 29 | }, [publicKey]) 30 | 31 | return <>{children} 32 | } 33 | 34 | export default ViewerProvider 35 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /src/hooks/sidebar/sidebar.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect } from 'react' 2 | import { useReactiveVar } from '@apollo/client' 3 | import { sidebarOpenVar } from '../../cache' 4 | 5 | interface SidebarContext { 6 | sidebarOpen: boolean 7 | toggleSidebar: () => void 8 | } 9 | 10 | export const useSidebar = (): SidebarContext => { 11 | const sidebarOpen = useReactiveVar(sidebarOpenVar) 12 | 13 | const toggleSidebar = useCallback(() => { 14 | const next = !!!sidebarOpen 15 | 16 | if (next) { 17 | document?.querySelector('body')?.classList.add('overflow-hidden') 18 | } else { 19 | document?.querySelector('body')?.classList.remove('overflow-hidden') 20 | } 21 | 22 | sidebarOpenVar(next) 23 | }, [sidebarOpen]) 24 | 25 | useEffect(() => { 26 | if (typeof window === 'undefined') { 27 | return 28 | } 29 | 30 | const resize = () => { 31 | if (window.innerWidth > 640) { 32 | document?.querySelector('body')?.classList.remove('overflow-hidden') 33 | sidebarOpenVar(false) 34 | } 35 | } 36 | 37 | window.addEventListener('resize', resize) 38 | ;() => window.removeEventListener('resize', resize) 39 | }, []) 40 | 41 | return { sidebarOpen, toggleSidebar } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/errorCodes.ts: -------------------------------------------------------------------------------- 1 | enum MarketplaceErrorCodes { 2 | PublicKeyMismatch, 3 | InvalidMintAuthority, 4 | UninitializedAccount, 5 | IncorrectOwner, 6 | PublicKeyShouldBeUnique, 7 | StatementFalse, 8 | NotRentExempt, 9 | NumericalOverflow, 10 | ExpectedSolAccount, 11 | CannotExchangeSOLForSol, 12 | SOLWalletMustSign, 13 | CannotTakeThisActionWithoutAuctionHouseSignOff, 14 | NoPayerPresent, 15 | DerivedKeyInvalid, 16 | MetadataDoesntExist, 17 | InvalidTokenAmount, 18 | BothPartiesNeedToAgreeToSale, 19 | CannotMatchFreeSalesWithoutAuctionHouseOrSellerSignoff, 20 | SaleRequiresSigner, 21 | OldSellerNotInitialized, 22 | SellerATACannotHaveDelegate, 23 | BuyerATACannotHaveDelegate, 24 | NoValidSignerPresent, 25 | InvalidBasisPoints, 26 | } 27 | 28 | export const errorCodeHelper = (err: string) => { 29 | const errorCode = err.match(/0x[0-9A-F]+/)?.toString() || '' 30 | const decimal = parseInt(errorCode, 16) 31 | const errorVal = Number(decimal.toString().slice(2, 3)) 32 | const errorMsg = `There was an error whilst performing an action that resulted in an error code of ${errorCode}: (${MarketplaceErrorCodes[errorVal]}). Full message: ${err} ` 33 | return { 34 | message: errorCode ? errorMsg : err, 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/**/**/*.{ts,tsx,js,jsx}'], 3 | theme: { 4 | colors: { 5 | black: '#000000', 6 | white: '#ffffff', 7 | green: { 8 | 500: 'var(--green-500)', 9 | }, 10 | gray: { 11 | 100: 'var(--gray-100)', 12 | 300: 'var(--gray-300)', 13 | 500: 'var(--gray-500)', 14 | 700: 'var(--gray-700)', 15 | 800: 'var(--gray-800)', 16 | 900: 'var(--gray-900)', 17 | }, 18 | error: { 19 | 300: 'var(--error-300)', 20 | 500: 'var(--error-500)', 21 | 800: 'var(--error-800)', 22 | 900: 'var(--error-900)', 23 | }, 24 | success: { 25 | 300: 'var(--success-300)', 26 | 500: 'var(--success-500)', 27 | 800: 'var(--success-800)', 28 | 900: 'var(--success-900)', 29 | }, 30 | transparent: 'transparent', 31 | }, 32 | fontFamily: { 33 | sans: ['Inter', 'sans-serif'], 34 | serif: ['Merriweather', 'serif'], 35 | }, 36 | extend: { 37 | boxShadow: { 38 | card: '0px 12px 16px rgba(0, 0, 0, 0.3)', 39 | }, 40 | spacing: { 41 | '8xl': '96rem', 42 | '9xl': '128rem', 43 | }, 44 | borderRadius: { 45 | '4xl': '2rem', 46 | }, 47 | }, 48 | }, 49 | variants: {}, 50 | plugins: [], 51 | darkMode: 'class', 52 | } 53 | -------------------------------------------------------------------------------- /src/layouts/Admin/AdminLayout.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link' 2 | import { ReactElement } from 'react' 3 | import WalletPortal from '../../components/WalletPortal' 4 | import { Marketplace } from '@holaplex/marketplace-js-sdk' 5 | 6 | interface AdminLayoutProps { 7 | marketplace: Marketplace 8 | children: ReactElement 9 | } 10 | 11 | export const AdminLayout = ({ marketplace, children }: AdminLayoutProps) => { 12 | return ( 13 |
14 |
15 | 16 | 23 | 24 |
25 |
26 | Admin Dashboard 27 |
28 | 29 |
30 |
31 | {children} 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/components/SplToken/index.tsx: -------------------------------------------------------------------------------- 1 | import { TokenInfo } from '@solana/spl-token-registry' 2 | import React from 'react' 3 | import { truncateAddress } from './../../modules/address' 4 | 5 | export const SplToken = ({ 6 | tokenInfo, 7 | mintAddress, 8 | loading, 9 | }: { 10 | tokenInfo: TokenInfo | undefined 11 | mintAddress: string 12 | loading?: boolean 13 | }) => { 14 | if (loading) { 15 | return 16 | } 17 | 18 | if (!tokenInfo) { 19 | return ( 20 | 21 | {truncateAddress(mintAddress)} 22 | 23 | ) 24 | } 25 | 26 | return ( 27 |
28 | 29 |
30 | {tokenInfo.name} 31 | {tokenInfo.symbol} 32 |
33 |
34 | ) 35 | } 36 | 37 | SplToken.Skeleton = function SplTokenSkeleton(): JSX.Element { 38 | return ( 39 |
40 |
41 |
42 | 43 | 44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /k8s/prod/argocd/argo-proj.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: AppProject 3 | metadata: 4 | name: prod-marketplace 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | description: Marketplace App 10 | # Allow manifests to deploy from any Git repos 11 | sourceRepos: 12 | - '*' 13 | # Only permit applications to deploy to the marketplace namespace in the same cluster 14 | destinations: 15 | - namespace: prod-marketplace 16 | server: https://kubernetes.default.svc 17 | # Deny all cluster-scoped resources from being created, except for Namespace 18 | clusterResourceWhitelist: 19 | - group: '' 20 | kind: Namespace 21 | # Allow all namespaced-scoped resources to be created, except for ResourceQuota, LimitRange, NetworkPolicy 22 | namespaceResourceBlacklist: 23 | - group: '' 24 | kind: ResourceQuota 25 | - group: '' 26 | kind: LimitRange 27 | - group: '' 28 | kind: NetworkPolicy 29 | namespaceResourceWhitelist: 30 | - group: '' 31 | kind: ConfigMap 32 | - group: '' 33 | kind: Service 34 | - group: 'apps' 35 | kind: Deployment 36 | - group: 'apps' 37 | kind: StatefulSet 38 | roles: 39 | # A role which provides read-only access to all applications in the project 40 | - name: read-only 41 | description: Read-only privileges to prod-marketplace 42 | policies: 43 | - p, proj:prod-marketplace:read-only, applications, get, prod-marketplace/*, allow 44 | groups: 45 | - my-oidc-group 46 | -------------------------------------------------------------------------------- /k8s/dev/argocd/argo-proj.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: AppProject 3 | metadata: 4 | name: stage-marketplace 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | description: Marketplace App 10 | # Allow manifests to deploy from any Git repos 11 | sourceRepos: 12 | - '*' 13 | # Only permit applications to deploy to the marketplace namespace in the same cluster 14 | destinations: 15 | - namespace: stage-marketplace 16 | server: https://kubernetes.default.svc 17 | # Deny all cluster-scoped resources from being created, except for Namespace 18 | clusterResourceWhitelist: 19 | - group: '' 20 | kind: Namespace 21 | # Allow all namespaced-scoped resources to be created, except for ResourceQuota, LimitRange, NetworkPolicy 22 | namespaceResourceBlacklist: 23 | - group: '' 24 | kind: ResourceQuota 25 | - group: '' 26 | kind: LimitRange 27 | - group: '' 28 | kind: NetworkPolicy 29 | namespaceResourceWhitelist: 30 | - group: '' 31 | kind: ConfigMap 32 | - group: '' 33 | kind: Service 34 | - group: 'apps' 35 | kind: Deployment 36 | - group: 'apps' 37 | kind: StatefulSet 38 | - group: networking.k8s.io 39 | kind: Ingress 40 | roles: 41 | # A role which provides read-only access to all applications in the project 42 | - name: read-only 43 | description: Read-only privileges to stage-marketplace 44 | policies: 45 | - p, proj:stage-marketplace:read-only, applications, get, stage-marketplace/*, allow 46 | groups: 47 | - my-oidc-group 48 | -------------------------------------------------------------------------------- /k8s/canary/argocd/argo-proj.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: AppProject 3 | metadata: 4 | name: canary-marketplace 5 | namespace: argocd 6 | finalizers: 7 | - resources-finalizer.argocd.argoproj.io 8 | spec: 9 | description: Marketplace App 10 | # Allow manifests to deploy from any Git repos 11 | sourceRepos: 12 | - '*' 13 | # Only permit applications to deploy to the marketplace namespace in the same cluster 14 | destinations: 15 | - namespace: canary-marketplace 16 | server: https://kubernetes.default.svc 17 | # Deny all cluster-scoped resources from being created, except for Namespace 18 | clusterResourceWhitelist: 19 | - group: '' 20 | kind: Namespace 21 | # Allow all namespaced-scoped resources to be created, except for ResourceQuota, LimitRange, NetworkPolicy 22 | namespaceResourceBlacklist: 23 | - group: '' 24 | kind: ResourceQuota 25 | - group: '' 26 | kind: LimitRange 27 | - group: '' 28 | kind: NetworkPolicy 29 | namespaceResourceWhitelist: 30 | - group: '' 31 | kind: ConfigMap 32 | - group: '' 33 | kind: Service 34 | - group: 'apps' 35 | kind: Deployment 36 | - group: 'apps' 37 | kind: StatefulSet 38 | - group: networking.k8s.io 39 | kind: Ingress 40 | roles: 41 | # A role which provides read-only access to all applications in the project 42 | - name: read-only 43 | description: Read-only privileges to canary-marketplace 44 | policies: 45 | - p, proj:canary-marketplace:read-only, applications, get, canary-marketplace/*, allow 46 | groups: 47 | - my-oidc-group 48 | -------------------------------------------------------------------------------- /src/components/NftPreview/index.tsx: -------------------------------------------------------------------------------- 1 | import { Nft } from '@holaplex/marketplace-js-sdk' 2 | import { PublicKey } from '@solana/web3.js' 3 | import { always, isNil, when } from 'ramda' 4 | import { addressAvatar } from 'src/modules/address' 5 | 6 | interface NftPreviewProps { 7 | nft: Nft 8 | } 9 | 10 | export const NftPreview = ({ nft }: NftPreviewProps) => { 11 | return ( 12 | <> 13 |
14 | {nft?.image && ( 15 | nft-mini-image 20 | )} 21 |
22 |
23 | {nft.name} 24 |
25 | {nft.creators.map((creator) => ( 26 |
27 | 32 | 41 | 42 |
43 | ))} 44 |
45 |
46 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/components/DialectNotificationsButton/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | defaultVariables, 3 | IncomingThemeVariables, 4 | NotificationsButton, 5 | } from '@dialectlabs/react-ui' 6 | import { useWallet } from '@solana/wallet-adapter-react' 7 | import { PublicKey } from '@solana/web3.js' 8 | import Bell from './BellIcon' 9 | import Settings from './SettingsIcon' 10 | import Close from './CloseIcon' 11 | 12 | const HOLAPLEX_MONITORING_PUBLIC_KEY = new PublicKey( 13 | 'BpVYWaAPbv5vyeRxiX9PMsmAVJVoL2Lp4YtuRgjuhoZh' 14 | ) 15 | 16 | export const themeVariables: IncomingThemeVariables = { 17 | dark: { 18 | colors: { 19 | bg: 'bg-gray-800', 20 | }, 21 | bellButton: 22 | 'w-[36px] h-[36px] bg-gray-800 rounded-full hover:bg-gray-600 transition-transform hover:scale-[1.02]', 23 | modal: `sm:rounded-md shadow-xl shadow-neutral-900 pt-1`, 24 | icons: { 25 | bell: Bell, 26 | settings: Settings, 27 | x: Close, 28 | }, 29 | divider: `${defaultVariables.dark.divider} h-px opacity-10 mx-0`, 30 | notificationMessage: `${defaultVariables.dark.notificationMessage} bg-transparent`, 31 | notificationTimestamp: `${defaultVariables.dark.notificationTimestamp} text-left`, 32 | notificationsDivider: '', // Empty line is intentional to ovveride dt-hidden 33 | }, 34 | } 35 | 36 | export default function DialectNotificationsButton() { 37 | const wallet = useWallet() 38 | return ( 39 | 47 | ) 48 | } 49 | -------------------------------------------------------------------------------- /src/components/UploadFile/index.tsx: -------------------------------------------------------------------------------- 1 | import ipfsSDK from './../../modules/ipfs/client' 2 | import { useEffect, useState } from 'react' 3 | import { useForm, InternalFieldName } from 'react-hook-form' 4 | import { TailSpin } from 'react-loader-spinner' 5 | 6 | interface UploadFileProps { 7 | onChange: (...event: any) => void 8 | name: InternalFieldName 9 | } 10 | 11 | const UploadFile = ({ onChange, name }: UploadFileProps) => { 12 | const { watch, register } = useForm() 13 | const [uploading, setUploading] = useState(false) 14 | useEffect(() => { 15 | const subscription = watch(async ({ upload }) => { 16 | const file = upload[0] 17 | 18 | if (!file) { 19 | return 20 | } 21 | 22 | setUploading(true) 23 | 24 | const uploaded = await ipfsSDK.uploadFile(file) 25 | 26 | onChange(uploaded) 27 | 28 | setUploading(false) 29 | }) 30 | 31 | return () => subscription.unsubscribe() 32 | }, [watch, onChange]) 33 | 34 | return ( 35 |
36 |
37 | 44 | 56 |
57 |
58 | ) 59 | } 60 | 61 | export default UploadFile 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Install dependencies only when needed 2 | FROM node:16-alpine AS deps 3 | RUN apk add --no-cache libc6-compat 4 | WORKDIR /app 5 | COPY package.json yarn.lock ./ 6 | RUN yarn install --frozen-lockfile 7 | 8 | # Rebuild the source code only when needed 9 | FROM node:16-alpine AS builder 10 | WORKDIR /app 11 | COPY --from=deps /app/node_modules ./node_modules 12 | COPY . . 13 | 14 | #GraphQL 15 | ARG GRAPHQL_URI 16 | ENV NEXT_PUBLIC_GRAPHQL_ENDPOINT $GRAPHQL_URI 17 | 18 | #Env 19 | ARG NEXT_ENVIRONMENT 20 | ENV NEXT_PUBLIC_ENVIRONMENT $NEXT_ENVIRONMENT 21 | ENV NODE_ENV $NEXT_ENVIRONMENT 22 | #Solana 23 | ARG SOLANA_ENDPOINT 24 | ENV SOLANA_ENDPOINT $SOLANA_ENDPOINT 25 | ENV NEXT_PUBLIC_SOLANA_ENDPOINT $SOLANA_ENDPOINT 26 | 27 | RUN yarn build 28 | 29 | 30 | # Production image, copy all the files and run next 31 | FROM node:16-alpine AS runner 32 | WORKDIR /app 33 | 34 | ENV NODE_ENV production 35 | 36 | # Uncomment the following line in case you want to disable telemetry during runtime. 37 | ENV NEXT_TELEMETRY_DISABLED 1 38 | RUN yarn add next@12.0.7 39 | RUN addgroup --system --gid 1001 nodejs 40 | RUN adduser --system --uid 1001 nextjs 41 | 42 | # You only need to copy next.config.js if you are NOT using the default configuration 43 | COPY --from=builder /app/next.config.js ./ 44 | COPY --from=builder /app/public ./public 45 | COPY --from=builder /app/package.json ./package.json 46 | 47 | # Automatically leverage output traces to reduce image size 48 | # https://nextjs.org/docs/advanced-features/output-file-tracing 49 | COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ 50 | COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static 51 | 52 | USER nextjs 53 | 54 | EXPOSE 3000 55 | 56 | ENV PORT 3000 57 | 58 | CMD ["yarn", "start"] 59 | -------------------------------------------------------------------------------- /src/components/AdminMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import { DollarSign, User, Image as ImageIcon, Circle } from 'react-feather' 2 | 3 | import cx from 'classnames' 4 | import Link from 'next/link' 5 | 6 | export enum AdminMenuItemType { 7 | Marketplace, 8 | Creators, 9 | Financials, 10 | Tokens, 11 | } 12 | 13 | export interface AdminMenuItem { 14 | type: AdminMenuItemType 15 | name: string 16 | url: string 17 | icon: JSX.Element 18 | } 19 | 20 | export const ADMIN_MENU_ITEMS: AdminMenuItem[] = [ 21 | { 22 | type: AdminMenuItemType.Marketplace, 23 | name: 'Marketplace', 24 | url: '/admin/marketplace/edit', 25 | icon: , 26 | }, 27 | { 28 | type: AdminMenuItemType.Creators, 29 | name: 'Creators', 30 | url: '/admin/creators/edit', 31 | icon: , 32 | }, 33 | { 34 | type: AdminMenuItemType.Financials, 35 | name: 'Financials', 36 | url: '/admin/financials/edit', 37 | icon: , 38 | }, 39 | { 40 | type: AdminMenuItemType.Tokens, 41 | name: 'Supported Tokens', 42 | url: '/admin/tokens/edit', 43 | icon: , 44 | }, 45 | ] 46 | 47 | interface Props { 48 | selectedItem: AdminMenuItemType 49 | } 50 | 51 | const AdminMenu = ({ selectedItem }: Props) => ( 52 |
53 | 70 |
71 | ) 72 | 73 | export default AdminMenu 74 | -------------------------------------------------------------------------------- /src/components/EmptyTreasuryWalletForm/EmptyTreasuryWalletForm.tsx: -------------------------------------------------------------------------------- 1 | import { useMemo, useState } from 'react' 2 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 3 | import { toast } from 'react-toastify' 4 | import Button, { ButtonSize, ButtonType } from './../Button' 5 | import { initMarketplaceSDK, AuctionHouse } from '@holaplex/marketplace-js-sdk' 6 | import { useLogin } from './../../hooks/login' 7 | import { Connection, Wallet } from '@metaplex/js' 8 | import { useTokenList } from './../../hooks/tokenList' 9 | import { TokenInfo } from '@solana/spl-token-registry' 10 | 11 | interface EmptyTreasuryWalletFormProps { 12 | onEmpty: () => Promise 13 | token?: TokenInfo 14 | } 15 | 16 | export const EmptyTreasuryWalletForm = ({ 17 | onEmpty, 18 | token, 19 | }: EmptyTreasuryWalletFormProps) => { 20 | const wallet = useWallet() 21 | const { publicKey, signTransaction } = wallet 22 | const { connection } = useConnection() 23 | const sdk = useMemo( 24 | () => initMarketplaceSDK(connection, wallet as Wallet), 25 | [connection, wallet] 26 | ) 27 | const login = useLogin() 28 | const [withdrawlLoading, setWithdrawlLoading] = useState(false) 29 | 30 | const claimFunds = async () => { 31 | if (!publicKey || !signTransaction || !wallet) { 32 | login() 33 | return 34 | } 35 | 36 | try { 37 | toast.info('Please approve the transaction.') 38 | setWithdrawlLoading(true) 39 | await onEmpty() 40 | toast.success('The transaction was confirmed.') 41 | } catch (e: any) { 42 | toast.error(e.message) 43 | } finally { 44 | setWithdrawlLoading(false) 45 | } 46 | } 47 | 48 | return ( 49 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/components/List/List.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { KeyType } from '@holaplex/marketplace-js-sdk' 3 | import { isEmpty, range, map } from 'ramda' 4 | import { InView } from 'react-intersection-observer' 5 | 6 | interface ListProps { 7 | itemRender: (item: D, index: number) => React.ReactNode 8 | data: D[] | undefined 9 | dataKey?: string 10 | loading: boolean 11 | emptyComponent: React.ReactNode 12 | loadingComponent: React.ReactNode 13 | loadingCount?: number 14 | gridClassName?: string 15 | hasMore: boolean 16 | onLoadMore: ( 17 | inView: boolean, 18 | entry: IntersectionObserverEntry 19 | ) => Promise 20 | } 21 | 22 | export function List({ 23 | itemRender, 24 | data, 25 | loading, 26 | emptyComponent, 27 | loadingComponent, 28 | hasMore, 29 | onLoadMore, 30 | dataKey = 'address', 31 | loadingCount = 12, 32 | gridClassName = 'grid grid-cols-1 gap-8 md:grid-cols-2 md:gap-8 lg:grid-cols-3 lg:gap-8 xl:grid-cols-4 xl:gap-8', 33 | }: ListProps) { 34 | return ( 35 |
36 | {loading ? ( 37 |
    38 | {map((i: number) =>
  • {loadingComponent}
  • )( 39 | range(0, loadingCount) 40 | )} 41 |
42 | ) : isEmpty(data) ? ( 43 | emptyComponent 44 | ) : ( 45 |
    46 | {(data || []).map((d, i) => { 47 | return
  • {itemRender(d, i)}
  • 48 | })} 49 | {hasMore && ( 50 | <> 51 |
  • 52 | 53 | {loadingComponent} 54 | 55 |
  • 56 |
  • {loadingComponent}
  • 57 |
  • {loadingComponent}
  • 58 |
  • {loadingComponent}
  • 59 | 60 | )} 61 |
62 | )} 63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /k8s/prod/app/marketplace-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | name: app-marketplace 6 | spec: 7 | replicas: 5 8 | strategy: 9 | type: RollingUpdate 10 | rollingUpdate: 11 | maxSurge: 1 12 | maxUnavailable: 0 13 | selector: 14 | matchLabels: 15 | app: app-marketplace 16 | template: 17 | metadata: 18 | labels: 19 | app: app-marketplace 20 | spec: 21 | containers: 22 | - name: marketplace 23 | image: 011737333588.dkr.ecr.us-east-1.amazonaws.com/marketplace:main-ed8852c 24 | envFrom: 25 | - configMapRef: 26 | name: marketplace-config 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - containerPort: 3000 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: app-marketplace 35 | spec: 36 | selector: 37 | app: app-marketplace 38 | ports: 39 | - port: 3000 40 | targetPort: 3000 41 | protocol: TCP 42 | --- 43 | apiVersion: networking.k8s.io/v1 44 | kind: Ingress 45 | metadata: 46 | name: marketplace 47 | annotations: 48 | nginx.ingress.kubernetes.io/server-snippet: | 49 | server_name ~^(?\w+)\.${HOSTNAME}$; 50 | nginx.ingress.kubernetes.io/configuration-snippet: | 51 | if ($subdomain = '') { 52 | set $subdomain haus; 53 | } 54 | proxy_set_header X-Holaplex-Subdomain $subdomain; 55 | spec: 56 | ingressClassName: nginx 57 | rules: 58 | - host: holaplex.market 59 | http: 60 | paths: 61 | - path: / 62 | pathType: Prefix 63 | backend: 64 | service: 65 | name: app-marketplace 66 | port: 67 | number: 3000 68 | - host: "*.holaplex.market" 69 | http: 70 | paths: 71 | - path: / 72 | pathType: Prefix 73 | backend: 74 | service: 75 | name: app-marketplace 76 | port: 77 | number: 3000 78 | tls: 79 | - hosts: 80 | - "*.holaplex.market" 81 | - holaplex.market 82 | -------------------------------------------------------------------------------- /k8s/dev/app/marketplace-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | name: app-marketplace 6 | spec: 7 | revisionHistoryLimit: 3 8 | replicas: 2 9 | strategy: 10 | type: RollingUpdate 11 | rollingUpdate: 12 | maxSurge: 1 13 | maxUnavailable: 0 14 | selector: 15 | matchLabels: 16 | app: app-marketplace 17 | template: 18 | metadata: 19 | labels: 20 | app: app-marketplace 21 | spec: 22 | containers: 23 | - name: marketplace 24 | image: 011737333588.dkr.ecr.us-east-1.amazonaws.com/marketplace:dev-5998ce1 25 | envFrom: 26 | - configMapRef: 27 | name: marketplace-config 28 | imagePullPolicy: IfNotPresent 29 | ports: 30 | - containerPort: 3000 31 | --- 32 | apiVersion: v1 33 | kind: Service 34 | metadata: 35 | name: app-marketplace 36 | spec: 37 | selector: 38 | app: app-marketplace 39 | ports: 40 | - port: 3000 41 | targetPort: 3000 42 | protocol: TCP 43 | --- 44 | apiVersion: networking.k8s.io/v1 45 | kind: Ingress 46 | metadata: 47 | name: marketplace 48 | annotations: 49 | nginx.ingress.kubernetes.io/server-snippet: | 50 | server_name ~^(?\w+)\.${HOSTNAME}$; 51 | nginx.ingress.kubernetes.io/configuration-snippet: | 52 | if ($subdomain = '') { 53 | set $subdomain haus; 54 | } 55 | proxy_set_header X-Holaplex-Subdomain $subdomain; 56 | spec: 57 | ingressClassName: nginx 58 | rules: 59 | - host: dev.holaplex.market 60 | http: 61 | paths: 62 | - path: / 63 | pathType: Prefix 64 | backend: 65 | service: 66 | name: app-marketplace 67 | port: 68 | number: 3000 69 | - host: "*.dev.holaplex.market" 70 | http: 71 | paths: 72 | - path: / 73 | pathType: Prefix 74 | backend: 75 | service: 76 | name: app-marketplace 77 | port: 78 | number: 3000 79 | tls: 80 | - hosts: 81 | - "*.dev.holaplex.market" 82 | - dev.holaplex.market 83 | -------------------------------------------------------------------------------- /k8s/canary/app/marketplace-deploy.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | annotations: 5 | name: app-marketplace 6 | spec: 7 | replicas: 3 8 | strategy: 9 | type: RollingUpdate 10 | rollingUpdate: 11 | maxSurge: 1 12 | maxUnavailable: 0 13 | selector: 14 | matchLabels: 15 | app: app-marketplace 16 | template: 17 | metadata: 18 | labels: 19 | app: app-marketplace 20 | spec: 21 | containers: 22 | - name: marketplace 23 | image: 011737333588.dkr.ecr.us-east-1.amazonaws.com/marketplace:canary-75c328e 24 | env: 25 | - name: NEXT_PUBLIC_GRAPHQL_ENDPOINT 26 | value: https://graph-test.holaplex.tools/v1 27 | imagePullPolicy: IfNotPresent 28 | ports: 29 | - containerPort: 3000 30 | --- 31 | apiVersion: v1 32 | kind: Service 33 | metadata: 34 | name: app-marketplace 35 | spec: 36 | selector: 37 | app: app-marketplace 38 | ports: 39 | - port: 3000 40 | targetPort: 3000 41 | protocol: TCP 42 | --- 43 | apiVersion: networking.k8s.io/v1 44 | kind: Ingress 45 | metadata: 46 | name: marketplace 47 | annotations: 48 | nginx.ingress.kubernetes.io/server-snippet: | 49 | server_name ~^(?\w+)\.${HOSTNAME}$; 50 | nginx.ingress.kubernetes.io/configuration-snippet: | 51 | if ($subdomain = '') { 52 | set $subdomain haus; 53 | } 54 | proxy_set_header X-Holaplex-Subdomain $subdomain; 55 | spec: 56 | ingressClassName: nginx 57 | rules: 58 | - host: canary.holaplex.market 59 | http: 60 | paths: 61 | - path: / 62 | pathType: Prefix 63 | backend: 64 | service: 65 | name: app-marketplace 66 | port: 67 | number: 3000 68 | - host: "*.canary.holaplex.market" 69 | http: 70 | paths: 71 | - path: / 72 | pathType: Prefix 73 | backend: 74 | service: 75 | name: app-marketplace 76 | port: 77 | number: 3000 78 | tls: 79 | - hosts: 80 | - "*.canary.holaplex.market" 81 | - canary.holaplex.market 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solana NFT Marketplace 2 | 3 | No-code Solana NFT marketplaces powered by Holaplex. Feel free to reach out of me when you face difficulty or if you need to build marketplace[Whatspp: https://wa.me/13137423660, Telegram: https://t.me/DevCutup]. 4 | 5 | Tech Stack: 6 | 7 | - Typescript 8 | - Apollo GraphQL 9 | - Next.JS/React 10 | 11 | ## Getting Started 12 | 13 | 0. Create a Marketplace at https://holaplex.com/marketplace/new 14 | 1. Clone the repo 15 | 2. `cd` into the folder 16 | 3. Install dependencies with `yarn install` 17 | 4. Edit `.env.development` and add the subdomain you chose in step 0, or any other active subdomain, eg: `espi` 18 | 5. Run it with `yarn dev` 19 | 20 | ## Create a Production Build 21 | 22 | To create a production ready build use `yarn build`. Build files can be found in `.next`. 23 | 24 | Detailed documentation can be found at https://nextjs.org/docs/deployment#nextjs-build-api 25 | 26 | ## Proxy 27 | 28 | The active marketplace can be set by the `x-holaplex-subdomain` request header. Starting the `nginx` with [default.conf](/main/templates/default.conf.template) will set the subdomain header based on the current hostname context of the request. 29 | The nginx conf relies on envstub provided by the official [nginx image](https://hub.docker.com/_/nginx) on Dockerhub. 30 | 31 | To start nginx using the required configuration, use the following command: 32 | 33 | ```bash 34 | $ docker run --network=host -v $(pwd)/templates:/etc/nginx/templates \ 35 | -e HOSTNAME=dev.holaplex.market.127.0.0.1.nip.io -e PORT=80 -e WEB_PORT=3000 \ 36 | -e PROXY_HOST=127.0.0.1.nip.io nginx:latest 37 | ``` 38 | 39 | If you already have something running in port `80` already, feel free to change that to a different port. Keep in mind that you'll need to append `:` on the url to access the NGINX server. 40 | 41 | ## Test your proxy setup! 42 | 43 | Open a test marketplace, like [espi's marketplace](http://espi.dev.holaplex.market.127.0.0.1.nip.io). 44 | Page should load without issues. 45 | 46 | 47 | ### Contact Information 48 | 49 | - Telegram: https://t.me/DevCutup 50 | - Whatsapp: https://wa.me/13137423660 51 | - Twitter: https://x.com/devcutup 52 | -------------------------------------------------------------------------------- /src/components/Chart/index.tsx: -------------------------------------------------------------------------------- 1 | import { format, parseISO } from 'date-fns' 2 | import { Area, AreaChart, ResponsiveContainer, XAxis, YAxis } from 'recharts' 3 | import { toSOL } from '../../modules/sol' 4 | import { PricePoint } from '@holaplex/marketplace-js-sdk' 5 | import BN from 'bn.js' 6 | 7 | interface ChartProps { 8 | height?: number 9 | showXAxis?: boolean 10 | className?: string 11 | chartData?: PricePoint[] 12 | } 13 | 14 | const Chart = ({ 15 | height = 200, 16 | showXAxis = true, 17 | className = '', 18 | chartData, 19 | }: ChartProps) => { 20 | const actualData: any[] | undefined = [] 21 | chartData && 22 | chartData.forEach((cd) => { 23 | const price = new BN(cd.price) 24 | actualData.push({ 25 | date: cd.date.substr(0, 10), 26 | price: toSOL(price.toNumber()), // 1 + Math.random() 27 | }) 28 | }) 29 | 30 | if (actualData.length == 0) return null 31 | return ( 32 | 33 | 43 | 51 | { 57 | if (index > 0) { 58 | return `${number.toFixed(1)}` 59 | } 60 | return '' 61 | }} 62 | /> 63 | { 69 | if (showXAxis) { 70 | const date = parseISO(str) 71 | return format(date, 'M/d') 72 | } 73 | return '' 74 | }} 75 | /> 76 | 77 | 78 | ) 79 | } 80 | 81 | export default Chart 82 | -------------------------------------------------------------------------------- /src/modules/auction-house/UpdateAuctionHouse.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PublicKey, 3 | PublicKeyInitData, 4 | TransactionInstruction, 5 | } from '@solana/web3.js' 6 | import { NATIVE_MINT } from '@solana/spl-token' 7 | import { Wallet } from '@metaplex/js' 8 | import { AuctionHouseProgram } from '@holaplex/mpl-auction-house' 9 | 10 | const { createUpdateAuctionHouseInstruction } = AuctionHouseProgram.instructions 11 | 12 | interface UpdateAuctionHouseParams { 13 | wallet: Wallet 14 | sellerFeeBasisPoints: number 15 | canChangeSalePrice?: boolean 16 | requiresSignOff?: boolean 17 | treasuryWithdrawalDestination?: PublicKeyInitData 18 | feeWithdrawalDestination?: PublicKeyInitData 19 | treasuryMint?: PublicKeyInitData 20 | } 21 | 22 | export const updateAuctionHouse = async ( 23 | params: UpdateAuctionHouseParams 24 | ): Promise => { 25 | const { 26 | wallet, 27 | sellerFeeBasisPoints, 28 | canChangeSalePrice = false, 29 | requiresSignOff = false, 30 | treasuryWithdrawalDestination, 31 | feeWithdrawalDestination, 32 | treasuryMint, 33 | } = params 34 | 35 | const twdKey = treasuryWithdrawalDestination 36 | ? new PublicKey(treasuryWithdrawalDestination) 37 | : wallet.publicKey 38 | 39 | const fwdKey = feeWithdrawalDestination 40 | ? new PublicKey(feeWithdrawalDestination) 41 | : wallet.publicKey 42 | 43 | const tMintKey = treasuryMint ? new PublicKey(treasuryMint) : NATIVE_MINT 44 | 45 | const twdAta = tMintKey.equals(NATIVE_MINT) 46 | ? twdKey 47 | : ( 48 | await AuctionHouseProgram.findAssociatedTokenAccountAddress( 49 | tMintKey, 50 | twdKey 51 | ) 52 | )[0] 53 | 54 | const [auctionHouse] = await AuctionHouseProgram.findAuctionHouseAddress( 55 | wallet.publicKey, 56 | tMintKey 57 | ) 58 | 59 | return createUpdateAuctionHouseInstruction( 60 | { 61 | treasuryMint: tMintKey, 62 | payer: wallet.publicKey, 63 | authority: wallet.publicKey, 64 | newAuthority: wallet.publicKey, 65 | feeWithdrawalDestination: fwdKey, 66 | treasuryWithdrawalDestination: twdAta, 67 | treasuryWithdrawalDestinationOwner: twdKey, 68 | auctionHouse, 69 | }, 70 | { 71 | sellerFeeBasisPoints, 72 | requiresSignOff, 73 | canChangeSalePrice, 74 | } 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/modules/auction-house/updateAuctionHouse.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PublicKey, 3 | PublicKeyInitData, 4 | TransactionInstruction, 5 | } from '@solana/web3.js' 6 | import { NATIVE_MINT } from '@solana/spl-token' 7 | import { Wallet } from '@metaplex/js' 8 | import { AuctionHouseProgram } from '@holaplex/mpl-auction-house' 9 | 10 | const { createUpdateAuctionHouseInstruction } = AuctionHouseProgram.instructions 11 | 12 | interface UpdateAuctionHouseParams { 13 | wallet: Wallet 14 | sellerFeeBasisPoints: number 15 | canChangeSalePrice?: boolean 16 | requiresSignOff?: boolean 17 | treasuryWithdrawalDestination?: PublicKeyInitData 18 | feeWithdrawalDestination?: PublicKeyInitData 19 | treasuryMint?: PublicKeyInitData 20 | } 21 | 22 | export const updateAuctionHouse = async ( 23 | params: UpdateAuctionHouseParams 24 | ): Promise => { 25 | const { 26 | wallet, 27 | sellerFeeBasisPoints, 28 | canChangeSalePrice = false, 29 | requiresSignOff = false, 30 | treasuryWithdrawalDestination, 31 | feeWithdrawalDestination, 32 | treasuryMint, 33 | } = params 34 | 35 | const twdKey = treasuryWithdrawalDestination 36 | ? new PublicKey(treasuryWithdrawalDestination) 37 | : wallet.publicKey 38 | 39 | const fwdKey = feeWithdrawalDestination 40 | ? new PublicKey(feeWithdrawalDestination) 41 | : wallet.publicKey 42 | 43 | const tMintKey = treasuryMint ? new PublicKey(treasuryMint) : NATIVE_MINT 44 | 45 | const twdAta = tMintKey.equals(NATIVE_MINT) 46 | ? twdKey 47 | : ( 48 | await AuctionHouseProgram.findAssociatedTokenAccountAddress( 49 | tMintKey, 50 | twdKey 51 | ) 52 | )[0] 53 | 54 | const [auctionHouse] = await AuctionHouseProgram.findAuctionHouseAddress( 55 | wallet.publicKey, 56 | tMintKey 57 | ) 58 | 59 | return createUpdateAuctionHouseInstruction( 60 | { 61 | treasuryMint: tMintKey, 62 | payer: wallet.publicKey, 63 | authority: wallet.publicKey, 64 | newAuthority: wallet.publicKey, 65 | feeWithdrawalDestination: fwdKey, 66 | treasuryWithdrawalDestination: twdAta, 67 | treasuryWithdrawalDestinationOwner: twdKey, 68 | auctionHouse, 69 | }, 70 | { 71 | sellerFeeBasisPoints, 72 | requiresSignOff, 73 | canChangeSalePrice, 74 | } 75 | ) 76 | } 77 | -------------------------------------------------------------------------------- /src/layouts/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import { Dispatch, ReactNode, SetStateAction, useRef } from 'react' 2 | import cx from 'classnames' 3 | import { X } from 'react-feather' 4 | 5 | type ModalProps = { 6 | open: Boolean 7 | // for whatever reason big B and little B are diff to TS... 8 | setOpen: 9 | | Dispatch> 10 | | Dispatch> 11 | | ((open: Boolean) => void) 12 | children: ReactNode 13 | title?: String 14 | priority?: boolean 15 | short?: Boolean 16 | } 17 | 18 | export const Modal = ({ 19 | open, 20 | setOpen, 21 | children, 22 | title, 23 | priority = false, 24 | short, 25 | }: ModalProps) => { 26 | const modalRef = useRef(null!) 27 | 28 | if (!open) { 29 | return null 30 | } 31 | 32 | return ( 33 |
50 |
57 | 63 | {title && ( 64 |
65 |

{title}

66 |
67 | )} 68 |
69 | {children} 70 |
71 |
72 |
73 | ) 74 | } 75 | 76 | export default Modal 77 | -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import { TailSpin } from 'react-loader-spinner' 2 | import cx from 'classnames' 3 | import { cond, equals, always, not } from 'ramda' 4 | 5 | export enum ButtonType { 6 | Primary = 'primary', 7 | Secondary = 'secondary', 8 | Tertiary = 'tertiary', 9 | } 10 | 11 | export enum ButtonSize { 12 | Small = 'sm', 13 | Large = 'lg', 14 | } 15 | 16 | interface ButtonProps { 17 | children?: any 18 | htmlType?: 'button' | 'submit' | 'reset' | undefined 19 | size?: ButtonSize 20 | block?: boolean 21 | type?: ButtonType 22 | disabled?: boolean 23 | loading?: boolean 24 | icon?: React.ReactElement 25 | className?: string 26 | onClick?: () => any 27 | } 28 | 29 | const isPrimary = equals(ButtonType.Primary) 30 | const isSecondary = equals(ButtonType.Secondary) 31 | const isTertiary = equals(ButtonType.Tertiary) 32 | const isLarge = equals(ButtonSize.Large) 33 | const isSmall = equals(ButtonSize.Small) 34 | 35 | const Button = ({ 36 | children, 37 | icon, 38 | size = ButtonSize.Large, 39 | htmlType = 'button', 40 | disabled = false, 41 | loading = false, 42 | type = ButtonType.Primary, 43 | className = '', 44 | block = false, 45 | onClick, 46 | }: ButtonProps) => { 47 | return ( 48 | 82 | ) 83 | } 84 | 85 | export default Button 86 | -------------------------------------------------------------------------------- /src/components/CancelOfferForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form' 2 | import { OperationVariables, ApolloQueryResult } from '@apollo/client' 3 | import Button, { ButtonSize, ButtonType } from './../../components/Button' 4 | import { toast } from 'react-toastify' 5 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 6 | import { Wallet } from '@metaplex/js' 7 | import { 8 | initMarketplaceSDK, 9 | Marketplace, 10 | Nft, 11 | Offer, 12 | } from '@holaplex/marketplace-js-sdk' 13 | import { useMemo } from 'react' 14 | 15 | interface CancelOfferFormProps { 16 | offer: Offer 17 | nft?: Nft 18 | marketplace: Marketplace 19 | refetch: ( 20 | variables?: Partial | undefined 21 | ) => Promise> 22 | } 23 | 24 | const CancelOfferForm = ({ 25 | offer, 26 | nft, 27 | marketplace, 28 | refetch, 29 | }: CancelOfferFormProps) => { 30 | const wallet = useWallet() 31 | const { publicKey, signTransaction } = wallet 32 | const { connection } = useConnection() 33 | const { 34 | formState: { isSubmitting }, 35 | handleSubmit, 36 | } = useForm() 37 | 38 | const sdk = useMemo( 39 | () => initMarketplaceSDK(connection, wallet as Wallet), 40 | [connection, wallet] 41 | ) 42 | 43 | const cancelOfferTransaction = async () => { 44 | if (!publicKey || !signTransaction || !offer || !nft) { 45 | return 46 | } 47 | 48 | try { 49 | toast('Sending the transaction to Solana.') 50 | await sdk 51 | .transaction() 52 | .add( 53 | sdk.offers(offer.auctionHouse).cancel({ 54 | nft, 55 | offer, 56 | }) 57 | ) 58 | .add( 59 | sdk.escrow(offer.auctionHouse).withdraw({ 60 | amount: offer.price.toNumber(), 61 | }) 62 | ) 63 | .send() 64 | 65 | await refetch() 66 | 67 | toast.success('The transaction was confirmed.') 68 | } catch (e: any) { 69 | console.log('Cancel Offer Error', e) 70 | toast.error(e.message) 71 | } 72 | } 73 | 74 | return ( 75 |
76 | 84 |
85 | ) 86 | } 87 | export default CancelOfferForm 88 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Dispatch, FC, ReactNode, SetStateAction, useRef } from 'react' 3 | import cx from 'classnames' 4 | import { Close } from '../icons/Close' 5 | import { useOutsideAlerter } from '../../hooks/outsideAlerter' 6 | 7 | type ModalProps = { 8 | open: Boolean 9 | // for whatever reason big B and little B are diff to TS... 10 | setOpen: 11 | | Dispatch> 12 | | Dispatch> 13 | | ((open: Boolean) => void) 14 | children: ReactNode 15 | title?: String 16 | priority?: boolean 17 | short?: Boolean 18 | } 19 | 20 | export const Modal: FC = ({ 21 | open, 22 | setOpen, 23 | children, 24 | title, 25 | priority = false, 26 | short, 27 | }) => { 28 | const modalRef = useRef(null!) 29 | useOutsideAlerter(modalRef, () => setOpen(priority)) // disable outside alerter if priority (require clicking close) 30 | 31 | if (!open) { 32 | return null 33 | } 34 | 35 | return ( 36 |
53 |
60 | 66 | {title && ( 67 |
68 |

{title}

69 |
70 | )} 71 |
74 | {children} 75 |
76 |
77 |
78 | ) 79 | } 80 | 81 | export default Modal 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marketplace", 3 | "private": true, 4 | "scripts": { 5 | "dev": "next dev", 6 | "build": "next build", 7 | "start": "next start", 8 | "lint": "next lint", 9 | "format:write": "prettier --write **/*.{ts,css,tsx}", 10 | "format:check": "prettier --check **/*.{ts,css,tsx}", 11 | "prepare": "husky install" 12 | }, 13 | "dependencies": { 14 | "@apollo/client": "^3.5.6", 15 | "@cardinal/namespaces": "^4.1.15", 16 | "@dialectlabs/react": "^0.4.11", 17 | "@dialectlabs/react-ui": "^0.7.3", 18 | "@fontsource/inter": "^4.5.1", 19 | "@fontsource/jetbrains-mono": "^4.5.0", 20 | "@fontsource/material-icons": "^4.5.1", 21 | "@heroicons/react": "^1.0.5", 22 | "@holaplex/marketplace-js-sdk": "^0.2.8", 23 | "@metaplex-foundation/mpl-token-metadata": "^1.2.5", 24 | "@metaplex/js": "^4.12.0", 25 | "@radix-ui/react-popover": "^0.1.6", 26 | "@solana/spl-token-registry": "^0.2.3692", 27 | "@solana/wallet-adapter-base": "^0.9.2", 28 | "@solana/wallet-adapter-react": "^0.15.2", 29 | "@solana/wallet-adapter-react-ui": "^0.9.4", 30 | "@solana/wallet-adapter-wallets": "^0.15.5", 31 | "@solana/web3.js": "1.37.1", 32 | "classnames": "^2.3.1", 33 | "date-fns": "^2.28.0", 34 | "formidable": "^2.0.1", 35 | "fs-extra": "^10.0.1", 36 | "graphql": "^16.2.0", 37 | "next": "12.0.7", 38 | "next-compose-plugins": "^2.2.1", 39 | "next-images": "^1.8.4", 40 | "next-with-less": "^2.0.5", 41 | "nextjs-cors": "^2.1.0", 42 | "ramda": "^0.28.0", 43 | "react": "17.0.2", 44 | "react-dom": "17.0.2", 45 | "react-feather": "^2.0.9", 46 | "react-hook-form": "^7.25.1", 47 | "react-intersection-observer": "^8.33.1", 48 | "react-loader-spinner": "^6.0.0-0", 49 | "react-multi-carousel": "^2.8.0", 50 | "react-select": "^5.2.2", 51 | "react-toastify": "^8.2.0", 52 | "recharts": "^2.1.9", 53 | "timeago.js": "^4.0.2" 54 | }, 55 | "devDependencies": { 56 | "@babel/core": "^7.16.10", 57 | "@types/formidable": "^2.0.4", 58 | "@types/node": "17.0.8", 59 | "@types/ramda": "^0.27.64", 60 | "@types/react": "17.0.38", 61 | "@types/recharts": "^1.8.23", 62 | "autoprefixer": "^10.4.2", 63 | "babel-plugin-graphql-tag": "^3.3.0", 64 | "eslint": "8.6.0", 65 | "eslint-config-next": "12.0.7", 66 | "husky": ">=6", 67 | "lint-staged": ">=10", 68 | "postcss": "^8.4.5", 69 | "postcss-import": "^14.0.2", 70 | "prettier": "2.6.0", 71 | "tailwindcss": "^3.0.15", 72 | "typescript": "4.5.4" 73 | }, 74 | "lint-staged": { 75 | "**/*.{ts,css,tsx}": "yarn format:write" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/client.ts: -------------------------------------------------------------------------------- 1 | import { ApolloClient, gql, InMemoryCache } from '@apollo/client' 2 | import { offsetLimitPagination } from '@apollo/client/utilities' 3 | import BN from 'bn.js' 4 | import { constructN, ifElse, isNil } from 'ramda' 5 | import { viewerVar } from './cache' 6 | 7 | const asBN = ifElse(isNil, () => new BN(0), constructN(1, BN)) 8 | 9 | const GRAPHQL_ENDPOINT = process.env.NEXT_PUBLIC_GRAPHQL_ENDPOINT 10 | 11 | const typeDefs = gql` 12 | type Viewer { 13 | id: ID 14 | balance: Number 15 | } 16 | 17 | extend type Query { 18 | viewer(address: String!): Viewer 19 | } 20 | ` 21 | 22 | const client = new ApolloClient({ 23 | uri: GRAPHQL_ENDPOINT, 24 | typeDefs, 25 | cache: new InMemoryCache({ 26 | typePolicies: { 27 | Query: { 28 | fields: { 29 | nfts: offsetLimitPagination(), 30 | viewer: { 31 | read() { 32 | return viewerVar() 33 | }, 34 | }, 35 | }, 36 | }, 37 | MintStats: { 38 | fields: { 39 | volume24hr: { 40 | read: asBN, 41 | }, 42 | volumeTotal: { 43 | read: asBN, 44 | }, 45 | average: { 46 | read: asBN, 47 | }, 48 | floor: { 49 | read: asBN, 50 | }, 51 | }, 52 | }, 53 | PricePoint: { 54 | fields: { 55 | price: { 56 | read: asBN, 57 | }, 58 | }, 59 | }, 60 | StoreCreator: { 61 | keyFields: ['creatorAddress', 'storeConfigAddress'], 62 | }, 63 | Marketplace: { 64 | keyFields: ['ownerAddress'], 65 | }, 66 | Nft: { 67 | keyFields: ['address'], 68 | }, 69 | Wallet: { 70 | keyFields: ['address'], 71 | }, 72 | Creator: { 73 | keyFields: ['address'], 74 | }, 75 | NftCreator: { 76 | keyFields: ['address'], 77 | }, 78 | NftOwner: { 79 | keyFields: ['address'], 80 | }, 81 | Purchase: { 82 | keyFields: ['id'], 83 | fields: { 84 | price: { 85 | read: asBN, 86 | }, 87 | }, 88 | }, 89 | AhListing: { 90 | keyFields: ['id'], 91 | fields: { 92 | price: { 93 | read: asBN, 94 | }, 95 | }, 96 | }, 97 | NftActivity: { 98 | keyFields: ['id'], 99 | fields: { 100 | price: { 101 | read: asBN, 102 | }, 103 | }, 104 | }, 105 | Offer: { 106 | keyFields: ['id'], 107 | fields: { 108 | price: { 109 | read: asBN, 110 | }, 111 | }, 112 | }, 113 | NftAttribute: { 114 | keyFields: ['traitType', 'value'], 115 | }, 116 | }, 117 | }), 118 | }) 119 | 120 | export default client 121 | -------------------------------------------------------------------------------- /src/layouts/Banner/BannerLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import { equals } from 'ramda' 3 | import cx from 'classnames' 4 | import { useWallet } from '@solana/wallet-adapter-react' 5 | import { Marketplace } from '@holaplex/marketplace-js-sdk' 6 | import { useSidebar } from '../../hooks/sidebar' 7 | import Link from 'next/link' 8 | import WalletPortal from '../../components/WalletPortal' 9 | import DialectNotificationsButton from '../../components/DialectNotificationsButton' 10 | 11 | interface BannerLayoutProps { 12 | marketplace: Marketplace 13 | children: ReactElement 14 | } 15 | 16 | export const BannerLayout = ({ marketplace, children }: BannerLayoutProps) => { 17 | const { publicKey } = useWallet() 18 | const { sidebarOpen } = useSidebar() 19 | 20 | return ( 21 |
26 |
27 |
28 | 29 | 30 | 37 | 38 | 39 |
40 | {equals( 41 | publicKey?.toBase58(), 42 | marketplace.auctionHouses[0].authority 43 | ) && ( 44 | 45 | 46 | Admin Dashboard 47 | 48 | 49 | )} 50 | 51 | 52 | Creators 53 | 54 | 55 | 56 | 57 | Activity 58 | 59 | 60 |
61 | 62 |
63 | 64 |
65 |
66 | {marketplace.name} 71 |
72 |
{children}
73 |
74 | ) 75 | } 76 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Kubernetes deployment 2 | 3 | on: 4 | push: 5 | branches: [ dev, main ] 6 | 7 | jobs: 8 | build: 9 | name: Building and Pushing Image 10 | if: ${{ !contains(github.event.head_commit.message, '#k8s') }} 11 | runs-on: self-hosted 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Configure AWS credentials 18 | uses: aws-actions/configure-aws-credentials@v1 19 | with: 20 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 21 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 22 | aws-region: ${{ secrets.AWS_REGION }} 23 | 24 | - name: Login to Amazon ECR 25 | id: login-ecr 26 | uses: aws-actions/amazon-ecr-login@v1 27 | 28 | - name: Build, tag, and push Frontend to Amazon ECR 29 | id: build-frontend-image 30 | env: 31 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 32 | ECR_REPOSITORY: ${{ github.event.repository.name }} 33 | 34 | run: | 35 | git_hash=$(git rev-parse --short "$GITHUB_SHA") 36 | git_branch=${GITHUB_REF##*/} 37 | solana_endpoint="https://rpc.mainnet.holaplex.tools" 38 | if [ $git_branch == 'main' ];then 39 | graphql_endpoint="https://graph.holaplex.com/v1" 40 | node_env=production 41 | else 42 | graphql_endpoint="https://graph-test.holaplex.com/v1" 43 | node_env=development 44 | fi 45 | image_tag="$ECR_REGISTRY/$ECR_REPOSITORY:$git_branch-$git_hash" 46 | #build 47 | docker build -t $image_tag \ 48 | --build-arg GRAPHQL_URI=${graphql_endpoint} \ 49 | --build-arg SOLANA_ENDPOINT=${solana_endpoint} \ 50 | --build-arg NEXT_ENVIRONMENT=$node_env \ 51 | . 52 | 53 | echo "Pushing image to ECR..." 54 | docker push $image_tag 55 | echo "::set-output name=image::$image_tag" 56 | 57 | - name: Update deployment image version (dev) 58 | if: github.ref == 'refs/heads/dev' 59 | run: | 60 | git_hash=$(git rev-parse --short "$GITHUB_SHA") 61 | git_branch=${GITHUB_REF##*/} 62 | version=$(cat ./k8s/$git_branch/app/marketplace-deploy.yaml | grep -i image | awk {'print $2'} | head -n1 | cut -d: -f2) 63 | sed -i "s/$version/$git_branch-$git_hash/" ./k8s/$git_branch/app/marketplace-deploy.yaml 64 | 65 | - name: Update deployment image version (prod) 66 | if: github.ref == 'refs/heads/main' 67 | run: | 68 | git_hash=$(git rev-parse --short "$GITHUB_SHA") 69 | version=$(cat ./k8s/prod/app/marketplace-deploy.yaml | grep -i image | awk {'print $2'} | head -n1 | cut -d: -f2) 70 | sed -i "s/$version/${GITHUB_REF##*/}-$git_hash/" ./k8s/prod/app/marketplace-deploy.yaml 71 | 72 | - name: Commit and push changes 73 | uses: devops-infra/action-commit-push@master 74 | with: 75 | github_token: ${{ secrets.ACTIONS_TOKEN}} 76 | commit_message: Updated deployment image version 77 | -------------------------------------------------------------------------------- /src/layouts/Basic/BasicLayout.tsx: -------------------------------------------------------------------------------- 1 | import React, { ReactElement } from 'react' 2 | import { Marketplace } from '@holaplex/marketplace-js-sdk' 3 | import Link from 'next/link' 4 | import { equals } from 'ramda' 5 | import cx from 'classnames' 6 | import WalletPortal from '../../components/WalletPortal' 7 | import { useWallet } from '@solana/wallet-adapter-react' 8 | import DialectNotificationsButton from '../../components/DialectNotificationsButton' 9 | 10 | interface BasicLayoutProps { 11 | marketplace: Marketplace 12 | children: ReactElement | ReactElement[] 13 | active?: NavigationLink 14 | } 15 | 16 | export enum NavigationLink { 17 | Creators, 18 | Activity, 19 | Admin, 20 | } 21 | 22 | export const BasicLayout = ({ 23 | marketplace, 24 | children, 25 | active, 26 | }: BasicLayoutProps) => { 27 | const { publicKey } = useWallet() 28 | 29 | return ( 30 |
31 |
32 | 33 | 34 | 41 | 42 | 43 |
44 |
45 | 46 | 51 | Creators 52 | 53 | 54 | 55 | 60 | Activity 61 | 62 | 63 | {equals( 64 | publicKey?.toBase58(), 65 | marketplace.auctionHouses[0].authority 66 | ) && ( 67 | 68 | 73 | Admin Dashboard 74 | 75 | 76 | )} 77 |
78 | 79 |
80 | 81 |
82 |
83 |
84 |
{children}
85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import '@dialectlabs/react-ui/index.css' 2 | import '../styles/globals.css' 3 | import '@fontsource/inter/300.css' 4 | import '@fontsource/inter/600.css' 5 | import '@fontsource/jetbrains-mono/200.css' 6 | import '@fontsource/material-icons' 7 | import 'react-multi-carousel/lib/styles.css' 8 | import type { AppProps } from 'next/app' 9 | import { ApolloProvider } from '@apollo/client' 10 | import React, { ReactNode, useMemo } from 'react' 11 | import { 12 | ConnectionProvider, 13 | WalletProvider, 14 | } from '@solana/wallet-adapter-react' 15 | import { WalletAdapterNetwork } from '@solana/wallet-adapter-base' 16 | import { 17 | GlowWalletAdapter, 18 | LedgerWalletAdapter, 19 | PhantomWalletAdapter, 20 | SlopeWalletAdapter, 21 | SolflareWalletAdapter, 22 | SolletExtensionWalletAdapter, 23 | SolletWalletAdapter, 24 | TorusWalletAdapter, 25 | } from '@solana/wallet-adapter-wallets' 26 | import { WalletModalProvider } from '@solana/wallet-adapter-react-ui' 27 | import '@solana/wallet-adapter-react-ui/styles.css' 28 | import { NextPage } from 'next' 29 | import { Cluster } from '@solana/web3.js' 30 | import client from '../client' 31 | import { ToastContainer } from 'react-toastify' 32 | import { ViewerProvider } from './../providers/Viewer' 33 | import 'react-toastify/dist/ReactToastify.css' 34 | import { MultiTransactionProvider } from '../modules/multi-transaction' 35 | 36 | const network = WalletAdapterNetwork.Mainnet 37 | 38 | const CLUSTER_API_URL = process.env.NEXT_PUBLIC_SOLANA_ENDPOINT || '' 39 | 40 | const clusterApiUrl = (cluster: Cluster): string => CLUSTER_API_URL 41 | 42 | type NextPageWithLayout = NextPage & { 43 | getLayout?: ({ children: ReactNode }) => ReactNode 44 | } 45 | 46 | type AppPropsWithLayout = AppProps & { 47 | Component: NextPageWithLayout 48 | } 49 | 50 | function App({ Component, pageProps }: AppPropsWithLayout) { 51 | const endpoint = useMemo(() => clusterApiUrl(network), []) 52 | 53 | const wallets = useMemo( 54 | () => [ 55 | new GlowWalletAdapter(), 56 | new PhantomWalletAdapter(), 57 | new SlopeWalletAdapter(), 58 | new SolflareWalletAdapter(), 59 | new TorusWalletAdapter({ params: { network } }), 60 | new SolletWalletAdapter({ network }), 61 | new SolletExtensionWalletAdapter({ network }), 62 | ], 63 | [] 64 | ) 65 | 66 | const Layout = Component.getLayout ?? (({ children }) => children) 67 | 68 | return ( 69 | 70 | 74 | 75 | 76 | 77 | 78 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | ) 95 | } 96 | 97 | export default App 98 | -------------------------------------------------------------------------------- /src/components/AcceptOfferForm/index.tsx: -------------------------------------------------------------------------------- 1 | import { useForm } from 'react-hook-form' 2 | import Button, { ButtonSize, ButtonType } from './../../components/Button' 3 | import { OperationVariables, ApolloQueryResult } from '@apollo/client' 4 | import { toast } from 'react-toastify' 5 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 6 | import { Wallet } from '@metaplex/js' 7 | import { 8 | initMarketplaceSDK, 9 | AhListing, 10 | Nft, 11 | Offer, 12 | } from '@holaplex/marketplace-js-sdk' 13 | import { useContext, useMemo } from 'react' 14 | import { 15 | Action, 16 | MultiTransactionContext, 17 | } from '../../modules/multi-transaction' 18 | 19 | interface AcceptOfferFormProps { 20 | offer: Offer 21 | listing?: AhListing 22 | nft?: Nft 23 | refetch: ( 24 | variables?: Partial | undefined 25 | ) => Promise> 26 | } 27 | 28 | const AcceptOfferForm = ({ 29 | offer, 30 | nft, 31 | listing, 32 | refetch, 33 | }: AcceptOfferFormProps) => { 34 | const wallet = useWallet() 35 | const { publicKey, signTransaction } = wallet 36 | const { connection } = useConnection() 37 | const sdk = useMemo( 38 | () => initMarketplaceSDK(connection, wallet as Wallet), 39 | [connection, wallet] 40 | ) 41 | const { runActions } = useContext(MultiTransactionContext) 42 | 43 | const { 44 | formState: { isSubmitting }, 45 | handleSubmit, 46 | } = useForm() 47 | 48 | const onAcceptOffer = async () => { 49 | if (!offer || !nft || !offer.auctionHouse) { 50 | return 51 | } 52 | 53 | toast('Sending the transaction to Solana.') 54 | await sdk 55 | .transaction() 56 | .add( 57 | sdk.offers(offer.auctionHouse).accept({ 58 | nft, 59 | offer, 60 | }) 61 | ) 62 | .send() 63 | } 64 | 65 | const onCancelListing = (listing: AhListing) => async () => { 66 | if (!offer || !nft || !offer.auctionHouse) { 67 | return 68 | } 69 | 70 | await sdk 71 | .transaction() 72 | .add(sdk.listings(offer.auctionHouse).cancel({ listing, nft })) 73 | .send() 74 | } 75 | 76 | const acceptOfferTransaction = async () => { 77 | if (!publicKey || !signTransaction || !offer || !nft) { 78 | return 79 | } 80 | 81 | let newActions: Action[] = [] 82 | 83 | if (listing) { 84 | newActions = [ 85 | { 86 | name: 'Cancel previous listing...', 87 | id: 'cancelListing', 88 | action: onCancelListing(listing), 89 | param: undefined, 90 | }, 91 | ] 92 | } 93 | 94 | newActions = [ 95 | ...newActions, 96 | { 97 | name: 'Accepting offer...', 98 | id: 'acceptOffer', 99 | action: onAcceptOffer, 100 | param: undefined, 101 | }, 102 | ] 103 | 104 | await runActions(newActions, { 105 | onActionSuccess: async () => { 106 | toast.success('The transaction was confirmed.') 107 | }, 108 | onComplete: async () => { 109 | await refetch() 110 | }, 111 | onActionFailure: async (err) => { 112 | await refetch() 113 | toast.error(err.message) 114 | }, 115 | }) 116 | } 117 | 118 | return ( 119 |
120 | 128 |
129 | ) 130 | } 131 | 132 | export default AcceptOfferForm 133 | -------------------------------------------------------------------------------- /src/pages/analytics/index.tsx: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client' 2 | import { NextPageContext, NextPage } from 'next' 3 | import { isNil, map, prop } from 'ramda' 4 | import { subDays } from 'date-fns' 5 | import client from '../../client' 6 | import { AnalyticsLayout } from './../../layouts/Analytics' 7 | import { 8 | Marketplace, 9 | GetActivities, 10 | GetPriceChartData, 11 | } from '@holaplex/marketplace-js-sdk' 12 | import { isSol } from '../../modules/sol' 13 | 14 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 15 | 16 | const GET_PRICE_CHART_DATA = gql` 17 | query GetPriceChartData( 18 | $auctionHouses: [PublicKey!]! 19 | $startDate: DateTimeUtc! 20 | $endDate: DateTimeUtc! 21 | ) { 22 | charts( 23 | auctionHouses: $auctionHouses 24 | startDate: $startDate 25 | endDate: $endDate 26 | ) { 27 | listingFloor { 28 | price 29 | date 30 | } 31 | salesAverage { 32 | price 33 | date 34 | } 35 | totalVolume { 36 | price 37 | date 38 | } 39 | } 40 | } 41 | ` 42 | 43 | const GET_ACTIVITIES = gql` 44 | query GetActivities($auctionHouses: [PublicKey!]!) { 45 | activities(auctionHouses: $auctionHouses) { 46 | id 47 | metadata 48 | auctionHouse { 49 | address 50 | treasuryMint 51 | } 52 | price 53 | createdAt 54 | wallets { 55 | address 56 | profile { 57 | handle 58 | profileImageUrlLowres 59 | } 60 | } 61 | activityType 62 | nft { 63 | name 64 | image 65 | address 66 | } 67 | } 68 | } 69 | ` 70 | 71 | export async function getServerSideProps({ req }: NextPageContext) { 72 | const subdomain = req?.headers['x-holaplex-subdomain'] || SUBDOMAIN 73 | 74 | const response = await client.query({ 75 | fetchPolicy: 'no-cache', 76 | query: gql` 77 | query GetMarketplacePage($subdomain: String!) { 78 | marketplace(subdomain: $subdomain) { 79 | subdomain 80 | name 81 | description 82 | logoUrl 83 | bannerUrl 84 | auctionHouses { 85 | authority 86 | address 87 | treasuryMint 88 | } 89 | } 90 | } 91 | `, 92 | variables: { 93 | subdomain, 94 | }, 95 | }) 96 | 97 | const { 98 | data: { marketplace }, 99 | } = response 100 | 101 | if (isNil(marketplace)) { 102 | return { 103 | notFound: true, 104 | } 105 | } 106 | 107 | return { 108 | props: { 109 | marketplace, 110 | }, 111 | } 112 | } 113 | 114 | interface GetMarketplace { 115 | marketplace: Marketplace | null 116 | } 117 | 118 | interface AnalyticsProps { 119 | marketplace: Marketplace 120 | } 121 | 122 | const startDate = subDays(new Date(), 6).toISOString() 123 | const endDate = new Date().toISOString() 124 | 125 | const Analytics: NextPage = ({ marketplace }) => { 126 | const solAH = 127 | marketplace.auctionHouses.filter((ah) => isSol(ah.treasuryMint))[0] 128 | .address || '' 129 | 130 | const priceChartQuery = useQuery(GET_PRICE_CHART_DATA, { 131 | variables: { 132 | auctionHouses: [solAH], 133 | startDate, 134 | endDate, 135 | }, 136 | }) 137 | 138 | const activitiesQuery = useQuery(GET_ACTIVITIES, { 139 | variables: { 140 | auctionHouses: [solAH], 141 | }, 142 | }) 143 | 144 | return ( 145 | {marketplace.name}} 147 | metaTitle={`${marketplace.name} Activity`} 148 | marketplace={marketplace} 149 | priceChartQuery={priceChartQuery} 150 | activitiesQuery={activitiesQuery} 151 | /> 152 | ) 153 | } 154 | 155 | export default Analytics 156 | -------------------------------------------------------------------------------- /src/components/WalletPortal/WalletPortal.tsx: -------------------------------------------------------------------------------- 1 | import { isNil, not, or, and } from 'ramda' 2 | import { useQuery, gql } from '@apollo/client' 3 | import { useWallet } from '@solana/wallet-adapter-react' 4 | import * as Popover from '@radix-ui/react-popover' 5 | import Button, { ButtonSize, ButtonType } from '../Button' 6 | import { toSOL } from '../../modules/sol' 7 | import { Viewer } from '@holaplex/marketplace-js-sdk' 8 | import { addressAvatar, truncateAddress } from '../../modules/address' 9 | import { useLogin } from './../../hooks/login' 10 | import { Check, ChevronRight } from 'react-feather' 11 | import { toast } from 'react-toastify' 12 | 13 | const GET_VIEWER = gql` 14 | query GetViewer { 15 | viewer @client { 16 | balance 17 | } 18 | } 19 | ` 20 | 21 | interface GetViewerData { 22 | viewer: Viewer 23 | } 24 | 25 | const WalletPortal = () => { 26 | const login = useLogin() 27 | const { connected, publicKey, disconnect, connecting } = useWallet() 28 | const { loading, data } = useQuery(GET_VIEWER) 29 | 30 | const isLoading = loading || connecting 31 | 32 | const handleLabelClick = async () => { 33 | if (publicKey?.toBase58().length) { 34 | await navigator.clipboard.writeText(publicKey.toBase58()) 35 | toast( 36 |
37 |
38 | 39 |
Wallet address copied to clipboard.
40 |
41 |
42 | ) 43 | } 44 | } 45 | 46 | return or(connected, isLoading) ? ( 47 | 48 | 49 |
50 | {not(isLoading) && publicKey && ( 51 | 55 | )} 56 |
57 |
58 | 59 | {/* */} 60 |
61 |
62 | {not(isLoading) && publicKey && ( 63 | 67 | )} 68 |
69 | {not(isLoading) && ( 70 | 76 | View profile 77 | 78 | )} 79 |
80 |
81 |
82 | {or(isLoading, isNil(data?.viewer)) ? ( 83 |
84 | ) : ( 85 | toSOL(data?.viewer.balance as number) + ' SOL' 86 | )} 87 |
88 | {isLoading ? ( 89 |
90 | ) : ( 91 |
95 | {truncateAddress(publicKey?.toBase58() as string)} 96 |
97 | )} 98 |
99 | {isLoading ? ( 100 |
101 | ) : ( 102 | 111 | )} 112 | 113 | 114 | ) : ( 115 | 118 | ) 119 | } 120 | 121 | export default WalletPortal 122 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .light { 6 | --base-fg: #292929; 7 | --base-bg: #efefef; 8 | --overlay-1: #7773; 9 | } 10 | 11 | :root { 12 | --green-500: #22c55e; 13 | --gray-100: #e0e0e0; 14 | --gray-300: #a8a8a8; 15 | --gray-500: #6f6f6f; 16 | --gray-700: #393939; 17 | --gray-800: #262626; 18 | --gray-900: #171717; 19 | --error-300: #fda29b; 20 | --error-500: #f04438; 21 | --error-800: #912018; 22 | --error-900: #7a271a; 23 | --success-300: #6ce9a6; 24 | --success-500: #12b76a; 25 | --success-800: #05603a; 26 | --success-900: #054f31; 27 | } 28 | 29 | @layer components { 30 | .label { 31 | @apply text-xs text-gray-300; 32 | } 33 | 34 | .button { 35 | @apply flex font-semibold items-center justify-center h-12 text-gray-900 bg-white hover:scale-[1.02] px-4 rounded-full whitespace-nowrap transition-transform grow; 36 | } 37 | 38 | .button.small { 39 | @apply h-10 text-sm; 40 | } 41 | 42 | .button.secondary { 43 | @apply text-white bg-gray-900 grow; 44 | } 45 | 46 | .button.tertiary { 47 | @apply text-gray-300 bg-gray-700 grow; 48 | } 49 | 50 | .input { 51 | @apply bg-gray-900 px-4 py-2 border border-gray-500 focus:border-gray-100 text-white rounded-md w-full focus:outline-none placeholder-gray-500; 52 | } 53 | 54 | .sol-input { 55 | @apply relative; 56 | } 57 | 58 | .sol-input .input { 59 | @apply pl-10; 60 | } 61 | 62 | .sol-input:before { 63 | content: '◎'; 64 | @apply h-full absolute px-4 flex items-center text-gray-500; 65 | } 66 | 67 | .sol-amount:before { 68 | content: '◎'; 69 | @apply h-full text-gray-500 mr-1; 70 | } 71 | 72 | .connected-status:before { 73 | content: ''; 74 | @apply inline-block w-2 h-2 rounded-full bg-green-500 mr-2; 75 | } 76 | } 77 | 78 | * { 79 | font-family: 'Inter', sans-serif; 80 | } 81 | 82 | html, 83 | body { 84 | @apply bg-gray-900; 85 | } 86 | 87 | h1, 88 | h2, 89 | h3, 90 | h4 { 91 | @apply font-semibold; 92 | } 93 | 94 | h1 { 95 | @apply text-5xl; 96 | } 97 | 98 | h2 { 99 | @apply text-3xl; 100 | } 101 | 102 | h3 { 103 | @apply text-xl; 104 | } 105 | 106 | h4 { 107 | @apply text-base; 108 | } 109 | 110 | .pubkey { 111 | font-family: 'JetBrains Mono', monospace; 112 | word-break: break-all; 113 | } 114 | 115 | .app-slider .react-multiple-carousel__arrow--right { 116 | @apply right-2; 117 | } 118 | 119 | .app-slider .react-multiple-carousel__arrow--left { 120 | @apply left-2; 121 | } 122 | 123 | .app-slider.react-multi-carousel-list { 124 | @apply p-1; 125 | } 126 | 127 | .overlay-1 { 128 | background: var(--overlay-1); 129 | } 130 | 131 | .icon-sol::before { 132 | content: '◎'; 133 | @apply inline-block text-gray-500 mr-1; 134 | } 135 | 136 | .wallet-modal-theme .wallet-adapter-modal-wrapper { 137 | @apply bg-gray-900 font-sans; 138 | } 139 | 140 | .wallet-modal-theme .wallet-adapter-button:not([disabled]):hover { 141 | @apply bg-gray-800; 142 | } 143 | 144 | .wallet-modal-theme .wallet-adapter-modal-button-close { 145 | @apply bg-gray-800; 146 | } 147 | .select-base-theme .base__control { 148 | @apply bg-gray-800 border-0 text-white outline-0 shadow-none cursor-pointer rounded-md py-1 px-4; 149 | } 150 | 151 | .select-base-theme .base__control:hover { 152 | @apply bg-gray-700 border-0 text-white shadow-none; 153 | } 154 | 155 | .select-base-theme .base__value-container, 156 | .select-base-theme .base__input-container, 157 | .select-base-theme .base__placeholder { 158 | @apply p-0 m-0; 159 | } 160 | 161 | .select-base-theme .base__control.base__control--is-focused { 162 | @apply border-0 bg-gray-700 outline-0 shadow-none; 163 | } 164 | 165 | .select-base-theme .base__dropdown-indicator { 166 | @apply text-gray-500 pr-0; 167 | } 168 | 169 | .select-base-theme .base__indicator-separator { 170 | background-color: transparent; 171 | } 172 | 173 | .select-base-theme .base__menu { 174 | @apply bg-gray-800 border-0; 175 | } 176 | 177 | .select-base-theme .base__menu-list .base__option { 178 | @apply bg-gray-800; 179 | } 180 | 181 | .select-base-theme .base__menu-list .base__option:hover { 182 | @apply bg-gray-700; 183 | } 184 | 185 | .select-base-theme .base__option.base__option--is-focused { 186 | @apply bg-gray-800 cursor-pointer; 187 | } 188 | 189 | .select-base-theme .base__multi-value { 190 | @apply text-white; 191 | } 192 | 193 | .select-base-theme .base__multi-value__label { 194 | @apply text-white; 195 | } 196 | 197 | .select-base-theme .base__single-value { 198 | @apply text-white; 199 | } 200 | 201 | /* Dialect-specific */ 202 | @supports (overflow: overlay) { 203 | .dt-overflow-y-auto { 204 | overflow-y: overlay; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/pages/creators/[creator]/analytics.tsx: -------------------------------------------------------------------------------- 1 | import { gql, useQuery } from '@apollo/client' 2 | import { NextPageContext, NextPage } from 'next' 3 | import { isNil, or, any, equals, not, map, prop } from 'ramda' 4 | import { subDays } from 'date-fns' 5 | import client from '../../../client' 6 | import { AnalyticsLayout } from './../../../layouts/Analytics' 7 | import { truncateAddress } from './../../../modules/address' 8 | import { useRouter } from 'next/router' 9 | import { 10 | Marketplace, 11 | PriceChart, 12 | GetActivities, 13 | GetPriceChartData, 14 | } from '@holaplex/marketplace-js-sdk' 15 | 16 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 17 | 18 | const pluckCreatorAddresses = map(prop('creatorAddress')) 19 | 20 | const GET_ACTIVITIES = gql` 21 | query GetActivities($auctionHouses: [PublicKey!]!, $creators: [PublicKey!]) { 22 | activities(auctionHouses: $auctionHouses, creators: $creators) { 23 | id 24 | metadata 25 | auctionHouse { 26 | address 27 | treasuryMint 28 | } 29 | price 30 | createdAt 31 | wallets { 32 | address 33 | profile { 34 | handle 35 | profileImageUrlLowres 36 | } 37 | } 38 | activityType 39 | nft { 40 | name 41 | image 42 | address 43 | } 44 | } 45 | } 46 | ` 47 | 48 | const GET_PRICE_CHART_DATA = gql` 49 | query GetPriceChartData( 50 | $auctionHouses: [PublicKey!]! 51 | $creators: [PublicKey!] 52 | $startDate: DateTimeUtc! 53 | $endDate: DateTimeUtc! 54 | ) { 55 | charts( 56 | auctionHouses: $auctionHouses 57 | creators: $creators 58 | startDate: $startDate 59 | endDate: $endDate 60 | ) { 61 | listingFloor { 62 | price 63 | date 64 | } 65 | salesAverage { 66 | price 67 | date 68 | } 69 | totalVolume { 70 | price 71 | date 72 | } 73 | } 74 | } 75 | ` 76 | 77 | export async function getServerSideProps({ req, query }: NextPageContext) { 78 | const subdomain = req?.headers['x-holaplex-subdomain'] || SUBDOMAIN 79 | 80 | const response = await client.query({ 81 | fetchPolicy: 'no-cache', 82 | query: gql` 83 | query GetMarketplacePage($subdomain: String!) { 84 | marketplace(subdomain: $subdomain) { 85 | subdomain 86 | name 87 | description 88 | logoUrl 89 | bannerUrl 90 | creators { 91 | creatorAddress 92 | storeConfigAddress 93 | } 94 | auctionHouses { 95 | authority 96 | address 97 | treasuryMint 98 | } 99 | } 100 | } 101 | `, 102 | variables: { 103 | subdomain, 104 | }, 105 | }) 106 | 107 | const { 108 | data: { marketplace }, 109 | } = response 110 | const marketplaceCreatorAddresses = pluckCreatorAddresses( 111 | marketplace?.creators || [] 112 | ) 113 | 114 | const isMarketplaceCreator = any(equals(query.creator))( 115 | marketplaceCreatorAddresses 116 | ) 117 | 118 | if (or(isNil(marketplace), not(isMarketplaceCreator))) { 119 | return { 120 | notFound: true, 121 | } 122 | } 123 | 124 | return { 125 | props: { 126 | marketplace, 127 | }, 128 | } 129 | } 130 | 131 | interface GetMarketplace { 132 | marketplace: Marketplace | null 133 | } 134 | 135 | interface CreatorAnalyticsProps { 136 | marketplace: Marketplace 137 | } 138 | 139 | const startDate = subDays(new Date(), 6).toISOString() 140 | const endDate = new Date().toISOString() 141 | 142 | const CreatorAnalytics: NextPage = ({ marketplace }) => { 143 | const auctionHouses = map(prop('address'))(marketplace.auctionHouses || []) 144 | 145 | const router = useRouter() 146 | const priceChartQuery = useQuery(GET_PRICE_CHART_DATA, { 147 | fetchPolicy: 'network-only', 148 | variables: { 149 | auctionHouses: auctionHouses, 150 | creators: [router.query.creator], 151 | startDate, 152 | endDate, 153 | }, 154 | }) 155 | 156 | const activitiesQuery = useQuery(GET_ACTIVITIES, { 157 | variables: { 158 | auctionHouses: auctionHouses, 159 | creators: [router.query.creator], 160 | }, 161 | }) 162 | 163 | const truncatedAddress = truncateAddress(router.query?.creator as string) 164 | 165 | return ( 166 | {truncatedAddress}} 168 | metaTitle={`${truncatedAddress} Activity`} 169 | marketplace={marketplace} 170 | priceChartQuery={priceChartQuery} 171 | activitiesQuery={activitiesQuery} 172 | /> 173 | ) 174 | } 175 | 176 | export default CreatorAnalytics 177 | -------------------------------------------------------------------------------- /src/components/NftCard/NftCard.tsx: -------------------------------------------------------------------------------- 1 | import { useWallet } from '@solana/wallet-adapter-react' 2 | import { PublicKey } from '@solana/web3.js' 3 | import { 4 | equals, 5 | find, 6 | not, 7 | pipe, 8 | prop, 9 | when, 10 | isNil, 11 | always, 12 | map, 13 | isEmpty, 14 | ifElse, 15 | view, 16 | flip, 17 | includes, 18 | lensPath, 19 | } from 'ramda' 20 | import React from 'react' 21 | import Link from 'next/link' 22 | import { addressAvatar } from '../../modules/address' 23 | import { AhListing, Marketplace, Nft } from '@holaplex/marketplace-js-sdk' 24 | import Price from '../Price' 25 | import { TokenInfo } from '@solana/spl-token-registry' 26 | 27 | interface NftCardProps { 28 | nft: Nft 29 | marketplace: Marketplace 30 | tokenMap: Map 31 | } 32 | 33 | export const NftCard = ({ nft, marketplace, tokenMap }: NftCardProps) => { 34 | const { publicKey } = useWallet() 35 | const marketplaceAuctionHouseAddresses = map(prop('address'))( 36 | marketplace.auctionHouses 37 | ) 38 | const listing = ifElse( 39 | isEmpty, 40 | always(null), 41 | find( 42 | pipe( 43 | view(lensPath(['auctionHouse', 'address'])), 44 | flip(includes)(marketplaceAuctionHouseAddresses) 45 | ) 46 | ) 47 | )(nft.listings || []) 48 | const isOwner = equals(nft.owner?.address, publicKey?.toBase58()) 49 | 50 | return ( 51 |
52 |
53 | Placeholder 58 | {nft.offers && nft.offers.length > 0 && ( 59 |
60 | {nft.offers.length} {nft.offers.length == 1 ? 'Offer' : 'Offers'} 61 |
62 | )} 63 |
64 |
65 |

{nft.name}

66 |
67 |
68 | {nft.creators.map((creator) => { 69 | return ( 70 |
74 | {creator.profile?.handle} 84 |
85 | ) 86 | })} 87 |
88 | {nft.creators?.length === 1 && ( 89 |
Creator
90 | )} 91 |
92 |
93 | 124 |
125 | ) 126 | } 127 | 128 | const Skeleton = () => { 129 | return ( 130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | ) 140 | } 141 | 142 | NftCard.Skeleton = Skeleton 143 | -------------------------------------------------------------------------------- /docs/readme.md: -------------------------------------------------------------------------------- 1 | # Run your own Marketplace 2 | 3 | ## Introduction 4 | 5 | This is how you would create your own NFT marketplace using the Holaplex API 6 | 7 | ## Marketplace Data 8 | 9 | > Graph QL API Endpoint: https://graph.holaplex.com/v0 10 | ### Nft Schema 11 | ```graphql 12 | type Nft { 13 | address: String! 14 | name: String! 15 | sellerFeeBasisPoints: Int! 16 | mintAddress: String! 17 | primarySaleHappened: Boolean! 18 | description: String! 19 | image: String! 20 | creators: [NftCreator!]! 21 | attributes: [NftAttribute!]! 22 | listings: [ListingReceipt!]! 23 | offers: [BidReceipt!]! 24 | } 25 | ``` 26 | 27 | ### NftCreator Schema 28 | ```graphql 29 | type NftCreator { 30 | address: String! 31 | metadataAddress: String! 32 | share: Int! 33 | verified: Boolean! 34 | } 35 | ``` 36 | 37 | ### NftAttribute Schema 38 | ```graphql 39 | type NftAttribute { 40 | metadataAddress: String! 41 | value: String! 42 | traitType: String! 43 | } 44 | ``` 45 | 46 | ### ListingReceipt Schema 47 | ``` 48 | type ListingReceipt { 49 | address: String! 50 | tradeState: String! 51 | seller: String! 52 | metadata: String! 53 | auctionHouse: String! 54 | price: Lamports! 55 | tradeStateBump: Int! 56 | createdAt: DateTimeUtc! 57 | canceledAt: DateTimeUtc 58 | bookkeeper: String! 59 | purchaseReceipt: String 60 | tokenSize: Int! 61 | bump: Int! 62 | } 63 | ``` 64 | ### BidReceipt Schema 65 | ``` 66 | type BidReceipt { 67 | address: String! 68 | tradeState: String! 69 | buyer: String! 70 | metadata: String! 71 | auctionHouse: String! 72 | price: Lamports! 73 | tradeStateBump: Int! 74 | tokenAccount: String 75 | createdAt: DateTimeUtc! 76 | canceledAt: DateTimeUtc 77 | } 78 | ``` 79 | 80 | 81 | --- 82 | ## Root Queries 83 | 84 | - `nft(address: PublicKey){}` 85 | 86 | - `nfts(creators:[PublicKey,...]){}` 87 | 88 | ### Example Queries 89 | 90 | Get all NFTs from a specifc creator (PublicKey) 91 | > Request 92 | ```graphql 93 | { 94 | nfts(creators: ["232PpcrPc6Kz7geafvbRzt5HnHP4kX88yvzUCN69WXQC"]){ 95 | name 96 | address 97 | image 98 | creators { 99 | address 100 | share 101 | } 102 | } 103 | } 104 | ``` 105 | > Response 106 | ``` 107 | { 108 | "data": { 109 | "nfts": [ 110 | { 111 | "name": "Whirlpools of Honey", 112 | "address": "3UF9qYsW9NNkUhAKtv42RZbWDVCiPRPW1FT3LY7RgcAP", 113 | "image": "https://bafybeidy6ardd2pvpcg5y6boidc2hfhvasb4fhx6wrn2sc675utpky5wy4.ipfs.dweb.link?ext=png", 114 | "creators": [ 115 | { 116 | "address": "232PpcrPc6Kz7geafvbRzt5HnHP4kX88yvzUCN69WXQC", 117 | "share": 100 118 | } 119 | ] 120 | }, 121 | {...}, 122 | {...}, 123 | ] 124 | } 125 | } 126 | ``` 127 | 128 | 129 | ### Get details about a specific NFT 130 | > Request 131 | ```graphql 132 | { 133 | nft(address: "3UF9qYsW9NNkUhAKtv42RZbWDVCiPRPW1FT3LY7RgcAP"){ 134 | address 135 | name 136 | } 137 | } 138 | ``` 139 | > Response 140 | ``` 141 | { 142 | "data": { 143 | "nft": { 144 | "address": "3UF9qYsW9NNkUhAKtv42RZbWDVCiPRPW1FT3LY7RgcAP", 145 | "name": "Whirlpools of Honey" 146 | } 147 | } 148 | } 149 | ``` 150 | 151 | As you can see with our two root queries you have the ability to quickly and effiecently find the data you're looking for. 152 | 153 | --- 154 | ## Marketplace Actions 155 | Using data retrieved from Holaplex API endpoint we are able to construct transactions to perform actions we’re interested in. Metaplex Foundation Programs power Holaplex marketplaces, but ultimately the data can be used in conjunction with any on-chain Program; open source or bespoke. 156 | 157 | ### Metaplex Foundation Auction House 158 | 159 | ``` 160 | AuctionHouse is a protocol for marketplaces to implement a decentralized sales contract. It is simple, fast and very cheap. 161 | 162 | AuctionHouse is a Solana program available on Mainnet Beta and Devnet. Anyone can create an AuctionHouse and accept any SPL token they wish. 163 | ``` 164 | 165 | > Source: [What is Auction House | Metaplex Docs](https://docs.metaplex.com/auction-house/definition) 166 | 167 | Metaplex also offers example TypeScript examples on constructing transaction instructions for the Auction House 168 | 169 | > **Auction House Program TypeScript Definition** - [metaplex-program-library/AuctionHouseProgram.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/AuctionHouseProgram.ts) 170 | 171 | ### Marketplace Actions 172 | 173 | The Auction House Program enables us to perform the following types of actions 174 | 175 | * **Sell NFT** - Sell a NFT you own - [instructions/sell.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/sell.ts) 176 | 177 | * **Buy NFT** - Purchase a NFT currently listed for sale - [instructions/buy.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/buy.ts) 178 | 179 | 180 | * **Make Offer** - Make an offer on a NFT, listed for sale or not - [instructions/buy.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/buy.ts) & [instructions/printBidReceipt.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/printBidReceipt.ts) 181 | 182 | * **Accept Offer** - Accept a buy offer on a NFT you own - [createSellInstruction](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/sell.ts) & [createPrintListingReceiptInstruction](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/printListingReceipt.ts) & [executePrintPurchaseReceiptInstruction](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/printPurchaseReceipt.ts) 183 | 184 | * **Cancel Listing** - [instructions/cancel.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/cancel.ts) & cancelListingReceiptInstruction 185 | 186 | * **Cancel Offer** - [instructions/cancel.ts](https://github.com/metaplex-foundation/metaplex-program-library/blob/master/auction-house/js/src/generated/instructions/cancel.ts) 187 | 188 | ### How do you perform them? 189 | Solana provides a javascript web3 library for data queries and sending transactions. [@solana/web3.js](https://solana-labs.github.io/solana-web3.js/) In conjunction with the solana library we’ll use the Auction House package. 190 | 191 | Sell NFT example 192 | ```TypeScript 193 | # Create Sell Instruction 194 | const sellInstruction = createSellInstruction(sellInstructionAccounts,sellInstructionArgs) 195 | 196 | # Create Receipt Instruction 197 | const printListingReceiptInstruction = createPrintListingReceiptInstruction(listingReceiptInstructionAccounts, listingReceiptInstructionArgs) 198 | 199 | # Create Transaction Object 200 | const tx = new Transaction() 201 | 202 | # Add instructions to Transaction 203 | tx.add(sellInstruction).add(printListingReceiptInstruction) 204 | 205 | # Get recent blockhash 206 | tx.recentBlockhash = (await connection.getRecentBlockhash()).blockhash 207 | 208 | # Set transaction fee payer 209 | tx.feePayer = publicKey 210 | 211 | # Sign tranaction 212 | signed = await signTransaction(tx); 213 | 214 | # Send tranaction and get its ID 215 | signature = await connection.sendRawTransaction(signed.serialize()); 216 | 217 | # Wait for tranaction to be confirmed 218 | await connection.confirmTransaction(signature, 'confirmed'); 219 | ``` -------------------------------------------------------------------------------- /src/modules/multi-transaction/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useState, useContext } from 'react' 2 | import Button, { ButtonSize } from '../../components/Button' 3 | import Modal from '../../components/Modal/Modal' 4 | import { errorCodeHelper } from '../../utils/errorCodes' 5 | 6 | type AsyncFunction = (arg?: any) => Promise 7 | export type Action = { 8 | name: string 9 | id: string 10 | action: AsyncFunction 11 | param: any 12 | } 13 | 14 | interface ActionSettings { 15 | onComplete?: () => Promise 16 | onActionSuccess?: (txName: string) => Promise 17 | onActionFailure?: (err: any) => Promise 18 | } 19 | 20 | interface MultiTransactionState { 21 | hasActionPending: boolean 22 | hasRemainingActions: boolean 23 | actions: Action[] 24 | runActions: (actions: Action[], settings?: ActionSettings) => Promise 25 | retryActions: (settings?: ActionSettings) => Promise 26 | clearActions: () => void 27 | onFinished?: () => Promise 28 | onStart?: () => Promise 29 | } 30 | 31 | const defaultState: MultiTransactionState = { 32 | hasRemainingActions: false, 33 | hasActionPending: false, 34 | actions: [], 35 | clearActions: () => {}, 36 | runActions: async ([]) => {}, 37 | retryActions: async () => {}, 38 | } 39 | 40 | export const MultiTransactionContext = 41 | createContext(defaultState) 42 | 43 | interface Props { 44 | children: React.ReactNode 45 | } 46 | 47 | export const MultiTransactionProvider: React.FC = ({ children }) => { 48 | const [hasActionPending, setHasActionPending] = useState(false) 49 | const [actions, setActions] = useState([]) 50 | const [numActions, setNumActions] = useState(0) 51 | const [hasRemainingActions, setHasRemainingActions] = useState(false) 52 | const [message, setMessage] = useState( 53 | `Sign the message in your wallet to continue` 54 | ) 55 | const [hasError, setHasError] = useState(false) 56 | 57 | const closeModal = () => { 58 | setHasRemainingActions(false) 59 | setActions([]) 60 | } 61 | 62 | const clearActions = () => { 63 | setActions([]) 64 | setHasActionPending(false) 65 | setNumActions(0) 66 | } 67 | 68 | const retryActions = async (settings?: ActionSettings) => { 69 | setNumActions(actions.length) 70 | 71 | if (actions.length <= 0) { 72 | // no actions 73 | return 74 | } 75 | if (!hasActionPending) { 76 | try { 77 | setHasActionPending(true) 78 | let filtered = actions 79 | for (const action of actions) { 80 | setMessage(action.name) 81 | await action.action(action.param) 82 | await settings?.onActionSuccess?.(action.id) 83 | // clear action 84 | filtered = filtered.filter((x) => x.id !== action.id) 85 | setActions(filtered) 86 | setHasError(false) 87 | } 88 | setHasRemainingActions(false) 89 | } catch (err: any) { 90 | const errorMsg: string = err.message 91 | if ( 92 | errorMsg.includes(`User rejected the request`) || 93 | errorMsg.includes(`was not confirmed`) || 94 | errorMsg.includes(`It is unknown if it succeeded or failed`) 95 | ) { 96 | setActions([]) 97 | setHasRemainingActions(false) 98 | } else { 99 | setHasError(true) 100 | } 101 | await settings?.onActionFailure?.(err) 102 | setHasActionPending(false) 103 | } finally { 104 | setHasActionPending(false) 105 | await settings?.onComplete?.() 106 | } 107 | } 108 | } 109 | 110 | const runActions = async ( 111 | newActions: Action[], 112 | settings?: ActionSettings 113 | ) => { 114 | if (hasRemainingActions) { 115 | throw new Error(`Has pending actions from a previous transaction`) 116 | } 117 | const newActionsWithIds: Action[] = newActions.map((action) => { 118 | return { 119 | ...action, 120 | } 121 | }) 122 | 123 | if (!hasActionPending && !hasRemainingActions) { 124 | // clears old actions if running without retry 125 | clearActions() 126 | setActions(newActionsWithIds) 127 | setNumActions(newActionsWithIds.length) 128 | 129 | if (newActionsWithIds.length <= 0) { 130 | // no actions 131 | return 132 | } 133 | try { 134 | setHasError(false) 135 | setHasRemainingActions(true) 136 | setHasActionPending(true) 137 | let filtered = newActionsWithIds 138 | for (const action of newActionsWithIds) { 139 | setMessage(action.name) 140 | await action.action(action.param) 141 | await settings?.onActionSuccess?.(action.id) 142 | // clear action 143 | filtered = filtered.filter((x) => x.id !== action.id) 144 | setActions(filtered) 145 | } 146 | setHasRemainingActions(false) 147 | } catch (err: any) { 148 | const errorMsg: string = err.message 149 | 150 | if ( 151 | errorMsg.includes(`User rejected the request`) || 152 | errorMsg.includes(`was not confirmed`) || 153 | errorMsg.includes(`It is unknown if it succeeded or failed`) 154 | ) { 155 | setActions([]) 156 | setHasRemainingActions(false) 157 | } else { 158 | setHasError(true) 159 | } 160 | await settings?.onActionFailure?.(errorCodeHelper(err.message)) 161 | setHasActionPending(false) 162 | } finally { 163 | setHasActionPending(false) 164 | await settings?.onComplete?.() 165 | } 166 | } 167 | } 168 | 169 | const completedActions = numActions - actions.length 170 | const percentage = 171 | numActions > 0 && completedActions < numActions 172 | ? ((completedActions + 1) / numActions) * 100 173 | : 0 174 | 175 | return ( 176 | 186 | 0} 189 | setOpen={closeModal} 190 | priority={true} 191 | > 192 |
193 |

{message}

194 |
195 |
196 |
197 |
201 |
204 |
205 |
206 |

207 | {completedActions} of {numActions} 208 |

209 | {hasError && ( 210 |
211 | 220 |
221 | )} 222 |
223 |
224 | 225 | {children} 226 |
227 | ) 228 | } 229 | 230 | export const useMultiTransactionModal = () => { 231 | const multiTransactionModal = useContext(MultiTransactionContext) 232 | if (!multiTransactionModal) { 233 | throw new Error( 234 | 'useMultiTransactionModal must be used within a MultiTransactionContext' 235 | ) 236 | } 237 | return multiTransactionModal 238 | } 239 | -------------------------------------------------------------------------------- /src/pages/admin/financials/edit.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useEffect, useMemo, useState } from 'react' 2 | import { NextPageContext } from 'next' 3 | import { gql } from '@apollo/client' 4 | import { isNil, pipe, zip, map, forEach, values } from 'ramda' 5 | import { TokenInfo } from '@solana/spl-token-registry' 6 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 7 | import { AppProps } from 'next/app' 8 | import client from './../../../client' 9 | import { initMarketplaceSDK, Marketplace } from '@holaplex/marketplace-js-sdk' 10 | import AdminMenu, { AdminMenuItemType } from '../../../components/AdminMenu' 11 | import { AdminLayout } from '../../../layouts/Admin' 12 | import { Wallet } from '@metaplex/js' 13 | import { AuctionHouse } from '@holaplex/marketplace-js-sdk/dist/types' 14 | import { useTokenList } from 'src/hooks/tokenList' 15 | import Price from 'src/components/Price' 16 | import { EmptyTreasuryWalletForm } from './../../../components/EmptyTreasuryWalletForm' 17 | 18 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 19 | 20 | interface GetMarketplace { 21 | marketplace: Marketplace | null 22 | } 23 | 24 | export async function getServerSideProps({ req }: NextPageContext) { 25 | const subdomain = req?.headers['x-holaplex-subdomain'] 26 | 27 | const { 28 | data: { marketplace }, 29 | } = await client.query({ 30 | fetchPolicy: 'no-cache', 31 | query: gql` 32 | query GetMarketplace($subdomain: String!) { 33 | marketplace(subdomain: $subdomain) { 34 | subdomain 35 | name 36 | description 37 | logoUrl 38 | bannerUrl 39 | ownerAddress 40 | creators { 41 | creatorAddress 42 | storeConfigAddress 43 | } 44 | auctionHouses { 45 | address 46 | treasuryMint 47 | auctionHouseTreasury 48 | treasuryWithdrawalDestination 49 | feeWithdrawalDestination 50 | authority 51 | creator 52 | auctionHouseFeeAccount 53 | bump 54 | treasuryBump 55 | feePayerBump 56 | sellerFeeBasisPoints 57 | requiresSignOff 58 | canChangeSalePrice 59 | } 60 | } 61 | } 62 | `, 63 | variables: { 64 | subdomain: subdomain || SUBDOMAIN, 65 | }, 66 | }) 67 | 68 | if (isNil(marketplace)) { 69 | return { 70 | notFound: true, 71 | } 72 | } 73 | 74 | return { 75 | props: { 76 | marketplace, 77 | }, 78 | } 79 | } 80 | 81 | interface AdminEditFinancialsProps extends AppProps { 82 | marketplace: Marketplace 83 | } 84 | 85 | const AdminEditFinancials = ({ marketplace }: AdminEditFinancialsProps) => { 86 | const { connection } = useConnection() 87 | const wallet = useWallet() 88 | const [loadingBalances, setLoadinBalances] = useState(true) 89 | const [tokenMap, loadingTokens] = useTokenList() 90 | const [balances, setBalances] = useState<{ 91 | [auctionHouse: string]: [AuctionHouse, TokenInfo | undefined, number] 92 | }>({}) 93 | const sdk = useMemo( 94 | () => initMarketplaceSDK(connection, wallet as Wallet), 95 | [connection, wallet] 96 | ) 97 | 98 | useEffect(() => { 99 | if (!marketplace.auctionHouses || loadingTokens) { 100 | return 101 | } 102 | 103 | ;(async () => { 104 | const next = { ...balances } 105 | 106 | const amounts = await Promise.all( 107 | marketplace.auctionHouses.map((auctionHouse) => { 108 | return sdk.treasury(auctionHouse).balance() 109 | }) 110 | ) 111 | 112 | pipe( 113 | zip(marketplace.auctionHouses), 114 | forEach(([auctionHouse, balance]: [AuctionHouse, number]) => { 115 | next[auctionHouse.address] = [ 116 | auctionHouse, 117 | tokenMap.get(auctionHouse.treasuryMint), 118 | balance, 119 | ] 120 | }) 121 | )(amounts) 122 | 123 | setBalances(next) 124 | setLoadinBalances(false) 125 | })() 126 | }, [marketplace.auctionHouses, loadingTokens]) 127 | 128 | const loading = loadingBalances || loadingTokens 129 | 130 | return ( 131 |
132 |
133 | {marketplace.name} 138 |
139 |
140 |
141 | 145 |
146 |
147 |
148 |
149 | 150 |
151 |
152 |
153 |
154 |
155 |
156 |

Transaction fees collected

157 |
158 |
159 | {loading ? ( 160 | <> 161 |
162 |
163 |
164 | 165 | ) : ( 166 | pipe( 167 | values, 168 | map( 169 | ([auctionHouse, token, amount]: [ 170 | AuctionHouse, 171 | TokenInfo | undefined, 172 | number 173 | ]) => { 174 | return ( 175 |
179 |
180 | 181 | {token?.symbol} Unredeemed 182 | 183 | 188 |
189 | { 191 | await sdk 192 | .transaction() 193 | .add( 194 | sdk 195 | .treasury(auctionHouse) 196 | .withdraw({ amount }) 197 | ) 198 | .send() 199 | 200 | const next = { ...balances } 201 | 202 | next[auctionHouse.address] = [ 203 | auctionHouse, 204 | token, 205 | 0, 206 | ] 207 | 208 | setBalances(next) 209 | }} 210 | token={token} 211 | /> 212 |
213 | ) 214 | } 215 | ) 216 | )(balances) 217 | )} 218 | {} 219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 | ) 227 | } 228 | 229 | interface AdminFinancialsLayoutProps { 230 | marketplace: Marketplace 231 | children: ReactElement 232 | } 233 | 234 | AdminEditFinancials.getLayout = function GetLayout({ 235 | marketplace, 236 | children, 237 | }: AdminFinancialsLayoutProps): ReactElement { 238 | return {children} 239 | } 240 | 241 | export default AdminEditFinancials 242 | -------------------------------------------------------------------------------- /src/pages/creators/index.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement } from 'react' 2 | import { gql, useQuery } from '@apollo/client' 3 | import { useWallet } from '@solana/wallet-adapter-react' 4 | import { NextPage, NextPageContext } from 'next' 5 | import { PublicKey } from '@solana/web3.js' 6 | import { AppProps } from 'next/app' 7 | import Head from 'next/head' 8 | import { equals, length, map, pipe, prop, when, isNil, always } from 'ramda' 9 | import Link from 'next/link' 10 | import client from '../../client' 11 | import cx from 'classnames' 12 | import { BasicLayout, NavigationLink } from './../../layouts/Basic' 13 | import { truncateAddress, addressAvatar } from '../../modules/address' 14 | import { Marketplace } from '@holaplex/marketplace-js-sdk' 15 | 16 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 17 | 18 | const GET_CREATORS_PREVIEW = gql` 19 | query GetCretorsPreview($subdomain: String!) { 20 | marketplace(subdomain: $subdomain) { 21 | subdomain 22 | ownerAddress 23 | creators { 24 | creatorAddress 25 | storeConfigAddress 26 | twitterHandle 27 | nftCount 28 | preview { 29 | address 30 | image 31 | } 32 | profile { 33 | handle 34 | profileImageUrlLowres 35 | } 36 | } 37 | } 38 | } 39 | ` 40 | 41 | export async function getServerSideProps({ req }: NextPageContext) { 42 | const subdomain = req?.headers['x-holaplex-subdomain'] || SUBDOMAIN 43 | 44 | const response = await client.query({ 45 | fetchPolicy: 'no-cache', 46 | query: gql` 47 | query GetMarketplacePage($subdomain: String!) { 48 | marketplace(subdomain: $subdomain) { 49 | subdomain 50 | name 51 | logoUrl 52 | bannerUrl 53 | description 54 | creators { 55 | creatorAddress 56 | storeConfigAddress 57 | } 58 | auctionHouses { 59 | authority 60 | } 61 | } 62 | } 63 | `, 64 | variables: { 65 | subdomain, 66 | }, 67 | }) 68 | 69 | const { 70 | data: { marketplace }, 71 | } = response 72 | 73 | if (isNil(marketplace)) { 74 | return { 75 | notFound: true, 76 | } 77 | } 78 | 79 | if (marketplace.creators && pipe(length, equals(1))(marketplace.creators)) { 80 | return { 81 | redirect: { 82 | permanent: false, 83 | destination: `/creators/${marketplace.creators[0].creatorAddress}`, 84 | }, 85 | } 86 | } 87 | 88 | return { 89 | props: { 90 | marketplace, 91 | }, 92 | } 93 | } 94 | 95 | interface GetMarketplace { 96 | marketplace: Marketplace | null 97 | } 98 | 99 | interface GetCreatorPreviews { 100 | marketplace: Marketplace 101 | } 102 | 103 | interface CreatorsPageProps extends AppProps { 104 | marketplace: Marketplace 105 | } 106 | 107 | const Creators: NextPage = ({ marketplace }) => { 108 | const { publicKey, connected } = useWallet() 109 | //const creators = map(prop('creatorAddress'))(marketplace.creators) 110 | 111 | const creatorsQuery = useQuery(GET_CREATORS_PREVIEW, { 112 | variables: { 113 | subdomain: marketplace.subdomain, 114 | }, 115 | }) 116 | 117 | const loading = creatorsQuery.loading 118 | 119 | return ( 120 | <> 121 | 122 | {`${marketplace.name} Creators`} 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 |
131 |

{`${marketplace.name} Creators`}

132 |
133 |
134 | {loading ? ( 135 | <> 136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | 232 | 233 | ) 234 | } 235 | 236 | interface CreatorsLayoutProps { 237 | marketplace: Marketplace 238 | children: ReactElement 239 | } 240 | 241 | Creators.getLayout = ({ marketplace, children }: CreatorsLayoutProps) => { 242 | return ( 243 | 244 | {children} 245 | 246 | ) 247 | } 248 | 249 | export default Creators 250 | -------------------------------------------------------------------------------- /src/pages/admin/creators/edit.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, useMemo, useState } from 'react' 2 | import { NextPageContext } from 'next' 3 | import { gql } from '@apollo/client' 4 | import { isNil, map, prop } from 'ramda' 5 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 6 | import { toast } from 'react-toastify' 7 | import { AppProps } from 'next/app' 8 | import { useForm, useFieldArray, Controller } from 'react-hook-form' 9 | import client from './../../../client' 10 | import Button, { ButtonSize, ButtonType } from '../../../components/Button' 11 | import { useLogin } from '../../../hooks/login' 12 | import { truncateAddress } from '../../../modules/address' 13 | import { AdminLayout } from '../../../layouts/Admin' 14 | import AdminMenu, { AdminMenuItemType } from '../../../components/AdminMenu' 15 | import { initMarketplaceSDK, Marketplace } from '@holaplex/marketplace-js-sdk' 16 | import { Wallet } from '@metaplex/js' 17 | 18 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 19 | 20 | interface GetMarketplace { 21 | marketplace: Marketplace | null 22 | } 23 | 24 | export async function getServerSideProps({ req }: NextPageContext) { 25 | const subdomain = req?.headers['x-holaplex-subdomain'] 26 | 27 | const { 28 | data: { marketplace }, 29 | } = await client.query({ 30 | fetchPolicy: 'no-cache', 31 | query: gql` 32 | query GetMarketplace($subdomain: String!) { 33 | marketplace(subdomain: $subdomain) { 34 | subdomain 35 | name 36 | description 37 | logoUrl 38 | bannerUrl 39 | ownerAddress 40 | creators { 41 | creatorAddress 42 | storeConfigAddress 43 | } 44 | auctionHouses { 45 | address 46 | treasuryMint 47 | auctionHouseTreasury 48 | treasuryWithdrawalDestination 49 | feeWithdrawalDestination 50 | authority 51 | creator 52 | auctionHouseFeeAccount 53 | bump 54 | treasuryBump 55 | feePayerBump 56 | sellerFeeBasisPoints 57 | requiresSignOff 58 | canChangeSalePrice 59 | } 60 | } 61 | } 62 | `, 63 | variables: { 64 | subdomain: subdomain || SUBDOMAIN, 65 | }, 66 | }) 67 | 68 | if (isNil(marketplace)) { 69 | return { 70 | notFound: true, 71 | } 72 | } 73 | 74 | return { 75 | props: { 76 | marketplace, 77 | }, 78 | } 79 | } 80 | 81 | interface AdminEditCreatorsProps extends AppProps { 82 | marketplace: Marketplace 83 | } 84 | 85 | interface MarketplaceForm { 86 | domain: string 87 | logo: { uri: string; type?: string; name?: string } 88 | banner: { uri: string; type?: string; name?: string } 89 | subdomain: string 90 | name: string 91 | description: string 92 | transactionFee: number 93 | creators: { address: string }[] 94 | creator: string 95 | } 96 | 97 | const AdminEditCreators = ({ marketplace }: AdminEditCreatorsProps) => { 98 | const wallet = useWallet() 99 | const { publicKey, signTransaction } = wallet 100 | const { connection } = useConnection() 101 | const auctionHouses = marketplace.auctionHouses?.map(({ address }) => ({ 102 | address: address, 103 | })) 104 | 105 | const login = useLogin() 106 | 107 | const sdk = useMemo( 108 | () => initMarketplaceSDK(connection, wallet as Wallet), 109 | [connection, wallet] 110 | ) 111 | 112 | const [showAdd, setShowAdd] = useState(false) 113 | 114 | const { 115 | register, 116 | control, 117 | handleSubmit, 118 | formState: { errors, isDirty, isSubmitting }, 119 | } = useForm({ 120 | defaultValues: { 121 | domain: `${marketplace.subdomain}.holaplex.market`, 122 | logo: { uri: marketplace.logoUrl }, 123 | banner: { uri: marketplace.bannerUrl }, 124 | subdomain: marketplace.subdomain, 125 | name: marketplace.name, 126 | description: marketplace.description, 127 | creators: marketplace.creators?.map(({ creatorAddress }) => ({ 128 | address: creatorAddress, 129 | })), 130 | transactionFee: marketplace.auctionHouses[0].sellerFeeBasisPoints, 131 | creator: '', 132 | }, 133 | }) 134 | 135 | const { fields, append, prepend, remove, swap, move, insert } = useFieldArray( 136 | { 137 | control, 138 | name: 'creators', 139 | } 140 | ) 141 | 142 | const onSubmit = async ({ 143 | name, 144 | banner, 145 | logo, 146 | description, 147 | transactionFee, 148 | creators, 149 | }: MarketplaceForm) => { 150 | if (!publicKey || !signTransaction || !wallet) { 151 | toast.error('Wallet not connected') 152 | 153 | login() 154 | 155 | return 156 | } 157 | 158 | toast('Saving changes...') 159 | 160 | const settings = { 161 | meta: { 162 | name, 163 | description, 164 | }, 165 | theme: { 166 | logo: { 167 | name: logo.name, 168 | type: logo.type, 169 | url: logo.uri, 170 | }, 171 | banner: { 172 | name: banner.name, 173 | type: banner.type, 174 | url: banner.uri, 175 | }, 176 | }, 177 | creators, 178 | subdomain: marketplace.subdomain, 179 | address: {}, 180 | auctionHouses: auctionHouses, 181 | } 182 | 183 | try { 184 | await sdk.transaction().add(sdk.update(settings, transactionFee)).send() 185 | 186 | toast.success( 187 | <> 188 | Marketplace updated successfully! It may take a few moments for the 189 | change to go live. 190 | , 191 | { autoClose: 5000 } 192 | ) 193 | } catch (e: any) { 194 | toast.error(e.message) 195 | } 196 | } 197 | 198 | return ( 199 |
200 |
201 | {marketplace.name} 206 |
207 |
208 |
209 | 213 |
214 |
215 |
216 |
217 | 218 |
219 |
220 |
221 |
222 |
223 |
224 |

Creators

225 |

226 | Manage the creators whose work will be available on your 227 | marketplace. 228 |

229 |
230 |
231 | 239 |
240 |
241 | {showAdd && ( 242 | { 246 | return ( 247 | <> 248 | 251 | { 254 | if (e.key !== 'Enter') { 255 | return 256 | } 257 | 258 | append({ address: value }) 259 | onChange('') 260 | }} 261 | placeholder="SOL wallet address" 262 | className="w-full px-3 py-2 mb-10 text-base text-gray-100 bg-gray-900 border border-gray-700 rounded-sm focus:outline-none" 263 | value={value} 264 | onChange={onChange} 265 | /> 266 | 267 | ) 268 | }} 269 | /> 270 | )} 271 |
    272 | {fields.map((field, index) => { 273 | return ( 274 |
  • 278 | {truncateAddress(field.address)} 279 | 285 |
  • 286 | ) 287 | })} 288 |
289 |
290 | {isDirty && ( 291 | 300 | )} 301 |
302 |
303 |
304 |
305 |
306 |
307 | ) 308 | } 309 | 310 | interface AdminEditCreatorsLayoutProps { 311 | marketplace: Marketplace 312 | children: ReactElement 313 | } 314 | 315 | AdminEditCreators.getLayout = function GetLayout({ 316 | marketplace, 317 | children, 318 | }: AdminEditCreatorsLayoutProps): ReactElement { 319 | return {children} 320 | } 321 | 322 | export default AdminEditCreators 323 | -------------------------------------------------------------------------------- /src/pages/nfts/[address].tsx: -------------------------------------------------------------------------------- 1 | import { gql, useQuery, QueryResult, OperationVariables } from '@apollo/client' 2 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 3 | import { useRouter } from 'next/router' 4 | import { NextPage, NextPageContext } from 'next' 5 | import { AppProps } from 'next/app' 6 | import { any, intersection, isEmpty, isNil, map, or, pipe, prop } from 'ramda' 7 | import { useForm } from 'react-hook-form' 8 | import Link from 'next/link' 9 | import { toast } from 'react-toastify' 10 | import { NftLayout } from './../../layouts/Nft' 11 | import client from '../../client' 12 | import Button, { ButtonType } from '../../components/Button' 13 | import { useLogin } from '../../hooks/login' 14 | import { Marketplace, Offer } from '@holaplex/marketplace-js-sdk' 15 | import { ReactElement, useContext, useMemo } from 'react' 16 | import { Wallet } from '@metaplex/js' 17 | import { 18 | Nft, 19 | AhListing, 20 | initMarketplaceSDK, 21 | GetNftData, 22 | } from '@holaplex/marketplace-js-sdk' 23 | import { 24 | Action, 25 | MultiTransactionContext, 26 | } from '../../modules/multi-transaction' 27 | 28 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 29 | 30 | interface GetNftPage { 31 | marketplace: Marketplace | null 32 | nft: Nft | null 33 | } 34 | 35 | const GET_NFT = gql` 36 | query GetNft($address: String!) { 37 | nft(address: $address) { 38 | name 39 | address 40 | image(width: 1400) 41 | sellerFeeBasisPoints 42 | mintAddress 43 | description 44 | primarySaleHappened 45 | category 46 | files { 47 | fileType 48 | uri 49 | } 50 | owner { 51 | address 52 | associatedTokenAccountAddress 53 | twitterHandle 54 | profile { 55 | handle 56 | profileImageUrlLowres 57 | } 58 | } 59 | attributes { 60 | traitType 61 | value 62 | } 63 | creators { 64 | address 65 | twitterHandle 66 | profile { 67 | handle 68 | profileImageUrlLowres 69 | } 70 | } 71 | offers { 72 | id 73 | tradeState 74 | price 75 | buyer 76 | createdAt 77 | auctionHouse { 78 | address 79 | treasuryMint 80 | auctionHouseTreasury 81 | treasuryWithdrawalDestination 82 | feeWithdrawalDestination 83 | authority 84 | creator 85 | auctionHouseFeeAccount 86 | bump 87 | treasuryBump 88 | feePayerBump 89 | sellerFeeBasisPoints 90 | requiresSignOff 91 | canChangeSalePrice 92 | } 93 | } 94 | activities { 95 | id 96 | metadata 97 | auctionHouse { 98 | address 99 | treasuryMint 100 | } 101 | price 102 | createdAt 103 | wallets { 104 | address 105 | profile { 106 | handle 107 | profileImageUrlLowres 108 | } 109 | } 110 | activityType 111 | } 112 | listings { 113 | id 114 | auctionHouse { 115 | address 116 | treasuryMint 117 | auctionHouseTreasury 118 | treasuryWithdrawalDestination 119 | feeWithdrawalDestination 120 | authority 121 | creator 122 | auctionHouseFeeAccount 123 | bump 124 | treasuryBump 125 | feePayerBump 126 | sellerFeeBasisPoints 127 | requiresSignOff 128 | canChangeSalePrice 129 | } 130 | seller 131 | metadata 132 | price 133 | tokenSize 134 | tradeState 135 | tradeStateBump 136 | createdAt 137 | canceledAt 138 | } 139 | } 140 | } 141 | ` 142 | 143 | export async function getServerSideProps({ req, query }: NextPageContext) { 144 | const subdomain = req?.headers['x-holaplex-subdomain'] 145 | 146 | const { 147 | data: { marketplace, nft }, 148 | } = await client.query({ 149 | fetchPolicy: 'no-cache', 150 | query: gql` 151 | query GetNftPage($subdomain: String!, $address: String!) { 152 | marketplace(subdomain: $subdomain) { 153 | subdomain 154 | name 155 | description 156 | logoUrl 157 | bannerUrl 158 | ownerAddress 159 | creators { 160 | creatorAddress 161 | storeConfigAddress 162 | } 163 | auctionHouses { 164 | address 165 | treasuryMint 166 | auctionHouseTreasury 167 | treasuryWithdrawalDestination 168 | feeWithdrawalDestination 169 | authority 170 | creator 171 | auctionHouseFeeAccount 172 | bump 173 | treasuryBump 174 | feePayerBump 175 | sellerFeeBasisPoints 176 | requiresSignOff 177 | canChangeSalePrice 178 | } 179 | } 180 | nft(address: $address) { 181 | address 182 | image 183 | name 184 | description 185 | mintAddress 186 | owner { 187 | associatedTokenAccountAddress 188 | } 189 | creators { 190 | address 191 | } 192 | } 193 | } 194 | `, 195 | variables: { 196 | subdomain: subdomain || SUBDOMAIN, 197 | address: query?.address, 198 | }, 199 | }) 200 | 201 | const nftCreatorAddresses = map(prop('address'))(nft?.creators || []) 202 | const marketplaceCreatorAddresses = map(prop('creatorAddress'))( 203 | marketplace?.creators || [] 204 | ) 205 | const notAllowed = pipe( 206 | intersection(marketplaceCreatorAddresses), 207 | isEmpty 208 | )(nftCreatorAddresses) 209 | 210 | if (or(any(isNil)([marketplace, nft]), notAllowed)) { 211 | return { 212 | notFound: true, 213 | } 214 | } 215 | 216 | return { 217 | props: { 218 | marketplace, 219 | nft, 220 | }, 221 | } 222 | } 223 | 224 | interface NftPageProps extends AppProps { 225 | isOwner: boolean 226 | offer: Offer 227 | listing: AhListing 228 | nft: Nft 229 | nftQuery: QueryResult 230 | } 231 | 232 | const NftShow: NextPage = ({ 233 | nft, 234 | isOwner, 235 | offer, 236 | listing, 237 | nftQuery, 238 | }) => { 239 | const cancelListingForm = useForm() 240 | const buyNowForm = useForm() 241 | const wallet = useWallet() 242 | const { publicKey, signTransaction } = wallet 243 | const { connection } = useConnection() 244 | const login = useLogin() 245 | const sdk = useMemo( 246 | () => initMarketplaceSDK(connection, wallet as Wallet), 247 | [connection, wallet] 248 | ) 249 | const { runActions } = useContext(MultiTransactionContext) 250 | 251 | const onMakeOffer = async () => { 252 | const auctionHouse = listing.auctionHouse 253 | 254 | if (!listing || !nft || !auctionHouse) { 255 | return 256 | } 257 | 258 | toast('Sending the transaction to Solana.') 259 | 260 | await sdk 261 | .transaction() 262 | .add( 263 | sdk.escrow(auctionHouse).desposit({ amount: listing.price.toNumber() }) 264 | ) 265 | .add( 266 | sdk.offers(auctionHouse).make({ 267 | amount: listing.price.toNumber(), 268 | nft, 269 | }) 270 | ) 271 | .send() 272 | } 273 | 274 | const onBuy = async () => { 275 | if (!listing || !listing.auctionHouse || !nft) { 276 | return 277 | } 278 | 279 | toast('Sending the transaction to Solana.') 280 | await sdk 281 | .transaction() 282 | .add( 283 | sdk.listings(listing.auctionHouse).buy({ 284 | listing, 285 | nft, 286 | }) 287 | ) 288 | .send() 289 | } 290 | 291 | const buyNftTransaction = async () => { 292 | if (!publicKey || !signTransaction) { 293 | login() 294 | return 295 | } 296 | if (!listing || isOwner) { 297 | return 298 | } 299 | 300 | const newActions: Action[] = [ 301 | { 302 | name: `Funding escrow...`, 303 | id: 'offerNFT', 304 | action: onMakeOffer, 305 | param: undefined, 306 | }, 307 | { 308 | name: `Buying ${nft.name}...`, 309 | id: 'buyNFT', 310 | action: onBuy, 311 | param: undefined, 312 | }, 313 | ] 314 | 315 | await runActions(newActions, { 316 | onActionSuccess: async () => { 317 | toast.success('The transaction was confirmed.') 318 | }, 319 | onComplete: async () => { 320 | await nftQuery.refetch() 321 | }, 322 | onActionFailure: async (err) => { 323 | toast.error(err.message) 324 | }, 325 | }) 326 | } 327 | 328 | const cancelListingTransaction = async () => { 329 | if (!publicKey || !signTransaction) { 330 | login() 331 | return 332 | } 333 | 334 | if (!listing || !isOwner) { 335 | return 336 | } 337 | 338 | try { 339 | toast('Sending the transaction to Solana.') 340 | 341 | await sdk 342 | .transaction() 343 | .add( 344 | sdk.listings(listing?.auctionHouse).cancel({ 345 | listing, 346 | nft, 347 | }) 348 | ) 349 | .send() 350 | 351 | toast.success('The transaction was confirmed.') 352 | await nftQuery.refetch() 353 | } catch (e: any) { 354 | toast.error(e.message) 355 | } 356 | } 357 | 358 | return ( 359 | <> 360 | {!isOwner && !offer && ( 361 | 362 | 363 | 366 | 367 | 368 | )} 369 | {isOwner && !listing && ( 370 | 371 | 372 | 373 | 374 | 375 | )} 376 | {listing && !isOwner && ( 377 |
381 | 382 | 389 | 390 |
391 | )} 392 | {listing && isOwner && ( 393 |
397 | 405 |
406 | )} 407 | 408 | ) 409 | } 410 | 411 | interface NftShowLayoutProps { 412 | marketplace: Marketplace 413 | nft: Nft 414 | children: ReactElement 415 | } 416 | 417 | NftShow.getLayout = function NftShowLayout({ 418 | marketplace, 419 | nft, 420 | children, 421 | }: NftShowLayoutProps) { 422 | const router = useRouter() 423 | 424 | const nftQuery = useQuery(GET_NFT, { 425 | client, 426 | variables: { 427 | address: router.query?.address, 428 | }, 429 | }) 430 | 431 | return ( 432 | 433 | {children} 434 | 435 | ) 436 | } 437 | 438 | export default NftShow 439 | -------------------------------------------------------------------------------- /src/pages/admin/marketplace/edit.tsx: -------------------------------------------------------------------------------- 1 | import { NextPageContext } from 'next' 2 | import { gql } from '@apollo/client' 3 | import { isNil, not, or } from 'ramda' 4 | import { useConnection, useWallet } from '@solana/wallet-adapter-react' 5 | import { toast } from 'react-toastify' 6 | import { AppProps } from 'next/app' 7 | import { useForm, Controller } from 'react-hook-form' 8 | import client from './../../../client' 9 | import UploadFile from './../../../../src/components/UploadFile' 10 | import Link from 'next/link' 11 | import Button, { ButtonSize, ButtonType } from '../../../components/Button' 12 | import { useLogin } from '../../../hooks/login' 13 | import { AdminLayout } from 'src/layouts/Admin' 14 | import AdminMenu, { AdminMenuItemType } from '../../../components/AdminMenu' 15 | import { ReactElement, useMemo } from 'react' 16 | import { Wallet } from '@metaplex/js' 17 | import { initMarketplaceSDK, Marketplace } from '@holaplex/marketplace-js-sdk' 18 | 19 | const SUBDOMAIN = process.env.MARKETPLACE_SUBDOMAIN 20 | 21 | interface GetMarketplace { 22 | marketplace: Marketplace | null 23 | } 24 | 25 | export async function getServerSideProps({ req }: NextPageContext) { 26 | const subdomain = req?.headers['x-holaplex-subdomain'] 27 | 28 | const { 29 | data: { marketplace }, 30 | } = await client.query({ 31 | fetchPolicy: 'no-cache', 32 | query: gql` 33 | query GetMarketplace($subdomain: String!) { 34 | marketplace(subdomain: $subdomain) { 35 | subdomain 36 | name 37 | description 38 | logoUrl 39 | bannerUrl 40 | ownerAddress 41 | creators { 42 | creatorAddress 43 | storeConfigAddress 44 | } 45 | auctionHouses { 46 | address 47 | treasuryMint 48 | auctionHouseTreasury 49 | treasuryWithdrawalDestination 50 | feeWithdrawalDestination 51 | authority 52 | creator 53 | auctionHouseFeeAccount 54 | bump 55 | treasuryBump 56 | feePayerBump 57 | sellerFeeBasisPoints 58 | requiresSignOff 59 | canChangeSalePrice 60 | } 61 | } 62 | } 63 | `, 64 | variables: { 65 | subdomain: subdomain || SUBDOMAIN, 66 | }, 67 | }) 68 | 69 | if (isNil(marketplace)) { 70 | return { 71 | notFound: true, 72 | } 73 | } 74 | 75 | return { 76 | props: { 77 | marketplace, 78 | }, 79 | } 80 | } 81 | 82 | interface AdminEditMarketplaceProps extends AppProps { 83 | marketplace: Marketplace 84 | } 85 | 86 | interface MarketplaceForm { 87 | domain: string 88 | logo: { uri: string; type?: string; name?: string } 89 | banner: { uri: string; type?: string; name?: string } 90 | subdomain: string 91 | name: string 92 | description: string 93 | transactionFee: number 94 | creators: { address: string }[] 95 | } 96 | 97 | const AdminEditMarketplace = ({ marketplace }: AdminEditMarketplaceProps) => { 98 | const wallet = useWallet() 99 | const { publicKey, signTransaction } = wallet 100 | const { connection } = useConnection() 101 | const login = useLogin() 102 | const auctionHouses = marketplace.auctionHouses?.map(({ address }) => ({ 103 | address: address, 104 | })) 105 | const sdk = useMemo( 106 | () => initMarketplaceSDK(connection, wallet as Wallet), 107 | [connection, wallet] 108 | ) 109 | const { 110 | register, 111 | control, 112 | handleSubmit, 113 | formState: { errors, isDirty, isSubmitting }, 114 | } = useForm({ 115 | defaultValues: { 116 | domain: `${marketplace.subdomain}.holaplex.market`, 117 | logo: { uri: marketplace.logoUrl }, 118 | banner: { uri: marketplace.bannerUrl }, 119 | subdomain: marketplace.subdomain, 120 | name: marketplace.name, 121 | description: marketplace.description, 122 | creators: marketplace.creators?.map(({ creatorAddress }) => ({ 123 | address: creatorAddress, 124 | })), 125 | transactionFee: marketplace.auctionHouses[0].sellerFeeBasisPoints, 126 | }, 127 | }) 128 | 129 | const onSubmit = async ({ 130 | name, 131 | banner, 132 | logo, 133 | description, 134 | transactionFee, 135 | creators, 136 | }: MarketplaceForm) => { 137 | if (!publicKey || !signTransaction || !wallet) { 138 | toast.error('Wallet not connected') 139 | 140 | login() 141 | 142 | return 143 | } 144 | 145 | toast('Saving changes...') 146 | 147 | const settings = { 148 | meta: { 149 | name, 150 | description, 151 | }, 152 | theme: { 153 | logo: { 154 | name: logo.name, 155 | type: logo.type, 156 | url: logo.uri, 157 | }, 158 | banner: { 159 | name: banner.name, 160 | type: banner.type, 161 | url: banner.uri, 162 | }, 163 | }, 164 | creators, 165 | subdomain: marketplace.subdomain, 166 | address: {}, 167 | auctionHouses: auctionHouses, 168 | } 169 | 170 | try { 171 | await sdk.transaction().add(sdk.update(settings, transactionFee)).send() 172 | 173 | toast.success( 174 | <> 175 | Marketplace updated successfully! It may take a few moments for the 176 | change to go live. 177 | , 178 | { autoClose: 5000 } 179 | ) 180 | } catch (e: any) { 181 | toast.error(e.message) 182 | } 183 | } 184 | 185 | return ( 186 |
187 |
188 | ( 192 | <> 193 | {marketplace.name} 198 |
199 | 200 |
201 | 202 | )} 203 | /> 204 |
205 |
206 |
207 | { 211 | return ( 212 | <> 213 | 217 |
218 | 219 |
220 | 221 | ) 222 | }} 223 | /> 224 |
225 | 226 |
227 |
228 |
229 | 230 |
231 |
232 |
233 |
234 |
235 |

236 | Edit marketplace 237 |

238 |
239 | 240 | 248 | 249 | 259 |
260 |
261 |
262 | 263 | 264 | Your domain is managed by Holaplex. If you need to change it, 265 | please{' '} 266 | 270 | contact us. 271 | 272 | 273 | 277 | {errors.domain && This field is required} 278 | 279 | 280 | 284 | {errors.name && This field is required} 285 | 286 | 287 |