-
Notifications
You must be signed in to change notification settings - Fork 45
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
Mutations #20
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
node_modules | ||
package | ||
dist | ||
bundle |
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 |
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" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
# TODO: Schema |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
//TODO: tests |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
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" | ||
|
||
// State Payloads + Events + StateBuilder | ||
// TODO: Which custom events/reducers will we use? Will we use them at all? | ||
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) { | ||
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) | ||
} | ||
} | ||
|
||
// TODO: Best way to get ABIs? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good question. @davekaj do you have any ideas? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
or when lerna is activated
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, Ill make it that way, just like in the curation-starter |
||
async function getContract(context: Context) { } | ||
|
||
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... | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
||
//TODO: are we sending metadata hash through state.dispatch? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe @nenadjaja knows There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @namesty we don't need that hash for anything else except for the contract call that you are making inside of that resolver. So I don't know if we need to dispatch it... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, great then :) |
||
|
||
// 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
//TODO: Args are any by default in graphQL, do we really need these interfaces? | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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<any> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. should be |
||
} | ||
|
||
export interface removeProjectArgs{ | ||
projectId: string | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
import gql from 'graphql-tag' | ||
|
||
export const uploadToIpfs = async (ipfs: any, data: any): Promise<string> => { | ||
let result | ||
|
||
for await (const returnedValue of ipfs.add(data)) { | ||
result = returnedValue | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What if this errors out? How do we want to handle the error? 🤔 |
||
return result.path | ||
} | ||
|
||
export const sleep = (ms: number): Promise<void> => { | ||
return new Promise(resolve => setTimeout(resolve, ms)); | ||
} | ||
|
||
export const PROJECT_QUERY = gql` | ||
query everestProject($id: ID!) { | ||
project(where: { id: $id }) { | ||
id | ||
name | ||
description | ||
categories | ||
createdAt | ||
reputation | ||
isChallenged | ||
website | ||
github | ||
image | ||
avatar | ||
totalVotes | ||
owner { | ||
id | ||
name | ||
} | ||
} | ||
} | ||
` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
{ | ||
"compilerOptions": { | ||
"target": "es2015", | ||
"moduleResolution": "node", | ||
"declaration": true, | ||
"allowSyntheticDefaultImports": true, | ||
"esModuleInterop": true | ||
}, | ||
"include": [ | ||
"src/**/*" | ||
], | ||
"exclude": [ | ||
"node_modules/**/*" | ||
] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
const path = require("path"); | ||
|
||
module.exports = { | ||
entry: { | ||
app: "./src/index.ts" | ||
}, | ||
output: { | ||
filename: "index.js", | ||
path: path.resolve(__dirname, "bundle"), | ||
globalObject: "this", | ||
libraryTarget: "commonjs" | ||
}, | ||
target: "node", | ||
mode: "development", | ||
module: { | ||
rules: [ | ||
// note that babel-loader is configured to run after ts-loader | ||
{ | ||
test: /\.(ts)$/, | ||
include: path.resolve(__dirname, "./src"), | ||
use: [ | ||
{ | ||
loader: "babel-loader", | ||
options: { | ||
presets: [ | ||
["@babel/preset-env", { forceAllTransforms: true }] | ||
], | ||
plugins: ["add-module-exports"] | ||
} | ||
}, | ||
{ | ||
loader: "ts-loader", | ||
options: { | ||
compilerOptions: { | ||
outDir: path.resolve(__dirname, "bundle") | ||
} | ||
} | ||
} | ||
] | ||
} | ||
] | ||
} | ||
}; |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ?
There was a problem hiding this comment.
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
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.