DEV Community

loading...
Cover image for Building an Aavegotchi DApp using React + Web3

Building an Aavegotchi DApp using React + Web3

ccoyotedev profile image Caleb Coyote ・15 min read

Welcome fren! In this tutorial, I will be guiding you through the process of building a DApp (decentralised app) that is connected to the Aavegotchi blockchain. The tutorial will be in React and Typescript, however, don't worry if you do not have any React experience as the real meat of the tutorial is using Web3 and the Aavegotchi Subgraph.

You can find the completed code here:
https://github.com/cgcbrown/aavegotchi-dex-tutorial

What is Web3?

When developing an app on the blockchain, there are 2 sides to it.

  1. Smart contract development - writing code that gets deployed to the blockchain with the Solidity programming language.
  2. Developing websites or clients that interact with the blockchain via smart contracts.

As the Aavegotchi smart contract is already deployed on the Matic network, all we have to worry about is using Web3 to fulfil the second responsibility. Essentially you can think of Web3 as an API for the blockchain where all you need is the address of the smart contract, an ABI and a Provider.

What is the Aavegotchi Subgraph?

The Subgraph is a GraphQL API built by the Pixelcraft team on The Graph that allows you to more efficiently get data from the Aavegotchi blockchain without having to worry about any Web3 setup. It is useful for viewing data from the contract, however, it has its limitations. You cannot call methods that require gas (like petting your Aavegotchi) and some data that you want may not be integrated into the subgraph yet.

 

The Build

For this tutorial, we are going to build an Aavegotchi Pokédex that allows the user to search and view all of the summoned Aavegotchis. The end result will look something like this:

Final result

Initial setup

Before you can start you will need to make sure you have node >= 10.16 and npm >= 5.6 installed on your machine. You will also need an Ethereum compatible browser (if you are using Chrome or Firefox you will need to install the Metamask browser extension) connected to the Matic Network.

Now that is done, let us create our React app. To do this, open up your terminal, and run the following lines:

mkdir tutorials
cd tutorials
npx create-react-app aavegotchi-dex --template typescript
Enter fullscreen mode Exit fullscreen mode

This will build a react app for you called aavegotchi-dex inside a newly created directory called tutorials. using --template typescript configures the React app to use Typescript.

After it's done installing, in your terminal run:

cd aavegotchi-dex
npm start
Enter fullscreen mode Exit fullscreen mode

This should automatically open up your browser and you should see your app running on localhost:3000/. If not open up your browser and manually put http://localhost:3000/ in the address bar.

create-react-app default screen

Now in your code editor of choice (I personally use Visual Studio code) open up aavegotchi-dex

In src/App.tsx replace all the code with the following and save:

//App.tsx

import { useEffect } from 'react';
import './App.css';

function App() {

 const fetchGotchis = () => {
   console.log('Hello fren');
 }

 useEffect(() => {
   fetchGotchis();
 }, [])

 return (
   <div className="App">
   </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Your screen in the browser should now be blank. However, if you open up your dev tools in the browser, you should see Hello fren in the console.

This isn’t a React tutorial, so don't worry if you don't fully understand what is happening. All you need to know is when the component is rendered, the useEffect() hook is triggered which in turn is triggering the fetchGotchis() function. This is where we are going to put our logic to fetch the Aavegotchi logic from the blockchain.
 

Using the Aavegotchi Subgraph

Now with the boring bits out of the way, let us start pulling in data from the blockchain!

The Subgraph

To make our lives easier we can use the Aavegotchi Subgraph to pull in our list of Aavegotchi's data. What's handy about the subgraph is you can open up the playground here to get your graphQL query before even writing any lines of code.

Aavegotchi Core Matic Subgraph

On the right, there is a Schema that allows you to visualise the data we can fetch.

The subgraph has a hard limit of 1000 on the amount of data you can fetch with a single query. This is not a problem as for performance reasons we will fetch 100, and then later, you can add the ability to fetch more data as you scroll.

We can now pick and choose what data we want to be returned from the query. For the Aavegotchidex we know we want the:

  • name
  • id
  • collateral
  • numeric traits

TheGraph query and response

We are ordering the data by gotchiId instead of id because gotchiId is numeric. Try ordering it by id and you will see why this is important.

So how come we also aren’t getting the SVG data? Well if you look at the Aavegotchi schema you will see that there is no corresponding property for the SVG (at the time of writing this tutorial that is). This is an example of where we will be using Web3 later.

Using our query in React

Now that we have our query let's use it in our app. For this, we need to install 2 packages, graphQL and graphql-request as our graphQL client. So open up a new terminal, and inside your aavegotchi-dex directory run:

npm install graphql-request graphql
Enter fullscreen mode Exit fullscreen mode

Once they have installed, in App.tsx put in the following new lines of code

//App.tsx

import { useEffect } from 'react';
import { request } from "graphql-request"; // <-- New line
import './App.css';

const uri = 'https://api.thegraph.com/subgraphs/name/aavegotchi/aavegotchi-core-matic';

function App() {

// Make sure this function is now Asynchronous
const fetchGotchis = async () => {
   const query = `
   {
     aavegotchis(first: 100, orderBy: gotchiId) {
       id
       name
       collateral
       withSetsNumericTraits
     }
   }
 `
 const response = await request(uri, query);
 console.log(response);
}
...
Enter fullscreen mode Exit fullscreen mode

If you are getting a Typescript runtime error, run npm install @types/react --dev.

If you have done everything correctly you should now see the aavegotchi data logged in your console exactly as you requested it. So what's happening?

Well, the imported request function requires 2 arguments, the target URL and the query. We get the URL from thegraph.com under Queries (HTTP), this tells the GraphQL request where to target.

The query is what we mocked up earlier and have now converted into a string. We then asynchronously waited for the response to return and logged it in the console.

Now that we know our request works, we need to store it in the Apps state so we can display it in the UI. For this, we use a React hook called useState(). However, because we are using Typescript we need to first set up our interface.

Let us create a new folder under src called types and inside create an index.ts file. Now in src/types/index.ts put in the following code:

//types/index.ts

export interface Gotchi {
 collateral: string;
 id: string;
 name: string;
 withSetsNumericTraits: Array<Number>;
}

export interface QueryResponse {
 aavegotchis: Array<Gotchi>
}
Enter fullscreen mode Exit fullscreen mode

Again, I'm not going to go over what the syntax of the Typescript means. You just need to understand that we are copying what we expect the response to look like from our Subgraph query.

Now at the top of App.tsx import our types and the useState hook from React, and edit the fetchGotchis function to store the response in the state:

//App.tsx

import { useEffect, useState } from 'react';
import { Gotchi, QueryResponse } from './types';
...

function App() {
  const [ gotchis, setGotchis ] = useState<Array<Gotchi>>([]);

  const fetchGotchis = async () => {
    const query = `
      {
        aavegotchis(first: 100, orderBy: gotchiId) {
          id
          name
          collateral
          withSetsNumericTraits
        }
      }
    `
    const response = await request<QueryResponse>(uri, query);
    setGotchis(response.aavegotchis)
  }
  ...
}
Enter fullscreen mode Exit fullscreen mode

Now that we have stored the data, we can map it onto the screen. If you type this out manually within the App components return function you will be able to see Typescript coming into play and suggesting properties for you. It will also flag up any mistypes (the amount of time this will save you from bug fixing is dreamy).

//App.tsx

return (
  <div className="App">
    {gotchis.map((gotchi, i) => {
      return (
        <p key={i}>{gotchi.name}</p>
      )
    })}
  </div>
);
Enter fullscreen mode Exit fullscreen mode

We should now see a list of names on the screen.

Alt Text

However, this doesn't look very interesting. So what we are gonna do is create a new component for the Aavegotchi listing, that allows you to select an Aavegotchi.
 

Structuring our code

In App.tsx replace the returned JSX with the following code:

//App.tsx

return (
  <div className="App">
    <div className="container">
      <div className="selected-container">
      </div>
      <div className="gotchi-list">
      </div>
    </div>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

and inside App.css replace the CSS with:

.App {
 display: block;
 text-align: center;
 height: 100vh;
 background-color: #FA34F3;
 box-sizing: border-box;
}

.container {
 display: grid;
 grid-template-rows: 50% 50%;
 box-sizing: border-box;
 height: 100%;
 width: 100%;
}

.gotchi-list {
 background-color: white;
 border-left: 5px solid black;
 border-right: 5px solid black;
 height: 100%;
 overflow-x: hidden;
 overflow-y: scroll;
 box-sizing: border-box;
}

@media (min-width: 768px) {
 .container {
   max-width: 1300px;
   margin: 0 auto;
   grid-template-columns: 1fr 1fr;
   grid-template-rows: revert;
 }
 .selected-container {
   box-sizing: border-box;
   padding: 16px;
   height: 100%;
 }
}
Enter fullscreen mode Exit fullscreen mode

We now want to create a new component for each Aavegotchi listing as well as for the selected Aavegotchi.

So within src create a new folder called components and inside create two more folders called GotchiListing and SelectedGotchi which both have an index.tsx and a styles.css file.

Your folder structure should now look like this:
Folder structure

Render our Aavegotchi listing

Inside GotchiListing/index.tsx copy and paste in the following content:

//GotchiListing/index.tsx

import "./styles.css"

interface Props {
  id: string;
  name: string;
  collateralColor: string;
  selected: boolean;
  selectGotchi: () => void;
}

export const GotchiListing = ({ id, name, collateralColor, selected, selectGotchi }: Props) => {
 return (
   <div className={`gotchi-listing ${selected && 'selected'}`} onClick={() => selectGotchi()}>
     <div className="collateral-container">
       <div className="collateral" style={{ backgroundColor: collateralColor }} />
       </div>
     <p className="id">{id}</p>
     <p className="name">{name}</p>
   </div>
 )
}
Enter fullscreen mode Exit fullscreen mode

The interface tells the editor that the GotchiListing component expects the following properties:

  • name - Name of the Aavegotchi
  • Id - Id of the Aavegotchi
  • collateralColor - Primary color of the collateral (more on this later)
  • selected - boolean of whether the item is selected or not
  • selectGotchi - Function that passes the click event to the parent

Inside GotchiListing/styles.css put:

.gotchi-listing {
  display: flex;
  cursor: pointer;
}
.gotchi-listing.selected,
.gotchi-listing:hover {
  background-color: #fffa65;
}

.collateral-container {
  width: 54px;
  display: grid;
  place-items: center;
}

.collateral {
  width: 32px;
  height: 32px;
  border-radius: 50%;
}

.id {
  padding-right: 12px;
  width: 60px;
  text-align: right;
}

.name {
  text-transform: uppercase;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}
Enter fullscreen mode Exit fullscreen mode

Now inside App.tsx lets import in and render our new component!

At the top with the other imports put:

//App.tsx

import { GotchiListing } from './components/GotchiListing';
Enter fullscreen mode Exit fullscreen mode

And inside the div with a className of gotchi-list we should map out our <GotchiListing /> component for each Aavegotchi stored in our state:

//App.tsx

<div className="gotchi-list">
  {
     gotchis.map((gotchi, i) => (
       <GotchiListing
         key={gotchi.id}
         id={gotchi.id}
         name={gotchi.name}
         collateralColor="black"
         selectGotchi={() => null}
         selected={false}
       />
     ))
   }
</div>
Enter fullscreen mode Exit fullscreen mode

If you are getting a run time error, inside SelectedGotchi/index.tsx type and save export {} as a temporary fix.

By doing this you should now be able to scroll through the list of Aavegotchis.

Gotchi list displaying

You may have noticed that we are not passing the gotchi.collateral to the collateralColor. That's because the collateral we get returned isn't a hex code, but a unique ID for the collateral. We will have to use Web3 later to call the aavegotchi contract to receive what the corresponding color should be.
 

Selecting an Aavegotchi

Time to put in our selection logic. First, we create another state within App.tsx for the selected Aavegotchi's index:

//App.tsx

const [ selectedGotchi, setSelectedGotchi ] = useState<number>(0);
Enter fullscreen mode Exit fullscreen mode

Now when we click on a listing, we want to set the index position of the selected gotchi inside the state. And then we can use that information to check whether a listed gotchi is selected or not:

//App.tsx

<GotchiListing
  ...
  selectGotchi={() => setSelectedGotchi(i)}
  selected={i === selectedGotchi}
/>
Enter fullscreen mode Exit fullscreen mode

Great! When you click a listing you should now see the listing is highlighted.

Now let us display the selection in a new component called SelectedGotchi. Inside SelectedGotchi/index.tsx paste in the following code:

//SelectedGotchi/index.tsx

import './styles.css'

interface Props {
 name: string;
 traits: Array<Number>;
}

export const SelectedGotchi = ({ name, traits }: Props) => {
  return (
    <div className="selected-gotchi-container">
      <div className="name-container">
        <h2>{name}</h2>
      </div>
      <div className="svg-container" />
      <div className="traits-container">
        <div className="trait">
          <p>⚡ Energy</p>
          <p>{traits[0]}</p>
        </div>
        <div className="trait">
          <p>👹 Aggression</p>
          <p>{traits[1]}</p>
        </div>
        <div className="trait">
          <p>👻 Spookiness</p>
          <p>{traits[2]}</p>
        </div>
        <div className="trait">
          <p>🧠 Brain size</p>
          <p>{traits[3]}</p>
        </div>
        <div className="trait">
          <p>👀 Eye shape</p>
          <p>{traits[4]}</p>
        </div>
        <div className="trait">
          <p>👁 Eye color</p>
          <p>{traits[5]}</p>
        </div>
      </div>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Inside SelectedGotchi/styles.css:

.selected-gotchi-container {
  display: grid;
  grid-template-rows: 15% 35% 50%;
  width: 100%;
  height: 100%;
  max-height: 100%;
}

.name-container {
  display: grid;
  place-items: center;
  border: 5px solid #e5df40;
  background-color: #fffa65;
  text-transform: uppercase;
}
.name-container h2 {
  margin: 0;
}

.svg-container {
  display: grid;
  place-items: center;
}
.svg-container > svg {
  height: 100%;
}

.traits-container {
  padding: 0.4rem;
  background-color: white;
  border: 5px solid black;
  display: grid;
  grid-template-columns: 1fr 1fr;
  row-gap: 12px;
  column-gap: 16px;
}
.trait {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.trait p {
  margin: 0;
  text-transform: uppercase;
}

@media (min-width: 768px) {
  .selected-gotchi-container {
    grid-template-rows: 72px 1fr 170px;
  }

  .svg-container > svg {
    height: revert;
    max-height: 450px;
  }

  .traits-container {
    padding: 1.6rem;
  }
}

Enter fullscreen mode Exit fullscreen mode

Now we render our new component in App.tsx like so:

//App.tsx

...

import { SelectedGotchi } from './components/SelectedGotchi';

...

function App() {

 ...

 return (
  <div className="App">
    ...
      <div className="selected-container">
        {gotchis.length > 0 && (
          <SelectedGotchi
            name={gotchis[selectedGotchi].name} 
            traits={gotchis[selectedGotchi].withSetsNumericTraits}
          />
        )}
      </div>
      ...
  </div>
 );
}

export default App;
Enter fullscreen mode Exit fullscreen mode

What we are doing is checking if any gotchis' exist in the array, then we're rendering in our <SelectedGotchi /> component. We then use the selectedGotchi index to get the target gotchis name and traits from the gotchis array.

You should now be able to select a gotchi and see the name and traits change in our new component!

Selected gotchi component

Great! Well done for making it this far, for the final part of the tutorial we will be using Web3 to fetch the data that we couldn't get from the subgraph.
 

Using Web3

To view information on the blockchain you need 3 things:

  1. A provider
    The provider is your choice of the node which talks to the Matic Network. If you have Metamask installed or are you using an Ethereum compatible browser then that will be used as your line of communication.

  2. The smart contracts address
    This is essentially the URL of the target smart contract, we can find what this is by going on the Aavegotchi Contracts GitHub for the contracts.
    We want the Aavegotchi diamond address as it has access to all the facets we need.

  3. The ABI (application binary interface)
    This is a JSON file whose job is to encode and decode the calls to and from the Solidity contract. We also can download/copy this from the Aavegotchi Github here.

Once we have located everything we need, we can start using it within our App.

Setting up the contract

Let us start by installing web3:

npm install web3
Enter fullscreen mode Exit fullscreen mode

Now within the src folder of our app lets create a new folder called abi and inside it create a JSON file called diamondABI.json. In this file, we want to copy and paste the whole JSON object from the Github.

Inside App.tsx we can now import the following:

//App.tsx

import Web3 from 'web3';
import diamondABI from './abi/diamondABI.json';
import { Contract } from 'web3-eth-contract';
import { AbiItem } from 'web3-utils/types'

const diamondAddress = '0x86935F11C86623deC8a25696E1C19a8659CbF95d';
Enter fullscreen mode Exit fullscreen mode

Contract and AbiItem are used only for Typescript purposes.

We also set the diamondAddress as a const using the address we found in the Aavegotchi Contract Github.

Now we have everything we need to view data from the Aavegotchi Blockchain. Inside App() let's create a new function called connectToWeb3() that will create our contract and save it in our state.

We want to call this function when the page first renders, therefore we put it in the useEffect() after fetchGotchis().

// App.tsx

function App() {
  ...
  const [ contract, setContract ] = useState<Contract | null>(null);

  const connectToWeb3 = () => {
    const web3 = new Web3(Web3.givenProvider);
    const aavegotchiContract = new web3.eth.Contract(diamondABI as AbiItem[], diamondAddress);
    setContract(aavegotchiContract);
  }

  ...

  useEffect(() => {
    fetchGotchis();
    connectToWeb3();
  }, [])
Enter fullscreen mode Exit fullscreen mode

For the provider we have used Web3.givenProvider, this is automatically available if you are using an Ethereum compatible browser. If you don't have an Ethereum compatible browser then instead you can set up a remote or local node and use that as your provider.

Calling methods from the contract

Now that our contract is set up we can start calling methods off of it. You may be aware that calling methods on a contract may require gas. However, this only applies to methods that have to add, delete or change information on the contract. Just viewing data requires no manipulation of the contract and is therefore completely gas-free!

The first method we want to call is one to fetch the collateral primary colors so we can pass each <GotchiListing /> the correct color. By visiting the Aavegotchi Developer Documentation you can find the methods names for the various contracts. We want the getCollateralInfo() function as located here.

You can also find all of the contracts functions as well as their outputs and inputs within the ABI!

We want to fetch all of the collateral information in one request, however, we need to make sure that the contract is set up first.

To do this, create a new useEffect() hook within App.tsx which has the contract as a dependency:

//App.tsx

useEffect(() => {
  if (!!contract) {
    const fetchAavegotchiCollaterals = async () => {
      const collaterals = await contract.methods.getCollateralInfo().call();
      console.log(collaterals);
    };
    fetchAavegotchiCollaterals();
  }
}, [contract]);
Enter fullscreen mode Exit fullscreen mode

As you can see, the fetchAavegotiCollaterals() function will only be triggered if contract is truthy. Therefore on initial render, it will not trigger as the contract wouldn't be set up yet. Therefore by adding contract as a dependency, useEffect() will now trigger as a side effect to the contract changing.

If everything has been put in correctly you should now see the different collaterals logged in your browser's console.

Logged collaterals

We can use the logged output to create our type definition so our code editor knows what we are expecting. So inside src/types/index.ts let's create a new interface for Collateral like so:

// types/index.ts

export interface Collateral {
  collateralType: string;
  collateralTypeInfo: {
    cheekColor: string;
    conversionRate: string;
    delisted: boolean;
    eyeShapeSvgId: string;
    modifiers: Array<string>;
    primaryColor: string;
    secondaryColor: string;
    svgId: string;
  }
}
Enter fullscreen mode Exit fullscreen mode

In App.tsx lets import our new interface and create a new state which expects an array of collaterals and in our fetchAavegotchiCollaterals() function we can set the state:

//App.tsx

function App() {
  ...
  const [ collaterals, setCollaterals ] = useState<Array<Collateral>>([]);
  ...

 useEffect(() => {
  if (!!contract) {
    const fetchAavegotchiCollaterals = async () => {
      const collaterals = await contract.methods.getCollateralInfo().call();
      setCollaterals(collaterals); // <- Replaced console.log()
    };

    fetchAavegotchiCollaterals();
  }
}, [contract]);

Enter fullscreen mode Exit fullscreen mode

We should now have all the collaterals stored in the state, so let's create a function that takes the gotchi.collateral, finds it within collaterals and returns the corresponding primaryColor.

//App.tsx

function App() {

  ...

  const getCollateralColor = (gotchiCollateral: string) => {
    const collateral = collaterals.find(item => item.collateralType.toLowerCase() === gotchiCollateral);
    if (collateral) {
      return collateral.collateralTypeInfo.primaryColor.replace("0x", '#');
    }
    return "white";
  }

  ...

  return (
    <div className="App">
        ...
        <div className="gotchi-list">
          {
            gotchis.map((gotchi, i) => (
              <GotchiListing
                ...
                collateralColor={getCollateralColor(gotchi.collateral)}
                ...
              />
            ))
          }
        </div>
        ...
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

The replace operation correctly formats the primaryColor value into a hex code that the CSS can interpret.

Your gotchi listing should now have the color that represents the gotchis collateral (If you wanted to go a step further you could see if you can put in the logic to display the correct collateral icon).

Collateral colors in listings
 

Displaying the Aavegotchi SVG

All we have left to do is display the selected Aavegotchis image. This is arguably my favourite thing about Aavegotchi as all the SVG’s are stored within the blockchain itself!

If you go back to the Aavegotchi developer wiki you can locate the method we want, which is getAavegotchiSvg(tokenId). This method requires passing an Aavegotchis Id as a parameter.

Every time we select a gotchi we want to render the SVG within our SelectedGotchi component. Therefore we need a new useEffect() hook that will trigger every time selectedGotchi, gotchis or contract changes:

//App.tsx

function App() {
  ...
  const [ gotchiSVG, setGotchiSVG ] = useState<string>('');

  ...

  useEffect(() => {
    const getAavegotchiSVG = async (tokenId: string) => {
      const svg = await contract?.methods.getAavegotchiSvg(tokenId).call();
      setGotchiSVG(svg);
    };

    if (contract && gotchis.length > 0) {
      getAavegotchiSVG(gotchis[selectedGotchi].id)
    }
  }, [selectedGotchi, contract, gotchis]);

Enter fullscreen mode Exit fullscreen mode

If you were to console.log(gotchiSVG) you would see that the output is HTML code. This is the SVG data that we want to inject into our code.

We then pass the SVG data into our <SelectedGotchi /> component like so:

//App.tsx

<SelectedGotchi
  svg={gotchiSVG}
  name={gotchis[selectedGotchi].name} 
  traits={gotchis[selectedGotchi].withSetsNumericTraits}
/>
Enter fullscreen mode Exit fullscreen mode

Within the SelectedGotchi component, we need to add the svg property to the interface so we can use it as a prop.

So go to src/components/SelectedGotchi/index.tsx and add the following changes:

// SelectedGotchi/index.tsx

import './styles.css'

interface Props {
  name: string;
  traits: Array<Number>;
  svg: string; //<-- New prop
}

export const SelectedGotchi = ({ name, traits, svg }: Props) => {
  return (
    <div className="selected-gotchi-container">
      ...
      <div className="svg-container" dangerouslySetInnerHTML={{ __html: svg }} />
      ...

Enter fullscreen mode Exit fullscreen mode

If everything is done correctly you should now be able to see your selected Aavegotchi!

Aavegotchi SVG displaying

As the SVG data is being rendered in the DOM you can use your browsers element inspector to identify the class names of the different layers of the SVG. This is useful if you want to animate or hide certain layers of the Aavegotchi SVG.

To show this we are going to hide the SVG background by pasting into SelectedGotchi/styles.css:

.svg-container .gotchi-bg {
  display: none;
}
Enter fullscreen mode Exit fullscreen mode

The background should now be hidden!

Aavegotchi background hidden
 

Conclusion

Nice one fren! In this tutorial, you have learnt how to use both the Subgraph and Web3 to create a decentralised React App!

You should now be equipped with the knowledge you need to take the app a step further. You could add in an infinite scroll that concats more Aavegotchi data into the listing... Or perhaps some filter or sorting functionality that sends a new query to the subgraph to fetch more data?

If you have any questions about Aavegotchi or want to build more DApps, then join Aavegotchi discord community where you will be welcomed with open arms!

 

You can find the completed code here:
https://github.com/cgcbrown/aavegotchi-dex-tutorial

Discussion (2)

pic
Editor guide
Collapse
dominicwong profile image
Dominic Wong

Great tutorial, looking forward to seeing more Web3 tutorials from you!

Collapse
highplains66 profile image
highplains66

this is awesome ty!! cant wait to get some time and pour over this. cheers!!!!