Using RedwoodJS to create an Ethereum app

RedwoodJS is well-suited for dapp development. In this brief tutorial you'll get an idea why.

November 19, 2020

In this code-along I’ll walk you through how I used RedwoodJS to create an ethereum app. The key ethereum-related advantage of Redwood is that it is built for graphql. And I don’t just mean like how GatsbyJS is built. Redwood is … really, really good at graphql. If you’re familiar with using TheGraph to build dapps, then this is going to be unicorns and rainbows in terms of opening up the creative mental space.

In a way, you can use Redwood to create your own “poor-man’s subgraph” (as James Young would call it), with very little effort. This is especially thanks to the amazing folks at Prisma, who are heads-down working on their second version, which is what Redwood uses. The time it took to create this app could have easily taken weeks using the original Prisma v1 and Gatsby, but it only took me a couple days here. The Redwood team has created something magical.

Did I mention that the devops flow is STUPID EASY? This all may seem daunting at first, but trust me - you will quickly come to love Redwood.

I know, I know, you’re having TheGraph withdrawal symptoms just thinking about leaving them. Luckily, you can still plug those sweet subgraphs directly into Redwood. After tackling your first app using native Redwood tooling, you’ll be able to add TheGraph with confidence.

So what are you waiting for? Give it a go!

yarn create redwood-app ./redwoodblog

But Patrick, what are we building?

Here are the rough steps I took to create Emanator, a hackathon submission for the recent Superfluid hackathon hosted on Gitcoin.

The basic idea is this:

  1. Allow users to create a series of NFTs and auction them off.
  2. Each NFT contract gets its own auction page.
  3. We want to keep track of every new auction deployed, and show some more info besides just on-chain data (remember when we used to use databases before we had blockchains?)
  4. Demo redwood web3 app: https://emanator.patrickgallagher.dev
  5. Hackathon submission + writeup: https://github.com/superfluid-finance/superfluid-protocol-preview/pull/8
  6. Source: https://github.com/emaNaFTe/monorepo.

👩‍💻 Setting up the App

The following redwood commands do a bunch of tasks to save time, without getting in the way like some code-generators and templates you may be familiar with.

yarn rw generate layout default
yarn rw generate page home /
yarn rw generate page auction {auctionAddress}

That last commands sets up all the routing we need so we can navigate to a particular auction using an identifier. In this case we call it “auctionAddress”.

🤖 Database setup

Update schema.prisma to add the auction object. I just made up some stuff I thought would be useful- since the contracts weren’t written yet.

model Auction {
  id Int @id @default(autoincrement())
  address String @unique
  name String
  winLength Int
  owner String
  description String?
  createdAt  DateTime  @default(now())
  status String @default("started")
  highBid Int @default(0)
  generation Int @default(0)
  revenue Int @default(0)
}

Now we can create our local development database

yarn rw db save
yarn rw db up

Finally we will use schema.prisma to generate some components for us.

yarn rw generate scaffold Auction

This command also provides things for edit/& delete functionality (blockchain is immutable?) so I removed these components and routes from routes.js

Boom! A working frontend & backend. We should now be able to create new auctions by entering dummy data.

✍ Edit the form

Now we need to add functionality to deploy the contract when the Auction form is submitted. Once we can deploy a new auction, we can use the contract address and owner address to populate our database.

I added the web3 deployment function to web/src/web3/deploy.js which takes the form data and deploys a contract from the user’s wallet. The return values from deployAuction() are then added to the existing Auction mutation like this:

// web/src/components/NewAuction/NewAuction.js
const onSave = async (input) => {
  const { address, owner, error } = await deployAuction(input)
  if (error) return console.log(error.message)
  createAuction({ variables: { input: { ...input, address, owner } } })
}

🐕 Fetch web3 data

Now that we can deploy a contract, we need to get web3 data into our app. I took a page from the Redwood cookbook Using a Third Party API to do this. I choose to use the server, rather than the frontend, to make Web3 calls. Doing it this way makes things muuuuch simpler.

Why? Because Redwood is very very good at consuming graphql data. So if my server can just spit everything out in graphql format, then the app won’t even know that it’s asking for slow, asynchronous web3 data. It also means better security. Remember how you aren’t technically supposed to put your Infura/provider keys in the FE… but everyone does it anyways?

To start, I made a new SDL called “Web3”. Define the schema in web3.sdl.js.

   type Web3User {
     superTokenBalance: String!
     isSubscribed: Boolean!
   }

   type Query {
     web3User(address: String!, auctionAddress: String!): Web3User!
     // ...
   }

Here I did not follow the Redwood established pattern of naming the SDL and Services by graphql Type (eg. “Web3Auction” and “Web3User”)

Note: Since web3 data is external to our database, we do not need to update our graphwl schema.prisma. Additionally, we cannot use the yarn rw scaffold command since it relies on schema.prisma.

Create the service file services/web3/web3.js.

yarn rw g service web3

Add the Web3 calls using web3/ethers. Then we can start writing some queries in the graphql playground at http://localhost:8911/graphql

   query GET_WEB3_USER($address: String!, $auctionAddress: String!) {
      web3User(address:$address, auctionAddress: $auctionAddress) {
      superTokenBalance
      isSubscribed
   }

💅 Display web3 data

Now that we have some new web3 queries, there are two ways to use them depending on our data source: web3, or web3+web2

web3-only - Make a new component

For Users I only care about web3 data, so I chose to make a new cell and component. Remember that since prisma.schema isn’t defined for these, you will need to manually add the beautiful graphql queries you wrote earlier to each new “Cell”.

yarn rw g cell web3User
yarn rw g component web3User

web3+web2 - Combine with existing components

For Auctions I wanted to use data from both the Prisma Database and web3, so I elected to add the new query to the existing one in AuctionCell.js. I did not make any new components for this.

query FIND_AUCTION_BY_ADDRESS($address: String!) {
  auction(address: $address) {
    id
    name
    address
    description
    createdAt
    revenue
    winLength
    owner
  }
  // NEW STUFF
  web3Auction(address: $address) {
    endTime
    lastBidTime
    auctionBalance
    highBid
    highBidder
    status
  }
}

⏳ Staying updated

As a quick and dirty way to keep the app updated with Web3 data, I added the following function to Auctioncell.js and Web3UserCell.js (see Generating a Cell). This causes ApolloClient to poll every 5 seconds, and not rely on cached data, so the server must actually make the web3 calls again. This could be done a better way, but it works for now.

export const beforeQuery = (props) => {
  return { variables: props, fetchPolicy: 'network-only', pollInterval: 5000 }
}

🏅 Challenge - Keep the Database in sync with Web3

Right now, when a new auction is deployed, the database is updated immediately, without waiting to see if the deployment is successful. A better solution would be to wait for the deployment to succeed before creating the database entry. If we write this logic in the frontend, the user may close the page before it can finish, and the database will not include the freshly deployed auction.

Instead we should send the deployment details to the server, which will wait on the pending transaction before processing the mutation. I’ll leave this up to you to think of a good way to implement this.

🚢 Ship it

I used Heroku for the database since it was fast+easy and this was a hackathon. I just followed the Redwood tutorial here. For the serverless component, things got a little tricky.

Since Redwood is not in production yet, and this is my first time using it, I met a couple of challenges here. None of which were out-of-the-ordinary, and I was able to overcome all of them after some help from the community.

The first issue I encountered was that yarn rw build was not building my contracts. I solved this by adding to the build command:

// Default
yarn rw build && yarn rw db up --no-db-client --auto-approve && yarn rw dataMigrate up

// Now contracts are built first
cd contracts && yarn build && cd .. && yarn rw build //...

I scrapped this effort (for multiple reasons), and checked the built contracts into version-control and updated the package.json accordingly. Its hacky, but its simple.

Next, Vercel complained that my serverless function was too large by about 5mb. Thanks to some helpful advice from the Redwood Discord, and some Github issue hunting, I learned of a tool to help find the culprit. I discovered that truffle was eating up a ton of space. I moved some things around, and tried again.

yarn rw build api
yarn zip-it-and-ship-it api/dist/functions/ zipped

I was now down to 62mb from ~72mb, which is still over the 50mb limit… so no dice. My suggestion for you is to:

  • Avoid using web3.js in your serverless functions, in favor of ethers.
  • Avoid importing contract packages which have lots of compiled contract files and web3 dependencies

In the end, I had to self-host the app using pm2 + nginx. Thankfully there was a cool example provided in the Redwood docs. I am really looking forward to my next redwood app, so I can apply the things I learned here!

Hit me up if you want some DAIx so you can bid on an NFT!

Happy building!

Does Redwood have an emoji? Would it be 🌲?

📔 Notes + Resources