diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index d1179aa..0000000 --- a/.eslintrc +++ /dev/null @@ -1,25 +0,0 @@ -{ - "parser": "@typescript-eslint/parser", - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "plugin:react/recommended", - "plugin:@typescript-eslint/eslint-recommended", - "plugin:@typescript-eslint/recommended", - "prettier", - "next/core-web-vitals" - ], - "parserOptions": { - "ecmaFeatures": { - "jsx": true - }, - "ecmaVersion": 12, - "sourceType": "module" - }, - "plugins": ["react","@typescript-eslint"], - "rules": { - "@next/next/no-img-element": "off" - } -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7ce406a --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "extends": "next/core-web-vitals", + "rules": { + "semi": [2, "never"], + "space-before-blocks": "error", + "space-before-function-paren": [2, "never"], + "object-curly-spacing": ["error", "always"], + "indent": ["error", 2] + } +} diff --git a/components/Navigation.tsx b/components/Navigation.tsx index c423c18..4cfc346 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -1,4 +1,4 @@ -import Link from "next/link"; +import Link from "next/link" import { useRouter } from "next/router" const config = [ @@ -34,10 +34,9 @@ const config = [ export const Navigation = () => { const router = useRouter() - console.log(router.asPath); const isActivePath = (path: string) => router.asPath === path return ( -
+
{ config.map((item) => { return ( diff --git a/constants/index.ts b/constants/index.ts new file mode 100644 index 0000000..14b2ab3 --- /dev/null +++ b/constants/index.ts @@ -0,0 +1,20 @@ +export const RPC_URLS = [ + "https://speedy-nodes-nyc.moralis.io/cebf590f4bcd4f12d78ee1d4/polygon/mumbai", +] + +export const BLOCK_EXPLORER_URLS = ["https://explorer-mumbai.maticvigil.com/"] +export const SUPPORT_NETWORKS = [80001] +export const STORAGE_KEY_ACCOUNT = 'ethAccount' +export const STORAGE_KEY_ACCOUNT_SIG = 'sig_login' +export const DOMAIN = { + name: "DwebLab Alpha", + version: "1", + chainId: 80001, +} + +export const signInfo = { + types: { + Message: [{name: "content", type: "string"}], + }, + message: {content: "Sign this msg to login"} +} diff --git a/context/web3Context.ts b/context/web3Context.ts new file mode 100644 index 0000000..fb1d16b --- /dev/null +++ b/context/web3Context.ts @@ -0,0 +1,68 @@ +import {createContext, Dispatch, useContext} from "react" + +type StateType = { + provider?: any + web3Provider?: any + account?: string + chainId?: number +} + +export const initialWeb3State: StateType = { + provider: null, + web3Provider: null, + account: null, + chainId: null, +} + +type ActionType = + | { + type: 'SET_WEB3_PROVIDER' + provider?: StateType['provider'] + web3Provider?: StateType['web3Provider'] + account?: StateType['account'] + chainId?: StateType['chainId'] +} + | { + type: 'SET_ACCOUNT' + account?: StateType['account'] +} + | { + type: 'SET_CHAIN_ID' + chainId?: StateType['chainId'] +} + | { + type: 'RESET_WEB3_PROVIDER' +} + +export function web3Reducer(state: StateType, action: ActionType): StateType { + switch (action.type) { + case 'SET_WEB3_PROVIDER': + return { + ...state, + provider: action.provider, + web3Provider: action.web3Provider, + account: action.account, + chainId: action.chainId, + } + case 'SET_ACCOUNT': + return { + ...state, + account: action.account, + } + case 'SET_CHAIN_ID': + return { + ...state, + chainId: action.chainId, + } + case 'RESET_WEB3_PROVIDER': + return initialWeb3State + default: + throw new Error() + } +} + + +export const Web3Context = createContext<{ state: StateType, dispatch: Dispatch | undefined}>({ state: initialWeb3State, dispatch: undefined}); +export function useWeb3Context() { + return useContext(Web3Context); +} \ No newline at end of file diff --git a/hooks/useAccount.ts b/hooks/useAccount.ts new file mode 100644 index 0000000..4877d32 --- /dev/null +++ b/hooks/useAccount.ts @@ -0,0 +1,6 @@ +import {useWeb3Context} from "../context/web3Context"; + +export const useAccount = () => { + const { state} = useWeb3Context() + return state.account +} \ No newline at end of file diff --git a/hooks/useChainId.ts b/hooks/useChainId.ts new file mode 100644 index 0000000..3685b3a --- /dev/null +++ b/hooks/useChainId.ts @@ -0,0 +1,6 @@ +import {useWeb3Context} from "../context/web3Context"; + +export const useChainId = () => { + const { state: { chainId }} = useWeb3Context() + return chainId +} \ No newline at end of file diff --git a/hooks/useProvider.ts b/hooks/useProvider.ts new file mode 100644 index 0000000..0bf9e54 --- /dev/null +++ b/hooks/useProvider.ts @@ -0,0 +1,6 @@ +import {useWeb3Context} from "../context/web3Context"; + +export const useProvider = () => { + const { state: { provider }} = useWeb3Context() + return provider +} diff --git a/hooks/useWeb3.ts b/hooks/useWeb3.ts new file mode 100644 index 0000000..3eb1f70 --- /dev/null +++ b/hooks/useWeb3.ts @@ -0,0 +1,6 @@ +import {useWeb3Context} from "../context/web3Context"; + +export const useWeb3 = () => { + const { state: { web3Provider }} = useWeb3Context() + return web3Provider +} \ No newline at end of file diff --git a/package.json b/package.json index 161fad3..5992706 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@react-hookz/web": "^12.0.4", "@textile/eth-storage": "^1.0.0", "axios": "^0.24.0", + "bignumber.js": "^9.0.2", "ethers": "^5.5.2", "graphql-tag": "^2.12.6", "ipfs-http-client": "^55.0.0", @@ -27,6 +28,7 @@ "react-dom": "^17.0.2", "react-hook-form": "^7.22.5", "react-markdown": "^7.1.1", + "web3": "^1.6.1", "web3modal": "^1.9.4", "yup": "^0.32.11" }, diff --git a/pages/_app.js b/pages/_app.js deleted file mode 100644 index e9f4b7a..0000000 --- a/pages/_app.js +++ /dev/null @@ -1,263 +0,0 @@ -import "../styles/globals.css" -import "../styles/markdown.css" -import { FrontendVersion } from "../version.js" -import Head from "next/head" -import { useState } from "react" -import { ethers } from "ethers" -import { Menu, Transition } from "@headlessui/react" -import { Fragment, useEffect } from "react" -import { ChevronDownIcon } from "@heroicons/react/solid" -import {Navigation} from "../components/Navigation"; -import axios from "axios" - -// On production, you should use something like web3Modal -// to support additional wallet providers, like WalletConnect - -let provider - -function Marketplace({ Component, pageProps }) { - const [ethAccount, setethAccount] = useState(null) - const [Logined, setLogined] = useState(false) - const [loadingState, setLoadingState] = useState("not-loaded") - const [BackendVersion, setBackendVersion] = useState("err") - - useEffect(() => { - if (typeof window !== "undefined") { - const aethAccount = localStorage.getItem("ethAccount") - if (aethAccount) { - setethAccount(aethAccount) - loginSig() - setLogined(true) - } - async function listenMMAccount() { - window.ethereum.on("accountsChanged", async function () { - const accounts = await window.ethereum.request({ - method: "eth_requestAccounts", - }) - if (accounts[0] != localStorage.getItem("ethAccount")) { - console.log("Got new ethAccount", accounts[0]) - localStorage.setItem("ethAccount", accounts[0]) - localStorage.removeItem("sig_login") - setethAccount(aethAccount) - loginSig() - } - }) - } - listenMMAccount() - getBackendVersion() - } - }, []) - - async function getBackendVersion() { - try { - const dweb_search_ver_api = "https://dweb-search-api.anwen.cc/version" - const ret = await axios.get(dweb_search_ver_api) - if (ret.status == 200 && 'version' in ret.data) { - setBackendVersion(ret.data['version']) - } - } catch (error) { - console.log(error) - } - } - - async function loginSig() { - // change network and sig login - const sig_login = localStorage.getItem("sig_login") - if (sig_login) { - console.log("sig_login already done", sig_login) - return - } - await addPolygonTestnetNetwork() - const provider = new ethers.providers.Web3Provider(window.ethereum) - const signer = provider.getSigner() - const types = { - Message: [{ name: "content", type: "string" }], - } - const domain = { - name: "DwebLab Alpha", - version: "1", - chainId: 80001, - } - const message = { - content: "Sign this msg to login", - } - signer.getAddress().then((walletAddress) => { - signer._signTypedData(domain, types, message).then((signature) => { - let verifiedAddress = ethers.utils.verifyTypedData( - domain, - types, - message, - signature, - ) - if (verifiedAddress !== walletAddress) { - alert(`Signed by: ${verifiedAddress}\r\nExpected: ${walletAddress}`) - } else { - localStorage.setItem("sig_login", signature) - } - console.log("signature", signature) - }) - }) - setLoadingState("loaded") - } - - async function addPolygonTestnetNetwork() { - try { - await ethereum.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: "0x13881" }], // Hexadecimal version of 80001, prefixed with 0x - }) - } catch (error) { - if (error.code === 4902) { - try { - await ethereum.request({ - method: "wallet_addEthereumChain", - params: [ - { - chainId: "0x13881", // Hexadecimal version of 80001, prefixed with 0x - chainName: "POLYGON Mumbai", - nativeCurrency: { - name: "MATIC", - symbol: "MATIC", - decimals: 18, - }, - rpcUrls: [ - "https://speedy-nodes-nyc.moralis.io/cebf590f4bcd4f12d78ee1d4/polygon/mumbai", - ], - blockExplorerUrls: ["https://explorer-mumbai.maticvigil.com/"], - iconUrls: [""], - }, - ], - }) - } catch (addError) { - console.log("Did not add network") - } - } - } - } - - async function ConnectWallet() { - // if (window.ethereum) - await window.ethereum.enable() - const accounts = await window.ethereum.request({ - method: "eth_requestAccounts", - }) - if (accounts.length > 0) { - setethAccount(accounts[0]) - console.log("Got ethAccount", accounts[0]) - if (typeof window !== "undefined") { - localStorage.setItem("ethAccount", accounts[0]) - } - loginSig() - setLogined(true) - } - } - - async function DisconnectWallet() { - setLogined(false) - setethAccount(null) - console.log("Killing the wallet connection", provider) - if (provider && provider.close) { - await provider.close() - provider = null - } - if (typeof window !== "undefined") { - localStorage.removeItem("ethAccount") - localStorage.removeItem("sig_login") - } - } - - function getBrief(astr) { - if (!astr) return "" - return astr.substring(0, 6) + "..." + astr.substr(astr.length - 4) - } - - return ( -
- - Creative Comomons NFT Playground - - - - - - - - - -
- ) -} - -export default Marketplace diff --git a/pages/_app.tsx b/pages/_app.tsx new file mode 100644 index 0000000..9587541 --- /dev/null +++ b/pages/_app.tsx @@ -0,0 +1,234 @@ +import "../styles/globals.css" +import "../styles/markdown.css" +import Head from "next/head" +import { ethers, providers } from "ethers" +import { Menu, Transition } from "@headlessui/react" +import { Fragment, useCallback, useMemo, useReducer, useState } from "react" +import { ChevronDownIcon } from "@heroicons/react/solid" +import { Navigation } from "../components/Navigation" +import { + DOMAIN, + signInfo, + STORAGE_KEY_ACCOUNT, + STORAGE_KEY_ACCOUNT_SIG, + SUPPORT_NETWORKS +} from "../constants" +import { useAsync, useLocalStorageValue, useMountEffect } from "@react-hookz/web" +import { initialWeb3State, Web3Context, web3Reducer } from "../context/web3Context" +import { createProvider, switchNetwork } from "../web3" +import { getBrief } from "../web3/utils" +import axios from "axios" +import { FrontendVersion } from "../version.js" + + +function App({ Component, pageProps }) { + const [accountInLocal, setLocalAccount, removeLocalAccount] = useLocalStorageValue(STORAGE_KEY_ACCOUNT) + const [sigInLocal, setLocalSig, removeLocalSig] = useLocalStorageValue(STORAGE_KEY_ACCOUNT_SIG) + const [state, dispatch] = useReducer(web3Reducer, { ...initialWeb3State, account: accountInLocal }) + const { account, provider, chainId } = state + const isSupportCurrentNetwork = SUPPORT_NETWORKS.includes(chainId) + const [backendVersion, setBackendVersion] = useState("err") + + const [, actions] = useAsync(async() => { + if (!sigInLocal || !accountInLocal) return + const cachedProvider = await createProvider(undefined, (id) => dispatch({ type: "SET_CHAIN_ID", chainId: id })) + if (!cachedProvider) return + dispatch({ type: 'SET_WEB3_PROVIDER', provider: cachedProvider }) + const web3Provider = new providers.Web3Provider(cachedProvider) + const signer = web3Provider.getSigner() + const address = await signer.getAddress() + const network = await web3Provider.getNetwork() + if (address !== accountInLocal) return + dispatch({ + type: 'SET_WEB3_PROVIDER', + provider: cachedProvider, + web3Provider, + account: address, + chainId: network.chainId, + }) + }) + + useMountEffect(actions.execute) + useMountEffect(getBackendVersion) + + async function getBackendVersion() { + try { + const dweb_search_ver_api = "https://dweb-search-api.anwen.cc/version" + const ret = await axios.get(dweb_search_ver_api) + if (ret.status == 200 && 'version' in ret.data) { + setBackendVersion(ret.data['version']) + } + } catch (error) { + console.log(error) + } + } + + const web3ContextValue = useMemo(() => { + return { state, dispatch } + }, [state, dispatch]) + + const connectWallet = useCallback(async function() { + const provider = await createProvider(undefined, (id) => dispatch({ type: "SET_CHAIN_ID", chainId: id })) + if (provider.chainId !== '0x13881') { + await switchNetwork(provider) + } + if (!provider) return + dispatch({ type: 'SET_WEB3_PROVIDER', provider }) + + const web3Provider = new providers.Web3Provider(provider) + const signer = web3Provider.getSigner() + const address = await signer.getAddress() + + if (!sigInLocal) { + const signature = await signer._signTypedData(DOMAIN, signInfo.types, signInfo.message) + const verifiedAddress = ethers.utils.verifyTypedData( + DOMAIN, + signInfo.types, + signInfo.message, + signature, + ) + if (verifiedAddress !== address) return + setLocalSig(signature) + } + + const network = await web3Provider.getNetwork() + setLocalAccount(address) + + dispatch({ + type: 'SET_WEB3_PROVIDER', + provider, + web3Provider, + account: address, + chainId: network.chainId, + }) + }, []) + + const disconnectWallet = async() => { + dispatch({ + type: 'SET_WEB3_PROVIDER', + provider: undefined, + web3Provider: undefined, + account: undefined, + chainId: undefined, + }) + removeLocalAccount() + removeLocalSig() + } + + + const renderActionButton = () => { + if (!sigInLocal || !accountInLocal) { + return ( + + ) + } + + if (!isSupportCurrentNetwork) { + return ( + + ) + + } + return null + } + return ( +
+ + Creative Comomons NFT Playground + + + + + + + + +
+ ) +} + +export default App diff --git a/pages/all-assets.js b/pages/all-assets.js index cc6625f..44fd498 100644 --- a/pages/all-assets.js +++ b/pages/all-assets.js @@ -7,22 +7,17 @@ import { nftaddress, nftmarketaddress } from "../config" import NFT from "../artifacts/contracts/NFT.sol/NFT.json" import Market from "../artifacts/contracts/Market.sol/NFTMarket.json" +import { useWeb3 } from "../hooks/useWeb3" export default function AllAssets() { const [nfts, setNfts] = useState([]) const [sold, setSold] = useState([]) const [loadingState, setLoadingState] = useState("not-loaded") + const provider = useWeb3() useEffect(() => { loadNFTs() }, []) async function loadNFTs() { - const web3Modal = new Web3Modal({ - network: "mainnet", - cacheProvider: true, - }) - const connection = await web3Modal.connect() - // const provider = new ethers.providers.JsonRpcProvider() - const provider = new ethers.providers.Web3Provider(connection) const signer = provider.getSigner() // const marketContract = new ethers.Contract(nftmarketaddress, Market.abi, provider) @@ -36,7 +31,7 @@ export default function AllAssets() { // const data = await marketContract.fetchItemsCreated() const items = await Promise.all( - data.map(async (i) => { + data.map(async(i) => { const tokenUri = await tokenContract.tokenURI(i.tokenId) const meta = await axios.get(tokenUri) let price = ethers.utils.formatUnits(i.price.toString(), "ether") diff --git a/pages/article.js b/pages/article.js index bf60c6b..97034ff 100644 --- a/pages/article.js +++ b/pages/article.js @@ -15,6 +15,7 @@ import Market from "../artifacts/contracts/Market.sol/NFTMarket.json" import { providers } from "ethers" import { init } from "@textile/eth-storage" +import { useWeb3 } from "../hooks/useWeb3" let ethAccount let myethAccount @@ -22,6 +23,7 @@ let cid let nft = {} export default function MyAssets() { // const [nft, setNft] = useState({}) + const provider = useWeb3() const [loadingState, setLoadingState] = useState("not-loaded") const router = useRouter() @@ -45,10 +47,7 @@ export default function MyAssets() { } async function storeNFTtoFilecoin() { - await window.ethereum.enable() - const provider = new providers.Web3Provider(window.ethereum) const wallet = provider.getSigner() - const storage = await init(wallet) // const blob = new Blob(["Hello, world!"], { type: "text/plain" }); const jsonse = JSON.stringify(nft) @@ -75,9 +74,6 @@ export default function MyAssets() { } async function createSale(url) { - const web3Modal = new Web3Modal() - const connection = await web3Modal.connect() - const provider = new ethers.providers.Web3Provider(connection) const signer = provider.getSigner() /* next, create the item */ diff --git a/pages/articles-my.js b/pages/articles-my.js index 50a9f78..9577ec8 100644 --- a/pages/articles-my.js +++ b/pages/articles-my.js @@ -1,6 +1,7 @@ import { ethers } from "ethers" import { useEffect, useState } from "react" import axios from "axios" +import { useAccount } from "../hooks/useAccount" import { nftmarketaddress, nftaddress } from "../config" @@ -8,10 +9,7 @@ let ethAccount export default function MyAssets() { const [nfts, setNfts] = useState([]) const [loadingState, setLoadingState] = useState("not-loaded") - - if (typeof window !== "undefined") { - ethAccount = localStorage.getItem("ethAccount") - } + const ethAccount = useAccount() useEffect(() => { loadNFTs() diff --git a/pages/articles.js b/pages/articles.js index 7ed1af6..f0fdb60 100644 --- a/pages/articles.js +++ b/pages/articles.js @@ -4,9 +4,11 @@ import axios from "axios" import { useRouter } from "next/router" import { nftmarketaddress, nftaddress } from "../config" +import { useAccount } from "../hooks/useAccount" let ethAccount export default function MyAssets() { + const account = useAccount() const [nfts, setNfts] = useState([]) const [loadingState, setLoadingState] = useState("not-loaded") @@ -14,7 +16,6 @@ export default function MyAssets() { // },[]); const router = useRouter() - console.log(router.query) if ("author" in router.query) { ethAccount = router.query.author console.log("ethAccount", ethAccount) diff --git a/pages/ccmarket.js b/pages/ccmarket.js index 29065db..d2f30a0 100644 --- a/pages/ccmarket.js +++ b/pages/ccmarket.js @@ -29,7 +29,7 @@ export default function Home() { const data = await marketContract.fetchMarketItems() const items = await Promise.all( - data.map(async (i) => { + data.map(async(i) => { const tokenUri = await tokenContract.tokenURI(i.tokenId) const meta = await axios.get(tokenUri) console.log(i.price.toString(), "raw price") diff --git a/pages/create.tsx b/pages/create.tsx index 6934c7f..f6a570f 100644 --- a/pages/create.tsx +++ b/pages/create.tsx @@ -1,16 +1,17 @@ -import {useRouter} from "next/router" +import { useRouter } from "next/router" import axios from "axios" -import {useForm} from "react-hook-form"; -import {yupResolver} from '@hookform/resolvers/yup'; -import * as yup from "yup"; -import {addNFTToNFTStorage} from "../services/NFTStorage"; -import {addToIPFS} from "../services/IPFSHttpClient"; +import { useForm } from "react-hook-form" +import { yupResolver } from '@hookform/resolvers/yup' +import * as yup from "yup" +import { addNFTToNFTStorage } from "../services/NFTStorage" +import { addToIPFS } from "../services/IPFSHttpClient" -import {nftaddress, nftmarketaddress} from "../config" +import { nftaddress, nftmarketaddress } from "../config" import NFT from "../artifacts/contracts/NFT.sol/NFT.json" import Market from "../artifacts/contracts/Market.sol/NFTMarket.json" -import {useEffect, useState} from "react"; -import {InputFieldError} from "../components/InputFieldError"; +import { useEffect, useState } from "react" +import { InputFieldError } from "../components/InputFieldError" +import { useAccount } from "../hooks/useAccount" interface IFormInputs { price: string @@ -27,31 +28,26 @@ const schema = yup.object({ description: yup.string().required("Content is not optional"), s_tags: yup.string().required("Tags is not optional"), author: yup.string().required("Authors Name is not optional"), - files: yup.mixed().test({ test: (value) => value.length, message: "Feature Image is not optional"}), -}).required(); + files: yup.mixed().test({ test: (value) => value.length, message: "Feature Image is not optional" }), +}).required() export default function CreateItem() { const router = useRouter() - // TODO: create useAccount Hook and listen account change - const [ethAccount, setEthAccount] = useState() + const account = useAccount() const [preview, setPreview] = useState() useEffect(() => { - if (typeof window !== "undefined") { - const account = localStorage.getItem("ethAccount") - if (!account) { - alert("No ETH Account, Please login") - router.push("/articles-all") - return - } - setEthAccount(account) + if (!account) { + alert("No ETH Account, Please login") + router.push("/articles-all") + return } }, []) - const {register, handleSubmit, formState: {errors, isSubmitting}, watch} = useForm({ + const { register, handleSubmit, formState: { errors, isSubmitting }, watch } = useForm({ resolver: yupResolver(schema) - }); - const watchedFiles = watch("files", null); + }) + const watchedFiles = watch("files", null) useEffect(() => { if (!watchedFiles) return @@ -61,9 +57,9 @@ export default function CreateItem() { setPreview(url) }, [watchedFiles]) - const onSubmit = async (data: IFormInputs) => { + const onSubmit = async(data: IFormInputs) => { const file = data.files[0] - const {type: filetype, size: filesize, name: filename} = file + const { type: filetype, size: filesize, name: filename } = file const addedImage = await addToIPFS(file) const imageURL = `https://ipfs.infura.io/ipfs/${addedImage.path}` const license = "CC-BY-SA" @@ -72,7 +68,7 @@ export default function CreateItem() { const authors = [{ name: data.author, wallet: { - eth: ethAccount, + eth: account, }, }] const nftData = JSON.stringify({ @@ -99,7 +95,7 @@ export default function CreateItem() { axios.defaults.headers.common['address'] = aethAccount const ret = await axios.post(dweb_search_url, { path: addedNFT.path, - eth: ethAccount, + eth: account, name: data.name, image: imageURL, tags: data.s_tags, @@ -145,15 +141,15 @@ export default function CreateItem() {
-

-- Markdown Tips:   - 参考1  - 参考2 -

-