├── public ├── robots.txt ├── favicon.ico ├── index.html └── media │ └── logo.svg ├── CODEOWNERS ├── src ├── pages │ ├── ProjectsPage.tsx │ ├── UserPage.tsx │ ├── TokenPage.tsx │ ├── LandingPage.tsx │ └── ProjectPage.tsx ├── index.tsx ├── utils │ ├── contractInfoHelper.ts │ ├── numbers.ts │ ├── scriptJSON.ts │ ├── types.ts │ └── getMintingInterface.ts ├── components │ ├── Connect.tsx │ ├── MintingCountdown.tsx │ ├── Page.tsx │ ├── Address.tsx │ ├── TokenImage.tsx │ ├── Loading.tsx │ ├── ProjectDate.tsx │ ├── MintingButton.tsx │ ├── MintingProgress.tsx │ ├── EditProjectButton.tsx │ ├── Collapsible.tsx │ ├── TokenView.tsx │ ├── ProjectStatusBadge.tsx │ ├── ProjectPreview.tsx │ ├── MintingPrice.tsx │ ├── MintingInterfaceFilter.tsx │ ├── TokenTraits.tsx │ ├── Providers.tsx │ ├── Tokens.tsx │ ├── TokenLive.tsx │ ├── ProjectExplore.tsx │ ├── OwnedTokens.tsx │ ├── MinterInterfaces │ │ ├── GenArt721MinterInterface.tsx │ │ ├── MinterSetPriceERC20V4Interface.tsx │ │ ├── MinterSetPriceV4Interface.tsx │ │ ├── MinterHolderV4Interface.tsx │ │ ├── MinterDAExpV4Interface.tsx │ │ ├── MinterMerkleV5Interface.tsx │ │ └── MinterDAExpSettlementV1Interface.tsx │ ├── Header.tsx │ ├── Projects.tsx │ ├── MinterButtons │ │ ├── MinterSetPriceV4Button.tsx │ │ ├── MinterDAExpV4Button.tsx │ │ ├── MinterHolderV4Button.tsx │ │ ├── MinterMerkleV5Button.tsx │ │ ├── MinterDAExpSettlementV1Button.tsx │ │ ├── MinterSetPriceERC20V4Button.tsx │ │ └── GenArt721MinterButton.tsx │ ├── OwnedProjects.tsx │ ├── TokenDetails.tsx │ └── ProjectDetails.tsx ├── index.css ├── hooks │ ├── useCountOwnedTokens.tsx │ ├── useCountProjects.tsx │ ├── useToken.tsx │ ├── useInterval.tsx │ ├── useWindowSize.tsx │ ├── useCountOwnedProjects.tsx │ ├── useTokenTraits.tsx │ ├── useTokens.tsx │ ├── useOwnedTokens.tsx │ ├── useProject.tsx │ ├── useGeneratorPreview.tsx │ ├── useProjects.tsx │ └── useOwnedProjects.tsx ├── config.ts ├── App.tsx ├── theme.tsx ├── contractConfig.ts └── abi │ ├── ERC20.json │ ├── V3 │ └── MinterFilterV1.json │ └── V2 │ └── GenArt721MintV2.json ├── .gitignore ├── sample.env ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ArtBlocks/artblocks-engine-react/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Request review from the best fit approvers group for this repo. 2 | * @ArtBlocks/Eng-Approvers-Backend 3 | -------------------------------------------------------------------------------- /src/pages/ProjectsPage.tsx: -------------------------------------------------------------------------------- 1 | import Page from "components/Page" 2 | import Projects from "components/Projects" 3 | 4 | const ProjectsPage = () => { 5 | return ( 6 | 7 | 8 | 9 | ) 10 | } 11 | 12 | export default ProjectsPage -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | /.pnp 4 | .pnp.js 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env 15 | *.env 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | !sample.env 21 | 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom/client" 3 | import "./index.css" 4 | import App from "./App" 5 | 6 | window.Buffer = window.Buffer || require("buffer").Buffer 7 | 8 | const root = ReactDOM.createRoot( 9 | document.getElementById("root") as HTMLElement 10 | ) 11 | 12 | root.render( 13 | 14 | 15 | 16 | ) 17 | -------------------------------------------------------------------------------- /src/utils/contractInfoHelper.ts: -------------------------------------------------------------------------------- 1 | import { CONTRACT_INFO } from "config" 2 | 3 | export const getContractConfigByAddress = (contractAddress: string) => { 4 | return CONTRACT_INFO.find( 5 | x => x.CORE_CONTRACT_ADDRESS.toLowerCase() === contractAddress.toLowerCase() 6 | ) 7 | } 8 | 9 | export const getConfiguredContractAddresses = () => { 10 | return CONTRACT_INFO.map(x => x.CORE_CONTRACT_ADDRESS) 11 | } 12 | -------------------------------------------------------------------------------- /src/pages/UserPage.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom" 2 | import Page from "components/Page" 3 | import OwnedProjects from "components/OwnedProjects" 4 | 5 | const UserPage = () => { 6 | const { walletAddress } = useParams() 7 | return ( 8 | 9 | { 10 | walletAddress && 11 | } 12 | 13 | ) 14 | } 15 | 16 | export default UserPage 17 | -------------------------------------------------------------------------------- /src/utils/numbers.ts: -------------------------------------------------------------------------------- 1 | import { utils, BigNumber } from "ethers" 2 | 3 | export const multiplyBigNumberByFloat = function(x: BigNumber, y: number) { 4 | return BigNumber.from(Math.floor(x.toNumber()*y)) 5 | } 6 | 7 | export const formatEtherFixed = function(priceWei: string, fractionDigits: number) { 8 | const priceEther = utils.formatEther(priceWei) 9 | const priceEtherFixed = parseFloat(priceEther).toFixed(fractionDigits) 10 | return priceEtherFixed 11 | } -------------------------------------------------------------------------------- /src/components/Connect.tsx: -------------------------------------------------------------------------------- 1 | import { ConnectButton } from "@rainbow-me/rainbowkit" 2 | import Box from "@mui/material/Box" 3 | 4 | const Connect = () => { 5 | return ( 6 | 7 | 15 | 16 | ) 17 | } 18 | 19 | export default Connect 20 | -------------------------------------------------------------------------------- /src/pages/TokenPage.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom" 2 | import Page from "components/Page" 3 | import TokenDetails from "components/TokenDetails" 4 | 5 | const TokenPage = () => { 6 | const { contractAddress, id } = useParams() 7 | return ( 8 | 9 | { 10 | contractAddress && id && 11 | } 12 | 13 | ) 14 | } 15 | 16 | export default TokenPage 17 | -------------------------------------------------------------------------------- /src/components/MintingCountdown.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from "@mui/material" 2 | 3 | interface Props { 4 | auctionStartFormatted: any, 5 | auctionStartCountdown: any 6 | } 7 | 8 | const MintingCountdown = ({auctionStartFormatted, auctionStartCountdown}: Props) => { 9 | return ( 10 | 11 | Live: {auctionStartFormatted} ({auctionStartCountdown}) 12 | 13 | ) 14 | } 15 | 16 | export default MintingCountdown -------------------------------------------------------------------------------- /src/pages/LandingPage.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Typography 4 | } from "@mui/material" 5 | import Page from "components/Page" 6 | 7 | const LandingPage = () => { 8 | return ( 9 | 10 | 11 | ArtBlocks Engine 12 | Template Project 13 | 14 | 15 | ) 16 | } 17 | 18 | export default LandingPage 19 | -------------------------------------------------------------------------------- /src/pages/ProjectPage.tsx: -------------------------------------------------------------------------------- 1 | import { useParams } from "react-router-dom" 2 | import Page from "components/Page" 3 | import ProjectDetails from "components/ProjectDetails" 4 | 5 | const ProjectPage = () => { 6 | const { contractAddress, projectId } = useParams() 7 | return ( 8 | 9 | { 10 | contractAddress && projectId && 11 | } 12 | 13 | ) 14 | } 15 | 16 | export default ProjectPage 17 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | Engine 12 | 13 | 14 | 15 |
16 | 17 | -------------------------------------------------------------------------------- /sample.env: -------------------------------------------------------------------------------- 1 | # MAINNET 2 | #REACT_APP_EXPECTED_CHAIN_ID=1 3 | #REACT_APP_GRAPHQL_URL=https://api.thegraph.com/subgraphs/name/artblocks/art-blocks 4 | 5 | # TESTNET 6 | REACT_APP_EXPECTED_CHAIN_ID=3 7 | REACT_APP_GRAPHQL_URL=https://api.thegraph.com/subgraphs/name/artblocks/art-blocks-artist-staging-goerli 8 | 9 | REACT_APP_WALLET_CONNECT_PROJECT_ID=xyz789 10 | REACT_APP_INFURA_KEY=abc123 11 | REACT_APP_MERKLE_PROOF_API_URL=https://media.plottables.io/api/getMerkleProof 12 | REACT_APP_HOLDER_PROOF_API_URL=https://media.plottables.io/api/getHolderProof 13 | -------------------------------------------------------------------------------- /src/components/Page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Container, 3 | Box 4 | } from "@mui/material" 5 | 6 | import Header from "components/Header" 7 | 8 | interface Props { 9 | children: React.ReactNode 10 | } 11 | 12 | const Page = ({ children }: Props) => { 13 | return ( 14 | 15 |
16 |
17 | 18 | {children} 19 | 20 |
21 | 22 | ) 23 | } 24 | 25 | export default Page 26 | -------------------------------------------------------------------------------- /src/components/Address.tsx: -------------------------------------------------------------------------------- 1 | import { useEnsName } from "wagmi" 2 | import Tooltip from "@mui/material/Tooltip" 3 | 4 | interface Props { 5 | address?: any 6 | } 7 | 8 | const Address = ({ address }: Props) => { 9 | const ensName = useEnsName({ 10 | address: address, 11 | chainId: 1 12 | }) 13 | 14 | const shortAddress = address ? `${address.slice(0, 6)}...${ address.slice(38, 42)}` : null 15 | 16 | return ( 17 | address !== null ? 18 | 19 | {ensName.data || shortAddress} 20 | 21 | : null 22 | ) 23 | } 24 | 25 | export default Address -------------------------------------------------------------------------------- /src/components/TokenImage.tsx: -------------------------------------------------------------------------------- 1 | import { getContractConfigByAddress } from "utils/contractInfoHelper"; 2 | 3 | interface Props { 4 | contractAddress: string 5 | tokenId: string 6 | width: number 7 | height: number 8 | } 9 | 10 | const TokenImage = ({contractAddress, tokenId, width, height}: Props) => { 11 | const contractConfig = getContractConfigByAddress(contractAddress) 12 | 13 | return ( 14 | {tokenId} 20 | ) 21 | } 22 | 23 | export default TokenImage 24 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Archivo+Black&display=swap"); 2 | @import url("https://fonts.googleapis.com/css2?family=Raleway:wght@400;500;600&display=swap"); 3 | 4 | body { 5 | margin: 0; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | font-family: "Geometric"; 9 | } 10 | 11 | code { 12 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 13 | } 14 | 15 | a, a:visited { 16 | color: "black"; 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | .mobile-link:hover { 25 | text-decoration: none; 26 | } -------------------------------------------------------------------------------- /src/hooks/useCountOwnedTokens.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | 3 | const countOwnedTokensQuery = (projectId: string, walletAddress: string) => ` 4 | query GetTokens { 5 | tokens( 6 | where: { 7 | project: "${projectId}" 8 | owner: "${walletAddress}" 9 | } 10 | ) { 11 | id 12 | } 13 | }` 14 | 15 | const useCountOwnedTokens = (projectId: string, walletAddress: string) => { 16 | const { loading, error, data } = useQuery(gql(countOwnedTokensQuery(projectId, walletAddress))) 17 | 18 | return { 19 | loading, 20 | error, 21 | data 22 | } 23 | } 24 | 25 | export default useCountOwnedTokens 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "baseUrl": "./src", 11 | "skipLibCheck": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "strict": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "module": "esnext", 18 | "moduleResolution": "node", 19 | "resolveJsonModule": true, 20 | "isolatedModules": true, 21 | "noEmit": true, 22 | "jsx": "react-jsx" 23 | }, 24 | "include": [ 25 | "src" 26 | ] 27 | } -------------------------------------------------------------------------------- /src/hooks/useCountProjects.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | import { getConfiguredContractAddresses } from "utils/contractInfoHelper" 3 | 4 | const countProjectsQuery = () => ` 5 | query GetProjects { 6 | projects( 7 | where: { 8 | contract_in: ["${getConfiguredContractAddresses().join("\",\"").toLowerCase()}"] 9 | active: true 10 | } 11 | ) { 12 | id 13 | } 14 | }` 15 | 16 | const useCountProjects = () => { 17 | const { loading, error, data } = useQuery(gql(countProjectsQuery())) 18 | 19 | return { 20 | loading, 21 | error, 22 | data 23 | } 24 | } 25 | 26 | export default useCountProjects 27 | -------------------------------------------------------------------------------- /src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { 3 | Box, 4 | CircularProgress 5 | } from "@mui/material" 6 | import { Container } from "@mui/system" 7 | import useInterval from "hooks/useInterval" 8 | 9 | const Loading = () => { 10 | const [waitTime, setWaitTime] = useState(0) 11 | 12 | useInterval(() => { 13 | setWaitTime(waitTime+1) 14 | }, 1000) 15 | 16 | return ( 17 | 18 | { 19 | waitTime > 0 && 20 | ( 21 | 22 | 23 | 24 | ) 25 | } 26 | 27 | ) 28 | } 29 | 30 | export default Loading -------------------------------------------------------------------------------- /src/hooks/useToken.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | 3 | const tokenQuery = (id: string) => ` 4 | query GetToken { 5 | token( 6 | id: "${id}" 7 | ) { 8 | id 9 | tokenId 10 | invocation 11 | createdAt 12 | uri 13 | owner { 14 | id 15 | } 16 | project { 17 | id 18 | projectId 19 | name 20 | artistName 21 | artistAddress 22 | scriptJSON 23 | } 24 | } 25 | }` 26 | 27 | const useToken = (id: string) => { 28 | const { loading, error, data } = useQuery(gql(tokenQuery(id))) 29 | 30 | return { 31 | loading, 32 | error, 33 | data 34 | } 35 | } 36 | 37 | export default useToken 38 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { mainnetContractConfig, testnetContractConfig } from "./contractConfig"; 2 | 3 | export const EXPECTED_CHAIN_ID = Number(process.env.REACT_APP_EXPECTED_CHAIN_ID) 4 | export const GRAPHQL_URL = process.env.REACT_APP_GRAPHQL_URL 5 | export const WALLET_CONNECT_PROJECT_ID = process.env.REACT_APP_WALLET_CONNECT_PROJECT_ID || "" 6 | export const INFURA_KEY = process.env.REACT_APP_INFURA_KEY || "" 7 | export const PROJECTS_PER_PAGE = 8 8 | export const TOKENS_PER_PAGE = 9 9 | export const MULTIPLY_GAS_LIMIT = 1.1 10 | export const CONTRACT_INFO = EXPECTED_CHAIN_ID === 1 ? mainnetContractConfig : testnetContractConfig 11 | export const MERKLE_PROOF_API_URL = process.env.REACT_APP_MERKLE_PROOF_API_URL || "" 12 | export const HOLDER_PROOF_API_URL = process.env.REACT_APP_HOLDER_PROOF_API_URL || "" 13 | -------------------------------------------------------------------------------- /src/components/ProjectDate.tsx: -------------------------------------------------------------------------------- 1 | import moment from "moment" 2 | import { 3 | Box, 4 | Typography 5 | } from "@mui/material" 6 | 7 | interface Props { 8 | startTime?: BigInt 9 | } 10 | 11 | const ProjectDate = ({ startTime }: Props) => { 12 | const startDate = startTime ? moment.unix(parseInt(startTime.toString())) : null 13 | 14 | return ( 15 | 16 | { 17 | startDate ? 18 | ( 19 | 20 | {startDate.isBefore() ? "Launched" : ""} {startDate.format("LL")} 21 | 22 | ) : 23 | ( 24 | 25 |
26 |
27 | ) 28 | } 29 |
30 | ) 31 | } 32 | 33 | export default ProjectDate 34 | -------------------------------------------------------------------------------- /src/components/MintingButton.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Typography } from "@mui/material" 2 | 3 | interface Props { 4 | disabled: boolean, 5 | message: string, 6 | contractPurchase: any 7 | } 8 | 9 | const MintingButton = ({disabled, message, contractPurchase}: Props) => { 10 | return ( 11 | 29 | ) 30 | } 31 | 32 | export default MintingButton 33 | -------------------------------------------------------------------------------- /src/hooks/useInterval.tsx: -------------------------------------------------------------------------------- 1 | // Source: https://usehooks-ts.com/react-hook/use-interval 2 | import { useEffect, useRef } from "react" 3 | import { useIsomorphicLayoutEffect } from "usehooks-ts" 4 | 5 | function useInterval(callback: () => void, delay: number | null) { 6 | const savedCallback = useRef(callback) 7 | 8 | // Remember the latest callback if it changes. 9 | useIsomorphicLayoutEffect(() => { 10 | savedCallback.current = callback 11 | }, [callback]) 12 | 13 | // Set up the interval. 14 | useEffect(() => { 15 | // Don"t schedule if no delay is specified. 16 | // Note: 0 is a valid value for delay. 17 | if (!delay && delay !== 0) { 18 | return 19 | } 20 | 21 | const id = setInterval(() => savedCallback.current(), delay) 22 | 23 | return () => clearInterval(id) 24 | }, [delay]) 25 | } 26 | 27 | export default useInterval -------------------------------------------------------------------------------- /src/utils/scriptJSON.ts: -------------------------------------------------------------------------------- 1 | export const parseJson = (json: string) => { 2 | try { 3 | return JSON.parse(json) 4 | } catch (error) { 5 | return null 6 | } 7 | } 8 | 9 | export const parseAspectRatio = (scriptJSON:string) => { 10 | const scriptParams = parseJson(scriptJSON) 11 | 12 | if (!scriptParams) { 13 | return 1 14 | } 15 | 16 | const { aspectRatio } = scriptParams 17 | 18 | if (typeof aspectRatio === "string") { 19 | if (aspectRatio.indexOf("/") !== -1) { 20 | const [numerator, denominator] = aspectRatio.split("/") 21 | return parseFloat(numerator) / parseFloat(denominator) 22 | } else { 23 | return parseFloat(aspectRatio) 24 | } 25 | } 26 | return aspectRatio 27 | } 28 | 29 | export const parseScriptType = (scriptJSON: string) => { 30 | const scriptParams = parseJson(scriptJSON) 31 | return scriptParams?.type 32 | } 33 | -------------------------------------------------------------------------------- /src/hooks/useWindowSize.tsx: -------------------------------------------------------------------------------- 1 | // Source: https://usehooks-ts.com/react-hook/use-window-size 2 | import { useState } from "react" 3 | import { useEventListener, useIsomorphicLayoutEffect } from "usehooks-ts" 4 | 5 | interface WindowSize { 6 | width: number 7 | height: number 8 | } 9 | 10 | function useWindowSize(): WindowSize { 11 | const [windowSize, setWindowSize] = useState({ 12 | width: 0, 13 | height: 0 14 | }) 15 | 16 | const handleSize = () => { 17 | setWindowSize({ 18 | width: window.innerWidth, 19 | height: window.innerHeight, 20 | }) 21 | } 22 | 23 | useEventListener("resize", handleSize) 24 | 25 | // Set size at the first client-side load 26 | useIsomorphicLayoutEffect(() => { 27 | handleSize() 28 | // eslint-disable-next-line react-hooks/exhaustive-deps 29 | }, []) 30 | 31 | return windowSize 32 | } 33 | 34 | export default useWindowSize 35 | -------------------------------------------------------------------------------- /src/components/MintingProgress.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Typography, 4 | LinearProgress 5 | } from "@mui/material" 6 | 7 | interface Props { 8 | invocations: number, 9 | maxInvocations: number, 10 | maxHasBeenInvoked: boolean 11 | } 12 | 13 | const MintingProgress = ({invocations, maxInvocations, maxHasBeenInvoked}: Props) => { 14 | return ( 15 | 16 | 17 | 18 | {invocations.toString()} / {maxInvocations.toString()} minted 19 | 20 | 21 | 22 | 28 | 29 | 30 | ) 31 | } 32 | 33 | export default MintingProgress -------------------------------------------------------------------------------- /src/hooks/useCountOwnedProjects.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | import { getConfiguredContractAddresses } from "utils/contractInfoHelper" 3 | 4 | const countOwnedProjectsQuery = (walletAddress: string) => ` 5 | query GetProjects { 6 | projects( 7 | where: { 8 | contract_in: ["${getConfiguredContractAddresses().join("\",\"").toLowerCase()}"] 9 | active: true 10 | } 11 | ) { 12 | id 13 | tokens ( 14 | where: { 15 | owner: "${walletAddress}" 16 | } 17 | first: 1 18 | ) { 19 | id 20 | tokenId 21 | invocation 22 | } 23 | } 24 | }` 25 | 26 | const useCountOwnedProjects = (walletAddress: string) => { 27 | const { loading, error, data } = useQuery(gql(countOwnedProjectsQuery(walletAddress))) 28 | 29 | return { 30 | loading, 31 | error, 32 | data 33 | } 34 | } 35 | 36 | export default useCountOwnedProjects 37 | -------------------------------------------------------------------------------- /src/hooks/useTokenTraits.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { useState, useEffect } from "react" 3 | import { getContractConfigByAddress } from "utils/contractInfoHelper"; 4 | 5 | const useTokenTraits = (contractAddress: string, tokenId: string) => { 6 | const [data, setData] = useState(null) 7 | const [error, setError] = useState(false) 8 | const [loading, setLoading] = useState(false) 9 | const contractConfig = getContractConfigByAddress(contractAddress) 10 | 11 | useEffect(() => { 12 | setLoading(true) 13 | 14 | const fetchData = async () => { 15 | try { 16 | const tokenUrl = contractConfig?.TOKEN_URL 17 | const r = await axios.get(`${tokenUrl}/${contractAddress}/${tokenId}`) 18 | setData(r.data) 19 | } catch (error) { 20 | setError(true) 21 | } finally { 22 | setLoading(false) 23 | } 24 | } 25 | 26 | fetchData() 27 | }, [tokenId, contractAddress]) 28 | 29 | return { 30 | loading, 31 | error, 32 | data 33 | } 34 | } 35 | 36 | export default useTokenTraits 37 | -------------------------------------------------------------------------------- /src/components/EditProjectButton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Typography 4 | } from "@mui/material" 5 | 6 | interface Props { 7 | contractAddress: string, 8 | projectId: string, 9 | editProjectUrl: string 10 | } 11 | 12 | const EditProjectButton = ({contractAddress, projectId, editProjectUrl}: Props) => { 13 | return ( 14 | 33 | ) 34 | } 35 | 36 | export default EditProjectButton 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Art Blocks 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/hooks/useTokens.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | import { TOKENS_PER_PAGE } from "config" 3 | import { OrderDirection } from "utils/types" 4 | 5 | interface TokensQueryParams { 6 | first?: number 7 | skip?: number 8 | orderDirection?: OrderDirection 9 | } 10 | 11 | const tokensQuery = (projectId: string, { 12 | first, 13 | skip, 14 | orderDirection 15 | }: TokensQueryParams) => ` 16 | query GetTokens { 17 | tokens( 18 | first: ${first}, 19 | skip: ${skip}, 20 | orderBy: createdAt orderDirection: ${orderDirection}, 21 | where: { 22 | project: "${projectId}" 23 | } 24 | ) { 25 | id 26 | tokenId 27 | invocation 28 | } 29 | }` 30 | 31 | const useTokens = (projectId: string, params: TokensQueryParams) => { 32 | const first = params?.first || TOKENS_PER_PAGE 33 | const skip = params?.skip || 0 34 | const orderDirection = params?.orderDirection || OrderDirection.ASC 35 | 36 | const { loading, error, data } = useQuery(gql(tokensQuery(projectId, { 37 | first, 38 | skip, 39 | orderDirection 40 | }))) 41 | 42 | return { 43 | loading, 44 | error, 45 | data 46 | } 47 | } 48 | 49 | export default useTokens 50 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Routes, Route } from "react-router-dom" 2 | import { ToastContainer } from "react-toastify" 3 | import "react-toastify/dist/ReactToastify.css" 4 | import LandingPage from "pages/LandingPage" 5 | import ProjectsPage from "pages/ProjectsPage" 6 | import ProjectPage from "pages/ProjectPage" 7 | import TokenPage from "pages/TokenPage" 8 | import UserPage from "pages/UserPage" 9 | import Providers from "components/Providers" 10 | 11 | function App() { 12 | return ( 13 | 14 | 15 | 16 | }/> 17 | }/> 18 | }/> 19 | }/> 20 | }/> 21 | 22 | 23 | 31 | 32 | ) 33 | } 34 | 35 | export default App 36 | -------------------------------------------------------------------------------- /src/utils/types.ts: -------------------------------------------------------------------------------- 1 | export interface Project { 2 | id: string 3 | contract: Contract 4 | projectId: BigInt 5 | name: string 6 | description: string 7 | artistName: string 8 | artistAddress: string 9 | invocations: BigInt 10 | maxInvocations: BigInt 11 | activatedAt: BigInt 12 | scriptJSON: string 13 | aspectRatio: number 14 | active: boolean 15 | paused: boolean 16 | complete: boolean 17 | tokens: Token[] 18 | pricePerTokenInWei: BigInt 19 | currencyAddress: string 20 | currencySymbol: string 21 | minterConfiguration?: MinterConfiguration 22 | } 23 | 24 | export interface Contract { 25 | id: string 26 | } 27 | 28 | export interface Account { 29 | id: string 30 | } 31 | 32 | export interface Token { 33 | id: string 34 | tokenId: string 35 | invocation: BigInt 36 | uri: string 37 | createdAt: BigInt 38 | owner?: Account 39 | } 40 | 41 | export interface MinterConfiguration { 42 | basePrice: BigInt 43 | startPrice: BigInt 44 | priceIsconfigured: boolean 45 | currencySymbol: string 46 | currencyAddress: string, 47 | startTime: BigInt, 48 | endTime: BigInt 49 | } 50 | 51 | export interface Trait { 52 | trait_type: string 53 | value: string 54 | } 55 | 56 | export enum OrderDirection { 57 | ASC = "asc", 58 | DESC = "desc" 59 | } 60 | -------------------------------------------------------------------------------- /src/hooks/useOwnedTokens.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | import { TOKENS_PER_PAGE } from "config" 3 | import { OrderDirection } from "utils/types" 4 | 5 | interface Params { 6 | first?: number 7 | skip?: number 8 | orderDirection?: OrderDirection 9 | } 10 | 11 | const ownedTokensQuery = (projectId: string, walletAddress: string, { 12 | first, 13 | skip, 14 | orderDirection 15 | }: Params) => ` 16 | query GetTokens { 17 | tokens( 18 | first: ${first}, 19 | skip: ${skip}, 20 | orderBy: createdAt orderDirection: ${orderDirection}, 21 | where: { 22 | project: "${projectId}" 23 | owner: "${walletAddress}" 24 | } 25 | ) { 26 | id 27 | tokenId 28 | invocation 29 | } 30 | }` 31 | 32 | const useOwnedTokens = (projectId: string, walletAddress: string, params: Params) => { 33 | const first = params?.first || TOKENS_PER_PAGE 34 | const skip = params?.skip || 0 35 | const orderDirection = params?.orderDirection || OrderDirection.ASC 36 | 37 | const { loading, error, data } = useQuery(gql(ownedTokensQuery(projectId, walletAddress,{ 38 | first, 39 | skip, 40 | orderDirection 41 | }))) 42 | 43 | return { 44 | loading, 45 | error, 46 | data 47 | } 48 | } 49 | 50 | export default useOwnedTokens 51 | -------------------------------------------------------------------------------- /src/hooks/useProject.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | 3 | const projectQuery = (id: string) => ` 4 | query GetProject { 5 | project( 6 | id: "${id.toLowerCase()}" 7 | ) { 8 | id 9 | contract { 10 | id 11 | } 12 | projectId 13 | name 14 | description 15 | license 16 | locked 17 | pricePerTokenInWei 18 | active 19 | paused 20 | complete 21 | artistName 22 | artistAddress 23 | invocations 24 | maxInvocations 25 | scriptJSON 26 | scriptTypeAndVersion 27 | aspectRatio 28 | currencyAddress 29 | currencySymbol 30 | createdAt 31 | activatedAt 32 | tokens (first:1 orderBy: createdAt orderDirection: desc) { 33 | id 34 | tokenId 35 | invocation 36 | } 37 | minterConfiguration { 38 | basePrice 39 | startPrice 40 | priceIsConfigured 41 | currencySymbol 42 | currencyAddress 43 | startTime 44 | endTime 45 | } 46 | } 47 | }` 48 | 49 | const useProject = (id: string) => { 50 | const { loading, error, data } = useQuery(gql(projectQuery(id))) 51 | 52 | return { 53 | loading, 54 | error, 55 | data 56 | } 57 | } 58 | 59 | export default useProject 60 | -------------------------------------------------------------------------------- /src/components/Collapsible.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { 3 | Box, 4 | Typography, 5 | ButtonBase 6 | } from "@mui/material" 7 | import ReactMarkdown from "react-markdown" 8 | 9 | interface Props { 10 | content: string 11 | maxWords?: number 12 | useMarkdown?: boolean 13 | } 14 | 15 | const Collapsible = ({ content, maxWords=50, useMarkdown=true }: Props) => { 16 | const [open, setOpen] = useState(false) 17 | const words = content ? content.split(" ") : [] 18 | const truncated = words.slice(0, maxWords).join(" ") 19 | const overflows = words.length > maxWords 20 | 21 | return ( 22 | <> 23 | { 24 | useMarkdown 25 | ? 26 | 27 | {open ? content : overflows ? `${truncated}...` : truncated} 28 | 29 | : 30 | 31 | {open ? content : truncated} {overflows && !open && "..."} 32 | 33 | } 34 | { overflows && ( 35 | 36 | { !open && ( 37 | setOpen(true)} 39 | sx={{ textDecoration: "underline", textTransform: "none" }} 40 | > 41 | More 42 | 43 | )} 44 | 45 | )} 46 | 47 | ) 48 | } 49 | 50 | export default Collapsible 51 | -------------------------------------------------------------------------------- /src/utils/getMintingInterface.ts: -------------------------------------------------------------------------------- 1 | import GenArt721MinterInterface from "components/MinterInterfaces/GenArt721MinterInterface" 2 | import MinterSetPriceV4Interface from "components/MinterInterfaces/MinterSetPriceV4Interface" 3 | import MinterDAExpV4Interface from "components/MinterInterfaces/MinterDAExpV4Interface" 4 | import MinterMerkleV5Interface from "components/MinterInterfaces/MinterMerkleV5Interface" 5 | import MinterHolderV4Interface from "components/MinterInterfaces/MinterHolderV4Interface" 6 | import MinterSetPriceERC20V4Interface from "components/MinterInterfaces/MinterSetPriceERC20V4Interface" 7 | import MinterDAExpSettlementV1Interface from "components/MinterInterfaces/MinterDAExpSettlementV1Interface" 8 | 9 | export const getMintingInterface = (contractVersion: string, minterType: string | null) => { 10 | if (contractVersion === "V2") { 11 | return GenArt721MinterInterface 12 | } else if (contractVersion === "V3") { 13 | if (minterType === "MinterDAExpV4") return MinterDAExpV4Interface 14 | if (minterType === "MinterSetPriceV4") return MinterSetPriceV4Interface 15 | if (minterType === "MinterMerkleV5") return MinterMerkleV5Interface 16 | if (minterType === "MinterHolderV4") return MinterHolderV4Interface 17 | if (minterType === "MinterSetPriceERC20V4") return MinterSetPriceERC20V4Interface 18 | if (minterType === "MinterDAExpSettlementV1") return MinterDAExpSettlementV1Interface 19 | } 20 | return null 21 | } 22 | -------------------------------------------------------------------------------- /src/components/TokenView.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Card, 4 | Link 5 | } from "@mui/material" 6 | import TokenImage from "components/TokenImage" 7 | import TokenLive from "components/TokenLive" 8 | 9 | interface Props { 10 | contractAddress: string 11 | tokenId: string 12 | width: number 13 | invocation?: BigInt 14 | aspectRatio?: number 15 | live?: boolean 16 | } 17 | 18 | const TokenView = ({ 19 | contractAddress, 20 | tokenId, 21 | width, 22 | invocation, 23 | aspectRatio=1, 24 | live=false 25 | }: Props) => { 26 | const height = width / aspectRatio 27 | return ( 28 | 29 | 30 | { 31 | live ? 32 | ( 33 | 34 | ) : 35 | ( 36 | 37 | ) 38 | } 39 | 40 | { invocation !== undefined && 41 | ( 42 | 43 | 44 | No. { invocation?.toString() } 45 | 46 | 47 | ) 48 | } 49 | 50 | ) 51 | } 52 | 53 | export default TokenView 54 | -------------------------------------------------------------------------------- /src/hooks/useGeneratorPreview.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect, useCallback } from 'react' 2 | import axios from 'axios' 3 | import { Project } from 'utils/types' 4 | import { getContractConfigByAddress } from "utils/contractInfoHelper"; 5 | 6 | interface TokenData { 7 | tokenId: number 8 | hash: string 9 | } 10 | 11 | const generateToken = (p: number): TokenData => { 12 | let hash = '0x' 13 | for (var i = 0; i < 64; i++) { 14 | hash += Math.floor(Math.random()*16).toString(16) 15 | } 16 | return { 17 | hash, 18 | tokenId: p * 1000000 + Math.floor(Math.random()*1000) 19 | } 20 | } 21 | 22 | const useGeneratorPreview = (project: Project) => { 23 | const [content, setContent] = useState('') 24 | const [loading, setLoading] = useState(false) 25 | const [error, setError] = useState(false) 26 | const contractConfig = getContractConfigByAddress(project.contract.id) 27 | 28 | const refreshPreview = useCallback(async () => { 29 | setLoading(true) 30 | try { 31 | const token = generateToken(Number(project.projectId)) 32 | const generatorUrl = contractConfig?.GENERATOR_URL 33 | const { data } = await axios.get(`${generatorUrl}/${project.id}/${token.tokenId}/${token.hash}`) 34 | setContent(data) 35 | setError(false) 36 | } catch(error) { 37 | setError(true) 38 | } finally { 39 | setLoading(false) 40 | } 41 | }, [project, contractConfig?.GENERATOR_URL]) 42 | 43 | useEffect(() => { 44 | refreshPreview() 45 | }, [refreshPreview]) 46 | 47 | return { 48 | content, 49 | loading, 50 | error, 51 | refreshPreview 52 | } 53 | } 54 | 55 | export default useGeneratorPreview 56 | -------------------------------------------------------------------------------- /src/components/ProjectStatusBadge.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | import Box from '@mui/material/Box'; 3 | import Chip from '@mui/material/Chip'; 4 | import Typography from '@mui/material/Typography'; 5 | 6 | interface Props { 7 | complete: boolean; 8 | paused: boolean; 9 | startTime?: BigInt; 10 | } 11 | 12 | const ProjectStatusBadge = ({ complete, paused, startTime }: Props) => { 13 | const startDate = startTime ? moment.unix(parseInt(startTime.toString())) : null; 14 | 15 | return ( 16 | 20 | { 21 | startDate?.isAfter() ? 22 | 28 | : paused ? ( 29 | 35 | ) : !complete ? ( 36 | 42 | ) : ( 43 | 50 | ) 51 | } 52 | 53 | { 54 | startDate && ( 55 | 56 |
57 |
58 | ) 59 | } 60 |
61 | ) 62 | } 63 | 64 | export default ProjectStatusBadge; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "artblocks-engine", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@apollo/client": "^3.6.6", 7 | "@artblocks/contracts": "1.0.2", 8 | "@emotion/react": "^11.9.0", 9 | "@emotion/styled": "^11.8.1", 10 | "@mui/icons-material": "^5.8.4", 11 | "@mui/lab": "^5.0.0-alpha.86", 12 | "@mui/material": "^5.8.2", 13 | "@rainbow-me/rainbowkit": "^0.12.4", 14 | "@testing-library/jest-dom": "^5.16.4", 15 | "@testing-library/react": "^13.3.0", 16 | "@testing-library/user-event": "^13.5.0", 17 | "@types/jest": "^27.5.2", 18 | "@types/node": "^16.11.38", 19 | "@types/react": "^18.0.12", 20 | "@types/react-dom": "^18.0.5", 21 | "buffer": "^5.7.1", 22 | "ethers": "^5.7.2", 23 | "graphql": "^16.5.0", 24 | "moment": "^2.29.4", 25 | "moment-timezone": "^0.5.39", 26 | "react": "^18.1.0", 27 | "react-dom": "^18.1.0", 28 | "react-markdown": "^8.0.7", 29 | "react-query": "^3.39.1", 30 | "react-router-dom": "^6.3.0", 31 | "react-scripts": "5.0.1", 32 | "react-toastify": "^9.0.5", 33 | "typescript": "^4.7.3", 34 | "usehooks-ts": "^2.9.1", 35 | "util": "^0.12.4", 36 | "wagmi": "^0.12.6" 37 | }, 38 | "scripts": { 39 | "start": "GENERATE_SOURCEMAP=false react-scripts start", 40 | "build": "react-scripts build", 41 | "test": "react-scripts test", 42 | "eject": "react-scripts eject" 43 | }, 44 | "eslintConfig": { 45 | "extends": [ 46 | "react-app", 47 | "react-app/jest" 48 | ] 49 | }, 50 | "browserslist": { 51 | "production": [ 52 | ">0.2%", 53 | "not dead", 54 | "not op_mini all" 55 | ], 56 | "development": [ 57 | "last 1 chrome version", 58 | "last 1 firefox version", 59 | "last 1 safari version" 60 | ] 61 | }, 62 | "devDependencies": { 63 | "@typechain/ethers-v5": "^10.1.0", 64 | "typechain": "^8.1.0" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/theme.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { 3 | Link as RouterLink, 4 | LinkProps as RouterLinkProps 5 | } from "react-router-dom" 6 | import { createTheme, PaletteColor } from "@mui/material/styles" 7 | import { LinkProps } from "@mui/material/Link" 8 | 9 | declare module "@mui/material/styles" { 10 | interface Palette { 11 | upcoming: PaletteColor 12 | } 13 | interface PaletteOptions { 14 | upcoming: PaletteColor 15 | } 16 | } 17 | 18 | declare module "@mui/material/Chip" { 19 | interface ChipPropsColorOverrides { 20 | upcoming: true 21 | } 22 | } 23 | 24 | const LinkBehavior = React.forwardRef< 25 | any, 26 | Omit & { href: RouterLinkProps["to"] } 27 | >((props, ref) => { 28 | const { href, ...other } = props 29 | // Map href (MUI) -> to (react-router) 30 | return 31 | }) 32 | 33 | const { palette } = createTheme() 34 | 35 | const theme = createTheme({ 36 | palette: { 37 | primary: { 38 | main: "#212121", 39 | contrastText: "#ECF0F1" 40 | }, 41 | secondary: { 42 | main: "#2C3E50", 43 | contrastText: "#ECF0F1" 44 | }, 45 | success: { 46 | main: "#27AE60" 47 | }, 48 | error: { 49 | main: "#E74C3C" 50 | }, 51 | upcoming: palette.augmentColor({ 52 | color: { 53 | main: "#CE7A18" 54 | } 55 | }) 56 | }, 57 | typography: { 58 | fontFamily: [ 59 | "Raleway", 60 | "Geometric", 61 | "Segoe UI", 62 | "Helvetica Neue", 63 | "Arial", 64 | "sans-serif" 65 | ].join(",") 66 | }, 67 | components: { 68 | MuiLink: { 69 | defaultProps: { 70 | component: LinkBehavior 71 | } as LinkProps 72 | }, 73 | MuiButtonBase: { 74 | defaultProps: { 75 | LinkComponent: LinkBehavior 76 | } 77 | } 78 | } 79 | }) 80 | 81 | theme.typography.h1 = { 82 | fontFamily: "Archivo Black", 83 | fontWeight: "400" 84 | } 85 | 86 | export default theme -------------------------------------------------------------------------------- /src/components/ProjectPreview.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Typography, 4 | Link 5 | } from "@mui/material" 6 | import { Project } from "utils/types" 7 | import { parseAspectRatio } from "utils/scriptJSON" 8 | import Collapsible from "components/Collapsible" 9 | import ProjectDate from "components/ProjectDate" 10 | import TokenView from "components/TokenView" 11 | import ProjectStatusBadge from "./ProjectStatusBadge" 12 | 13 | interface Props { 14 | project: Project 15 | width?: number 16 | showDescription?: boolean 17 | } 18 | 19 | const ProjectPreview = ({project, width=280, showDescription=false}: Props) => { 20 | if (!project) { 21 | return null 22 | } 23 | 24 | const token = project?.tokens[0] 25 | return ( 26 | 27 | 28 | 29 | 30 | {project.name} 31 | 32 | 33 | 34 | by {project.artistName} 35 | 36 | 37 | 44 | 45 | 46 | 47 | 48 | 51 | 52 | { 53 | showDescription && ( 54 | 55 | 56 | 57 | ) 58 | } 59 | 60 | 61 | ) 62 | } 63 | 64 | export default ProjectPreview 65 | -------------------------------------------------------------------------------- /src/components/MintingPrice.tsx: -------------------------------------------------------------------------------- 1 | import { utils, BigNumber } from "ethers" 2 | import { 3 | Box, 4 | LinearProgress, 5 | Typography 6 | } from "@mui/material" 7 | 8 | interface Props { 9 | startPriceWei: BigNumber, 10 | currentPriceWei: BigNumber, 11 | endPriceWei: BigNumber 12 | currencySymbol: string 13 | } 14 | 15 | const MintingPrice = ({startPriceWei, currentPriceWei, endPriceWei, currencySymbol}: Props) => { 16 | const fixedPrice = startPriceWei === endPriceWei 17 | const startToEnd = Number(startPriceWei.toBigInt()-endPriceWei.toBigInt()) 18 | const startToCurrent = Number(startPriceWei.toBigInt()-currentPriceWei.toBigInt()) 19 | return ( 20 | 21 | 22 | { 23 | fixedPrice ? 24 | ( 25 | Fixed Price: {utils.formatEther(startPriceWei.toString())} {currencySymbol} 26 | ) : 27 | ( 28 | Auction Price ({currencySymbol}) 29 | ) 30 | } 31 | 32 | { 33 | !fixedPrice && 34 | ( 35 | 36 | 37 | 38 | {utils.formatEther(startPriceWei.toString())} 39 | 40 | 41 | {utils.formatEther(endPriceWei.toString())} 42 | 43 | 44 | 45 | 51 | 52 | 53 | ) 54 | } 55 | 56 | ) 57 | } 58 | 59 | export default MintingPrice -------------------------------------------------------------------------------- /src/hooks/useProjects.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | import { PROJECTS_PER_PAGE } from "config" 3 | import { OrderDirection } from "utils/types" 4 | import { getConfiguredContractAddresses } from "utils/contractInfoHelper"; 5 | 6 | interface ProjectsQueryParams { 7 | first?: number 8 | skip?: number 9 | orderDirection?: OrderDirection 10 | } 11 | 12 | const projectsQuery = ({ first, skip, orderDirection }: ProjectsQueryParams) => ` 13 | query GetProjects { 14 | projects( 15 | where: { 16 | contract_in: ["${getConfiguredContractAddresses().join("\",\"").toLowerCase()}"] 17 | active: true 18 | } 19 | first: ${first} 20 | skip: ${skip} 21 | orderBy: createdAt 22 | orderDirection: ${orderDirection} 23 | ) { 24 | id 25 | contract { 26 | id 27 | } 28 | projectId 29 | name 30 | description 31 | license 32 | locked 33 | pricePerTokenInWei 34 | active 35 | paused 36 | complete 37 | artistName 38 | artistAddress 39 | invocations 40 | maxInvocations 41 | scriptJSON 42 | aspectRatio 43 | currencyAddress 44 | currencySymbol 45 | createdAt 46 | activatedAt 47 | tokens (first:10 orderBy: createdAt orderDirection: desc) { 48 | id 49 | tokenId 50 | invocation 51 | } 52 | minterConfiguration { 53 | basePrice 54 | startPrice 55 | priceIsConfigured 56 | currencySymbol 57 | currencyAddress 58 | startTime 59 | endTime 60 | } 61 | } 62 | }` 63 | 64 | const useProjects = (params?: ProjectsQueryParams) => { 65 | const first = params?.first || PROJECTS_PER_PAGE 66 | const skip = params?.skip || 0 67 | const orderDirection = params?.orderDirection || OrderDirection.DESC 68 | const { loading, error, data } = useQuery(gql(projectsQuery({ first, skip, orderDirection }))) 69 | 70 | return { 71 | loading, 72 | error, 73 | data 74 | } 75 | } 76 | 77 | export default useProjects 78 | -------------------------------------------------------------------------------- /src/components/MintingInterfaceFilter.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react" 2 | import { useContractRead } from "wagmi" 3 | import { BigNumber } from "ethers" 4 | import MinterFilterV1ABI from "abi/V3/MinterFilterV1.json" 5 | import { getMintingInterface } from "utils/getMintingInterface" 6 | 7 | interface Props { 8 | contractVersion: string, 9 | coreContractAddress: string, 10 | mintContractAddress: string, 11 | projectId: string, 12 | artistAddress: string, 13 | scriptAspectRatio: number 14 | } 15 | 16 | const MintingInterfaceFilter = ( 17 | { 18 | contractVersion, 19 | coreContractAddress, 20 | mintContractAddress, 21 | projectId, 22 | artistAddress, 23 | scriptAspectRatio 24 | }: Props 25 | ) => { 26 | 27 | const [v3ProjectAndMinterInfo, setV3ProjectAndMinterInfo] = useState(null) 28 | const { data, isError, isLoading } = useContractRead({ 29 | address: mintContractAddress as `0x${string}`, 30 | abi: MinterFilterV1ABI, 31 | functionName: "getProjectAndMinterInfoAt", 32 | args: [BigNumber.from(projectId)], 33 | enabled: contractVersion === "V3", 34 | watch: true, 35 | onSuccess(data) { 36 | setV3ProjectAndMinterInfo(data) 37 | } 38 | }) 39 | 40 | if (contractVersion === "V3" && (!data || !v3ProjectAndMinterInfo || isLoading || isError)) { 41 | return null 42 | } 43 | 44 | let minterType = null 45 | let minterAddress = mintContractAddress 46 | if (contractVersion === "V3") { 47 | if (!data || !v3ProjectAndMinterInfo || isLoading || isError) return null 48 | minterType = v3ProjectAndMinterInfo?.minterType 49 | minterAddress = v3ProjectAndMinterInfo.minterAddress 50 | } 51 | 52 | const MintingInterface = getMintingInterface(contractVersion, minterType) 53 | return MintingInterface && ( 54 | 61 | ) 62 | } 63 | 64 | export default MintingInterfaceFilter 65 | -------------------------------------------------------------------------------- /src/components/TokenTraits.tsx: -------------------------------------------------------------------------------- 1 | import { Trait } from "utils/types" 2 | import { 3 | Typography, 4 | Alert, 5 | Table, 6 | TableBody, 7 | TableCell, 8 | TableContainer, 9 | TableHead, 10 | TableRow 11 | } from "@mui/material" 12 | import Loading from "components/Loading" 13 | import useTokenTraits from "hooks/useTokenTraits" 14 | 15 | interface Props { 16 | contractAddress: string 17 | tokenId: string 18 | } 19 | 20 | const TokenTraits = ({ contractAddress, tokenId }: Props) => { 21 | const { loading, error, data } = useTokenTraits(contractAddress, tokenId) 22 | const traits = data?.traits?.filter((t:Trait) => t.value.indexOf('All') === -1) 23 | 24 | if (loading) { 25 | return 26 | } 27 | 28 | if (error) { 29 | return ( 30 | 31 | Error loading traits 32 | 33 | ) 34 | } 35 | 36 | return traits && traits.length > 0 && ( 37 | 38 | Features 39 | 40 | 41 | 42 | 43 | 44 | Feature 45 | 46 | 47 | 48 | 49 | Value 50 | 51 | 52 | 53 | 54 | 55 | {traits.map((trait:Trait) => { 56 | const p = trait.value.split(":") 57 | return ( 58 | 59 | 60 | 61 | {p[0]} 62 | 63 | 64 | 65 | 66 | {p[1]} 67 | 68 | 69 | 70 | ) 71 | })} 72 | 73 |
74 |
75 | ) 76 | } 77 | 78 | export default TokenTraits 79 | -------------------------------------------------------------------------------- /src/hooks/useOwnedProjects.tsx: -------------------------------------------------------------------------------- 1 | import { useQuery, gql } from "@apollo/client" 2 | import { PROJECTS_PER_PAGE } from "config" 3 | import { OrderDirection } from "utils/types" 4 | import { getConfiguredContractAddresses } from "utils/contractInfoHelper" 5 | 6 | interface Params { 7 | first?: number 8 | skip?: number 9 | orderDirection?: OrderDirection 10 | } 11 | 12 | const ownedProjectsQuery = (walletAddress: string, { first, skip, orderDirection }: Params) => ` 13 | query GetProjects { 14 | projects( 15 | where: { 16 | contract_in: ["${getConfiguredContractAddresses().join("\",\"").toLowerCase()}"] 17 | active: true 18 | } 19 | first: ${first} 20 | skip: ${skip} 21 | orderBy: createdAt 22 | orderDirection: ${orderDirection} 23 | ) { 24 | id 25 | contract { 26 | id 27 | } 28 | projectId 29 | name 30 | description 31 | license 32 | locked 33 | pricePerTokenInWei 34 | active 35 | paused 36 | complete 37 | artistName 38 | artistAddress 39 | invocations 40 | maxInvocations 41 | scriptJSON 42 | aspectRatio 43 | currencyAddress 44 | currencySymbol 45 | createdAt 46 | activatedAt 47 | tokens ( 48 | where: { 49 | owner: "${walletAddress}" 50 | } 51 | first: 2 52 | ) { 53 | id 54 | tokenId 55 | invocation 56 | } 57 | minterConfiguration { 58 | basePrice 59 | startPrice 60 | priceIsConfigured 61 | currencySymbol 62 | currencyAddress 63 | startTime 64 | endTime 65 | } 66 | } 67 | }` 68 | 69 | const useOwnedProjects = (walletAddress: string, params?: Params) => { 70 | const first = params?.first || PROJECTS_PER_PAGE 71 | const skip = params?.skip || 0 72 | const orderDirection = params?.orderDirection || OrderDirection.DESC 73 | const { loading, error, data } = useQuery(gql(ownedProjectsQuery(walletAddress, { first, skip, orderDirection }))) 74 | 75 | return { 76 | loading, 77 | error, 78 | data 79 | } 80 | } 81 | 82 | export default useOwnedProjects 83 | -------------------------------------------------------------------------------- /src/components/Providers.tsx: -------------------------------------------------------------------------------- 1 | import CssBaseline from "@mui/material/CssBaseline" 2 | import "@rainbow-me/rainbowkit/styles.css" 3 | import { ThemeProvider } from "@mui/material/styles" 4 | import theme from "theme" 5 | import { RainbowKitProvider, getDefaultWallets, midnightTheme } from "@rainbow-me/rainbowkit" 6 | import {configureChains, createClient, WagmiConfig} from "wagmi" 7 | import { mainnet, goerli } from 'wagmi/chains' 8 | import { infuraProvider } from "wagmi/providers/infura" 9 | import { publicProvider } from "wagmi/providers/public" 10 | import { ApolloClient, ApolloProvider, InMemoryCache } from "@apollo/client" 11 | import { GRAPHQL_URL, INFURA_KEY, EXPECTED_CHAIN_ID, WALLET_CONNECT_PROJECT_ID } from "config" 12 | 13 | const client = new ApolloClient({ 14 | uri: GRAPHQL_URL, 15 | cache: new InMemoryCache() 16 | }) 17 | 18 | // Defaults to goerli testing network if mainnet is not set 19 | const expectedChains = [EXPECTED_CHAIN_ID === 1 ? mainnet : goerli] 20 | const initialChain = EXPECTED_CHAIN_ID === 1 ? mainnet : goerli 21 | 22 | const { chains, provider, webSocketProvider } = configureChains( 23 | expectedChains, 24 | [ 25 | infuraProvider({apiKey: INFURA_KEY, priority: 0}), 26 | publicProvider({priority: 1}) 27 | ] 28 | ) 29 | 30 | const { connectors } = getDefaultWallets({ 31 | appName: "Engine", 32 | chains, 33 | projectId: WALLET_CONNECT_PROJECT_ID 34 | }) 35 | 36 | const wagmiClient = createClient({ 37 | autoConnect: true, 38 | connectors, 39 | provider, 40 | webSocketProvider 41 | }) 42 | 43 | interface Props { 44 | children: React.ReactNode 45 | } 46 | 47 | const AppProvider = ({children}:Props) => { 48 | return ( 49 | 50 | 58 | 59 | 60 | 61 | {children} 62 | 63 | 64 | 65 | 66 | ) 67 | } 68 | 69 | export default AppProvider 70 | -------------------------------------------------------------------------------- /src/components/Tokens.tsx: -------------------------------------------------------------------------------- 1 | import useTheme from "@mui/material/styles/useTheme" 2 | import { TOKENS_PER_PAGE } from "config" 3 | import { OrderDirection, Token } from "utils/types" 4 | import { 5 | Grid, 6 | Link, 7 | Alert, 8 | Typography 9 | } from "@mui/material" 10 | import Loading from "components/Loading" 11 | import TokenView from "components/TokenView" 12 | import useTokens from "hooks/useTokens" 13 | import useWindowSize from "hooks/useWindowSize" 14 | 15 | interface Props { 16 | contractAddress: string 17 | projectId: string 18 | first?: number 19 | skip?: number 20 | orderDirection?: OrderDirection 21 | aspectRatio?: number 22 | } 23 | 24 | const Tokens = ({ 25 | contractAddress, 26 | projectId, 27 | first=TOKENS_PER_PAGE, 28 | skip=0, 29 | orderDirection=OrderDirection.ASC, 30 | aspectRatio=1 31 | }: Props) => { 32 | const theme = useTheme() 33 | const windowSize = useWindowSize() 34 | const {loading, error, data } = useTokens(projectId, { 35 | first, 36 | skip, 37 | orderDirection 38 | }) 39 | 40 | if (loading) { 41 | return 42 | } 43 | 44 | if (error) { 45 | return ( 46 | 47 | Error loading tokens 48 | 49 | ) 50 | } 51 | 52 | if (!data || !data.tokens) { 53 | return ( 54 | 55 | No tokens found for this project. 56 | 57 | ) 58 | } 59 | 60 | let width = 280 61 | if (windowSize && !isNaN(windowSize.width)) { 62 | width = windowSize.width > theme.breakpoints.values.md 63 | ? (Math.min(windowSize.width, 1200)-96) / 3 64 | : (windowSize.width-60) / 2 65 | } 66 | 67 | return ( 68 | data.tokens.length > 0 ? 69 | 70 | { 71 | data.tokens.map(((token:Token) => ( 72 | 73 | 74 | 80 | 81 | 82 | #{token.invocation.toString()} 83 | 84 | 85 | ))) 86 | } 87 | 88 | : null 89 | ) 90 | } 91 | 92 | export default Tokens 93 | -------------------------------------------------------------------------------- /src/contractConfig.ts: -------------------------------------------------------------------------------- 1 | export const mainnetContractConfig = [ 2 | { 3 | "CONTRACT_VERSION": "V2", 4 | "CORE_CONTRACT_ADDRESS": "0xa319c382a702682129fcbf55d514e61a16f97f9c", 5 | "MINT_CONTRACT_ADDRESS": "0x463b8ced7d22a55aa4a5d69ef6a54a08aa0feb93", 6 | "MEDIA_URL": "https://plottables-mainnet.s3.amazonaws.com", 7 | "TOKEN_URL": "https://token.artblocks.io", 8 | "GENERATOR_URL": "https://generator.artblocks.io", 9 | "EDIT_PROJECT_URL": "https://artblocks.io/engine/fullyonchain/projects" 10 | }, 11 | { 12 | "CONTRACT_VERSION": "V2", 13 | "CORE_CONTRACT_ADDRESS": "0x18de6097ce5b5b2724c9cae6ac519917f3f178c0", 14 | "MINT_CONTRACT_ADDRESS": "0xe6e728361b7c824cba64cc1e5323efb7a5bb65da", 15 | "MEDIA_URL": "https://plottables-flex-mainnet.s3.amazonaws.com", 16 | "TOKEN_URL": "https://token.artblocks.io", 17 | "GENERATOR_URL": "https://generator.artblocks.io", 18 | "EDIT_PROJECT_URL": "https://artblocks.io/engine/flex/projects" 19 | } 20 | ] 21 | 22 | export const testnetContractConfig = [ 23 | { 24 | "CONTRACT_VERSION": "V2", 25 | "CORE_CONTRACT_ADDRESS": "0x9B0c67496Be8c6422fED0372be7a87707e3a6F09", 26 | "MINT_CONTRACT_ADDRESS": "0x068C519D00A60CCD1830fabfe6eC428F2FDb4146", 27 | "MEDIA_URL": "https://plottables-goerli.s3.amazonaws.com", 28 | "TOKEN_URL": "https://token.staging.artblocks.io", 29 | "GENERATOR_URL": "https://generator-staging-goerli.artblocks.io", 30 | "EDIT_PROJECT_URL": "https://artist-staging.artblocks.io/engine/fullyonchain/projects" 31 | }, 32 | { 33 | "CONTRACT_VERSION": "V2", 34 | "CORE_CONTRACT_ADDRESS": "0x48742D38a0809135EFd643c1150BfC13768C3907", 35 | "MINT_CONTRACT_ADDRESS": "0x1DEC9E52f1320F7Deb29cBCd7B7d67f3dF785142", 36 | "MEDIA_URL": "https://plottables-flex-goerli.s3.amazonaws.com", 37 | "TOKEN_URL": "https://token.staging.artblocks.io", 38 | "GENERATOR_URL": "https://generator-staging-goerli.artblocks.io", 39 | "EDIT_PROJECT_URL": "https://artist-staging.artblocks.io/engine/flex/projects" 40 | }, 41 | { 42 | "CONTRACT_VERSION": "V3", 43 | "CORE_CONTRACT_ADDRESS": "0xCEd5350f5a2Ba24946F92C08260931CFf65dc954", 44 | "MINT_CONTRACT_ADDRESS": "0x0AB754254d7243315FFFDC363a6A0997aD9c3118", 45 | "MEDIA_URL": "https://plottablesv3-goerli.s3.amazonaws.com", 46 | "TOKEN_URL": "https://token.staging.artblocks.io", 47 | "GENERATOR_URL": "https://generator-staging-goerli.artblocks.io", 48 | "EDIT_PROJECT_URL": "https://artist-staging.artblocks.io/engine/fullyonchain/projects" 49 | } 50 | ] 51 | -------------------------------------------------------------------------------- /src/components/TokenLive.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios" 2 | import { useState } from "react" 3 | import { 4 | Box, 5 | Typography 6 | } from "@mui/material" 7 | import Loading from "components/Loading" 8 | import TokenImage from "components/TokenImage" 9 | import useInterval from "hooks/useInterval" 10 | import { getContractConfigByAddress } from "utils/contractInfoHelper"; 11 | 12 | interface Props { 13 | contractAddress: string 14 | tokenId: string 15 | width: number 16 | height: number 17 | } 18 | 19 | const TokenLive = ({contractAddress, tokenId, width, height}: Props) => { 20 | const [status, setStatus] = useState(404) 21 | const [pollingTime, setPollingTime] = useState(0) 22 | const [pollingDelay, setPollingDelay] = useState(0) 23 | const [pollingAttempts, setPollingAttempts] = useState(0) 24 | const contractConfig = getContractConfigByAddress(contractAddress) 25 | const generatorUrl = contractConfig?.GENERATOR_URL 26 | const endpoint = `${generatorUrl}/${contractAddress.toLowerCase()}/${tokenId}` 27 | 28 | useInterval(() => { 29 | setPollingTime(pollingTime+1) 30 | }, 1000) 31 | 32 | useInterval(() => { 33 | setPollingDelay(pollingDelay+3) 34 | if (status === 404) { 35 | axios 36 | .get(endpoint) 37 | .then(function(response) { 38 | setStatus(response.status) 39 | }) 40 | .catch((error) => { 41 | setStatus(404) 42 | }) 43 | .finally(() => { 44 | setPollingAttempts(pollingAttempts+1) 45 | }) 46 | } 47 | }, 1000*pollingDelay) 48 | 49 | if (pollingAttempts === 0) { 50 | return ( 51 | 52 | ) 53 | } 54 | 55 | if (pollingTime > 500) { 56 | return ( 57 | 58 | ) 59 | } 60 | 61 | return ( 62 | 63 | { 64 | status === 200 ? 65 | ( 66 |