From 30833fb21ce2cc773a2cf7d57840cc878cf745ed Mon Sep 17 00:00:00 2001 From: zyfrank Date: Wed, 8 May 2019 22:34:54 +0800 Subject: [PATCH] [Ethereal Hackathon] GraphQL EIP-1767 Implementation for Pantheon (#1311) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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` --- ethereum/graphqlrpc/build.gradle | 47 +++ .../graphqlrpc/GraphQLDataFetcherContext.java | 64 +++ .../graphqlrpc/GraphQLDataFetchers.java | 182 ++++++++ .../ethereum/graphqlrpc/GraphQLProvider.java | 78 ++++ .../graphqlrpc/GraphQLRpcConfiguration.java | 120 ++++++ .../graphqlrpc/GraphQLRpcException.java | 60 +++ .../graphqlrpc/GraphQLRpcHttpService.java | 387 ++++++++++++++++++ .../GraphQLRpcServiceException.java | 20 + .../internal/BlockWithMetadata.java | 67 +++ .../graphqlrpc/internal/BlockchainQuery.java | 283 +++++++++++++ .../graphqlrpc/internal/LogWithMetadata.java | 109 +++++ .../graphqlrpc/internal/LogsQuery.java | 112 +++++ .../graphqlrpc/internal/TopicsParameter.java | 52 +++ .../TransactionReceiptWithMetadata.java | 74 ++++ .../internal/TransactionWithMetadata.java | 51 +++ .../internal/pojoadapter/AccountAdapter.java | 53 +++ .../internal/pojoadapter/AdapterBase.java | 24 ++ .../pojoadapter/BlockAdapterBase.java | 228 +++++++++++ .../internal/pojoadapter/CallResult.java | 40 ++ .../internal/pojoadapter/LogAdapter.java | 73 ++++ .../pojoadapter/NormalBlockAdapter.java | 95 +++++ .../pojoadapter/SyncStateAdapter.java | 48 +++ .../pojoadapter/TransactionAdapter.java | 185 +++++++++ .../pojoadapter/UncleBlockAdapter.java | 56 +++ .../internal/response/GraphQLJsonRequest.java | 54 +++ .../internal/response/GraphQLRpcError.java | 43 ++ .../response/GraphQLRpcErrorResponse.java | 28 ++ .../response/GraphQLRpcNoResponse.java | 25 ++ .../internal/response/GraphQLRpcResponse.java | 46 +++ .../response/GraphQLRpcResponseType.java | 21 + .../response/GraphQLRpcSuccessResponse.java | 28 ++ .../internal/scalar/AddressScalar.java | 63 +++ .../internal/scalar/BigIntScalar.java | 66 +++ .../internal/scalar/Bytes32Scalar.java | 63 +++ .../internal/scalar/BytesScalar.java | 63 +++ .../internal/scalar/LongScalar.java | 85 ++++ .../src/main/resources/schema.graphqls | 303 ++++++++++++++ .../graphqlrpc/AbstractDataFetcherTest.java | 53 +++ .../AbstractEthGraphQLRpcHttpServiceTest.java | 192 +++++++++ .../graphqlrpc/BlockDataFetcherTest.java | 49 +++ .../EthGraphQLRpcHttpBySpecErrorCaseTest.java | 91 ++++ .../EthGraphQLRpcHttpBySpecTest.java | 123 ++++++ .../GraphQLRpcConfigurationTest.java | 29 ++ .../GraphQLRpcHttpServiceCorsTest.java | 230 +++++++++++ ...raphQLRpcHttpServiceHostWhitelistTest.java | 156 +++++++ .../graphqlrpc/GraphQLRpcHttpServiceTest.java | 318 ++++++++++++++ .../graphqlrpc/GraphQLRpcTestHelper.java | 36 ++ .../internal/scalar/AddressScalarTest.java | 91 ++++ .../internal/scalar/BigIntScalarTest.java | 87 ++++ .../internal/scalar/Bytes32ScalarTest.java | 87 ++++ .../internal/scalar/BytesScalarTest.java | 87 ++++ .../internal/scalar/LongScalarTest.java | 93 +++++ .../ethereum/graphqlrpc/eth_blockNumber.json | 13 + .../ethereum/graphqlrpc/eth_call_Block8.json | 16 + .../graphqlrpc/eth_call_BlockLatest.json | 16 + .../eth_estimateGas_contractDeploy.json | 12 + .../graphqlrpc/eth_estimateGas_noParams.json | 12 + .../graphqlrpc/eth_estimateGas_transfer.json | 11 + .../ethereum/graphqlrpc/eth_gasPrice.json | 11 + .../graphqlrpc/eth_getBalance_0x19.json | 12 + .../graphqlrpc/eth_getBalance_latest.json | 12 + .../graphqlrpc/eth_getBalance_toobig_bn.json | 20 + .../eth_getBalance_without_addr.json | 5 + .../graphqlrpc/eth_getBlockByHash.json | 34 ++ .../graphqlrpc/eth_getBlockByNumber.json | 44 ++ .../eth_getBlockTransactionCountByHash.json | 15 + .../eth_getBlockTransactionCountByNumber.json | 33 ++ .../graphqlrpc/eth_getBlockWrongParams.json | 7 + .../graphqlrpc/eth_getBlocksByRange.json | 40 ++ .../graphqlrpc/eth_getBlocksByWrongRange.json | 15 + .../ethereum/graphqlrpc/eth_getCode.json | 13 + .../graphqlrpc/eth_getCode_noCode.json | 14 + .../graphqlrpc/eth_getLogs_matchTopic.json | 22 + .../ethereum/graphqlrpc/eth_getStorageAt.json | 13 + ..._getStorageAt_illegalRangeGreaterThan.json | 13 + ...eth_getTransactionByBlockHashAndIndex.json | 20 + ...h_getTransactionByBlockNumberAndIndex.json | 20 + ...ansactionByBlockNumberAndInvalidIndex.json | 14 + .../graphqlrpc/eth_getTransactionByHash.json | 32 ++ .../eth_getTransactionByHashNull.json | 13 + .../graphqlrpc/eth_getTransactionCount.json | 13 + .../graphqlrpc/eth_getTransactionReceipt.json | 27 ++ ...h_sendRawTransaction_contractCreation.json | 9 + .../eth_sendRawTransaction_messageCall.json | 10 + .../eth_sendRawTransaction_transferEther.json | 9 + ...endRawTransaction_unsignedTransaction.json | 19 + .../ethereum/graphqlrpc/eth_syncing.json | 17 + .../graphQLRpcTestBlockchain.blocks | Bin 0 -> 23287 bytes .../graphqlrpc/graphQLRpcTestGenesis.json | 20 + ethereum/jsonrpc/build.gradle | 2 +- .../ethereum/jsonrpc/JsonRpcHttpService.java | 0 .../jsonrpc/EthJsonRpcHttpBySpecTest.java | 1 - gradle/check-licenses.gradle | 12 +- gradle/versions.gradle | 2 + pantheon/build.gradle | 2 + .../java/tech/pegasys/pantheon/Runner.java | 13 + .../tech/pegasys/pantheon/RunnerBuilder.java | 43 +- .../pegasys/pantheon/cli/PantheonCommand.java | 103 ++++- .../tech/pegasys/pantheon/RunnerTest.java | 22 +- .../pantheon/cli/CommandTestAbstract.java | 3 + .../pantheon/cli/PantheonCommandTest.java | 92 +++++ .../src/test/resources/complete_config.toml | 2 + .../src/test/resources/everything_config.toml | 6 + settings.gradle | 1 + 104 files changed, 6016 insertions(+), 32 deletions(-) create mode 100644 ethereum/graphqlrpc/build.gradle create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetcherContext.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetchers.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLProvider.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfiguration.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcException.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpService.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcServiceException.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockWithMetadata.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockchainQuery.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogWithMetadata.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogsQuery.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TopicsParameter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionReceiptWithMetadata.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionWithMetadata.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AccountAdapter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AdapterBase.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/BlockAdapterBase.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/CallResult.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/LogAdapter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/NormalBlockAdapter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/SyncStateAdapter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/TransactionAdapter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/UncleBlockAdapter.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLJsonRequest.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcError.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcErrorResponse.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcNoResponse.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponse.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponseType.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcSuccessResponse.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalar.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalar.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32Scalar.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalar.java create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalar.java create mode 100644 ethereum/graphqlrpc/src/main/resources/schema.graphqls create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractDataFetcherTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractEthGraphQLRpcHttpServiceTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/BlockDataFetcherTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecErrorCaseTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfigurationTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceCorsTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceHostWhitelistTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcTestHelper.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalarTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalarTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32ScalarTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalarTest.java create mode 100644 ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalarTest.java create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_blockNumber.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_Block8.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_BlockLatest.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_contractDeploy.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_noParams.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_transfer.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_gasPrice.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_0x19.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_latest.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_toobig_bn.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_without_addr.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByHash.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByNumber.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByHash.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByNumber.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockWrongParams.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByRange.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByWrongRange.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode_noCode.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getLogs_matchTopic.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt_illegalRangeGreaterThan.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockHashAndIndex.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndIndex.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndInvalidIndex.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHash.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHashNull.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionCount.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionReceipt.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_contractCreation.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_messageCall.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_transferEther.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_unsignedTransaction.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_syncing.json create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestBlockchain.blocks create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestGenesis.json mode change 100644 => 100755 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcHttpService.java diff --git a/ethereum/graphqlrpc/build.gradle b/ethereum/graphqlrpc/build.gradle new file mode 100644 index 0000000000..8ef1c88018 --- /dev/null +++ b/ethereum/graphqlrpc/build.gradle @@ -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' +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetcherContext.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetcherContext.java new file mode 100644 index 0000000000..e6ca6e1f81 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetcherContext.java @@ -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; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetchers.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetchers.java new file mode 100644 index 0000000000..15eddac265 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLDataFetchers.java @@ -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 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> getProtocolVersionDataFetcher() { + return dataFetchingEnvironment -> Optional.of(highestEthVersion); + } + + DataFetcher> 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 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> getSyncingDataFetcher() { + return dataFetchingEnvironment -> { + final Synchronizer synchronizer = + ((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getSynchronizer(); + final Optional syncStatus = synchronizer.getSyncStatus(); + return syncStatus.map(SyncStateAdapter::new); + }; + } + + DataFetcher> getGasPriceDataFetcher() { + return dataFetchingEnvironment -> { + final MiningCoordinator miningCoordinator = + ((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getMiningCoordinator(); + + return Optional.of(miningCoordinator.getMinTransactionGasPrice().asUInt256()); + }; + } + + DataFetcher> 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 results = new ArrayList<>(); + for (long i = from; i <= to; i++) { + final Optional> block = + blockchain.blockByNumber(i); + block.ifPresent(e -> results.add(new NormalBlockAdapter(e))); + } + return results; + }; + } + + public DataFetcher> 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> 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> 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 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 ows = blockchainQuery.getWorldState(latestBn); + return ows.flatMap(ws -> Optional.ofNullable(ws.get(addr))).map(AccountAdapter::new); + }; + } + + DataFetcher> getTransactionDataFetcher() { + return dataFetchingEnvironment -> { + final BlockchainQuery blockchain = + ((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getBlockchainQuery(); + final Bytes32 hash = dataFetchingEnvironment.getArgument("hash"); + final Optional tran = blockchain.transactionByHash(Hash.wrap(hash)); + return tran.map(TransactionAdapter::new); + }; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLProvider.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLProvider.java new file mode 100644 index 0000000000..5805e5b5f4 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLProvider.java @@ -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(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfiguration.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfiguration.java new file mode 100644 index 0000000000..7705499195 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfiguration.java @@ -0,0 +1,120 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Objects; +import com.google.common.collect.Lists; + +public class GraphQLRpcConfiguration { + private static final String DEFAULT_GRAPHQL_RPC_HOST = "127.0.0.1"; + public static final int DEFAULT_GRAPHQL_RPC_PORT = 8547; + + private boolean enabled; + private int port; + private String host; + private Collection corsAllowedDomains = Collections.emptyList(); + private Collection hostsWhitelist = Arrays.asList("localhost", "127.0.0.1"); + + public static GraphQLRpcConfiguration createDefault() { + final GraphQLRpcConfiguration config = new GraphQLRpcConfiguration(); + config.setEnabled(false); + config.setPort(DEFAULT_GRAPHQL_RPC_PORT); + config.setHost(DEFAULT_GRAPHQL_RPC_HOST); + return config; + } + + private GraphQLRpcConfiguration() {} + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(final boolean enabled) { + this.enabled = enabled; + } + + public int getPort() { + return port; + } + + public void setPort(final int port) { + this.port = port; + } + + public String getHost() { + return host; + } + + public void setHost(final String host) { + this.host = host; + } + + Collection getCorsAllowedDomains() { + return corsAllowedDomains; + } + + public void setCorsAllowedDomains(final Collection corsAllowedDomains) { + checkNotNull(corsAllowedDomains); + this.corsAllowedDomains = corsAllowedDomains; + } + + Collection getHostsWhitelist() { + return Collections.unmodifiableCollection(this.hostsWhitelist); + } + + public void setHostsWhitelist(final Collection hostsWhitelist) { + checkNotNull(hostsWhitelist); + this.hostsWhitelist = hostsWhitelist; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("enabled", enabled) + .add("port", port) + .add("host", host) + .add("corsAllowedDomains", corsAllowedDomains) + .add("hostsWhitelist", hostsWhitelist) + .toString(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final GraphQLRpcConfiguration that = (GraphQLRpcConfiguration) o; + return enabled == that.enabled + && port == that.port + && Objects.equal(host, that.host) + && Objects.equal( + Lists.newArrayList(corsAllowedDomains), Lists.newArrayList(that.corsAllowedDomains)) + && Objects.equal( + Lists.newArrayList(hostsWhitelist), Lists.newArrayList(that.hostsWhitelist)); + } + + @Override + public int hashCode() { + return Objects.hashCode(enabled, port, host, corsAllowedDomains, hostsWhitelist); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcException.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcException.java new file mode 100644 index 0000000000..b3ba721cdf --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcException.java @@ -0,0 +1,60 @@ +/* + * 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.graphqlrpc.internal.response.GraphQLRpcError; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import graphql.ErrorType; +import graphql.GraphQLError; +import graphql.language.SourceLocation; + +class GraphQLRpcException extends RuntimeException implements GraphQLError { + private final GraphQLRpcError error; + + GraphQLRpcException(final GraphQLRpcError error) { + + super(error.getMessage()); + + this.error = error; + } + + @Override + public Map getExtensions() { + final Map customAttributes = new LinkedHashMap<>(); + + customAttributes.put("errorCode", this.error.getCode()); + customAttributes.put("errorMessage", this.getMessage()); + + return customAttributes; + } + + @Override + public List getLocations() { + return null; + } + + @Override + public ErrorType getErrorType() { + switch (error) { + case INVALID_PARAMS: + return ErrorType.ValidationError; + case INTERNAL_ERROR: + default: + return ErrorType.DataFetchingException; + } + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpService.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpService.java new file mode 100644 index 0000000000..5a0a0e5ff6 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpService.java @@ -0,0 +1,387 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.Streams.stream; +import static tech.pegasys.pantheon.util.NetworkUtility.urlForSocketAddress; + +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response.GraphQLJsonRequest; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response.GraphQLRpcErrorResponse; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response.GraphQLRpcResponse; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response.GraphQLRpcResponseType; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response.GraphQLRpcSuccessResponse; +import tech.pegasys.pantheon.util.NetworkUtility; + +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.StringJoiner; +import java.util.concurrent.CompletableFuture; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; +import com.google.common.collect.Iterables; +import graphql.ExecutionInput; +import graphql.ExecutionResult; +import graphql.GraphQL; +import graphql.GraphQLError; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpHeaders; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServer; +import io.vertx.core.http.HttpServerOptions; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.core.json.DecodeException; +import io.vertx.core.json.Json; +import io.vertx.ext.web.Router; +import io.vertx.ext.web.RoutingContext; +import io.vertx.ext.web.handler.BodyHandler; +import io.vertx.ext.web.handler.CorsHandler; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +public class GraphQLRpcHttpService { + + private static final Logger LOG = LogManager.getLogger(); + + private static final InetSocketAddress EMPTY_SOCKET_ADDRESS = new InetSocketAddress("0.0.0.0", 0); + private static final String APPLICATION_JSON = "application/json"; + private static final String EMPTY_RESPONSE = ""; + + private static final TypeReference> MAP_TYPE = + new TypeReference>() {}; + + private final Vertx vertx; + private final GraphQLRpcConfiguration config; + private final Path dataDir; + + private HttpServer httpServer; + + private final GraphQL graphQL; + + private final GraphQLDataFetcherContext dataFetcherContext; + + /** + * Construct a GraphQLRpcHttpService handler + * + * @param vertx The vertx process that will be running this service + * @param dataDir The data directory where requests can be buffered + * @param config Configuration for the rpc methods being loaded + * @param graphQL GraphQL engine + * @param dataFetcherContext DataFetcherContext required by GraphQL to finish it's job + */ + public GraphQLRpcHttpService( + final Vertx vertx, + final Path dataDir, + final GraphQLRpcConfiguration config, + final GraphQL graphQL, + final GraphQLDataFetcherContext dataFetcherContext) { + this.dataDir = dataDir; + + validateConfig(config); + this.config = config; + this.vertx = vertx; + this.graphQL = graphQL; + this.dataFetcherContext = dataFetcherContext; + } + + private void validateConfig(final GraphQLRpcConfiguration config) { + checkArgument( + config.getPort() == 0 || NetworkUtility.isValidPort(config.getPort()), + "Invalid port configuration."); + checkArgument(config.getHost() != null, "Required host is not configured."); + } + + public CompletableFuture start() { + LOG.info("Starting GraphQLRPC service on {}:{}", config.getHost(), config.getPort()); + // Create the HTTP server and a router object. + httpServer = + vertx.createHttpServer( + new HttpServerOptions().setHost(config.getHost()).setPort(config.getPort())); + + // Handle graphql rpc requests + final Router router = Router.router(vertx); + + // Verify Host header to avoid rebind attack. + router.route().handler(checkWhitelistHostHeader()); + + router + .route() + .handler( + CorsHandler.create(buildCorsRegexFromConfig()) + .allowedHeader("*") + .allowedHeader("content-type")); + router + .route() + .handler( + BodyHandler.create() + .setUploadsDirectory(dataDir.resolve("uploads").toString()) + .setDeleteUploadedFilesOnEnd(true)); + router.route("/").method(HttpMethod.GET).handler(this::handleEmptyRequest); + router + .route("/graphql") + .method(HttpMethod.GET) + .method(HttpMethod.POST) + .produces(APPLICATION_JSON) + .handler(this::handleGraphQLRPCRequest); + + final CompletableFuture resultFuture = new CompletableFuture<>(); + httpServer + .requestHandler(router) + .listen( + res -> { + if (!res.failed()) { + resultFuture.complete(null); + LOG.info( + "GraphQL RPC service started and listening on {}:{}", + config.getHost(), + httpServer.actualPort()); + return; + } + httpServer = null; + final Throwable cause = res.cause(); + if (cause instanceof SocketException) { + resultFuture.completeExceptionally( + new GraphQLRpcServiceException( + String.format( + "Failed to bind Ethereum GraphQL RPC listener to %s:%s: %s", + config.getHost(), config.getPort(), cause.getMessage()))); + return; + } + resultFuture.completeExceptionally(cause); + }); + + return resultFuture; + } + + private Handler checkWhitelistHostHeader() { + return event -> { + final Optional hostHeader = getAndValidateHostHeader(event); + if (config.getHostsWhitelist().contains("*") + || (hostHeader.isPresent() && hostIsInWhitelist(hostHeader.get()))) { + event.next(); + } else { + event + .response() + .setStatusCode(403) + .putHeader("Content-Type", "application/json; charset=utf-8") + .end("{\"message\":\"Host not authorized.\"}"); + } + }; + } + + private Optional getAndValidateHostHeader(final RoutingContext event) { + final Iterable splitHostHeader = Splitter.on(':').split(event.request().host()); + final long hostPieces = stream(splitHostHeader).count(); + if (hostPieces > 1) { + // If the host contains a colon, verify the host is correctly formed - host [ ":" port ] + if (hostPieces > 2 || !Iterables.get(splitHostHeader, 1).matches("\\d{1,5}+")) { + return Optional.empty(); + } + } + return Optional.ofNullable(Iterables.get(splitHostHeader, 0)); + } + + private boolean hostIsInWhitelist(final String hostHeader) { + return config.getHostsWhitelist().stream() + .anyMatch(whitelistEntry -> whitelistEntry.toLowerCase().equals(hostHeader.toLowerCase())); + } + + public CompletableFuture stop() { + if (httpServer == null) { + return CompletableFuture.completedFuture(null); + } + + final CompletableFuture resultFuture = new CompletableFuture<>(); + httpServer.close( + res -> { + if (res.failed()) { + resultFuture.completeExceptionally(res.cause()); + } else { + httpServer = null; + resultFuture.complete(null); + } + }); + return resultFuture; + } + + public InetSocketAddress socketAddress() { + if (httpServer == null) { + return EMPTY_SOCKET_ADDRESS; + } + return new InetSocketAddress(config.getHost(), httpServer.actualPort()); + } + + @VisibleForTesting + public String url() { + if (httpServer == null) { + return ""; + } + return urlForSocketAddress("http", socketAddress()); + } + + // Facilitate remote health-checks in AWS, inter alia. + private void handleEmptyRequest(final RoutingContext routingContext) { + routingContext.response().setStatusCode(201).end(); + } + + private void handleGraphQLRPCRequest(final RoutingContext routingContext) { + try { + final String query; + final String operationName; + final Map variables; + final HttpServerRequest request = routingContext.request(); + + switch (request.method()) { + case GET: + query = request.getParam("query"); + operationName = request.getParam("operationName"); + final String variableString = request.getParam("variables"); + if (variableString != null) { + variables = Json.decodeValue(variableString, MAP_TYPE); + } else { + variables = null; + } + break; + case POST: + if (request.getHeader(HttpHeaders.CONTENT_TYPE).equalsIgnoreCase(APPLICATION_JSON)) { + + final String requestBody = routingContext.getBodyAsString().trim(); + final GraphQLJsonRequest jsonRequest = + Json.decodeValue(requestBody, GraphQLJsonRequest.class); + query = jsonRequest.getQuery(); + operationName = jsonRequest.getOperationName(); + variables = jsonRequest.getVariables(); + } else { + // treat all else as application/graphql + query = routingContext.getBodyAsString().trim(); + operationName = null; + variables = null; + } + break; + default: + routingContext + .response() + .setStatusCode(HttpResponseStatus.METHOD_NOT_ALLOWED.code()) + .end(); + return; + } + + final HttpServerResponse response = routingContext.response(); + vertx.executeBlocking( + future -> { + try { + final GraphQLRpcResponse graphQLRpcResponse = + process(query, operationName, variables); + future.complete(graphQLRpcResponse); + } catch (final Exception e) { + future.fail(e); + } + }, + false, + (res) -> { + response.putHeader("Content-Type", APPLICATION_JSON); + if (res.failed()) { + response.setStatusCode(HttpResponseStatus.INTERNAL_SERVER_ERROR.code()); + response.end( + serialise( + new GraphQLRpcErrorResponse( + Collections.singletonMap( + "errors", + Collections.singletonList( + Collections.singletonMap( + "message", res.cause().getMessage())))))); + } else { + final GraphQLRpcResponse graphQLRpcResponse = (GraphQLRpcResponse) res.result(); + response.setStatusCode(status(graphQLRpcResponse).code()); + response.end(serialise(graphQLRpcResponse)); + } + }); + + } catch (final DecodeException ex) { + handleGraphQLRpcError(routingContext, ex); + } + } + + private HttpResponseStatus status(final GraphQLRpcResponse response) { + + switch (response.getType()) { + case UNAUTHORIZED: + return HttpResponseStatus.UNAUTHORIZED; + case ERROR: + return HttpResponseStatus.BAD_REQUEST; + case SUCCESS: + case NONE: + default: + return HttpResponseStatus.OK; + } + } + + private String serialise(final GraphQLRpcResponse response) { + + if (response.getType() == GraphQLRpcResponseType.NONE) { + return EMPTY_RESPONSE; + } + + return Json.encodePrettily(response.getResult()); + } + + private GraphQLRpcResponse process( + final String requestJson, final String operationName, final Map variables) { + final ExecutionInput executionInput = + ExecutionInput.newExecutionInput() + .query(requestJson) + .operationName(operationName) + .variables(variables) + .context(dataFetcherContext) + .build(); + final ExecutionResult result = graphQL.execute(executionInput); + final Map toSpecificationResult = result.toSpecification(); + final List errors = result.getErrors(); + if (errors.size() == 0) { + return new GraphQLRpcSuccessResponse(toSpecificationResult); + } else { + return new GraphQLRpcErrorResponse(toSpecificationResult); + } + } + + private void handleGraphQLRpcError(final RoutingContext routingContext, final Exception ex) { + LOG.debug("Error handling GraphQL request", ex); + routingContext + .response() + .setStatusCode(HttpResponseStatus.BAD_REQUEST.code()) + .end(Json.encode(new GraphQLRpcErrorResponse(ex.getMessage()))); + } + + private String buildCorsRegexFromConfig() { + if (config.getCorsAllowedDomains().isEmpty()) { + return ""; + } + if (config.getCorsAllowedDomains().contains("*")) { + return "*"; + } else { + final StringJoiner stringJoiner = new StringJoiner("|"); + config.getCorsAllowedDomains().stream().filter(s -> !s.isEmpty()).forEach(stringJoiner::add); + return stringJoiner.toString(); + } + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcServiceException.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcServiceException.java new file mode 100644 index 0000000000..7bb8a2a2de --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcServiceException.java @@ -0,0 +1,20 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +class GraphQLRpcServiceException extends RuntimeException { + + public GraphQLRpcServiceException(final String message) { + super(message); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockWithMetadata.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockWithMetadata.java new file mode 100644 index 0000000000..c298fb09b5 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockWithMetadata.java @@ -0,0 +1,67 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.List; + +public class BlockWithMetadata { + + private final BlockHeader header; + private final List transactions; + private final List ommers; + private final UInt256 totalDifficulty; + private final int size; + + /** + * @param header The block header + * @param transactions Block transactions in generic format + * @param ommers Block ommers in generic format + * @param totalDifficulty The cumulative difficulty up to and including this block + * @param size The size of the rlp-encoded block (header + body). + */ + public BlockWithMetadata( + final BlockHeader header, + final List transactions, + final List ommers, + final UInt256 totalDifficulty, + final int size) { + this.header = header; + this.transactions = transactions; + this.ommers = ommers; + this.totalDifficulty = totalDifficulty; + this.size = size; + } + + public BlockHeader getHeader() { + return header; + } + + public List getOmmers() { + return ommers; + } + + public List getTransactions() { + return transactions; + } + + public UInt256 getTotalDifficulty() { + return totalDifficulty; + } + + public int getSize() { + return size; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockchainQuery.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockchainQuery.java new file mode 100644 index 0000000000..29b99de9cc --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/BlockchainQuery.java @@ -0,0 +1,283 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.chain.Blockchain; +import tech.pegasys.pantheon.ethereum.chain.TransactionLocation; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockBody; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.TransactionReceipt; +import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.collect.Lists; + +public class BlockchainQuery { + + private final WorldStateArchive worldStateArchive; + private final Blockchain blockchain; + + public BlockchainQuery(final Blockchain blockchain, final WorldStateArchive worldStateArchive) { + this.blockchain = blockchain; + this.worldStateArchive = worldStateArchive; + } + + public Blockchain getBlockchain() { + return blockchain; + } + + public WorldStateArchive getWorldStateArchive() { + return worldStateArchive; + } + + /** + * Returns the ommer at the given index for the referenced block. + * + * @param blockHeaderHash The hash of the block to be queried. + * @param index The index of the ommer in the blocks ommers list. + * @return The ommer at the given index belonging to the referenced block. + */ + public Optional getOmmer(final Hash blockHeaderHash, final int index) { + return blockchain.getBlockBody(blockHeaderHash).map(blockBody -> getOmmer(blockBody, index)); + } + + private BlockHeader getOmmer(final BlockBody blockBody, final int index) { + final List ommers = blockBody.getOmmers(); + if (ommers.size() > index) { + return ommers.get(index); + } else { + return null; + } + } + + /** + * Given a block hash, returns the associated block augmented with metadata. + * + * @param blockHeaderHash The hash of the target block's header. + * @return The referenced block. + */ + public Optional> blockByHash( + final Hash blockHeaderHash) { + return blockchain + .getBlockHeader(blockHeaderHash) + .flatMap( + header -> + blockchain + .getBlockBody(blockHeaderHash) + .flatMap( + body -> + blockchain + .getTotalDifficultyByHash(blockHeaderHash) + .map( + (td) -> { + final List txs = body.getTransactions(); + final List formattedTxs = + formatTransactions( + txs, header.getNumber(), blockHeaderHash); + final List ommers = + body.getOmmers().stream() + .map(BlockHeader::getHash) + .collect(Collectors.toList()); + final int size = new Block(header, body).calculateSize(); + return new BlockWithMetadata<>( + header, formattedTxs, ommers, td, size); + }))); + } + + /** + * Given a block number, returns the associated block augmented with metadata. + * + * @param number The height of the target block. + * @return The referenced block. + */ + public Optional> blockByNumber( + final long number) { + return blockchain.getBlockHashByNumber(number).flatMap(this::blockByHash); + } + + /** + * Returns the latest block augmented with metadata. + * + * @return The latest block. + */ + public Optional> latestBlock() { + return this.blockByHash(blockchain.getChainHeadHash()); + } + + /** + * Given a transaction hash, returns the associated transaction. + * + * @param transactionHash The hash of the target transaction. + * @return The transaction associated with the given hash. + */ + public Optional transactionByHash(final Hash transactionHash) { + final Optional maybeLocation = + blockchain.getTransactionLocation(transactionHash); + if (!maybeLocation.isPresent()) { + return Optional.empty(); + } + final TransactionLocation loc = maybeLocation.get(); + final Hash blockHash = loc.getBlockHash(); + final BlockHeader header = blockchain.getBlockHeader(blockHash).get(); + final Transaction transaction = blockchain.getTransactionByHash(transactionHash).get(); + return Optional.of( + new TransactionWithMetadata( + transaction, header.getNumber(), blockHash, loc.getTransactionIndex())); + } + + /** + * Returns the transaction receipt associated with the given transaction hash. + * + * @param transactionHash The hash of the transaction that corresponds to the receipt to retrieve. + * @return The transaction receipt associated with the referenced transaction. + */ + public Optional transactionReceiptByTransactionHash( + final Hash transactionHash) { + final Optional maybeLocation = + blockchain.getTransactionLocation(transactionHash); + if (!maybeLocation.isPresent()) { + return Optional.empty(); + } + final TransactionLocation location = maybeLocation.get(); + final BlockBody blockBody = blockchain.getBlockBody(location.getBlockHash()).get(); + final Transaction transaction = blockBody.getTransactions().get(location.getTransactionIndex()); + + final Hash blockhash = location.getBlockHash(); + final BlockHeader header = blockchain.getBlockHeader(blockhash).get(); + final List transactionReceipts = blockchain.getTxReceipts(blockhash).get(); + final TransactionReceipt transactionReceipt = + transactionReceipts.get(location.getTransactionIndex()); + + long gasUsed = transactionReceipt.getCumulativeGasUsed(); + if (location.getTransactionIndex() > 0) { + gasUsed = + gasUsed + - transactionReceipts.get(location.getTransactionIndex() - 1).getCumulativeGasUsed(); + } + + return Optional.of( + new TransactionReceiptWithMetadata( + transactionReceipt, + transaction, + transactionHash, + location.getTransactionIndex(), + gasUsed, + blockhash, + header.getNumber())); + } + + /** + * Returns the world state for the corresponding block number + * + * @param blockNumber the block number + * @return the world state at the block number + */ + public Optional getWorldState(final long blockNumber) { + final Optional header = blockchain.getBlockHeader(blockNumber); + return header + .map(BlockHeader::getStateRoot) + .flatMap(worldStateArchive::getMutable) + .map(mws -> mws); // to satisfy typing + } + + private List formatTransactions( + final List txs, final long blockNumber, final Hash blockHash) { + final int count = txs.size(); + final List result = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + result.add(new TransactionWithMetadata(txs.get(i), blockNumber, blockHash, i)); + } + return result; + } + + public List matchingLogs(final Hash blockhash, final LogsQuery query) { + final List matchingLogs = Lists.newArrayList(); + final Optional blockHeader = blockchain.getBlockHeader(blockhash); + if (!blockHeader.isPresent()) { + return matchingLogs; + } + final List receipts = blockchain.getTxReceipts(blockhash).get(); + final List transaction = + blockchain.getBlockBody(blockhash).get().getTransactions(); + final long number = blockHeader.get().getNumber(); + final boolean logHasBeenRemoved = !blockchain.blockIsOnCanonicalChain(blockhash); + return generateLogWithMetadata( + receipts, number, query, blockhash, matchingLogs, transaction, logHasBeenRemoved); + } + + private List generateLogWithMetadata( + final List receipts, + final long number, + final LogsQuery query, + final Hash blockhash, + final List matchingLogs, + final List transaction, + final boolean removed) { + for (int transactionIndex = 0; transactionIndex < receipts.size(); ++transactionIndex) { + final TransactionReceipt receipt = receipts.get(transactionIndex); + for (int logIndex = 0; logIndex < receipt.getLogs().size(); ++logIndex) { + if (query.matches(receipt.getLogs().get(logIndex))) { + final LogWithMetadata logWithMetaData = + new LogWithMetadata( + logIndex, + number, + blockhash, + transaction.get(transactionIndex).hash(), + transactionIndex, + receipts.get(transactionIndex).getLogs().get(logIndex).getLogger(), + receipts.get(transactionIndex).getLogs().get(logIndex).getData(), + receipts.get(transactionIndex).getLogs().get(logIndex).getTopics(), + removed); + matchingLogs.add(logWithMetaData); + } + } + } + return matchingLogs; + } + + public static List generateLogWithMetadataForTransaction( + final TransactionReceipt receipt, + final long number, + final Hash blockhash, + final Hash transactionHash, + final int transactionIndex, + final boolean removed) { + + final List logs = new ArrayList<>(); + for (int logIndex = 0; logIndex < receipt.getLogs().size(); ++logIndex) { + + final LogWithMetadata logWithMetaData = + new LogWithMetadata( + logIndex, + number, + blockhash, + transactionHash, + transactionIndex, + receipt.getLogs().get(logIndex).getLogger(), + receipt.getLogs().get(logIndex).getData(), + receipt.getLogs().get(logIndex).getTopics(), + removed); + logs.add(logWithMetaData); + } + + return logs; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogWithMetadata.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogWithMetadata.java new file mode 100644 index 0000000000..0de573b91e --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogWithMetadata.java @@ -0,0 +1,109 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.LogTopic; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.List; + +import com.google.common.base.MoreObjects; + +public class LogWithMetadata { + + private final int logIndex; + private final long blockNumber; + private final Hash blockHash; + private final Hash transactionHash; + private final int transactionIndex; + private final Address address; + private final BytesValue data; + private final List topics; + private final boolean removed; + + LogWithMetadata( + final int logIndex, + final long blockNumber, + final Hash blockHash, + final Hash transactionHash, + final int transactionIndex, + final Address address, + final BytesValue data, + final List topics, + final boolean removed) { + this.logIndex = logIndex; + this.blockNumber = blockNumber; + this.blockHash = blockHash; + this.transactionHash = transactionHash; + this.transactionIndex = transactionIndex; + this.address = address; + this.data = data; + this.topics = topics; + this.removed = removed; + } + + // The index of this log within the entire ordered list of logs associated with the block this log + // belongs to. + public int getLogIndex() { + return logIndex; + } + + public long getBlockNumber() { + return blockNumber; + } + + public Hash getBlockHash() { + return blockHash; + } + + public Hash getTransactionHash() { + return transactionHash; + } + + public int getTransactionIndex() { + return transactionIndex; + } + + public Address getAddress() { + return address; + } + + public BytesValue getData() { + return data; + } + + public List getTopics() { + return topics; + } + + public boolean isRemoved() { + return removed; + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("logIndex", logIndex) + .add("blockNumber", blockNumber) + .add("blockHash", blockHash) + .add("transactionHash", transactionHash) + .add("transactionIndex", transactionIndex) + .add("address", address) + .add("data", data) + .add("topics", topics) + .add("removed", removed) + .toString(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogsQuery.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogsQuery.java new file mode 100644 index 0000000000..958876ec69 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/LogsQuery.java @@ -0,0 +1,112 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Log; +import tech.pegasys.pantheon.ethereum.core.LogTopic; + +import java.util.Arrays; +import java.util.List; + +import com.google.common.collect.Lists; + +public class LogsQuery { + + private final List
queryAddresses; + private final List> queryTopics; + + private LogsQuery(final List
addresses, final List> topics) { + this.queryAddresses = addresses; + this.queryTopics = topics; + } + + public boolean matches(final Log log) { + return matchesAddresses(log.getLogger()) && matchesTopics(log.getTopics()); + } + + private boolean matchesAddresses(final Address address) { + return queryAddresses.isEmpty() || queryAddresses.contains(address); + } + + private boolean matchesTopics(final List topics) { + if (queryTopics.isEmpty()) { + return true; + } + if (topics.size() < queryTopics.size()) { + return false; + } + for (int i = 0; i < queryTopics.size(); ++i) { + if (!matchesTopic(topics.get(i), queryTopics.get(i))) { + return false; + } + } + return true; + } + + private boolean matchesTopic(final LogTopic topic, final List matchCriteria) { + for (final LogTopic candidate : matchCriteria) { + if (candidate == null) { + return true; + } + if (candidate.equals(topic)) { + return true; + } + } + return false; + } + + public static class Builder { + private final List
queryAddresses = Lists.newArrayList(); + private final List> queryTopics = Lists.newArrayList(); + + public Builder address(final Address address) { + if (address != null) { + queryAddresses.add(address); + } + return this; + } + + public Builder addresses(final Address... addresses) { + if (addresses != null && addresses.length > 0) { + queryAddresses.addAll(Arrays.asList(addresses)); + } + return this; + } + + public Builder addresses(final List
addresses) { + if (addresses != null && !addresses.isEmpty()) { + queryAddresses.addAll(addresses); + } + return this; + } + + public Builder topics(final List> topics) { + if (topics != null && !topics.isEmpty()) { + queryTopics.addAll(topics); + } + return this; + } + + public Builder topics(final TopicsParameter topicsParameter) { + if (topicsParameter != null) { + topics(topicsParameter.getTopics()); + } + return this; + } + + public LogsQuery build() { + return new LogsQuery(queryAddresses, queryTopics); + } + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TopicsParameter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TopicsParameter.java new file mode 100644 index 0000000000..2cf20ffd3c --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TopicsParameter.java @@ -0,0 +1,52 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.core.LogTopic; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; + +class TopicsParameter { + + private final List> queryTopics = new ArrayList<>(); + + @JsonCreator + public TopicsParameter(final List> topics) { + if (topics != null) { + for (final List list : topics) { + final List inputTopics = new ArrayList<>(); + if (list != null) { + for (final String input : list) { + final LogTopic topic = + input != null ? LogTopic.create(BytesValue.fromHexString(input)) : null; + inputTopics.add(topic); + } + } + queryTopics.add(inputTopics); + } + } + } + + public List> getTopics() { + return queryTopics; + } + + @Override + public String toString() { + return "TopicsParameter{" + "queryTopics=" + queryTopics + '}'; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionReceiptWithMetadata.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionReceiptWithMetadata.java new file mode 100644 index 0000000000..ce4faaba7c --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionReceiptWithMetadata.java @@ -0,0 +1,74 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.TransactionReceipt; + +public class TransactionReceiptWithMetadata { + private final TransactionReceipt receipt; + private final Hash transactionHash; + private final int transactionIndex; + private final long gasUsed; + private final long blockNumber; + private final Hash blockHash; + private final Transaction transaction; + + TransactionReceiptWithMetadata( + final TransactionReceipt receipt, + final Transaction transaction, + final Hash transactionHash, + final int transactionIndex, + final long gasUsed, + final Hash blockHash, + final long blockNumber) { + this.receipt = receipt; + this.transactionHash = transactionHash; + this.transactionIndex = transactionIndex; + this.gasUsed = gasUsed; + this.blockHash = blockHash; + this.blockNumber = blockNumber; + this.transaction = transaction; + } + + public TransactionReceipt getReceipt() { + return receipt; + } + + public Hash getTransactionHash() { + return transactionHash; + } + + public Transaction getTransaction() { + return transaction; + } + + public int getTransactionIndex() { + return transactionIndex; + } + + public Hash getBlockHash() { + return blockHash; + } + + public long getBlockNumber() { + return blockNumber; + } + + // The gas used for this particular transaction (as opposed to cumulativeGas which is included in + // the receipt itself) + public long getGasUsed() { + return gasUsed; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionWithMetadata.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionWithMetadata.java new file mode 100644 index 0000000000..dc0017c994 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/TransactionWithMetadata.java @@ -0,0 +1,51 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal; + +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Transaction; + +public class TransactionWithMetadata { + + private final Transaction transaction; + private final long blockNumber; + private final Hash blockHash; + private final int transactionIndex; + + public TransactionWithMetadata( + final Transaction transaction, + final long blockNumber, + final Hash blockHash, + final int transactionIndex) { + this.transaction = transaction; + this.blockNumber = blockNumber; + this.blockHash = blockHash; + this.transactionIndex = transactionIndex; + } + + public Transaction getTransaction() { + return transaction; + } + + public long getBlockNumber() { + return blockNumber; + } + + public Hash getBlockHash() { + return blockHash; + } + + public int getTransactionIndex() { + return transactionIndex; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AccountAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AccountAdapter.java new file mode 100644 index 0000000000..50e0530c46 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AccountAdapter.java @@ -0,0 +1,53 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.Account; +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.Optional; + +import graphql.schema.DataFetchingEnvironment; + +@SuppressWarnings("unused") // reflected by GraphQL +public class AccountAdapter extends AdapterBase { + private final Account account; + + public AccountAdapter(final Account account) { + this.account = account; + } + + public Optional
getAddress() { + return Optional.of(account.getAddress()); + } + + public Optional getBalance() { + return Optional.of(account.getBalance().asUInt256()); + } + + public Optional getTransactionCount() { + return Optional.of(account.getNonce()); + } + + public Optional getCode() { + return Optional.of(account.getCode()); + } + + public Optional getStorage(final DataFetchingEnvironment environment) { + final Bytes32 slot = environment.getArgument("slot"); + return Optional.of(account.getStorageValue(slot.asUInt256()).getBytes()); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AdapterBase.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AdapterBase.java new file mode 100644 index 0000000000..60846d9efc --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/AdapterBase.java @@ -0,0 +1,24 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLDataFetcherContext; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; + +import graphql.schema.DataFetchingEnvironment; + +abstract class AdapterBase { + BlockchainQuery getBlockchainQuery(final DataFetchingEnvironment environment) { + return ((GraphQLDataFetcherContext) environment.getContext()).getBlockchainQuery(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/BlockAdapterBase.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/BlockAdapterBase.java new file mode 100644 index 0000000000..3fa2b8f3d0 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/BlockAdapterBase.java @@ -0,0 +1,228 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.LogTopic; +import tech.pegasys.pantheon.ethereum.core.Wei; +import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLDataFetcherContext; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockWithMetadata; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.LogWithMetadata; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.LogsQuery; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.TransactionWithMetadata; +import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; +import tech.pegasys.pantheon.ethereum.transaction.CallParameter; +import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulator; +import tech.pegasys.pantheon.ethereum.transaction.TransactionSimulatorResult; +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.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.google.common.primitives.Longs; +import graphql.schema.DataFetchingEnvironment; + +@SuppressWarnings("unused") // reflected by GraphQL +public class BlockAdapterBase extends AdapterBase { + + private final BlockHeader header; + + BlockAdapterBase(final BlockHeader header) { + this.header = header; + } + + public Optional getParent(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final Hash parentHash = header.getParentHash(); + final Optional> block = + query.blockByHash(parentHash); + return block.map(NormalBlockAdapter::new); + } + + public Optional getHash() { + return Optional.of(header.getHash()); + } + + public Optional getNonce() { + final long nonce = header.getNonce(); + final byte[] bytes = Longs.toByteArray(nonce); + return Optional.of(BytesValue.wrap(bytes)); + } + + public Optional getTransactionsRoot() { + return Optional.of(header.getTransactionsRoot()); + } + + public Optional getStateRoot() { + return Optional.of(header.getStateRoot()); + } + + public Optional getReceiptsRoot() { + return Optional.of(header.getReceiptsRoot()); + } + + public Optional getMiner(final DataFetchingEnvironment environment) { + + final BlockchainQuery query = getBlockchainQuery(environment); + long blockNumber = header.getNumber(); + final Long bn = environment.getArgument("block"); + if (bn != null) { + blockNumber = bn; + } + return Optional.of( + new AccountAdapter(query.getWorldState(blockNumber).get().get(header.getCoinbase()))); + } + + public Optional getExtraData() { + return Optional.of(header.getExtraData()); + } + + public Optional getGasLimit() { + return Optional.of(header.getGasLimit()); + } + + public Optional getGasUsed() { + return Optional.of(header.getGasUsed()); + } + + public Optional getTimestamp() { + return Optional.of(UInt256.of(header.getTimestamp())); + } + + public Optional getLogsBloom() { + return Optional.of(header.getLogsBloom().getBytes()); + } + + public Optional getMixHash() { + return Optional.of(header.getMixHash()); + } + + public Optional getDifficulty() { + return Optional.of(header.getDifficulty()); + } + + public Optional getOmmerHash() { + return Optional.of(header.getOmmersHash()); + } + + public Optional getNumber() { + final long bn = header.getNumber(); + return Optional.of(bn); + } + + public Optional getAccount(final DataFetchingEnvironment environment) { + + final BlockchainQuery query = getBlockchainQuery(environment); + final long bn = header.getNumber(); + final WorldState ws = query.getWorldState(bn).get(); + + if (ws != null) { + final Address addr = environment.getArgument("address"); + return Optional.of(new AccountAdapter(ws.get(addr))); + } + return Optional.empty(); + } + + public List getLogs(final DataFetchingEnvironment environment) { + + final Map filter = environment.getArgument("filter"); + + @SuppressWarnings("unchecked") + final List
addrs = (List
) filter.get("addresses"); + @SuppressWarnings("unchecked") + final List> topics = (List>) filter.get("topics"); + + final List> transformedTopics = new ArrayList<>(); + for (final List topic : topics) { + transformedTopics.add(topic.stream().map(LogTopic::of).collect(Collectors.toList())); + } + + final LogsQuery query = + new LogsQuery.Builder().addresses(addrs).topics(transformedTopics).build(); + + final BlockchainQuery blockchain = getBlockchainQuery(environment); + + final Hash hash = header.getHash(); + final List logs = blockchain.matchingLogs(hash, query); + final List results = new ArrayList<>(); + for (final LogWithMetadata log : logs) { + results.add(new LogAdapter(log)); + } + return results; + } + + public Optional getEstimateGas(final DataFetchingEnvironment environment) { + final Optional result = executeCall(environment); + return result.map(CallResult::getGasUsed); + } + + public Optional getCall(final DataFetchingEnvironment environment) { + return executeCall(environment); + } + + private Optional executeCall(final DataFetchingEnvironment environment) { + final Map callData = environment.getArgument("data"); + final Address from = (Address) callData.get("from"); + final Address to = (Address) callData.get("to"); + final Long gas = (Long) callData.get("gas"); + final UInt256 gasPrice = (UInt256) callData.get("gasPrice"); + final UInt256 value = (UInt256) callData.get("value"); + final BytesValue data = (BytesValue) callData.get("data"); + + final BlockchainQuery query = getBlockchainQuery(environment); + final ProtocolSchedule protocolSchedule = + ((GraphQLDataFetcherContext) environment.getContext()).getProtocolSchedule(); + final long bn = header.getNumber(); + + final TransactionSimulator transactionSimulator = + new TransactionSimulator( + query.getBlockchain(), query.getWorldStateArchive(), protocolSchedule); + + long gasParam = -1; + Wei gasPriceParam = null; + Wei valueParam = null; + if (gas != null) { + gasParam = gas; + } + if (gasPrice != null) { + gasPriceParam = Wei.of(gasPrice); + } + if (value != null) { + valueParam = Wei.of(value); + } + final CallParameter param = + new CallParameter(from, to, gasParam, gasPriceParam, valueParam, data); + + final Optional opt = transactionSimulator.process(param, bn); + if (opt.isPresent()) { + final TransactionSimulatorResult result = opt.get(); + long status = 0; + if (result.isSuccessful()) { + status = 1; + } + final CallResult callResult = + new CallResult(status, result.getGasEstimate(), result.getOutput()); + return Optional.of(callResult); + } + return Optional.empty(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/CallResult.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/CallResult.java new file mode 100644 index 0000000000..d9fbe78c6d --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/CallResult.java @@ -0,0 +1,40 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +@SuppressWarnings("unused") // reflected by GraphQL +class CallResult { + private final Long status; + private final Long gasUsed; + private final BytesValue data; + + CallResult(final Long status, final Long gasUsed, final BytesValue data) { + this.status = status; + this.gasUsed = gasUsed; + this.data = data; + } + + public Long getStatus() { + return status; + } + + public Long getGasUsed() { + return gasUsed; + } + + public BytesValue getData() { + return data; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/LogAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/LogAdapter.java new file mode 100644 index 0000000000..a8624126df --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/LogAdapter.java @@ -0,0 +1,73 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.LogTopic; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.LogWithMetadata; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.TransactionWithMetadata; +import tech.pegasys.pantheon.util.bytes.Bytes32; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import graphql.schema.DataFetchingEnvironment; + +@SuppressWarnings("unused") // reflected by GraphQL +public class LogAdapter extends AdapterBase { + private final LogWithMetadata logWithMetadata; + + LogAdapter(final LogWithMetadata logWithMetadata) { + this.logWithMetadata = logWithMetadata; + } + + public Optional getIndex() { + return Optional.of(logWithMetadata.getLogIndex()); + } + + public List getTopics() { + final List topics = logWithMetadata.getTopics(); + final List result = new ArrayList<>(); + for (final LogTopic topic : topics) { + result.add(Bytes32.leftPad(topic)); + } + return result; + } + + public Optional getData() { + return Optional.of(logWithMetadata.getData()); + } + + public Optional getTransaction(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final Hash hash = logWithMetadata.getTransactionHash(); + final Optional tran = query.transactionByHash(hash); + return tran.map(TransactionAdapter::new); + } + + public Optional getAccount(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + long blockNumber = logWithMetadata.getBlockNumber(); + final Long bn = environment.getArgument("block"); + if (bn != null) { + blockNumber = bn; + } + + return query + .getWorldState(blockNumber) + .map(ws -> new AccountAdapter(ws.get(logWithMetadata.getAddress()))); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/NormalBlockAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/NormalBlockAdapter.java new file mode 100644 index 0000000000..69a3f37bd7 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/NormalBlockAdapter.java @@ -0,0 +1,95 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.Hash; +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.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import graphql.schema.DataFetchingEnvironment; + +@SuppressWarnings("unused") // reflected by GraphQL +public class NormalBlockAdapter extends BlockAdapterBase { + + public NormalBlockAdapter( + final BlockWithMetadata blockWithMetaData) { + super(blockWithMetaData.getHeader()); + this.blockWithMetaData = blockWithMetaData; + } + + private final BlockWithMetadata blockWithMetaData; + + public Optional getTransactionCount() { + return Optional.of(blockWithMetaData.getTransactions().size()); + } + + public Optional getTotalDifficulty() { + return Optional.of(blockWithMetaData.getTotalDifficulty()); + } + + public Optional getOmmerCount() { + return Optional.of(blockWithMetaData.getOmmers().size()); + } + + public List getOmmers(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final List ommers = blockWithMetaData.getOmmers(); + final List results = new ArrayList<>(); + final Hash hash = blockWithMetaData.getHeader().getHash(); + for (int i = 0; i < ommers.size(); i++) { + final Optional header = query.getOmmer(hash, i); + header.ifPresent(item -> results.add(new UncleBlockAdapter(item))); + } + + return results; + } + + public Optional getOmmerAt(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final int index = environment.getArgument("index"); + final List ommers = blockWithMetaData.getOmmers(); + if (ommers.size() > index) { + final Hash hash = blockWithMetaData.getHeader().getHash(); + final Optional header = query.getOmmer(hash, index); + return header.map(UncleBlockAdapter::new); + } + return Optional.empty(); + } + + public List getTransactions() { + final List trans = blockWithMetaData.getTransactions(); + final List results = new ArrayList<>(); + for (final TransactionWithMetadata tran : trans) { + results.add(new TransactionAdapter(tran)); + } + return results; + } + + public Optional getTransactionAt(final DataFetchingEnvironment environment) { + final int index = environment.getArgument("index"); + final List trans = blockWithMetaData.getTransactions(); + + if (trans.size() > index) { + return Optional.of(new TransactionAdapter(trans.get(index))); + } + + return Optional.empty(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/SyncStateAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/SyncStateAdapter.java new file mode 100644 index 0000000000..c7f2170807 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/SyncStateAdapter.java @@ -0,0 +1,48 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.SyncStatus; + +import java.util.Optional; + +@SuppressWarnings("unused") // reflected by GraphQL +public class SyncStateAdapter { + private final SyncStatus syncStatus; + + public SyncStateAdapter(final SyncStatus syncStatus) { + this.syncStatus = syncStatus; + } + + public Optional getStartingBlock() { + return Optional.of(syncStatus.getStartingBlock()); + } + + public Optional getCurrentBlock() { + return Optional.of(syncStatus.getCurrentBlock()); + } + + public Optional getHighestBlock() { + return Optional.of(syncStatus.getHighestBlock()); + } + + public Optional getPulledStates() { + // currently synchronizer has no this information + return Optional.empty(); + } + + public Optional getKnownStates() { + // currently synchronizer has no this information + return Optional.empty(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/TransactionAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/TransactionAdapter.java new file mode 100644 index 0000000000..c9237f1b1a --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/TransactionAdapter.java @@ -0,0 +1,185 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.Address; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Transaction; +import tech.pegasys.pantheon.ethereum.core.TransactionReceipt; +import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockWithMetadata; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.LogWithMetadata; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.TransactionReceiptWithMetadata; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.TransactionWithMetadata; +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 graphql.schema.DataFetchingEnvironment; + +@SuppressWarnings("unused") // reflected by GraphQL +public class TransactionAdapter extends AdapterBase { + private final TransactionWithMetadata transactionWithMetadata; + + public TransactionAdapter(final TransactionWithMetadata transactionWithMetadata) { + this.transactionWithMetadata = transactionWithMetadata; + } + + public Optional getHash() { + return Optional.of(transactionWithMetadata.getTransaction().hash()); + } + + public Optional getNonce() { + final long nonce = transactionWithMetadata.getTransaction().getNonce(); + return Optional.of(nonce); + } + + public Optional getIndex() { + return Optional.of(transactionWithMetadata.getTransactionIndex()); + } + + public Optional getFrom(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + long blockNumber = transactionWithMetadata.getBlockNumber(); + final Long bn = environment.getArgument("block"); + if (bn != null) { + blockNumber = bn; + } + return query + .getWorldState(blockNumber) + .map( + mutableWorldState -> + new AccountAdapter( + mutableWorldState.get(transactionWithMetadata.getTransaction().getSender()))); + } + + public Optional getTo(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + long blockNumber = transactionWithMetadata.getBlockNumber(); + final Long bn = environment.getArgument("block"); + if (bn != null) { + blockNumber = bn; + } + + return query + .getWorldState(blockNumber) + .flatMap( + ws -> + transactionWithMetadata + .getTransaction() + .getTo() + .map(addr -> new AccountAdapter(ws.get(addr)))); + } + + public Optional getValue() { + return Optional.of(transactionWithMetadata.getTransaction().getValue().asUInt256()); + } + + public Optional getGasPrice() { + return Optional.of(transactionWithMetadata.getTransaction().getGasPrice().asUInt256()); + } + + public Optional getGas() { + return Optional.of(transactionWithMetadata.getTransaction().getGasLimit()); + } + + public Optional getInputData() { + return Optional.of(transactionWithMetadata.getTransaction().getPayload()); + } + + public Optional getBlock(final DataFetchingEnvironment environment) { + final Hash blockHash = transactionWithMetadata.getBlockHash(); + final BlockchainQuery query = getBlockchainQuery(environment); + final Optional> block = + query.blockByHash(blockHash); + return block.map(NormalBlockAdapter::new); + } + + public Optional getStatus(final DataFetchingEnvironment environment) { + return Optional.ofNullable(transactionWithMetadata.getTransaction()) + .map(Transaction::hash) + .flatMap(rpt -> getBlockchainQuery(environment).transactionReceiptByTransactionHash(rpt)) + .map(TransactionReceiptWithMetadata::getReceipt) + .flatMap( + receipt -> + receipt.getStatus() == -1 + ? Optional.empty() + : Optional.of((long) receipt.getStatus())); + } + + public Optional getGasUsed(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final Optional rpt = + query.transactionReceiptByTransactionHash(transactionWithMetadata.getTransaction().hash()); + return rpt.map(TransactionReceiptWithMetadata::getGasUsed); + } + + public Optional getCumulativeGasUsed(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final Optional rpt = + query.transactionReceiptByTransactionHash(transactionWithMetadata.getTransaction().hash()); + if (rpt.isPresent()) { + final TransactionReceipt receipt = rpt.get().getReceipt(); + return Optional.of(receipt.getCumulativeGasUsed()); + } + return Optional.empty(); + } + + public Optional getCreatedContract(final DataFetchingEnvironment environment) { + final boolean contractCreated = transactionWithMetadata.getTransaction().isContractCreation(); + if (contractCreated) { + final Optional
addr = transactionWithMetadata.getTransaction().getTo(); + + if (addr.isPresent()) { + final BlockchainQuery query = getBlockchainQuery(environment); + long blockNumber = transactionWithMetadata.getBlockNumber(); + final Long bn = environment.getArgument("block"); + if (bn != null) { + blockNumber = bn; + } + + final Optional ws = query.getWorldState(blockNumber); + if (ws.isPresent()) { + return Optional.of(new AccountAdapter(ws.get().get(addr.get()))); + } + } + } + return Optional.empty(); + } + + public List getLogs(final DataFetchingEnvironment environment) { + final BlockchainQuery query = getBlockchainQuery(environment); + final Hash hash = transactionWithMetadata.getTransaction().hash(); + final Optional tranRpt = + query.transactionReceiptByTransactionHash(hash); + final List results = new ArrayList<>(); + if (tranRpt.isPresent()) { + final List logs = + BlockchainQuery.generateLogWithMetadataForTransaction( + tranRpt.get().getReceipt(), + transactionWithMetadata.getBlockNumber(), + transactionWithMetadata.getBlockHash(), + hash, + transactionWithMetadata.getTransactionIndex(), + false); + for (final LogWithMetadata log : logs) { + results.add(new LogAdapter(log)); + } + } + return results; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/UncleBlockAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/UncleBlockAdapter.java new file mode 100644 index 0000000000..9c2a1ae150 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/UncleBlockAdapter.java @@ -0,0 +1,56 @@ +/* + * 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.internal.pojoadapter; + +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("unused") // reflected by GraphQL +class UncleBlockAdapter extends BlockAdapterBase { + + UncleBlockAdapter(final BlockHeader uncleHeader) { + super(uncleHeader); + } + + public Optional getTransactionCount() { + return Optional.of(0); + } + + public Optional getTotalDifficulty() { + return Optional.of(UInt256.of(0)); + } + + public Optional getOmmerCount() { + return Optional.empty(); + } + + public List getOmmers() { + return new ArrayList<>(); + } + + public Optional getOmmerAt() { + return Optional.empty(); + } + + public List getTransactions() { + return new ArrayList<>(); + } + + public Optional getTransactionAt() { + return Optional.empty(); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLJsonRequest.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLJsonRequest.java new file mode 100644 index 0000000000..401ded84db --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLJsonRequest.java @@ -0,0 +1,54 @@ +/* + * 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.internal.response; + +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonSetter; + +public class GraphQLJsonRequest { + private String query; + private String operationName; + private Map variables; + + @JsonGetter + public String getQuery() { + return query; + } + + @JsonSetter + public void setQuery(final String query) { + this.query = query; + } + + @JsonGetter + public String getOperationName() { + return operationName; + } + + @JsonSetter + public void setOperationName(final String operationName) { + this.operationName = operationName; + } + + @JsonGetter + public Map getVariables() { + return variables; + } + + @JsonSetter + public void setVariables(final Map variables) { + this.variables = variables; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcError.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcError.java new file mode 100644 index 0000000000..2440094e5a --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcError.java @@ -0,0 +1,43 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.fasterxml.jackson.annotation.JsonGetter; + +@JsonFormat(shape = JsonFormat.Shape.OBJECT) +public enum GraphQLRpcError { + // Standard errors + INVALID_PARAMS(-32602, "Invalid params"), + INTERNAL_ERROR(-32603, "Internal error"), + + CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE(-32008, "Initial sync is still in progress"); + + private final int code; + private final String message; + + GraphQLRpcError(final int code, final String message) { + this.code = code; + this.message = message; + } + + @JsonGetter("code") + public int getCode() { + return code; + } + + @JsonGetter("message") + public String getMessage() { + return message; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcErrorResponse.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcErrorResponse.java new file mode 100644 index 0000000000..f7b87b5741 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcErrorResponse.java @@ -0,0 +1,28 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class GraphQLRpcErrorResponse extends GraphQLRpcResponse { + + public GraphQLRpcErrorResponse(final Object errors) { + super(errors); + } + + @Override + @JsonIgnore + public GraphQLRpcResponseType getType() { + return GraphQLRpcResponseType.ERROR; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcNoResponse.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcNoResponse.java new file mode 100644 index 0000000000..f55e9bb8c6 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcNoResponse.java @@ -0,0 +1,25 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response; + +public class GraphQLRpcNoResponse extends GraphQLRpcResponse { + + public GraphQLRpcNoResponse() { + super(null); + } + + @Override + public GraphQLRpcResponseType getType() { + return GraphQLRpcResponseType.NONE; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponse.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponse.java new file mode 100644 index 0000000000..ac9fe579e3 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponse.java @@ -0,0 +1,46 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response; + +import com.google.common.base.Objects; + +public abstract class GraphQLRpcResponse { + public abstract GraphQLRpcResponseType getType(); + + private final Object result; + + GraphQLRpcResponse(final Object result) { + this.result = result; + } + + public Object getResult() { + return result; + } + + @Override + public boolean equals(final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final GraphQLRpcResponse that = (GraphQLRpcResponse) o; + return Objects.equal(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hashCode(result); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponseType.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponseType.java new file mode 100644 index 0000000000..2343e172a2 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcResponseType.java @@ -0,0 +1,21 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response; + +/** Various types of responses that the JSON-RPC component may produce. */ +public enum GraphQLRpcResponseType { + NONE, + SUCCESS, + ERROR, + UNAUTHORIZED +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcSuccessResponse.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcSuccessResponse.java new file mode 100644 index 0000000000..81b793847c --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/response/GraphQLRpcSuccessResponse.java @@ -0,0 +1,28 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc.internal.response; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +public class GraphQLRpcSuccessResponse extends GraphQLRpcResponse { + + public GraphQLRpcSuccessResponse(final Object data) { + super(data); + } + + @Override + @JsonIgnore + public GraphQLRpcResponseType getType() { + return GraphQLRpcResponseType.SUCCESS; + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalar.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalar.java new file mode 100644 index 0000000000..0d8fccbb78 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalar.java @@ -0,0 +1,63 @@ +/* + * 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.internal.scalar; + +import tech.pegasys.pantheon.ethereum.core.Address; + +import graphql.Internal; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +@Internal +public class AddressScalar extends GraphQLScalarType { + + public AddressScalar() { + super( + "Address", + "Address scalar", + new Coercing() { + @Override + public String serialize(final Object input) throws CoercingSerializeException { + if (input instanceof Address) { + return input.toString(); + } + throw new CoercingSerializeException("Unable to serialize " + input + " as an Address"); + } + + @Override + public String parseValue(final Object input) throws CoercingParseValueException { + if (input instanceof Address) { + return input.toString(); + } + throw new CoercingParseValueException( + "Unable to parse variable value " + input + " as an Address"); + } + + @Override + public Address parseLiteral(final Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException("Value is not any Address : '" + input + "'"); + } + try { + return Address.fromHexStringStrict(((StringValue) input).getValue()); + } catch (final IllegalArgumentException e) { + throw new CoercingParseLiteralException("Value is not any Address : '" + input + "'"); + } + } + }); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalar.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalar.java new file mode 100644 index 0000000000..dfd4d60dbb --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalar.java @@ -0,0 +1,66 @@ +/* + * 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.internal.scalar; + +import tech.pegasys.pantheon.util.uint.UInt256; + +import graphql.Internal; +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +@Internal +public class BigIntScalar extends GraphQLScalarType { + + public BigIntScalar() { + super( + "BigInt", + "A BigInt scalar", + new Coercing() { + @Override + public String serialize(final Object input) throws CoercingSerializeException { + if (input instanceof UInt256) { + return ((UInt256) input).toShortHexString(); + } + throw new CoercingSerializeException("Unable to serialize " + input + " as an BigInt"); + } + + @Override + public String parseValue(final Object input) throws CoercingParseValueException { + if (input instanceof UInt256) { + return ((UInt256) input).toShortHexString(); + } + throw new CoercingParseValueException( + "Unable to parse variable value " + input + " as an BigInt"); + } + + @Override + public UInt256 parseLiteral(final Object input) throws CoercingParseLiteralException { + try { + if (input instanceof StringValue) { + return UInt256.fromHexString(((StringValue) input).getValue()); + } else if (input instanceof IntValue) { + return UInt256.of(((IntValue) input).getValue()); + } + } catch (final IllegalArgumentException e) { + // fall through + } + throw new CoercingParseLiteralException("Value is not any BigInt : '" + input + "'"); + } + }); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32Scalar.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32Scalar.java new file mode 100644 index 0000000000..81a8b2ec5b --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32Scalar.java @@ -0,0 +1,63 @@ +/* + * 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.internal.scalar; + +import tech.pegasys.pantheon.util.bytes.Bytes32; + +import graphql.Internal; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +@Internal +public class Bytes32Scalar extends GraphQLScalarType { + + public Bytes32Scalar() { + super( + "Bytes32", + "A Byte32 scalar", + new Coercing() { + @Override + public String serialize(final Object input) throws CoercingSerializeException { + if (input instanceof Bytes32) { + return input.toString(); + } + throw new CoercingSerializeException("Unable to serialize " + input + " as an Bytes32"); + } + + @Override + public String parseValue(final Object input) throws CoercingParseValueException { + if (input instanceof Bytes32) { + return input.toString(); + } + throw new CoercingParseValueException( + "Unable to parse variable value " + input + " as an Bytes32"); + } + + @Override + public Bytes32 parseLiteral(final Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException("Value is not any Bytes32 : '" + input + "'"); + } + try { + return Bytes32.fromHexString(((StringValue) input).getValue()); + } catch (final IllegalArgumentException e) { + throw new CoercingParseLiteralException("Value is not any Bytes32 : '" + input + "'"); + } + } + }); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalar.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalar.java new file mode 100644 index 0000000000..0dbebe3dba --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalar.java @@ -0,0 +1,63 @@ +/* + * 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.internal.scalar; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import graphql.Internal; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +@Internal +public class BytesScalar extends GraphQLScalarType { + + public BytesScalar() { + super( + "Bytes", + "A Byte32 scalar", + new Coercing() { + @Override + public String serialize(final Object input) throws CoercingSerializeException { + if (input instanceof BytesValue) { + return input.toString(); + } + throw new CoercingSerializeException("Unable to serialize " + input + " as an Bytes"); + } + + @Override + public String parseValue(final Object input) throws CoercingParseValueException { + if (input instanceof BytesValue) { + return input.toString(); + } + throw new CoercingParseValueException( + "Unable to parse variable value " + input + " as an Bytes"); + } + + @Override + public BytesValue parseLiteral(final Object input) throws CoercingParseLiteralException { + if (!(input instanceof StringValue)) { + throw new CoercingParseLiteralException("Value is not any Bytes : '" + input + "'"); + } + try { + return BytesValue.fromHexString(((StringValue) input).getValue()); + } catch (final IllegalArgumentException e) { + throw new CoercingParseLiteralException("Value is not any Bytes : '" + input + "'"); + } + } + }); + } +} diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalar.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalar.java new file mode 100644 index 0000000000..8a2e830c02 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalar.java @@ -0,0 +1,85 @@ +/* + * 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.internal.scalar; + +import tech.pegasys.pantheon.util.bytes.Bytes32; + +import graphql.Internal; +import graphql.language.IntValue; +import graphql.language.StringValue; +import graphql.schema.Coercing; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import graphql.schema.GraphQLScalarType; + +@Internal +public class LongScalar extends GraphQLScalarType { + + public LongScalar() { + super( + "Long", + "Long is a 64 bit unsigned integer", + new Coercing() { + @Override + public Number serialize(final Object input) throws CoercingSerializeException { + if (input instanceof Number) { + return (Number) input; + } else if (input instanceof String) { + final String value = ((String) input).toLowerCase(); + if (value.startsWith("0x")) { + return Bytes32.fromHexStringLenient(value).asUInt256().toLong(); + } else { + return Long.parseLong(value); + } + } + throw new CoercingSerializeException("Unable to serialize " + input + " as an Long"); + } + + @Override + public Number parseValue(final Object input) throws CoercingParseValueException { + if (input instanceof Number) { + return (Number) input; + } else if (input instanceof String) { + final String value = ((String) input).toLowerCase(); + if (value.startsWith("0x")) { + return Bytes32.fromHexStringLenient(value).asUInt256().toLong(); + } else { + return Long.parseLong(value); + } + } + throw new CoercingParseValueException( + "Unable to parse variable value " + input + " as an Long"); + } + + @Override + public Object parseLiteral(final Object input) throws CoercingParseLiteralException { + try { + if (input instanceof IntValue) { + return ((IntValue) input).getValue().longValue(); + } else if (input instanceof StringValue) { + final String value = ((StringValue) input).getValue().toLowerCase(); + if (value.startsWith("0x")) { + return Bytes32.fromHexStringLenient(value).asUInt256().toLong(); + } else { + return Long.parseLong(value); + } + } + } catch (final NumberFormatException e) { + // fall through + } + throw new CoercingParseLiteralException("Value is not any Long : '" + input + "'"); + } + }); + } +} diff --git a/ethereum/graphqlrpc/src/main/resources/schema.graphqls b/ethereum/graphqlrpc/src/main/resources/schema.graphqls new file mode 100644 index 0000000000..b065daf502 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/resources/schema.graphqls @@ -0,0 +1,303 @@ +# Bytes32 is a 32 byte binary string, represented as 0x-prefixed hexadecimal. +scalar Bytes32 +# Address is a 20 byte Ethereum address, represented as 0x-prefixed hexadecimal. +scalar Address +# Bytes is an arbitrary length binary string, represented as 0x-prefixed hexadecimal. +# An empty byte string is represented as '0x'. Byte strings must have an even number of hexadecimal nybbles. +scalar Bytes +# BigInt is a large integer. Input is accepted as either a JSON number or as a string. +# Strings may be either decimal or 0x-prefixed hexadecimal. Output values are all +# 0x-prefixed hexadecimal. +scalar BigInt +# Long is a 64 bit unsigned integer. +scalar Long + +schema { + query: Query + mutation: Mutation +} + +# Account is an Ethereum account at a particular block. +type Account { + # Address is the address owning the account. + address: Address! + # Balance is the balance of the account, in wei. + balance: BigInt! + # TransactionCount is the number of transactions sent from this account, + # or in the case of a contract, the number of contracts created. Otherwise + # known as the nonce. + transactionCount: Long! + # Code contains the smart contract code for this account, if the account + # is a (non-self-destructed) contract. + code: Bytes! + # Storage provides access to the storage of a contract account, indexed + # by its 32 byte slot identifier. + storage(slot: Bytes32!): Bytes32! +} + +# Log is an Ethereum event log. +type Log { + # Index is the index of this log in the block. + index: Int! + # Account is the account which generated this log - this will always + # be a contract account. + account(block: Long): Account! + # Topics is a list of 0-4 indexed topics for the log. + topics: [Bytes32!]! + # Data is unindexed data for this log. + data: Bytes! + # Transaction is the transaction that generated this log entry. + transaction: Transaction! +} + +# Transaction is an Ethereum transaction. +type Transaction { + # Hash is the hash of this transaction. + hash: Bytes32! + # Nonce is the nonce of the account this transaction was generated with. + nonce: Long! + # Index is the index of this transaction in the parent block. This will + # be null if the transaction has not yet been mined. + index: Int + # From is the account that sent this transaction - this will always be + # an externally owned account. + from(block: Long): Account! + # To is the account the transaction was sent to. This is null for + # contract-creating transactions. + to(block: Long): Account + # Value is the value, in wei, sent along with this transaction. + value: BigInt! + # GasPrice is the price offered to miners for gas, in wei per unit. + gasPrice: BigInt! + # Gas is the maximum amount of gas this transaction can consume. + gas: Long! + # InputData is the data supplied to the target of the transaction. + inputData: Bytes! + # Block is the block this transaction was mined in. This will be null if + # the transaction has not yet been mined. + block: Block + + # Status is the return status of the transaction. This will be 1 if the + # transaction succeeded, or 0 if it failed (due to a revert, or due to + # running out of gas). If the transaction has not yet been mined, this + # field will be null. + status: Long + # GasUsed is the amount of gas that was used processing this transaction. + # If the transaction has not yet been mined, this field will be null. + gasUsed: Long + # CumulativeGasUsed is the total gas used in the block up to and including + # this transaction. If the transaction has not yet been mined, this field + # will be null. + cumulativeGasUsed: Long + # CreatedContract is the account that was created by a contract creation + # transaction. If the transaction was not a contract creation transaction, + # or it has not yet been mined, this field will be null. + createdContract(block: Long): Account + # Logs is a list of log entries emitted by this transaction. If the + # transaction has not yet been mined, this field will be null. + logs: [Log!] +} + +# BlockFilterCriteria encapsulates log filter criteria for a filter applied +# to a single block. +input BlockFilterCriteria { + # Addresses is list of addresses that are of interest. If this list is + # empty, results will not be filtered by address. + addresses: [Address!] + # Topics list restricts matches to particular event topics. Each event has a list + # of topics. Topics matches a prefix of that list. An empty element array matches any + # topic. Non-empty elements represent an alternative that matches any of the + # contained topics. + # + # Examples: + # - [] or nil matches any topic list + # - [[A]] matches topic A in first position + # - [[], [B]] matches any topic in first position, B in second position + # - [[A], [B]] matches topic A in first position, B in second position + # - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + topics: [[Bytes32!]!] +} + +# Block is an Ethereum block. +type Block { + # Number is the number of this block, starting at 0 for the genesis block. + number: Long! + # Hash is the block hash of this block. + hash: Bytes32! + # Parent is the parent block of this block. + parent: Block + # Nonce is the block nonce, an 8 byte sequence determined by the miner. + nonce: Bytes! + # TransactionsRoot is the keccak256 hash of the root of the trie of transactions in this block. + transactionsRoot: Bytes32! + # TransactionCount is the number of transactions in this block. if + # transactions are not available for this block, this field will be null. + transactionCount: Int + # StateRoot is the keccak256 hash of the state trie after this block was processed. + stateRoot: Bytes32! + # ReceiptsRoot is the keccak256 hash of the trie of transaction receipts in this block. + receiptsRoot: Bytes32! + # Miner is the account that mined this block. + miner(block: Long): Account! + # ExtraData is an arbitrary data field supplied by the miner. + extraData: Bytes! + # GasLimit is the maximum amount of gas that was available to transactions in this block. + gasLimit: Long! + # GasUsed is the amount of gas that was used executing transactions in this block. + gasUsed: Long! + # Timestamp is the unix timestamp at which this block was mined. + timestamp: BigInt! + # LogsBloom is a bloom filter that can be used to check if a block may + # contain log entries matching a filter. + logsBloom: Bytes! + # MixHash is the hash that was used as an input to the PoW process. + mixHash: Bytes32! + # Difficulty is a measure of the difficulty of mining this block. + difficulty: BigInt! + # TotalDifficulty is the sum of all difficulty values up to and including + # this block. + totalDifficulty: BigInt! + # OmmerCount is the number of ommers (AKA uncles) associated with this + # block. If ommers are unavailable, this field will be null. + ommerCount: Int + # Ommers is a list of ommer (AKA uncle) blocks associated with this block. + # If ommers are unavailable, this field will be null. Depending on your + # node, the transactions, transactionAt, transactionCount, ommers, + # ommerCount and ommerAt fields may not be available on any ommer blocks. + ommers: [Block] + # OmmerAt returns the ommer (AKA uncle) at the specified index. If ommers + # are unavailable, or the index is out of bounds, this field will be null. + ommerAt(index: Int!): Block + # OmmerHash is the keccak256 hash of all the ommers (AKA uncles) + # associated with this block. + ommerHash: Bytes32! + # Transactions is a list of transactions associated with this block. If + # transactions are unavailable for this block, this field will be null. + transactions: [Transaction!] + # TransactionAt returns the transaction at the specified index. If + # transactions are unavailable for this block, or if the index is out of + # bounds, this field will be null. + transactionAt(index: Int!): Transaction + # Logs returns a filtered set of logs from this block. + logs(filter: BlockFilterCriteria!): [Log!]! + # Account fetches an Ethereum account at the current block's state. + account(address: Address!): Account! + # Call executes a local call operation at the current block's state. + call(data: CallData!): CallResult + # EstimateGas estimates the amount of gas that will be required for + # successful execution of a transaction at the current block's state. + estimateGas(data: CallData!): Long! +} + +# CallData represents the data associated with a local contract call. +# All fields are optional. +input CallData { + # From is the address making the call. + from: Address + # To is the address the call is sent to. + to: Address + # Gas is the amount of gas sent with the call. + gas: Long + # GasPrice is the price, in wei, offered for each unit of gas. + gasPrice: BigInt + # Value is the value, in wei, sent along with the call. + value: BigInt + # Data is the data sent to the callee. + data: Bytes +} + +# CallResult is the result of a local call operation. +type CallResult { + # Data is the return data of the called contract. + data: Bytes! + # GasUsed is the amount of gas used by the call, after any refunds. + gasUsed: Long! + # Status is the result of the call - 1 for success or 0 for failure. + status: Long! +} + +# FilterCriteria encapsulates log filter criteria for searching log entries. +input FilterCriteria { + # FromBlock is the block at which to start searching, inclusive. Defaults + # to the latest block if not supplied. + fromBlock: Long + # ToBlock is the block at which to stop searching, inclusive. Defaults + # to the latest block if not supplied. + toBlock: Long + # Addresses is a list of addresses that are of interest. If this list is + # empty, results will not be filtered by address. + addresses: [Address!] + # Topics list restricts matches to particular event topics. Each event has a list + # of topics. Topics matches a prefix of that list. An empty element array matches any + # topic. Non-empty elements represent an alternative that matches any of the + # contained topics. + # + # Examples: + # - [] or nil matches any topic list + # - [[A]] matches topic A in first position + # - [[], [B]] matches any topic in first position, B in second position + # - [[A], [B]] matches topic A in first position, B in second position + # - [[A, B]], [C, D]] matches topic (A OR B) in first position, (C OR D) in second position + topics: [[Bytes32!]!] +} + +# SyncState contains the current synchronisation state of the client. +type SyncState{ + # StartingBlock is the block number at which synchronisation started. + startingBlock: Long! + # CurrentBlock is the point at which synchronisation has presently reached. + currentBlock: Long! + # HighestBlock is the latest known block number. + highestBlock: Long! + # PulledStates is the number of state entries fetched so far, or null + # if this is not known or not relevant. + pulledStates: Long + # KnownStates is the number of states the node knows of so far, or null + # if this is not known or not relevant. + knownStates: Long +} + +# Pending represents the current pending state. +type Pending { + # TransactionCount is the number of transactions in the pending state. + transactionCount: Int! + # Transactions is a list of transactions in the current pending state. + transactions: [Transaction!] + # Account fetches an Ethereum account for the pending state. + account(address: Address!): Account! + # Call executes a local call operation for the pending state. + call(data: CallData!): CallResult + # EstimateGas estimates the amount of gas that will be required for + # successful execution of a transaction for the pending state. + estimateGas(data: CallData!): Long! +} + +type Query { + # Account fetches an Ethereum account at the specified block number. + # If blockNumber is not provided, it defaults to the most recent block. + account(address: Address!, blockNumber: Long): Account! + # Block fetches an Ethereum block by number or by hash. If neither is + # supplied, the most recent known block is returned. + block(number: Long, hash: Bytes32): Block! + # Blocks returns all the blocks between two numbers, inclusive. If + # to is not supplied, it defaults to the most recent known block. + blocks(from: Long!, to: Long): [Block!]! + # Pending returns the current pending state. + pending: Pending! + # Transaction returns a transaction specified by its hash. + transaction(hash: Bytes32!): Transaction + # Logs returns log entries matching the provided filter. + logs(filter: FilterCriteria!): [Log!]! + # GasPrice returns the node's estimate of a gas price sufficient to + # ensure a transaction is mined in a timely fashion. + gasPrice: BigInt! + # ProtocolVersion returns the current wire protocol version number. + protocolVersion: Int! + # Syncing returns information on the current synchronisation state. + syncing: SyncState +} + +type Mutation { + # SendRawTransaction sends an RLP-encoded transaction to the network. + sendRawTransaction(data: Bytes!): Bytes32! +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractDataFetcherTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractDataFetcherTest.java new file mode 100644 index 0000000000..026a0e394f --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractDataFetcherTest.java @@ -0,0 +1,53 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.pojoadapter.NormalBlockAdapter; +import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.Optional; +import java.util.Set; + +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import org.junit.Before; +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.mockito.Mock; + +public abstract class AbstractDataFetcherTest { + + DataFetcher> fetcher; + private GraphQLDataFetchers fetchers; + + @Mock protected Set supportedCapabilities; + + @Mock protected DataFetchingEnvironment environment; + + @Mock protected GraphQLDataFetcherContext context; + + @Mock protected BlockchainQuery query; + + @Rule public ExpectedException thrown = ExpectedException.none(); + + @Before + public void before() { + fetchers = new GraphQLDataFetchers(supportedCapabilities); + fetcher = fetchers.getBlockDataFetcher(); + when(environment.getContext()).thenReturn(context); + when(context.getBlockchainQuery()).thenReturn(query); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractEthGraphQLRpcHttpServiceTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractEthGraphQLRpcHttpServiceTest.java new file mode 100644 index 0000000000..2bc994b759 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/AbstractEthGraphQLRpcHttpServiceTest.java @@ -0,0 +1,192 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryBlockchain; +import static tech.pegasys.pantheon.ethereum.core.InMemoryStorageProvider.createInMemoryWorldStateArchive; + +import tech.pegasys.pantheon.ethereum.ProtocolContext; +import tech.pegasys.pantheon.ethereum.blockcreation.EthHashMiningCoordinator; +import tech.pegasys.pantheon.ethereum.chain.GenesisState; +import tech.pegasys.pantheon.ethereum.chain.MutableBlockchain; +import tech.pegasys.pantheon.ethereum.core.Block; +import tech.pegasys.pantheon.ethereum.core.BlockHeader; +import tech.pegasys.pantheon.ethereum.core.BlockImporter; +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.Wei; +import tech.pegasys.pantheon.ethereum.eth.EthProtocol; +import tech.pegasys.pantheon.ethereum.eth.transactions.PendingTransactions; +import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool; +import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; +import tech.pegasys.pantheon.ethereum.mainnet.MainnetBlockHashFunction; +import tech.pegasys.pantheon.ethereum.mainnet.MainnetProtocolSchedule; +import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSchedule; +import tech.pegasys.pantheon.ethereum.mainnet.ProtocolSpec; +import tech.pegasys.pantheon.ethereum.mainnet.ValidationResult; +import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; +import tech.pegasys.pantheon.ethereum.util.RawBlockIterator; +import tech.pegasys.pantheon.ethereum.worldstate.WorldStateArchive; + +import java.net.URL; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import graphql.GraphQL; +import io.vertx.core.Vertx; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import org.junit.After; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.rules.TemporaryFolder; + +public abstract class AbstractEthGraphQLRpcHttpServiceTest { + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + private static ProtocolSchedule PROTOCOL_SCHEDULE; + + static List BLOCKS; + + private static Block GENESIS_BLOCK; + + private static GenesisState GENESIS_CONFIG; + + private final Vertx vertx = Vertx.vertx(); + + private GraphQLRpcHttpService service; + + OkHttpClient client; + + String baseUrl; + + final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + private MutableBlockchain blockchain; + + private WorldStateArchive stateArchive; + + private ProtocolContext context; + + @BeforeClass + public static void setupConstants() throws Exception { + PROTOCOL_SCHEDULE = MainnetProtocolSchedule.create(); + + final URL blocksUrl = + EthGraphQLRpcHttpBySpecTest.class + .getClassLoader() + .getResource( + "tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + EthGraphQLRpcHttpBySpecTest.class + .getClassLoader() + .getResource("tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + + BLOCKS = new ArrayList<>(); + try (final RawBlockIterator iterator = + new RawBlockIterator( + Paths.get(blocksUrl.toURI()), + rlp -> BlockHeader.readFrom(rlp, MainnetBlockHashFunction::createHash))) { + while (iterator.hasNext()) { + BLOCKS.add(iterator.next()); + } + } + + final String genesisJson = Resources.toString(genesisJsonUrl, Charsets.UTF_8); + + GENESIS_BLOCK = BLOCKS.get(0); + GENESIS_CONFIG = GenesisState.fromJson(genesisJson, PROTOCOL_SCHEDULE); + } + + @Before + public void setupTest() throws Exception { + final Synchronizer synchronizerMock = mock(Synchronizer.class); + final SyncStatus status = new SyncStatus(1, 2, 3); + when(synchronizerMock.getSyncStatus()).thenReturn(Optional.of(status)); + + final EthHashMiningCoordinator miningCoordinatorMock = mock(EthHashMiningCoordinator.class); + when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(Wei.of(16)); + + final TransactionPool transactionPoolMock = mock(TransactionPool.class); + + when(transactionPoolMock.addLocalTransaction(any(Transaction.class))) + .thenReturn(ValidationResult.valid()); + final PendingTransactions pendingTransactionsMock = mock(PendingTransactions.class); + when(transactionPoolMock.getPendingTransactions()).thenReturn(pendingTransactionsMock); + + stateArchive = createInMemoryWorldStateArchive(); + GENESIS_CONFIG.writeStateTo(stateArchive.getMutable()); + + blockchain = createInMemoryBlockchain(GENESIS_BLOCK); + context = new ProtocolContext<>(blockchain, stateArchive, null); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + + final GraphQLRpcConfiguration config = GraphQLRpcConfiguration.createDefault(); + + config.setPort(0); + final GraphQLDataFetcherContext dataFetcherContext = + new GraphQLDataFetcherContext( + blockchain, + stateArchive, + PROTOCOL_SCHEDULE, + transactionPoolMock, + miningCoordinatorMock, + synchronizerMock); + + final GraphQLDataFetchers dataFetchers = new GraphQLDataFetchers(supportedCapabilities); + final GraphQL graphQL = GraphQLProvider.buildGraphQL(dataFetchers); + + service = + new GraphQLRpcHttpService( + vertx, folder.newFolder().toPath(), config, graphQL, dataFetcherContext); + service.start().join(); + + client = new OkHttpClient(); + baseUrl = service.url() + "/graphql/"; + } + + @After + public void shutdownServer() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + service.stop().join(); + vertx.close(); + } + + void importBlock(final int n) { + final Block block = BLOCKS.get(n); + final ProtocolSpec protocolSpec = + PROTOCOL_SCHEDULE.getByBlockNumber(block.getHeader().getNumber()); + final BlockImporter blockImporter = protocolSpec.getBlockImporter(); + blockImporter.importBlock(context, block, HeaderValidationMode.FULL); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/BlockDataFetcherTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/BlockDataFetcherTest.java new file mode 100644 index 0000000000..fbd6acf884 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/BlockDataFetcherTest.java @@ -0,0 +1,49 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BlockDataFetcherTest extends AbstractDataFetcherTest { + + @Test + public void bothNumberAndHashThrows() throws Exception { + final Hash fakedHash = Hash.hash(BytesValue.of(1)); + when(environment.getArgument(eq("number"))).thenReturn(1L); + when(environment.getArgument(eq("hash"))).thenReturn(fakedHash); + + thrown.expect(GraphQLRpcException.class); + fetcher.get(environment); + } + + @Test + public void onlyNumber() throws Exception { + + when(environment.getArgument(eq("number"))).thenReturn(1L); + when(environment.getArgument(eq("hash"))).thenReturn(null); + + when(environment.getContext()).thenReturn(context); + when(context.getBlockchainQuery()).thenReturn(query); + + fetcher.get(environment); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecErrorCaseTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecErrorCaseTest.java new file mode 100644 index 0000000000..0e780c1878 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecErrorCaseTest.java @@ -0,0 +1,91 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import io.vertx.core.json.JsonObject; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class EthGraphQLRpcHttpBySpecErrorCaseTest extends AbstractEthGraphQLRpcHttpServiceTest { + + private final String specFileName; + + public EthGraphQLRpcHttpBySpecErrorCaseTest(final String specFileName) { + this.specFileName = specFileName; + } + + @Parameters(name = "{index}: {0}") + public static Collection specs() { + final List specs = new ArrayList<>(); + specs.add("eth_getBlockWrongParams"); + specs.add("eth_getBlocksByWrongRange"); + specs.add("eth_getBalance_toobig_bn"); + specs.add("eth_getBalance_without_addr"); + + return specs; + } + + @Test + public void graphQLRPCCallWithSpecFile() throws Exception { + graphQLRPCCall(specFileName); + } + + private void graphQLRPCCall(final String name) throws IOException { + final String testSpecFile = name + ".json"; + final String json = + Resources.toString( + EthGraphQLRpcHttpBySpecTest.class.getResource(testSpecFile), Charsets.UTF_8); + final JsonObject spec = new JsonObject(json); + + final String rawRequestBody = spec.getString("request"); + final RequestBody requestBody = RequestBody.create(JSON, rawRequestBody); + final Request request = new Request.Builder().post(requestBody).url(baseUrl).build(); + + importBlocks(1, BLOCKS.size()); + try (final Response resp = client.newCall(request).execute()) { + final int expectedStatusCode = spec.getInteger("statusCode"); + final String resultStr = resp.body().string(); + + assertThat(resp.code()).isEqualTo(expectedStatusCode); + try { + final JsonObject expectedRespBody = spec.getJsonObject("response"); + final JsonObject result = new JsonObject(resultStr); + if (expectedRespBody != null) { + assertThat(result).isEqualTo(expectedRespBody); + } + } catch (final IllegalStateException ignored) { + } + } + } + + private void importBlocks(final int from, final int to) { + for (int i = from; i < to; ++i) { + importBlock(i); + } + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecTest.java new file mode 100644 index 0000000000..1a06844f0b --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/EthGraphQLRpcHttpBySpecTest.java @@ -0,0 +1,123 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import com.google.common.base.Charsets; +import com.google.common.io.Resources; +import io.vertx.core.json.JsonObject; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class EthGraphQLRpcHttpBySpecTest extends AbstractEthGraphQLRpcHttpServiceTest { + + private final String specFileName; + + public EthGraphQLRpcHttpBySpecTest(final String specFileName) { + this.specFileName = specFileName; + } + + @Parameters(name = "{index}: {0}") + public static Collection specs() { + final List specs = new ArrayList<>(); + + specs.add("eth_blockNumber"); + specs.add("eth_getTransactionByHash"); + specs.add("eth_getTransactionByHashNull"); + specs.add("eth_getBlockByHash"); + specs.add("eth_getBlockByNumber"); + specs.add("eth_getBlockTransactionCountByHash"); + specs.add("eth_getBlockTransactionCountByNumber"); + specs.add("eth_getTransactionByBlockHashAndIndex"); + specs.add("eth_getTransactionByBlockNumberAndIndex"); + + specs.add("eth_estimateGas_transfer"); + specs.add("eth_estimateGas_noParams"); + specs.add("eth_estimateGas_contractDeploy"); + + specs.add("eth_getCode"); + specs.add("eth_getCode_noCode"); + + specs.add("eth_getStorageAt"); + specs.add("eth_getStorageAt_illegalRangeGreaterThan"); + + specs.add("eth_getTransactionCount"); + + specs.add("eth_getTransactionByBlockNumberAndInvalidIndex"); + + specs.add("eth_getBlocksByRange"); + specs.add("eth_call_Block8"); + specs.add("eth_call_BlockLatest"); + specs.add("eth_getBalance_latest"); + specs.add("eth_getBalance_0x19"); + specs.add("eth_gasPrice"); + + specs.add("eth_getTransactionReceipt"); + + specs.add("eth_syncing"); + specs.add("eth_sendRawTransaction_contractCreation"); + + specs.add("eth_sendRawTransaction_messageCall"); + specs.add("eth_sendRawTransaction_transferEther"); + specs.add("eth_sendRawTransaction_unsignedTransaction"); + + specs.add("eth_getLogs_matchTopic"); + return specs; + } + + @Test + public void graphQLRPCCallWithSpecFile() throws Exception { + graphQLRPCCall(specFileName); + } + + private void graphQLRPCCall(final String name) throws IOException { + final String testSpecFile = name + ".json"; + final String json = + Resources.toString( + EthGraphQLRpcHttpBySpecTest.class.getResource(testSpecFile), Charsets.UTF_8); + final JsonObject spec = new JsonObject(json); + final String rawRequestBody = spec.getString("request"); + final RequestBody requestBody = RequestBody.create(JSON, rawRequestBody); + final Request request = new Request.Builder().post(requestBody).url(baseUrl).build(); + + importBlocks(1, BLOCKS.size()); + try (final Response resp = client.newCall(request).execute()) { + final int expectedStatusCode = spec.getInteger("statusCode"); + assertThat(resp.code()).isEqualTo(expectedStatusCode); + + final JsonObject expectedRespBody = spec.getJsonObject("response"); + final String resultStr = resp.body().string(); + + final JsonObject result = new JsonObject(resultStr); + assertThat(result).isEqualTo(expectedRespBody); + } + } + + private void importBlocks(final int from, final int to) { + for (int i = from; i < to; ++i) { + importBlock(i); + } + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfigurationTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfigurationTest.java new file mode 100644 index 0000000000..d36f8f7d56 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcConfigurationTest.java @@ -0,0 +1,29 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Test; + +public class GraphQLRpcConfigurationTest { + + @Test + public void defaultConfiguration() { + final GraphQLRpcConfiguration configuration = GraphQLRpcConfiguration.createDefault(); + + assertThat(configuration.isEnabled()).isFalse(); + assertThat(configuration.getHost()).isEqualTo("127.0.0.1"); + assertThat(configuration.getPort()).isEqualTo(8547); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceCorsTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceCorsTest.java new file mode 100644 index 0000000000..63ac4b9c93 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceCorsTest.java @@ -0,0 +1,230 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.blockcreation.EthHashMiningCoordinator; +import tech.pegasys.pantheon.ethereum.core.Synchronizer; +import tech.pegasys.pantheon.ethereum.eth.EthProtocol; +import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.Lists; +import graphql.GraphQL; +import io.vertx.core.Vertx; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.Response; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class GraphQLRpcHttpServiceCorsTest { + @Rule public final TemporaryFolder folder = new TemporaryFolder(); + + private final Vertx vertx = Vertx.vertx(); + private final OkHttpClient client = new OkHttpClient(); + private GraphQLRpcHttpService graphQLRpcHttpService; + + @Before + public void before() { + final GraphQLRpcConfiguration configuration = GraphQLRpcConfiguration.createDefault(); + configuration.setPort(0); + } + + @After + public void after() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + graphQLRpcHttpService.stop().join(); + vertx.close(); + } + + @Test + public void requestWithNonAcceptedOriginShouldFail() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .header("Origin", "http://bar.me") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isFalse(); + } + } + + @Test + public void requestWithAcceptedOriginShouldSucceed() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .header("Origin", "http://foo.io") + .build(); + + try (final Response response = client.newCall(request).execute()) { + System.out.println(response.body().string()); + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithOneOfMultipleAcceptedOriginsShouldSucceed() throws Exception { + graphQLRpcHttpService = + createGraphQLRpcHttpServiceWithAllowedDomains("http://foo.io", "http://bar.me"); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .header("Origin", "http://bar.me") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithNoneOfMultipleAcceptedOriginsShouldFail() throws Exception { + graphQLRpcHttpService = + createGraphQLRpcHttpServiceWithAllowedDomains("http://foo.io", "http://bar.me"); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .header("Origin", "http://hel.lo") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isFalse(); + } + } + + @Test + public void requestWithNoOriginShouldSucceedWhenNoCorsConfigSet() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains(); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithNoOriginShouldSucceedWhenCorsIsSet() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = new Request.Builder().url(graphQLRpcHttpService.url()).build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithAnyOriginShouldNotSucceedWhenCorsIsEmpty() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains(""); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .header("Origin", "http://bar.me") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isFalse(); + } + } + + @Test + public void requestWithAnyOriginShouldSucceedWhenCorsIsStart() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains("*"); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .header("Origin", "http://bar.me") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.isSuccessful()).isTrue(); + } + } + + @Test + public void requestWithAccessControlRequestMethodShouldReturnAllowedHeaders() throws Exception { + graphQLRpcHttpService = createGraphQLRpcHttpServiceWithAllowedDomains("http://foo.io"); + + final Request request = + new Request.Builder() + .url(graphQLRpcHttpService.url() + "/graphql?query={protocolVersion}") + .method("OPTIONS", null) + .header("Access-Control-Request-Method", "OPTIONS") + .header("Origin", "http://foo.io") + .build(); + + try (final Response response = client.newCall(request).execute()) { + assertThat(response.header("Access-Control-Allow-Headers")).contains("*", "content-type"); + } + } + + private GraphQLRpcHttpService createGraphQLRpcHttpServiceWithAllowedDomains( + final String... corsAllowedDomains) throws Exception { + final GraphQLRpcConfiguration config = GraphQLRpcConfiguration.createDefault(); + config.setPort(0); + if (corsAllowedDomains != null) { + config.setCorsAllowedDomains(Lists.newArrayList(corsAllowedDomains)); + } + + final BlockchainQuery blockchainQueries = mock(BlockchainQuery.class); + final Synchronizer synchronizer = mock(Synchronizer.class); + + final EthHashMiningCoordinator miningCoordinatorMock = mock(EthHashMiningCoordinator.class); + + final GraphQLDataFetcherContext dataFetcherContext = mock(GraphQLDataFetcherContext.class); + when(dataFetcherContext.getBlockchainQuery()).thenReturn(blockchainQueries); + when(dataFetcherContext.getMiningCoordinator()).thenReturn(miningCoordinatorMock); + + when(dataFetcherContext.getTransactionPool()).thenReturn(mock(TransactionPool.class)); + when(dataFetcherContext.getSynchronizer()).thenReturn(synchronizer); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + final GraphQLDataFetchers dataFetchers = new GraphQLDataFetchers(supportedCapabilities); + final GraphQL graphQL = GraphQLProvider.buildGraphQL(dataFetchers); + + final GraphQLRpcHttpService graphQLRpcHttpService = + new GraphQLRpcHttpService( + vertx, folder.newFolder().toPath(), config, graphQL, dataFetcherContext); + graphQLRpcHttpService.start().join(); + + return graphQLRpcHttpService; + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceHostWhitelistTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceHostWhitelistTest.java new file mode 100644 index 0000000000..715a9e8da0 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceHostWhitelistTest.java @@ -0,0 +1,156 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.blockcreation.EthHashMiningCoordinator; +import tech.pegasys.pantheon.ethereum.core.Synchronizer; +import tech.pegasys.pantheon.ethereum.eth.EthProtocol; +import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +import tech.pegasys.pantheon.ethereum.p2p.wire.Capability; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import graphql.GraphQL; +import io.vertx.core.Vertx; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class GraphQLRpcHttpServiceHostWhitelistTest { + + @ClassRule public static final TemporaryFolder folder = new TemporaryFolder(); + + protected static Vertx vertx; + + private static GraphQLRpcHttpService service; + private static OkHttpClient client; + private static String baseUrl; + private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private final GraphQLRpcConfiguration graphQLRpcConfig = createGraphQLRpcConfig(); + private final List hostsWhitelist = Arrays.asList("ally", "friend"); + + @Before + public void initServerAndClient() throws Exception { + vertx = Vertx.vertx(); + + service = createGraphQLRpcHttpService(); + service.start().join(); + + client = new OkHttpClient(); + baseUrl = service.url(); + } + + private GraphQLRpcHttpService createGraphQLRpcHttpService() throws Exception { + final BlockchainQuery blockchainQueries = mock(BlockchainQuery.class); + final Synchronizer synchronizer = mock(Synchronizer.class); + + final EthHashMiningCoordinator miningCoordinatorMock = mock(EthHashMiningCoordinator.class); + + final GraphQLDataFetcherContext dataFetcherContext = mock(GraphQLDataFetcherContext.class); + when(dataFetcherContext.getBlockchainQuery()).thenReturn(blockchainQueries); + when(dataFetcherContext.getMiningCoordinator()).thenReturn(miningCoordinatorMock); + + when(dataFetcherContext.getTransactionPool()).thenReturn(mock(TransactionPool.class)); + when(dataFetcherContext.getSynchronizer()).thenReturn(synchronizer); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + final GraphQLDataFetchers dataFetchers = new GraphQLDataFetchers(supportedCapabilities); + final GraphQL graphQL = GraphQLProvider.buildGraphQL(dataFetchers); + + return new GraphQLRpcHttpService( + vertx, folder.newFolder().toPath(), graphQLRpcConfig, graphQL, dataFetcherContext); + } + + private static GraphQLRpcConfiguration createGraphQLRpcConfig() { + final GraphQLRpcConfiguration config = GraphQLRpcConfiguration.createDefault(); + config.setPort(0); + return config; + } + + @After + public void shutdownServer() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + service.stop().join(); + vertx.close(); + } + + @Test + public void requestWithDefaultHeaderAndDefaultConfigIsAccepted() throws IOException { + assertThat(doRequest("localhost:50012")).isEqualTo(200); + } + + @Test + public void requestWithEmptyHeaderAndDefaultConfigIsRejected() throws IOException { + assertThat(doRequest("")).isEqualTo(403); + } + + @Test + public void requestWithAnyHostnameAndWildcardConfigIsAccepted() throws IOException { + graphQLRpcConfig.setHostsWhitelist(Collections.singletonList("*")); + assertThat(doRequest("ally")).isEqualTo(200); + assertThat(doRequest("foe")).isEqualTo(200); + } + + @Test + public void requestWithWhitelistedHostIsAccepted() throws IOException { + graphQLRpcConfig.setHostsWhitelist(hostsWhitelist); + assertThat(doRequest("ally")).isEqualTo(200); + assertThat(doRequest("ally:12345")).isEqualTo(200); + assertThat(doRequest("friend")).isEqualTo(200); + } + + @Test + public void requestWithUnknownHostIsRejected() throws IOException { + graphQLRpcConfig.setHostsWhitelist(hostsWhitelist); + assertThat(doRequest("foe")).isEqualTo(403); + } + + private int doRequest(final String hostname) throws IOException { + final RequestBody body = RequestBody.create(JSON, "{protocolVersion}"); + + final Request build = + new Request.Builder() + .post(body) + .url(baseUrl + "/graphql") + .addHeader("Host", hostname) + .build(); + return client.newCall(build).execute().code(); + } + + @Test + public void requestWithMalformedHostIsRejected() throws IOException { + graphQLRpcConfig.setHostsWhitelist(hostsWhitelist); + assertThat(doRequest("ally:friend")).isEqualTo(403); + assertThat(doRequest("ally:123456")).isEqualTo(403); + assertThat(doRequest("ally:friend:1234")).isEqualTo(403); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceTest.java new file mode 100644 index 0000000000..dc3cd2a59f --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcHttpServiceTest.java @@ -0,0 +1,318 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import tech.pegasys.pantheon.ethereum.blockcreation.EthHashMiningCoordinator; +import tech.pegasys.pantheon.ethereum.core.Hash; +import tech.pegasys.pantheon.ethereum.core.Synchronizer; +import tech.pegasys.pantheon.ethereum.core.Wei; +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.p2p.wire.Capability; +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import java.net.InetSocketAddress; +import java.net.URL; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import graphql.GraphQL; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; + +public class GraphQLRpcHttpServiceTest { + + @ClassRule public static final TemporaryFolder folder = new TemporaryFolder(); + + private static final Vertx vertx = Vertx.vertx(); + + private static GraphQLRpcHttpService service; + private static OkHttpClient client; + private static String baseUrl; + protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + private static BlockchainQuery blockchainQueries; + private static Synchronizer synchronizer; + private static GraphQL graphQL; + private static GraphQLDataFetchers dataFetchers; + private static GraphQLDataFetcherContext dataFetcherContext; + private static EthHashMiningCoordinator miningCoordinatorMock; + + private final GraphQLRpcTestHelper testHelper = new GraphQLRpcTestHelper(); + + @BeforeClass + public static void initServerAndClient() throws Exception { + blockchainQueries = mock(BlockchainQuery.class); + synchronizer = mock(Synchronizer.class); + graphQL = mock(GraphQL.class); + + miningCoordinatorMock = mock(EthHashMiningCoordinator.class); + + dataFetcherContext = mock(GraphQLDataFetcherContext.class); + when(dataFetcherContext.getBlockchainQuery()).thenReturn(blockchainQueries); + when(dataFetcherContext.getMiningCoordinator()).thenReturn(miningCoordinatorMock); + + when(dataFetcherContext.getTransactionPool()).thenReturn(mock(TransactionPool.class)); + when(dataFetcherContext.getSynchronizer()).thenReturn(synchronizer); + + final Set supportedCapabilities = new HashSet<>(); + supportedCapabilities.add(EthProtocol.ETH62); + supportedCapabilities.add(EthProtocol.ETH63); + dataFetchers = new GraphQLDataFetchers(supportedCapabilities); + graphQL = GraphQLProvider.buildGraphQL(dataFetchers); + service = createGraphQLRpcHttpService(); + service.start().join(); + // Build an OkHttp client. + client = new OkHttpClient(); + baseUrl = service.url() + "/graphql/"; + } + + private static GraphQLRpcHttpService createGraphQLRpcHttpService( + final GraphQLRpcConfiguration config) throws Exception { + return new GraphQLRpcHttpService( + vertx, folder.newFolder().toPath(), config, graphQL, dataFetcherContext); + } + + private static GraphQLRpcHttpService createGraphQLRpcHttpService() throws Exception { + return new GraphQLRpcHttpService( + vertx, folder.newFolder().toPath(), createGraphQLRpcConfig(), graphQL, dataFetcherContext); + } + + private static GraphQLRpcConfiguration createGraphQLRpcConfig() { + final GraphQLRpcConfiguration config = GraphQLRpcConfiguration.createDefault(); + config.setPort(0); + return config; + } + + @BeforeClass + public static void setupConstants() { + + final URL blocksUrl = + GraphQLRpcHttpServiceTest.class + .getClassLoader() + .getResource( + "tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestBlockchain.blocks"); + + final URL genesisJsonUrl = + GraphQLRpcHttpServiceTest.class + .getClassLoader() + .getResource("tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestGenesis.json"); + + assertThat(blocksUrl).isNotNull(); + assertThat(genesisJsonUrl).isNotNull(); + } + + /** Tears down the HTTP server. */ + @AfterClass + public static void shutdownServer() { + client.dispatcher().executorService().shutdown(); + client.connectionPool().evictAll(); + service.stop().join(); + vertx.close(); + } + + @Test + public void invalidCallToStart() { + service + .start() + .whenComplete( + (unused, exception) -> assertThat(exception).isInstanceOf(IllegalStateException.class)); + } + + @Test + public void http404() throws Exception { + try (final Response resp = client.newCall(buildGetRequest("/foo")).execute()) { + assertThat(resp.code()).isEqualTo(404); + } + } + + @Test + public void handleEmptyRequest() throws Exception { + try (final Response resp = + client.newCall(new Request.Builder().get().url(service.url()).build()).execute()) { + assertThat(resp.code()).isEqualTo(201); + } + } + + @Test + public void handleInvalidQuerySchema() throws Exception { + final RequestBody body = RequestBody.create(JSON, "{gasPrice1}"); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + // assertThat(resp.code()).isEqualTo(200); // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLRpcError(json); // Check result final + } + } + + @Test + public void getGasprice() throws Exception { + final RequestBody body = RequestBody.create(JSON, "{gasPrice}"); + final Wei price = Wei.of(16); + when(miningCoordinatorMock.getMinTransactionGasPrice()).thenReturn(price); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); // Check general format of result + final JsonObject json = new JsonObject(resp.body().string()); + testHelper.assertValidGraphQLRpcResult(json); + final String result = json.getJsonObject("data").getString("gasPrice"); + assertThat(result).isEqualTo("0x10"); + } + } + + @Test + public void getSocketAddressWhenActive() { + final InetSocketAddress socketAddress = service.socketAddress(); + assertThat("127.0.0.1").isEqualTo(socketAddress.getAddress().getHostAddress()); + assertThat(socketAddress.getPort() > 0).isTrue(); + } + + @Test + public void getSocketAddressWhenStoppedIsEmpty() throws Exception { + final GraphQLRpcHttpService service = createGraphQLRpcHttpService(); + + final InetSocketAddress socketAddress = service.socketAddress(); + assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress()); + assertThat(0).isEqualTo(socketAddress.getPort()); + assertThat("").isEqualTo(service.url()); + } + + @Test + public void getSocketAddressWhenBindingToAllInterfaces() throws Exception { + final GraphQLRpcConfiguration config = createGraphQLRpcConfig(); + config.setHost("0.0.0.0"); + final GraphQLRpcHttpService service = createGraphQLRpcHttpService(config); + service.start().join(); + + try { + final InetSocketAddress socketAddress = service.socketAddress(); + assertThat("0.0.0.0").isEqualTo(socketAddress.getAddress().getHostAddress()); + assertThat(socketAddress.getPort() > 0).isTrue(); + assertThat(!service.url().contains("0.0.0.0")).isTrue(); + } finally { + service.stop().join(); + } + } + + @Test + public void responseContainsJsonContentTypeHeader() throws Exception { + + final RequestBody body = RequestBody.create(JSON, "{gasPrice}"); + + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.header("Content-Type")).isEqualTo("application/json"); + } + } + + @Test + public void ethGetUncleCountByBlockHash() throws Exception { + final int uncleCount = 4; + final Hash blockHash = Hash.hash(BytesValue.of(1)); + @SuppressWarnings("unchecked") + final BlockWithMetadata block = mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final List list = mock(List.class); + + when(blockchainQueries.blockByHash(eq(blockHash))).thenReturn(Optional.of(block)); + when(block.getOmmers()).thenReturn(list); + when(list.size()).thenReturn(uncleCount); + + final String query = "{block(hash:\"" + blockHash.toString() + "\") {ommerCount}}"; + + final RequestBody body = RequestBody.create(JSON, query); + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final String jsonStr = resp.body().string(); + final JsonObject json = new JsonObject(jsonStr); + testHelper.assertValidGraphQLRpcResult(json); + final int result = json.getJsonObject("data").getJsonObject("block").getInteger("ommerCount"); + assertThat(result).isEqualTo(uncleCount); + } + } + + @Test + public void ethGetUncleCountByBlockNumber() throws Exception { + final int uncleCount = 5; + @SuppressWarnings("unchecked") + final BlockWithMetadata block = mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final List list = mock(List.class); + when(blockchainQueries.blockByNumber(anyLong())).thenReturn(Optional.of(block)); + when(block.getOmmers()).thenReturn(list); + when(list.size()).thenReturn(uncleCount); + + final String query = "{block(number:\"3\") {ommerCount}}"; + + final RequestBody body = RequestBody.create(JSON, query); + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final String jsonStr = resp.body().string(); + final JsonObject json = new JsonObject(jsonStr); + testHelper.assertValidGraphQLRpcResult(json); + final int result = json.getJsonObject("data").getJsonObject("block").getInteger("ommerCount"); + assertThat(result).isEqualTo(uncleCount); + } + } + + @Test + public void ethGetUncleCountByBlockLatest() throws Exception { + final int uncleCount = 5; + @SuppressWarnings("unchecked") + final BlockWithMetadata block = mock(BlockWithMetadata.class); + @SuppressWarnings("unchecked") + final List list = mock(List.class); + when(blockchainQueries.latestBlock()).thenReturn(Optional.of(block)); + when(block.getOmmers()).thenReturn(list); + when(list.size()).thenReturn(uncleCount); + + final String query = "{block {ommerCount}}"; + + final RequestBody body = RequestBody.create(JSON, query); + try (final Response resp = client.newCall(buildPostRequest(body)).execute()) { + assertThat(resp.code()).isEqualTo(200); + final String jsonStr = resp.body().string(); + final JsonObject json = new JsonObject(jsonStr); + testHelper.assertValidGraphQLRpcResult(json); + final int result = json.getJsonObject("data").getJsonObject("block").getInteger("ommerCount"); + assertThat(result).isEqualTo(uncleCount); + } + } + + private Request buildPostRequest(final RequestBody body) { + return new Request.Builder().post(body).url(baseUrl).build(); + } + + private Request buildGetRequest(final String path) { + return new Request.Builder().get().url(baseUrl + path).build(); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcTestHelper.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcTestHelper.java new file mode 100644 index 0000000000..01286b919d --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/GraphQLRpcTestHelper.java @@ -0,0 +1,36 @@ +/* + * 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. + */ +package tech.pegasys.pantheon.ethereum.graphqlrpc; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Set; + +import io.vertx.core.json.JsonObject; + +class GraphQLRpcTestHelper { + + void assertValidGraphQLRpcResult(final JsonObject json) { + // Check all expected fieldnames are set + final Set fieldNames = json.fieldNames(); + assertThat(fieldNames.size()).isEqualTo(1); + assertThat(fieldNames.contains("data")).isTrue(); + } + + void assertValidGraphQLRpcError(final JsonObject json) { + // Check all expected fieldnames are set + final Set fieldNames = json.fieldNames(); + assertThat(fieldNames.size()).isEqualTo(1); + assertThat(fieldNames.contains("errors")).isTrue(); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalarTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalarTest.java new file mode 100644 index 0000000000..84bfcec6b8 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/AddressScalarTest.java @@ -0,0 +1,91 @@ +/* + * 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.internal.scalar; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.ethereum.core.Address; + +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class AddressScalarTest { + + private AddressScalar scalar; + @Rule public ExpectedException thrown = ExpectedException.none(); + + private final String addrStr = "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f"; + private final String invalidAddrStr = "0x295ee1b4f6dd65047762f924ecd367c17eabf8f"; + private final Address addr = Address.fromHexString(addrStr); + private final StringValue addrValue = StringValue.newStringValue(addrStr).build(); + private final StringValue invalidAddrValue = StringValue.newStringValue(invalidAddrStr).build(); + + @Test + public void pareValueTest() { + final String result = (String) scalar.getCoercing().parseValue(addr); + assertThat(result).isEqualTo(addrStr); + } + + @Test + public void pareValueErrorTest() { + + thrown.expect(CoercingParseValueException.class); + scalar.getCoercing().parseValue(addrStr); + } + + @Test + public void serializeTest() { + + final String result = (String) scalar.getCoercing().serialize(addr); + assertThat(result).isEqualTo(addrStr); + } + + @Test + public void serializeErrorTest() { + + thrown.expect(CoercingSerializeException.class); + scalar.getCoercing().serialize(addrStr); + } + + @Test + public void pareLiteralTest() { + final Address result = (Address) scalar.getCoercing().parseLiteral(addrValue); + assertThat(result).isEqualTo(addr); + } + + @Test + public void pareLiteralErrorTest() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(addrStr); + } + + @Test + public void pareLiteralErrorTest2() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(invalidAddrValue); + } + + @Before + public void before() { + scalar = new AddressScalar(); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalarTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalarTest.java new file mode 100644 index 0000000000..a1a176d6f5 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BigIntScalarTest.java @@ -0,0 +1,87 @@ +/* + * 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.internal.scalar; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.util.uint.UInt256; + +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BigIntScalarTest { + + private BigIntScalar scalar; + @Rule public ExpectedException thrown = ExpectedException.none(); + + private final String str = "0x10"; + private final UInt256 value = UInt256.fromHexString(str); + private final StringValue strValue = StringValue.newStringValue(str).build(); + private final StringValue invalidStrValue = StringValue.newStringValue("0xgh").build(); + + @Test + public void pareValueTest() { + final String result = (String) scalar.getCoercing().parseValue(value); + assertThat(result).isEqualTo(str); + } + + @Test + public void pareValueErrorTest() { + thrown.expect(CoercingParseValueException.class); + scalar.getCoercing().parseValue(str); + } + + @Test + public void serializeTest() { + final String result = (String) scalar.getCoercing().serialize(value); + assertThat(result).isEqualTo(str); + } + + @Test + public void serializeErrorTest() { + thrown.expect(CoercingSerializeException.class); + scalar.getCoercing().serialize(str); + } + + @Test + public void pareLiteralTest() { + final UInt256 result = (UInt256) scalar.getCoercing().parseLiteral(strValue); + assertThat(result).isEqualTo(value); + } + + @Test + public void pareLiteralErrorTest() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(str); + } + + @Test + public void pareLiteralErrorTest2() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(invalidStrValue); + } + + @Before + public void before() { + scalar = new BigIntScalar(); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32ScalarTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32ScalarTest.java new file mode 100644 index 0000000000..d60dfd53bc --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/Bytes32ScalarTest.java @@ -0,0 +1,87 @@ +/* + * 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.internal.scalar; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.util.bytes.Bytes32; + +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class Bytes32ScalarTest { + + private Bytes32Scalar scalar; + @Rule public ExpectedException thrown = ExpectedException.none(); + + private final String str = "0x1234567812345678123456781234567812345678123456781234567812345678"; + private final Bytes32 value = Bytes32.fromHexString(str); + private final StringValue strValue = StringValue.newStringValue(str).build(); + private final StringValue invalidStrValue = StringValue.newStringValue("0xgh").build(); + + @Test + public void pareValueTest() { + final String result = (String) scalar.getCoercing().parseValue(value); + assertThat(result).isEqualTo(str); + } + + @Test + public void pareValueErrorTest() { + thrown.expect(CoercingParseValueException.class); + scalar.getCoercing().parseValue(str); + } + + @Test + public void serializeTest() { + final String result = (String) scalar.getCoercing().serialize(value); + assertThat(result).isEqualTo(str); + } + + @Test + public void serializeErrorTest() { + thrown.expect(CoercingSerializeException.class); + scalar.getCoercing().serialize(str); + } + + @Test + public void pareLiteralTest() { + final Bytes32 result = (Bytes32) scalar.getCoercing().parseLiteral(strValue); + assertThat(result).isEqualTo(value); + } + + @Test + public void pareLiteralErrorTest() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(str); + } + + @Test + public void pareLiteralErrorTest2() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(invalidStrValue); + } + + @Before + public void before() { + scalar = new Bytes32Scalar(); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalarTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalarTest.java new file mode 100644 index 0000000000..19892117c6 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/BytesScalarTest.java @@ -0,0 +1,87 @@ +/* + * 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.internal.scalar; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.util.bytes.BytesValue; + +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BytesScalarTest { + + private BytesScalar scalar; + @Rule public ExpectedException thrown = ExpectedException.none(); + + private final String str = "0x10"; + private final BytesValue value = BytesValue.fromHexString(str); + private final StringValue strValue = StringValue.newStringValue(str).build(); + private final StringValue invalidStrValue = StringValue.newStringValue("0xgh").build(); + + @Test + public void pareValueTest() { + final String result = (String) scalar.getCoercing().parseValue(value); + assertThat(result).isEqualTo(str); + } + + @Test + public void pareValueErrorTest() { + thrown.expect(CoercingParseValueException.class); + scalar.getCoercing().parseValue(str); + } + + @Test + public void serializeTest() { + final String result = (String) scalar.getCoercing().serialize(value); + assertThat(result).isEqualTo(str); + } + + @Test + public void serializeErrorTest() { + thrown.expect(CoercingSerializeException.class); + scalar.getCoercing().serialize(str); + } + + @Test + public void pareLiteralTest() { + final BytesValue result = (BytesValue) scalar.getCoercing().parseLiteral(strValue); + assertThat(result).isEqualTo(value); + } + + @Test + public void pareLiteralErrorTest() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(str); + } + + @Test + public void pareLiteralErrorTest2() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(invalidStrValue); + } + + @Before + public void before() { + scalar = new BytesScalar(); + } +} diff --git a/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalarTest.java b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalarTest.java new file mode 100644 index 0000000000..b01abf11e6 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/scalar/LongScalarTest.java @@ -0,0 +1,93 @@ +/* + * 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.internal.scalar; + +import static org.assertj.core.api.Assertions.assertThat; + +import graphql.language.StringValue; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class LongScalarTest { + + private LongScalar scalar; + @Rule public ExpectedException thrown = ExpectedException.none(); + + private final String str = "0xf4240"; + private final Long value = Long.decode(str); + private final StringValue strValue = StringValue.newStringValue(str).build(); + private final StringValue invalidStrValue = StringValue.newStringValue("gh").build(); + + @Test + public void parseLongValueTest() { + assertThat(scalar.getCoercing().parseValue(value)).isEqualTo(value); + } + + @Test + public void parseStringValueTest() { + assertThat(scalar.getCoercing().parseValue(str)).isEqualTo(value); + } + + @Test + public void pareValueErrorTest() { + thrown.expect(CoercingParseValueException.class); + scalar.getCoercing().parseValue(invalidStrValue); + } + + @Test + public void serializeLongTest() { + assertThat(scalar.getCoercing().serialize(value)).isEqualTo(value); + } + + @Test + public void serializeStringTest() { + assertThat(scalar.getCoercing().serialize(str)).isEqualTo(value); + } + + @Test + public void serializeErrorTest() { + thrown.expect(CoercingSerializeException.class); + scalar.getCoercing().serialize(invalidStrValue); + } + + @Test + public void pareLiteralTest() { + final Long result = (Long) scalar.getCoercing().parseLiteral(strValue); + assertThat(result).isEqualTo(value); + } + + @Test + public void pareLiteralErrorTest() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(str); + } + + @Test + public void pareLiteralErrorTest2() { + thrown.expect(CoercingParseLiteralException.class); + scalar.getCoercing().parseLiteral(invalidStrValue); + } + + @Before + public void before() { + scalar = new LongScalar(); + } +} diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_blockNumber.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_blockNumber.json new file mode 100644 index 0000000000..b3b7361850 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_blockNumber.json @@ -0,0 +1,13 @@ +{ + "request": + "{ block { number } }", + + "response": { + "data" : { + "block" : { + "number" : 32 + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_Block8.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_Block8.json new file mode 100644 index 0000000000..c2d0014b30 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_Block8.json @@ -0,0 +1,16 @@ +{ + "request": "{block(number :\"0x8\") {number call (data : {from : \"a94f5374fce5edbc8e2a8697c15331677e6ebf0b\", to: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\", data :\"0x12a7b914\"}){data status}}}" + , + "response":{ + "data" : { + "block" : { + "number" : 8, + "call" : { + "data" : "0x0000000000000000000000000000000000000000000000000000000000000000", + "status" : 1 + } + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_BlockLatest.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_BlockLatest.json new file mode 100644 index 0000000000..129811c38f --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_call_BlockLatest.json @@ -0,0 +1,16 @@ +{ + "request": "{block {number call (data : {from : \"a94f5374fce5edbc8e2a8697c15331677e6ebf0b\", to: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\", data :\"0x12a7b914\"}){data status}}}" + , + "response":{ + "data" : { + "block" : { + "number" : 32, + "call" : { + "data" : "0x0000000000000000000000000000000000000000000000000000000000000001", + "status" : 1 + } + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_contractDeploy.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_contractDeploy.json new file mode 100644 index 0000000000..a97ae25867 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_contractDeploy.json @@ -0,0 +1,12 @@ +{ + + "request" :"{block{estimateGas (data: {from :\"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\", data :\"0x608060405234801561001057600080fd5b50610157806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bdab8bf146100515780639ae97baa14610068575b600080fd5b34801561005d57600080fd5b5061006661007f565b005b34801561007457600080fd5b5061007d6100b9565b005b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60016040518082815260200191505060405180910390a1565b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60026040518082815260200191505060405180910390a17fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60036040518082815260200191505060405180910390a15600a165627a7a7230582010ddaa52e73a98c06dbcd22b234b97206c1d7ed64a7c048e10c2043a3d2309cb0029\"})}}", + "response":{ + "data" : { + "block" : { + "estimateGas" : 111953 + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_noParams.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_noParams.json new file mode 100644 index 0000000000..251bd7ebe5 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_noParams.json @@ -0,0 +1,12 @@ +{ + "request" :"{block{ estimateGas(data:{}) }}", + "response":{ + "data" : { + "block" : { + "estimateGas" : 21000 + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_transfer.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_transfer.json new file mode 100644 index 0000000000..7e8b72f7a1 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_estimateGas_transfer.json @@ -0,0 +1,11 @@ +{ + "request" :"{block{estimateGas (data: {from :\"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\", to :\"0x8888f1f195afa192cfee860698584c030f4c9db1\"})}}", + "response":{ + "data" : { + "block" : { + "estimateGas" : 21000 + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_gasPrice.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_gasPrice.json new file mode 100644 index 0000000000..c4d83011a0 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_gasPrice.json @@ -0,0 +1,11 @@ +{ + "request": + "{ gasPrice }", + + "response": { + "data" : { + "gasPrice" : "0x10" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_0x19.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_0x19.json new file mode 100644 index 0000000000..0fb6139a4c --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_0x19.json @@ -0,0 +1,12 @@ +{ + "request": "{account(blockNumber:\"0x19\", address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { balance } }", + "response": { + "data" : { + "account" : { + "balance" : "0xfa" + } + } + }, + "statusCode": 200 +} + diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_latest.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_latest.json new file mode 100644 index 0000000000..2ff04665d8 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_latest.json @@ -0,0 +1,12 @@ +{ + "request": "{account(address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { balance } }", + "response": { + "data" : { + "account" : { + "balance" : "0x140" + } + } + }, + "statusCode": 200 +} + diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_toobig_bn.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_toobig_bn.json new file mode 100644 index 0000000000..048f077593 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_toobig_bn.json @@ -0,0 +1,20 @@ +{ + "request": "{account(blockNumber:\"0x21\", address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { balance } }", + "response": { + "data" : null, + "errors" : [ { + "message" : "Exception while fetching data (/account) : Invalid params", + "locations" : [ { + "line" : 1, + "column" : 2 + } ], + "path" : [ "account" ], + "extensions" : { + "errorCode" : -32602, + "errorMessage" : "Invalid params" + } + } ] + }, + "statusCode": 400 +} + diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_without_addr.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_without_addr.json new file mode 100644 index 0000000000..c47196ffc8 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBalance_without_addr.json @@ -0,0 +1,5 @@ +{ + "request": "{account(address: \"0x8895ee1b4f6dd65047762f924ecd367c17eabf8f\") { balance } }", + "statusCode": 400 +} + diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByHash.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByHash.json new file mode 100644 index 0000000000..bfaef823d6 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByHash.json @@ -0,0 +1,34 @@ +{ + "request": + + "{block (hash : \"0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6\") {number transactions{hash} timestamp difficulty totalDifficulty gasUsed gasLimit hash nonce ommerCount logsBloom mixHash ommerHash extraData stateRoot receiptsRoot transactionCount transactionsRoot}} ", + + + "response": { + "data" : { + "block" : { + "number" : 30, + "transactions" : [ { + "hash" : "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4" + } ], + "timestamp" : "0x561bc336", + "difficulty" : "0x20740", + "totalDifficulty" : "0x3e6cc0", + "gasUsed" : 23585, + "gasLimit" : 3141592, + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6", + "nonce" : "0x5c321bd9e9f040f1", + "ommerCount" : 0, + "logsBloom" : "0x00000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000080000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000400000000000000000200000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000800000000040000000000000000000000000000000000000000010000000000000000000000000", + "mixHash" : "0x6ce1c4afb4f85fefd1b0ed966b20cd248f08d9a5b0df773f75c6c2f5cc237b7c", + "ommerHash" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "extraData" : "0x", + "stateRoot" : "0xdb46d6bb168130fe2cb60b4b24346137b5741f11283e0d7edace65c5f5466b2e", + "receiptsRoot" : "0x88b3b304b058b39791c26fdb94a05cc16ce67cf8f84f7348cb3c60c0ff342d0d", + "transactionCount" : 1, + "transactionsRoot" : "0x5a8d5d966b48e1331ae19eb459eb28882cdc7654e615d37774b79204e875dc01" + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByNumber.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByNumber.json new file mode 100644 index 0000000000..2c85fdbce0 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockByNumber.json @@ -0,0 +1,44 @@ +{ + "request": + + "{block (number : \"0x1e\") {transactions{hash} timestamp difficulty totalDifficulty gasUsed gasLimit hash nonce ommerCount logsBloom mixHash ommerHash extraData stateRoot receiptsRoot transactionCount transactionsRoot ommers{hash} ommerAt(index : 1){hash} miner{address} account(address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\"){balance} parent{hash} }} ", + + + "response":{ + "data" : { + "block" : { + "transactions" : [ { + "hash" : "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4" + } ], + "timestamp" : "0x561bc336", + "difficulty" : "0x20740", + "totalDifficulty" : "0x3e6cc0", + "gasUsed" : 23585, + "gasLimit" : 3141592, + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6", + "nonce" : "0x5c321bd9e9f040f1", + "ommerCount" : 0, + "logsBloom" : "0x00000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000080000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000400000000000000000200000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000800000000040000000000000000000000000000000000000000010000000000000000000000000", + "mixHash" : "0x6ce1c4afb4f85fefd1b0ed966b20cd248f08d9a5b0df773f75c6c2f5cc237b7c", + "ommerHash" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "extraData" : "0x", + "stateRoot" : "0xdb46d6bb168130fe2cb60b4b24346137b5741f11283e0d7edace65c5f5466b2e", + "receiptsRoot" : "0x88b3b304b058b39791c26fdb94a05cc16ce67cf8f84f7348cb3c60c0ff342d0d", + "transactionCount" : 1, + "transactionsRoot" : "0x5a8d5d966b48e1331ae19eb459eb28882cdc7654e615d37774b79204e875dc01", + "ommers" : [ ], + "ommerAt" : null, + "miner" : { + "address" : "0x8888f1f195afa192cfee860698584c030f4c9db1" + }, + "account" : { + "balance" : "0x12c" + }, + "parent" : { + "hash" : "0xf8cfa377bd766cdf22edb388dd08cc149e85d24f2796678c835f3c54ab930803" + } + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByHash.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByHash.json new file mode 100644 index 0000000000..9ee5053e36 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByHash.json @@ -0,0 +1,15 @@ +{ + "request": + + "{block (hash : \"0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6\") {transactionCount}} ", + + + "response": { + "data" : { + "block" : { + "transactionCount" : 1 + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByNumber.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByNumber.json new file mode 100644 index 0000000000..db2a5684ee --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockTransactionCountByNumber.json @@ -0,0 +1,33 @@ +{ + "request": + + "{block (number : \"0x1e\") {transactions{hash} timestamp difficulty totalDifficulty gasUsed gasLimit hash nonce ommerCount logsBloom mixHash ommerHash extraData stateRoot receiptsRoot transactionCount transactionsRoot}} ", + + + "response": { + "data" : { + "block" : { + "transactions" : [ { + "hash" : "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4" + } ], + "timestamp" : "0x561bc336", + "difficulty" : "0x20740", + "totalDifficulty" : "0x3e6cc0", + "gasUsed" : 23585, + "gasLimit" : 3141592, + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6", + "nonce" : "0x5c321bd9e9f040f1", + "ommerCount" : 0, + "logsBloom" : "0x00000000000000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000080000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000400000000000000000200000000000000000002000000000000000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000800000000040000000000000000000000000000000000000000010000000000000000000000000", + "mixHash" : "0x6ce1c4afb4f85fefd1b0ed966b20cd248f08d9a5b0df773f75c6c2f5cc237b7c", + "ommerHash" : "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "extraData" : "0x", + "stateRoot" : "0xdb46d6bb168130fe2cb60b4b24346137b5741f11283e0d7edace65c5f5466b2e", + "receiptsRoot" : "0x88b3b304b058b39791c26fdb94a05cc16ce67cf8f84f7348cb3c60c0ff342d0d", + "transactionCount" : 1, + "transactionsRoot" : "0x5a8d5d966b48e1331ae19eb459eb28882cdc7654e615d37774b79204e875dc01" + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockWrongParams.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockWrongParams.json new file mode 100644 index 0000000000..01c8691816 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlockWrongParams.json @@ -0,0 +1,7 @@ +{ + "request": + + "{block (number: \"0x03\", hash : \"0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6\") {number transactions{hash} timestamp difficulty totalDifficulty gasUsed gasLimit hash nonce ommerCount logsBloom mixHash ommerHash extraData stateRoot receiptsRoot transactionCount transactionsRoot}} ", + + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByRange.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByRange.json new file mode 100644 index 0000000000..10f3e10022 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByRange.json @@ -0,0 +1,40 @@ +{ + "request": + + "{blocks (from : \"0x1e\", to: \"0x20\") { number gasUsed gasLimit hash nonce stateRoot receiptsRoot transactionCount }} ", + + + "response":{ + "data" : { + "blocks" : [ { + "number" : 30, + "gasUsed" : 23585, + "gasLimit" : 3141592, + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6", + "nonce" : "0x5c321bd9e9f040f1", + "stateRoot" : "0xdb46d6bb168130fe2cb60b4b24346137b5741f11283e0d7edace65c5f5466b2e", + "receiptsRoot" : "0x88b3b304b058b39791c26fdb94a05cc16ce67cf8f84f7348cb3c60c0ff342d0d", + "transactionCount" : 1 + }, { + "number" : 31, + "gasUsed" : 24303, + "gasLimit" : 3141592, + "hash" : "0x0f765087745aa259d9e5ac39c367c57432a16ed98e3b0d81c5b51d10f301dc49", + "nonce" : "0xd3a27a3001616468", + "stateRoot" : "0xa80997cf804269d64f2479baf535cf8f9090b70fbf515741c6995564f1e678bd", + "receiptsRoot" : "0x2440c44a3f75ad8b0425a73e7be2f61a5171112465cfd14e62e735b56d7178e6", + "transactionCount" : 1 + }, { + "number" : 32, + "gasUsed" : 23705, + "gasLimit" : 3141592, + "hash" : "0x71d59849ddd98543bdfbe8548f5eed559b07b8aaf196369f39134500eab68e53", + "nonce" : "0xdb063000b00e8026", + "stateRoot" : "0xf65f3dd13f72f5fa5607a5224691419969b4f4bae7a00a6cdb853f2ca9eeb1be", + "receiptsRoot" : "0xa50a7e67e833f4502524371ee462ccbcc6c6cabd2aeb1555c56150007a53183c", + "transactionCount" : 1 + } ] + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByWrongRange.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByWrongRange.json new file mode 100644 index 0000000000..99e028e747 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getBlocksByWrongRange.json @@ -0,0 +1,15 @@ +{ + "request": + "{blocks (from : \"0x1e\", to: \"0x1c\") { number gasUsed gasLimit hash nonce stateRoot receiptsRoot transactionCount }} ", + + "response": { + "data":null, + "errors": + [{"message":"Exception while fetching data (/blocks) : Invalid params", + "locations":[{"line":1,"column":2}],"path":["blocks"], + "extensions":{"errorCode":-32602,"errorMessage":"Invalid params"}}] + }, + + "statusCode": 400 + +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode.json new file mode 100644 index 0000000000..94ab5411b4 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode.json @@ -0,0 +1,13 @@ +{ + "request" : "{ account(address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { code } }", + + "response": { + "data" : { + "account" :{ + "code" :"0x6000357c010000000000000000000000000000000000000000000000000000000090048063102accc11461012c57806312a7b9141461013a5780631774e6461461014c5780631e26fd331461015d5780631f9030371461016e578063343a875d1461018057806338cc4831146101955780634e7ad367146101bd57806357cb2fc4146101cb57806365538c73146101e057806368895979146101ee57806376bc21d9146102005780639a19a9531461020e5780639dc2c8f51461021f578063a53b1c1e1461022d578063a67808571461023e578063b61c05031461024c578063c2b12a731461025a578063d2282dc51461026b578063e30081a01461027c578063e8beef5b1461028d578063f38b06001461029b578063f5b53e17146102a9578063fd408767146102bb57005b6101346104d6565b60006000f35b61014261039b565b8060005260206000f35b610157600435610326565b60006000f35b6101686004356102c9565b60006000f35b610176610442565b8060005260206000f35b6101886103d3565b8060ff1660005260206000f35b61019d610413565b8073ffffffffffffffffffffffffffffffffffffffff1660005260206000f35b6101c56104c5565b60006000f35b6101d36103b7565b8060000b60005260206000f35b6101e8610454565b60006000f35b6101f6610401565b8060005260206000f35b61020861051f565b60006000f35b6102196004356102e5565b60006000f35b610227610693565b60006000f35b610238600435610342565b60006000f35b610246610484565b60006000f35b610254610493565b60006000f35b61026560043561038d565b60006000f35b610276600435610350565b60006000f35b61028760043561035e565b60006000f35b6102956105b4565b60006000f35b6102a3610547565b60006000f35b6102b16103ef565b8060005260206000f35b6102c3610600565b60006000f35b80600060006101000a81548160ff021916908302179055505b50565b80600060016101000a81548160ff02191690837f01000000000000000000000000000000000000000000000000000000000000009081020402179055505b50565b80600060026101000a81548160ff021916908302179055505b50565b806001600050819055505b50565b806002600050819055505b50565b80600360006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908302179055505b50565b806004600050819055505b50565b6000600060009054906101000a900460ff1690506103b4565b90565b6000600060019054906101000a900460000b90506103d0565b90565b6000600060029054906101000a900460ff1690506103ec565b90565b600060016000505490506103fe565b90565b60006002600050549050610410565b90565b6000600360009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905061043f565b90565b60006004600050549050610451565b90565b7f65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be5806000602a81526020016000a15b565b6000602a81526020016000a05b565b60017f81933b308056e7e85668661dcd102b1f22795b4431f9cf4625794f381c271c6b6000602a81526020016000a25b565b60016000602a81526020016000a15b565b3373ffffffffffffffffffffffffffffffffffffffff1660017f0e216b62efbb97e751a2ce09f607048751720397ecfb9eef1e48a6644948985b6000602a81526020016000a35b565b3373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a25b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017f317b31292193c2a4f561cc40a95ea0d97a2733f14af6d6d59522473e1f3ae65f6000602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660016000602a81526020016000a35b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff1660017fd5f0a30e4be0c6be577a71eceb7464245a796a7e6a55c0d971837b250de05f4e60007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a45b565b7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff6001023373ffffffffffffffffffffffffffffffffffffffff16600160007fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe98152602001602a81526020016000a35b56" + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode_noCode.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode_noCode.json new file mode 100644 index 0000000000..6dc3568190 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getCode_noCode.json @@ -0,0 +1,14 @@ +{ + "request" : "{ account(address: \"0x8888f1f195afa192cfee860698584c030f4c9db1\") { code } }", + + "response": { + "data" : { + "account" :{ + "code" :"0x" + } + } + }, + + "statusCode": 200 + +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getLogs_matchTopic.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getLogs_matchTopic.json new file mode 100644 index 0000000000..46f79ccd75 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getLogs_matchTopic.json @@ -0,0 +1,22 @@ +{ + "request": "{ block(number: \"0x17\") { logs( filter: { topics : [[\"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b\", \"0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580\"]]}) { index topics data account{address} transaction{hash} } } }", + "response": { + "data" : { + "block" : { + "logs" : [ { + "index" : 0, + "topics" : [ "0x65c9ac8011e286e89d02a269890f41d67ca2cc597b2c76c7c69321ff492be580" ], + "data" : "0x000000000000000000000000000000000000000000000000000000000000002a", + "account" : { + "address" : "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f" + }, + "transaction" : { + "hash" : "0x97a385bf570ced7821c6495b3877ddd2afd5c452f350f0d4876e98d9161389c6" + } + } ] + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt.json new file mode 100644 index 0000000000..aba1ecb173 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt.json @@ -0,0 +1,13 @@ +{ + "request" :"{ account(address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { storage(slot: \"0x00000000000000000000000000000004\") } }", + + "response": { + "data" : { + "account" :{ + "storage" :"0xaabbccffffffffffffffffffffffffffffffffffffffffffffffffffffffffee" + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt_illegalRangeGreaterThan.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt_illegalRangeGreaterThan.json new file mode 100644 index 0000000000..535dd825d7 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getStorageAt_illegalRangeGreaterThan.json @@ -0,0 +1,13 @@ +{ + "request" :"{ account(address: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { storage(slot: \"0x00000000000000000000000000000021\") } }", + + "response":{ + "data" : { + "account" : { + "storage" : "0x0000000000000000000000000000000000000000000000000000000000000000" + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockHashAndIndex.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockHashAndIndex.json new file mode 100644 index 0000000000..50c82e9536 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockHashAndIndex.json @@ -0,0 +1,20 @@ +{ + "request": + + "{ block(hash: \"0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6\") { transactionAt(index: 0) {block{hash} hash } } }", + + "response":{ + "data" : { + "block" : { + "transactionAt" : { + "block" : { + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6" + }, + "hash" : "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4" + } + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndIndex.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndIndex.json new file mode 100644 index 0000000000..84f8b19b32 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndIndex.json @@ -0,0 +1,20 @@ +{ + "request": + + "{ block(number: \"0x1e\") { transactionAt(index: 0) {block{hash} hash} } }", + + "response":{ + "data" : { + "block" : { + "transactionAt" : { + "block" : { + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6" + }, + "hash" : "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4" + } + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndInvalidIndex.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndInvalidIndex.json new file mode 100644 index 0000000000..b15fe8df6d --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByBlockNumberAndInvalidIndex.json @@ -0,0 +1,14 @@ +{ + "request": + + "{ block(number: \"0x1e\") { transactionAt(index: 1) {block{hash} hash} } }", + + "response":{ + "data" : { + "block" : { + "transactionAt" : null + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHash.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHash.json new file mode 100644 index 0000000000..307a94cdd3 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHash.json @@ -0,0 +1,32 @@ +{ + "request": + "{transaction (hash : \"0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4\") { block{hash} gas gasPrice hash inputData nonce index value from {address} to {address} logs{index} status createdContract{address} } } ", + "response": { + "data" : { + "transaction" : { + "block" : { + "hash" : "0xc8df1f061abb4d0c107b2b1a794ade8780b3120e681f723fe55a7be586d95ba6" + }, + "gas" : 314159, + "gasPrice" : "0x1", + "hash" : "0x9cc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4", + "inputData" : "0xe8beef5b", + "nonce" : 29, + "index" : 0, + "value" : "0xa", + "from" : { + "address" : "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + }, + "to" : { + "address" : "0x6295ee1b4f6dd65047762f924ecd367c17eabf8f" + }, + "logs" : [ { + "index" : 0 + } ], + "status" : null, + "createdContract" : null + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHashNull.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHashNull.json new file mode 100644 index 0000000000..d7386982f4 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionByHashNull.json @@ -0,0 +1,13 @@ +{ + "request": + + "{transaction (hash : \"0xffc6c7e602c56aa30c554bb691377f8703d778cec8845f4b88c0f72516b304f4\") { block{hash} gas gasPrice hash inputData nonce index value }} ", + + + "response": { + "data" : { + "transaction" : null + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionCount.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionCount.json new file mode 100644 index 0000000000..78d5822bb5 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionCount.json @@ -0,0 +1,13 @@ +{ + "request" :"{ account(address: \"0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b\") { transactionCount } }", + + "response": { + "data" : { + "account" :{ + "transactionCount" : 32 + } + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionReceipt.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionReceipt.json new file mode 100644 index 0000000000..9a3454d437 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_getTransactionReceipt.json @@ -0,0 +1,27 @@ +{ + "request" : "{ transaction(hash: \"0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed\") {block{hash logsBloom} hash createdContract{address} cumulativeGasUsed gas gasUsed logs{topics} from{address} to{address} index } }", + + "response":{ + "data" : { + "transaction" : { + "block" : { + "hash" : "0x10aaf14a53caf27552325374429d3558398a36d3682ede6603c2c6511896e9f9", + "logsBloom" : "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + }, + "hash" : "0x812742182a79a8e67733edc58cfa3767aa2d7ad06439d156ddbbb33e3403b4ed", + "createdContract" : null, + "cumulativeGasUsed" : 493172, + "gas" : 3141592, + "gasUsed" : 493172, + "logs" : [ ], + "from" : { + "address" : "0xa94f5374fce5edbc8e2a8697c15331677e6ebf0b" + }, + "to" : null, + "index" : 0 + } + } + }, + + "statusCode": 200 +} diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_contractCreation.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_contractCreation.json new file mode 100644 index 0000000000..12370766ba --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_contractCreation.json @@ -0,0 +1,9 @@ +{ + "request" : "mutation { sendRawTransaction(data: \"0xf901ca0685174876e800830fffff8080b90177608060405234801561001057600080fd5b50610157806100206000396000f30060806040526004361061004c576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680633bdab8bf146100515780639ae97baa14610068575b600080fd5b34801561005d57600080fd5b5061006661007f565b005b34801561007457600080fd5b5061007d6100b9565b005b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60016040518082815260200191505060405180910390a1565b7fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60026040518082815260200191505060405180910390a17fa53887c1eed04528e23301f55ad49a91634ef5021aa83a97d07fd16ed71c039a60036040518082815260200191505060405180910390a15600a165627a7a7230582010ddaa52e73a98c06dbcd22b234b97206c1d7ed64a7c048e10c2043a3d2309cb00291ca00297f7489c9e70447d917f7069a145c9fd0543633bec0a17ac072f1e07ab7f24a0185bd6435c17603b85fd84b8b45605988e855238fe2bbc6ea1f7e9ee6a5fc15f\") }", + "response":{ + "data" : { + "sendRawTransaction" : "0x84df486b376e7eaf35792d710fc38ce110e62ab9cdb73a45d191da74c2190617" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_messageCall.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_messageCall.json new file mode 100644 index 0000000000..3ef2372b3f --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_messageCall.json @@ -0,0 +1,10 @@ +{ + "request" : "mutation { sendRawTransaction(data: \"0xf8690885174876e800830fffff94450b61224a7df4d8a70f3e20d4fd6a6380b920d180843bdab8bf1ba0efcd6b9df2054a4e8599c0967f8e1e45bca79e2998ed7e8bafb4d29aba7dd5c2a01097184ba24f20dc097f1915fbb5f6ac955bbfc014f181df4d80bf04f4a1cfa5\") }", + "response":{ + "data" : { + "sendRawTransaction" : "0xaa6e6646456c576edcd712dbb3f30bf46c3d8310b203960c1e675534553b2daf" + } + }, + + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_transferEther.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_transferEther.json new file mode 100644 index 0000000000..4b19d37e46 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_transferEther.json @@ -0,0 +1,9 @@ +{ + "request" : "mutation { sendRawTransaction(data: \"0xf86d0485174876e800830222e0945aae326516b4f8fe08074b7e972e40a713048d62880de0b6b3a7640000801ba05d4e7998757264daab67df2ce6f7e7a0ae36910778a406ca73898c9899a32b9ea0674700d5c3d1d27f2e6b4469957dfd1a1c49bf92383d80717afc84eb05695d5b\") }", + "response":{ + "data" : { + "sendRawTransaction" : "0xbaabcc1bd699e7378451e4ce5969edb9bdcae76cb79bdacae793525c31e423c7" + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_unsignedTransaction.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_unsignedTransaction.json new file mode 100644 index 0000000000..d7527d7ab6 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_sendRawTransaction_unsignedTransaction.json @@ -0,0 +1,19 @@ +{ + "request" : "mutation { sendRawTransaction(data: \"0xed0a85174876e800830222e0945aae326516b4f8fe08074b7e972e40a713048d62880de0b6b3a7640000801c8080\") }", + "response":{ + "data" : null, + "errors" : [ { + "message" : "Exception while fetching data (/sendRawTransaction) : Invalid params", + "locations" : [ { + "line" : 1, + "column" : 12 + } ], + "path" : [ "sendRawTransaction" ], + "extensions" : { + "errorCode" : -32602, + "errorMessage" : "Invalid params" + } + } ] + }, + "statusCode": 400 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_syncing.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_syncing.json new file mode 100644 index 0000000000..fc7eb0bbce --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/eth_syncing.json @@ -0,0 +1,17 @@ +{ + "request": + "{ syncing {startingBlock currentBlock highestBlock pulledStates knownStates } }", + + "response": { + "data" : { + "syncing" : { + "startingBlock" : 1, + "currentBlock" : 2, + "highestBlock" : 3, + "pulledStates" : null, + "knownStates" : null + } + } + }, + "statusCode": 200 +} \ No newline at end of file diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestBlockchain.blocks b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphQLRpcTestBlockchain.blocks new file mode 100644 index 0000000000000000000000000000000000000000..d29453d3e53b4382cc3ff4254e1931182a6c630d GIT binary patch literal 23287 zcmeI4cT^O~+wW&!$QcAll2t&EAWQgs;ax6Q`^v;ZRi&O@(%$x)pl)W zv`OEiAgJ8pY1=#M9#UDFxG*QO<#g#?$#=2xaY*FKN_jl> z>qj+EW3KW~CS{EOSfCR64=w-yKRW;o0zm|DE%hS;)!y9olLk0~s;VYU*@*ENu}@zq zzlnL;+5d4-YoCABy-klaokcmMTN}lfvQ4$1nwo7Ki*4vvU~8Gof5XHEZK2S2-{PL_ zoQEc-U}pSaeucZx-qSrs-(zT~pjFLEnGx3SE(u}lyL$0KknvB-6G;aC_5IE2^;B5VX% zgBU^hy^j2gMZpjz#O!UgC*V*HO@s+)ZsiG-yoiz~ou=ea@+wNwo!PyBlKLn~AH^+* zlDAQk?_!8PN+M8Fs7-+fCF4=@x<`)%N>-z!W-Hfglx#&wGu69}C^?RjmZ942D7lD| zE>(;JC*Wuhl)Oikr;3t5?adE^3mhi> z>oG_;dJkyXr}(L&6gZ3wWpv#CoA7hF1~{zYN98?m^ooPZ@qVgm1`bpEQDhSigZ^@U zG;BBq{U7;gR0j>7{9!l?$Bg}v5<0j*)D`^KJwTI#!vcS#)Znn63Y#4i4*!wua$r#Y zVF)=e82m8A!!gQ!q%z?c@;_1~aP*~Ln_dscgdFChy1@tnheB}u)clP0(Woe*0?m`=tW3Un}@24{8Ka_WR>hXuqV;|7g_T_T``33H#|_ z@PdpWQEE}(iKAdBK%$i4=&0Kh_5GTSpg-~;cwk=Fk36&=dEbwlKg>{f*_%QH$;aTC6ts++p+r)BUKwrLo*XVs zjj@dTm(NXt-NEjM`qQAAlCFDjF^5S2OhSGAVnw(S8q+fxmgcA0iO%+EW9#zp#<-ivB{>}gN7n2bb?Z0mnk0%csV{Bd42E0u&&j1+kID275akH;C zo=IMu{^FG3PbbUzwZ|XbWGb3M?q#Bh6#oh%zlbqtoW<4Kn z`SxWIC8MNHA_e97b8ff}*XgX>8gUxeey_B^m6jwLxYF`hT?Q9g9}z&#y~u*xavICg zngn@gF$&E$l+kx^$Ld&|qMhdO$GP&5%cori0C9V{E%Pl?C^P`p$`xlEzeuBG)2A%& z!WDD9UBK()?5jxJKycwi11y{;vNveUFB9OPs_o=|gy1QlEtsYrl$d#Pb1%igf)p^1 z#!_NU^S5Ds!;1Y_Kv;V&KC&W@zKJo!pyRR@$yKvM&IsAo)t?P{lEcpLr5238EbicN zheqFjPJ&g!&lo+PEPYJQCRD@uongz9ZFDoKx6A9P_oVCg^ zHR&nN0Xn@svN*pv135#WA8@9Y3UW4q06g#M&ADhsE$%%V;dMqBmD&z61}y4loz8QX zDiyk6bO#xV&6LhlxyKH2hISNZ;F_8SNU47nJI-8Yqi-XzI&}{7172o`XZYql$?(XC zxUA-Up>~R8-TH9GXvR&-oJ}GwE;-Q|4nrs)0i~FSY%5uadCI<*&*)y$Q zpAM6=z2&GiddyV}zQ^T~$2>xhFZADG>{Sy9=ZIi-tv;tUE%t0SlttKZ9q6~NS%T^5o-E=rM5DUC1@YqD+}u9l7B&bIA6wqcLO zIr}E#9xk_QOVdQTKU`bOiuj|Z-Qsbd$_O~1S95PpoQY9)Gg`#qx%E6{St-|2k*TJ? zrK6E(MF;SQo-{mbhxJu#+QOcCQAtfmis5dGDarbLi`QzP|?gg5r z_aD7evQqCH-xfhTIbSq~q&t;1Z#c2d4RVHY6ldVOW?G;k#v8K(+E_dA{7G^&tf_6t za%3l!rbKl+~UAoX*tt^EA1=aoO#aHw4Ja$^R*lj;(1BukTY{@ z1Cw43rO0wiZt{r0mMCt&b#1oCL&~y^mrZr0uvCsYo)zaT)W{3%RASGke}vwKD_+3I7(d*6d3TRE*KRxmH$2gC<82S;yvErybKC5kF8Ul`Q{BdY zkmF_hgIR?dVkPNsB?#(R7f5pbpw*LU<{PJ+e{%+MhQRvHnJLKG3Ifahk>|)jSd0~ffIp14t=t3AW6gheGJom<1kTcAqI0L_b&;ZYE%!H?c7B>i-pq)2LS5mkD@tz} z3Mf4+4>`M>II-qYcX_X2`07%}-PsHaiqJYe9mlj-qYYQ_?&|`_A>XC`=ZhTwoqc7trFojzyb25}h2%@wms#D-0HSgoA`@2N#} zKeI_-SVX{g;=H^?bnf-w^1^e?KAySE>=HTU*KUcw#^IrA{mmK3Sq=7g&d5N{))4@e zS+8n!YUv#8GAu#q*B&pQ3zwz@azMqG0Pb4kLG53wcg(0eH_f}CL;#ToeZjTRud zesYF#1)Wvuxj}pAwc>XH=V`ISVoD3P(J^b@*x{T4>^0@N7c0&J0_-*0>;Y$j#lI<) zG;sUv>GSah(k$Xwqc{r!pZ;YxNhqab#8lL>9}hTEE@&*Ub8yBMrqWgjNtAZRIyxPb zvxkN04x{JD8PhYtBq|+>0nEuHY zrhb*`if?yrIy;D??>mkHEi{j7%%jXceVW-bUE@){AQ0i2piAH;@U%Ax^6K6u*?%_fF;!f;n#s0=XXp4ato!C4mg!S^4Y7@#zO!hWj-w9DXuj}jsDf$@El8ceQ=brJDY+s_yk%o=wd z1HPOc>RXa_^;fJAXys|{v`g<0bdw}_OXjJo7|7aYlS2;Cjk^}36Hip2weobY?^)>u zC<(MtsfV@tWd(2rJ4Of}lf$70Q#YC4%`1~|x)uzbAB@IY(R=;L$4Yxl^YY_jd_w|2 zP(u_;G)X1nU8u0dEptIiZo4K}kFooT_SE1m)EDy`kj2KBbvb2^Dko35mcYJE9{6Gz zOyPEp!Pj*{K8~Kx{x^pphmyDl99kNI9DYFnqx?5yN$vH^lTN-r{ZSFCuGEfkhvET^ z_T}~m-PbOi1dtg|HcrbBwc3Cj;{00c%lSr;!F%C?!z`k?rxUbC{GWvr`OIo* zRahjE$*}>_ITJe^f_9~aGfAdD3U6CHHnf zFxxthKkepytM=s!oSOnrZ_-zfC~J<%p)z^xBc7yOeq#i@t4D6C-dl*nlFtT8%Z)os z+cZWG0W5BH$mgh>3PXv?<7oe$ju)jRy zP+QyaV_TdGs}Il9yp266AiL?A66Y{R>!1T5g+Eo+4fw9$kkY(vkZo3==OJrf7i#K>7uFj!k6xg}v z+?y_%k>Gd%?c+Vue&q(5Mq)Bq;y?$>8$o>UrFedG2y$41|9w1E13BD90JSE2>N9G!@P`53j}sa zXs_nHUx*`kMwGhH%=^ZD4mwfDG$9^kMwCfG{sb^?7c~4rlbRqTZ8EdTUu58nMs2E{ zQ$fZwiE)y4_1V@Tha8k$iKS@J{2SQMhXiPbI%OW!A}1_f z=P!+=eol}*UleEk&g1$yq4x=?b(&n+uSE_ytbfAJif2gx82}>~g>=W>LA=C;MPQ-f+}<1C8lSB;erUJIr%KE_+QWmHdoWT`)(UpuD6gIa_v| zE${Gt+qK{0A;_U5;dc&mP#o5yApq}{^Sw@5K@0evpRP7ndyGhtk`%Syb&gCmVbkg@ z6)QyMrl#2kF|ZJV91EWLY2p&6yj3u>CtwN5SZhV zEtP=Mh3UqI)kk*j=o@0*-i1mMJ5S$G=Pq_vZa4WdVzokn9=u4$kYK^p+*vF78_#;3 zY~>jF7uNZltS{C-7NX9T`pqH8A%f^Thy5UjFa+>?+lFc>9jZEXDnRev%mNd-2~k4U z7h}j~-z$@dwr>}akOWS#oq*(dkVC?wI0PMz^IxHdW-sP4@QhPfwyWk~rir zrQLv`je8AE(=|EwEZ&bRzOC1blnUG@*xV2>0T$Df#iH_nBbcg?E4$eQ_E6ntgTqo&<7B93d95~q#ItnR-71E&SbNv06ZkxrVHuU z+I;kajCLEv;ej_KrCZ~wz1wLbF?GsYtRclNv1Z9@SOYc%ofl}hmNO&qkIA9;(k?0A z7pb+oNDkgFP80X{F6}cW*lkhf`kHb|C3S=X_Dx1?drzFtM21+Ue zc;Ka0kBb0i;vvOeD;2R1X>&EZ96T5;=iLNI)$l`huFE|G!jd|W8y8*Gt|#$Pf$yn_ zkKiy6^irb*FbSG5)MjMM9SW?FgT;|f(OV*yF!nN*Egs?tN|X8)11xZ(izSR|@q&Sr6IJ~5;)VUK!Tdz7>4!JT(=M6l@f zRB!5|E;LF(u0T5DY&XZj7;&PTUBIh1WACK2ECt>+xr<}biu=6$iqkhK*^|mbWJV{x z(xv{JGmtY$((jysUTXD(D6d-<{KsC>QVk5E_&dX&wx+c(_Lf!1i_$pn!eti*HFc4a zayC8BlxL$s&Pa~p?7Np*No)!0P+qhdjV7jZSE*n~J;ArDCM06l6OeOr@n@>b0U_Zz zyz|Yq#JT1kY{EOO${&gQ-^m1BFQt-kJ^eu~iTsc=iV=aXm6L|nYYQcWvobP*cB55U zrw9X6;!Q^+6-L!FkI5O^8g;XvzD5~=03bC##Mwi=#ebV~;|l?-9w$umme3afcwU)A zuor%Q{Lx1qJ(|^kQLN`}<*nrez(=M>f~KGE0Mz+cOx_1s&n7KAg3ISVG)Ft7?M!X? zr8ma`78( zr;xZOL5ZZ%^&Xp*zI^cAjr1taKz|h)An{?jIE~>5Rxi1$9L-qv>xmmh(r86I-yAKT zm_5m^AP3l7*iIq*l7ejphn45#7m(Oz`1#ENC1yz<{CcVeSLQ>`WH?GPWL}tm3UPBj z>pe`{{MwsUX{uDy&lfXeGqSUB;h3D|Wdt!uV>J8SZ}T$FG#Hrn^XF7~>-}WU_5$IU z+;o2~z$|#JAZ#p*!H7(I=XI+ys|X(bTc$_Dy;V=yLi$YgrvdM(q_NtW&P(}k`iUE9)>&bXW$+cVMkzw1cv=&*H zM;~nad=tjQxhn$T|Fjefw}LMFbn0K&hUQv)(bN|Q#c^w zY?tVM%BP@Y!eZzH9YCk_`catNFoy1)sOH2nS>){;Sj|}(=O7_#eXJUDs!l+|fmdeU zx~CG3rocnU^1N`07=txQtg6}#vWHz3Rg(XkGZbeS5c2Pwf&MD>Liu|%!_kh2pZt`}JWm3!OKTSQW^buFRg>OVskEX~HPQJraT8xyYt3F7 zmoldLkh51+wO!tQlv$Q~PQ#{82(P(J^iskK6dm0zu)E7oi5=HpC4rW#zfY}@FA^K! z+G?USEgo`6^qqCcAHewaHyvqY#tM^rTY}*sXL+B@zI;ZP^pvW)4RhswzUKo>Vt2tsQ!_ z5+j9Tn6vQbrE}nSv4iK$j9fjdTU$&-t=DWs@Xd_GdoM(!U1FR)tLaPFx39fxdCc*Q ztwltbw<-K;*TXR4Q!ddT?%sS^79`^TUgU}1w~a|$Uf_P+U>>9O66;G!b!ihyhPm!B zSHGI%o!9Mq+vKuiO&UPdw2*v7cajXDO#kXF_4Tj*+~pfrt^lk41N9oh*!HtOaVGf> z-*5iUWdBVo82)nRzbW*WGN`>P{>Ju;3GE<-;tX-}fHST0AZIiPAYU+;^CD$=3YUU( zjcxF)Hgmnvq>OuE#a>e78-lOYl#$DRU!T4H*7ySCjN&NH!0iQSfeW$cRJC#`(A78+ zY(iQ~jE7I?M`Bdn74DbaV%K00s{}~#H7C7mJG0=F1HH99x7)a!0jZr=8L#@uXs=`r z2%b9ROjIgOxKuK$Ua!tG(hX7JK1}^0>BJ+ljO=V(q+atp=P@}`siZbChcK^(Z%dq2 zyvz5|aD>Y!N)COSX@O?Q>;j7*!1S;z4iI_h;o*+8HXSKv+_AD{v9d;YwfDw{uh^jy za{wg|gjUyS@d97)iy@ZOxHTT^LM;(2De=$V*)YDhi)cS__DdK1UrR{-^~?-^HT_2x z#D7!@9M5Vf4>;2l0y(2c03(n6-_y9c+~k77I-+?WM;O;(Q}3=x*KwUywj8_BRF0fj zP3E?0ywD7CcJe6Bz)dJ<0I8{R7vqUxr>5=h(KG#LqN+)=lov*?Nzdn)0K3y$UVv)S z%UD^`SEYhy?}mPRFx9{lF)l4x9+Uq$J;l+RDn<8@Ga5n$9U&6|PxyEy4*Rgj1Bw{~ z=AmaZx%G-!cGdNaWXI&JGr>y2wTVw)?(LF&zm6b7G7!cB(px>3V^XH19>cK!7_2zvHIqWC3j2TAAv`moKA=p+?Bw%G4Omo)EZZo?EqSf3IHv4C z6e<9JYpSGQ+tWq$Xm;4L=_O!j_U3}=PKavEYZhTXW4`d`M2+G8={sy(4GtMEt<|;v zPjhx;kAs<#e=WiASJQuFL4c(s!RW|<;!KkI`@D$;a>k4R(C$oeH|U!>-T9zUS(Ik( z6c)8{%>o*Ai}P_=DKjIL6|&x_x+=skEdu0>>L|{@&3b46pr$Pcmnma7@mgAdGG89Lr zXu2{}&LH-FW0-7Umtlh+P%1X^_HT;~yt*8kTq1VGN6E%@x%4SEevA5fUJ{naNSN`>Qqiou#_Yi9T`xZA!xpjXK;`+4g|1$JwlGwv&n278Vi?- zbuyxH_(<*b>ZTB0Qo^Yb^DQZ4&Q4Xi0tS2)CJn@^XXi)NW0le3--k&P?TV_ZgcEQ#Ah zND!vZU>fG&iB7lVy9pt#nh}n%Vh1W@_ab@;;-Y`|D&A!Hy9y6yCfJL8BmnE6JpgjXjQ~!ldN)ao)@M4sd6^U{&rO=bnB-IbNJOng` z$QjL1oPisP(Et%Q@@})T)#&+sy%Zl6Ib`BJ8Ap<{r}Dh3|Ac}$?#u3$tudD#wylqEhW#Ed?&W=WRFrY4_K>rS>wWsTqTNr)oljcKM>gi1tMyU!t_RVj;t zb2(+Y8=kKzp6N{$xLZX|@izPd|q)9m0PZoRp6#aISE{n-qqWgMowWIcolu|8vrR1p4 zDr!`-&&NsdZ#g^U3Gt_{JhcDG{5MI4Uu}|y$shI~>M-x`e*anu0Tz=4qd$i~b0$f5 zz?mK+$e92Fu>SD6sBFt{>22{sf)ztMOCv-!?IN>2@&13YK)Gw~#6MLScc2^t?L6njbTGBg6OzL<} z1`p`w9Fwz$IEkGI>6?8@Ebh-Y_&Xz`qAG}9sc1?yCaasSOu1D9LJX_jas}@t0%6I6 zL8-IZ$sGX%M$8kRIkHSNadYzR02ayDiW1I`?!ZpxihF+Cq*k#fVKM7`d);`Nzud;& zZR-Cu&VC`}7kn6gNr4Fj7)gQ=1bPtv5&ft|67(CwLeL*@X0QZub`iDPOMQd%lOb8{ zl!48ut?HT<@qwF)F?|MbuSB#lV6srS-lD;YH~scNgb0COL-{GDE3yuVdJX? zRyLDdv{drVvi0ZF0Iu~=kOW8GV#!Ou$azGV_Dgh=QYth^KUEptg;NtHR*N=MjMuU| zv&||DWaIi+%v@MkKFuOXH(}gX)!5il%|1u2-T+sIc&JkT={x%WjrAGBAH9DdP#yn0 zFdT{_=FLBfpu~^zhgJkAB*Ey<;Rk1^9293Y4ByAIWRNp41aN)C_0_(HhV~2G@i@ll zh-*t`144O_xj1IRVtTz9{AA=1CO4#*5OD_NjQ%Ljz^z?r06Sd>yZ0s*R*w+rI9niz z1I specs() { return specs.values(); } - // @formatter:on @Test public void jsonRPCCallWithSpecFile() throws Exception { diff --git a/gradle/check-licenses.gradle b/gradle/check-licenses.gradle index e969e742d2..a05bebfd8f 100644 --- a/gradle/check-licenses.gradle +++ b/gradle/check-licenses.gradle @@ -42,7 +42,8 @@ ext.acceptedLicenses = [ 'Mozilla Public License Version 2.0', 'CC0 1.0 Universal License', 'Common Development and Distribution License 1.0', - 'Unicode/ICU License' + 'Unicode/ICU License', + 'Public Domain (CC0) License 1.0' ]*.toLowerCase() /** @@ -64,6 +65,7 @@ downloadLicenses { ext.mpl2_0 = license('Mozilla Public License, Version 2.0', 'http://www.mozilla.org/MPL/2.0/') ext.cddl = license('Common Development and Distribution License 1.0', 'http://opensource.org/licenses/CDDL-1.0') ext.cddl1_1 = license('Common Development and Distribution License 1.0', 'http://oss.oracle.com/licenses/CDDL-1.1') + ext.cc0 = license('Public Domain (CC0) License 1.0', 'https://creativecommons.org/publicdomain/zero/1.0') aliases = [ (apache) : [ 'The Apache Software License, Version 2.0', @@ -79,7 +81,7 @@ downloadLicenses { license('Apache Software Licenses', 'http://www.apache.org/licenses/LICENSE-2.0.txt'), license('Apache', 'http://www.opensource.org/licenses/Apache-2.0') ], - (mit) : ['The MIT License'], + (mit) : ['The MIT License', 'MIT'], (bsd) : [ 'BSD', 'BSD licence', @@ -115,11 +117,11 @@ downloadLicenses { 'CDDL + GPLv2 with classpath exception', 'Dual license consisting of the CDDL v1.1 and GPL v2' ], - (cddl1_1): [ + (cddl1_1): [ 'CDDL 1.1', 'COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1', - ] - + ], + (cc0): ['Public Domain (CC0) License 1.0', 'CC0'] ] licenses = [ diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 180292dc1c..54fcad5312 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -25,6 +25,8 @@ dependencyManagement { dependency 'com.google.errorprone:error_prone_annotation:2.3.3' dependency 'com.google.errorprone:error_prone_test_helpers:2.3.3' + dependency 'com.graphql-java:graphql-java:11.0' + dependency 'com.google.guava:guava:27.1-jre' dependency 'com.squareup.okhttp3:okhttp:3.13.1' diff --git a/pantheon/build.gradle b/pantheon/build.gradle index 36714361ff..3c350b9dec 100644 --- a/pantheon/build.gradle +++ b/pantheon/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation project(':ethereum:core') implementation project(':ethereum:eth') implementation project(':ethereum:jsonrpc') + implementation project(':ethereum:graphqlrpc') implementation project(':ethereum:permissioning') implementation project(':ethereum:p2p') implementation project(':ethereum:rlp') @@ -43,6 +44,7 @@ dependencies { implementation project(':enclave') implementation project(':services:kvstore') + implementation 'com.graphql-java:graphql-java' implementation 'com.google.guava:guava' implementation 'info.picocli:picocli' implementation 'io.vertx:vertx-core' diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java index 041648cc49..3d759d8aea 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/Runner.java @@ -13,6 +13,7 @@ package tech.pegasys.pantheon; import tech.pegasys.pantheon.controller.PantheonController; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketService; import tech.pegasys.pantheon.ethereum.p2p.NetworkRunner; @@ -43,6 +44,7 @@ public class Runner implements AutoCloseable { private final NetworkRunner networkRunner; private final Optional jsonRpc; + private final Optional graphQLRpc; private final Optional websocketRpc; private final Optional metrics; @@ -53,12 +55,14 @@ public class Runner implements AutoCloseable { final Vertx vertx, final NetworkRunner networkRunner, final Optional jsonRpc, + final Optional graphQLRpc, final Optional websocketRpc, final Optional metrics, final PantheonController pantheonController, final Path dataDir) { this.vertx = vertx; this.networkRunner = networkRunner; + this.graphQLRpc = graphQLRpc; this.jsonRpc = jsonRpc; this.websocketRpc = websocketRpc; this.metrics = metrics; @@ -81,6 +85,7 @@ public void start() { .getPendingTransactions() .evictOldTransactions()); jsonRpc.ifPresent(service -> waitForServiceToStart("jsonRpc", service.start())); + graphQLRpc.ifPresent(service -> waitForServiceToStart("graphQLRpc", service.start())); websocketRpc.ifPresent(service -> waitForServiceToStop("websocketRpc", service.start())); metrics.ifPresent(service -> waitForServiceToStart("metrics", service.start())); LOG.info("Ethereum main loop is up."); @@ -111,6 +116,7 @@ public void close() throws Exception { networkRunner.awaitStop(); jsonRpc.ifPresent(service -> waitForServiceToStop("jsonRpc", service.stop())); + graphQLRpc.ifPresent(service -> waitForServiceToStop("graphQLRpc", service.stop())); websocketRpc.ifPresent(service -> waitForServiceToStop("websocketRpc", service.stop())); metrics.ifPresent(service -> waitForServiceToStop("metrics", service.stop())); } finally { @@ -173,6 +179,9 @@ private void writePantheonPortsToFile() { if (getJsonRpcPort().isPresent()) { properties.setProperty("json-rpc", String.valueOf(getJsonRpcPort().get())); } + if (getGraphQLRpcPort().isPresent()) { + properties.setProperty("graphql-rpc", String.valueOf(getGraphQLRpcPort().get())); + } if (getWebsocketPort().isPresent()) { properties.setProperty("ws-rpc", String.valueOf(getWebsocketPort().get())); } @@ -196,6 +205,10 @@ public Optional getJsonRpcPort() { return jsonRpc.map(service -> service.socketAddress().getPort()); } + public Optional getGraphQLRpcPort() { + return graphQLRpc.map(service -> service.socketAddress().getPort()); + } + public Optional getWebsocketPort() { return websocketRpc.map(service -> service.socketAddress().getPort()); } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 175506eede..5aa38a57f0 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -21,6 +21,11 @@ import tech.pegasys.pantheon.ethereum.core.PrivacyParameters; import tech.pegasys.pantheon.ethereum.core.Synchronizer; import tech.pegasys.pantheon.ethereum.eth.transactions.TransactionPool; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLDataFetcherContext; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLDataFetchers; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLProvider; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcHttpService; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcMethodsFactory; @@ -70,6 +75,7 @@ import tech.pegasys.pantheon.util.bytes.BytesValue; import tech.pegasys.pantheon.util.enode.EnodeURL; +import java.io.IOException; import java.net.URI; import java.nio.file.Path; import java.util.ArrayList; @@ -83,6 +89,7 @@ import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import graphql.GraphQL; import io.vertx.core.Vertx; public class RunnerBuilder { @@ -96,6 +103,7 @@ public class RunnerBuilder { private int p2pListenPort; private int maxPeers; private JsonRpcConfiguration jsonRpcConfiguration; + private GraphQLRpcConfiguration graphQLRpcConfiguration; private WebSocketConfiguration webSocketConfiguration; private Path dataDir; private Collection bannedNodeIds; @@ -149,6 +157,12 @@ public RunnerBuilder jsonRpcConfiguration(final JsonRpcConfiguration jsonRpcConf return this; } + public RunnerBuilder graphQLRpcConfiguration( + final GraphQLRpcConfiguration graphQLRpcConfiguration) { + this.graphQLRpcConfiguration = graphQLRpcConfiguration; + return this; + } + public RunnerBuilder webSocketConfiguration(final WebSocketConfiguration webSocketConfiguration) { this.webSocketConfiguration = webSocketConfiguration; return this; @@ -331,6 +345,30 @@ public Runner build() { vertx, dataDir, jsonRpcConfiguration, metricsSystem, jsonRpcMethods)); } + Optional graphQLRpcHttpService = Optional.empty(); + if (graphQLRpcConfiguration.isEnabled()) { + final GraphQLDataFetchers fetchers = new GraphQLDataFetchers(supportedCapabilities); + final GraphQLDataFetcherContext dataFetcherContext = + new GraphQLDataFetcherContext( + context.getBlockchain(), + context.getWorldStateArchive(), + protocolSchedule, + transactionPool, + miningCoordinator, + synchronizer); + GraphQL graphQL = null; + try { + graphQL = GraphQLProvider.buildGraphQL(fetchers); + } catch (final IOException ioe) { + throw new RuntimeException(ioe); + } + + graphQLRpcHttpService = + Optional.of( + new GraphQLRpcHttpService( + vertx, dataDir, graphQLRpcConfiguration, graphQL, dataFetcherContext)); + } + Optional webSocketService = Optional.empty(); if (webSocketConfiguration.isEnabled()) { final Map webSocketsJsonRpcMethods = @@ -379,6 +417,7 @@ public Runner build() { vertx, networkRunner, jsonRpcHttpService, + graphQLRpcHttpService, webSocketService, metricsService, pantheonController, @@ -390,7 +429,7 @@ private Optional buildNodePermissioningController( final Synchronizer synchronizer, final TransactionSimulator transactionSimulator, final BytesValue localNodeId) { - Collection fixedNodes = getFixedNodes(bootnodesAsEnodeURLs, staticNodes); + final Collection fixedNodes = getFixedNodes(bootnodesAsEnodeURLs, staticNodes); return permissioningConfiguration.map( config -> new NodePermissioningControllerFactory() @@ -400,7 +439,7 @@ private Optional buildNodePermissioningController( @VisibleForTesting public static Collection getFixedNodes( final Collection someFixedNodes, final Collection moreFixedNodes) { - Collection fixedNodes = new ArrayList<>(someFixedNodes); + final Collection fixedNodes = new ArrayList<>(someFixedNodes); fixedNodes.addAll(moreFixedNodes); return fixedNodes; } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index e009e9135f..aea3a61776 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -20,6 +20,7 @@ import static tech.pegasys.pantheon.cli.DefaultCommandValues.getDefaultPantheonDataPath; import static tech.pegasys.pantheon.cli.NetworkName.MAINNET; import static tech.pegasys.pantheon.controller.PantheonController.DATABASE_PATH; +import static tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration.DEFAULT_GRAPHQL_RPC_PORT; import static tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration.DEFAULT_JSON_RPC_PORT; import static tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis.DEFAULT_JSON_RPC_APIS; import static tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration.DEFAULT_WEBSOCKET_PORT; @@ -48,6 +49,7 @@ import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; import tech.pegasys.pantheon.ethereum.eth.sync.TrailingPeerRequirements; import tech.pegasys.pantheon.ethereum.eth.transactions.PendingTransactions; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; @@ -153,14 +155,18 @@ protected KeyLoader getKeyLoader() { arity = "1") private final Boolean p2pEnabled = true; - // Boolean option to indicate if peers should NOT be discovered, default to false indicates that + // Boolean option to indicate if peers should NOT be discovered, default to + // false indicates that // the peers should be discovered by default. // - // This negative option is required because of the nature of the option that is true when - // added on the command line. You can't do --option=false, so false is set as default + // This negative option is required because of the nature of the option that is + // true when + // added on the command line. You can't do --option=false, so false is set as + // default // and you have not to set the option at all if you want it false. // This seems to be the only way it works with Picocli. - // Also many other software use the same negative option scheme for false defaults + // Also many other software use the same negative option scheme for false + // defaults // meaning that it's probably the right way to handle disabling options. @Option( names = {"--discovery-enabled"}, @@ -250,6 +256,32 @@ void setBootnodes(final List values) { arity = "1") private final Integer networkId = null; + @Option( + names = {"--graphql-http-enabled"}, + description = "Set to start the GraphQL-RPC HTTP service (default: ${DEFAULT-VALUE})") + private final Boolean isGraphQLHttpEnabled = false; + + @SuppressWarnings("FieldMayBeFinal") // Because PicoCLI requires Strings to not be final. + @Option( + names = {"--graphql-http-host"}, + paramLabel = MANDATORY_HOST_FORMAT_HELP, + description = "Host for GraphQL-RPC HTTP to listen on (default: ${DEFAULT-VALUE})", + arity = "1") + private String graphQLHttpHost = autoDiscoverDefaultIP().getHostAddress(); + + @Option( + names = {"--graphql-http-port"}, + paramLabel = MANDATORY_PORT_FORMAT_HELP, + description = "Port for GraphQL-RPC HTTP to listen on (default: ${DEFAULT-VALUE})", + arity = "1") + private final Integer graphQLHttpPort = DEFAULT_GRAPHQL_RPC_PORT; + + @Option( + names = {"--graphql-http-cors-origins"}, + description = "Comma separated origin domain URLs for CORS validation (default: none)") + private final CorsAllowedOriginsProperty graphQLHttpCorsAllowedOrigins = + new CorsAllowedOriginsProperty(); + @Option( names = {"--rpc-http-enabled"}, description = "Set to start the JSON-RPC HTTP service (default: ${DEFAULT-VALUE})") @@ -397,7 +429,7 @@ void setBootnodes(final List values) { names = {"--host-whitelist"}, paramLabel = "[,...]... or * or all", description = - "Comma separated list of hostnames to whitelist for JSON-RPC access, or * to accept any host (default: ${DEFAULT-VALUE})", + "Comma separated list of hostnames to whitelist for RPC access, or * to accept any host (default: ${DEFAULT-VALUE})", defaultValue = "localhost,127.0.0.1") private final JsonRPCWhitelistHostsProperty hostsWhitelist = new JsonRPCWhitelistHostsProperty(); @@ -589,7 +621,8 @@ public void parse( "Ethereum Wire Protocol", ethereumWireConfigurationBuilder)); - // Create a handler that will search for a config file option and use it for default values + // Create a handler that will search for a config file option and use it for + // default values // and eventually it will run regular parsing of the remaining options. final ConfigOptionSearchAndRunHandler configParsingHandler = new ConfigOptionSearchAndRunHandler( @@ -633,7 +666,7 @@ public void run() { !SyncMode.FAST.equals(syncMode), singletonList("--fast-sync-min-peers")); - //noinspection ConstantConditions + // noinspection ConstantConditions if (isMiningEnabled && coinbase == null) { throw new ParameterException( this.commandLine, @@ -644,6 +677,7 @@ public void run() { final EthNetworkConfig ethNetworkConfig = updateNetworkConfig(getNetwork()); try { final JsonRpcConfiguration jsonRpcConfiguration = jsonRpcConfiguration(); + final GraphQLRpcConfiguration graphQLRpcConfiguration = graphQLRpcConfiguration(); final WebSocketConfiguration webSocketConfiguration = webSocketConfiguration(); final Optional permissioningConfiguration = permissioningConfiguration(); @@ -671,6 +705,7 @@ public void run() { maxPeers, p2pHost, p2pPort, + graphQLRpcConfiguration, jsonRpcConfiguration, webSocketConfiguration, metricsConfiguration(), @@ -682,7 +717,8 @@ public void run() { } private NetworkName getNetwork() { - //noinspection ConstantConditions network is not always null but injected by PicoCLI if used + // noinspection ConstantConditions network is not always null but injected by + // PicoCLI if used return network == null ? MAINNET : network; } @@ -721,6 +757,25 @@ PantheonController buildController() { } } + private GraphQLRpcConfiguration graphQLRpcConfiguration() { + + checkOptionDependencies( + logger, + commandLine, + "--graphql-http-enabled", + !isRpcHttpEnabled, + asList("--graphql-http-cors-origins", "--graphql-http-host", "--graphql-http-port")); + + final GraphQLRpcConfiguration graphQLRpcConfiguration = GraphQLRpcConfiguration.createDefault(); + graphQLRpcConfiguration.setEnabled(isGraphQLHttpEnabled); + graphQLRpcConfiguration.setHost(graphQLHttpHost); + graphQLRpcConfiguration.setPort(graphQLHttpPort); + graphQLRpcConfiguration.setHostsWhitelist(hostsWhitelist); + graphQLRpcConfiguration.setCorsAllowedDomains(graphQLHttpCorsAllowedOrigins); + + return graphQLRpcConfiguration; + } + private JsonRpcConfiguration jsonRpcConfiguration() { checkOptionDependencies( @@ -953,6 +1008,7 @@ private void synchronize( final int maxPeers, final String p2pAdvertisedHost, final int p2pListenPort, + final GraphQLRpcConfiguration graphQLRpcConfiguration, final JsonRpcConfiguration jsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, final MetricsConfiguration metricsConfiguration, @@ -974,6 +1030,7 @@ private void synchronize( .p2pAdvertisedHost(p2pAdvertisedHost) .p2pListenPort(p2pListenPort) .maxPeers(maxPeers) + .graphQLRpcConfiguration(graphQLRpcConfiguration) .jsonRpcConfiguration(jsonRpcConfiguration) .webSocketConfiguration(webSocketConfiguration) .dataDir(dataDir()) @@ -1029,16 +1086,20 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { final EthNetworkConfig.Builder builder = new EthNetworkConfig.Builder(EthNetworkConfig.getNetworkConfig(network)); - // custom genesis file use comes with specific default values for the genesis file itself + // custom genesis file use comes with specific default values for the genesis + // file itself // but also for the network id and the bootnodes list. final File genesisFile = genesisFile(); if (genesisFile != null) { - //noinspection ConstantConditions network is not always null but injected by PicoCLI if used + // noinspection ConstantConditions network is not always null but injected by + // PicoCLI if used if (this.network != null) { - // We check if network option was really provided by user and not only looking at the + // We check if network option was really provided by user and not only looking + // at the // default value. - // if user provided it and provided the genesis file option at the same time, it raises a + // if user provided it and provided the genesis file option at the same time, it + // raises a // conflict error throw new ParameterException( this.commandLine, @@ -1049,13 +1110,17 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { builder.setGenesisConfig(genesisConfig()); if (networkId == null) { - // if no network id option is defined on the CLI we have to set a default value from the + // if no network id option is defined on the CLI we have to set a default value + // from the // genesis file. - // We do the genesis parsing only in this case as we already have network id constants + // We do the genesis parsing only in this case as we already have network id + // constants // for known networks to speed up the process. - // Also we have to parse the genesis as we don't already have a parsed version at this + // Also we have to parse the genesis as we don't already have a parsed version + // at this // stage. - // If no chain id is found in the genesis as it's an optional, we use mainnet network id. + // If no chain id is found in the genesis as it's an optional, we use mainnet + // network id. try { final GenesisConfigFile genesisConfigFile = GenesisConfigFile.fromConfig(genesisConfig()); builder.setNetworkId( @@ -1076,9 +1141,11 @@ private EthNetworkConfig updateNetworkConfig(final NetworkName network) { } if (bootNodes == null) { - // We default to an empty bootnodes list if the option is not provided on CLI because + // We default to an empty bootnodes list if the option is not provided on CLI + // because // mainnet bootnodes won't work as the default value for a custom genesis, - // so it's better to have an empty list as default value that forces to create a custom one + // so it's better to have an empty list as default value that forces to create a + // custom one // than a useless one that may make user think that it can work when it can't. builder.setBootNodes(new ArrayList<>()); } diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java index 740714c5dd..94bd76582c 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/RunnerTest.java @@ -34,6 +34,7 @@ import tech.pegasys.pantheon.ethereum.eth.sync.SyncMode; import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; import tech.pegasys.pantheon.ethereum.eth.transactions.PendingTransactions; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.mainnet.HeaderValidationMode; @@ -82,17 +83,17 @@ public final class RunnerTest { @Test public void getFixedNodes() { - EnodeURL staticNode = + final EnodeURL staticNode = EnodeURL.fromString( "enode://8f4b88336cc40ef2516d8b27df812e007fb2384a61e93635f1899051311344f3dcdbb49a4fe49a79f66d2f589a9f282e8cc4f1d7381e8ef7e4fcc6b0db578c77@127.0.0.1:30301"); - EnodeURL bootnode = + final EnodeURL bootnode = EnodeURL.fromString( "enode://8f4b88336cc40ef2516d8b27df812e007fb2384a61e93635f1899051311344f3dcdbb49a4fe49a79f66d2f589a9f282e8cc4f1d7381e8ef7e4fcc6b0db578c77@127.0.0.1:30302"); - final List bootnodes = new ArrayList(); + final List bootnodes = new ArrayList<>(); bootnodes.add(bootnode); - Collection staticNodes = new ArrayList(); + final Collection staticNodes = new ArrayList<>(); staticNodes.add(staticNode); - Collection fixedNodes = RunnerBuilder.getFixedNodes(bootnodes, staticNodes); + final Collection fixedNodes = RunnerBuilder.getFixedNodes(bootnodes, staticNodes); assertThat(fixedNodes).containsExactlyInAnyOrder(staticNode, bootnode); // bootnodes should be unchanged assertThat(bootnodes).containsExactly(bootnode); @@ -157,6 +158,7 @@ private void syncFromGenesis(final SyncMode mode) throws Exception { .build(); final String listenHost = InetAddress.getLoopbackAddress().getHostAddress(); final JsonRpcConfiguration aheadJsonRpcConfiguration = jsonRpcConfiguration(); + final GraphQLRpcConfiguration aheadGraphQLRpcConfiguration = graphQLRpcConfiguration(); final WebSocketConfiguration aheadWebSocketConfiguration = wsRpcConfiguration(); final MetricsConfiguration aheadMetricsConfiguration = metricsConfiguration(); final RunnerBuilder runnerBuilder = @@ -176,6 +178,7 @@ private void syncFromGenesis(final SyncMode mode) throws Exception { .pantheonController(controllerAhead) .ethNetworkConfig(EthNetworkConfig.getNetworkConfig(DEV)) .jsonRpcConfiguration(aheadJsonRpcConfiguration) + .graphQLRpcConfiguration(aheadGraphQLRpcConfiguration) .webSocketConfiguration(aheadWebSocketConfiguration) .metricsConfiguration(aheadMetricsConfiguration) .dataDir(dbAhead) @@ -192,6 +195,7 @@ private void syncFromGenesis(final SyncMode mode) throws Exception { .build(); final Path dataDirBehind = temp.newFolder().toPath(); final JsonRpcConfiguration behindJsonRpcConfiguration = jsonRpcConfiguration(); + final GraphQLRpcConfiguration behindGraphQLRpcConfiguration = graphQLRpcConfiguration(); final WebSocketConfiguration behindWebSocketConfiguration = wsRpcConfiguration(); final MetricsConfiguration behindMetricsConfiguration = metricsConfiguration(); @@ -223,6 +227,7 @@ private void syncFromGenesis(final SyncMode mode) throws Exception { .pantheonController(controllerBehind) .ethNetworkConfig(behindEthNetworkConfiguration) .jsonRpcConfiguration(behindJsonRpcConfiguration) + .graphQLRpcConfiguration(behindGraphQLRpcConfiguration) .webSocketConfiguration(behindWebSocketConfiguration) .metricsConfiguration(behindMetricsConfiguration) .dataDir(temp.newFolder().toPath()) @@ -342,6 +347,13 @@ private JsonRpcConfiguration jsonRpcConfiguration() { return configuration; } + private GraphQLRpcConfiguration graphQLRpcConfiguration() { + final GraphQLRpcConfiguration configuration = GraphQLRpcConfiguration.createDefault(); + configuration.setPort(0); + configuration.setEnabled(false); + return configuration; + } + private WebSocketConfiguration wsRpcConfiguration() { final WebSocketConfiguration configuration = WebSocketConfiguration.createDefault(); configuration.setPort(0); diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java index 0305705eec..b0d35a1c8a 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/CommandTestAbstract.java @@ -27,6 +27,7 @@ import tech.pegasys.pantheon.crypto.SECP256K1.KeyPair; import tech.pegasys.pantheon.ethereum.eth.EthereumWireProtocolConfiguration; import tech.pegasys.pantheon.ethereum.eth.sync.SynchronizerConfiguration; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.permissioning.PermissioningConfiguration; @@ -91,6 +92,7 @@ public abstract class CommandTestAbstract { @Captor ArgumentCaptor intArgumentCaptor; @Captor ArgumentCaptor ethNetworkConfigArgumentCaptor; @Captor ArgumentCaptor jsonRpcConfigArgumentCaptor; + @Captor ArgumentCaptor graphQLRpcConfigArgumentCaptor; @Captor ArgumentCaptor wsRpcConfigArgumentCaptor; @Captor ArgumentCaptor metricsConfigArgumentCaptor; @@ -143,6 +145,7 @@ public void initMocks() throws Exception { when(mockRunnerBuilder.maxPeers(anyInt())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.p2pEnabled(anyBoolean())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.jsonRpcConfiguration(any())).thenReturn(mockRunnerBuilder); + when(mockRunnerBuilder.graphQLRpcConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.webSocketConfiguration(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.dataDir(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.bannedNodeIds(any())).thenReturn(mockRunnerBuilder); diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java index c83ae96340..a4ac106c72 100644 --- a/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java +++ b/pantheon/src/test/java/tech/pegasys/pantheon/cli/PantheonCommandTest.java @@ -41,6 +41,7 @@ import tech.pegasys.pantheon.ethereum.core.Wei; import tech.pegasys.pantheon.ethereum.eth.sync.SyncMode; import tech.pegasys.pantheon.ethereum.eth.transactions.PendingTransactions; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.JsonRpcConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApi; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; @@ -88,6 +89,7 @@ public class PantheonCommandTest extends CommandTestAbstract { static final String PERMISSIONING_CONFIG_TOML = "/permissioning_config.toml"; private static final JsonRpcConfiguration defaultJsonRpcConfiguration; + private static final GraphQLRpcConfiguration defaultGraphQLRpcConfiguration; private static final WebSocketConfiguration defaultWebSocketConfiguration; private static final MetricsConfiguration defaultMetricsConfiguration; private static final int GENESIS_CONFIG_TEST_CHAINID = 3141592; @@ -106,6 +108,8 @@ public class PantheonCommandTest extends CommandTestAbstract { static { defaultJsonRpcConfiguration = JsonRpcConfiguration.createDefault(); + defaultGraphQLRpcConfiguration = GraphQLRpcConfiguration.createDefault(); + defaultWebSocketConfiguration = WebSocketConfiguration.createDefault(); defaultMetricsConfiguration = MetricsConfiguration.createDefault(); @@ -151,6 +155,7 @@ public void callingPantheonCommandWithoutOptionsMustSyncWithDefaultValues() thro verify(mockRunnerBuilder).p2pListenPort(eq(30303)); verify(mockRunnerBuilder).maxPeers(eq(25)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(defaultJsonRpcConfiguration)); + verify(mockRunnerBuilder).graphQLRpcConfiguration(eq(defaultGraphQLRpcConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(defaultWebSocketConfiguration)); verify(mockRunnerBuilder).metricsConfiguration(eq(defaultMetricsConfiguration)); verify(mockRunnerBuilder).ethNetworkConfig(ethNetworkArg.capture()); @@ -265,6 +270,11 @@ public void overrideDefaultValuesIfKeyIsPresentInConfigFile() throws IOException jsonRpcConfiguration.setCorsAllowedDomains(Collections.emptyList()); jsonRpcConfiguration.setRpcApis(expectedApis); + final GraphQLRpcConfiguration graphQLRpcConfiguration = GraphQLRpcConfiguration.createDefault(); + graphQLRpcConfiguration.setEnabled(false); + graphQLRpcConfiguration.setHost("6.7.8.9"); + graphQLRpcConfiguration.setPort(6789); + final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); webSocketConfiguration.setEnabled(false); webSocketConfiguration.setHost("9.10.11.12"); @@ -284,6 +294,7 @@ public void overrideDefaultValuesIfKeyIsPresentInConfigFile() throws IOException verify(mockRunnerBuilder).p2pListenPort(eq(1234)); verify(mockRunnerBuilder).maxPeers(eq(42)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); + verify(mockRunnerBuilder).graphQLRpcConfiguration(eq(graphQLRpcConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration)); verify(mockRunnerBuilder).build(); @@ -586,6 +597,8 @@ public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() throws IOExce parseCommand("--config-file", configFile); final JsonRpcConfiguration jsonRpcConfiguration = JsonRpcConfiguration.createDefault(); + final GraphQLRpcConfiguration graphQLRpcConfiguration = GraphQLRpcConfiguration.createDefault(); + final WebSocketConfiguration webSocketConfiguration = WebSocketConfiguration.createDefault(); final MetricsConfiguration metricsConfiguration = MetricsConfiguration.createDefault(); @@ -601,6 +614,7 @@ public void noOverrideDefaultValuesIfKeyIsNotPresentInConfigFile() throws IOExce verify(mockRunnerBuilder).p2pListenPort(eq(30303)); verify(mockRunnerBuilder).maxPeers(eq(25)); verify(mockRunnerBuilder).jsonRpcConfiguration(eq(jsonRpcConfiguration)); + verify(mockRunnerBuilder).graphQLRpcConfiguration(eq(graphQLRpcConfiguration)); verify(mockRunnerBuilder).webSocketConfiguration(eq(webSocketConfiguration)); verify(mockRunnerBuilder).metricsConfiguration(eq(metricsConfiguration)); verify(mockRunnerBuilder).build(); @@ -1190,6 +1204,32 @@ public void rpcHttpEnabledPropertyMustBeUsed() { assertThat(commandErrorOutput.toString()).isEmpty(); } + @Test + public void graphQLRpcHttpEnabledPropertyDefaultIsFalse() { + parseCommand(); + + verify(mockRunnerBuilder).graphQLRpcConfiguration(graphQLRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLRpcConfigArgumentCaptor.getValue().isEnabled()).isFalse(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void graphQLRpcHttpEnabledPropertyMustBeUsed() { + parseCommand("--graphql-http-enabled"); + + verify(mockRunnerBuilder).graphQLRpcConfiguration(graphQLRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLRpcConfigArgumentCaptor.getValue().isEnabled()).isTrue(); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + @Test public void rpcApisPropertyMustBeUsed() { parseCommand("--rpc-http-api", "ETH,NET,PERM", "--rpc-http-enabled"); @@ -1313,6 +1353,58 @@ public void rpcHttpHostMayBeIPv6() { assertThat(commandErrorOutput.toString()).isEmpty(); } + @Test + public void graphQLRpcHttpHostAndPortOptionsMustBeUsed() { + + final String host = "1.2.3.4"; + final int port = 1234; + parseCommand( + "--graphql-http-enabled", + "--graphql-http-host", + host, + "--graphql-http-port", + String.valueOf(port)); + + verify(mockRunnerBuilder).graphQLRpcConfiguration(graphQLRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + assertThat(graphQLRpcConfigArgumentCaptor.getValue().getPort()).isEqualTo(port); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void graphQLRpcHttpHostMayBeLocalhost() { + + final String host = "localhost"; + parseCommand("--graphql-http-enabled", "--graphql-http-host", host); + + verify(mockRunnerBuilder).graphQLRpcConfiguration(graphQLRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + + @Test + public void graphQLRpcHttpHostMayBeIPv6() { + + final String host = "2600:DB8::8545"; + parseCommand("--graphql-http-enabled", "--graphql-http-host", host); + + verify(mockRunnerBuilder).graphQLRpcConfiguration(graphQLRpcConfigArgumentCaptor.capture()); + verify(mockRunnerBuilder).build(); + + assertThat(graphQLRpcConfigArgumentCaptor.getValue().getHost()).isEqualTo(host); + + assertThat(commandOutput.toString()).isEmpty(); + assertThat(commandErrorOutput.toString()).isEmpty(); + } + @Test public void rpcHttpCorsOriginsTwoDomainsMustBuildListWithBothDomains() { final String[] origins = {"http://domain1.com", "https://domain2.com"}; diff --git a/pantheon/src/test/resources/complete_config.toml b/pantheon/src/test/resources/complete_config.toml index 472fd7d25d..c7502b58c4 100644 --- a/pantheon/src/test/resources/complete_config.toml +++ b/pantheon/src/test/resources/complete_config.toml @@ -16,6 +16,8 @@ p2p-port=1234 max-peers=42 rpc-http-host="5.6.7.8" rpc-http-port=5678 +graphql-http-host="6.7.8.9" +graphql-http-port=6789 rpc-http-api=["ETH","WEB3"] rpc-ws-host="9.10.11.12" rpc-ws-port=9101 diff --git a/pantheon/src/test/resources/everything_config.toml b/pantheon/src/test/resources/everything_config.toml index 68b7859f94..03b4ccf526 100644 --- a/pantheon/src/test/resources/everything_config.toml +++ b/pantheon/src/test/resources/everything_config.toml @@ -45,6 +45,12 @@ rpc-http-cors-origins=["none"] rpc-http-authentication-enabled=false rpc-http-authentication-credentials-file="none" +# GRAPHQL-RPC +graphql-http-enabled=false +graphql-http-host="6.7.8.9" +graphql-http-port=6789 +graphql-http-cors-origins=["none"] + # WebSockets API rpc-ws-enabled=false rpc-ws-api=["DEBUG","ETH"] diff --git a/settings.gradle b/settings.gradle index 03dd520bea..89a3ca2441 100644 --- a/settings.gradle +++ b/settings.gradle @@ -25,6 +25,7 @@ include 'ethereum:blockcreation' include 'ethereum:core' include 'ethereum:eth' include 'ethereum:jsonrpc' +include 'ethereum:graphqlrpc' include 'ethereum:mock-p2p' include 'ethereum:p2p' include 'ethereum:permissioning'