https://github.com/pooltogether/ethdenver-graphql-workshop
https://github.com/pooltogether/ethdenver-graphql-workshop
Last synced: 3 months ago
JSON representation
- Host: GitHub
- URL: https://github.com/pooltogether/ethdenver-graphql-workshop
- Owner: pooltogether
- Created: 2020-02-13T00:25:30.000Z (over 6 years ago)
- Default Branch: master
- Last Pushed: 2020-02-14T20:04:49.000Z (over 6 years ago)
- Last Synced: 2025-04-25T04:03:29.310Z (about 1 year ago)
- Size: 7.81 KB
- Stars: 9
- Watchers: 3
- Forks: 5
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# Building a Dapp Using GraphQL
You can literally cut-and-paste most of the code in this tutorial. A few spots just show the diff.
# Part 1: Build a simple dApp using Tightbeam
View the [complete dapp source on Github](https://github.com/pooltogether/ethdenver-graphql-workshop-app)
## Step 1: Setup
Create the app
```bash
$ yarn create next-app pool-app
$ cd pool-app
```
Install Ethers, Tightbeam, and Apollo Client
```bash
$ yarn add ethers @pooltogether/tightbeam apollo-boost apollo-link-state graphql graphql-tag @apollo/react-hooks
```
## Step 2: Initialize Apollo Client and Tightbeam
Create a new file `lib/createApolloClient.js`
```javascript
// lib/createApolloClient.js
import { Tightbeam } from '@pooltogether/tightbeam'
import { withClientState } from 'apollo-link-state'
import { InMemoryCache } from 'apollo-cache-inmemory'
import { ApolloClient } from 'apollo-client'
import { ApolloLink } from 'apollo-link'
export function createApolloClient() {
const tb = new Tightbeam()
// Create a place to store data client-side
const cache = new InMemoryCache()
// Ensure that the default state is set
cache.writeData(tb.defaultCacheData())
// Now attach the Tightbeam resolvers
const stateLink = withClientState({
cache,
resolvers: tb.resolvers()
})
// Hook up the Tightbeam Multicall for speedy call batching
const link = ApolloLink.from([
tb.multicallLink(),
stateLink
])
// Create the Apollo Client
return new ApolloClient({
cache,
link
})
}
```
Connect the Apollo Client to React in `pages/index.js`
```javascript
// pages/index.js
import React from 'react'
import { ApolloProvider } from '@apollo/react-hooks'
import { createApolloClient } from '../lib/createApolloClient'
let apolloClient = createApolloClient()
const Home = () => (
Ready for web3!
)
export default Home
```
## Step 3: Integrate ABIs
Create a directory `lib/abis` and import the following abis:
**Dai Stablecoin ABI**
Copy the [Dai ABI on Etherscan](https://etherscan.io/address/0x6b175474e89094c44da98b954eedeac495271d0f#code) and paste into `lib/abis/Dai.json`
**PoolTogether PoolDai ABI**
Copy the [PoolTogether Dai Pool ABI on Etherscan](https://etherscan.io/address/0x932773ae4b661029704e731722cf8129e1b32494#code) and paste into `lib/abis/DaiPool.json`
**Note**: This is the address for the proxy *implementation*, not the proxy itself.
Let's integrate our ABIs into Tightbeam.
Create a file `lib/abiMapping.js`:
```javascript
// lib/abiMapping.js
import { AbiMapping } from '@pooltogether/tightbeam'
import Dai from './abis/Dai'
import DaiPool from './abis/DaiPool'
export const abiMapping = new AbiMapping()
abiMapping.addContract('Dai', 1, '0x6B175474E89094C44Da98b954EedeAC495271d0F', Dai)
abiMapping.addContract('DaiPool', 1, '0x29fe7D60DdF151E5b52e5FAB4f1325da6b2bD958', DaiPool)
```
Now pass the abiMapping to the Tightbeam config in `lib/createApolloClient.js`:
```javascript
// lib/createApolloClient.js
// ...
import { abiMapping } from './abiMapping'
export function createApolloClient() {
const tb = new Tightbeam({
abiMapping
})
// ...
}
```
## Step 4: Display Token Supplies
Create a new component `components/TokenSupplies.jsx`
```javascript
// components/TokenSupplies.jsx
import { useQuery } from '@apollo/react-hooks'
import gql from 'graphql-tag'
import { ethers } from 'ethers'
const supplyQuery = gql`
query {
poolCommittedSupply: call(contract: "DaiPool", fn: "committedSupply") @client
poolOpenSupply: call(contract: "DaiPool", fn: "openSupply") @client
daiSupply: call(contract: "Dai", fn: "totalSupply") @client
}
`
export function TokenSupplies() {
const { loading, error, data } = useQuery(supplyQuery)
let result = 'Loading...'
if (error) {
result = `Error: ${error.message}`
} else if (data) {
result =
Pool Committed Supply: {ethers.utils.formatEther(data.poolCommittedSupply)}
Pool Open Supply: {ethers.utils.formatEther(data.poolOpenSupply)}
Dai Supply: {ethers.utils.formatEther(data.daiSupply)}
}
return result
}
```
Update `pages/index` to include the new component:
```javascript
// pages/index.js
// ... imports
import { TokenSupplies } from '../components/TokenSupplies'
const Home = () => (
Ready for web3!
)
// ... exports
```
# Part 2: Integrate The Graph Protocol
View the [complete subgraph source on Github](https://github.com/pooltogether/ethdenver-graphql-workshop-subgraph)
## Step 1: Setup
Move to another directory (i.e. not the front-end dir)
Make sure Graph Protocol is installed
```bash
$ yarn global add @graphprotocol/graph-cli
```
Init the project
```bash
$ graph init --from-contract 0x932773ae4b661029704e731722cf8129e1b32494 asselstine/pooltogether-churn pooltogether-churn
```
Edit `subgraph.yaml` and set the address so that it will point to the proxy, and set the starting block to the block before the contract was created.
```yaml
source:
address: "0x29fe7D60DdF151E5b52e5FAB4f1325da6b2bD958"
abi: Contract
startBlock: 9133722
```
The `startBlock` field is very important to make your subgraph speedy.
## Step 2: Define Your Schema
Let's begin by thinking about some questions we'd like answered.
The question of churn:
How many users leave each prize? How many new users are added for each prize?
Let's define the schema in `schema.graphql`:
```graphql
type PoolPrize @entity {
id: ID!
drawId: BigInt!
depositCount: BigInt!
depositAmount: BigInt!
withdrawalCount: BigInt!
withdrawalAmount: BigInt!
}
```
Now re-generate the bindings:
```
$ yarn codegen
```
## Step 3: Process Events
The Graph Protocol can process Ethereum logs, new blocks, or even calls. We're going to focus on Events.
Let's start by clearing out the boilerplate in `src/mapping.ts`:
```typescript
// remove this line
import { ExampleEntity } from "../generated/schema"
// gut the following function so that it is empty
export function handleAdminAdded(event: AdminAdded): void {}
```
I have the benefit of knowing of how PoolTogether works, but I'll give a quick rundown here:
PoolTogether is a prize linked savings account. That means that a bunch of people deposit into a Pool that generates interest using the deposits and periodically that interest is awarded as a prize to a randomly selected participant.
Each prize goes through two phases to prevent people from gaming the system (i.e. depositing at the last second):
1. The Prize is opened. People can now deposit to be eligible for this prize.
2. The Prize is committed. No new deposits are accepted. If people withdraw they become ineligible for the prize.
3. The Prize is awarded to an eligible winner!
With that in mind, let's consider our schema. We defined a PoolPrize entity that counts it's deposits. There is an event called `Opened` that is emitted when the prize is opened. Let's start there in `src/mapping.ts`:
```typescript
// src/mapping.ts
// ...
import { PoolPrize } from '../generated/schema'
export function handleOpened(event: Opened): void {
let prize = new PoolPrize(event.params.drawId.toHexString())
prize.drawId = event.params.drawId
prize.depositCount = BigInt.fromI32(0)
prize.depositAmount = BigInt.fromI32(0)
prize.withdrawalCount = BigInt.fromI32(0)
prize.withdrawalAmount = BigInt.fromI32(0)
prize.save()
}
// ...
```
Now we need to count users deposits into the open Prize:
```typescript
// src/mapping.ts
// ...
export function handleDeposited(event: Deposited): void {
let contract = Contract.bind(event.address)
let drawId = contract.currentOpenDrawId()
let prize = PoolPrize.load(drawId.toHexString())
prize.depositCount = prize.depositCount.plus(BigInt.fromI32(1))
prize.depositAmount = prize.depositAmount.plus(event.params.amount)
prize.save()
}
// ...
```
Now let's count their withdrawals:
```typescript
// src/mapping.ts
// ...
export function handleWithdrawn(event: Withdrawn): void {
let contract = Contract.bind(event.address)
let drawId = contract.currentOpenDrawId()
let prize = PoolPrize.load(drawId.toHexString())
prize.withdrawalCount = prize.withdrawalCount.plus(BigInt.fromI32(1))
prize.withdrawalAmount = prize.withdrawalAmount.plus(event.params.amount)
prize.save()
}
// ...
```
Great! We're done.
## Step 4: Deploying
To deploy using The Graph hosted infrastructure, you'll need to go create a subgraph using [the explorer](https://thegraph.com/explorer).
Log in and create one.
Now let's add a convenience script to auth in `package.json`:
```json
{
"scripts": {
"auth": "graph auth https://api.thegraph.com/deploy/",
}
}
```
First we authorize ourselves
```bash
$ yarn auth
```
Then we deploy
```bash
$ yarn deploy
```
## Part 1: Step 5: Integration
Let's jump back to our previous project.
First we'll update our Apollo Client to point to the Graph Protocol endpoint in `lib/createApolloClient.js`:
```javascript
// lib/createApolloClient.js
// ...
import { createHttpLink } from 'apollo-link-http'
// ...
export function createApolloClient() {
// ...
const link = ApolloLink.from([
tb.multicallLink(),
stateLink,
createHttpLink({
uri: 'https://api.thegraph.com/subgraphs/name/asselstine/pooltogether-churn',
// this is a hack because we're using Next.js
fetch: (typeof window !== 'undefined') ? window.fetch : () => {}
})
])
// ...
}
```
And a component to view the data in `components/Prizes.jsx`:
```javascript
// components/Prizes.jsx
import { useQuery } from '@apollo/react-hooks'
import { ethers } from 'ethers'
import gql from 'graphql-tag'
const prizesQuery = gql`
query {
poolPrizes {
id
depositCount
depositAmount
withdrawalCount
withdrawalAmount
}
}
`
export function Prizes() {
const { loading, error, data } = useQuery(prizesQuery)
let result = 'Loading...'
if (error) {
result = `Error: ${error.message}`
} else if (data) {
result = (
Prize Id
Deposit Count
Withdrawal Count
Deposit Amount
Withdrawal Amount
{data.poolPrizes.map(poolPrize => (
{poolPrize.id.toString()}
{poolPrize.depositCount.toString()}
{poolPrize.withdrawalCount.toString()}
{ethers.utils.formatEther(poolPrize.depositAmount, {commify: true, pad: true})}
{ethers.utils.formatEther(poolPrize.withdrawalAmount, {commify: true, pad: true})}
))}
)
}
return result
}
```
...and add the component to the `pages/index.js`:
```javascript
// ...
import { Prizes } from '../components/Prizes'
// ...
const Home = () => (
Ready for web3!
)
```
And we're done!