mirror of
https://github.com/bisq-network/bisq.git
synced 2024-11-19 01:41:11 +01:00
Improve takeoffer output and failure reason messaging
- Added AvailabilityResultWithDescription proto for better takeoffer failure msgs. - Added VerifyBsqSentToAddress impl to api, but don't expose to CLI yet. - Show BSQ Buyer Address in gettrade output (changed cli output formatting classes). - Fixed api.model.PaymentAccountPayloadInfo altcoin instant acct support bug
This commit is contained in:
parent
6299dc33d9
commit
6bde12ba40
@ -51,6 +51,7 @@ class ColumnHeaderConstants {
|
||||
static final String COL_HEADER_PRICE = "Price in %-3s for 1 BTC";
|
||||
static final String COL_HEADER_PRICE_OF_ALTCOIN = "Price in BTC for 1 %-3s";
|
||||
static final String COL_HEADER_TRADE_AMOUNT = padStart("Amount(%-3s)", 12, ' ');
|
||||
static final String COL_HEADER_TRADE_BSQ_BUYER_ADDRESS = "BSQ Buyer Address";
|
||||
static final String COL_HEADER_TRADE_BUYER_COST = padEnd("Buyer Cost(%-3s)", 15, ' ');
|
||||
static final String COL_HEADER_TRADE_DEPOSIT_CONFIRMED = "Deposit Confirmed";
|
||||
static final String COL_HEADER_TRADE_DEPOSIT_PUBLISHED = "Deposit Published";
|
||||
|
@ -57,7 +57,7 @@ public class CurrencyFormat {
|
||||
return BSQ_FORMAT.format(BigDecimal.valueOf(sats).divide(BSQ_SATOSHI_DIVISOR));
|
||||
}
|
||||
|
||||
public static String formatBsqSendAmount(long bsqSats) {
|
||||
public static String formatBsqAmount(long bsqSats) {
|
||||
// BSQ sats = trade.getOffer().getVolume()
|
||||
NUMBER_FORMAT.setMinimumFractionDigits(2);
|
||||
NUMBER_FORMAT.setMaximumFractionDigits(2);
|
||||
|
@ -62,6 +62,7 @@ import bisq.proto.grpc.TxFeeRateInfo;
|
||||
import bisq.proto.grpc.TxInfo;
|
||||
import bisq.proto.grpc.UnlockWalletRequest;
|
||||
import bisq.proto.grpc.UnsetTxFeeRatePreferenceRequest;
|
||||
import bisq.proto.grpc.VerifyBsqSentToAddressRequest;
|
||||
import bisq.proto.grpc.WithdrawFundsRequest;
|
||||
|
||||
import protobuf.PaymentAccount;
|
||||
@ -166,6 +167,14 @@ public final class GrpcClient {
|
||||
return grpcStubs.walletsService.sendBtc(request).getTxInfo();
|
||||
}
|
||||
|
||||
public boolean verifyBsqSentToAddress(String address, String amount) {
|
||||
var request = VerifyBsqSentToAddressRequest.newBuilder()
|
||||
.setAddress(address)
|
||||
.setAmount(amount)
|
||||
.build();
|
||||
return grpcStubs.walletsService.verifyBsqSentToAddress(request).getIsAmountReceived();
|
||||
}
|
||||
|
||||
public TxFeeRateInfo getTxFeeRate() {
|
||||
var request = GetTxFeeRateRequest.newBuilder().build();
|
||||
return grpcStubs.walletsService.getTxFeeRate(request).getTxFeeRateInfo();
|
||||
@ -367,7 +376,7 @@ public final class GrpcClient {
|
||||
if (reply.hasTrade())
|
||||
return reply.getTrade();
|
||||
else
|
||||
throw new IllegalStateException(reply.getAvailabilityResultDescription());
|
||||
throw new IllegalStateException(reply.getFailureReason().getDescription());
|
||||
}
|
||||
|
||||
public TradeInfo getTrade(String tradeId) {
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
package bisq.cli;
|
||||
|
||||
import bisq.proto.grpc.ContractInfo;
|
||||
import bisq.proto.grpc.TradeInfo;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
@ -58,6 +59,10 @@ public class TradeFormat {
|
||||
"%" + (COL_HEADER_TRADE_TAKER_FEE.length() + 2) + "s"
|
||||
: "";
|
||||
|
||||
boolean showBsqBuyerAddress = shouldShowBqsBuyerAddress(tradeInfo, isTaker);
|
||||
Supplier<String> bsqBuyerAddressHeader = () -> showBsqBuyerAddress ? COL_HEADER_TRADE_BSQ_BUYER_ADDRESS : "";
|
||||
Supplier<String> bsqBuyerAddressHeaderSpec = () -> showBsqBuyerAddress ? "%s" : "";
|
||||
|
||||
String headersFormat = padEnd(COL_HEADER_TRADE_SHORT_ID, shortIdColWidth, ' ') + COL_HEADER_DELIMITER
|
||||
+ padEnd(COL_HEADER_TRADE_ROLE, roleColWidth, ' ') + COL_HEADER_DELIMITER
|
||||
+ priceHeader.apply(tradeInfo) + COL_HEADER_DELIMITER // includes %s -> currencyCode
|
||||
@ -73,6 +78,7 @@ public class TradeFormat {
|
||||
+ COL_HEADER_TRADE_PAYMENT_RECEIVED + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_PAYOUT_PUBLISHED + COL_HEADER_DELIMITER
|
||||
+ COL_HEADER_TRADE_WITHDRAWN + COL_HEADER_DELIMITER
|
||||
+ bsqBuyerAddressHeader.get()
|
||||
+ "%n";
|
||||
|
||||
String counterCurrencyCode = tradeInfo.getOffer().getCounterCurrencyCode();
|
||||
@ -100,14 +106,16 @@ public class TradeFormat {
|
||||
+ " %-" + (COL_HEADER_TRADE_PAYMENT_SENT.length() - 1) + "s" // left
|
||||
+ " %-" + (COL_HEADER_TRADE_PAYMENT_RECEIVED.length() - 1) + "s" // left
|
||||
+ " %-" + COL_HEADER_TRADE_PAYOUT_PUBLISHED.length() + "s" // lt justify
|
||||
+ " %-" + COL_HEADER_TRADE_WITHDRAWN.length() + "s"; // lt justify
|
||||
+ " %-" + (COL_HEADER_TRADE_WITHDRAWN.length() + 2) + "s"
|
||||
+ bsqBuyerAddressHeaderSpec.get();
|
||||
|
||||
return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker);
|
||||
return headerLine + formatTradeData(colDataFormat, tradeInfo, isTaker, showBsqBuyerAddress);
|
||||
}
|
||||
|
||||
private static String formatTradeData(String format,
|
||||
TradeInfo tradeInfo,
|
||||
boolean isTaker) {
|
||||
boolean isTaker,
|
||||
boolean showBsqBuyerAddress) {
|
||||
return String.format(format,
|
||||
tradeInfo.getShortId(),
|
||||
tradeInfo.getRole(),
|
||||
@ -121,7 +129,8 @@ public class TradeFormat {
|
||||
tradeInfo.getIsFiatSent() ? YES : NO,
|
||||
tradeInfo.getIsFiatReceived() ? YES : NO,
|
||||
tradeInfo.getIsPayoutPublished() ? YES : NO,
|
||||
tradeInfo.getIsWithdrawn() ? YES : NO);
|
||||
tradeInfo.getIsWithdrawn() ? YES : NO,
|
||||
bsqReceiveAddress.apply(tradeInfo, showBsqBuyerAddress));
|
||||
}
|
||||
|
||||
private static final Function<TradeInfo, String> priceHeader = (t) ->
|
||||
@ -181,4 +190,32 @@ public class TradeFormat {
|
||||
t.getOffer().getBaseCurrencyCode().equals("BTC")
|
||||
? formatOfferVolume(t.getOffer().getVolume())
|
||||
: formatSatoshis(t.getTradeAmountAsLong());
|
||||
|
||||
private static final BiFunction<TradeInfo, Boolean, String> bsqReceiveAddress = (t, showBsqBuyerAddress) -> {
|
||||
if (showBsqBuyerAddress) {
|
||||
ContractInfo contract = t.getContract();
|
||||
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
|
||||
return isBuyerMakerAndSellerTaker // (is BTC buyer / maker)
|
||||
? contract.getTakerPaymentAccountPayload().getAddress()
|
||||
: contract.getMakerPaymentAccountPayload().getAddress();
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
};
|
||||
|
||||
private static boolean shouldShowBqsBuyerAddress(TradeInfo tradeInfo, boolean isTaker) {
|
||||
if (tradeInfo.getOffer().getBaseCurrencyCode().equals("BTC")) {
|
||||
return false;
|
||||
} else {
|
||||
ContractInfo contract = tradeInfo.getContract();
|
||||
// Do not forget buyer and seller refer to BTC buyer and seller, not BSQ
|
||||
// buyer and seller. If you are buying BSQ, you are the (BTC) seller.
|
||||
boolean isBuyerMakerAndSellerTaker = contract.getIsBuyerMakerAndSellerTaker();
|
||||
if (isTaker) {
|
||||
return !isBuyerMakerAndSellerTaker;
|
||||
} else {
|
||||
return isBuyerMakerAndSellerTaker;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -52,8 +52,10 @@ import org.bitcoinj.core.Address;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.InsufficientMoneyException;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.bitcoinj.core.TransactionConfidence;
|
||||
import org.bitcoinj.core.TransactionOutput;
|
||||
import org.bitcoinj.crypto.KeyCrypterScrypt;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -75,6 +77,7 @@ import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -145,6 +148,10 @@ class CoreWalletsService {
|
||||
return tempAesKey;
|
||||
}
|
||||
|
||||
NetworkParameters getNetworkParameters() {
|
||||
return btcWalletService.getWallet().getContext().getParams();
|
||||
}
|
||||
|
||||
BalancesInfo getBalances(String currencyCode) {
|
||||
verifyWalletCurrencyCodeIsValid(currencyCode);
|
||||
verifyWalletsAreAvailable();
|
||||
@ -305,6 +312,41 @@ class CoreWalletsService {
|
||||
}
|
||||
}
|
||||
|
||||
boolean verifyBsqSentToAddress(String address, String amount) {
|
||||
Address receiverAddress = getValidBsqLegacyAddress(address);
|
||||
NetworkParameters networkParameters = getNetworkParameters();
|
||||
Predicate<TransactionOutput> isTxOutputAddressMatch = (txOut) ->
|
||||
txOut.getScriptPubKey().getToAddress(networkParameters).equals(receiverAddress);
|
||||
Coin coinValue = parseToCoin(amount, bsqFormatter);
|
||||
Predicate<TransactionOutput> isTxOutputValueMatch = (txOut) ->
|
||||
txOut.getValue().longValue() == coinValue.longValue();
|
||||
List<TransactionOutput> spendableBsqTxOutputs = bsqWalletService.getSpendableBsqTransactionOutputs();
|
||||
|
||||
log.info("Searching {} spendable tx outputs for matching address {} and value {}:",
|
||||
spendableBsqTxOutputs.size(),
|
||||
address,
|
||||
coinValue.toPlainString());
|
||||
long numMatches = 0;
|
||||
for (TransactionOutput txOut : spendableBsqTxOutputs) {
|
||||
if (isTxOutputAddressMatch.test(txOut) && isTxOutputValueMatch.test(txOut)) {
|
||||
log.info("\t\tTx {} output has matching address {} and value {}.",
|
||||
txOut.getParentTransaction().getTxId(),
|
||||
address,
|
||||
txOut.getValue().toPlainString());
|
||||
numMatches++;
|
||||
}
|
||||
}
|
||||
if (numMatches > 1) {
|
||||
log.warn("{} tx outputs matched address {} and value {}, could be a"
|
||||
+ " false positive BSQ payment verification result.",
|
||||
numMatches,
|
||||
address,
|
||||
coinValue.toPlainString());
|
||||
|
||||
}
|
||||
return numMatches > 0;
|
||||
}
|
||||
|
||||
void getTxFeeRate(ResultHandler resultHandler) {
|
||||
try {
|
||||
@SuppressWarnings({"unchecked", "Convert2MethodRef"})
|
||||
|
@ -18,10 +18,12 @@
|
||||
package bisq.core.api.model;
|
||||
|
||||
import bisq.core.payment.payload.CryptoCurrencyAccountPayload;
|
||||
import bisq.core.payment.payload.InstantCryptoCurrencyPayload;
|
||||
import bisq.core.payment.payload.PaymentAccountPayload;
|
||||
|
||||
import bisq.common.Payload;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import lombok.Getter;
|
||||
@ -45,12 +47,15 @@ public class PaymentAccountPayloadInfo implements Payload {
|
||||
}
|
||||
|
||||
public static PaymentAccountPayloadInfo toPaymentAccountPayloadInfo(PaymentAccountPayload paymentAccountPayload) {
|
||||
String address = paymentAccountPayload instanceof CryptoCurrencyAccountPayload
|
||||
? ((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress()
|
||||
: "";
|
||||
Optional<String> address = Optional.empty();
|
||||
if (paymentAccountPayload instanceof CryptoCurrencyAccountPayload)
|
||||
address = Optional.of(((CryptoCurrencyAccountPayload) paymentAccountPayload).getAddress());
|
||||
else if (paymentAccountPayload instanceof InstantCryptoCurrencyPayload)
|
||||
address = Optional.of(((InstantCryptoCurrencyPayload) paymentAccountPayload).getAddress());
|
||||
|
||||
return new PaymentAccountPayloadInfo(paymentAccountPayload.getId(),
|
||||
paymentAccountPayload.getPaymentMethodId(),
|
||||
address);
|
||||
address.orElse(""));
|
||||
}
|
||||
|
||||
// For transmitting TradeInfo messages when no contract & payloads are available.
|
||||
|
@ -19,6 +19,7 @@ package bisq.daemon.grpc;
|
||||
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
|
||||
import bisq.proto.grpc.AvailabilityResultWithDescription;
|
||||
import bisq.proto.grpc.TakeOfferReply;
|
||||
|
||||
import protobuf.AvailabilityResult;
|
||||
@ -77,7 +78,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
|
||||
this.isErrorHandled = true;
|
||||
log.error(errorMessage);
|
||||
|
||||
if (isTakeOfferError()) {
|
||||
if (takeOfferWasCalled()) {
|
||||
handleTakeOfferError(errorMessage);
|
||||
} else {
|
||||
exceptionHandler.handleErrorMessage(log,
|
||||
@ -88,14 +89,20 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
|
||||
}
|
||||
|
||||
private void handleTakeOfferError(String errorMessage) {
|
||||
// Send the AvailabilityResult to the client instead of throwing an exception.
|
||||
// The client should look at the grpc reply object's AvailabilityResult
|
||||
// field if reply.hasTrade = false, and use it give the user a human readable msg.
|
||||
// If the errorMessage originated from a UI purposed TaskRunner, it should
|
||||
// contain an AvailabilityResult enum name. If it does, derive the
|
||||
// AvailabilityResult enum from the errorMessage, wrap it in a new
|
||||
// AvailabilityResultWithDescription enum, then send the
|
||||
// AvailabilityResultWithDescription to the client instead of throwing
|
||||
// an exception. The client should use the grpc reply object's
|
||||
// AvailabilityResultWithDescription field if reply.hasTrade = false, and the
|
||||
// client can decide to throw an exception with the client friendly error
|
||||
// description, or take some other action based on the AvailabilityResult enum.
|
||||
// (Some offer availability problems are not fatal, and retries are appropriate.)
|
||||
try {
|
||||
AvailabilityResult availabilityResultProto = getAvailabilityResult(errorMessage);
|
||||
var failureReason = getAvailabilityResultWithDescription(errorMessage);
|
||||
var reply = TakeOfferReply.newBuilder()
|
||||
.setAvailabilityResult(availabilityResultProto)
|
||||
.setAvailabilityResultDescription(getAvailabilityResultDescription(availabilityResultProto))
|
||||
.setFailureReason(failureReason)
|
||||
.build();
|
||||
@SuppressWarnings("unchecked")
|
||||
var takeOfferResponseObserver = (StreamObserver<TakeOfferReply>) responseObserver;
|
||||
@ -109,6 +116,15 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
private AvailabilityResultWithDescription getAvailabilityResultWithDescription(String errorMessage) {
|
||||
AvailabilityResult proto = getAvailabilityResult(errorMessage);
|
||||
String description = getAvailabilityResultDescription(proto);
|
||||
return AvailabilityResultWithDescription.newBuilder()
|
||||
.setAvailabilityResult(proto)
|
||||
.setDescription(description)
|
||||
.build();
|
||||
}
|
||||
|
||||
private AvailabilityResult getAvailabilityResult(String errorMessage) {
|
||||
return stream(AvailabilityResult.values())
|
||||
.filter((e) -> errorMessage.toUpperCase().contains(e.name()))
|
||||
@ -121,7 +137,7 @@ public class GrpcErrorMessageHandler implements ErrorMessageHandler {
|
||||
return bisq.core.offer.AvailabilityResult.fromProto(proto).description();
|
||||
}
|
||||
|
||||
private boolean isTakeOfferError() {
|
||||
private boolean takeOfferWasCalled() {
|
||||
return fullMethodName.equals(getTakeOfferMethod().getFullMethodName());
|
||||
}
|
||||
}
|
||||
|
@ -161,6 +161,11 @@ message OfferInfo {
|
||||
uint64 makerFee = 23;
|
||||
}
|
||||
|
||||
message AvailabilityResultWithDescription {
|
||||
AvailabilityResult availabilityResult = 1;
|
||||
string description = 2;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PaymentAccounts
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -303,8 +308,7 @@ message TakeOfferRequest {
|
||||
|
||||
message TakeOfferReply {
|
||||
TradeInfo trade = 1;
|
||||
AvailabilityResult availabilityResult = 2;
|
||||
string availabilityResultDescription = 3;
|
||||
AvailabilityResultWithDescription failureReason = 2;
|
||||
}
|
||||
|
||||
message ConfirmPaymentStartedRequest {
|
||||
@ -430,6 +434,8 @@ service Wallets {
|
||||
}
|
||||
rpc SendBtc (SendBtcRequest) returns (SendBtcReply) {
|
||||
}
|
||||
rpc VerifyBsqSentToAddress (VerifyBsqSentToAddressRequest) returns (VerifyBsqSentToAddressReply) {
|
||||
}
|
||||
rpc GetTxFeeRate (GetTxFeeRateRequest) returns (GetTxFeeRateReply) {
|
||||
}
|
||||
rpc SetTxFeeRatePreference (SetTxFeeRatePreferenceRequest) returns (SetTxFeeRatePreferenceReply) {
|
||||
@ -494,6 +500,15 @@ message SendBtcReply {
|
||||
TxInfo txInfo = 1;
|
||||
}
|
||||
|
||||
message VerifyBsqSentToAddressRequest {
|
||||
string address = 1;
|
||||
string amount = 2;
|
||||
}
|
||||
|
||||
message VerifyBsqSentToAddressReply {
|
||||
bool isAmountReceived = 1;
|
||||
}
|
||||
|
||||
message GetTxFeeRateRequest {
|
||||
}
|
||||
|
||||
@ -606,4 +621,3 @@ message GetVersionRequest {
|
||||
message GetVersionReply {
|
||||
string version = 1;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user