From 1b7f05679ffa1959e46f93038c28ad425a797216 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Tue, 14 May 2019 10:13:49 -0600 Subject: [PATCH] Add pending object to GraphQL queries (#1419) * Add pending object to GraphQL queries --- .../eth/transactions/PendingTransactions.java | 2 +- .../graphqlrpc/GraphQLDataFetchers.java | 9 ++ .../ethereum/graphqlrpc/GraphQLProvider.java | 1 + .../internal/TransactionWithMetadata.java | 29 ++-- .../pojoadapter/PendingStateAdapter.java | 127 ++++++++++++++++++ .../pojoadapter/TransactionAdapter.java | 47 ++++--- .../AbstractEthGraphQLRpcHttpServiceTest.java | 9 ++ .../EthGraphQLRpcHttpBySpecTest.java | 2 + .../ethereum/graphqlrpc/graphql_pending.json | 27 ++++ 9 files changed, 218 insertions(+), 35 deletions(-) create mode 100644 ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/PendingStateAdapter.java create mode 100644 ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphql_pending.json diff --git a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/transactions/PendingTransactions.java b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/transactions/PendingTransactions.java index 8e8d15d434..132d6a5d85 100644 --- a/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/transactions/PendingTransactions.java +++ b/ethereum/eth/src/main/java/tech/pegasys/pantheon/ethereum/eth/transactions/PendingTransactions.java @@ -326,7 +326,7 @@ public static class TransactionInfo { private final Instant addedToPoolAt; private final long sequence; // Allows prioritization based on order transactions are added - TransactionInfo( + public TransactionInfo( final Transaction transaction, final boolean receivedFromLocalSource, final Instant addedToPoolAt) { 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 index 15eddac265..6c9e78dcf5 100644 --- 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 @@ -26,6 +26,7 @@ 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.PendingStateAdapter; 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; @@ -91,6 +92,14 @@ DataFetcher> getSyncingDataFetcher() { }; } + DataFetcher> getPendingStateDataFetcher() { + return dataFetchingEnvironment -> { + final TransactionPool txPool = + ((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getTransactionPool(); + return Optional.of(new PendingStateAdapter(txPool.getPendingTransactions())); + }; + } + DataFetcher> getGasPriceDataFetcher() { return dataFetchingEnvironment -> { final MiningCoordinator miningCoordinator = 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 index 5805e5b5f4..d87a24454e 100644 --- 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 @@ -67,6 +67,7 @@ private static RuntimeWiring buildWiring(final GraphQLDataFetchers graphQLDataFe .dataFetcher("transaction", graphQLDataFetchers.getTransactionDataFetcher()) .dataFetcher("gasPrice", graphQLDataFetchers.getGasPriceDataFetcher()) .dataFetcher("syncing", graphQLDataFetchers.getSyncingDataFetcher()) + .dataFetcher("pending", graphQLDataFetchers.getPendingStateDataFetcher()) .dataFetcher( "protocolVersion", graphQLDataFetchers.getProtocolVersionDataFetcher())) .type( 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 index dc0017c994..8dd67c1f73 100644 --- 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 @@ -15,37 +15,46 @@ import tech.pegasys.pantheon.ethereum.core.Hash; import tech.pegasys.pantheon.ethereum.core.Transaction; +import java.util.Optional; + public class TransactionWithMetadata { private final Transaction transaction; - private final long blockNumber; - private final Hash blockHash; - private final int transactionIndex; + private final Optional blockNumber; + private final Optional blockHash; + private final Optional transactionIndex; + + public TransactionWithMetadata(final Transaction transaction) { + this.transaction = transaction; + this.blockNumber = Optional.empty(); + this.blockHash = Optional.empty(); + this.transactionIndex = Optional.empty(); + } - public TransactionWithMetadata( + 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; + this.blockNumber = Optional.of(blockNumber); + this.blockHash = Optional.of(blockHash); + this.transactionIndex = Optional.of(transactionIndex); } public Transaction getTransaction() { return transaction; } - public long getBlockNumber() { + public Optional getBlockNumber() { return blockNumber; } - public Hash getBlockHash() { + public Optional getBlockHash() { return blockHash; } - public int getTransactionIndex() { + public Optional getTransactionIndex() { return transactionIndex; } } diff --git a/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/PendingStateAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/PendingStateAdapter.java new file mode 100644 index 0000000000..68f305c5d2 --- /dev/null +++ b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/PendingStateAdapter.java @@ -0,0 +1,127 @@ +/* + * 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.Wei; +import tech.pegasys.pantheon.ethereum.core.WorldState; +import tech.pegasys.pantheon.ethereum.eth.transactions.PendingTransactions; +import tech.pegasys.pantheon.ethereum.graphqlrpc.GraphQLDataFetcherContext; +import tech.pegasys.pantheon.ethereum.graphqlrpc.internal.BlockchainQuery; +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.BytesValue; +import tech.pegasys.pantheon.util.uint.UInt256; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import graphql.schema.DataFetchingEnvironment; + +@SuppressWarnings("unused") // reflected by GraphQL +public class PendingStateAdapter extends AdapterBase { + + private final PendingTransactions pendingTransactions; + + public PendingStateAdapter(final PendingTransactions pendingTransactions) { + this.pendingTransactions = pendingTransactions; + } + + public Integer getTransactionCount() { + return pendingTransactions.size(); + } + + public List getTransactions() { + return pendingTransactions.getTransactionInfo().stream() + .map(PendingTransactions.TransactionInfo::getTransaction) + .map(TransactionWithMetadata::new) + .map(TransactionAdapter::new) + .collect(Collectors.toList()); + } + + // until the miner can expose the current "proposed block" we have no + // speculative environment, so estimate against latest. + public Optional getAccount( + final DataFetchingEnvironment dataFetchingEnvironment) { + final BlockchainQuery blockchainQuery = + ((GraphQLDataFetcherContext) dataFetchingEnvironment.getContext()).getBlockchainQuery(); + final Address addr = dataFetchingEnvironment.getArgument("address"); + final Long blockNumber = dataFetchingEnvironment.getArgument("blockNumber"); + final long latestBlockNumber = blockchainQuery.latestBlock().get().getHeader().getNumber(); + final Optional optionalWorldState = + blockchainQuery.getWorldState(latestBlockNumber); + return optionalWorldState + .flatMap(worldState -> Optional.ofNullable(worldState.get(addr))) + .map(AccountAdapter::new); + } + + // until the miner can expose the current "proposed block" we have no + // speculative environment, so estimate against latest. + public Optional getEstimateGas(final DataFetchingEnvironment environment) { + final Optional result = getCall(environment); + return result.map(CallResult::getGasUsed); + } + + // until the miner can expose the current "proposed block" we have no + // speculative environment, so estimate against latest. + public Optional getCall(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 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.processAtHead(param); + 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/TransactionAdapter.java b/ethereum/graphqlrpc/src/main/java/tech/pegasys/pantheon/ethereum/graphqlrpc/internal/pojoadapter/TransactionAdapter.java index c9237f1b1a..bc52391671 100644 --- 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 @@ -17,7 +17,6 @@ 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; @@ -49,18 +48,18 @@ public Optional getNonce() { } public Optional getIndex() { - return Optional.of(transactionWithMetadata.getTransactionIndex()); + return 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; + final Optional txBlockNumber = transactionWithMetadata.getBlockNumber(); + final Optional bn = Optional.ofNullable(environment.getArgument("block")); + if (!txBlockNumber.isPresent() && !bn.isPresent()) { + return Optional.empty(); } return query - .getWorldState(blockNumber) + .getWorldState(bn.orElseGet(txBlockNumber::get)) .map( mutableWorldState -> new AccountAdapter( @@ -69,14 +68,14 @@ public Optional getFrom(final DataFetchingEnvironment environmen 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; + final Optional txBlockNumber = transactionWithMetadata.getBlockNumber(); + final Optional bn = Optional.ofNullable(environment.getArgument("block")); + if (!txBlockNumber.isPresent() && !bn.isPresent()) { + return Optional.empty(); } return query - .getWorldState(blockNumber) + .getWorldState(bn.orElseGet(txBlockNumber::get)) .flatMap( ws -> transactionWithMetadata @@ -102,11 +101,10 @@ public Optional getInputData() { } 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); + return transactionWithMetadata + .getBlockHash() + .flatMap(blockHash -> getBlockchainQuery(environment).blockByHash(blockHash)) + .map(NormalBlockAdapter::new); } public Optional getStatus(final DataFetchingEnvironment environment) { @@ -146,11 +144,12 @@ public Optional getCreatedContract(final DataFetchingEnvironment 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 txBlockNumber = transactionWithMetadata.getBlockNumber(); + final Optional bn = Optional.ofNullable(environment.getArgument("block")); + if (!txBlockNumber.isPresent() && !bn.isPresent()) { + return Optional.empty(); } + final long blockNumber = bn.orElseGet(txBlockNumber::get); final Optional ws = query.getWorldState(blockNumber); if (ws.isPresent()) { @@ -171,10 +170,10 @@ public List getLogs(final DataFetchingEnvironment environment) { final List logs = BlockchainQuery.generateLogWithMetadataForTransaction( tranRpt.get().getReceipt(), - transactionWithMetadata.getBlockNumber(), - transactionWithMetadata.getBlockHash(), + transactionWithMetadata.getBlockNumber().get(), + transactionWithMetadata.getBlockHash().get(), hash, - transactionWithMetadata.getTransactionIndex(), + transactionWithMetadata.getTransactionIndex().get(), false); for (final LogWithMetadata log : logs) { results.add(new LogAdapter(log)); 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 index 2bc994b759..faebb96ac4 100644 --- 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 @@ -45,7 +45,9 @@ import java.net.URL; import java.nio.file.Paths; +import java.time.Instant; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Optional; @@ -139,6 +141,13 @@ public void setupTest() throws Exception { .thenReturn(ValidationResult.valid()); final PendingTransactions pendingTransactionsMock = mock(PendingTransactions.class); when(transactionPoolMock.getPendingTransactions()).thenReturn(pendingTransactionsMock); + when(pendingTransactionsMock.getTransactionInfo()) + .thenReturn( + Collections.singleton( + new PendingTransactions.TransactionInfo( + Transaction.builder().nonce(42).gasLimit(654321).build(), + true, + Instant.ofEpochSecond(Integer.MAX_VALUE)))); stateArchive = createInMemoryWorldStateArchive(); GENESIS_CONFIG.writeStateTo(stateArchive.getMutable()); 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 index 1a06844f0b..61e1f4c251 100644 --- 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 @@ -84,6 +84,8 @@ public static Collection specs() { specs.add("eth_sendRawTransaction_unsignedTransaction"); specs.add("eth_getLogs_matchTopic"); + + specs.add("graphql_pending"); return specs; } diff --git a/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphql_pending.json b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphql_pending.json new file mode 100644 index 0000000000..0acb42b455 --- /dev/null +++ b/ethereum/graphqlrpc/src/test/resources/tech/pegasys/pantheon/ethereum/graphqlrpc/graphql_pending.json @@ -0,0 +1,27 @@ +{ + "request": + "{ pending { transactionCount transactions { nonce gas } account(address:\"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\") { balance} estimateGas(data:{}) call (data : {from : \"a94f5374fce5edbc8e2a8697c15331677e6ebf0b\", to: \"0x6295ee1b4f6dd65047762f924ecd367c17eabf8f\", data :\"0x12a7b914\"}){data status}} }", + + "response": { + "data": { + "pending": { + "transactionCount": 0, + "transactions": [ + { + "nonce": 42, + "gas": 654321 + } + ], + "account": { + "balance": "0x140" + }, + "estimateGas": 21000, + "call": { + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "status": 1 + } + } + } + }, + "statusCode": 200 +} \ No newline at end of file