【KryptoCamp】Office Hour
  • Season 2 2022/2-4
    • Outline
      • 晚間學習班課綱
      • 助教課課綱、直播 & 回放連結
    • OH Week1 Overview
      • MetaMask & Other Wallets
      • NFT Website Development
      • 實作區塊 0 的產生
      • Reference, Resources & AMA
    • OH Week3 Overview
      • 設計多人管理的智能合約保管箱 (Optional)
      • 架設私有區塊鏈發送第一筆交易(Optional)
      • 發行代幣 Token (Optional)
      • Reference, Resources & AMA
    • OH Week4 Overview
      • Supply Chain Management System(Optional)
      • 荷蘭拍(Optional)
      • Reference, Resources & AMA
    • OH Week5 Overview
      • DAPP
      • Reference, Resources & AMA
    • OH Week6 Overview
      • NFT - Layers Random Blending
      • NFT - Smart Contract
      • KYC Whitelisting 白名單設計(Optional)
      • Reference, Resources & AMA
    • OH Week7 Overview
      • DAPP - Minting Functionality
      • NFT 稀有度與持有人排行榜(Optional)
      • Reference, Resources & AMA
  • Season 1 2022/1
    • Outline
    • OH Week1 Overview
    • OH Week2 Overview
    • OH Week3 & Week4 Overview
Powered by GitBook
On this page
  • NFT Website Development
  • | Login System |
  • | Verify System |
  • | Produce NFT |
  • | Deploy NFT |
  • | Minting dApp |
  • Reference and Some Resources
  1. Season 1 2022/1

OH Week3 & Week4 Overview

NFT Website Development

「在 NFT 商品網站中與以太坊錢包 - MetaMask 連動,並且認證錢包登入者是否持有我們發行的 NFT。後讓登入者進入 NFT holders 的專屬 VIP 區域進行互動。」以及「產品上鏈工程 & 實作 Minting dAPP」

login=>start: Login Flow (Login System)
verify=>operation: Tokens Owner Check Flow  (Verify System)
produce=>operation: Produce NFT Flow
deploy=>operation: Deploy NFT Flow
dApp=>end: Minting dAPP Flow

login->verify->produce->deploy->dApp
  1. Login Flow (Login System) (react.js, MetaMask@onboarding, web3.js)

  2. Tokens Owner Check Flow (Verify System) (react.js, web3.js, ethers.js, opensea.js)

  3. Produce NFT Flow (python, JavaScript)

  4. Deploy NFT Flow (Solidity, Etherscan)

  5. Minting dAPP Flow (react.js, web3.js, ethers.js, Solidity)

| Login System |

【錢包登入區的前端建置】

Create a react app

目前的模板:

import * as React from 'react';
import AppBar from '@mui/material/AppBar';
import Button from '@mui/material/Button';
import ViewInArTwoToneIcon from '@mui/icons-material/ViewInArTwoTone';
import Card from '@mui/material/Card';
import CardActions from '@mui/material/CardActions';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import CssBaseline from '@mui/material/CssBaseline';
import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';
import Box from '@mui/material/Box';
import Toolbar from '@mui/material/Toolbar';
import Typography from '@mui/material/Typography';
import Container from '@mui/material/Container';
import Link from '@mui/material/Link';
import { createTheme, ThemeProvider } from '@mui/material/styles';

function Copyright() {
  return (
    <Typography variant="body2" color="text.secondary" align="center">
      {'Copyright © '}
      <Link color="inherit" href="https://github.com/ChiHaoLu">
        ChiHaoLu
      </Link>{' '}
      {new Date().getFullYear()}
      {'.'}
    </Typography>
  );
}

const cards = [1, 2, 3];

const theme = createTheme({
  palette: {
    primary: {
      light: '#757ce8',
      main: '#fb8c00',
      dark: '#002884',
      contrastText: '#fff',
    },
    secondary: {
      light: '#ff7961',
      main: '#3d5afe',
      dark: '#ba000d',
      contrastText: '#000',
    },
  },
});

export default function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <AppBar position="relative">
        <Toolbar>
          <ViewInArTwoToneIcon sx={{ mr: 2 }} />
          <Typography variant="h6" color="inherit" noWrap>
            KilliFish
          </Typography>
        </Toolbar>
      </AppBar>
      <main>
        {/* Hero unit */}
        <Box
          sx={{
            bgcolor: 'background.paper',
            pt: 8,
            pb: 6,
          }}
        >
          <Container maxWidth="sm">
            <Typography
              component="h1"
              variant="h2"
              align="center"
              color="text.primary"
              gutterBottom
            >
              KilliFish
            </Typography>
            <Typography variant="h5" align="center" color="text.secondary" paragraph>
              The best courses platform for learning web3!
            </Typography>
            <Stack
              sx={{ pt: 4 }}
              direction="row"
              spacing={2}
              justifyContent="center"
            >
              <Button variant="contained">Login By MataMask</Button>
              <Button variant="outlined">Verify Your Certificate</Button>
            </Stack>
          </Container>
        </Box>
        <Container sx={{ py: 8 }} maxWidth="md">
          {/* End hero unit */}
          <Grid container spacing={4}>
            {cards.map((card) => (
              <Grid item key={card} xs={12} sm={6} md={4}>
                <Card
                  sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}
                >
                  <CardMedia
                    component="img"
                    sx={{
                      // 16:9
                      pt: '56.25%',
                    }}
                    image="https://source.unsplash.com/random"
                    alt="random"
                  />
                  <CardContent sx={{ flexGrow: 1 }}>
                    <Typography gutterBottom variant="h5" component="h2">
                      Courses
                    </Typography>
                    <Typography>
                      Join this Courses Free!
                    </Typography>
                  </CardContent>
                  <CardActions>
                    <Button size="small">View</Button>
                    <Button size="small">Enroll</Button>
                  </CardActions>
                </Card>
              </Grid>
            ))}
          </Grid>
        </Container>
      </main>
      {/* Footer */}
      <Box sx={{ bgcolor: 'background.paper', p: 6 }} component="footer">
        <Typography variant="h6" align="center" gutterBottom>
          killifish.eth
        </Typography>
        <Typography
          variant="subtitle1"
          align="center"
          color="text.secondary"
          component="p"
        >
         The best courses platform for learning web3!
        </Typography>
        <Copyright />
      </Box>
      {/* End footer */}
    </ThemeProvider>
  );
}

【連動 Metamask】


| Verify System |

import * as React from 'react';
import { styled } from '@mui/material/styles';
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Collapse from '@mui/material/Collapse';
import Avatar from '@mui/material/Avatar';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import SchoolIcon from '@mui/icons-material/School';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import { useState } from "react";
import Container from '@mui/material/Container';
import FactCheckIcon from '@mui/icons-material/FactCheck';

const ExpandMore = styled((props) => {
    const { expand, ...other } = props;
    return <IconButton {...other} />;
})(({ theme, expand }) => ({
    transform: !expand ? 'rotate(0deg)' : 'rotate(180deg)',
    marginLeft: 'auto',
    transition: theme.transitions.create('transform', {
        duration: theme.transitions.duration.shortest,
    }),
}));

export function MyProfile() {
    const [expanded, setExpanded] = useState(false);
    const handleExpandClick = () => {
        setExpanded(!expanded);
    };

    return (
        <Container sx={{ py: 6 }} maxWidth="md">
            <Card sx={{ maxWidth: 555 }}>
                <CardHeader
                    avatar={
                        <Avatar sx={{ bgcolor: "#A63446" }} aria-label="recipe">
                            0x
                        </Avatar>
                    }

                    title={accounts}
                />
                <CardContent>
                    <Typography variant="body2" color="text.secondary">
                        兔美。<br /> 小學生,是一隻兔子,有名偵探的美稱,在有靈感(分析出「重要」案情時)的時候目光會變得銳利。
                    </Typography>
                </CardContent>
                <CardActions disableSpacing>
                    <IconButton href="/" aria-label="home" color="secondary">
                        <SchoolIcon />
                    </IconButton>
                    <IconButton href="/verify" aria-label="check" color="secondary">
                        <FactCheckIcon />
                    </IconButton>
                    <ExpandMore
                        expand={expanded}
                        onClick={handleExpandClick}
                        aria-expanded={expanded}
                        aria-label="show more"
                        color="secondary"
                    >
                        <ExpandMoreIcon />
                    </ExpandMore>
                </CardActions>
                <Collapse in={expanded} timeout="auto" unmountOnExit>
                    <CardContent>
                        <Typography paragraph>《搞笑漫畫日和》</Typography>
                        <Typography paragraph>
                            《搞笑漫畫日和》(日語:ギャグマンガ日和)是日本漫畫家増田こうすけ創作的日本漫畫作品。於集英社漫畫雜誌《月刊少年JUMP》2000年1月號開始連載。之後因為雜誌休刊的關係,改為《Jump Square》,連載至2015年12月號完結。單行本全15卷。現在改標題為《搞笑漫畫日和GB》(ギャグマンガ日和GB)於《Jump Square》上連載。

                            動畫共有四部,最新的一部是為紀念漫畫連載11周年於2010年1月開始播放的第4季動畫「搞笑漫畫日和+」。每集動畫的長度約為5分鐘。
                        </Typography>
                    </CardContent>
                </Collapse>
            </Card>
        </Container>
    );
}

【Smart Contract, and how do we get the token's owner?】

【Who is login now?】

import * as React from 'react';
import Box from '@mui/material/Box';
import TextField from '@mui/material/TextField';
import MenuItem from '@mui/material/MenuItem';
import InputLabel from '@mui/material/InputLabel';
import InputAdornment from '@mui/material/InputAdornment';
import FormControl from '@mui/material/FormControl';
import Input from '@mui/material/Input';
import Button from '@mui/material/Button';
import { useState } from "react";
import Alert from '@mui/material/Alert';
import IconButton from '@mui/material/IconButton';
import Collapse from '@mui/material/Collapse';
import CloseIcon from '@mui/icons-material/Close';
import * as web3 from 'web3'
import { OpenSeaPort, Network } from 'opensea-js'
import AccessTimeIcon from '@mui/icons-material/AccessTime';
import { NFTE } from '@nfte/react';

const provider = new web3.providers.HttpProvider('https://mainnet.infura.io')

const seaport = new OpenSeaPort(provider, {
    networkName: Network.Main
})

const ERCTypes = [
    {
        value: 'ERC20',
        label: 'ERC-20 (Constructing)',
    },
    {
        value: 'ERC721',
        label: 'ERC-721 ',
    },
    {
        value: 'ERC1155',
        label: 'ERC-1155 (Constructing)',
    },
];

const Classes = [
    {
        value: '0x67d9417c9c3c250f61a83c7e8658dac487b56b09',
        label: 'DApp and Smart Contract Development (PHANTA BEAR)',
        // PHANTA BEAR: https://etherscan.io/address/0x67d9417c9c3c250f61a83c7e8658dac487b56b09
    },
    {
        value: 'smartcontract_address_2',
        label: 'Enterprise-level Consortium Blockchain Development (Constructing)',
    },
    {
        value: 'smartcontract_address_3',
        label: 'Underlying Architecture of Blockchain (Constructing)',
    },
];

export function MyVerify(props) {
    const [erc, setERC] = useState('ERC721');
    const [myclass, setClass] = useState('0x67d9417c9c3c250f61a83c7e8658dac487b56b09');
    const [values, setValues] = useState({
        id: ""
    });
    // tokenId: 7476 -- https://opensea.io/assets/0x67d9417c9c3c250f61a83c7e8658dac487b56b09/7476
    const [open, setOpen] = useState(false);
    const [account, setAccount] = useState("0x27b00e6109f246d9d42aaf4a12f0ae35fc4bde71");
    // owner of 7476: https://opensea.io/0x27b00e6109f246d9d42aaf4a12f0ae35fc4bde71
    const [ownership, setOwnerShip] = useState(false)
    const [fetching, setFetching] = useState(false);

    const handleChange = (prop) => (event) => {
        setValues({ ...values, [prop]: event.target.value });
    };

    const handleChange_ERC = (event) => {
        setERC(event.target.value);
    };

    const handleChange_Class = (event) => {
        setClass(event.target.value);
    };

    // useEffect(() => {
    //     window.ethereum
    //         .request({ method: 'eth_requestAccounts' })
    //         .then((newAccounts) => setAccount(newAccounts[0]));
    // }, []);

    const fetchData = async () => {
        // 使用 await 等待 API 取得回應後才繼續
        const balance = await seaport.getAssetBalance({
            accountAddress: account,
            asset: {
                tokenAddress: myclass,
                tokenId: values.id,
                schemaName: erc
            },
        })
        console.log(balance.toString());
        (balance.toString() === "1" ? setOwnerShip(true) : setOwnerShip(false));
    };

    return (
        <div>
            <Box
                component="form"
                sx={{
                    '& .MuiTextField-root': { m: 1, width: '25ch' },
                }}
                noValidate
                autoComplete="off"
            >
                <div>
                    <TextField
                        id="filled-select-currency"
                        select
                        label="Select ERC"
                        value={erc}
                        onChange={handleChange_ERC}
                        helperText="Please select your ERC"
                        variant="filled"
                    >
                        {ERCTypes.map((option) => (
                            <MenuItem key={option.value} value={option.value}>
                                {option.label}
                            </MenuItem>
                        ))}
                    </TextField>
                    <TextField
                        id="filled-select-class"
                        select
                        label="Select Class"
                        value={myclass}
                        onChange={handleChange_Class}
                        helperText="Please select your Classes"
                        variant="filled"
                    >
                        {Classes.map((option) => (
                            <MenuItem key={option.value} value={option.value}>
                                {option.label}
                            </MenuItem>
                        ))}
                    </TextField>
                </div>
            </Box>
            <Box sx={{ display: 'flex', flexWrap: 'wrap' }}>
                <FormControl fullWidth sx={{ m: 1, width: '25ch' }} variant="standard">
                    <InputLabel htmlFor="standard-adornment-id">Your Certificate ID</InputLabel>
                    <Input
                        id="standard-adornment-id"
                        value={values.id}
                        onChange={handleChange('id')}
                        startAdornment={<InputAdornment position="start">ID - </InputAdornment>}
                    />
                </FormControl>
                <Button sx={{ m: 2, width: '26.5ch' }}
                    onClick={() => {
                        setFetching(true);
                        setTimeout(() => {
                            setFetching(false);
                        }, 5000);
                        console.log(erc, myclass, account, values.id);
                        fetchData();
                        setOpen(true)
                    }}
                    variant="contained">Verify</Button>
                <Box sx={{ width: '100%' }}>
                    <Collapse in={open && ownership && !fetching}>
                        <Alert
                            severity="success"
                            action={
                                <IconButton
                                    aria-label="close"
                                    color="inherit"
                                    size="small"
                                    onClick={() => {
                                        setOpen(false);
                                    }}
                                >
                                    <CloseIcon fontSize="inherit" />
                                </IconButton>
                            }
                            sx={{ mb: 2 }}
                        >
                            Verify Successfully!
                        </Alert>
                        <NFTE 
                            contract={myclass}
                            tokenId={values.id}
                            darkMode={true} />
                    </Collapse>
                    <Collapse in={open && fetching}>
                        <Alert
                            severity="info"
                            sx={{ mb: 2 }}
                            icon={<AccessTimeIcon fontSize="inherit" />}
                        >
                            Now Verifying...
                        </Alert>
                    </Collapse>
                    <Collapse in={open && !ownership && !fetching}>
                        <Alert
                            severity="error"
                            action={
                                <IconButton
                                    aria-label="close"
                                    color="inherit"
                                    size="small"
                                    onClick={() => {
                                        setOpen(false);
                                    }}
                                >
                                    <CloseIcon fontSize="inherit" />
                                </IconButton>
                            }
                            sx={{ mb: 2 }}
                        >
                            Verify Failed!
                        </Alert>
                    </Collapse>
                </Box>
            </Box>
        </div>
    );
}

【Which is your token?】


| Produce NFT |

【Layers Blending & MetaData】

LayerBlending
│
├───index.js
├───package.json
│
├───utils
│   └───updateBaseUri.js
│
├───output
│   ├───metadata.json
│   ├───0~149.json
│   └───0~149.png
│
└───input
    ├───NFTConfig.js
    ├───races.txt
    ├───racesProduction.py
    └───part_image
        ├───1-logo
        ├───2-congra
        ├───3-middlelogo
        └───4-background
let Static = {
    Logo: {
        "blue": 0, "green": 0, "yellow": 0, "red": 0, "purple": 0
    },
    Congra: { "star": 0, "welldone": 0, "green": 0, "science": 0, "pink": 0 },
    BackGround: {
        "grey": 0, "red": 0, "pink": 0, "yellow": 0, "orange": 0, 
        "green": 0, "blue": 0, "cloud": 0, "purple": 0
    },
    Classes: { "A": 0, "B": 0, "C": 0, "D": 0 },
    MiddleLogo: { "balloon": 0, "wine": 0, "champagne": 0, "beer": 0, "salute": 0 },
}

function ReviseStatic(_layer) {
    let name = _layer.selectedElement.name;
    name = name.split('.')[0]
    if (_layer.name == "logo") {
        Static.Logo[name] += 1;
    }
    if (_layer.name == "congra") {
        Static.Congra[name] += 1;
    }
    if (_layer.name == "background") {
        Static.BackGround[name] += 1;
    }
    if (_layer.name == "middlelogo") {
        Static.MiddleLogo[name] += 1;
    }
}

const PrintStatic = () => {
    let _static = `
    - Produce ${editionSize} KF
    - Version: Beta_1.0
    --------------------Logo STATIC----------------------------
    There are ${Static.Logo.blue} Blue
              ${Static.Logo.green} Green
              ${Static.Logo.yellow} Yellow
              ${Static.Logo.red} Red
              ${Static.Logo.purple} Purple
    --------------------Congra STATIC-------------------------------
    There are ${Static.Congra.star} Star
              ${Static.Congra.welldone} WellDone 
              ${Static.Congra.green} Green 
              ${Static.Congra.science} Science 
              ${Static.Congra.pink} Pink 
    --------------------MiddleLogo STATIC-------------------------------
    There are ${Static.MiddleLogo.balloon} Balloon
              ${Static.MiddleLogo.wine} Wine
              ${Static.MiddleLogo.champagne} Champagne
              ${Static.MiddleLogo.beer} Beer
              ${Static.MiddleLogo.salute} Salute
    --------------------BACKGROUND_COLOR STATIC--------------------
    There are ${Static.BackGround.grey} Grey Background
              ${Static.BackGround.red} Red Background
              ${Static.BackGround.pink} Pink Background
              ${Static.BackGround.yellow} Yellow Background
              ${Static.BackGround.orange} Orange Background
              ${Static.BackGround.green} Green Background
              ${Static.BackGround.blue} Blue Background
              ${Static.BackGround.cloud} Cloud Background
              ${Static.BackGround.purple} Purple Background
    --------------------Classes STATIC------------------------------
    There are ${Static.Classes.A} Certificate born at A
              ${Static.Classes.B} Certificate born at B
              ${Static.Classes.C} Certificate born at C
              ${Static.Classes.D} Certificate born at D`

    fs.writeFileSync("./output/_Static.txt", _static);
}
"use strict";

const fs = require("fs");
const path = require("path");
const isLocal = typeof process.pkg === "undefined";
const basePath = isLocal ? process.cwd() : path.dirname(process.execPath);

const { baseImageUri } = require("../input/NFTConfig.js");
const baseUri = baseImageUri;

// read json data
let rawdata = fs.readFileSync(`${basePath}/output/json/_metadata.json`);
let data = JSON.parse(rawdata);

data.forEach((item) => {
    item.image = `${baseUri}/${item.id}.png`;
    fs.writeFileSync(
        `${basePath}/output/json/${item.id}.json`,
        JSON.stringify(item, null, 2)
    );
});

fs.writeFileSync(
    `${basePath}/output/json/_metadata.json`,
    JSON.stringify(data, null, 2)
);

console.log(`Updated baseUri for images to ===> ${baseUri}`);

| Deploy NFT |

【Structuring Smart Contract】

【Deploy on Testnet】


| Minting dApp |

【Deploy the Lazy Mint in Website】

├───index.jsx
├───mint.jsx
├───Contract
│   └───SmartContract.json
├───Redux
│   │   └───store.js
│   ├───blockchain
│   │   ├───blockchainAction.js
│   │   └───blockchainReducer.js
└───└───data
        ├───dataAction.js
        └───dataReducer.js

blockchainAction.js

// constants
import Web3 from "web3";
import SmartContract from "../../contracts/SmartContract.json";
// log
import { fetchData } from "../data/dataActions";

const connectRequest = () => {
  return {
    type: "CONNECTION_REQUEST",
  };
};

const connectSuccess = (payload) => {
  return {
    type: "CONNECTION_SUCCESS",
    payload: payload,
  };
};

const connectFailed = (payload) => {
  return {
    type: "CONNECTION_FAILED",
    payload: payload,
  };
};

const updateAccountRequest = (payload) => {
  return {
    type: "UPDATE_ACCOUNT",
    payload: payload,
  };
};

export const connect = () => {
  return async (dispatch) => {
    dispatch(connectRequest());
    if (window.ethereum) {
      let web3 = new Web3(window.ethereum);
      try {
        const accounts = await window.ethereum.request({
          method: "eth_requestAccounts",
        });
        const networkId = await window.ethereum.request({
          method: "net_version",
        });
        // const NetworkData = await SmartContract.networks[networkId];
        if (networkId == 4) {
          const SmartContractObj = new web3.eth.Contract(
            SmartContract.abi,
            // NetworkData.address
            "0xC904c79AF32eEDd3b4d6E88A0f18CCe3f28837B9"
          );
          dispatch(
            connectSuccess({
              account: accounts[0],
              smartContract: SmartContractObj,
              web3: web3,
            })
          );
          // Add listeners start
          window.ethereum.on("accountsChanged", (accounts) => {
            dispatch(updateAccount(accounts[0]));
          });
          window.ethereum.on("chainChanged", () => {
            window.location.reload();
          });
          // Add listeners end
        } else {
          dispatch(connectFailed("Change network to Polygon."));
        }
      } catch (err) {
        dispatch(connectFailed("Something went wrong."));
      }
    } else {
      dispatch(connectFailed("Install Metamask."));
    }
  };
};

export const updateAccount = (account) => {
  return async (dispatch) => {
    dispatch(updateAccountRequest({ account: account }));
    dispatch(fetchData(account));
  };
};

blockchainReducer.js

const initialState = {
  loading: false,
  account: null,
  smartContract: null,
  web3: null,
  errorMsg: "",
};

const blockchainReducer = (state = initialState, action) => {
  switch (action.type) {
    case "CONNECTION_REQUEST":
      return {
        ...initialState,
        loading: true,
      };
    case "CONNECTION_SUCCESS":
      return {
        ...state,
        loading: false,
        account: action.payload.account,
        smartContract: action.payload.smartContract,
        web3: action.payload.web3,
      };
    case "CONNECTION_FAILED":
      return {
        ...initialState,
        loading: false,
        errorMsg: action.payload,
      };
    case "UPDATE_ACCOUNT":
      return {
        ...state,
        account: action.payload.account,
      };
    default:
      return state;
  }
};

export default blockchainReducer;

dataActions.js

// log
import store from "../store";

const fetchDataRequest = () => {
  return {
    type: "CHECK_DATA_REQUEST",
  };
};

const fetchDataSuccess = (payload) => {
  return {
    type: "CHECK_DATA_SUCCESS",
    payload: payload,
  };
};

const fetchDataFailed = (payload) => {
  return {
    type: "CHECK_DATA_FAILED",
    payload: payload,
  };
};

export const fetchData = (account) => {
  return async (dispatch) => {
    dispatch(fetchDataRequest());
    try {
      let name = await store
        .getState()
        .blockchain.smartContract.methods.name()
        .call();

      dispatch(
        fetchDataSuccess({
          name,
        })
      );
    } catch (err) {
      console.log(err);
      dispatch(fetchDataFailed("Could not load data from contract."));
    }
  };
};

dataReducers.js

const initialState = {
  loading: false,
  name: "",
  error: false,
  errorMsg: "",
};

const dataReducer = (state = initialState, action) => {
  switch (action.type) {
    case "CHECK_DATA_REQUEST":
      return {
        ...initialState,
        loading: true,
      };
    case "CHECK_DATA_SUCCESS":
      return {
        ...initialState,
        loading: false,
        name: action.payload.name,
      };
    case "CHECK_DATA_FAILED":
      return {
        ...initialState,
        loading: false,
        error: true,
        errorMsg: action.payload,
      };
    default:
      return state;
  }
};

export default dataReducer;

store.js

import { applyMiddleware, compose, createStore, combineReducers } from "redux";
import thunk from "redux-thunk";
import blockchainReducer from "./blockchain/blockchainReducer";
import dataReducer from "./data/dataReducer";

const rootReducer = combineReducers({
  blockchain: blockchainReducer,
  data: dataReducer,
});

const middleware = [thunk];
const composeEnhancers = compose(applyMiddleware(...middleware));

const configureStore = () => {
  return createStore(rootReducer, composeEnhancers);
};

const store = configureStore();

export default store;

【Deploy on Mainnet】


把圖片放在乙太坊上到底要多少錢

1 mb 的圖片

每個 uint 是 256bits = 32 bytes = 2^5 bytes 1MB = 1024 * 1024 = 2 ^ 20 (指數律ㄏㄏ)

2^20 / 2^5 = 2^15 = 32768

32768 * 20000 + 21000 = 655381000 GAS

655381000 GAS * 70 Gwei = 45876670000

10^9 Gwei = 1 eth

45876670000 / 10^9 = 45.8767 eth

118857.35436 USD


Reference and Some Resources

論壇、群組或者是學習資源

PreviousOH Week2 Overview

Last updated 3 years ago