From e4c98bfe9e9bbdae335c967792a55bb8df458fee Mon Sep 17 00:00:00 2001 From: Igor Artamonov Date: Thu, 21 Nov 2024 22:41:20 +0000 Subject: [PATCH] solution: implement RPC mapping for eth_simulateV1 --- .../etherjar/contract/ContractCall.java | 2 +- .../emeraldpay/etherjar/rpc/EthCommands.java | 11 + .../etherjar/rpc/EtherjarModule.java | 5 + .../rpc/json/AddressDeserializer.java | 8 + .../etherjar/rpc/json/BlockJson.java | 55 +++-- .../etherjar/rpc/json/BlockOverridesJson.java | 152 +++++++++++++ .../etherjar/rpc/json/BlockSimulatedJson.java | 123 +++++++++++ .../etherjar/rpc/json/Hex32Deserializer.java | 8 + .../etherjar/rpc/json/HexDataSerializer.java | 18 ++ .../etherjar/rpc/json/SimulateJson.java | 148 +++++++++++++ .../etherjar/rpc/json/StateOverrideJson.java | 122 +++++++++++ .../rpc/json/TransactionCallJson.java | 204 +++++++++++++++--- .../etherjar/rpc/json/TransactionLogJson.java | 2 +- .../etherjar/rpc/json/WithdrawalJson.java | 5 +- .../rpc/JacksonEthRpcConverterSpec.groovy | 11 +- .../rpc/json/BlockSimulatedJsonSpec.groovy | 128 +++++++++++ .../etherjar/rpc/json/SimulateJsonSpec.groovy | 67 ++++++ .../test/resources/simulate/simulate-1.json | 30 +++ .../test/resources/simulate/simulated-1.json | 77 +++++++ 19 files changed, 1112 insertions(+), 64 deletions(-) create mode 100644 etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockOverridesJson.java create mode 100644 etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJson.java create mode 100644 etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/SimulateJson.java create mode 100644 etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/StateOverrideJson.java create mode 100644 etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy create mode 100644 etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/SimulateJsonSpec.groovy create mode 100644 etherjar-rpc-json/src/test/resources/simulate/simulate-1.json create mode 100644 etherjar-rpc-json/src/test/resources/simulate/simulated-1.json diff --git a/etherjar-contract/src/main/java/io/emeraldpay/etherjar/contract/ContractCall.java b/etherjar-contract/src/main/java/io/emeraldpay/etherjar/contract/ContractCall.java index 70624d35..5eafd8d0 100644 --- a/etherjar-contract/src/main/java/io/emeraldpay/etherjar/contract/ContractCall.java +++ b/etherjar-contract/src/main/java/io/emeraldpay/etherjar/contract/ContractCall.java @@ -32,7 +32,7 @@ public ContractData getData() { public TransactionCallJson toJson() { TransactionCallJson json = new TransactionCallJson(); json.setTo(contract); - json.setData(data.toData()); + json.setInput(data.toData()); return json; } diff --git a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java index 864ead09..b33ab32c 100644 --- a/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java +++ b/etherjar-rpc-api/src/main/java/io/emeraldpay/etherjar/rpc/EthCommands.java @@ -348,6 +348,17 @@ public RpcCall call(TransactionCallJson call, BlockTag block) { return RpcCall.create("eth_call", call, block.getCode()).converted(HexData.class, HexData::from); } + /** + * The eth_simulateV1 method allows the simulation of multiple blocks and transactions without creating transactions or blocks on the blockchain. It functions similarly to eth_call, but offers more control. + * + * @param payload the simulation payload + * @param block The simulated blocks will be built on top of this. If null, the current block is used. + * @return simulated block + */ + public RpcCall simulateV1(SimulateJson payload, BlockTag block) { + return RpcCall.create("eth_simulateV1", BlockSimulatedJson.class, payload, block.getCode()); + } + /** * Executes a new message call immediately without creating a transaction on the block chain. * diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/EtherjarModule.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/EtherjarModule.java index 9a77f7db..afc3bd54 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/EtherjarModule.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/EtherjarModule.java @@ -27,5 +27,10 @@ public EtherjarModule() { addDeserializer(Address.class, new AddressDeserializer()); addDeserializer(MethodId.class, new MethodIdDeserializer()); addDeserializer(Bloom.class, new BloomDeserializer()); + + addKeySerializer(Address.class, new HexDataSerializer.AsKey()); + + addKeyDeserializer(Address.class, new AddressDeserializer.FromKey()); + addKeyDeserializer(Hex32.class, new Hex32Deserializer.FromKey()); } } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/AddressDeserializer.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/AddressDeserializer.java index 2a5c4cfc..14553261 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/AddressDeserializer.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/AddressDeserializer.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.KeyDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import io.emeraldpay.etherjar.domain.Address; @@ -31,4 +32,11 @@ public Address deserialize(JsonParser p, DeserializationContext ctxt) throws IOE } throw JsonMappingException.from(p,"Invalid Address type: " + token); } + + public static class FromKey extends KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return Address.from(key); + } + } } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockJson.java index d740aa63..b5871cae 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockJson.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockJson.java @@ -466,34 +466,33 @@ public BlockJson copy() { @Override public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof BlockJson)) return false; - - BlockJson blockJson = (BlockJson) o; - - if (!Objects.equals(number, blockJson.number)) return false; - if (!Objects.equals(hash, blockJson.hash)) return false; - if (!Objects.equals(parentHash, blockJson.parentHash)) return false; - if (!Objects.equals(sha3Uncles, blockJson.sha3Uncles)) return false; - if (!Objects.equals(logsBloom, blockJson.logsBloom)) return false; - if (!Objects.equals(transactionsRoot, blockJson.transactionsRoot)) return false; - if (!Objects.equals(stateRoot, blockJson.stateRoot)) return false; - if (!Objects.equals(receiptsRoot, blockJson.receiptsRoot)) return false; - if (!Objects.equals(miner, blockJson.miner)) return false; - if (!Objects.equals(difficulty, blockJson.difficulty)) return false; - if (!Objects.equals(totalDifficulty, blockJson.totalDifficulty)) return false; - if (!Objects.equals(extraData, blockJson.extraData)) return false; - if (!Objects.equals(size, blockJson.size)) return false; - if (!Objects.equals(gasLimit, blockJson.gasLimit)) return false; - if (!Objects.equals(gasUsed, blockJson.gasUsed)) return false; - if (!Objects.equals(timestamp, blockJson.timestamp)) return false; - if (!Objects.equals(transactions, blockJson.transactions)) return false; - if (!Objects.equals(baseFeePerGas, blockJson.baseFeePerGas)) return false; - if (!Objects.equals(mixHash, blockJson.mixHash)) return false; - if (!Objects.equals(nonce, blockJson.nonce)) return false; - if (!Objects.equals(withdrawalsRoot, blockJson.withdrawalsRoot)) return false; - if (!Objects.equals(withdrawals, blockJson.withdrawals)) return false; - return Objects.equals(uncles, blockJson.uncles); + if (!(o instanceof BlockJson blockJson)) return false; + return Objects.equals(number, blockJson.number) + && Objects.equals(hash, blockJson.hash) + && Objects.equals(parentHash, blockJson.parentHash) + && Objects.equals(sha3Uncles, blockJson.sha3Uncles) + && Objects.equals(logsBloom, blockJson.logsBloom) + && Objects.equals(transactionsRoot, blockJson.transactionsRoot) + && Objects.equals(stateRoot, blockJson.stateRoot) + && Objects.equals(receiptsRoot, blockJson.receiptsRoot) + && Objects.equals(miner, blockJson.miner) + && Objects.equals(difficulty, blockJson.difficulty) + && Objects.equals(totalDifficulty, blockJson.totalDifficulty) + && Objects.equals(extraData, blockJson.extraData) + && Objects.equals(mixHash, blockJson.mixHash) + && Objects.equals(nonce, blockJson.nonce) + && Objects.equals(size, blockJson.size) + && Objects.equals(gasLimit, blockJson.gasLimit) + && Objects.equals(gasUsed, blockJson.gasUsed) + && Objects.equals(timestamp, blockJson.timestamp) + && Objects.equals(transactions, blockJson.transactions) + && Objects.equals(uncles, blockJson.uncles) + && Objects.equals(baseFeePerGas, blockJson.baseFeePerGas) + && Objects.equals(withdrawalsRoot, blockJson.withdrawalsRoot) + && Objects.equals(withdrawals, blockJson.withdrawals) + && Objects.equals(blobGasUsed, blockJson.blobGasUsed) + && Objects.equals(excessBlobGas, blockJson.excessBlobGas) + && Objects.equals(parentBeaconBlockRoot, blockJson.parentBeaconBlockRoot); } @Override diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockOverridesJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockOverridesJson.java new file mode 100644 index 00000000..9398b8de --- /dev/null +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockOverridesJson.java @@ -0,0 +1,152 @@ +package io.emeraldpay.etherjar.rpc.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.emeraldpay.etherjar.domain.Address; +import io.emeraldpay.etherjar.domain.Wei; +import io.emeraldpay.etherjar.hex.Hex32; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * The fields of this object customize the block as part of which a call is simulated. This object can be passed to eth_call, eth_simulateV1 as well as debug_traceCall methods. + * + * @see eth_simulateV1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BlockOverridesJson { + + /** + * Block number + */ + @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) + private Long number; + + /** + * The previous value of randomness beacon + */ + private Hex32 prevRandao; + + /** + * Block timestamp + */ + @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) + private Long time; + + /** + * Gas limit. + */ + @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) + private Long gasLimit; + + /** + * Fee recipient (also known as coinbase). + */ + private Address feeRecipient; + + /** + * Withdrawals made by validators. + */ + private List withdrawals; + + /** + * Base fee per unit of gas (see EIP-1559). + */ + private Wei baseFeePerGas; + + /** + * Base fee per unit of blob gas (see EIP-4844). + */ + private Wei blobBaseFee; + + public Long getNumber() { + return number; + } + + public void setNumber(Long number) { + this.number = number; + } + + public Hex32 getPrevRandao() { + return prevRandao; + } + + public void setPrevRandao(Hex32 prevRandao) { + this.prevRandao = prevRandao; + } + + public Long getTime() { + return time; + } + + public void setTime(Long time) { + this.time = time; + } + + public Long getGasLimit() { + return gasLimit; + } + + public void setGasLimit(Long gasLimit) { + this.gasLimit = gasLimit; + } + + public Address getFeeRecipient() { + return feeRecipient; + } + + public void setFeeRecipient(Address feeRecipient) { + this.feeRecipient = feeRecipient; + } + + public List getWithdrawals() { + return withdrawals; + } + + public void setWithdrawals(List withdrawals) { + this.withdrawals = withdrawals; + } + + public BlockOverridesJson appendWithdrawal(WithdrawalJson withdrawal) { + if (withdrawals == null) { + withdrawals = new ArrayList<>(); + } + withdrawals.add(withdrawal); + return this; + } + + public Wei getBaseFeePerGas() { + return baseFeePerGas; + } + + public void setBaseFeePerGas(Wei baseFeePerGas) { + this.baseFeePerGas = baseFeePerGas; + } + + public Wei getBlobBaseFee() { + return blobBaseFee; + } + + public void setBlobBaseFee(Wei blobBaseFee) { + this.blobBaseFee = blobBaseFee; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BlockOverridesJson that)) return false; + return Objects.equals(number, that.number) && Objects.equals(prevRandao, that.prevRandao) && Objects.equals(time, that.time) && Objects.equals(gasLimit, that.gasLimit) && Objects.equals(feeRecipient, that.feeRecipient) && Objects.equals(withdrawals, that.withdrawals) && Objects.equals(baseFeePerGas, that.baseFeePerGas) && Objects.equals(blobBaseFee, that.blobBaseFee); + } + + @Override + public int hashCode() { + return Objects.hash(number, prevRandao, time, gasLimit, feeRecipient, withdrawals, baseFeePerGas, blobBaseFee); + } +} diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJson.java new file mode 100644 index 00000000..3009e878 --- /dev/null +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJson.java @@ -0,0 +1,123 @@ +package io.emeraldpay.etherjar.rpc.json; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.emeraldpay.etherjar.domain.Wei; +import io.emeraldpay.etherjar.hex.HexData; +import io.emeraldpay.etherjar.hex.HexQuantity; + +import java.util.List; +import java.util.Objects; + +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class BlockSimulatedJson extends BlockJson { + + /** + * Log events emitted during call. This includes ETH logs, if traceTransfers is enabled + */ + private List calls; + + public List getCalls() { + return calls; + } + + public void setCalls(List calls) { + this.calls = calls; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class CallResultLog { + + /** + * Transactions return data + */ + private HexData returnData; + + /** + * Log events emitted during call. This includes ETH logs, if traceTransfers is enabled + */ + private List logs; + + /** + * Gas used by the transaction + */ + private HexQuantity gasUsed; + + /** + * Status indicating that the transaction succeeded + * 1 is success + */ + @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) + private Long status; + + public HexData getReturnData() { + return returnData; + } + + public void setReturnData(HexData returnData) { + this.returnData = returnData; + } + + public List getLogs() { + return logs; + } + + public void setLogs(List logs) { + this.logs = logs; + } + + public HexQuantity getGasUsed() { + return gasUsed; + } + + public void setGasUsed(HexQuantity gasUsed) { + this.gasUsed = gasUsed; + } + + public Long getStatus() { + return status; + } + + public void setStatus(Long status) { + this.status = status; + } + + @JsonIgnore + public boolean isSuccessful() { + return status == 1; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof CallResultLog that)) return false; + return Objects.equals(returnData, that.returnData) + && Objects.equals(logs, that.logs) + && Objects.equals(gasUsed, that.gasUsed) + && Objects.equals(status, that.status); + } + + @Override + public int hashCode() { + return Objects.hash(returnData, logs, gasUsed, status); + } + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BlockSimulatedJson that)) return false; + if (!super.equals(o)) return false; + return Objects.equals(calls, that.calls); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), calls); + } +} + diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/Hex32Deserializer.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/Hex32Deserializer.java index f276d2fe..593d3a1e 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/Hex32Deserializer.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/Hex32Deserializer.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.core.JsonToken; import com.fasterxml.jackson.databind.DeserializationContext; import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.KeyDeserializer; import com.fasterxml.jackson.databind.deser.std.StdDeserializer; import io.emeraldpay.etherjar.hex.Hex32; import io.emeraldpay.etherjar.hex.HexData; @@ -32,4 +33,11 @@ public Hex32 deserialize(JsonParser p, DeserializationContext ctxt) throws IOExc } throw JsonMappingException.from(p,"Invalid Hex32 type: " + token); } + + public static class FromKey extends KeyDeserializer { + @Override + public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException { + return Hex32.from(key); + } + } } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/HexDataSerializer.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/HexDataSerializer.java index ba5ad949..c87a7514 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/HexDataSerializer.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/HexDataSerializer.java @@ -36,4 +36,22 @@ public void serialize(HexData value, JsonGenerator gen, SerializerProvider provi gen.writeString(value.toHex()); } } + + /** + * To serialize Hex Data (including Hex32 and Address) as a JSON object key. + */ + public static class AsKey extends StdSerializer { + public AsKey() { + super(HexData.class); + } + + @Override + public void serialize(HexData value, JsonGenerator gen, SerializerProvider provider) throws IOException { + if (value == null) { + gen.writeNull(); + } else { + gen.writeFieldName(value.toHex()); + } + } + } } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/SimulateJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/SimulateJson.java new file mode 100644 index 00000000..b906c549 --- /dev/null +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/SimulateJson.java @@ -0,0 +1,148 @@ +package io.emeraldpay.etherjar.rpc.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.emeraldpay.etherjar.domain.Address; + +import java.util.*; + +/** + * eth_simulateV1 payload. + * @see eth_simulateV1 + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SimulateJson { + + /** + * REQUIRED
+ * + * Definition of blocks that can contain calls and overrides + */ + private List blockStateCalls; + + /** + * When true, the eth_simulateV1 does all the validation that a normal EVM would do, except contract sender and signature checks. When false, eth_simulateV1 behaves like eth_call. + */ + private Boolean validation; + + /** + * Adds ETH transfers as ERC20 transfer events to the logs. These transfers have emitter contract parameter set as address(0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee). This allows you to track movements of ETH in your calls. + */ + private Boolean traceTransfers; + + /** + * When true, the method returns full transaction objects, otherwise, just hashes are returned. + */ + private Boolean returnFullTransactions; + + public List getBlockStateCalls() { + return blockStateCalls; + } + + public void setBlockStateCalls(List blockStateCalls) { + this.blockStateCalls = blockStateCalls; + } + + public Boolean getTraceTransfers() { + return traceTransfers; + } + + public void setTraceTransfers(Boolean traceTransfers) { + this.traceTransfers = traceTransfers; + } + + public Boolean getValidation() { + return validation; + } + + public void setValidation(Boolean validation) { + this.validation = validation; + } + + public Boolean getReturnFullTransactions() { + return returnFullTransactions; + } + + public void setReturnFullTransactions(Boolean returnFullTransactions) { + this.returnFullTransactions = returnFullTransactions; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof SimulateJson that)) return false; + return Objects.equals(blockStateCalls, that.blockStateCalls) + && Objects.equals(traceTransfers, that.traceTransfers) + && Objects.equals(validation, that.validation) + && Objects.equals(returnFullTransactions, that.returnFullTransactions); + } + + @Override + public int hashCode() { + return Objects.hash(blockStateCalls, traceTransfers, validation, returnFullTransactions); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static class BlockStateCalls { + private BlockOverridesJson blockOverrides; + + private Map stateOverrides; + + private List calls; + + public BlockOverridesJson getBlockOverrides() { + return blockOverrides; + } + + public void setBlockOverrides(BlockOverridesJson blockOverrides) { + this.blockOverrides = blockOverrides; + } + + public Map getStateOverrides() { + return stateOverrides; + } + + public void setStateOverrides(Map stateOverrides) { + this.stateOverrides = stateOverrides; + } + + public List getCalls() { + return calls; + } + + public void setCalls(List calls) { + this.calls = calls; + } + + public BlockStateCalls appendCall(TransactionCallJson call) { + if (calls == null) { + calls = new ArrayList<>(); + } + calls.add(call); + return this; + } + + public BlockStateCalls overrideState(Address address, StateOverrideJson state) { + if (stateOverrides == null) { + stateOverrides = new HashMap<>(); + } + stateOverrides.put(address, state); + return this; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof BlockStateCalls that)) return false; + return Objects.equals(blockOverrides, that.blockOverrides) && Objects.equals(stateOverrides, that.stateOverrides) && Objects.equals(calls, that.calls); + } + + @Override + public int hashCode() { + return Objects.hash(blockOverrides, stateOverrides, calls); + } + } + +} diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/StateOverrideJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/StateOverrideJson.java new file mode 100644 index 00000000..627de510 --- /dev/null +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/StateOverrideJson.java @@ -0,0 +1,122 @@ +package io.emeraldpay.etherjar.rpc.json; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import io.emeraldpay.etherjar.domain.Address; +import io.emeraldpay.etherjar.domain.Wei; +import io.emeraldpay.etherjar.hex.Hex32; +import io.emeraldpay.etherjar.hex.HexData; + +import java.util.Map; +import java.util.Objects; + +/** + * The state override set is an optional address-to-state mapping, used in eth_call and eth_simulateV1, where each entry specifies some state to be ephemerally overridden prior to executing the call. + *
+ * The goal of the state override set is manyfold: + *
    + *
  • It can be used by websites to reduce the amount of contract code needed to be deployed on chain. Code that simply returns internal state or does pre-defined validations can be kept off chain and fed to the node on-demand.
  • + *
  • It can be used for smart contract analysis by extending the code deployed on chain with custom methods and invoking them. This avoids having to download and reconstruct the entire state in a sandbox to run custom code against.
  • + *
  • It can be used to debug smart contracts in an already deployed large suite of contracts by selectively overriding some code or state and seeing how execution changes. Specialized tooling will probably be necessary.
  • + *
  • It can be used to override ecrecover precompile to spoof signatures
  • + *
+ * + * @see State Override Set + */ +@JsonIgnoreProperties(ignoreUnknown = true) +@JsonInclude(JsonInclude.Include.NON_NULL) +public class StateOverrideJson { + + /** + * Fake balance to set for the account before executing the call. + */ + private Wei balance; + + /** + * Fake nonce to set for the account before executing the call. + */ + @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) + private Long nonce; + + /** + * Fake EVM bytecode to inject into the account before executing the call. + */ + private HexData code; + + /** + * Fake key-value mapping to override all slots in the account storage before executing the call. + */ + private Map state; + + /** + * Fake key-value mapping to override individual slots in the account storage before executing the call. + */ + private Map stateDiff; + + /** + * Moves precompile to given address + */ + private Address movePrecompileToAddress; + + public Wei getBalance() { + return balance; + } + + public void setBalance(Wei balance) { + this.balance = balance; + } + + public Long getNonce() { + return nonce; + } + + public void setNonce(Long nonce) { + this.nonce = nonce; + } + + public HexData getCode() { + return code; + } + + public void setCode(HexData code) { + this.code = code; + } + + public Map getState() { + return state; + } + + public void setState(Map state) { + this.state = state; + } + + public Map getStateDiff() { + return stateDiff; + } + + public void setStateDiff(Map stateDiff) { + this.stateDiff = stateDiff; + } + + public Address getMovePrecompileToAddress() { + return movePrecompileToAddress; + } + + public void setMovePrecompileToAddress(Address movePrecompileToAddress) { + this.movePrecompileToAddress = movePrecompileToAddress; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof StateOverrideJson that)) return false; + return Objects.equals(balance, that.balance) && Objects.equals(nonce, that.nonce) && Objects.equals(code, that.code) && Objects.equals(state, that.state) && Objects.equals(stateDiff, that.stateDiff) && Objects.equals(movePrecompileToAddress, that.movePrecompileToAddress); + } + + @Override + public int hashCode() { + return Objects.hash(balance, nonce, code, state, stateDiff, movePrecompileToAddress); + } +} diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionCallJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionCallJson.java index 8a9f02bb..31f52c7c 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionCallJson.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionCallJson.java @@ -17,36 +17,116 @@ package io.emeraldpay.etherjar.rpc.json; +import com.fasterxml.jackson.annotation.JsonAlias; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.emeraldpay.etherjar.domain.Address; import io.emeraldpay.etherjar.domain.Wei; +import io.emeraldpay.etherjar.hex.Hex32; import io.emeraldpay.etherjar.hex.HexData; import java.io.Serializable; +import java.util.List; +import java.util.Objects; @JsonInclude(JsonInclude.Include.NON_NULL) public class TransactionCallJson implements Serializable { + /** + * Address the transaction is simulated to have been sent from. Defaults to first account in the local keystore or the 0x00..0 address if no local accounts are available. + */ private Address from; + + /** + * Address the transaction is sent to. + */ private Address to; + + /** + * Maximum gas allowance for the code execution to avoid infinite loops. Defaults to 2^63 or whatever value the node operator specified via --rpc.gascap. + */ @JsonDeserialize(using = HexLongDeserializer.class) @JsonSerialize(using = HexLongSerializer.class) private Long gas; + + /** + * Number of wei to simulate paying for each unit of gas during execution. Defaults to 1 gwei. + */ private Wei gasPrice; + + /** + * Maximum fee per gas the transaction should pay in total. Relevant for type-2 transactions. + */ + private Wei maxFeePerGas; + + /** + * Maximum tip per gas that's given directly to the miner. Relevant for type-2 transactions. + */ + private Wei maxPriorityFeePerGas; + + /** + * Amount of wei to simulate sending along with the transaction. Defaults to 0. + */ private Wei value; - private HexData data; + + /** + * Nonce of sender account. + */ @JsonDeserialize(using = HexLongDeserializer.class) @JsonSerialize(using = HexLongSerializer.class) private Long nonce; + /** + * Binary data to send to the target contract. Generally the 4 byte hash of the method signature followed by the ABI encoded parameters. For details please see the Ethereum Contract ABI. This field was previously called data. + */ + @JsonAlias({"data"}) + private HexData input; + + /** + * A list of addresses and storage keys that the transaction plans to access. Used in non-legacy, i.e. type 1 and 2 transactions. + */ + private List accessList; + + /** + * Transaction only valid on networks with this chain ID. Used in non-legacy, i.e. type 1 and 2 transactions. + */ + @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) + private Long chainId; + + /** + * Max fee per blob gas the transaction should pay in total. Relevant for blob transactions. + */ + private Wei maxFeePerBlobGas; + + /** + * Blob versioned hashes that can be accessed from EVM. They will be validated in case blobs also provided. Relevant for blob transactions. + */ + private List blobVersionedHashes; + + /** + * EIP-4844 Blobs. + */ + private List blobs; + + /** + * Commitments to EIP-4844 Blobs. They will be generated if only blobs are present and validated if both blobs and commitments are provided. + */ + private List commitments; + + /** + * Proofs for EIP-4844 Blobs. They must be provided along with commitments. Else they will be generated. + */ + private List proofs; + + public TransactionCallJson() { } - public TransactionCallJson(Address to, HexData data) { + public TransactionCallJson(Address to, HexData input) { this.to = to; - this.data = data; + this.input = input; } public TransactionCallJson(Address from, Address to, Wei value) { @@ -55,12 +135,12 @@ public TransactionCallJson(Address from, Address to, Wei value) { this.value = value; } - public TransactionCallJson(Address from, Address to, Long gas, Wei value, HexData data) { + public TransactionCallJson(Address from, Address to, Long gas, Wei value, HexData input) { this.from = from; this.to = to; this.gas = gas; this.value = value; - this.data = data; + this.input = input; } public Address getFrom() { @@ -103,12 +183,12 @@ public void setValue(Wei value) { this.value = value; } - public HexData getData() { - return data; + public HexData getInput() { + return input; } - public void setData(HexData data) { - this.data = data; + public void setInput(HexData input) { + this.input = input; } public Long getNonce() { @@ -119,27 +199,101 @@ public void setNonce(Long nonce) { this.nonce = nonce; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (!(o instanceof TransactionCallJson)) return false; + public Wei getMaxFeePerGas() { + return maxFeePerGas; + } + + public void setMaxFeePerGas(Wei maxFeePerGas) { + this.maxFeePerGas = maxFeePerGas; + } + + public Wei getMaxPriorityFeePerGas() { + return maxPriorityFeePerGas; + } + + public void setMaxPriorityFeePerGas(Wei maxPriorityFeePerGas) { + this.maxPriorityFeePerGas = maxPriorityFeePerGas; + } + + public List getAccessList() { + return accessList; + } + + public void setAccessList(List accessList) { + this.accessList = accessList; + } + + public Long getChainId() { + return chainId; + } + + public void setChainId(Long chainId) { + this.chainId = chainId; + } + + public Wei getMaxFeePerBlobGas() { + return maxFeePerBlobGas; + } + + public void setMaxFeePerBlobGas(Wei maxFeePerBlobGas) { + this.maxFeePerBlobGas = maxFeePerBlobGas; + } + + public List getBlobVersionedHashes() { + return blobVersionedHashes; + } + + public void setBlobVersionedHashes(List blobVersionedHashes) { + this.blobVersionedHashes = blobVersionedHashes; + } + + public List getBlobs() { + return blobs; + } + + public void setBlobs(List blobs) { + this.blobs = blobs; + } + + public List getCommitments() { + return commitments; + } - TransactionCallJson that = (TransactionCallJson) o; + public void setCommitments(List commitments) { + this.commitments = commitments; + } + + public List getProofs() { + return proofs; + } - if (from != null ? !from.equals(that.from) : that.from != null) return false; - if (!to.equals(that.to)) return false; - if (gas != null ? !gas.equals(that.gas) : that.gas != null) return false; - if (gasPrice != null ? !gasPrice.equals(that.gasPrice) : that.gasPrice != null) return false; - if (value != null ? !value.equals(that.value) : that.value != null) return false; - if (nonce != null ? !nonce.equals(that.nonce) : that.nonce != null) return false; - return data != null ? data.equals(that.data) : that.data == null; + public void setProofs(List proofs) { + this.proofs = proofs; + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof TransactionCallJson that)) return false; + return Objects.equals(from, that.from) + && Objects.equals(to, that.to) + && Objects.equals(gas, that.gas) + && Objects.equals(gasPrice, that.gasPrice) + && Objects.equals(maxFeePerGas, that.maxFeePerGas) + && Objects.equals(maxPriorityFeePerGas, that.maxPriorityFeePerGas) + && Objects.equals(value, that.value) + && Objects.equals(nonce, that.nonce) + && Objects.equals(input, that.input) + && Objects.equals(accessList, that.accessList) + && Objects.equals(chainId, that.chainId) + && Objects.equals(maxFeePerBlobGas, that.maxFeePerBlobGas) + && Objects.equals(blobVersionedHashes, that.blobVersionedHashes) + && Objects.equals(blobs, that.blobs) + && Objects.equals(commitments, that.commitments) + && Objects.equals(proofs, that.proofs); } @Override public int hashCode() { - int result = from != null ? from.hashCode() : 0; - result = 31 * result + to.hashCode(); - result = 31 * result + (data != null ? data.hashCode() : 0); - return result; + return Objects.hash(from, to, gas, gasPrice, maxFeePerGas, maxPriorityFeePerGas, value, nonce, input, accessList, chainId, maxFeePerBlobGas, blobVersionedHashes, blobs, commitments, proofs); } } diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionLogJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionLogJson.java index 6e47ebf5..f78cb6bf 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionLogJson.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/TransactionLogJson.java @@ -36,7 +36,7 @@ public class TransactionLogJson implements TransactionRef, Serializable { /** - * true when the log was removed, due to a chain reorganization. false if its a valid log. + * A flag indicating if a log was removed in a chain reorganization. Always false for eth_simulateV1. */ private Boolean removed; diff --git a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/WithdrawalJson.java b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/WithdrawalJson.java index 7000c9db..fed38194 100644 --- a/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/WithdrawalJson.java +++ b/etherjar-rpc-json/src/main/java/io/emeraldpay/etherjar/rpc/json/WithdrawalJson.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; import io.emeraldpay.etherjar.domain.Address; import io.emeraldpay.etherjar.domain.Wei; @@ -13,15 +14,15 @@ @JsonIgnoreProperties(ignoreUnknown = true) public class WithdrawalJson { @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) private Long index; @JsonDeserialize(using = HexLongDeserializer.class) + @JsonSerialize(using = HexLongSerializer.class) private Long validatorIndex; - @JsonDeserialize(using = AddressDeserializer.class) private Address address; - @JsonDeserialize(using = WeiDeserializer.class) private Wei amount; public Long getIndex() { diff --git a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonEthRpcConverterSpec.groovy b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonEthRpcConverterSpec.groovy index a60121b2..3691be37 100644 --- a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonEthRpcConverterSpec.groovy +++ b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/JacksonEthRpcConverterSpec.groovy @@ -23,9 +23,6 @@ import io.emeraldpay.etherjar.hex.HexData import io.emeraldpay.etherjar.rpc.json.* import spock.lang.Specification -import java.time.ZoneId -import java.time.format.DateTimeFormatter - class JacksonEthRpcConverterSpec extends Specification { JacksonRpcConverter jacksonRpcConverter = new JacksonRpcConverter() @@ -33,7 +30,7 @@ class JacksonEthRpcConverterSpec extends Specification { def "Encode basic call data"() { def callData = new TransactionCallJson( to: Address.from('0x57d90b64a1a57749b0f932f1a3395792e12e7055'), - data: HexData.from('0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2') + input: HexData.from('0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2') ) def req = new RequestJson( "eth_call", @@ -45,14 +42,14 @@ class JacksonEthRpcConverterSpec extends Specification { def act = jacksonRpcConverter.toJson(req) then: - act == '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x57d90b64a1a57749b0f932f1a3395792e12e7055","data":"0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2"},"latest"],"id":1}' + act == '{"jsonrpc":"2.0","method":"eth_call","params":[{"to":"0x57d90b64a1a57749b0f932f1a3395792e12e7055","input":"0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2"},"latest"],"id":1}' } def "Encode full call data"() { def callData = new TransactionCallJson( from: Address.from("0xb7819ff807d9d52a9ce5d713dc7053e8871e077b"), to: Address.from('0x57d90b64a1a57749b0f932f1a3395792e12e7055'), - data: HexData.from('0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2'), + input: HexData.from('0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2'), gas: 100000, gasPrice: Wei.ofEthers(0.002), value: Wei.ofEthers(1.5) @@ -73,7 +70,7 @@ class JacksonEthRpcConverterSpec extends Specification { '"gas":"0x186a0",' + '"gasPrice":"0x71afd498d0000",' + '"value":"0x14d1120d7b160000",' + - '"data":"0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2"}' + + '"input":"0xa9059cbb00000000000000000000000014dd45d07d1d700579a9b7cfb3a4536890aafdc2"}' + ',"latest"],"id":1}' } diff --git a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy new file mode 100644 index 00000000..df0b512f --- /dev/null +++ b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/BlockSimulatedJsonSpec.groovy @@ -0,0 +1,128 @@ +package io.emeraldpay.etherjar.rpc.json + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.etherjar.domain.Address +import io.emeraldpay.etherjar.domain.BlockHash +import io.emeraldpay.etherjar.domain.Bloom +import io.emeraldpay.etherjar.domain.TransactionId +import io.emeraldpay.etherjar.domain.Wei +import io.emeraldpay.etherjar.hex.Hex32 +import io.emeraldpay.etherjar.hex.HexData +import io.emeraldpay.etherjar.hex.HexQuantity +import io.emeraldpay.etherjar.rpc.JacksonRpcConverter +import spock.lang.Specification + +import java.time.Instant + +class BlockSimulatedJsonSpec extends Specification { + JacksonRpcConverter jacksonRpcConverter = new JacksonRpcConverter() + ObjectMapper objectMapper = jacksonRpcConverter.getObjectMapper() + + BlockSimulatedJson block1 = new BlockSimulatedJson().tap { + baseFeePerGas = new Wei(9) + blobGasUsed = 0 + calls = [ + new BlockSimulatedJson.CallResultLog().tap { + returnData = null + logs = [ + new TransactionLogJson().tap { + address = Address.from("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + topics = [ + Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + Hex32.from("0x000000000000000000000000c000000000000000000000000000000000000000"), + Hex32.from("0x000000000000000000000000c000000000000000000000000000000000000001") + ] + data = HexData.from("0x0000000000000000000000000000000000000000000000000000000000000001") + blockNumber = 0x13d2747 + transactionHash = TransactionId.from("0xe7217784e0c3f7b35d39303b1165046e9b7e8af9b9cf80d5d5f96c3163de8f51") + transactionIndex = 0x0 + blockHash = BlockHash.from("0x5e28f54a56dc9df973a058cd54b3eeef8c67a1a613cb5db1df8a0a434c931d56") + logIndex = 0x0 + removed = false + } + ] + gasUsed = HexQuantity.from(0x5208) + status = 0x1 + }, + new BlockSimulatedJson.CallResultLog().tap { + returnData = null + logs = [ + new TransactionLogJson().tap { + address = Address.from("0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee") + topics = [ + Hex32.from("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"), + Hex32.from("0x000000000000000000000000c000000000000000000000000000000000000000"), + Hex32.from("0x000000000000000000000000c000000000000000000000000000000000000002") + ] + data = HexData.from("0x0000000000000000000000000000000000000000000000000000000000000001") + blockNumber = 0x13d2747 + transactionHash = TransactionId.from("0xf0182201606ec03701ba3a07d965fabdb4b7d06b424f226ea7ec3581802fc6fa") + transactionIndex = 0x1 + blockHash = BlockHash.from("0x5e28f54a56dc9df973a058cd54b3eeef8c67a1a613cb5db1df8a0a434c931d56") + logIndex = 0x1 + removed = false + } + ] + gasUsed = HexQuantity.from(0x5208) + status = 0x1 + } + ] + difficulty = new BigInteger("0") + excessBlobGas = 0 + extraData = null + gasLimit = 0x1c9c380 + gasUsed = 0xa410 + hash = BlockHash.from("0x5e28f54a56dc9df973a058cd54b3eeef8c67a1a613cb5db1df8a0a434c931d56") + logsBloom = Bloom.from("0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") + miner = Address.from("0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97") + mixHash = Hex32.from("0x0000000000000000000000000000000000000000000000000000000000000000") + nonce = HexData.from("0x0000000000000000") + number = 0x13d2747 + parentBeaconBlockRoot = Hex32.from("0x0000000000000000000000000000000000000000000000000000000000000000") + parentHash = BlockHash.from("0xd24222b93a05a066cf79dc20e333f5aa6bb06d36eb50eb2b6b0b744b937e7975") + receiptsRoot = Hex32.from("0x75308898d571eafb5cd8cde8278bf5b3d13c5f6ec074926de3bb895b519264e1") + sha3Uncles = Hex32.from("0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347") + size = 0x298 + stateRoot = Hex32.from("0xbb0740745211507e2a2a6cdb627dfa171ef5050ad2a01e5401c2e3df4be5b919") + timestamp = Instant.ofEpochSecond(0x66ec2853) + totalDifficulty = new BigInteger("c70d815d562d3cfa955", 16) + transactions = [ + new TransactionRefJson(TransactionId.from("0xe7217784e0c3f7b35d39303b1165046e9b7e8af9b9cf80d5d5f96c3163de8f51")), + new TransactionRefJson(TransactionId.from("0xf0182201606ec03701ba3a07d965fabdb4b7d06b424f226ea7ec3581802fc6fa")) + ] + transactionsRoot = Hex32.from("0x9bdb74f3ce41f5893a02a631e904ae0d21ae8c4e416786d8dbd9cb5c54f1dc0f") + uncles = [] + withdrawals = [] + withdrawalsRoot = Hex32.from("0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") + } + + def "Encodes request"() { + setup: + JsonNode expected = objectMapper.readTree(BlockSimulatedJsonSpec.classLoader.getResourceAsStream("simulate/simulated-1.json")) + + when: + block1.calls[0].returnData = HexData.empty() + block1.calls[1].returnData = HexData.empty() + block1.extraData = HexData.empty() + JsonNode act = objectMapper.readTree(objectMapper.writeValueAsString(block1)) + + then: + act == expected + } + + def "Decodes request"() { + setup: + String json = BlockSimulatedJsonSpec.classLoader.getResourceAsStream("simulate/simulated-1.json").text + + when: + def act = objectMapper.readValue(json, BlockSimulatedJson) + + then: + act.calls.size() == block1.calls.size() + act.calls.size() == 2 + act.calls[0] == block1.calls[0] + act.calls[1] == block1.calls[1] + act == block1 + } +} diff --git a/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/SimulateJsonSpec.groovy b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/SimulateJsonSpec.groovy new file mode 100644 index 00000000..8d7604b4 --- /dev/null +++ b/etherjar-rpc-json/src/test/groovy/io/emeraldpay/etherjar/rpc/json/SimulateJsonSpec.groovy @@ -0,0 +1,67 @@ +package io.emeraldpay.etherjar.rpc.json + +import com.fasterxml.jackson.databind.ObjectMapper +import io.emeraldpay.etherjar.domain.Address +import io.emeraldpay.etherjar.domain.Wei +import io.emeraldpay.etherjar.rpc.JacksonRpcConverter +import spock.lang.Specification + +class SimulateJsonSpec extends Specification { + + JacksonRpcConverter jacksonRpcConverter = new JacksonRpcConverter() + ObjectMapper objectMapper = jacksonRpcConverter.getObjectMapper() + + SimulateJson request1 = new SimulateJson().tap { + blockStateCalls = [ + new SimulateJson.BlockStateCalls().tap { + blockOverrides = new BlockOverridesJson().tap { + baseFeePerGas = new Wei(9) + } + overrideState( + Address.from("0xc000000000000000000000000000000000000000"), + new StateOverrideJson().tap { + balance = new Wei(0x4a817c800) + } + ) + appendCall(new TransactionCallJson().tap { + from = Address.from("0xc000000000000000000000000000000000000000") + to = Address.from("0xc000000000000000000000000000000000000001") + maxFeePerGas = new Wei(0xf) + value = new Wei(0x1) + }) + appendCall(new TransactionCallJson().tap { + from = Address.from("0xc000000000000000000000000000000000000000") + to = Address.from("0xc000000000000000000000000000000000000002") + maxFeePerGas = new Wei(0xf) + value = new Wei(0x1) + }) + } + ] + validation = true + traceTransfers = true + } + + def "Encodes request"() { + setup: + String expected = BlockJsonSpec.classLoader.getResourceAsStream("simulate/simulate-1.json") + .text + .replaceAll("\\s+", "") + + when: + def act = objectMapper.writeValueAsString(request1) + + then: + act == expected + } + + def "Decodes request"() { + setup: + String json = BlockJsonSpec.classLoader.getResourceAsStream("simulate/simulate-1.json").text + + when: + def act = objectMapper.readValue(json, SimulateJson) + + then: + act == request1 + } +} diff --git a/etherjar-rpc-json/src/test/resources/simulate/simulate-1.json b/etherjar-rpc-json/src/test/resources/simulate/simulate-1.json new file mode 100644 index 00000000..dc7b7587 --- /dev/null +++ b/etherjar-rpc-json/src/test/resources/simulate/simulate-1.json @@ -0,0 +1,30 @@ +{ + "blockStateCalls": [ + { + "blockOverrides": { + "baseFeePerGas": "0x9" + }, + "stateOverrides": { + "0xc000000000000000000000000000000000000000": { + "balance": "0x4a817c800" + } + }, + "calls": [ + { + "from": "0xc000000000000000000000000000000000000000", + "to": "0xc000000000000000000000000000000000000001", + "maxFeePerGas": "0xf", + "value": "0x1" + }, + { + "from": "0xc000000000000000000000000000000000000000", + "to": "0xc000000000000000000000000000000000000002", + "maxFeePerGas": "0xf", + "value": "0x1" + } + ] + } + ], + "validation": true, + "traceTransfers": true +} diff --git a/etherjar-rpc-json/src/test/resources/simulate/simulated-1.json b/etherjar-rpc-json/src/test/resources/simulate/simulated-1.json new file mode 100644 index 00000000..d73ad1e3 --- /dev/null +++ b/etherjar-rpc-json/src/test/resources/simulate/simulated-1.json @@ -0,0 +1,77 @@ +{ + "baseFeePerGas": "0x9", + "blobGasUsed": "0x0", + "calls": [ + { + "returnData": "0x", + "logs": [ + { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000c000000000000000000000000000000000000000", + "0x000000000000000000000000c000000000000000000000000000000000000001" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockNumber": "0x13d2747", + "transactionHash": "0xe7217784e0c3f7b35d39303b1165046e9b7e8af9b9cf80d5d5f96c3163de8f51", + "transactionIndex": "0x0", + "blockHash": "0x5e28f54a56dc9df973a058cd54b3eeef8c67a1a613cb5db1df8a0a434c931d56", + "logIndex": "0x0", + "removed": false + } + ], + "gasUsed": "0x5208", + "status": "0x1" + }, + { + "returnData": "0x", + "logs": [ + { + "address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "topics": [ + "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", + "0x000000000000000000000000c000000000000000000000000000000000000000", + "0x000000000000000000000000c000000000000000000000000000000000000002" + ], + "data": "0x0000000000000000000000000000000000000000000000000000000000000001", + "blockNumber": "0x13d2747", + "transactionHash": "0xf0182201606ec03701ba3a07d965fabdb4b7d06b424f226ea7ec3581802fc6fa", + "transactionIndex": "0x1", + "blockHash": "0x5e28f54a56dc9df973a058cd54b3eeef8c67a1a613cb5db1df8a0a434c931d56", + "logIndex": "0x1", + "removed": false + } + ], + "gasUsed": "0x5208", + "status": "0x1" + } + ], + "difficulty": "0x0", + "excessBlobGas": "0x0", + "extraData": "0x", + "gasLimit": "0x1c9c380", + "gasUsed": "0xa410", + "hash": "0x5e28f54a56dc9df973a058cd54b3eeef8c67a1a613cb5db1df8a0a434c931d56", + "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + "miner": "0x4838b106fce9647bdf1e7877bf73ce8b0bad5f97", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "nonce": "0x0000000000000000", + "number": "0x13d2747", + "parentBeaconBlockRoot": "0x0000000000000000000000000000000000000000000000000000000000000000", + "parentHash": "0xd24222b93a05a066cf79dc20e333f5aa6bb06d36eb50eb2b6b0b744b937e7975", + "receiptsRoot": "0x75308898d571eafb5cd8cde8278bf5b3d13c5f6ec074926de3bb895b519264e1", + "sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + "size": "0x298", + "stateRoot": "0xbb0740745211507e2a2a6cdb627dfa171ef5050ad2a01e5401c2e3df4be5b919", + "timestamp": "0x66ec2853", + "totalDifficulty": "0xc70d815d562d3cfa955", + "transactions": [ + "0xe7217784e0c3f7b35d39303b1165046e9b7e8af9b9cf80d5d5f96c3163de8f51", + "0xf0182201606ec03701ba3a07d965fabdb4b7d06b424f226ea7ec3581802fc6fa" + ], + "transactionsRoot": "0x9bdb74f3ce41f5893a02a631e904ae0d21ae8c4e416786d8dbd9cb5c54f1dc0f", + "uncles": [], + "withdrawals": [], + "withdrawalsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" +}