From 5b1e2ed4440d045b9801429730fa52685ab69066 Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Fri, 5 Dec 2025 16:22:58 +0100 Subject: [PATCH 1/3] feat: refactor method to check if wallet has enough gas --- .../poco/chain/IexecHubAbstractService.java | 36 +++++++++++++++++++ .../poco/chain/Web3jAbstractService.java | 19 ++++++++-- .../iexec/commons/poco/itest/ChainTests.java | 12 +++++++ .../commons/poco/itest/Web3jTestService.java | 6 +++- 4 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java b/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java index e5a5a95..8ed5639 100644 --- a/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java +++ b/src/main/java/com/iexec/commons/poco/chain/IexecHubAbstractService.java @@ -33,6 +33,7 @@ import org.web3j.utils.Numeric; import java.io.IOException; +import java.math.BigDecimal; import java.math.BigInteger; import java.util.Map; import java.util.Optional; @@ -68,6 +69,7 @@ public abstract class IexecHubAbstractService { private final int maxRetries; private final Map categories = new ConcurrentHashMap<>(); private final Map taskDescriptions = new ConcurrentHashMap<>(); + private BigInteger lastKnownBalance = BigInteger.ZERO; protected IexecHubAbstractService( Credentials credentials, @@ -444,6 +446,40 @@ private void setMaxNbOfPeriodsForConsensus() { } } + public boolean hasEnoughGas() { + // if a sidechain is used, there is no need to check if the wallet has enough gas. + // if mainnet is used, the check should be done. + if (web3jAbstractService.isSidechain()) { + return true; + } + + final Optional oWeiBalance = web3jAbstractService.getBalance(credentials.getAddress()); + if (oWeiBalance.isEmpty()) { + log.warn("ETH balance not retrieved on chain, falling back on last known balance"); + } + + final BigInteger weiBalance = oWeiBalance.orElse(lastKnownBalance); + // preserve last known balance for future checks + lastKnownBalance = weiBalance; + final BigInteger estimateTxNb = weiBalance.divide(web3jAbstractService.getMaxTxCost()); + final BigDecimal balanceToShow = ChainUtils.weiToEth(weiBalance); + + if (estimateTxNb.compareTo(BigInteger.ONE) < 0) { + log.error("ETH balance is empty, please refill gas now [balance:{}, estimateTxNb:{}]", balanceToShow, estimateTxNb); + return false; + } else if (estimateTxNb.compareTo(BigInteger.TEN) < 0) { + log.warn("ETH balance very low, should refill gas now [balance:{}, estimateTxNb:{}]", balanceToShow, estimateTxNb); + } else { + log.debug("ETH balance is fine [balance:{}, estimateTxNb:{}]", balanceToShow, estimateTxNb); + } + + return true; + } + + /** + * @deprecated Use hasEnoughGas() instead + */ + @Deprecated(forRemoval = true) public boolean hasEnoughGas(String address) { return web3jAbstractService.hasEnoughGas(address); } diff --git a/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java b/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java index f52ae19..02015a7 100644 --- a/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java +++ b/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java @@ -62,12 +62,15 @@ public abstract class Web3jAbstractService { private final Duration blockTime; private final float gasPriceMultiplier; private final long gasPriceCap; + @Getter private final boolean isSidechain; @Getter private final Web3j web3j; @Getter private final ContractGasProvider contractGasProvider; + private BigInteger lastKnownBalance = BigInteger.ZERO; + /** * Apart from initializing usual business entities, it initializes a single * and shared web3j instance. This inner web3j instance allows to connect to @@ -121,6 +124,10 @@ public boolean isConnected() { return false; } + /** + * @deprecated only used from deprecated method + */ + @Deprecated(forRemoval = true) public static BigInteger getMaxTxCost(long gasPriceCap) { return BigInteger.valueOf(GAS_LIMIT_CAP * gasPriceCap); } @@ -240,6 +247,10 @@ private void decodeAndThrowEvmRpcError(final Response.Error error) { throw new JsonRpcError(error.getCode(), message, null); } + /** + * @deprecated Use IexecHubAbstractService#hasEnoughGas() instead + */ + @Deprecated(forRemoval = true) public boolean hasEnoughGas(String address) { // if a sidechain is used, there is no need to check if the wallet has enough gas. // if mainnet is used, the check should be done. @@ -268,9 +279,9 @@ public boolean hasEnoughGas(String address) { return true; } - public Optional getBalance(String address) { + public Optional getBalance(final String address) { try { - BigInteger balance = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send().getBalance(); + final BigInteger balance = web3j.ethGetBalance(address, DefaultBlockParameterName.LATEST).send().getBalance(); log.debug("{} current balance is {}", address, balance); return Optional.of(balance); } catch (IOException e) { @@ -279,6 +290,10 @@ public Optional getBalance(String address) { } } + public BigInteger getMaxTxCost() { + return BigInteger.valueOf(GAS_LIMIT_CAP * gasPriceCap); + } + /** * Request current gas price on the network. *

diff --git a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java index 63b5136..84d291f 100644 --- a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java @@ -81,6 +81,18 @@ void shouldGetAccount() { assertThat(chainAccount.getLocked()).isZero(); } + @Test + void shouldHaveEnoughGasOnBellecour() { + assertThat(iexecHubService.hasEnoughGas()).isTrue(); + } + + @Test + void shouldHaveEnoughGasOnArbitrum() throws IOException { + final Web3jTestService arbitrumWeb3j = new Web3jTestService(chainNodeAddress, 1.0f, 22_000_000_000L, false); + final IexecHubTestService arbitrumHub = new IexecHubTestService(credentials, arbitrumWeb3j); + assertThat(arbitrumHub.hasEnoughGas()).isTrue(); + } + @Test void shouldGetBalance() { final BigInteger balance = web3jService.getBalance(credentials.getAddress()).orElse(null); diff --git a/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java b/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java index 72b392a..2b3be44 100644 --- a/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java +++ b/src/test/java/com/iexec/commons/poco/itest/Web3jTestService.java @@ -42,7 +42,11 @@ public Web3jTestService(String chainNodeAddress) { } public Web3jTestService(String chainNodeAddress, float gasPriceMultiplier, long gasPriceCap) { - super(65535, chainNodeAddress, Duration.ofSeconds(BLOCK_TIME), gasPriceMultiplier, gasPriceCap, true); + this(chainNodeAddress, gasPriceMultiplier, gasPriceCap, true); + } + + public Web3jTestService(String chainNodeAddress, float gasPriceMultiplier, long gasPriceCap, boolean isSidechain) { + super(65535, chainNodeAddress, Duration.ofSeconds(BLOCK_TIME), gasPriceMultiplier, gasPriceCap, isSidechain); } @SneakyThrows From 04baaa04c39d61f72f873eca706f91fee8a1896a Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Fri, 5 Dec 2025 17:44:51 +0100 Subject: [PATCH 2/3] test: improve test coverage --- .../java/com/iexec/commons/poco/itest/ChainTests.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java index 84d291f..b39c3c1 100644 --- a/src/test/java/com/iexec/commons/poco/itest/ChainTests.java +++ b/src/test/java/com/iexec/commons/poco/itest/ChainTests.java @@ -29,6 +29,7 @@ import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; import org.web3j.crypto.Credentials; +import org.web3j.crypto.Keys; import org.web3j.crypto.WalletUtils; import org.web3j.crypto.exception.CipherException; @@ -93,6 +94,14 @@ void shouldHaveEnoughGasOnArbitrum() throws IOException { assertThat(arbitrumHub.hasEnoughGas()).isTrue(); } + @Test + void shouldNotHaveEnoughGasOnArbitrum() throws Exception { + final Credentials newCredentials = Credentials.create(Keys.createEcKeyPair()); + final Web3jTestService arbitrumWeb3j = new Web3jTestService(chainNodeAddress, 1.0f, 22_000_000_000L, false); + final IexecHubTestService arbitrumHub = new IexecHubTestService(newCredentials, arbitrumWeb3j); + assertThat(arbitrumHub.hasEnoughGas()).isFalse(); + } + @Test void shouldGetBalance() { final BigInteger balance = web3jService.getBalance(credentials.getAddress()).orElse(null); From 2f1e112b120ccb9d2799f54fcdcb66eab7cc7b1d Mon Sep 17 00:00:00 2001 From: Jeremy Bernard Date: Fri, 5 Dec 2025 17:53:26 +0100 Subject: [PATCH 3/3] fix: unused field that was not removed --- .../java/com/iexec/commons/poco/chain/Web3jAbstractService.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java b/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java index 02015a7..aedcaf2 100644 --- a/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java +++ b/src/main/java/com/iexec/commons/poco/chain/Web3jAbstractService.java @@ -69,8 +69,6 @@ public abstract class Web3jAbstractService { @Getter private final ContractGasProvider contractGasProvider; - private BigInteger lastKnownBalance = BigInteger.ZERO; - /** * Apart from initializing usual business entities, it initializes a single * and shared web3j instance. This inner web3j instance allows to connect to