Merge pull request #6088 from ghubstan/2-improve-grpc-exception-status-code-mapping

Send meaningful io.grpc.Status.Code to gRPC clients [No. 2]
This commit is contained in:
Christoph Atteneder 2022-03-08 11:25:35 +01:00 committed by GitHub
commit 20a3ec0592
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 316 additions and 96 deletions

View file

@ -26,6 +26,9 @@ import bisq.common.util.Utilities;
import bisq.proto.grpc.BalancesInfo;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
@ -190,6 +193,15 @@ public class MethodTest extends ApiTestCase {
return Utilities.bytesAsHexString(s.getBytes(UTF_8));
}
protected static Status.Code getStatusRuntimeExceptionStatusCode(Exception grpcException) {
if (grpcException instanceof StatusRuntimeException)
return ((StatusRuntimeException) grpcException).getStatus().getCode();
else
throw new IllegalArgumentException(
format("Expected a io.grpc.StatusRuntimeException argument, but got a %s",
grpcException.getClass().getName()));
}
protected void verifyNoLoggedNodeExceptions() {
var loggedExceptions = getNodeExceptionMessages();
if (loggedExceptions != null) {

View file

@ -61,7 +61,7 @@ public class RegisterDisputeAgentsTest extends MethodTest {
public void testRegisterArbitratorShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
arbClient.registerDisputeAgent(ARBITRATOR, DEV_PRIVILEGE_PRIV_KEY));
assertEquals("INVALID_ARGUMENT: arbitrators must be registered in a Bisq UI",
assertEquals("UNIMPLEMENTED: arbitrators must be registered in a Bisq UI",
exception.getMessage());
}

View file

@ -31,6 +31,7 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.apitest.config.ApiTestConfig.BSQ;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static io.grpc.Status.Code.NOT_FOUND;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -140,6 +141,8 @@ public class BsqSwapOfferTest extends AbstractOfferTest {
break;
} catch (Exception ex) {
log.warn(ex.getMessage());
var statusCode = getStatusRuntimeExceptionStatusCode(ex);
assertEquals(NOT_FOUND, statusCode, "Expected a NOT_FOUND status code from server");
if (numFetchAttempts >= 9)
fail(format("Alice giving up on fetching her (my) bsq swap offer after %d attempts.", numFetchAttempts), ex);
@ -160,6 +163,8 @@ public class BsqSwapOfferTest extends AbstractOfferTest {
break;
} catch (Exception ex) {
log.warn(ex.getMessage());
var statusCode = getStatusRuntimeExceptionStatusCode(ex);
assertEquals(NOT_FOUND, statusCode, "Expected a NOT_FOUND status code from server");
if (numFetchAttempts > 9)
fail(format("Bob gave up on fetching available bsq swap offer after %d attempts.", numFetchAttempts), ex);

View file

@ -437,7 +437,7 @@ public class EditOfferTest extends AbstractOfferTest {
NO_TRIGGER_PRICE,
ACTIVATE_OFFER,
MKT_PRICE_MARGIN_ONLY));
String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or"
String expectedExceptionMessage = format("INVALID_ARGUMENT: cannot set mkt price margin or"
+ " trigger price on fixed price bsq offer with id '%s'",
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
@ -465,7 +465,7 @@ public class EditOfferTest extends AbstractOfferTest {
newTriggerPriceAsLong,
ACTIVATE_OFFER,
TRIGGER_PRICE_ONLY));
String expectedExceptionMessage = format("UNKNOWN: cannot set mkt price margin or"
String expectedExceptionMessage = format("INVALID_ARGUMENT: cannot set mkt price margin or"
+ " trigger price on fixed price bsq offer with id '%s'",
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
@ -850,8 +850,10 @@ public class EditOfferTest extends AbstractOfferTest {
NO_TRIGGER_PRICE,
ACTIVATE_OFFER,
TRIGGER_PRICE_ONLY));
String expectedExceptionMessage = format("UNKNOWN: cannot edit bsq swap offer with id '%s'",
originalOffer.getId());
String expectedExceptionMessage =
format("INVALID_ARGUMENT: cannot edit bsq swap offer with id '%s',"
+ " replace it with a new swap offer instead",
originalOffer.getId());
assertEquals(expectedExceptionMessage, exception.getMessage());
}

View file

@ -207,10 +207,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
String receiverAddress = contract.getIsBuyerMakerAndSellerTaker()
? contract.getTakerPaymentAccountPayload().getAddress()
: contract.getMakerPaymentAccountPayload().getAddress();
// TODO Fix trade vol src bug for subclasses.
// This bug was fixed for production CLI with https://github.com/bisq-network/bisq/pull/5704 on Sep 27, 2021
String sendBsqAmount = trade.getOffer().getVolume();
// String sendBsqAmount = trade.getTradeVolume();
String sendBsqAmount = trade.getTradeVolume();
log.debug("Sending {} BSQ to address {}", sendBsqAmount, receiverAddress);
grpcClient.sendBsq(receiverAddress, sendBsqAmount, "");
}
@ -219,10 +216,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
GrpcClient grpcClient,
TradeInfo trade) {
var contract = trade.getContract();
// TODO Fix trade vol src bug for subclasses.
// This bug was fixed for production with https://github.com/bisq-network/bisq/pull/5704 on Sep 27, 2021
var receiveAmountAsString = trade.getOffer().getVolume();
// String receiveAmountAsString = trade.getTradeVolume();
String receiveAmountAsString = trade.getTradeVolume();
var address = contract.getIsBuyerMakerAndSellerTaker()
? contract.getTakerPaymentAccountPayload().getAddress()
: contract.getMakerPaymentAccountPayload().getAddress();

View file

@ -36,6 +36,7 @@ import static bisq.apitest.config.ApiTestConfig.BSQ;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.offer.OfferDirection.BUY;
import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP;
import static io.grpc.Status.Code.NOT_FOUND;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -171,6 +172,9 @@ public class BsqSwapBuyBtcTradeTest extends AbstractTradeTest {
return client.getTrade(tradeId);
} catch (Exception ex) {
log.warn(ex.getMessage());
var statusCode = getStatusRuntimeExceptionStatusCode(ex);
assertEquals(NOT_FOUND, statusCode, "Expected a NOT_FOUND status code from server");
if (numFetchAttempts > 9) {
if (checkForLoggedExceptions) {
printNodeExceptionMessages(log);

View file

@ -34,6 +34,7 @@ import static bisq.apitest.config.ApiTestConfig.BSQ;
import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.core.offer.OfferDirection.SELL;
import static bisq.proto.grpc.GetOfferCategoryReply.OfferCategory.BSQ_SWAP;
import static io.grpc.Status.Code.NOT_FOUND;
import static java.lang.String.format;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@ -169,6 +170,9 @@ public class BsqSwapSellBtcTradeTest extends AbstractTradeTest {
return client.getTrade(tradeId);
} catch (Exception ex) {
log.warn(ex.getMessage());
var statusCode = getStatusRuntimeExceptionStatusCode(ex);
assertEquals(NOT_FOUND, statusCode, "Expected a NOT_FOUND status code from server");
if (numFetchAttempts > 9) {
if (checkForLoggedExceptions) {
printNodeExceptionMessages(log);

View file

@ -65,7 +65,7 @@ public class FailUnfailTradeTest extends AbstractTradeTest {
aliceClient.failTrade(tradeId);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId));
String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId);
String expectedExceptionMessage = format("NOT_FOUND: trade with id '%s' not found", tradeId);
assertEquals(expectedExceptionMessage, exception.getMessage());
try {
@ -86,7 +86,7 @@ public class FailUnfailTradeTest extends AbstractTradeTest {
aliceClient.failTrade(tradeId);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId));
String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId);
String expectedExceptionMessage = format("NOT_FOUND: trade with id '%s' not found", tradeId);
assertEquals(expectedExceptionMessage, exception.getMessage());
try {
@ -108,7 +108,7 @@ public class FailUnfailTradeTest extends AbstractTradeTest {
aliceClient.failTrade(tradeId);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId));
String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId);
String expectedExceptionMessage = format("NOT_FOUND: trade with id '%s' not found", tradeId);
assertEquals(expectedExceptionMessage, exception.getMessage());
try {
@ -130,7 +130,7 @@ public class FailUnfailTradeTest extends AbstractTradeTest {
aliceClient.failTrade(tradeId);
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getTrade(tradeId));
String expectedExceptionMessage = format("INVALID_ARGUMENT: trade with id '%s' not found", tradeId);
String expectedExceptionMessage = format("NOT_FOUND: trade with id '%s' not found", tradeId);
assertEquals(expectedExceptionMessage, exception.getMessage());
try {

View file

@ -58,7 +58,7 @@ public class BtcTxFeeRateTest extends MethodTest {
var currentTxFeeRateInfo = TxFeeRateInfo.fromProto(aliceClient.getTxFeeRate());
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.setTxFeeRate(1));
String expectedExceptionMessage =
format("UNKNOWN: tx fee rate preference must be >= %d sats/byte",
format("INVALID_ARGUMENT: tx fee rate preference must be >= %d sats/byte",
currentTxFeeRateInfo.getMinFeeServiceRate());
assertEquals(expectedExceptionMessage, exception.getMessage());
}

View file

@ -48,7 +48,7 @@ public class WalletProtectionTest extends MethodTest {
@Order(2)
public void testGetBalanceOnEncryptedWalletShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@ -58,7 +58,7 @@ public class WalletProtectionTest extends MethodTest {
aliceClient.getBtcBalances(); // should not throw 'wallet locked' exception
sleep(4500); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@ -67,7 +67,7 @@ public class WalletProtectionTest extends MethodTest {
aliceClient.unlockWallet("first-password", 3);
sleep(4000); // let unlock timeout expire
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@ -76,14 +76,14 @@ public class WalletProtectionTest extends MethodTest {
aliceClient.unlockWallet("first-password", 60);
aliceClient.lockWallet();
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.getBtcBalances());
assertEquals("UNKNOWN: wallet is locked", exception.getMessage());
assertEquals("FAILED_PRECONDITION: wallet is locked", exception.getMessage());
}
@Test
@Order(6)
public void testLockWalletWhenWalletAlreadyLockedShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () -> aliceClient.lockWallet());
assertEquals("UNKNOWN: wallet is already locked", exception.getMessage());
assertEquals("ALREADY_EXISTS: wallet is already locked", exception.getMessage());
}
@Test
@ -110,7 +110,7 @@ public class WalletProtectionTest extends MethodTest {
public void testSetNewWalletPasswordWithIncorrectNewPasswordShouldThrowException() {
Throwable exception = assertThrows(StatusRuntimeException.class, () ->
aliceClient.setWalletPassword("bad old password", "irrelevant"));
assertEquals("UNKNOWN: incorrect old password", exception.getMessage());
assertEquals("INVALID_ARGUMENT: incorrect old password", exception.getMessage());
}
@Test

View file

@ -740,7 +740,8 @@ public class CliMain {
}
}
} catch (StatusRuntimeException ex) {
// Remove the leading gRPC status code (e.g. "UNKNOWN: ") from the message
// Remove the leading gRPC status code, e.g., INVALID_ARGUMENT,
// NOT_FOUND, ..., UNKNOWN from the exception message.
String message = ex.getMessage().replaceFirst("^[A-Z_]+: ", "");
if (message.equals("io exception"))
throw new RuntimeException(message + ", server may not be running", ex);

View file

@ -17,6 +17,7 @@
package bisq.core.api;
import bisq.core.api.exception.NotAvailableException;
import bisq.core.support.SupportType;
import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
@ -79,12 +80,12 @@ class CoreDisputeAgentsService {
void registerDisputeAgent(String disputeAgentType, String registrationKey) {
if (!p2PService.isBootstrapped())
throw new IllegalStateException("p2p service is not bootstrapped yet");
throw new NotAvailableException("p2p service is not bootstrapped yet");
if (config.baseCurrencyNetwork.isMainnet()
|| config.baseCurrencyNetwork.isDaoBetaNet()
|| !config.useLocalhostForP2P)
throw new IllegalStateException("dispute agents must be registered in a Bisq UI");
throw new UnsupportedOperationException("dispute agents must be registered in a Bisq UI");
if (!registrationKey.equals(DEV_PRIVILEGE_PRIV_KEY))
throw new IllegalArgumentException("invalid registration key");
@ -95,7 +96,7 @@ class CoreDisputeAgentsService {
String signature;
switch (supportType.get()) {
case ARBITRATION:
throw new IllegalArgumentException("arbitrators must be registered in a Bisq UI");
throw new UnsupportedOperationException("arbitrators must be registered in a Bisq UI");
case MEDIATION:
ecKey = mediatorManager.getRegistrationKey(registrationKey);
signature = mediatorManager.signStorageSignaturePubKey(Objects.requireNonNull(ecKey));
@ -107,7 +108,7 @@ class CoreDisputeAgentsService {
registerRefundAgent(nodeAddress, languageCodes, ecKey, signature);
return;
case TRADE:
throw new IllegalArgumentException("trade agent registration not supported");
throw new UnsupportedOperationException("trade agent registration not supported");
}
} else {
throw new IllegalArgumentException(format("unknown dispute agent type '%s'", disputeAgentType));

View file

@ -17,6 +17,8 @@
package bisq.core.api;
import bisq.core.api.exception.NotFoundException;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -45,7 +47,7 @@ class CoreHelpService {
return readHelpFile(resourceFile);
} catch (NullPointerException ex) {
log.error("", ex);
throw new IllegalStateException(format("no help found for api method %s", methodName));
throw new NotFoundException(format("no help found for api method %s", methodName));
} catch (IOException ex) {
log.error("", ex);
throw new IllegalStateException(format("could not read %s help doc", methodName));

View file

@ -17,6 +17,7 @@
package bisq.core.api;
import bisq.core.api.exception.NotFoundException;
import bisq.core.monetary.Altcoin;
import bisq.core.monetary.Price;
import bisq.core.offer.Offer;
@ -145,7 +146,7 @@ class CoreOffersService {
Offer getOffer(String id) {
return findAvailableOffer(id).orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
new NotFoundException(format("offer with id '%s' not found", id)));
}
Optional<Offer> findAvailableOffer(String id) {
@ -158,7 +159,7 @@ class CoreOffersService {
OpenOffer getMyOffer(String id) {
return findMyOpenOffer(id).orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
new NotFoundException(format("offer with id '%s' not found", id)));
}
Optional<OpenOffer> findMyOpenOffer(String id) {
@ -170,7 +171,7 @@ class CoreOffersService {
Offer getBsqSwapOffer(String id) {
return findAvailableBsqSwapOffer(id).orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
new NotFoundException(format("offer with id '%s' not found", id)));
}
Optional<Offer> findAvailableBsqSwapOffer(String id) {
@ -184,7 +185,7 @@ class CoreOffersService {
Offer getMyBsqSwapOffer(String id) {
return findMyBsqSwapOffer(id).orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
new NotFoundException(format("offer with id '%s' not found", id)));
}
Optional<Offer> findMyBsqSwapOffer(String id) {
@ -272,14 +273,14 @@ class CoreOffersService {
.filter(open -> open.getOffer().isMyOffer(keyRing))
.filter(open -> open.getOffer().isBsqSwapOffer())
.orElseThrow(() ->
new IllegalStateException(format("openoffer with id '%s' not found", id)));
new NotFoundException(format("openoffer with id '%s' not found", id)));
}
OpenOffer getMyOpenOffer(String id) {
return openOfferManager.getOpenOfferById(id)
.filter(open -> open.getOffer().isMyOffer(keyRing))
.orElseThrow(() ->
new IllegalStateException(format("offer with id '%s' not found", id)));
new NotFoundException(format("offer with id '%s' not found", id)));
}
boolean isMyOffer(Offer offer) {

View file

@ -17,6 +17,7 @@
package bisq.core.api;
import bisq.core.api.exception.NotFoundException;
import bisq.core.btc.model.AddressEntry;
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.offer.Offer;
@ -204,7 +205,7 @@ class CoreTradesService {
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
new NotFoundException(format("trade with id '%s' not found", tradeId)));
log.info("Closing trade {}", tradeId);
tradeManager.onTradeCompleted(trade);
}
@ -215,7 +216,7 @@ class CoreTradesService {
verifyTradeIsNotClosed(tradeId);
var trade = getOpenTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
new NotFoundException(format("trade with id '%s' not found", tradeId)));
verifyIsValidBTCAddress(toAddress);
@ -263,7 +264,7 @@ class CoreTradesService {
return closedTrade.get();
return tradeManager.findBsqSwapTradeById(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId)));
new NotFoundException(format("trade with id '%s' not found", tradeId)));
}
String getTradeRole(TradeModel tradeModel) {
@ -285,7 +286,7 @@ class CoreTradesService {
coreWalletsService.verifyEncryptedWalletIsUnlocked();
return getOpenTrade(tradeId).orElseGet(() ->
getClosedTrade(tradeId).orElseThrow(() ->
new IllegalArgumentException(format("trade with id '%s' not found", tradeId))
new NotFoundException(format("trade with id '%s' not found", tradeId))
));
}
@ -333,7 +334,7 @@ class CoreTradesService {
tradeManager.addFailedTradeToPendingTrades(failedTrade);
log.info("Failed trade {} changed to open trade.", tradeId);
}, () -> {
throw new IllegalArgumentException(format("failed trade '%s' not found", tradeId));
throw new NotFoundException(format("failed trade '%s' not found", tradeId));
});
}

View file

@ -17,6 +17,10 @@
package bisq.core.api;
import bisq.core.api.exception.AlreadyExistsException;
import bisq.core.api.exception.FailedPreconditionException;
import bisq.core.api.exception.NotAvailableException;
import bisq.core.api.exception.NotFoundException;
import bisq.core.api.model.AddressBalanceInfo;
import bisq.core.api.model.BalancesInfo;
import bisq.core.api.model.BsqBalanceInfo;
@ -152,7 +156,7 @@ class CoreWalletsService {
verifyWalletsAreAvailable();
verifyEncryptedWalletIsUnlocked();
if (balances.getAvailableBalance().get() == null)
throw new IllegalStateException("balance is not yet available");
throw new NotAvailableException("balance is not yet available");
switch (currencyCode.trim().toUpperCase()) {
case "BSQ":
@ -241,13 +245,13 @@ class CoreWalletsService {
txFeePerVbyte.value);
bsqTransferService.sendFunds(model, callback);
} catch (InsufficientMoneyException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send bsq due to insufficient funds", ex);
log.error(ex.toString());
throw new NotAvailableException("cannot send bsq due to insufficient funds", ex);
} catch (NumberFormatException
| BsqChangeBelowDustException
| TransactionVerificationException
| WalletException ex) {
log.error("", ex);
log.error(ex.toString());
throw new IllegalStateException(ex);
}
}
@ -299,11 +303,11 @@ class CoreWalletsService {
memo.isEmpty() ? null : memo,
callback);
} catch (AddressEntryException ex) {
log.error("", ex);
log.error(ex.toString());
throw new IllegalStateException("cannot send btc from any addresses in wallet", ex);
} catch (InsufficientFundsException | InsufficientMoneyException ex) {
log.error("", ex);
throw new IllegalStateException("cannot send btc due to insufficient funds", ex);
log.error(ex.toString());
throw new NotAvailableException("cannot send btc due to insufficient funds", ex);
}
}
@ -362,7 +366,7 @@ class CoreWalletsService {
}, MoreExecutors.directExecutor());
} catch (Exception ex) {
log.error("", ex);
log.error(ex.toString());
throw new IllegalStateException("could not request fees from fee service", ex);
}
}
@ -371,7 +375,7 @@ class CoreWalletsService {
ResultHandler resultHandler) {
long minFeePerVbyte = feeService.getMinFeePerVByte();
if (txFeeRate < minFeePerVbyte)
throw new IllegalStateException(
throw new IllegalArgumentException(
format("tx fee rate preference must be >= %d sats/byte", minFeePerVbyte));
preferences.setUseCustomWithdrawalTxFee(true);
@ -416,11 +420,11 @@ class CoreWalletsService {
if (newPassword != null && !newPassword.isEmpty()) {
// TODO Validate new password before replacing old password.
if (!walletsManager.areWalletsEncrypted())
throw new IllegalStateException("wallet is not encrypted with a password");
throw new FailedPreconditionException("wallet is not encrypted with a password");
KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
if (!walletsManager.checkAESKey(aesKey))
throw new IllegalStateException("incorrect old password");
throw new IllegalArgumentException("incorrect old password");
walletsManager.decryptWallets(aesKey);
aesKey = keyCrypterScrypt.deriveKey(newPassword);
@ -430,7 +434,7 @@ class CoreWalletsService {
}
if (walletsManager.areWalletsEncrypted())
throw new IllegalStateException("wallet is encrypted with a password");
throw new AlreadyExistsException("wallet is already encrypted with a password");
// TODO Validate new password.
KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
@ -440,10 +444,10 @@ class CoreWalletsService {
void lockWallet() {
if (!walletsManager.areWalletsEncrypted())
throw new IllegalStateException("wallet is not encrypted with a password");
throw new FailedPreconditionException("wallet is not encrypted with a password");
if (tempAesKey == null)
throw new IllegalStateException("wallet is already locked");
throw new AlreadyExistsException("wallet is already locked");
tempAesKey = null;
}
@ -457,7 +461,7 @@ class CoreWalletsService {
tempAesKey = keyCrypterScrypt.deriveKey(password);
if (!walletsManager.checkAESKey(tempAesKey))
throw new IllegalStateException("incorrect password");
throw new IllegalArgumentException("incorrect password");
if (lockTimer != null) {
// The user has called unlockwallet again, before the prior unlockwallet
@ -488,7 +492,7 @@ class CoreWalletsService {
KeyParameter aesKey = keyCrypterScrypt.deriveKey(password);
if (!walletsManager.checkAESKey(aesKey))
throw new IllegalStateException("incorrect password");
throw new IllegalArgumentException("incorrect password");
walletsManager.decryptWallets(aesKey);
walletsManager.backupWallets();
@ -503,7 +507,7 @@ class CoreWalletsService {
// to leave this check in place until certain AppStartupState will always work
// as expected.
if (!walletsManager.areWalletsAvailable())
throw new IllegalStateException("wallet is not yet available");
throw new NotAvailableException("wallet is not yet available");
}
// Throws a RuntimeException if wallets are not available or not encrypted.
@ -511,28 +515,28 @@ class CoreWalletsService {
verifyWalletAndNetworkIsReady();
if (!walletsManager.areWalletsAvailable())
throw new IllegalStateException("wallet is not yet available");
throw new NotAvailableException("wallet is not yet available");
if (!walletsManager.areWalletsEncrypted())
throw new IllegalStateException("wallet is not encrypted with a password");
throw new FailedPreconditionException("wallet is not encrypted with a password");
}
// Throws a RuntimeException if wallets are encrypted and locked.
void verifyEncryptedWalletIsUnlocked() {
if (walletsManager.areWalletsEncrypted() && tempAesKey == null)
throw new IllegalStateException("wallet is locked");
throw new FailedPreconditionException("wallet is locked");
}
// Throws a RuntimeException if wallets and network are not ready.
void verifyWalletAndNetworkIsReady() {
if (!appStartupState.isWalletAndNetworkReady())
throw new IllegalStateException("wallet and network is not yet initialized");
throw new NotAvailableException("wallet and network are not yet initialized");
}
// Throws a RuntimeException if application is not fully initialized.
void verifyApplicationIsFullyInitialized() {
if (!appStartupState.isApplicationFullyInitialized())
throw new IllegalStateException("server is not fully initialized");
throw new NotAvailableException("server is not fully initialized");
}
// Returns an Address for the string, or a RuntimeException if invalid.
@ -541,7 +545,7 @@ class CoreWalletsService {
return bsqFormatter.getAddressFromBsqAddress(address);
} catch (RuntimeException e) {
log.error("", e);
throw new IllegalStateException(format("%s is not a valid bsq address", address));
throw new IllegalArgumentException(format("%s is not a valid bsq address", address));
}
}
@ -552,7 +556,7 @@ class CoreWalletsService {
if (!currencyCode.equalsIgnoreCase("BSQ")
&& !currencyCode.equalsIgnoreCase("BTC"))
throw new IllegalStateException(format("wallet does not support %s", currencyCode));
throw new UnsupportedOperationException(format("wallet does not support %s", currencyCode));
}
private void maybeSetWalletsManagerKey() {
@ -593,15 +597,15 @@ class CoreWalletsService {
var availableBalance = balances.getAvailableBalance().get();
if (availableBalance == null)
throw new IllegalStateException("balance is not yet available");
throw new NotAvailableException("balance is not yet available");
var reservedBalance = balances.getReservedBalance().get();
if (reservedBalance == null)
throw new IllegalStateException("reserved balance is not yet available");
throw new NotAvailableException("reserved balance is not yet available");
var lockedBalance = balances.getLockedBalance().get();
if (lockedBalance == null)
throw new IllegalStateException("locked balance is not yet available");
throw new NotAvailableException("locked balance is not yet available");
return new BtcBalanceInfo(availableBalance.value,
reservedBalance.value,
@ -613,7 +617,7 @@ class CoreWalletsService {
private Coin getValidTransferAmount(String amount, CoinFormatter coinFormatter) {
Coin amountAsCoin = parseToCoin(amount, coinFormatter);
if (amountAsCoin.isLessThan(getMinNonDustOutput()))
throw new IllegalStateException(format("%s is an invalid transfer amount", amount));
throw new IllegalArgumentException(format("%s is an invalid transfer amount", amount));
return amountAsCoin;
}
@ -639,7 +643,7 @@ class CoreWalletsService {
.findFirst();
if (!addressEntry.isPresent())
throw new IllegalStateException(format("address %s not found in wallet", addressString));
throw new NotFoundException(format("address %s not found in wallet", addressString));
return addressEntry.get();
}
@ -651,13 +655,13 @@ class CoreWalletsService {
try {
Transaction tx = btcWalletService.getTransaction(txId);
if (tx == null)
throw new IllegalArgumentException(format("tx with id %s not found", txId));
throw new NotFoundException(format("tx with id %s not found", txId));
else
return tx;
} catch (IllegalArgumentException ex) {
log.error("", ex);
throw new IllegalArgumentException(
log.error(ex.toString());
throw new IllegalStateException(
format("could not get transaction with id %s%ncause: %s",
txId,
ex.getMessage().toLowerCase()));

View file

@ -205,7 +205,8 @@ class EditOfferValidator {
private void checkNotBsqOffer() {
if ("BSQ".equals(currentlyOpenOffer.getOffer().getCurrencyCode())) {
throw new IllegalStateException(
// An illegal argument is a user error.
throw new IllegalArgumentException(
format("cannot set mkt price margin or trigger price on fixed price bsq offer with id '%s'",
currentlyOpenOffer.getId()));
}
@ -213,8 +214,10 @@ class EditOfferValidator {
private void checkNotBsqSwapOffer() {
if (currentlyOpenOffer.getOffer().isBsqSwapOffer()) {
throw new IllegalStateException(
format("cannot edit bsq swap offer with id '%s'", currentlyOpenOffer.getId()));
// An illegal argument is a user error.
throw new IllegalArgumentException(
format("cannot edit bsq swap offer with id '%s', replace it with a new swap offer instead",
currentlyOpenOffer.getId()));
}
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api.exception;
import bisq.common.BisqException;
/**
* To be thrown in cases when some value or state already exists, e.g., trying to lock
* an encrypted wallet that is already locked.
*/
public class AlreadyExistsException extends BisqException {
public AlreadyExistsException(String format, Object... args) {
super(format, args);
}
}

View file

@ -0,0 +1,32 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api.exception;
import bisq.common.BisqException;
/**
* To be thrown in cases when client is attempting to change some state requiring a
* pre-conditional state to exist, e.g., when attempting to lock or unlock a wallet that
* is not encrypted.
*/
public class FailedPreconditionException extends BisqException {
public FailedPreconditionException(String format, Object... args) {
super(format, args);
}
}

View file

@ -0,0 +1,31 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api.exception;
import bisq.common.BisqException;
/**
* To be thrown in cases where some service or value, e.g.,
* a wallet balance, or sufficient funds are unavailable.
*/
public class NotAvailableException extends BisqException {
public NotAvailableException(String format, Object... args) {
super(format, args);
}
}

View file

@ -0,0 +1,33 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.api.exception;
import bisq.common.BisqException;
/**
* To be thrown when a file or entity such as an Offer, PaymentAccount, or Trade
* is not found by RPC methods such as GetOffer(id), PaymentAccount(id), or GetTrade(id).
*
* May also be used if a resource such as a File is not found.
*/
public class NotFoundException extends BisqException {
public NotFoundException(String format, Object... args) {
super(format, args);
}
}

View file

@ -0,0 +1,29 @@
/*
* This file is part of Bisq.
*
* Bisq is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bisq is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bisq. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* This package contains exceptions thrown only from services in the bisq.core.api
* package. They are needed by the gRPC daemon service classes in the
* bisq.daemon.grpc package, for the purpose of mapping these custom exceptions to
* meaningful io.grpc.Status.Code values sent to gRPC clients.
*
* If / when a Bisq webapp module is created on top of the core API, these exceptions
* will serve the same purpose, to be mapped to meaningful HTTP status codes sent to
* REST clients.
*/
package bisq.core.api.exception;

View file

@ -17,6 +17,11 @@
package bisq.daemon.grpc;
import bisq.core.api.exception.AlreadyExistsException;
import bisq.core.api.exception.FailedPreconditionException;
import bisq.core.api.exception.NotAvailableException;
import bisq.core.api.exception.NotFoundException;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
import io.grpc.stub.StreamObserver;
@ -29,8 +34,7 @@ import java.util.function.Predicate;
import org.slf4j.Logger;
import static io.grpc.Status.INVALID_ARGUMENT;
import static io.grpc.Status.UNKNOWN;
import static io.grpc.Status.*;
/**
* The singleton instance of this class handles any expected core api Throwable by
@ -40,9 +44,26 @@ import static io.grpc.Status.UNKNOWN;
@Singleton
class GrpcExceptionHandler {
private final Predicate<Throwable> isExpectedException = (t) ->
t instanceof IllegalStateException || t instanceof IllegalArgumentException;
private static final String CORE_API_EXCEPTION_PKG_NAME = NotFoundException.class.getPackage().getName();
/**
* Returns true if Throwable is a custom core api exception instance,
* or one of the following native Java exception instances:
* <p>
* <pre>
* IllegalArgumentException
* IllegalStateException
* UnsupportedOperationException
* </pre>
* </p>
*/
private final Predicate<Throwable> isExpectedException = (t) ->
t.getClass().getPackage().getName().equals(CORE_API_EXCEPTION_PKG_NAME)
|| t instanceof IllegalArgumentException
|| t instanceof IllegalStateException
|| t instanceof UnsupportedOperationException;
@SuppressWarnings("unused")
@Inject
public GrpcExceptionHandler() {
}
@ -107,18 +128,27 @@ class GrpcExceptionHandler {
};
private Status mapGrpcErrorStatus(Throwable t, String description) {
// We default to the UNKNOWN status, except were the mapping of a core api
// exception to a gRPC Status is obvious. If we ever use a gRPC reverse-proxy
// to support RESTful clients, we will need to have more specific mappings
// to support correct HTTP 1.1. status codes.
//noinspection SwitchStatementWithTooFewBranches
switch (t.getClass().getSimpleName()) {
// We go ahead and use a switch statement instead of if, in anticipation
// of more, specific exception mappings.
case "IllegalArgumentException":
return INVALID_ARGUMENT.withDescription(description);
default:
return UNKNOWN.withDescription(description);
}
// Check if a custom core.api.exception was thrown, so we can map it to a more
// meaningful io.grpc.Status, something more useful to gRPC clients than UNKNOWN.
if (t instanceof AlreadyExistsException)
return ALREADY_EXISTS.withDescription(description);
else if (t instanceof FailedPreconditionException)
return FAILED_PRECONDITION.withDescription(description);
else if (t instanceof NotFoundException)
return NOT_FOUND.withDescription(description);
else if (t instanceof NotAvailableException)
return UNAVAILABLE.withDescription(description);
// If the above checks did not return an io.grpc.Status.Code, we map
// the native Java exception to an io.grpc.Status.
if (t instanceof IllegalArgumentException)
return INVALID_ARGUMENT.withDescription(description);
else if (t instanceof IllegalStateException)
return UNKNOWN.withDescription(description);
else if (t instanceof UnsupportedOperationException)
return UNIMPLEMENTED.withDescription(description);
else
return UNKNOWN.withDescription(description);
}
}

View file

@ -243,9 +243,9 @@ public class GrpcServiceRateMeteringConfig {
timeUnit.toMillis(1) * numTimeUnits);
rateMeterConfigs.stream().filter(c -> c.isConfigForGrpcService(grpcServiceClassName))
.findFirst().ifPresentOrElse(
(config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits),
() -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName)
.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits)));
(config) -> config.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits),
() -> rateMeterConfigs.add(new GrpcServiceRateMeteringConfig(grpcServiceClassName)
.addMethodCallRateMeter(methodName, maxCalls, timeUnit, numTimeUnits)));
}
public File build() {