Skip to content

Commit

Permalink
Implement GraphQL API (#858)
Browse files Browse the repository at this point in the history
* Create basic GraphQL server; implement stats query

* Implement order query

* Implement orders query

* Remove JSON-RPC API

* Fix CI

* Change types for some stats fields

* Remove references to JSON-RPC API from cut-release script

* Add GraphQL configuration options

* Update version of GraphiQL

* Use server.Shutdown for GraphQL server

* Use go-bindata to embed schema from text file

* Add graphql config file

* Install go-bindata in deps-no-lockfile

* Stub out remaining types and resolvers

* Switch to using gqlgen

* Set up new gqlgen server

* Re-implement resolvers

* Implement AddOrders

* Implement OrderEvents

* Implement AddOrders in client

* Implement and test Go client GetOrders without options

* Add test for more complicated GetOrders call

* Remove WIP TypeScript client code

* Implement and test Go client GetOrder

* Implement and test Go client GetStats

* Clean up GraphQL integration tests

* Implement basic working client with graphql-codegen

* Switch to using apollo client

* WIP subscriptions support

* Implement and test TypeScript client addOrdersAsync

* Implement and test TypeScript client getOrderAsync

* Implement and test TypeScript client onOrderEvents

* Remove ad hoc subscription test code

* Fix linter errors

* Implement and test TypeScript client getOrdersAsync

* Fix build error

* Remove old code related to typescript tests

* Implement and test TypeScript client rawQueryAsync

* Change env vars used for GraphQL integration tests

* Basic cleanup; remove old code and deps

* Cleanup types and file structure for TypeScript client

* Re-enable browser integration tests

* Add missing TSDoc comments

* Tweak environment variables.

* Remove old Go examples

* Add comment about mesh-bridge

* Update Dockerfiles

* Update documentation and comments

* Remove lingering references to coordinator error codes

* Improve error handling for TypeScript client subscriptions

* Address review feedback

* Fix failing TypeScript GraphQL client tests

* Update links in db_syncing.md

* Address PR feedback

* Throw all GraphQL errors in TypeScript client
  • Loading branch information
albrow committed Aug 11, 2020
1 parent c7fb4d7 commit 6f058bf
Show file tree
Hide file tree
Showing 78 changed files with 13,331 additions and 11,895 deletions.
2 changes: 1 addition & 1 deletion .gitattributes
@@ -1,4 +1,4 @@
# Automatically collapse generated files in GitHub.
ethereum/wrappers/*.go linguist-generated=true
docs/json-rpc-clients/typescript/*.md linguist-generated=true
docs/graphql-clients/typescript/*.md linguist-generated=true

3 changes: 3 additions & 0 deletions .graphqlconfig
@@ -0,0 +1,3 @@
{
"schemaPath": "graphql/schema.graphql"
}
30 changes: 22 additions & 8 deletions Makefile
@@ -1,5 +1,5 @@
.PHONY: deps
deps: deps-ts wasmbrowsertest
deps: deps-ts wasmbrowsertest gqlgen


.PHONY: deps-ts
Expand All @@ -13,6 +13,13 @@ gobin:
GO111MODULE=off go get -u github.com/myitcv/gobin


# gqlgen is a tool for embedding files so that they are included in binaries.
# This installs the CLI for go-bindata.
.PHONY: gqlgen
gqlgen: gobin
gobin github.com/99designs/gqlgen@v0.11.3


# wasmbrowsertest is required for running WebAssembly tests in the browser.
.PHONY: wasmbrowsertest
wasmbrowsertest: gobin
Expand All @@ -21,7 +28,7 @@ wasmbrowsertest: gobin

# Installs dependencies without updating Gopkg.lock or yarn.lock
.PHONY: deps-no-lockfile
deps-no-lockfile: deps-ts-no-lockfile wasmbrowsertest
deps-no-lockfile: deps-ts-no-lockfile wasmbrowsertest gqlgen


.PHONY: deps-ts-no-lockfile
Expand All @@ -34,7 +41,7 @@ test-all: test-go test-wasm-browser test-ts test-browser-conversion test-browser


.PHONY: test-go
test-go: test-go-parallel test-go-serial
test-go: generate test-go-parallel test-go-serial


.PHONY: test-go-parallel
Expand Down Expand Up @@ -84,8 +91,14 @@ lint-ts:
lint-prettier:
yarn prettier:ci


.PHONY: generate
generate:
go generate ./...


.PHONY: mesh
mesh:
mesh: generate
go install ./cmd/mesh


Expand All @@ -105,7 +118,7 @@ db-integrity-check:


.PHONY: cut-release
cut-release:
cut-release: generate
go run ./cmd/cut-release/main.go


Expand All @@ -117,7 +130,7 @@ all: mesh mesh-keygen mesh-bootstrap db-integrity-check


.PHONY: docker-mesh
docker-mesh:
docker-mesh: generate
docker build . -t 0xorg/mesh -f ./dockerfiles/mesh/Dockerfile


Expand All @@ -132,5 +145,6 @@ docker-mesh-fluent-bit:


.PHONY: docker-mesh-bridge
docker-mesh-bridge:
docker build . -t 0xorg/mesh-bridge -f ./dockerfiles/mesh-bridge/Dockerfile
docker-mesh-bridge: generate
@echo 'WARN: mesh-bridge is currently disabled since it has not been updated to use the new GraphQL API'
# docker build . -t 0xorg/mesh-bridge -f ./dockerfiles/mesh-bridge/Dockerfile
12 changes: 6 additions & 6 deletions README.md
Expand Up @@ -39,12 +39,12 @@ in the network and begin receiving orders from and sending orders to them. You
do not need to know the identities (e.g., IP address or domain name) of any
peers in the network ahead of time and they do not need to know about you.

Developers can use the JSON-RPC API to interact with a Mesh node that they
control. The API allows you to send orders into the network, receive any new
orders, and get notified when the status of an existing order changes (e.g. when
it is filled, canceled, or expired). Under the hood, Mesh performs efficient
order validation and order book pruning, which takes out a lot of the hard work
for developers.
Developers can use the GraphQL API to interact with a Mesh node that they
control. The API allows you to send orders into the network, query for existing
orders, and get notified when an order is added or the status of an existing
order changes (e.g. when it is filled, canceled, or expired). Under the hood,
Mesh performs efficient order validation and order book pruning, which takes out
a lot of the hard work for developers.

## Documentation

Expand Down
6 changes: 3 additions & 3 deletions cmd/cut-release/main.go
Expand Up @@ -98,8 +98,8 @@ func updateHardCodedVersions(version string) {
newBrowserLiteDependencyString := fmt.Sprintf(`"@0x/mesh-browser-lite": "^%s"`, version)
newBrowserDependencyString := fmt.Sprintf(`"@0x/mesh-browser": "^%s"`, version)

// Update `packages/mesh-rpc-client/package.json`
tsClientPackageJSONPath := "packages/mesh-rpc-client/package.json"
// Update `packages/mesh-graphql-client/package.json`
tsClientPackageJSONPath := "packages/mesh-graphql-client/package.json"
regex := `"version": "(.*)"`
updateFileWithRegex(tsClientPackageJSONPath, regex, newVersionString)

Expand Down Expand Up @@ -156,7 +156,7 @@ func updateHardCodedVersions(version string) {
updateFileWithRegex(changelog, regex, newChangelogSection)

// Update badge in README.md
pathToMDFilesWithBadges := []string{"README.md", "docs/rpc_api.md", "docs/deployment.md", "docs/deployment_with_telemetry.md"}
pathToMDFilesWithBadges := []string{"README.md", "docs/graphql_api.md", "docs/deployment.md", "docs/deployment_with_telemetry.md"}
doubleDashVersion := strings.Replace(version, "-", "--", -1)
newSvgName := fmt.Sprintf("version-%s-orange.svg", doubleDashVersion)
regex = `version-(.*)-orange.svg`
Expand Down
213 changes: 108 additions & 105 deletions cmd/mesh-bridge/main.go
Expand Up @@ -5,120 +5,123 @@
//to flow to another
package main

import (
"context"
"time"
// Note(albrow): If we want to use mesh-bridge in the future we will need to update it to
// use the new GraphQL API. Considered low-priority as of the time of writing this comment.

"github.com/0xProject/0x-mesh/rpc"
"github.com/0xProject/0x-mesh/zeroex"
ethrpc "github.com/ethereum/go-ethereum/rpc"
"github.com/plaid/go-envvar/envvar"
log "github.com/sirupsen/logrus"
)
// import (
// "context"
// "time"

const (
firstWSRPCAddressLabel = "FirstWSRPCAddress"
secondWSRPCAddressLabel = "SecondWSRPCAddress"
maxReceiveBatch = 100
receiveTimeout = 2 * time.Second
tenThousand = 10000
)
// "github.com/0xProject/0x-mesh/rpc"
// "github.com/0xProject/0x-mesh/zeroex"
// ethrpc "github.com/ethereum/go-ethereum/rpc"
// "github.com/plaid/go-envvar/envvar"
// log "github.com/sirupsen/logrus"
// )

type clientEnvVars struct {
FirstWSRPCAddress string `envvar:"FIRST_WS_RPC_ADDRESS"`
SecondWSRPCAddress string `envvar:"SECOND_WS_RPC_ADDRESS"`
Verbosity int `envvar:"VERBOSITY"`
}
// const (
// firstWSRPCAddressLabel = "FirstWSRPCAddress"
// secondWSRPCAddressLabel = "SecondWSRPCAddress"
// maxReceiveBatch = 100
// receiveTimeout = 2 * time.Second
// tenThousand = 10000
// )

// type clientEnvVars struct {
// FirstWSRPCAddress string `envvar:"FIRST_WS_RPC_ADDRESS"`
// SecondWSRPCAddress string `envvar:"SECOND_WS_RPC_ADDRESS"`
// Verbosity int `envvar:"VERBOSITY"`
// }

func main() {
env := clientEnvVars{}
if err := envvar.Parse(&env); err != nil {
panic(err)
}
// env := clientEnvVars{}
// if err := envvar.Parse(&env); err != nil {
// panic(err)
// }

log.SetLevel(log.Level(env.Verbosity))
// log.SetLevel(log.Level(env.Verbosity))

firstClient, err := rpc.NewClient(env.FirstWSRPCAddress)
if err != nil {
log.WithError(err).Fatal("could not create client")
}
stats, err := firstClient.GetStats()
if err != nil {
log.Fatal(err)
}
log.WithField("stats", stats).Info("Spun up first client")
// firstClient, err := rpc.NewClient(env.FirstWSRPCAddress)
// if err != nil {
// log.WithError(err).Fatal("could not create client")
// }
// stats, err := firstClient.GetStats()
// if err != nil {
// log.Fatal(err)
// }
// log.WithField("stats", stats).Info("Spun up first client")

secondClient, err := rpc.NewClient(env.SecondWSRPCAddress)
if err != nil {
log.WithError(err).Fatal("could not create client")
}
stats, err = secondClient.GetStats()
if err != nil {
log.Fatal(err)
}
log.WithField("stats", stats).Info("Spun up second client")
// secondClient, err := rpc.NewClient(env.SecondWSRPCAddress)
// if err != nil {
// log.WithError(err).Fatal("could not create client")
// }
// stats, err = secondClient.GetStats()
// if err != nil {
// log.Fatal(err)
// }
// log.WithField("stats", stats).Info("Spun up second client")

go func() {
pipeOrders(secondClient, firstClient, secondWSRPCAddressLabel, firstWSRPCAddressLabel)
}()
pipeOrders(firstClient, secondClient, firstWSRPCAddressLabel, secondWSRPCAddressLabel)
// go func() {
// pipeOrders(secondClient, firstClient, secondWSRPCAddressLabel, firstWSRPCAddressLabel)
// }()
// pipeOrders(firstClient, secondClient, firstWSRPCAddressLabel, secondWSRPCAddressLabel)
}

func pipeOrders(inClient, outClient *rpc.Client, inLabel, outLabel string) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
orderEventsChan := make(chan []*zeroex.OrderEvent, tenThousand)
clientSubscription, err := inClient.SubscribeToOrders(ctx, orderEventsChan)
if err != nil {
log.WithError(err).Fatal("Couldn't set up OrderStream subscription")
}
defer clientSubscription.Unsubscribe()
for {
incomingSignedOrders, err := receiveBatch(orderEventsChan, clientSubscription, inLabel, outLabel)
if err != nil {
log.Fatal(err)
}
if len(incomingSignedOrders) == 0 {
continue
}
validationResults, err := outClient.AddOrders(incomingSignedOrders)
if err != nil {
log.Fatal(err)
}
log.WithFields(log.Fields{
"from": inLabel,
"to": outLabel,
"numSent": len(incomingSignedOrders),
"numAccepted": len(validationResults.Accepted),
"numRejected": len(validationResults.Rejected),
}).Info("Finished bridging orders")
}
}
// func pipeOrders(inClient, outClient *rpc.Client, inLabel, outLabel string) {
// ctx, cancel := context.WithCancel(context.Background())
// defer cancel()
// orderEventsChan := make(chan []*zeroex.OrderEvent, tenThousand)
// clientSubscription, err := inClient.SubscribeToOrders(ctx, orderEventsChan)
// if err != nil {
// log.WithError(err).Fatal("Couldn't set up OrderStream subscription")
// }
// defer clientSubscription.Unsubscribe()
// for {
// incomingSignedOrders, err := receiveBatch(orderEventsChan, clientSubscription, inLabel, outLabel)
// if err != nil {
// log.Fatal(err)
// }
// if len(incomingSignedOrders) == 0 {
// continue
// }
// validationResults, err := outClient.AddOrders(incomingSignedOrders)
// if err != nil {
// log.Fatal(err)
// }
// log.WithFields(log.Fields{
// "from": inLabel,
// "to": outLabel,
// "numSent": len(incomingSignedOrders),
// "numAccepted": len(validationResults.Accepted),
// "numRejected": len(validationResults.Rejected),
// }).Info("Finished bridging orders")
// }
// }

func receiveBatch(inChan chan []*zeroex.OrderEvent, subscription *ethrpc.ClientSubscription, inLabel, outLabel string) ([]*zeroex.SignedOrder, error) {
signedOrders := []*zeroex.SignedOrder{}
timeoutChan := time.After(receiveTimeout)
for {
if len(signedOrders) >= maxReceiveBatch {
return signedOrders, nil
}
select {
case <-timeoutChan:
return signedOrders, nil
case orderEvents := <-inChan:
for _, orderEvent := range orderEvents {
if orderEvent.EndState != zeroex.ESOrderAdded {
continue
}
log.WithFields(log.Fields{
"from": inLabel,
"to": outLabel,
"orderHash": orderEvent.OrderHash.Hex(),
}).Info("Found new order over bridge")
signedOrders = append(signedOrders, orderEvent.SignedOrder)
}
case err := <-subscription.Err():
log.Fatal(err)
}
}
}
// func receiveBatch(inChan chan []*zeroex.OrderEvent, subscription *ethrpc.ClientSubscription, inLabel, outLabel string) ([]*zeroex.SignedOrder, error) {
// signedOrders := []*zeroex.SignedOrder{}
// timeoutChan := time.After(receiveTimeout)
// for {
// if len(signedOrders) >= maxReceiveBatch {
// return signedOrders, nil
// }
// select {
// case <-timeoutChan:
// return signedOrders, nil
// case orderEvents := <-inChan:
// for _, orderEvent := range orderEvents {
// if orderEvent.EndState != zeroex.ESOrderAdded {
// continue
// }
// log.WithFields(log.Fields{
// "from": inLabel,
// "to": outLabel,
// "orderHash": orderEvent.OrderHash.Hex(),
// }).Info("Found new order over bridge")
// signedOrders = append(signedOrders, orderEvent.SignedOrder)
// }
// case err := <-subscription.Err():
// log.Fatal(err)
// }
// }
// }

0 comments on commit 6f058bf

Please sign in to comment.