diff --git a/.vscode/settings.json b/.vscode/settings.json index ed2c8d598..08ea874d9 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,35 +1,34 @@ { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": true - }, - "eslint.validate": ["typescript", "typescriptreact"], - "files.exclude": { - "**/.git": true, - "**/.svn": true, - "**/.hg": true, - "**/CVS": true, - "**/.DS_Store": true, - "**/Thumbs.db": true, - "**/node_modules": true - }, - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[typescriptreact]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[jsonc]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - } + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "eslint.validate": ["typescript", "typescriptreact"], + "files.exclude": { + "**/.git": true, + "**/.svn": true, + "**/.hg": true, + "**/CVS": true, + "**/.DS_Store": true, + "**/Thumbs.db": true, + "**/node_modules": true + }, + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[javascriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[json]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[jsonc]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" } - \ No newline at end of file +} diff --git a/README.md b/README.md index e510c9b9f..4ba55c118 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Add the following variables to `apps/web/.env.local`: ``` #alchemy -NEXT_PUBLIC_ALCHEMY_ID= +NEXT_PUBLIC_TENDERLY_RPC_KEY= #tenderly diff --git a/apps/web/.env b/apps/web/.env index 46df7b866..d371e8c39 100644 --- a/apps/web/.env +++ b/apps/web/.env @@ -3,5 +3,4 @@ NEXT_PUBLIC_UPLOAD_API=https://upload-api.zora.co NEXT_PUBLIC_IPFS_GATEWAY=https://ipfs.decentralized-content.com -NEXT_PUBLIC_CHAIN_ID=5 -NEXT_PUBLIC_NETWORK_TYPE=mainnet \ No newline at end of file +NEXT_PUBLIC_NETWORK_TYPE=testnet diff --git a/apps/web/README.md b/apps/web/README.md index 7dddfac8b..33e33c5ba 100644 --- a/apps/web/README.md +++ b/apps/web/README.md @@ -48,13 +48,13 @@ Nouns Builder currently only supports two networks: `mainnet` and `goerli testne You can swap out the environment variables as defined below to run against mainnnet or testnet locally. ``` -# the default chain id defined in .env, to run against testnet -NEXT_PUBLIC_ALCHEMY_ID= -NEXT_PUBLIC_CHAIN_ID=5 +# the default network type is defined in .env, to run against testnet +NEXT_PUBLIC_TENDERLY_RPC_KEY= +NEXT_PUBLIC_NETWORK_TYPE="testnet" # to run against mainnet locally -NEXT_PUBLIC_ALCHEMY_ID= -NEXT_PUBLIC_CHAIN_ID=1 +NEXT_PUBLIC_TENDERLY_RPC_KEY= +NEXT_PUBLIC_NETWORK_TYPE="mainnet" ``` ### Writing Tests diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts index 4f11a03dc..fd36f9494 100644 --- a/apps/web/next-env.d.ts +++ b/apps/web/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. diff --git a/apps/web/package.json b/apps/web/package.json index d938f46fe..894a0ef8a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -19,7 +19,7 @@ "@fontsource/inter": "4.5.10", "@fontsource/londrina-solid": "^4.5.9", "@rainbow-me/rainbowkit": "^1.3.1", - "@sentry/nextjs": "^7.15.0", + "@sentry/nextjs": "^7.105.0", "@types/lodash": "^4.14.186", "@types/react-portal": "^4.0.4", "@types/tinycolor2": "^1.4.3", @@ -36,9 +36,11 @@ "flatpickr": "^4.6.13", "formik": "^2.2.9", "framer-motion": "^6.3.3", + "frog": "^0.5.5", "graphql": "^16.6.0", "graphql-request": "^4.3.0", "graphql-tag": "^2.12.6", + "hono": "^4.0.9", "html-react-parser": "^3.0.9", "ioredis": "^5.2.3", "ipfs-http-client": "^59.0.0", @@ -46,18 +48,18 @@ "lanyard": "^1.1.2", "lodash": "^4.17.21", "multiformats": "9.9.0", - "next": "^13.0.3", + "next": "^14.1.2", "nextjs-progressbar": "^0.0.16", "raw-loader": "^4.0.2", "react": "^18.2.0", "react-content-loader": "^6.0.2", "react-dom": "^18.2.0", - "react-markdown": "^8.0.5", + "react-markdown": "^9.0.1", "react-mde": "^11.5.0", "react-portal": "^4.2.2", - "rehype-raw": "^6.1.1", - "rehype-sanitize": "^5.0.1", - "remark-gfm": "^3.0.1", + "rehype-raw": "^7.0.0", + "rehype-sanitize": "^6.0.0", + "remark-gfm": "^4.0.0", "sharp": "^0.32.6", "swr": "^1.3.0", "tinycolor2": "^1.4.2", diff --git a/apps/web/src/app/api/frames/[[...routes]]/route.tsx b/apps/web/src/app/api/frames/[[...routes]]/route.tsx new file mode 100644 index 000000000..71c8a0855 --- /dev/null +++ b/apps/web/src/app/api/frames/[[...routes]]/route.tsx @@ -0,0 +1,113 @@ +/** @jsxImportSource frog/jsx */ +import { Frog } from 'frog' +import { handle } from 'frog/next' +import { isAddress, parseEther } from 'viem' + +import { auctionAbi, governorAbi } from 'src/data/contract/abis' +import { AddressType, BytesType, CHAIN_ID } from 'src/typings' + +const app = new Frog({ + basePath: '/api/frames', +}) + +app.transaction('/bid', async (c) => { + try { + const { inputText, req } = c + const { chainId, auctionContract, tokenId, amount, referral } = req.query() + + if (!tokenId) return new Response('Invalid tokenId', { status: 400 }) + + const tokenIdParsed = BigInt(tokenId) + const chainIdParsed = parseInt(chainId) + + let bidAmount: bigint | undefined + + if (amount) bidAmount = parseEther(amount) + else if (inputText) bidAmount = parseEther(inputText) + + if (!bidAmount) return new Response('Invalid bid amount', { status: 400 }) + + if ( + chainIdParsed !== CHAIN_ID.OPTIMISM && + chainIdParsed !== CHAIN_ID.ZORA && + chainIdParsed !== CHAIN_ID.BASE + ) + return new Response('Invalid chain id', { status: 400 }) + + const contract = { + abi: auctionAbi, + chainId: `eip155:${chainIdParsed}`, + to: auctionContract as AddressType, + } as const + + if (referral) { + if (!isAddress(referral)) return new Response('Invalid referral', { status: 400 }) + + return c.contract({ + ...contract, + functionName: 'createBidWithReferral', + value: bidAmount, + args: [tokenIdParsed, referral as AddressType], + }) + } + + return c.contract({ + ...contract, + functionName: 'createBid', + value: bidAmount, + args: [tokenIdParsed], + }) + } catch (err) { + return new Response((err as Error).message, { status: 500 }) + } +}) + +app.transaction('/vote', async (c) => { + try { + const { inputText, req } = c + const { chainId, governorContract, support, proposalId, reason } = req.query() + const chainIdParsed = parseInt(chainId) + + let reasonText + if (reason) reasonText = reason + else if (inputText) reasonText = inputText + + const supportParsed = BigInt(support) + + // 0 = Against, 1 = For, 2 = Abstain + if (supportParsed !== 0n && supportParsed !== 1n && supportParsed !== 2n) + return new Response('Invalid support value', { status: 400 }) + + if ( + chainIdParsed !== CHAIN_ID.OPTIMISM && + chainIdParsed !== CHAIN_ID.ZORA && + chainIdParsed !== CHAIN_ID.BASE + ) + return new Response('Invalid chain id', { status: 400 }) + + const contract = { + abi: governorAbi, + chainId: `eip155:${chainIdParsed}`, + to: governorContract as AddressType, + } as const + + if (reasonText) { + return c.contract({ + ...contract, + functionName: 'castVoteWithReason', + args: [proposalId as BytesType, supportParsed, reasonText], + }) + } + + return c.contract({ + ...contract, + functionName: 'castVote', + args: [proposalId as BytesType, supportParsed], + }) + } catch (err) { + return new Response((err as Error).message, { status: 500 }) + } +}) + +export const GET = handle(app) +export const POST = handle(app) diff --git a/apps/web/src/components/Icon/assets/share.svg b/apps/web/src/components/Icon/assets/share.svg new file mode 100644 index 000000000..7d632bf7a --- /dev/null +++ b/apps/web/src/components/Icon/assets/share.svg @@ -0,0 +1,3 @@ + + + diff --git a/apps/web/src/components/Icon/icons.ts b/apps/web/src/components/Icon/icons.ts index ad21397e1..61620c58b 100644 --- a/apps/web/src/components/Icon/icons.ts +++ b/apps/web/src/components/Icon/icons.ts @@ -35,6 +35,7 @@ import Play from './assets/play.svg' import Plus from './assets/plus.svg' import Refresh from './assets/refresh.svg' import ResumeTemplate from './assets/resume-template.svg' +import Share from './assets/share.svg' import Trash from './assets/trash.svg' import Twitter from './assets/twitter.svg' import Warning16 from './assets/warning-16.svg' @@ -80,6 +81,7 @@ export const icons = { resumeTemplate: ResumeTemplate, trash: Trash, twitter: Twitter, + share: Share, warning: Warning, 'warning-16': Warning16, } diff --git a/apps/web/src/constants/addresses.ts b/apps/web/src/constants/addresses.ts index e86dc907f..d9a9418cd 100644 --- a/apps/web/src/constants/addresses.ts +++ b/apps/web/src/constants/addresses.ts @@ -109,4 +109,8 @@ export const L1_CROSS_DOMAIN_MESSENGER = { export const ALLOWED_MIGRATION_DAOS: AddressType[] = [ '0xf3b8f2ef0933f601c2cceada242fc3948a6ba757', '0xc0a2527d25ad9c7dee3f47e3497ca0093def26bc', + '0xa45662638e9f3bbb7a6fecb4b17853b7ba0f3a60', + '0x96e396e66087b2b9dcad36fd473e1b049df18998', + '0xdf9b7d26c8fc806b1ae6273684556761ff02d422', + '0x795D300855069F602862c5e23814Bdeeb25DCa6b', ] diff --git a/apps/web/src/constants/etherscan.ts b/apps/web/src/constants/etherscan.ts index 92cb315fe..f02a5a4fe 100644 --- a/apps/web/src/constants/etherscan.ts +++ b/apps/web/src/constants/etherscan.ts @@ -1,13 +1,14 @@ import { CHAIN_ID } from 'src/typings' +// URLs should not include a trailing forward slash export const ETHERSCAN_BASE_URL = { [CHAIN_ID.ETHEREUM]: 'https://etherscan.io', [CHAIN_ID.OPTIMISM]: 'https://optimistic.etherscan.io', [CHAIN_ID.SEPOLIA]: 'https://sepolia.etherscan.io', - [CHAIN_ID.OPTIMISM_SEPOLIA]: 'https://sepolia-optimism.etherscan.io/', - [CHAIN_ID.BASE]: 'https://basescan.org/', - [CHAIN_ID.BASE_SEPOLIA]: 'https://sepolia.basescan.org/', - [CHAIN_ID.ZORA]: 'https://explorer.zora.energy/', - [CHAIN_ID.ZORA_SEPOLIA]: 'https://sepolia.explorer.zora.energy/', + [CHAIN_ID.OPTIMISM_SEPOLIA]: 'https://sepolia-optimism.etherscan.io', + [CHAIN_ID.BASE]: 'https://basescan.org', + [CHAIN_ID.BASE_SEPOLIA]: 'https://sepolia.basescan.org', + [CHAIN_ID.ZORA]: 'https://explorer.zora.energy', + [CHAIN_ID.ZORA_SEPOLIA]: 'https://sepolia.explorer.zora.energy', [CHAIN_ID.FOUNDRY]: '', } diff --git a/apps/web/src/constants/rpc.ts b/apps/web/src/constants/rpc.ts index 9a652eb29..1921b8679 100644 --- a/apps/web/src/constants/rpc.ts +++ b/apps/web/src/constants/rpc.ts @@ -3,12 +3,12 @@ import { foundry } from 'wagmi/chains' import { CHAIN_ID } from 'src/typings' export const RPC_URL = { - [CHAIN_ID.ETHEREUM]: `https://eth-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}`, - [CHAIN_ID.OPTIMISM]: `https://opt-mainnet.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}`, - [CHAIN_ID.SEPOLIA]: `https://eth-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}`, - [CHAIN_ID.OPTIMISM_SEPOLIA]: `https://opt-sepolia.g.alchemy.com/v2/${process.env.NEXT_PUBLIC_ALCHEMY_ID}`, - [CHAIN_ID.BASE]: `https://hardworking-wild-arm.base-mainnet.discover.quiknode.pro/${process.env.NEXT_PUBLIC_QUICKNODE_ID}`, - [CHAIN_ID.BASE_SEPOLIA]: 'https://sepolia.base.org', + [CHAIN_ID.ETHEREUM]: ` https://mainnet.gateway.tenderly.co/${process.env.NEXT_PUBLIC_TENDERLY_RPC_KEY}`, + [CHAIN_ID.OPTIMISM]: `https://optimism.gateway.tenderly.co/${process.env.NEXT_PUBLIC_TENDERLY_RPC_KEY}`, + [CHAIN_ID.SEPOLIA]: ` https://sepolia.gateway.tenderly.co/${process.env.NEXT_PUBLIC_TENDERLY_RPC_KEY}`, + [CHAIN_ID.OPTIMISM_SEPOLIA]: `https://optimism-sepolia.gateway.tenderly.co/${process.env.NEXT_PUBLIC_TENDERLY_RPC_KEY}`, + [CHAIN_ID.BASE]: `https://base.gateway.tenderly.co/${process.env.NEXT_PUBLIC_TENDERLY_RPC_KEY}`, + [CHAIN_ID.BASE_SEPOLIA]: `https://base-sepolia.gateway.tenderly.co/${process.env.NEXT_PUBLIC_TENDERLY_RPC_KEY}`, [CHAIN_ID.ZORA]: 'https://rpc.zora.energy', [CHAIN_ID.ZORA_SEPOLIA]: 'https://sepolia.rpc.zora.energy', [CHAIN_ID.FOUNDRY]: foundry.rpcUrls.default.http[0], diff --git a/apps/web/src/data/contract/chains.ts b/apps/web/src/data/contract/chains.ts index c09031c68..56159a279 100644 --- a/apps/web/src/data/contract/chains.ts +++ b/apps/web/src/data/contract/chains.ts @@ -9,7 +9,6 @@ import { zora, zoraSepolia, } from 'wagmi/chains' -import { alchemyProvider } from 'wagmi/providers/alchemy' import { jsonRpcProvider } from 'wagmi/providers/jsonRpc' import { PUBLIC_IS_TESTNET } from 'src/constants/defaultChains' @@ -29,9 +28,6 @@ export const L2_CHAINS = PUBLIC_IS_TESTNET const { chains, publicClient } = configureChains( [...TESTNET_CHAINS, ...MAINNET_CHAINS], [ - alchemyProvider({ - apiKey: process.env.NEXT_PUBLIC_ALCHEMY_ID as string, - }), jsonRpcProvider({ rpc: (chain) => ({ http: RPC_URL[chain.id as CHAIN_ID], diff --git a/apps/web/src/data/subgraph/requests/daoMetadata.ts b/apps/web/src/data/subgraph/requests/daoMetadata.ts index 1b1645044..7bc5f59c8 100644 --- a/apps/web/src/data/subgraph/requests/daoMetadata.ts +++ b/apps/web/src/data/subgraph/requests/daoMetadata.ts @@ -13,7 +13,7 @@ export const encodedDaoMetadataRequest = async ( if (!L1_CHAINS.find((x) => x === chain)) throw new Error('Only L1 Chains are supported') const res = await SDK.connect(chain) - .daoMetadata({ tokenAddress, first: 1000 }) + .daoMetadata({ tokenAddress: tokenAddress.toLowerCase(), first: 1000 }) .then((x) => x.dao?.metadataProperties) if (!res) throw new Error('No metadata found') diff --git a/apps/web/src/hooks/useDelayedGovernance.ts b/apps/web/src/hooks/useDelayedGovernance.ts index 496d6f668..b1ac01e4b 100644 --- a/apps/web/src/hooks/useDelayedGovernance.ts +++ b/apps/web/src/hooks/useDelayedGovernance.ts @@ -1,12 +1,14 @@ import { useContractRead } from 'wagmi' -import { governorAbi } from 'src/data/contract/abis' +import { governorAbi, tokenAbi } from 'src/data/contract/abis' import { AddressType, CHAIN_ID } from 'src/typings' export const useDelayedGovernance = ({ + tokenAddress, governorAddress, chainId, }: { + tokenAddress?: AddressType governorAddress?: AddressType chainId: CHAIN_ID }) => { @@ -17,6 +19,16 @@ export const useDelayedGovernance = ({ functionName: 'delayedGovernanceExpirationTimestamp', }) + const { data: remainingTokensInReserve } = useContractRead({ + abi: tokenAbi, + address: tokenAddress, + chainId, + functionName: 'remainingTokensInReserve', + }) + + if (remainingTokensInReserve === 0n) + return { delayedUntilTimestamp: 0, isGovernanceDelayed: false } + const isGovernanceDelayed = delayedUntilTimestamp ? new Date().getTime() < Number(delayedUntilTimestamp) * 1000 : false diff --git a/apps/web/src/layouts/DefaultLayout/Footer.tsx b/apps/web/src/layouts/DefaultLayout/Footer.tsx index 28d0162b1..65e2ca2d0 100644 --- a/apps/web/src/layouts/DefaultLayout/Footer.tsx +++ b/apps/web/src/layouts/DefaultLayout/Footer.tsx @@ -2,7 +2,6 @@ import { Box, Flex, Text } from '@zoralabs/zord' import Link from 'next/link' import React from 'react' -import { MadeWithZoraFooter } from './MadeWithZoraFooter' import { footerContent, footerLink, footerWrapper } from './Nav.styles.css' export const Footer = () => { @@ -79,8 +78,6 @@ export const Footer = () => { - - diff --git a/apps/web/src/layouts/DefaultLayout/MadeWithZoraFooter.tsx b/apps/web/src/layouts/DefaultLayout/MadeWithZoraFooter.tsx deleted file mode 100644 index 132ef9f56..000000000 --- a/apps/web/src/layouts/DefaultLayout/MadeWithZoraFooter.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { Box, Eyebrow, Flex } from '@zoralabs/zord' -import { Atoms } from '@zoralabs/zord' - -import Noggles from '../assets/noggles.svg' -import Zorb from '../assets/zorb.svg' -import { footerLogo, footerLogoTextLeft, footerLogoTextRight } from './Nav.styles.css' - -export type MadeWithZoraFooterProps = { - backgroundColor: Atoms['backgroundColor'] - color: Atoms['color'] - mb?: Atoms['mb'] -} - -export const MadeWithZoraFooter = ({ - backgroundColor, - color, - mb = 'x4', -}: MadeWithZoraFooterProps) => { - return ( - - - Made With - - - - - - - - - - - - On Ethereum - - - ) -} diff --git a/apps/web/src/layouts/HomeLayout/Footer.tsx b/apps/web/src/layouts/HomeLayout/Footer.tsx index 570ae3d04..722c21180 100644 --- a/apps/web/src/layouts/HomeLayout/Footer.tsx +++ b/apps/web/src/layouts/HomeLayout/Footer.tsx @@ -4,7 +4,6 @@ import React from 'react' import { Icon } from 'src/components/Icon' -import { MadeWithZoraFooter } from '../DefaultLayout/MadeWithZoraFooter' import { footerHeading, footerLeftWrapper, @@ -105,11 +104,6 @@ export const Footer = () => { - ) } diff --git a/apps/web/src/modules/auction/components/CurrentAuction/PlaceBid.tsx b/apps/web/src/modules/auction/components/CurrentAuction/PlaceBid.tsx index e15cde340..8abd5cb2d 100644 --- a/apps/web/src/modules/auction/components/CurrentAuction/PlaceBid.tsx +++ b/apps/web/src/modules/auction/components/CurrentAuction/PlaceBid.tsx @@ -1,4 +1,4 @@ -import { Box, Button, Flex } from '@zoralabs/zord' +import { Box, Button, Flex, PopUp, Text } from '@zoralabs/zord' import React, { Fragment, memo, useEffect, useState } from 'react' import useSWR, { useSWRConfig } from 'swr' import { formatEther, parseEther } from 'viem' @@ -6,7 +6,9 @@ import { Address, useAccount, useBalance, useContractReads, useNetwork } from 'w import { prepareWriteContract, waitForTransaction, writeContract } from 'wagmi/actions' import { ContractButton } from 'src/components/ContractButton' +import { Icon } from 'src/components/Icon/Icon' import AnimatedModal from 'src/components/Modal/AnimatedModal' +import { PUBLIC_IS_TESTNET } from 'src/constants/defaultChains' import SWR_KEYS from 'src/constants/swrKeys' import { auctionAbi } from 'src/data/contract/abis' import { averageWinningBid } from 'src/data/subgraph/requests/averageWinningBid' @@ -142,6 +144,8 @@ export const PlaceBid = ({ const isValidBid = bidAmount && isMinBid const isValidChain = wagmiChain?.id === chain.id + const [showTooltip, setShowTooltip] = useState(false) + const [copied, setCopied] = useState(false) return ( +
- - - Place bid - - + + + Place bid + + {chain.id !== 1 ? ( + + setShowTooltip(true)} + onMouseLeave={() => { + setShowTooltip(false) + setTimeout(() => { + setCopied(false) + }, 500) + }} + > + { + const network = PUBLIC_IS_TESTNET + ? 'https://testnet.nouns.build' + : 'https://nouns.build' + const baseUrl = `${network}/dao/${chain.name.toLowerCase()}/${ + addresses.token + }` + if (address === undefined) { + await navigator.clipboard.writeText(baseUrl) + return + } + const params = new URLSearchParams({ + referral: address.toString(), + }) + const fullUrl = `${baseUrl}?${params}` + + await navigator.clipboard.writeText(fullUrl) + setCopied(true) + }} + > + + + + } placement="top"> + {copied ? 'Copied' : 'Copy Referral Link'} + + + ) : null} + +
) : (