This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26173-web3j-defaults in repository https://gitbox.apache.org/repos/asf/camel.git
commit 7c21a65a619fe8e6bedc6fd7c54bff052684a5a5 Author: Claus Ibsen <[email protected]> AuthorDate: Mon Nov 2 23:33:06 2026 +0100 CAMEL-26173: camel-web3j - atBlock, fromBlock and toBlock default to latest, ETH_GET_TRANSACTION_RECEIPT gives the receipt, and a missing operation fails with a clear message The atBlock, fromBlock and toBlock options were documented with the default latest but were null, so ETH_GET_BALANCE, ETH_GET_BLOCK_BY_NUMBER and the other operations that take a block failed with a NullPointerException, and so did the replay consumers. They are now initialized to latest. ETH_GET_TRANSACTION_RECEIPT set an Optional as the body; it now sets the TransactionReceipt, or null, like the ETH_GET_TRANSACTION_BY_* operations. The default operation transaction is not an operation: the consumer now fails to start with a message that lists the observable operations (and accepts them in lower case like the producer), and the producer fails the exchange with a message that asks for the operation option or the CamelWeb3jOperation header. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../org/apache/camel/catalog/components/web3j.json | 6 +- .../org/apache/camel/component/web3j/web3j.json | 6 +- .../camel/component/web3j/Web3jConfiguration.java | 11 +- .../camel/component/web3j/Web3jConsumer.java | 24 +++- .../camel/component/web3j/Web3jEndpoint.java | 8 +- .../camel/component/web3j/Web3jProducer.java | 18 ++- .../camel/component/web3j/Web3jConsumerTest.java | 126 +++++++++++++++++++++ .../camel/component/web3j/Web3jProducerTest.java | 61 +++++++++- .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 16 +++ .../dsl/Web3jComponentBuilderFactory.java | 8 +- .../endpoint/dsl/Web3jEndpointBuilderFactory.java | 24 +++- 11 files changed, 284 insertions(+), 24 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/web3j.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/web3j.json index b0e0b75ce1c8..f2c6e04c0171 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/web3j.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/web3j.json @@ -30,7 +30,7 @@ "fromBlock": { "index": 3, "kind": "property", "displayName": "From Block", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "latest", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The block number, or the string latest for the last mined block or pending, earliest for [...] "fullTransactionObjects": { "index": 4, "kind": "property", "displayName": "Full Transaction Objects", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true it returns the full transaction objects, if false only the hashes [...] "gasLimit": { "index": 5, "kind": "property", "displayName": "Gas Limit", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.math.BigInteger", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The maximum gas allowed in this block." }, - "operation": { "index": 6, "kind": "property", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use." }, + "operation": { "index": 6, "kind": "property", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use. The producer needs an operation, set with this option or the CamelW [...] "privateFor": { "index": 7, "kind": "property", "displayName": "Private For", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "A comma separated transaction privateFor nodes with public keys in a Quorum network" }, "quorumAPI": { "index": 8, "kind": "property", "displayName": "Quorum API", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true, this will support Quorum API." }, "toAddress": { "index": 9, "kind": "property", "displayName": "To Address", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The address the transaction is directed to." }, @@ -107,7 +107,7 @@ "bodyTypes": { "consumerOut": { "carriers": [ "text", "pojo", "null" ], "dependsOn": [ "operation" ], "byOption": [ { "option": "operation", "value": "ETH_BLOCK_HASH_OBSERVABLE", "carriers": [ "text", "null" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "ETH_PENDING_TRANSACTION_HASH_OBSERVABLE", "carriers": [ "text", "null" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "BLOCK_OBSERVABLE", "carriers": [ "pojo", "null" ], "javaType": "org.web3j.protoc [...] "producerIn": { "carriers": [ "any" ], "description": "The operations take their input from headers and options, except WEB3_SHA3, DB_PUT_STRING, DB_PUT_HEX and QUORUM_GET_PRIVATE_PAYLOAD, which send the body converted to a String: a Map or a POJO is sent as its toString" }, - "producerOut": { "carriers": [ "unchanged" ], "byOption": [ { "option": "operation", "value": "WEB3_CLIENT_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "WEB3_SHA3", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "NET_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "ETH_PROTOCOL_VERSION", "carriers": [ "text" ], "javaType": "java.l [...] + "producerOut": { "carriers": [ "unchanged" ], "byOption": [ { "option": "operation", "value": "WEB3_CLIENT_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "WEB3_SHA3", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "NET_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "ETH_PROTOCOL_VERSION", "carriers": [ "text" ], "javaType": "java.l [...] }, "properties": { "nodeAddress": { "index": 0, "kind": "path", "displayName": "Node Address", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets the node address used to communicate" }, @@ -116,7 +116,7 @@ "fromBlock": { "index": 3, "kind": "parameter", "displayName": "From Block", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "latest", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The block number, or the string latest for the last mined block or pending, earliest for [...] "fullTransactionObjects": { "index": 4, "kind": "parameter", "displayName": "Full Transaction Objects", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true it returns the full transaction objects, if false only the hashe [...] "gasLimit": { "index": 5, "kind": "parameter", "displayName": "Gas Limit", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.math.BigInteger", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The maximum gas allowed in this block." }, - "operation": { "index": 6, "kind": "parameter", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use." }, + "operation": { "index": 6, "kind": "parameter", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use. The producer needs an operation, set with this option or the Camel [...] "privateFor": { "index": 7, "kind": "parameter", "displayName": "Private For", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "A comma separated transaction privateFor nodes with public keys in a Quorum network" }, "quorumAPI": { "index": 8, "kind": "parameter", "displayName": "Quorum API", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true, this will support Quorum API." }, "toAddress": { "index": 9, "kind": "parameter", "displayName": "To Address", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The address the transaction is directed to." }, diff --git a/components/camel-web3j/src/generated/resources/META-INF/org/apache/camel/component/web3j/web3j.json b/components/camel-web3j/src/generated/resources/META-INF/org/apache/camel/component/web3j/web3j.json index b0e0b75ce1c8..f2c6e04c0171 100644 --- a/components/camel-web3j/src/generated/resources/META-INF/org/apache/camel/component/web3j/web3j.json +++ b/components/camel-web3j/src/generated/resources/META-INF/org/apache/camel/component/web3j/web3j.json @@ -30,7 +30,7 @@ "fromBlock": { "index": 3, "kind": "property", "displayName": "From Block", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "latest", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The block number, or the string latest for the last mined block or pending, earliest for [...] "fullTransactionObjects": { "index": 4, "kind": "property", "displayName": "Full Transaction Objects", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true it returns the full transaction objects, if false only the hashes [...] "gasLimit": { "index": 5, "kind": "property", "displayName": "Gas Limit", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.math.BigInteger", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The maximum gas allowed in this block." }, - "operation": { "index": 6, "kind": "property", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use." }, + "operation": { "index": 6, "kind": "property", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use. The producer needs an operation, set with this option or the CamelW [...] "privateFor": { "index": 7, "kind": "property", "displayName": "Private For", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "A comma separated transaction privateFor nodes with public keys in a Quorum network" }, "quorumAPI": { "index": 8, "kind": "property", "displayName": "Quorum API", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true, this will support Quorum API." }, "toAddress": { "index": 9, "kind": "property", "displayName": "To Address", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The address the transaction is directed to." }, @@ -107,7 +107,7 @@ "bodyTypes": { "consumerOut": { "carriers": [ "text", "pojo", "null" ], "dependsOn": [ "operation" ], "byOption": [ { "option": "operation", "value": "ETH_BLOCK_HASH_OBSERVABLE", "carriers": [ "text", "null" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "ETH_PENDING_TRANSACTION_HASH_OBSERVABLE", "carriers": [ "text", "null" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "BLOCK_OBSERVABLE", "carriers": [ "pojo", "null" ], "javaType": "org.web3j.protoc [...] "producerIn": { "carriers": [ "any" ], "description": "The operations take their input from headers and options, except WEB3_SHA3, DB_PUT_STRING, DB_PUT_HEX and QUORUM_GET_PRIVATE_PAYLOAD, which send the body converted to a String: a Map or a POJO is sent as its toString" }, - "producerOut": { "carriers": [ "unchanged" ], "byOption": [ { "option": "operation", "value": "WEB3_CLIENT_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "WEB3_SHA3", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "NET_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "ETH_PROTOCOL_VERSION", "carriers": [ "text" ], "javaType": "java.l [...] + "producerOut": { "carriers": [ "unchanged" ], "byOption": [ { "option": "operation", "value": "WEB3_CLIENT_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "WEB3_SHA3", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "NET_VERSION", "carriers": [ "text" ], "javaType": "java.lang.String" }, { "option": "operation", "value": "ETH_PROTOCOL_VERSION", "carriers": [ "text" ], "javaType": "java.l [...] }, "properties": { "nodeAddress": { "index": 0, "kind": "path", "displayName": "Node Address", "group": "common", "label": "", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": false, "description": "Sets the node address used to communicate" }, @@ -116,7 +116,7 @@ "fromBlock": { "index": 3, "kind": "parameter", "displayName": "From Block", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "latest", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The block number, or the string latest for the last mined block or pending, earliest for [...] "fullTransactionObjects": { "index": 4, "kind": "parameter", "displayName": "Full Transaction Objects", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true it returns the full transaction objects, if false only the hashe [...] "gasLimit": { "index": 5, "kind": "parameter", "displayName": "Gas Limit", "group": "common", "label": "", "required": false, "type": "object", "javaType": "java.math.BigInteger", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The maximum gas allowed in this block." }, - "operation": { "index": 6, "kind": "parameter", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use." }, + "operation": { "index": 6, "kind": "parameter", "displayName": "Operation", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "transaction", "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "Operation to use. The producer needs an operation, set with this option or the Camel [...] "privateFor": { "index": 7, "kind": "parameter", "displayName": "Private For", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "A comma separated transaction privateFor nodes with public keys in a Quorum network" }, "quorumAPI": { "index": 8, "kind": "parameter", "displayName": "Quorum API", "group": "common", "label": "common", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "If true, this will support Quorum API." }, "toAddress": { "index": 9, "kind": "parameter", "displayName": "To Address", "group": "common", "label": "common", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.web3j.Web3jConfiguration", "configurationField": "configuration", "description": "The address the transaction is directed to." }, diff --git a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConfiguration.java b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConfiguration.java index 57e56cdf5d82..0b30e2d57b9f 100644 --- a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConfiguration.java +++ b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConfiguration.java @@ -31,13 +31,13 @@ public class Web3jConfiguration implements Cloneable { private String operation = Web3jConstants.TRANSACTION.toLowerCase(); // TODO: Make this an enum @UriParam(label = "common", defaultValue = "latest") - private String fromBlock; + private String fromBlock = "latest"; @UriParam(label = "common", defaultValue = "latest") - private String toBlock; + private String toBlock = "latest"; @UriParam(label = "producer", defaultValue = "latest") - private String atBlock; + private String atBlock = "latest"; @UriParam(label = "common") private String addresses; @@ -524,7 +524,10 @@ public class Web3jConfiguration implements Cloneable { } /** - * Operation to use. + * Operation to use. The producer needs an operation, set with this option or the CamelWeb3jOperation header. The + * consumer needs one of the observable operations: ETH_LOG_OBSERVABLE, ETH_BLOCK_HASH_OBSERVABLE, + * ETH_PENDING_TRANSACTION_HASH_OBSERVABLE, TRANSACTION_OBSERVABLE, PENDING_TRANSACTION_OBSERVABLE, + * BLOCK_OBSERVABLE, REPLAY_BLOCKS_OBSERVABLE or REPLAY_TRANSACTIONS_OBSERVABLE. */ public void setOperation(String operation) { this.operation = operation; diff --git a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java index ebff5d510a80..cd7b7941bfc3 100644 --- a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java +++ b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jConsumer.java @@ -16,6 +16,8 @@ */ package org.apache.camel.component.web3j; +import java.util.List; +import java.util.Locale; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -41,6 +43,11 @@ public class Web3jConsumer extends DefaultConsumer { private static final Logger LOG = LoggerFactory.getLogger(Web3jConsumer.class); // the delay before subscribing again after a subscription ended with an error private static final long DEFAULT_RESUBSCRIBE_DELAY = 15000; + private static final List<String> OPERATIONS = List.of( + Web3jConstants.ETH_LOG_OBSERVABLE, Web3jConstants.ETH_BLOCK_HASH_OBSERVABLE, + Web3jConstants.ETH_PENDING_TRANSACTION_HASH_OBSERVABLE, Web3jConstants.TRANSACTION_OBSERVABLE, + Web3jConstants.PENDING_TRANSACTION_OBSERVABLE, Web3jConstants.BLOCK_OBSERVABLE, + Web3jConstants.REPLAY_BLOCKS_OBSERVABLE, Web3jConstants.REPLAY_TRANSACTIONS_OBSERVABLE); private final Web3jConfiguration configuration; private Web3j web3j; private volatile Disposable subscription; @@ -72,6 +79,13 @@ public class Web3jConsumer extends DefaultConsumer { protected void doStart() throws Exception { super.doStart(); + String operation = operation(); + if (operation == null || !OPERATIONS.contains(operation)) { + throw new IllegalArgumentException( + "Unsupported operation " + configuration.getOperation() + + " for the web3j consumer: set the operation option to one of " + + String.join(", ", OPERATIONS)); + } this.web3j = getEndpoint().getWeb3j(); failing = false; resubscribeExecutor = getEndpoint().getCamelContext().getExecutorServiceManager() @@ -86,7 +100,7 @@ public class Web3jConsumer extends DefaultConsumer { } else { LOG.info("Subscribing to: {}", endpoint.getNodeAddress()); } - switch (configuration.getOperation()) { + switch (operation()) { case Web3jConstants.ETH_LOG_OBSERVABLE: EthFilter ethFilter = Web3jEndpoint.buildEthFilter(toDefaultBlockParameter(configuration.getFromBlock()), toDefaultBlockParameter(configuration.getToBlock()), configuration.getAddressesAsList(), @@ -160,6 +174,14 @@ public class Web3jConsumer extends DefaultConsumer { } } + /** + * The operation of the consumer, which may be written in lower case like the operations of the producer. + */ + private String operation() { + String operation = configuration.getOperation(); + return operation != null ? operation.toUpperCase(Locale.ROOT) : null; + } + /** * An item was received, so the subscription works: an error after this one is routed and logged again. */ diff --git a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jEndpoint.java b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jEndpoint.java index 84765eb6e2c0..7e348bcf07c9 100644 --- a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jEndpoint.java +++ b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jEndpoint.java @@ -266,16 +266,16 @@ import org.web3j.quorum.Quorum; carriers = { Carrier.POJO, Carrier.NULL }, javaType = "org.web3j.protocol.core.methods.response.Transaction"), @BodyType.When(option = "operation", value = "ETH_GET_TRANSACTION_RECEIPT", - carriers = Carrier.POJO, - javaType = "java.util.Optional<org.web3j.protocol.core.methods.response.TransactionReceipt>"), + carriers = { Carrier.POJO, Carrier.NULL }, + javaType = "org.web3j.protocol.core.methods.response.TransactionReceipt"), @BodyType.When(option = "operation", value = "QUORUM_NODE_INFO", carriers = Carrier.POJO, javaType = "org.web3j.protocol.core.methods.response.admin.AdminNodeInfo.NodeInfo") }, description = "The result of the JSON-RPC call of the operation, selected by the" + " CamelWeb3jOperation header or the operation option: a String for a" + " hash, an address or hex data, a BigInteger for a quantity, a Boolean, a" - + " List, or a web3j object such as an EthBlock.Block, a Transaction (null" - + " when not found) or an Optional of a TransactionReceipt. On a JSON-RPC" + + " List, or a web3j object such as an EthBlock.Block, a Transaction or a" + + " TransactionReceipt (null when not found). On a JSON-RPC" + " error the body is unchanged and the exchange fails, with the" + " CamelWeb3jErrorCode, CamelWeb3jErrorData and CamelWeb3jErrorMessage headers")) public class Web3jEndpoint extends DefaultEndpoint implements EndpointServiceLocation { diff --git a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jProducer.java b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jProducer.java index d4619fe14a37..c37f0b206bec 100644 --- a/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jProducer.java +++ b/components/camel-web3j/src/main/java/org/apache/camel/component/web3j/Web3jProducer.java @@ -97,7 +97,7 @@ public class Web3jProducer extends HeaderSelectorProducer { private final Web3jConfiguration configuration; public Web3jProducer(Web3jEndpoint endpoint, final Web3jConfiguration configuration) { - super(endpoint, Web3jConstants.OPERATION, configuration::getOperationOrDefault); + super(endpoint, Web3jConstants.OPERATION, () -> defaultOperation(configuration)); web3j = endpoint.getWeb3j(); this.configuration = configuration; if (web3j instanceof Quorum) { @@ -110,6 +110,20 @@ public class Web3jProducer extends HeaderSelectorProducer { return (Web3jEndpoint) super.getEndpoint(); } + /** + * The operation of the endpoint, used when the CamelWeb3jOperation header is not set. The default value of the + * operation option, transaction, is not an operation of the producer. + */ + private static String defaultOperation(Web3jConfiguration configuration) { + String operation = configuration.getOperationOrDefault(); + if (Web3jConstants.TRANSACTION.equalsIgnoreCase(operation)) { + throw new IllegalArgumentException( + "No operation for the web3j producer: set the operation option or the " + Web3jConstants.OPERATION + + " header"); + } + return operation; + } + @InvokeOnHeader(Web3jConstants.WEB3_CLIENT_VERSION) void web3ClientVersion(Message message) throws IOException { Request<?, Web3ClientVersion> web3ClientVersionRequest = web3j.web3ClientVersion(); @@ -561,7 +575,7 @@ public class Web3jProducer extends HeaderSelectorProducer { EthGetTransactionReceipt response = request.send(); boolean hasError = checkForError(message, response); if (!hasError) { - message.setBody(response.getTransactionReceipt()); + message.setBody(response.getTransactionReceipt().orElse(null)); } } diff --git a/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jConsumerTest.java b/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jConsumerTest.java new file mode 100644 index 000000000000..bc5c78723a3f --- /dev/null +++ b/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jConsumerTest.java @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.camel.component.web3j; + +import io.reactivex.Flowable; +import org.apache.camel.EndpointInject; +import org.apache.camel.builder.RouteBuilder; +import org.apache.camel.component.mock.MockEndpoint; +import org.apache.camel.spi.Registry; +import org.apache.camel.test.junit6.CamelTestSupport; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.web3j.protocol.Web3j; +import org.web3j.protocol.core.DefaultBlockParameterName; +import org.web3j.protocol.core.methods.request.EthFilter; +import org.web3j.protocol.core.methods.response.EthBlock; +import org.web3j.protocol.core.methods.response.Log; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +class Web3jConsumerTest extends CamelTestSupport { + + private final Web3j web3j = mock(Web3j.class); + + @EndpointInject("mock:result") + private MockEndpoint result; + + Web3jConsumerTest() { + // the mocked client is stubbed before the context starts + testConfiguration().withUseAdviceWith(true); + } + + @Override + protected void bindToRegistry(Registry registry) { + registry.bind("mockWeb3j", web3j); + } + + private static EthBlock block(String number) { + EthBlock.Block block = new EthBlock.Block(); + block.setNumber(number); + EthBlock ethBlock = new EthBlock(); + ethBlock.setResult(block); + return ethBlock; + } + + private void startRoute(String options) throws Exception { + context.addRoutes(new RouteBuilder() { + @Override + public void configure() { + from("web3j://http://127.0.0.1:8545?web3j=#mockWeb3j" + options) + .routeId("web3j").autoStartup(false) + .to("mock:result"); + } + }); + context.start(); + context.getRouteController().startRoute("web3j"); + } + + @Test + void consumerWithoutOperationFailsToStart() { + Exception e = assertThrows(Exception.class, () -> startRoute("")); + Throwable cause = e; + while (!(cause instanceof IllegalArgumentException) && cause.getCause() != null) { + cause = cause.getCause(); + } + assertTrue(cause.getMessage().startsWith( + "Unsupported operation transaction for the web3j consumer: set the operation option to one of" + + " ETH_LOG_OBSERVABLE, ETH_BLOCK_HASH_OBSERVABLE"), + cause.getMessage()); + } + + @Test + void operationInLowerCase() throws Exception { + when(web3j.blockFlowable(anyBoolean())).thenReturn(Flowable.concat(Flowable.just(block("0x1")), Flowable.never())); + result.expectedMessageCount(1); + + startRoute("&operation=block_observable"); + + MockEndpoint.assertIsSatisfied(context); + } + + @Test + void replayOfBlocksFromAndToTheLatestBlockByDefault() throws Exception { + when(web3j.replayPastBlocksFlowable(eq(DefaultBlockParameterName.LATEST), eq(DefaultBlockParameterName.LATEST), + anyBoolean())).thenReturn(Flowable.just(block("0x1"))); + // the block, then the message that ends the subscription + result.expectedMessageCount(2); + + startRoute("&operation=" + Web3jConstants.REPLAY_BLOCKS_OBSERVABLE); + + MockEndpoint.assertIsSatisfied(context); + } + + @Test + void logFilterFromAndToTheLatestBlockByDefault() throws Exception { + ArgumentCaptor<EthFilter> filter = ArgumentCaptor.forClass(EthFilter.class); + when(web3j.ethLogFlowable(filter.capture())).thenReturn(Flowable.<Log> never()); + + startRoute("&operation=" + Web3jConstants.ETH_LOG_OBSERVABLE); + + verify(web3j).ethLogFlowable(filter.getValue()); + assertEquals(DefaultBlockParameterName.LATEST, filter.getValue().getFromBlock()); + assertEquals(DefaultBlockParameterName.LATEST, filter.getValue().getToBlock()); + } +} diff --git a/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jProducerTest.java b/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jProducerTest.java index 54b8ff66f588..aae56348d383 100644 --- a/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jProducerTest.java +++ b/components/camel-web3j/src/test/java/org/apache/camel/component/web3j/Web3jProducerTest.java @@ -82,15 +82,20 @@ import org.web3j.protocol.core.methods.response.ShhPost; import org.web3j.protocol.core.methods.response.ShhUninstallFilter; import org.web3j.protocol.core.methods.response.ShhVersion; import org.web3j.protocol.core.methods.response.Transaction; +import org.web3j.protocol.core.methods.response.TransactionReceipt; import org.web3j.protocol.core.methods.response.Web3ClientVersion; import org.web3j.protocol.core.methods.response.Web3Sha3; import static org.apache.camel.component.web3j.Web3jConstants.OPERATION; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.eq; public class Web3jProducerTest extends Web3jMockTestSupport { @@ -291,6 +296,19 @@ public class Web3jProducerTest extends Web3jMockTestSupport { assertEquals(BigInteger.ONE, body); } + @Test + public void ethGetBalanceAtTheLatestBlockByDefaultTest() throws Exception { + EthGetBalance response = Mockito.mock(EthGetBalance.class); + Mockito.when(mockWeb3j.ethGetBalance(any(), any())).thenReturn(request); + Mockito.when(request.send()).thenReturn(response); + Mockito.when(response.getBalance()).thenReturn(BigInteger.ONE); + + Exchange exchange = createExchangeWithBodyAndHeader(null, OPERATION, Web3jConstants.ETH_GET_BALANCE); + template.send(exchange); + assertEquals(BigInteger.ONE, exchange.getIn().getBody(BigInteger.class)); + Mockito.verify(mockWeb3j).ethGetBalance(any(), eq(DefaultBlockParameterName.LATEST)); + } + @Test public void ethGetStorageAtTest() throws Exception { EthGetStorageAt response = Mockito.mock(EthGetStorageAt.class); @@ -463,6 +481,19 @@ public class Web3jProducerTest extends Web3jMockTestSupport { assertNotNull(body); } + @Test + public void ethGetBlockByNumberAtTheLatestBlockByDefaultTest() throws Exception { + EthBlock response = Mockito.mock(EthBlock.class); + Mockito.when(mockWeb3j.ethGetBlockByNumber(any(), anyBoolean())).thenReturn(request); + Mockito.when(request.send()).thenReturn(response); + Mockito.when(response.getBlock()).thenReturn(Mockito.mock(EthBlock.Block.class)); + + Exchange exchange = createExchangeWithBodyAndHeader(null, OPERATION, Web3jConstants.ETH_GET_BLOCK_BY_NUMBER); + template.send(exchange); + assertNotNull(exchange.getIn().getBody(EthBlock.Block.class)); + Mockito.verify(mockWeb3j).ethGetBlockByNumber(eq(DefaultBlockParameterName.LATEST), anyBoolean()); + } + @Test public void ethGetBlockByNumberTest() throws Exception { EthBlock response = Mockito.mock(EthBlock.class); @@ -528,15 +559,29 @@ public class Web3jProducerTest extends Web3jMockTestSupport { @Test public void ethGetTransactionReceiptTest() throws Exception { + TransactionReceipt receipt = new TransactionReceipt(); + receipt.setTransactionHash("0x01"); EthGetTransactionReceipt response = Mockito.mock(EthGetTransactionReceipt.class); Mockito.when(mockWeb3j.ethGetTransactionReceipt(any())).thenReturn(request); Mockito.when(request.send()).thenReturn(response); - Mockito.when(response.getTransactionReceipt()).thenReturn(Mockito.mock(Optional.class)); + Mockito.when(response.getTransactionReceipt()).thenReturn(Optional.of(receipt)); Exchange exchange = createExchangeWithBodyAndHeader(null, OPERATION, Web3jConstants.ETH_GET_TRANSACTION_RECEIPT); template.send(exchange); - Optional<Transaction> body = exchange.getIn().getBody(Optional.class); - assertNotNull(body); + assertSame(receipt, exchange.getIn().getBody()); + } + + @Test + public void ethGetTransactionReceiptNotFoundTest() throws Exception { + EthGetTransactionReceipt response = Mockito.mock(EthGetTransactionReceipt.class); + Mockito.when(mockWeb3j.ethGetTransactionReceipt(any())).thenReturn(request); + Mockito.when(request.send()).thenReturn(response); + Mockito.when(response.getTransactionReceipt()).thenReturn(Optional.empty()); + + Exchange exchange = createExchangeWithBodyAndHeader("hash", OPERATION, Web3jConstants.ETH_GET_TRANSACTION_RECEIPT); + template.send(exchange); + assertNull(exchange.getException()); + assertNull(exchange.getIn().getBody()); } @Test @@ -948,12 +993,22 @@ public class Web3jProducerTest extends Web3jMockTestSupport { assertEquals("error data", exchange.getIn().getHeader(Web3jConstants.ERROR_DATA, String.class)); } + @Test + public void producerWithoutOperationTest() { + Exchange exchange = template.send("direct:noOperation", e -> e.getIn().setBody("hello")); + IllegalArgumentException e = assertInstanceOf(IllegalArgumentException.class, exchange.getException()); + assertEquals("No operation for the web3j producer: set the operation option or the CamelWeb3jOperation header", + e.getMessage()); + } + @Override protected RouteBuilder createRouteBuilder() { return new RouteBuilder() { public void configure() { from("direct:start") .to(getUrl() + "operation" + "=" + Web3jConstants.WEB3_CLIENT_VERSION); + from("direct:noOperation") + .to("web3j://http://127.0.0.1:8545?web3j=#mockWeb3j"); } }; } diff --git a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc index 10f5a47c1b89..16007c53e1b4 100644 --- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc +++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc @@ -2567,6 +2567,22 @@ subscription are not routed. `REPLAY_BLOCKS_OBSERVABLE`, `REPLAY_TRANSACTIONS_OB `ETH_LOG_OBSERVABLE` with `fromBlock` set to a block number are not subscribed again, as that would route the same blocks or logs again. +=== camel-web3j - ETH_GET_TRANSACTION_RECEIPT gives the TransactionReceipt, and the block options default to latest + +The `ETH_GET_TRANSACTION_RECEIPT` operation now sets the `TransactionReceipt` as the body, or `null` when the node has no +receipt for the transaction, like the `ETH_GET_TRANSACTION_BY_*` operations do with the `Transaction`. Before, the body +was an `Optional<TransactionReceipt>`: a route that unwraps the `Optional` must now use the body as it is. + +The `atBlock`, `fromBlock` and `toBlock` options now default to `latest`, as documented. Before, they were not set, and +the operations that take a block, such as `ETH_GET_BALANCE` and `ETH_GET_BLOCK_BY_NUMBER`, failed with a +`NullPointerException` unless `atBlock` or the `CamelWeb3jAtBlock` header was set, and so did the +`REPLAY_BLOCKS_OBSERVABLE` and `REPLAY_TRANSACTIONS_OBSERVABLE` consumers without `fromBlock` and `toBlock`. + +The `operation` option defaults to `transaction`, which is not an operation of the producer nor of the consumer. A +consumer without an observable operation now fails to start with a message that lists them, and accepts the operation +in lower case, as the producer does. A producer without the option and without the `CamelWeb3jOperation` header now +fails the exchange with a message that asks for one. + === camel-google-vertexai - generateMultimodal sends the body as media, and fails without a MIME type When the `CamelGoogleVertexAIMediaData` and `CamelGoogleVertexAIMediaGcsUri` headers are not set, the `generateMultimodal` diff --git a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Web3jComponentBuilderFactory.java b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Web3jComponentBuilderFactory.java index efeabad9248b..8a01f4e22649 100644 --- a/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Web3jComponentBuilderFactory.java +++ b/dsl/camel-componentdsl/src/generated/java/org/apache/camel/builder/component/dsl/Web3jComponentBuilderFactory.java @@ -149,7 +149,13 @@ public interface Web3jComponentBuilderFactory { /** - * Operation to use. + * Operation to use. The producer needs an operation, set with this + * option or the CamelWeb3jOperation header. The consumer needs one of + * the observable operations: ETH_LOG_OBSERVABLE, + * ETH_BLOCK_HASH_OBSERVABLE, ETH_PENDING_TRANSACTION_HASH_OBSERVABLE, + * TRANSACTION_OBSERVABLE, PENDING_TRANSACTION_OBSERVABLE, + * BLOCK_OBSERVABLE, REPLAY_BLOCKS_OBSERVABLE or + * REPLAY_TRANSACTIONS_OBSERVABLE. * * The option is a: <code>java.lang.String</code> type. * diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Web3jEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Web3jEndpointBuilderFactory.java index 5f8090099565..de53e2fa680e 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Web3jEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/Web3jEndpointBuilderFactory.java @@ -149,7 +149,13 @@ public interface Web3jEndpointBuilderFactory { return this; } /** - * Operation to use. + * Operation to use. The producer needs an operation, set with this + * option or the CamelWeb3jOperation header. The consumer needs one of + * the observable operations: ETH_LOG_OBSERVABLE, + * ETH_BLOCK_HASH_OBSERVABLE, ETH_PENDING_TRANSACTION_HASH_OBSERVABLE, + * TRANSACTION_OBSERVABLE, PENDING_TRANSACTION_OBSERVABLE, + * BLOCK_OBSERVABLE, REPLAY_BLOCKS_OBSERVABLE or + * REPLAY_TRANSACTIONS_OBSERVABLE. * * The option is a: <code>java.lang.String</code> type. * @@ -528,7 +534,13 @@ public interface Web3jEndpointBuilderFactory { return this; } /** - * Operation to use. + * Operation to use. The producer needs an operation, set with this + * option or the CamelWeb3jOperation header. The consumer needs one of + * the observable operations: ETH_LOG_OBSERVABLE, + * ETH_BLOCK_HASH_OBSERVABLE, ETH_PENDING_TRANSACTION_HASH_OBSERVABLE, + * TRANSACTION_OBSERVABLE, PENDING_TRANSACTION_OBSERVABLE, + * BLOCK_OBSERVABLE, REPLAY_BLOCKS_OBSERVABLE or + * REPLAY_TRANSACTIONS_OBSERVABLE. * * The option is a: <code>java.lang.String</code> type. * @@ -1256,7 +1268,13 @@ public interface Web3jEndpointBuilderFactory { return this; } /** - * Operation to use. + * Operation to use. The producer needs an operation, set with this + * option or the CamelWeb3jOperation header. The consumer needs one of + * the observable operations: ETH_LOG_OBSERVABLE, + * ETH_BLOCK_HASH_OBSERVABLE, ETH_PENDING_TRANSACTION_HASH_OBSERVABLE, + * TRANSACTION_OBSERVABLE, PENDING_TRANSACTION_OBSERVABLE, + * BLOCK_OBSERVABLE, REPLAY_BLOCKS_OBSERVABLE or + * REPLAY_TRANSACTIONS_OBSERVABLE. * * The option is a: <code>java.lang.String</code> type. *
