This repository has been archived by the owner on Sep 26, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 130
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[Ethereal Hackathon] GraphQL EIP-1767 Implementation for Pantheon (#1311
) 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
Showing
104 changed files
with
6,016 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | ||
} |
64 changes: 64 additions & 0 deletions
64
...pc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetcherContext.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
182 changes: 182 additions & 0 deletions
182
...aphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetchers.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}; | ||
} | ||
} |
78 changes: 78 additions & 0 deletions
78
...m/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLProvider.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
Oops, something went wrong.