Skip to main content

Wprowadzenie

Jest to druga część naszego praktycznego mini kursu Web3 w której budujemy zdecentralizowaną aplikację wyświetlającą wiadomości od użytkowników, którzy za taką możliwość zapłacili tokenami. W pierwszym artykule stworzyliśmy smart kontrakt w języku Solidity, który został opublikowany w sieci testowej Ethereum Goerli. Dodatkowo skonfigurowaliśmy portfel kryptowalut MetaMask i utworzyliśmy konto deweloperskie Alchemy. Są to niezbędne kroki, które należy wykonać przed przystąpieniem do tego artykułu.

W aktualnej części frontendowej zbudujemy interfejs graficzny nawiązujący interakcję ze smart kontraktem w sieci blockchain. W tym przypadku smart kontrakt możemy traktować jak backend tradycyjnej aplikacji internetowej.

Stos technologiczny

  • React – Biblioteka JavaScript wykorzystywana do tworzenia interfejsów użytkownika
  • TypeScript – Język programowania oparty na JavaScript
  • Ethers.js – Biblioteka komunikująca się z blockchainem Ethereum
  • Mui – Biblioteka gotowych komponentów UI zgodna z wytycznymi Google

Cały kod który będziemy pisać jest dostępny w naszym repozytorium GitHub. Jeśli nie masz ochoty przechodzić krok po kroku przez proces tworzenia aplikacji, wystarczy sklonować repo i przeczytać pliki README.md:

git clone https://github.com/Chainkraft/message.git

Wymagania

Jeśli przeszedłeś przez wszystkie kroki pierwszej części naszego kursu Web3 to poniższą listę wymagań możesz pominąć.

  • Node.js w wersji >= 14
  • Portfel Ethereum
  • Testowe tokeny sieci Ethereum Goerli
  • Konto deweloperske Alchemy

Konfiguracja środowiska

Create-react-app

Aplikację frontendową skonfigurujemy za pomocą jednej komendy z wykorzystaniem narzędzia Create React App. Dzięki temu bardziej skupimy się na rozwiązaniu problemu niż na konfigurowaniu Reacta i różnych zależności.

npx create-react-app message-frontend --template typescript

Create-react-app skonfiguruje automatycznie takie zależności jak: Webpack, Babel, ESLint, TypeScript. Jeżeli chcemy mieć bezpośrednią kontrolę nad wszystkim co się dzieje, możemy całkowicie odpiąć narzędzie za pomocą komendy npm run eject. Komenda przeniesie zależności, konfiguracje oraz skrypty bezpośrednio do naszego projektu (proces nieodwracalny).

Przechodzimy do folderu z wygenerowanym projektem i uruchamiamy aplikację w trybie deweloperskim.

cd message-frontend
npm start

Domyślna aplikacja powinna być dostępna pod adresem http://localhost:3000. Strona zostanie automatycznie odświeżona, jeśli dokonamy zmian w plikach. Ewentualne błędy będą logowane do konsoli deweloperskiej przeglądarki (F12).

Domyślna strona dostępna pod adresem http://localhost:3000

Ethers.js

Dodajemy bibliotekę do komunikacji z siecią blockchain:

npm install ethers @typechain/ethers-v5

Material UI

Biblioteka MUI pozwoli nam szybko zbudować interfejs graficzny za pomocą gotowych komponentów.

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

dotenv

Create-react-app domyślnie dodaje zależność biblioteki dotenv którą poznaliśmy w części związanej ze smart kontraktem. Tworzymy plik .env w głównym folderze projektu i uzupełniamy zmienne środowiskowe podając wartości z poprzedniego artykułu. Nowością jest zmienna REACT_APP_NETWORK, gdzie wpisujemy nazwę sieci testowej goerli.

REACT_APP_NETWORK = "goerli" // Nazwa testowej sieci Ethereum
REACT_APP_API_KEY = "<secret>" // Alchemy API Key
REACT_APP_API_URL = "<secret>" // Alchemy API Url
REACT_APP_PRIVATE_KEY = "<secret>" // Klucz prywatny portfela MetaMask
REACT_APP_CONTRACT_ADDRESS = "<secret>" // Adres naszego smart kontraktu

Tworzenie interfejsu graficznego

Na początek skonfigurujemy w projekcie funkcje pomocnicze, które ułatwią interakcję ze smart kontraktem oraz utworzymy plik opisujący wygląd smart kontraktu, tzw. ABI.

Tworzymy folder src/config, a w nim plik Blockchain.ts. Plik zawiera pomocnicze zmienne oraz funkcje, które ułatwią tworzenie instancji klasy Provider oraz Contract.

import {ethers} from "ethers";
import MESSAGE_ABI from "./Message.abi.json"; // za chwilę dodamy plik ABI
import {Provider} from "@ethersproject/abstract-provider";

// Provider to klasa która jest warstwą abstrakcji dla połączenia z siecią Ethereum
// Provider Alchemy pozwala na dostęp do sieci Blockchain bez konieczności posiadania osobistego portfela.
// Dzięki temu, użytkownik który nie posiada np. MetaMask zobaczy aktualną wiadomość po wejściu na stronę.
const getAlchemyProvider = () => new ethers.providers.AlchemyProvider(
    process.env.REACT_APP_NETWORK,
    process.env.REACT_APP_API_KEY
);

// Provider Web3Provider wykorzystuje plugin przeglądarki MetaMask do połączenia z siecią Ethereum.
// Zmienna window.ethereum jest dostępna jeśli przeglądarka posiada zainstalowany plugin portfela.
const getWalletProvider = () => new ethers.providers.Web3Provider(window.ethereum)

// Contract to klasa która jest warstwą abstrakcji smart kontraktu w sieci Ethereum.
// Dzięki temu możemy komunikować się ze smart kontraktem jak ze zwykłym obiektem JavaScript.
const getMessageContract = (provider: Provider) =>
    new ethers.Contract(
        process.env.REACT_APP_CONTRACT_ADDRESS, // adres naszego smart kontraktu
        MESSAGE_ABI.abi, // ABI kontraktu z poprzedniego artykułu
        provider // provider jako dynamiczny parametr funkcji
    );

export {getAlchemyProvider, getWalletProvider, getMessageContract};

Aby umożliwić interakcję instancji klasy Contract z naszym smart kontraktem w sieci blockchain musimy przekazać informację jak wygląda nasz kontrakt za pomocą tzw. ABI (Application Binary Interface). W poprzednim artykule zbudowaliśmy smart kontrakt Solidity. Jego plik ABI znajdziemy w tamtym projekcie w lokalizacji: /artifacts/contracts/Message.sol/Message.json (po zbudowaniu lub opublikowaniu kontraktu). Tworzymy plik Message.abi.json w folderze src/config i kopiujemy zawartość pliku Message.json:

{
  "_format": "hh-sol-artifact-1",
  "contractName": "Message",
  "sourceName": "contracts/Message.sol",
  "abi": [
    {
      "inputs": [],
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "oldFeeRate",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "newFeeRate",
          "type": "uint256"
        }
      ],
      "name": "FeeRateSet",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "string",
          "name": "message",
          "type": "string"
        },
        {
          "indexed": true,
          "internalType": "address",
          "name": "author",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "price",
          "type": "uint256"
        }
      ],
      "name": "MessageSet",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "address",
          "name": "oldOwner",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "address",
          "name": "newOwner",
          "type": "address"
        }
      ],
      "name": "OwnershipTransferred",
      "type": "event"
    },
    {
      "anonymous": false,
      "inputs": [
        {
          "indexed": false,
          "internalType": "address",
          "name": "recipient",
          "type": "address"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "amount",
          "type": "uint256"
        },
        {
          "indexed": false,
          "internalType": "uint256",
          "name": "fee",
          "type": "uint256"
        }
      ],
      "name": "Withdrawal",
      "type": "event"
    },
    {
      "inputs": [],
      "name": "author",
      "outputs": [
        {
          "internalType": "address",
          "name": "",
          "type": "address"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_address",
          "type": "address"
        }
      ],
      "name": "balanceOf",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "feeParts",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "feeRate",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "message",
      "outputs": [
        {
          "internalType": "string",
          "name": "",
          "type": "string"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "owner",
      "outputs": [
        {
          "internalType": "address",
          "name": "",
          "type": "address"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "price",
      "outputs": [
        {
          "internalType": "uint256",
          "name": "",
          "type": "uint256"
        }
      ],
      "stateMutability": "view",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "uint256",
          "name": "_feeRate",
          "type": "uint256"
        }
      ],
      "name": "setFeeRate",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "string",
          "name": "_message",
          "type": "string"
        }
      ],
      "name": "setMessage",
      "outputs": [],
      "stateMutability": "payable",
      "type": "function"
    },
    {
      "inputs": [
        {
          "internalType": "address",
          "name": "_owner",
          "type": "address"
        }
      ],
      "name": "setOwner",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "inputs": [],
      "name": "withdraw",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ],
  "bytecode": "0x60806040523480156200001157600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506040518060400160405280600b81526020017f48656c6c6f20776f726c6400000000000000000000000000000000000000000081525060049080519060200190620000e0929190620000e7565b50620001fc565b828054620000f59062000197565b90600052602060002090601f01602090048101928262000119576000855562000165565b82601f106200013457805160ff191683800117855562000165565b8280016001018555821562000165579182015b828111156200016457825182559160200191906001019062000147565b5b50905062000174919062000178565b5090565b5b808211156200019357600081600090555060010162000179565b5090565b60006002820490506001821680620001b057607f821691505b60208210811415620001c757620001c6620001cd565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b611221806200020c6000396000f3fe60806040526004361061009c5760003560e01c806370a082311161006457806370a08231146101515780638da5cb5b1461018e578063978bbdb9146101b9578063a035b1fe146101e4578063a6c3e6b91461020f578063e21f37ce1461023a5761009c565b806313af4035146100a1578063368b8772146100ca5780633ccfd60b146100e657806345596e2e146100fd57806357526b3f14610126575b600080fd5b3480156100ad57600080fd5b506100c860048036038101906100c39190610aff565b610265565b005b6100e460048036038101906100df9190610b28565b6103b6565b005b3480156100f257600080fd5b506100fb610547565b005b34801561010957600080fd5b50610124600480360381019061011f9190610b6d565b61075d565b005b34801561013257600080fd5b5061013b61087d565b6040516101489190610ee9565b60405180910390f35b34801561015d57600080fd5b5061017860048036038101906101739190610aff565b610883565b6040516101859190610ee9565b60405180910390f35b34801561019a57600080fd5b506101a36108cc565b6040516101b09190610d9c565b60405180910390f35b3480156101c557600080fd5b506101ce6108f0565b6040516101db9190610ee9565b60405180910390f35b3480156101f057600080fd5b506101f96108f6565b6040516102069190610ee9565b60405180910390f35b34801561021b57600080fd5b506102246108fc565b6040516102319190610d9c565b60405180910390f35b34801561024657600080fd5b5061024f610922565b60405161025c9190610e17565b60405180910390f35b3373ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146102f3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102ea90610e89565b60405180910390fd5b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08160008054906101000a900473ffffffffffffffffffffffffffffffffffffffff166040516103aa929190610db7565b60405180910390a15050565b60035434116103fa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103f190610e69565b60405180910390fd5b60035460056000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461046d9190610f69565b925050819055503460038190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508181600491906104cd9291906109e8565b50600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f6c33a7aaf998952608f89344f6a3cbf2b2ffbb5b1ce184ef16e84e499a1f817a600460035460405161053b929190610e39565b60405180910390a25050565b6000600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490506000610596826109b0565b9050600081836105a6919061104a565b90506000600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550600060025411156106705781600560008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546106689190610f69565b925050819055505b60003373ffffffffffffffffffffffffffffffffffffffff168260405161069690610d87565b60006040518083038185875af1925050503d80600081146106d3576040519150601f19603f3d011682016040523d82523d6000602084013e6106d8565b606091505b505090508061071c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161071390610ea9565b60405180910390fd5b7fdf273cb619d95419a9cd0ec88123a0538c85064229baa6363788f743fff90deb33838560405161074f93929190610de0565b60405180910390a150505050565b3373ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146107eb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107e290610e89565b60405180910390fd5b612710811115610830576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161082790610ec9565b60405180910390fd5b60006002549050816002819055507f959ec4191db1bd972bfbc60dc7d735d4cfb897ca3fd297f4ebd6ee918feb84d481600254604051610871929190610f04565b60405180910390a15050565b61271081565b6000600560008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60025481565b60035481565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6004805461092f906110ed565b80601f016020809104026020016040519081016040528092919081815260200182805461095b906110ed565b80156109a85780601f1061097d576101008083540402835291602001916109a8565b820191906000526020600020905b81548152906001019060200180831161098b57829003601f168201915b505050505081565b60008060025411156109de57612710600254836109cd9190610ff0565b6109d79190610fbf565b90506109e3565b600090505b919050565b8280546109f4906110ed565b90600052602060002090601f016020900481019282610a165760008555610a5d565b82601f10610a2f57803560ff1916838001178555610a5d565b82800160010185558215610a5d579182015b82811115610a5c578235825591602001919060010190610a41565b5b509050610a6a9190610a6e565b5090565b5b80821115610a87576000816000905550600101610a6f565b5090565b600081359050610a9a816111bd565b92915050565b60008083601f840112610ab257600080fd5b8235905067ffffffffffffffff811115610acb57600080fd5b602083019150836001820283011115610ae357600080fd5b9250929050565b600081359050610af9816111d4565b92915050565b600060208284031215610b1157600080fd5b6000610b1f84828501610a8b565b91505092915050565b60008060208385031215610b3b57600080fd5b600083013567ffffffffffffffff811115610b5557600080fd5b610b6185828601610aa0565b92509250509250929050565b600060208284031215610b7f57600080fd5b6000610b8d84828501610aea565b91505092915050565b610b9f8161107e565b82525050565b6000610bb082610f42565b610bba8185610f58565b9350610bca8185602086016110ba565b610bd3816111ac565b840191505092915050565b60008154610beb816110ed565b610bf58186610f58565b94506001821660008114610c105760018114610c2257610c55565b60ff1983168652602086019350610c55565b610c2b85610f2d565b60005b83811015610c4d57815481890152600182019150602081019050610c2e565b808801955050505b50505092915050565b6000610c6b601083610f58565b91507f4e6f7420656e6f756768206574686572000000000000000000000000000000006000830152602082019050919050565b6000610cab600a83610f58565b91507f4f6e6c79206f776e6572000000000000000000000000000000000000000000006000830152602082019050919050565b6000610ceb601783610f58565b91507f4661696c75726521204574686572206e6f742073656e740000000000000000006000830152602082019050919050565b6000610d2b600083610f4d565b9150600082019050919050565b6000610d45601383610f58565b91507f46656520726174652061626f76652031303025000000000000000000000000006000830152602082019050919050565b610d81816110b0565b82525050565b6000610d9282610d1e565b9150819050919050565b6000602082019050610db16000830184610b96565b92915050565b6000604082019050610dcc6000830185610b96565b610dd96020830184610b96565b9392505050565b6000606082019050610df56000830186610b96565b610e026020830185610d78565b610e0f6040830184610d78565b949350505050565b60006020820190508181036000830152610e318184610ba5565b905092915050565b60006040820190508181036000830152610e538185610bde565b9050610e626020830184610d78565b9392505050565b60006020820190508181036000830152610e8281610c5e565b9050919050565b60006020820190508181036000830152610ea281610c9e565b9050919050565b60006020820190508181036000830152610ec281610cde565b9050919050565b60006020820190508181036000830152610ee281610d38565b9050919050565b6000602082019050610efe6000830184610d78565b92915050565b6000604082019050610f196000830185610d78565b610f266020830184610d78565b9392505050565b60008190508160005260206000209050919050565b600081519050919050565b600081905092915050565b600082825260208201905092915050565b6000610f74826110b0565b9150610f7f836110b0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610fb457610fb361111f565b5b828201905092915050565b6000610fca826110b0565b9150610fd5836110b0565b925082610fe557610fe461114e565b5b828204905092915050565b6000610ffb826110b0565b9150611006836110b0565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561103f5761103e61111f565b5b828202905092915050565b6000611055826110b0565b9150611060836110b0565b9250828210156110735761107261111f565b5b828203905092915050565b600061108982611090565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b60005b838110156110d85780820151818401526020810190506110bd565b838111156110e7576000848401525b50505050565b6000600282049050600182168061110557607f821691505b602082108114156111195761111861117d565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f8301169050919050565b6111c68161107e565b81146111d157600080fd5b50565b6111dd816110b0565b81146111e857600080fd5b5056fea26469706673582212206dabf31eb1621c76473da86b9848d45ff4743cc12466351578a2669ab9f3cf6d64736f6c63430008000033",
  "deployedBytecode": "0x60806040526004361061009c5760003560e01c806370a082311161006457806370a08231146101515780638da5cb5b1461018e578063978bbdb9146101b9578063a035b1fe146101e4578063a6c3e6b91461020f578063e21f37ce1461023a5761009c565b806313af4035146100a1578063368b8772146100ca5780633ccfd60b146100e657806345596e2e146100fd57806357526b3f14610126575b600080fd5b3480156100ad57600080fd5b506100c860048036038101906100c39190610aff565b610265565b005b6100e460048036038101906100df9190610b28565b6103b6565b005b3480156100f257600080fd5b506100fb610547565b005b34801561010957600080fd5b50610124600480360381019061011f9190610b6d565b61075d565b005b34801561013257600080fd5b5061013b61087d565b6040516101489190610ee9565b60405180910390f35b34801561015d57600080fd5b5061017860048036038101906101739190610aff565b610883565b6040516101859190610ee9565b60405180910390f35b34801561019a57600080fd5b506101a36108cc565b6040516101b09190610d9c565b60405180910390f35b3480156101c557600080fd5b506101ce6108f0565b6040516101db9190610ee9565b60405180910390f35b3480156101f057600080fd5b506101f96108f6565b6040516102069190610ee9565b60405180910390f35b34801561021b57600080fd5b506102246108fc565b6040516102319190610d9c565b60405180910390f35b34801561024657600080fd5b5061024f610922565b60405161025c9190610e17565b60405180910390f35b3373ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146102f3576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016102ea90610e89565b60405180910390fd5b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff169050816000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055507f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e08160008054906101000a900473ffffffffffffffffffffffffffffffffffffffff166040516103aa929190610db7565b60405180910390a15050565b60035434116103fa576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016103f190610e69565b60405180910390fd5b60035460056000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461046d9190610f69565b925050819055503460038190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055508181600491906104cd9291906109e8565b50600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff167f6c33a7aaf998952608f89344f6a3cbf2b2ffbb5b1ce184ef16e84e499a1f817a600460035460405161053b929190610e39565b60405180910390a25050565b6000600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1681526020019081526020016000205490506000610596826109b0565b9050600081836105a6919061104a565b90506000600560003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002081905550600060025411156106705781600560008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546106689190610f69565b925050819055505b60003373ffffffffffffffffffffffffffffffffffffffff168260405161069690610d87565b60006040518083038185875af1925050503d80600081146106d3576040519150601f19603f3d011682016040523d82523d6000602084013e6106d8565b606091505b505090508061071c576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161071390610ea9565b60405180910390fd5b7fdf273cb619d95419a9cd0ec88123a0538c85064229baa6363788f743fff90deb33838560405161074f93929190610de0565b60405180910390a150505050565b3373ffffffffffffffffffffffffffffffffffffffff1660008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16146107eb576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004016107e290610e89565b60405180910390fd5b612710811115610830576040517f08c379a000000000000000000000000000000000000000000000000000000000815260040161082790610ec9565b60405180910390fd5b60006002549050816002819055507f959ec4191db1bd972bfbc60dc7d735d4cfb897ca3fd297f4ebd6ee918feb84d481600254604051610871929190610f04565b60405180910390a15050565b61271081565b6000600560008373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050919050565b60008054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b60025481565b60035481565b600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6004805461092f906110ed565b80601f016020809104026020016040519081016040528092919081815260200182805461095b906110ed565b80156109a85780601f1061097d576101008083540402835291602001916109a8565b820191906000526020600020905b81548152906001019060200180831161098b57829003601f168201915b505050505081565b60008060025411156109de57612710600254836109cd9190610ff0565b6109d79190610fbf565b90506109e3565b600090505b919050565b8280546109f4906110ed565b90600052602060002090601f016020900481019282610a165760008555610a5d565b82601f10610a2f57803560ff1916838001178555610a5d565b82800160010185558215610a5d579182015b82811115610a5c578235825591602001919060010190610a41565b5b509050610a6a9190610a6e565b5090565b5b80821115610a87576000816000905550600101610a6f565b5090565b600081359050610a9a816111bd565b92915050565b60008083601f840112610ab257600080fd5b8235905067ffffffffffffffff811115610acb57600080fd5b602083019150836001820283011115610ae357600080fd5b9250929050565b600081359050610af9816111d4565b92915050565b600060208284031215610b1157600080fd5b6000610b1f84828501610a8b565b91505092915050565b60008060208385031215610b3b57600080fd5b600083013567ffffffffffffffff811115610b5557600080fd5b610b6185828601610aa0565b92509250509250929050565b600060208284031215610b7f57600080fd5b6000610b8d84828501610aea565b91505092915050565b610b9f8161107e565b82525050565b6000610bb082610f42565b610bba8185610f58565b9350610bca8185602086016110ba565b610bd3816111ac565b840191505092915050565b60008154610beb816110ed565b610bf58186610f58565b94506001821660008114610c105760018114610c2257610c55565b60ff1983168652602086019350610c55565b610c2b85610f2d565b60005b83811015610c4d57815481890152600182019150602081019050610c2e565b808801955050505b50505092915050565b6000610c6b601083610f58565b91507f4e6f7420656e6f756768206574686572000000000000000000000000000000006000830152602082019050919050565b6000610cab600a83610f58565b91507f4f6e6c79206f776e6572000000000000000000000000000000000000000000006000830152602082019050919050565b6000610ceb601783610f58565b91507f4661696c75726521204574686572206e6f742073656e740000000000000000006000830152602082019050919050565b6000610d2b600083610f4d565b9150600082019050919050565b6000610d45601383610f58565b91507f46656520726174652061626f76652031303025000000000000000000000000006000830152602082019050919050565b610d81816110b0565b82525050565b6000610d9282610d1e565b9150819050919050565b6000602082019050610db16000830184610b96565b92915050565b6000604082019050610dcc6000830185610b96565b610dd96020830184610b96565b9392505050565b6000606082019050610df56000830186610b96565b610e026020830185610d78565b610e0f6040830184610d78565b949350505050565b60006020820190508181036000830152610e318184610ba5565b905092915050565b60006040820190508181036000830152610e538185610bde565b9050610e626020830184610d78565b9392505050565b60006020820190508181036000830152610e8281610c5e565b9050919050565b60006020820190508181036000830152610ea281610c9e565b9050919050565b60006020820190508181036000830152610ec281610cde565b9050919050565b60006020820190508181036000830152610ee281610d38565b9050919050565b6000602082019050610efe6000830184610d78565b92915050565b6000604082019050610f196000830185610d78565b610f266020830184610d78565b9392505050565b60008190508160005260206000209050919050565b600081519050919050565b600081905092915050565b600082825260208201905092915050565b6000610f74826110b0565b9150610f7f836110b0565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610fb457610fb361111f565b5b828201905092915050565b6000610fca826110b0565b9150610fd5836110b0565b925082610fe557610fe461114e565b5b828204905092915050565b6000610ffb826110b0565b9150611006836110b0565b9250817fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff048311821515161561103f5761103e61111f565b5b828202905092915050565b6000611055826110b0565b9150611060836110b0565b9250828210156110735761107261111f565b5b828203905092915050565b600061108982611090565b9050919050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000819050919050565b60005b838110156110d85780820151818401526020810190506110bd565b838111156110e7576000848401525b50505050565b6000600282049050600182168061110557607f821691505b602082108114156111195761111861117d565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601260045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b6000601f19601f8301169050919050565b6111c68161107e565b81146111d157600080fd5b50565b6111dd816110b0565b81146111e857600080fd5b5056fea26469706673582212206dabf31eb1621c76473da86b9848d45ff4743cc12466351578a2669ab9f3cf6d64736f6c63430008000033",
  "linkReferences": {},
  "deployedLinkReferences": {}
}

W pliku src/config/Blockchain.ts korzystamy ze zmiennej window.ethereum, aby dobrać się do obiektu portfela MetaMask. Zmienna ta nie jest oficjalnie dodana do interfejsu Window. Z tego powodu IDE (np. Visual Studio, IntelliJ) mogą wyświetlać błędy. Aby temu zaradzić rozszerzymy interfejs o brakujący typ, przy okazji dodając również typy naszych zmiennych środowiskowych. Tworzymy folder src/types, a w nim plik: index.d.ts:

import { MetaMaskInpageProvider } from "@metamask/providers";

declare global {
  interface Window {
    ethereum?: MetaMaskInpageProvider;
  }

  namespace NodeJS {
    interface ProcessEnv {
      REACT_APP_NETWORK: string;
      REACT_APP_API_KEY: string;
      REACT_APP_API_URL: string;
      REACT_APP_PRIVATE_KEY: string;
      REACT_APP_CONTRACT_ADDRESS: string;
    }
  }
}

Następnie przejdziemy do tworzenia właściwych komponentów graficznych w React. Komentarze do kodu HTML pominiemy. Staraliśmy się maksymalnie uprościć aplikację.

Tworzymy folder src/components, a w nim plik DisplayMessage.tsx zawierający kod:

import React, {useEffect, useState} from "react";
import {Backdrop, CircularProgress, Container, Typography,} from "@mui/material";
import {getAlchemyProvider, getMessageContract} from "../config/Blockchain";

// Definiujemy komponent typu funkcyjnego 
const DisplayMessage = () => {

    // Zmienna stanowa przechowująca aktualną wiadomość ze smart kontraktu
    const [message, setMessage] = useState("");

    // Zmienna stanowa obsługująca pasek ładowania podczas czytania danych z sieci blockchain
    const [loading, setLoading] = useState(true);

    // Tzw. Hook, który odpali się raz po wyrenderowaniu komponentu.
    useEffect(() => {
        // Tworzymy instancję connectora naszego smart kontraktu
        // Providerem jest Alchemy, który umożliwia dostęp read-only do danych blockchain (nie potrzebujemy portfela)
        const contract = getMessageContract(getAlchemyProvider());

        // Jest to pierwsze pobranie aktualnej wiadomości zaraz po wejściu użytkownika na stronę
        // Wywołujemy funkcję message() smart kontraktu i wynik przypisujemy do zmiennej stanowej
        contract.message().then((newMessage: string) => {
            setMessage(newMessage);
            setLoading(false);
        }).catch(console.error);

        // Jeśli w między czasie zostanie opublikowana nowa wiadomość to zostanie ona również wyświetlona
        // Tworzymy słuchacza zdarzeń, który będzie monitorował wywołanie zdarzenia MessageSet w kontrakcie
        contract.on("MessageSet", (newMessage: string) => {
            setMessage(newMessage);
        });
    }, []);

    return (
        <div>
            {loading ? <Loader/> : null}
            <Container
                sx={{
                    height: "100vh",
                    display: "flex",
                    alignItems: "center",
                    justifyContent: "center",
                }}>
                <Typography variant="h1" align="center" color="#f9a822">
                    {message}
                </Typography>
            </Container>
        </div>
    );
};

const Loader = () => {
    return (
        <Backdrop open={true}>
            <CircularProgress size="5em"/>
        </Backdrop>
    );
};

export default DisplayMessage;

Skoro mamy już zaimplementowane wyświetlanie wiadomości, to musimy jeszcze obsłużyć dodanie nowej wiadomości z poziomu aplikacji.

Tworzymy nowy plik AddMessage.tsx w folderze src/components z poniższym kodem:

import React, {useState} from "react";
import {BigNumber, ethers} from "ethers";
import Button from "@mui/material/Button";
import AddIcon from "@mui/icons-material/Add";
import {Alert, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Fab, FormControl, Input, InputLabel, Snackbar} from "@mui/material";
import {getMessageContract, getWalletProvider} from "../config/Blockchain";

const AddMessage = () => {
    // Zmienna stanowa "form" to obiekt przechowujący różne pola formularza
    const [form, setForm] = useState({
        minPrice: BigNumber.from(0), // Minimalna cena do zapłaty, aby opublikować nową wiadomość
        price: BigNumber.from(0), // Cena nowej wiadomości
        message: "", // Nowa wiadomość
        error: "" // Ewentualne błędy zwracane przez smart kontrakt
    });

    // Zmienna stanowa obsługująca pasek ładowania podczas czytania danych z sieci blockchain
    const [loading, setLoading] = useState(false);

    // Zmienna stanowa obsługująca pasek ładowania podczas publikowania nowej wiadomości do blockchain
    const [processingTx, setProcessingTx] = useState(false);

    // Zmienna stanowa obsługująca otwieranie okienka popup z formularzem
    const [showDialog, setShowDialog] = useState(false);

    // Zmienna stanowa obsługująca przypadek, gdy użytkownik nie ma pluginu MetaMask
    const [walletError, setWalletError] = useState(false);

    // Funkcja obsługująca otwieranie okna popup z formularzem dodawania nowej wiadomości
    const handleDialogOpen = async () => {
        // Sprawdzamy czy użytkownik ma zainstalowany plugin MetaMask
        // Jeśli nie, wyświetlimy błąd na stronie i użytkownik nie doda wiadomości
        if(window.ethereum === undefined) {
            setWalletError(true);
            return;
        }

        setLoading(true);

        // Pobieramy Provider MetaMask
        const provider = getWalletProvider();

        // Wywołanie metody eth_requestAccounts portfela MetaMask otworzy plugin w przeglądarce 
        // użytkownika i poprosi o potwierdzenie dostępu. 
        // Możemy to porównać do opcji "Zaloguj za pomocą MetaMask" uzyskując dostęp do adresu portfela.
        await provider.send("eth_requestAccounts", []);

        // Tworzymy instancję connectora naszego smart kontraktu
        // Tym razem providerem jest portfel MetaMask, którym będziemy podpisywać transakcje
        const contract = getMessageContract(provider);

        // Aktualizujemy zmienne stanowe
        setForm({
            ...form,
            // Pobieramy informację o "cenie" aktualnej wiadomości
            // Użytkownik musi zapłacić więcej, jeśli chce dodać swoją wiadomość
            minPrice: await contract.price()
        });
        setLoading(false);
        setShowDialog(true);
    };

    // Funkcja obsługująca wysyłanie nowej wiadomości do smart kontraktu
    const handlePublish = async () => {
        // Blokujemy przycisk dodawania na czas przetwarzania transakcji
        setProcessingTx(true);

        // Pobieramy Provider MetaMask
        const provider = getWalletProvider();

        // Pobieramy adres portfela użytkownika, którym będziemy podpisywać transakcję blockchain
        const signer = provider.getSigner();

        // Tworzymy instancję connectora smart kontraktu połączonego z portfelem MetaMask (dostęp read-only)
        const contract = getMessageContract(provider);

        // Łączymy kontrakt z Signer dzięki czemu będziemy mogli podpisywać transakcje (dostęp read-write)
        const contractWithSigner = contract.connect(signer);

        // Wywołujemy funkcję setMessage smart kontraktu przekazując jako parametry nową wiadomość 
        // oraz zadaną ilość tokenów z konta Signer
        await contractWithSigner.setMessage(form.message, {value: form.price})
            .then(handleDialogClose)
            .catch((error: any) => setForm({...form, error: error.error.message}))
            .finally(() => setProcessingTx(false));
    };

    // Funkcja obsługująca zmianę wiadomości w formularzu
    const handleMessageChange = (e: React.ChangeEvent<HTMLInputElement>) =>
        // Ustawiamy zmienną stanową z nową wiadomością
        setForm({...form, message: e.target.value});

    // Funkcja obsługująca zmianę ceny w formularzu
    const handlePriceChange = (e: React.ChangeEvent<HTMLInputElement>) =>
        // Ustawiamy zmienną stanową z ceną w jednostce WEI
        setForm({...form, price: ethers.utils.parseEther(e.target.value)});

    // Funkcja zamykająca okno popup
    const handleDialogClose = () => setShowDialog(false);

    return (
        <div>
            <Fab
                onClick={handleDialogOpen}
                color="primary"
                aria-label="add"
                sx={{
                    position: "absolute",
                    bottom: 20,
                    right: 20,
                }}>
                {!loading && <AddIcon/>}
                {loading && <CircularProgress sx={{color: "red"}}/>}
            </Fab>

            <Dialog open={showDialog} onClose={handleDialogClose}>
                <DialogTitle>Publish message</DialogTitle>
                <DialogContent>
                    {form.error && (
                        <Alert severity="error" sx={{marginBottom: 2}}>
                            {form.error}
                        </Alert>
                    )}
                    <DialogContentText>
                        You need to pay more than {ethers.utils.formatEther(form.minPrice)} ETH to outbid the current message.
                    </DialogContentText>
                    <FormControl fullWidth variant="standard" margin="normal">
                        <InputLabel htmlFor="standard-adornment-amount" required>
                            The message
                        </InputLabel>
                        <Input
                            autoFocus
                            required
                            type="text"
                            value={form.message}
                            onChange={handleMessageChange} />
                    </FormControl>
                    <FormControl fullWidth variant="standard" margin="normal">
                        <InputLabel htmlFor="standard-adornment-amount" required>
                            Price
                        </InputLabel>
                        <Input
                            required
                            type="number"
                            value={ethers.utils.formatEther(form.price)}
                            onChange={handlePriceChange} />
                    </FormControl>
                </DialogContent>
                <DialogActions>
                    <Button onClick={handlePublish} disabled={processingTx}>
                        {!processingTx && "Publish"}
                        {processingTx && "Publishing..."}
                    </Button>
                </DialogActions>
            </Dialog>

            <Snackbar open={walletError} autoHideDuration={6000}>
                <Alert severity="error">
                    Install MetaMask plugin to add a message
                </Alert>
            </Snackbar>
        </div>
    );
};

export default AddMessage;

Na koniec musimy wyświetlić nasze dwa komponenty w głównym komponencie App oraz dodać drobne zmiany kosmetyczne, aby stworzyć ładny interfejs graficzny wykorzystujący MUI.

Modyfikujemy plik src/App.tsx:

import React from "react";
import DisplayMessage from "./components/DisplayMessage";
import AddMessage from "./components/AddMessage";
import {Container, createTheme, CssBaseline, ThemeProvider} from "@mui/material";

function App() {
  const theme = createTheme({
    palette: {
      primary: {
        main: "#f9a822",
      },
      background: {
        default: "#1a1a2e",
      },
    },
  });

  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Container
        style={{
          width: "100vw",
          height: "100vh",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}>
        <DisplayMessage></DisplayMessage>
        <AddMessage></AddMessage>
      </Container>
    </ThemeProvider>
  );
}

export default App;

Modyfikujemy plik public/index.html:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="description" content="Demo application providing smooth dive into Web3 application development" />
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"/>
    <title>The message</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

Przeszliśmy przez wszystkie niezbędne kroki, aby poprawnie wyświetlić aktualną wiadomość ze smart kontraktu oraz dodać nową (jeśli mamy wystarczające środki na koncie naszego krypto portfela). Jeżeli nie posiadacie wystarczającej ilości tokenów testowych ETH w sieci Goerli, możecie doładować portfel z przykładowego faucetu.

npm start

Ostatecznie powinniśmy zobaczyć piękną aplikację z odczytaną wiadomością bezpośrednio ze smart kontraktu. Po kliknięciu w przycisk + możemy przesłać nową wiadomość, która zostanie wyświetlona automatycznie po dodaniu bloku z naszą transakcją do blockchaina (do kilkudziesięciu sekund w sieci Goerli).

Finalna aplikacja dostępna pod adresem http://localhost:3000

Podsumowanie

Zbudowany interfejs graficzny nie wykorzystuje wszystkich mechanizmów zaimplementowanych w smart kontrakcie. Brakuje m.in zwracania środków do autorów starych wiadomości. Jeśli czujesz się na siłach zapraszamy do forkowania naszego projektu na GitHub!

Jest to ostatnia część naszego mini kursu wprowadzającego do technologii Web3. Jeśli był on dla Ciebie wartościowy i nie jesteś jeszcze naszym subskrybentem – zapraszamy do dołączenia poniżej. Jest to dla nas najwyższa forma uznania i motywacja do pracy.