Skip to content
This repository has been archived by the owner on Sep 26, 2019. It is now read-only.

Commit

Permalink
[Ethereal Hackathon] GraphQL EIP-1767 Implementation for Pantheon (#1311
Browse files Browse the repository at this point in the history
)

Implements a GraphQL interface to expose data that conforms to EIP-1767. As the EIP specifies, the implementation should allow “a complete replacement to the read-only information exposed via the present JSON-RPC interface”.

Supported CLI options:
* `--graphql-http-enabled` to enable GraphQL
* `--graphql-http-host` and `--graphql-http-port` to configure the host and port.
* `--graphql-http-cors-origins` to set the CORS-origin policies
* The `--host-whitelist` option is respected.  This option also applies to JSON-RPC and WS-RPC endpoints.

Default port is 8547.  The endpoint is `/graphrpc`, so the default URL is typically `http://127.0.0.1:8547/graphql`
  • Loading branch information
zyfrank authored and Danno Ferrin committed May 8, 2019
1 parent 96ecec9 commit 30833fb
Show file tree
Hide file tree
Showing 104 changed files with 6,016 additions and 32 deletions.
47 changes: 47 additions & 0 deletions ethereum/graphqlrpc/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
* Copyright 2018 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/

apply plugin: 'java-library'

jar {
baseName 'pantheon-graphql-rpc'
manifest {
attributes(
'Specification-Title': baseName,
'Specification-Version': project.version,
'Implementation-Title': baseName,
'Implementation-Version': calculateVersion()
)
}
}

dependencies {
implementation project(':ethereum:blockcreation')
implementation project(':ethereum:core')
implementation project(':ethereum:eth')
implementation project(':ethereum:p2p')
implementation project(':ethereum:rlp')
implementation project(':util')

implementation 'com.graphql-java:graphql-java'
implementation 'com.google.guava:guava'
implementation 'io.vertx:vertx-core'
implementation 'io.vertx:vertx-web'

testImplementation project(path: ':ethereum:core', configuration: 'testSupportArtifacts')

testImplementation 'com.squareup.okhttp3:okhttp'
testImplementation 'junit:junit'
testImplementation 'org.assertj:assertj-core'
testImplementation 'org.mockito:mockito-core'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.ethereum.graphqlrpc;

import tech.pegasys.pantheon.ethereum.blockcreation.MiningCoordinator;
import tech.pegasys.pantheon.ethereum.chain.Blockchain;
import tech.pegasys.pantheon.ethereum.core.Synchronizer;
import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery;
import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule;
import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive;

public class GraphQLDataFetcherContext {

private final BlockchainQuery blockchain;
private final MiningCoordinator miningCoordinator;
private final Synchronizer synchronizer;
private final ProtocolSchedule<?> protocolSchedule;
private final TransactionPool transactionPool;

public GraphQLDataFetcherContext(
final Blockchain blockchain,
final WorldStateArchive worldStateArchive,
final ProtocolSchedule<?> protocolSchedule,
final TransactionPool transactionPool,
final MiningCoordinator miningCoordinator,
final Synchronizer synchronizer) {
this.blockchain = new BlockchainQuery(blockchain, worldStateArchive);
this.protocolSchedule = protocolSchedule;
this.miningCoordinator = miningCoordinator;
this.synchronizer = synchronizer;
this.transactionPool = transactionPool;
}

public TransactionPool getTransactionPool() {
return transactionPool;
}

public BlockchainQuery getBlockchainQuery() {
return blockchain;
}

public MiningCoordinator getMiningCoordinator() {
return miningCoordinator;
}

public Synchronizer getSynchronizer() {
return synchronizer;
}

public ProtocolSchedule<?> getProtocolSchedule() {
return protocolSchedule;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.ethereum.graphqlrpc;

import tech.pegasys.pantheon.ethereum.blockcreation.MiningCoordinator;
import tech.pegasys.pantheon.ethereum.core.Address;
import tech.pegasys.pantheon.ethereum.core.Hash;
import tech.pegasys.pantheon.ethereum.core.SyncStatus;
import tech.pegasys.pantheon.ethereum.core.Synchronizer;
import tech.pegasys.pantheon.ethereum.core.Transaction;
import tech.pegasys.pantheon.ethereum.core.WorldState;
import tech.pegasys.pantheon.ethereum.eth.EthProtocol;
import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockWithMetadata;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.TransactionWithMetadata;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.pojoadapter.AccountAdapter;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.pojoadapter.NormalBlockAdapter;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.pojoadapter.SyncStateAdapter;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.pojoadapter.TransactionAdapter;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response.GraphQLRpcError;
import tech.pegasys.pantheon.ethereum.mainnet.TransactionValidator.TransactionInvalidReason;
import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult;
import tech.pegasys.pantheon.ethereum.p2p.wire.Capability;
import tech.pegasys.pantheon.ethereum.rlp.RLP;
import tech.pegasys.pantheon.ethereum.rlp.RLPException;
import tech.pegasys.pantheon.util.bytes.Bytes32;
import tech.pegasys.pantheon.util.bytes.BytesValue;
import tech.pegasys.pantheon.util.uint.UInt256;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;

import graphql.schema.DataFetcher;

public class GraphQLDataFetchers {
public GraphQLDataFetchers(final Set<Capability> supportedCapabilities) {
final OptionalInt version =
supportedCapabilities.stream()
.filter(cap -> EthProtocol.NAME.equals(cap.getName()))
.mapToInt(Capability::getVersion)
.max();
highestEthVersion = version.isPresent() ? version.getAsInt() : null;
}

private final Integer highestEthVersion;

DataFetcher<Optional<Integer>> getProtocolVersionDataFetcher() {
return dataFetchingEnvironment -> Optional.of(highestEthVersion);
}

DataFetcher<Optional<Bytes32>> getSendRawTransactionDataFetcher() {
return dataFetchingEnvironment -> {
try {
final TransactionPool transactionPool =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getTransactionPool();
final BytesValue rawTran = dataFetchingEnvironment.getArgument("data");

final Transaction transaction = Transaction.readFrom(RLP.input(rawTran));
final ValidationResult<TransactionInvalidReason> validationResult =
transactionPool.addLocalTransaction(transaction);
if (validationResult.isValid()) {
return Optional.of(transaction.hash());
}
} catch (final IllegalArgumentException | RLPException e) {
throw new GraphQLRpcException(GraphQLRpcError.INVALID_PARAMS);
}
throw new GraphQLRpcException(GraphQLRpcError.INVALID_PARAMS);
};
}

DataFetcher<Optional<SyncStateAdapter>> getSyncingDataFetcher() {
return dataFetchingEnvironment -> {
final Synchronizer synchronizer =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getSynchronizer();
final Optional<SyncStatus> syncStatus = synchronizer.getSyncStatus();
return syncStatus.map(SyncStateAdapter::new);
};
}

DataFetcher<Optional<UInt256>> getGasPriceDataFetcher() {
return dataFetchingEnvironment -> {
final MiningCoordinator miningCoordinator =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getMiningCoordinator();

return Optional.of(miningCoordinator.getMinTransactionGasPrice().asUInt256());
};
}

DataFetcher<List<NormalBlockAdapter>> getRangeBlockDataFetcher() {

return dataFetchingEnvironment -> {
final long from = dataFetchingEnvironment.getArgument("from");
final long to = dataFetchingEnvironment.getArgument("to");
if (from > to) {
throw new GraphQLRpcException(GraphQLRpcError.INVALID_PARAMS);
}

final BlockchainQuery blockchain =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getBlockchainQuery();

final List<NormalBlockAdapter> results = new ArrayList<>();
for (long i = from; i <= to; i++) {
final Optional<BlockWithMetadata<TransactionWithMetadata, Hash>> block =
blockchain.blockByNumber(i);
block.ifPresent(e -> results.add(new NormalBlockAdapter(e)));
}
return results;
};
}

public DataFetcher<Optional<NormalBlockAdapter>> getBlockDataFetcher() {

return dataFetchingEnvironment -> {
final BlockchainQuery blockchain =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getBlockchainQuery();
final Long number = dataFetchingEnvironment.getArgument("number");
final Bytes32 hash = dataFetchingEnvironment.getArgument("hash");
if ((number != null) && (hash != null)) {
throw new GraphQLRpcException(GraphQLRpcError.INVALID_PARAMS);
}

final Optional<BlockWithMetadata<TransactionWithMetadata, Hash>> block;
if (number != null) {
block = blockchain.blockByNumber(number);
} else if (hash != null) {
block = blockchain.blockByHash(Hash.wrap(hash));
} else {
block = blockchain.latestBlock();
}
return block.map(NormalBlockAdapter::new);
};
}

DataFetcher<Optional<AccountAdapter>> getAccountDataFetcher() {
return dataFetchingEnvironment -> {
final BlockchainQuery blockchainQuery =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getBlockchainQuery();
final Address addr = dataFetchingEnvironment.getArgument("address");
final Long bn = dataFetchingEnvironment.getArgument("blockNumber");
if (bn != null) {
final Optional<WorldState> ws = blockchainQuery.getWorldState(bn);
if (ws.isPresent()) {
return Optional.of(new AccountAdapter(ws.get().get(addr)));
} else if (bn > blockchainQuery.getBlockchain().getChainHeadBlockNumber()) {
// block is past chainhead
throw new GraphQLRpcException(GraphQLRpcError.INVALID_PARAMS);
} else {
// we don't have that block
throw new GraphQLRpcException(GraphQLRpcError.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE);
}
}
// return account on latest block
final long latestBn = blockchainQuery.latestBlock().get().getHeader().getNumber();
final Optional<WorldState> ows = blockchainQuery.getWorldState(latestBn);
return ows.flatMap(ws -> Optional.ofNullable(ws.get(addr))).map(AccountAdapter::new);
};
}

DataFetcher<Optional<TransactionAdapter>> getTransactionDataFetcher() {
return dataFetchingEnvironment -> {
final BlockchainQuery blockchain =
((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getBlockchainQuery();
final Bytes32 hash = dataFetchingEnvironment.getArgument("hash");
final Optional<TransactionWithMetadata> tran = blockchain.transactionByHash(Hash.wrap(hash));
return tran.map(TransactionAdapter::new);
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2019 ConsenSys AG.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package tech.pegasys.pantheon.ethereum.graphqlrpc;

import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring;

import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.scalar.AddressScalar;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.scalar.BigIntScalar;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.scalar.Bytes32Scalar;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.scalar.BytesScalar;
import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.scalar.LongScalar;

import java.io.IOException;
import java.net.URL;

import com.google.common.base.Charsets;
import com.google.common.io.Resources;
import graphql.GraphQL;
import graphql.schema.GraphQLSchema;
import graphql.schema.idl.RuntimeWiring;
import graphql.schema.idl.SchemaGenerator;
import graphql.schema.idl.SchemaParser;
import graphql.schema.idl.TypeDefinitionRegistry;

public class GraphQLProvider {

private GraphQLProvider() {}

public static GraphQL buildGraphQL(final GraphQLDataFetchers graphQLDataFetchers)
throws IOException {
final URL url = Resources.getResource("schema.graphqls");
final String sdl = Resources.toString(url, Charsets.UTF_8);
final GraphQLSchema graphQLSchema = buildSchema(sdl, graphQLDataFetchers);
return GraphQL.newGraphQL(graphQLSchema).build();
}

private static GraphQLSchema buildSchema(
final String sdl, final GraphQLDataFetchers graphQLDataFetchers) {
final TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(sdl);
final RuntimeWiring runtimeWiring = buildWiring(graphQLDataFetchers);
final SchemaGenerator schemaGenerator = new SchemaGenerator();
return schemaGenerator.makeExecutableSchema(typeRegistry, runtimeWiring);
}

private static RuntimeWiring buildWiring(final GraphQLDataFetchers graphQLDataFetchers) {
return RuntimeWiring.newRuntimeWiring()
.scalar(new AddressScalar())
.scalar(new Bytes32Scalar())
.scalar(new BytesScalar())
.scalar(new LongScalar())
.scalar(new BigIntScalar())
.type(
newTypeWiring("Query")
.dataFetcher("account", graphQLDataFetchers.getAccountDataFetcher())
.dataFetcher("block", graphQLDataFetchers.getBlockDataFetcher())
.dataFetcher("blocks", graphQLDataFetchers.getRangeBlockDataFetcher())
.dataFetcher("transaction", graphQLDataFetchers.getTransactionDataFetcher())
.dataFetcher("gasPrice", graphQLDataFetchers.getGasPriceDataFetcher())
.dataFetcher("syncing", graphQLDataFetchers.getSyncingDataFetcher())
.dataFetcher(
"protocolVersion", graphQLDataFetchers.getProtocolVersionDataFetcher()))
.type(
newTypeWiring("Mutation")
.dataFetcher(
"sendRawTransaction", graphQLDataFetchers.getSendRawTransactionDataFetcher()))
.build();
}
}
Loading

0 comments on commit 30833fb

Please sign in to comment.