Kategorie
Transkrypcje YouTube

Jak podłączyć portfel MetaMask do aplikacji React?

🔴 Pokazuję jak podłączyć portfel Metamask do aplikacji napisanej w React.js + TypeScript. Koduję też prymitywną maszynę stanów z wykorzystaniem hooka useState oraz discriminating unions. Stan jest podpięty do React Context, aby był dostępny globalnie w całej aplikacji.

Rozpoczęcie projektu w Next.js + TypeScript

Korzystam z gotowego szablonu z oficjalnego repo Next.js:
https://github.com/vercel/next.js/tree/canary/examples/with-typescript

Kliknij w przycisk Deploy. Zostaniesz przeniesiony na platformę Vercel. Jeśli nie jesteś zalogowany, połącz konto ze swoim GitHubem.

Utwórz nowy projekt. Powinien być gotowy po minucie. Pobierz repozytorium na dysk i otwórz w edytorze kodu.

Zainstaluj paczki yarn install . Odpal projekt yarn dev i przetestuj w przeglądarce.

Usuń niepotrzebne pliki i katalogi. Zaczynamy od zera. Stwórz katalog src – to w nim będziemy umieszczać kod.

Używam TypeScripta w trybie strict. Możesz to ustawić w pliku tsconfig.json i dalej "strict": true.

Podłączenie portfela Metamask

Jeśli jeszcze nie masz zainstalowanego Metamaska w przeglądarce, to zachęcam do obejrzenia odcinka na kanale Piotra Ostapowicza właśnie o portfelu kryptowalutowym Metamask.

Będzie Ci potrzebna biblioteka ethers.

yarn add ethers

Utwórz folder src/connection., a w nim plik connectMetamask.ts z kodem:

import { providers } from 'ethers';
import { Connection, WindowInstanceWithEthereum } from './types';


export const connectMetamask = async (): Promise<Connection> => {
  const ethereum = (window as WindowInstanceWithEthereum).ethereum;

  if (!ethereum) {
    throw new Error('No metamask detected');
  }

  const [myWalletAddress] = await ethereum.request({
    method: 'eth_requestAccounts',
  });

  const provider = new providers.Web3Provider(ethereum as any);

  return {
    myWalletAddress,
    provider,
  };
}

Zwracam tutaj obiekt typu Connection, gdzie jest adres mojego portfela Metamask oraz provider. Obiekt provider będzie służył do interakcji z portfelem.

Jest to funkcja asynchroniczna (async), więc zakładam, że będzie wyrzucać błędy, gdy coś pójdzie nie tak. Dla uproszczenia spodziewam się stringa z ludzkim komunikatem.

W moim kodzie sam wykrywam czy dostępny jest obiekt ethereum. Powinien być przypięty do obiektu window przeglądarki przez plugin walletu Metamask. Jeśli go nie ma, to znaczy, że plugin nie jest zainstalowany lub jest wyłączony.

Próbowałem znaleźć godne typowania, niestety nic mnie nie zadowalało. Dlatego zdecydowałem się dopisać samemu typy, które będą mi potrzebne. Ich deklaracje znajdziesz w pełnym kodzie w repo. Zwróć uwagę na nazwę

Stany portfela

Połączenie z moim walletem może znajdować się w jednym z trzech stanów:

  • niepodłączony
  • podłączony
  • błąd

Gdy jestem podłączony, chciałbym mieć dostęp do obiektu typu Connection, który jest wypluwany podczas ustanawiania połączenia. Natomiast gdy wystąpił błąd, to chcę wiedzieć, co to za błąd.

W pliku src/connection/states.ts definiuję typ dla takiej prymitywnej maszyny stanu:

import { AppError } from "../error/types";
import { Connection } from "./types";

export type ConnectionState = {
  type: 'NO_METAMASK';
} | {
  type: 'METAMASK_CONNECTED';
  connection: Connection;
} | {
  type: 'METAMASK_ERROR';
  error: AppError;
};

Opieram się tutaj o discriminating unions z TypeScripta.

Obsługa stanu połączenia w React Context

Zakładam, że informacja o połączeniu powinna być dostępna w każdym komponencie. Dlatego decyduję się na zarządzanie nim w kontekście reaktowym.

Szerzej o React Context mówię na moim kanale YouTube. Zobacz odcinek Jak typować React Context? #TypeScript. Szczególnie istotne dla mnie jest tutaj bezpieczne typowanie.

Pełny kod znajdziesz w repozytorium. Tutaj przedstawiam najważniejszy hook:

export const useProvideConnection = () => {
  const [connectionState, setConnectionState] = useState<ConnectionState>({
    type: 'NO_METAMASK',
  });

  const connect = async () => {
    try {
      const connection = await connectMetamask();
      setConnectionState({
        type: 'METAMASK_CONNECTED',
        connection,
      });
    } catch (e) {
      const error = handleError(e);
      setConnectionState({
        type: 'METAMASK_ERROR',
        error,
      });
    }
  }

  return {
    connectionState,
    connect,
  };
};

Później będę się do niego odwoływał przez useConnection.

Funkcja connect zajmuje się przejściami pomiędzy stanami connectionState. Dla uproszczenia pominąłem stan loading.

Wszystko znajduje się w bloku try catch. W sekcji try wywołuję wcześniej omówioną funkcję connectMetamask. Spodziewam się obiektu połączenia i zapisuję go do stanu z React.useState. Obsługuję też błąd, zapisując informację o nim w stanie.

Utworzenie przycisku do podłączania portfela

W pliku pages/index.tsx tworzę widok z samym przyciskiem Connect Metamask:

import { useConnection } from '../src/connection/context';

const HomePage = () => {
  const { connectionState, connect } = useConnection();
  const isError = connectionState.type === 'METAMASK_ERROR';
  const isConnected = connectionState.type === 'METAMASK_CONNECTED';

  if (isError) {
    return (
      <h2>Error: {connectionState.error.message}</h2>
    );
  }

  if (isConnected) {
    return (
      <>
        <h2>Your Matamask wallet is connected!</h2>
        <p>Your wallet address: {connectionState.connection.myWalletAddress}</p>
      </>
    );
  }

  return (
    <button type="button" onClick={connect}>
      Connect Metamask
    </button>
  )
}


export default HomePage

Domyślnie pojawi się button, bo Metamask nie jest podłączony do mojej aplikacji. Ale jeżeli stan będzie inny niż NO_METAMASK, to odzwierciedlam go w widoku aplikacji. Odpowiednio są to: komunikat błędu albo komunikat z adresem konta.

Aby przetestować przepadek z błędem, wyłącz na chwilę plugin Metamask.

Pozostały kod

Jeśli chcesz przepisać całą aplikację, to polecam zrobić to razem ze mną oglądając wideo krok po kroku. Znajdziesz je na górze tego artykułu.

To tyle! Na razie! 😎

Link do aplikacji: https://crypto-app-sage.vercel.app
Kod: https://github.com/lebrande/crypto-app

Metamask tutorial: https://www.youtube.com/watch?v=fozB8QqsUDM
Mój odcinek o tym jak robię React Context: https://www.youtube.com/watch?v=_fW3FbXx35U
Dokumentacja Nextjs, jak zrobić _app w TS: https://nextjs.org/docs/basic-features/typescript#custom-app