Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mutations #20

Merged
merged 5 commits into from
Feb 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions subgraph/mutations/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
package
dist
bundle
8 changes: 8 additions & 0 deletions subgraph/mutations/mutations.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
specVersion: 0.0.1
repository: ...
schema:
file: ./schema.graphql
resolvers:
kind: javascript
file: ./bundle/index.js
types: ./bundle/index.d.ts
33 changes: 33 additions & 0 deletions subgraph/mutations/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "example-mutations",
"version": "0.1.0",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "yarn build:package && yarn build:bundle",
"build:bundle": "rimraf ./bundle && webpack",
"build:package": "tsc --outDir ./dist && rimraf ./package && mkdir ./package && cp -r ./dist ./package/dist && cp package.json ./package && cp -r ./src ./package/src",
"es5:check": "es-check es5 ./bundle/index.js"
},
"dependencies": {
"@graphprotocol/mutations": "^0.0.0-alpha.0",
"@types/node": "^13.7.1",
"ethers": "^4.0.40",
"graphql-tag": "^2.10.1",
"ipfs-http-client": "^42.0.0"
},
"devDependencies": {
"@babel/core": "^7.7.5",
"@babel/preset-env": "^7.7.6",
"babel-core": "^7.0.0-bridge.0",
"babel-loader": "^7.1.5",
"babel-plugin-add-module-exports": "^1.0.2",
"es-check": "^5.1.0",
"rimraf": "3.0.0",
"ts-loader": "6.2.1",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10"
}
}
1 change: 1 addition & 0 deletions subgraph/mutations/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# TODO: Schema
1 change: 1 addition & 0 deletions subgraph/mutations/src/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//TODO: tests
233 changes: 233 additions & 0 deletions subgraph/mutations/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import {
EventPayload,
MutationContext,
MutationResolvers,
MutationState,
StateBuilder,
StateUpdater,
} from "@graphprotocol/mutations"
import { ethers } from 'ethers'
import { Transaction } from 'ethers/utils'
import {
AsyncSendable,
Web3Provider
} from "ethers/providers"
import ipfsHttpClient from 'ipfs-http-client'

import {
sleep,
uploadToIpfs,
PROJECT_QUERY
} from './utils'
import { editProjectArgs, removeProjectArgs, addProjectArgs } from "./types"

interface CustomEvent extends EventPayload {
myValue: string
}

type EventMap = {
'CUSTOM_EVENT': CustomEvent
}

interface State {
myValue: string
myFlag: boolean
}

const stateBuilder: StateBuilder<State, EventMap> = {
getInitialState(): State {
return {
myValue: '',
myFlag: false
}
},
reducers: {
'CUSTOM_EVENT': async (state: MutationState<State>, payload: CustomEvent) => {
return {
myValue: 'true'
}
}
}
}

type Config = typeof config

const config = {
ethereum: (provider: AsyncSendable): Web3Provider => {
return new Web3Provider(provider)
},
ipfs: (endpoint: string) => {
const url = new URL(endpoint)
return ipfsHttpClient({
protocol: url.protocol.replace(/[:]+$/, ''),
host: url.hostname,
port: url.port,
'api-path': url.pathname.replace(/\/$/, '') + '/api/v0/',
})
}
}

type Context = MutationContext<Config, State, EventMap>

async function queryProject(context: Context, projectId: string) {
const { client } = context

if (client) {
for (let i = 0; i < 20; ++i) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we want to loop 20 times and query a project? @namesty

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Its because of a bug we used to have in the spec, where the graph node wasn't updated fast enough, so at the time the query ran, the data was not still there

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nenadjaja this is because it's not guaranteed the graph-node we're querying has processed the block our "create project" transaction is in. We can solve this looping problem by using future queries, but I don't believe they're implemented yet. @Jannis ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a better way to loop. If we know the block that the transaction was mined in, we can query with

{
  project(id: ..., block: { hash: "..." }) {
    ...
  }
}

That will return an error if the block is not available yet. We can loop until it is available.

There is, of course, the chance, that the block never becomes available (e.g. when it never ends up on the canonical chain). For that, we could abort the loop after e.g. 30s or 60s and throw an error from the mutation. At that point we can be pretty certain that the transaction never truly made it to the chain, I think.

const { data } = await client.query({
query: PROJECT_QUERY,
variables: {
id: projectId,
}
}
)

if (data === null) {
await sleep(500)
} else {
return data.project
}
}
}

return null
}

async function sendTx(tx: Transaction, description: string, state: StateUpdater<State, EventMap>) {
try {
await state.dispatch('TRANSACTION_CREATED', {
id: tx.hash,
to: tx.to,
from: tx.from,
data: tx.data,
amount: tx.value.toString(),
network: `ethereum-${tx.chainId}`,
description
})
tx = await tx
await state.dispatch('TRANSACTION_COMPLETED', { id: tx.hash, description: tx.data })
return tx;
} catch (error) {
await state.dispatch('TRANSACTION_ERROR', error)
}
}

// const abis = {
// Context: require('token-registry-contracts/build/contracts/Context.json').abi,
// dai: require('token-registry-contracts/build/contracts/dai.json').abi,
// EthereumDIDRegistry: require('token-registry-contracts/build/contracts/EthereumDIDRegistry.json').abi,
// LibNote: require('token-registry-contracts/build/contracts/LibNote.json').abi,
// Ownable: require('token-registry-contracts/build/contracts/Ownable.json').abi,
// Registry: require('token-registry-contracts/build/contracts/Registry.json').abi,
// ReserveBank: require('token-registry-contracts/build/contracts/ReserveBank.json').abi,
// SafeMath: require('token-registry-contracts/build/contracts/SafeMath.json').abi,
// Everest: require('token-registry-contracts/build/contracts/Everest.json').abi,
// MemberStruct: require('token-registry-contracts/build/contracts/MemberStruct.json').abi,
// }

// const addresses = require('token-registry-contracts/addresses.json')

// const addressMap = {
// Dai: 'mockDAI',
// EthereumDIDRegistry: 'ethereumDIDRegistry',
// ReserveBank: 'reserveBank',
// TokenRegistry: 'tokenRegistry',
// }

async function getContract(context: Context, contract: string) {
const { ethereum } = context.graph.config

const abi = abis[contract]

if (!abi) {
throw new Error(`Missing the ABI for '${contract}'`)
}

const network = await ethereum.getNetwork()
let networkName = network.name

if (networkName === 'dev' || networkName === 'unknown') {
networkName = 'ganache'
}

const networkAddresses = addresses[networkName]

if (!networkAddresses) {
throw new Error(`Missing addresses for network '${networkName}'`)
}

const address = networkAddresses[addressMap[contract]]

if (!address) {
throw new Error(
`Missing contract address for '${contract}' on network '${networkName}'`,
)
}

const instance = new ethers.Contract(address, abi, ethereum.getSigner())
instance.connect(ethereum)

return instance
}

const uploadImage = async (_, { image }: any, context: Context) => {
const { ipfs } = context.graph.config

return await uploadToIpfs(ipfs, image)
}

const addProject = async (_, args: addProjectArgs, context: Context) => {

// const everest = await getContract(context)

// Dave's code goes here...
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! I will make a PR after we merge this one in, that adds the code

}

const removeProject = async (_, args: removeProjectArgs, context: Context) => {

const { projectId } = args

// const everest = await getContract(context)
// sendTx(everest.memberExit( ... ))

return true

}

const editProject = async (_, args: editProjectArgs, context: Context) => {

const { ipfs } = context.graph.config

const { id } = args

const metadata = Buffer.from( JSON.stringify( args ) )

const metadataHash = await uploadToIpfs(ipfs, metadata)

// const everest = await getContract(context)
// sendTx(everest.editOffChainDataSigned( ... ))

return await queryProject(context, id)
}

const resolvers: MutationResolvers<Config, State, EventMap> = {
Mutation: {
uploadImage,
addProject,
removeProject,
editProject
}
}

export default {
resolvers,
config,
stateBuilder
}

// Required Types
export {
State,
EventMap,
CustomEvent
}
96 changes: 96 additions & 0 deletions subgraph/mutations/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
//TODO: Args are any by default in graphQL, do we really need these interfaces?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's good to have them so it's easier to see them. Maybe we can use the same args for add/edit project though (instead of repeating them)

export interface editProjectArgs{
id: string
name: string
description: string
avatar: string
image: string
website: string
github: string
twitter: string
isRepresentative: boolean
categories: Array<any>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should be Any<string>

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@davekaj I think categories is an array of category objects. Basically we have a

type Category {
  id: ID!
  description: String!
}

type Project {
  id: ID!
  description: String!
  ...
  categories: [Category!]!
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we wanted an array of strings at the beginning, but then we realized that I need a Category entity and I need to pull all projects for a certain category, and all category info for a certain project. So we need to link them (many-to-many relationship)

}

export interface addProjectArgs{
id: string
name: string
description: string
avatar: string
image: string
website: string
github: string
twitter: string
isRepresentative: boolean
categories: Array<Category>
}

export interface removeProjectArgs{
projectId: string
}

interface Category {
id: string
description: string
slug: string
projects: Project[]
subcategories: Category[]
parentCategory: Category
}

interface Project {
id: string
name: string
description: string
website: string
twitter: string
github: string
avatar: string
image: string
categories: Category[]
isRepresentative: boolean
owner: User
currentChallenge: Challenge
pastChallenges: Challenge[]
membershipStartTime: number
delegates: string[]
delegateValidities: number[]
totalVotes: number
votes: Vote[]
}

interface Challenge {
id: string
description: string
endTime: number
votesFor: number
votesAgainst: number
project: Project
owner: string
votes: Vote[]
resolved: boolean
}

interface User {
id: string
name: string
bio: string
projects: Project[]
challenges: Challenge[]
votes: Project[]
}

interface Vote {
id: string
voter: Project
challenge: Challenge
choice: Choice
weight: number
}

enum Choice {
Null,
Yes,
No
}
Loading