From f170dd7a4f183b103dcdabd0b3997c47556db202 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Tue, 7 Jul 2020 13:24:50 +0200 Subject: [PATCH 01/12] Refactor: extract DonationAddressValidation Move donation address validation to its own class. Will be needed for atomic transaction validation. --- .../core/trade/DelayedPayoutTxValidation.java | 65 +---------- .../core/trade/DonationAddressValidation.java | 101 ++++++++++++++++++ .../java/bisq/core/trade/TradeManager.java | 5 +- .../BuyerVerifiesFinalDelayedPayoutTx.java | 3 +- .../BuyerVerifiesPreparedDelayedPayoutTx.java | 3 +- .../steps/buyer/BuyerStep1View.java | 3 +- .../steps/buyer/BuyerStep2View.java | 3 +- 7 files changed, 114 insertions(+), 69 deletions(-) create mode 100644 core/src/main/java/bisq/core/trade/DonationAddressValidation.java diff --git a/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java b/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java index 6c835d2408b..3251b6cc58e 100644 --- a/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java +++ b/core/src/main/java/bisq/core/trade/DelayedPayoutTxValidation.java @@ -19,21 +19,14 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.dao.DaoFacade; -import bisq.core.dao.governance.param.Param; import bisq.core.offer.Offer; -import bisq.common.config.Config; - -import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; import org.bitcoinj.core.Transaction; import org.bitcoinj.core.TransactionInput; import org.bitcoinj.core.TransactionOutPoint; import org.bitcoinj.core.TransactionOutput; -import java.util.List; - import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkNotNull; @@ -41,12 +34,6 @@ @Slf4j public class DelayedPayoutTxValidation { - public static class DonationAddressException extends Exception { - DonationAddressException(String msg) { - super(msg); - } - } - public static class MissingDelayedPayoutTxException extends Exception { MissingDelayedPayoutTxException(String msg) { super(msg); @@ -81,7 +68,7 @@ public static void validatePayoutTx(Trade trade, Transaction delayedPayoutTx, DaoFacade daoFacade, BtcWalletService btcWalletService) - throws DonationAddressException, MissingDelayedPayoutTxException, + throws DonationAddressValidation.DonationAddressException, MissingDelayedPayoutTxException, InvalidTxException, InvalidLockTimeException, AmountMismatchException { String errorMsg; if (delayedPayoutTx == null) { @@ -132,7 +119,8 @@ public static void validatePayoutTx(Trade trade, .add(checkNotNull(trade.getTradeAmount())); if (!output.getValue().equals(msOutputAmount)) { - errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + " / msOutputAmount: " + msOutputAmount; + errorMsg = "Output value of deposit tx and delayed payout tx is not matching. Output: " + output + + " / msOutputAmount: " + msOutputAmount; log.error(errorMsg); log.error(delayedPayoutTx.toString()); throw new AmountMismatchException(errorMsg); @@ -144,52 +132,7 @@ public static void validatePayoutTx(Trade trade, // We do not support past DAO param addresses to avoid that those receive funds (no bond set up anymore). // Users who have not synced the DAO cannot trade. - - NetworkParameters params = btcWalletService.getParams(); - Address address = output.getAddressFromP2PKHScript(params); - if (address == null) { - // The donation address can be as well be a multisig address. - address = output.getAddressFromP2SH(params); - if (address == null) { - errorMsg = "Donation address cannot be resolved (not of type P2PKHScript or P2SH). Output: " + output; - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new DonationAddressException(errorMsg); - } - } - - String addressAsString = address.toString(); - - // In case the seller has deactivated the DAO the default address will be used. - String defaultDonationAddressString = Param.RECIPIENT_BTC_ADDRESS.getDefaultValue(); - boolean defaultNotMatching = !defaultDonationAddressString.equals(addressAsString); - String recentDonationAddressString = daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS); - boolean recentFromDaoNotMatching = !recentDonationAddressString.equals(addressAsString); - - // If buyer has DAO deactivated or not synced he will not be able to see recent address used by the seller, so - // we add it hard coded here. We need to support also the default one as - // FIXME This is a quick fix and should be improved in future. - // We use the default addresses for non mainnet networks. For dev testing it need to be changed here. - // We use a list to gain more flexibility at updates of DAO param, but still might fail if buyer has not updated - // software. Needs a better solution.... - List hardCodedAddresses = Config.baseCurrencyNetwork().isMainnet() ? - List.of("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp", "3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV") : // mainnet - Config.baseCurrencyNetwork().isDaoBetaNet() ? List.of("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7") : // daoBetaNet - Config.baseCurrencyNetwork().isTestnet() ? List.of("2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV") : // testnet - List.of("2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w"); // regtest or DAO testnet (regtest) - - boolean noneOfHardCodedMatching = hardCodedAddresses.stream().noneMatch(e -> e.equals(addressAsString)); - - // If seller has DAO deactivated as well we get default address - if (recentFromDaoNotMatching && defaultNotMatching && noneOfHardCodedMatching) { - errorMsg = "Donation address is invalid." + - "\nAddress used by BTC seller: " + addressAsString + - "\nRecent donation address:" + recentDonationAddressString + - "\nDefault donation address: " + defaultDonationAddressString; - log.error(errorMsg); - log.error(delayedPayoutTx.toString()); - throw new DonationAddressException(errorMsg); - } + DonationAddressValidation.validateDonationAddress(output, delayedPayoutTx, daoFacade, btcWalletService); } public static void validatePayoutTxInput(Transaction depositTx, diff --git a/core/src/main/java/bisq/core/trade/DonationAddressValidation.java b/core/src/main/java/bisq/core/trade/DonationAddressValidation.java new file mode 100644 index 00000000000..3441acdc29a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/DonationAddressValidation.java @@ -0,0 +1,101 @@ +/* + * 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 . + */ + +package bisq.core.trade; + +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.param.Param; + +import bisq.common.config.Config; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.NetworkParameters; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionOutput; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DonationAddressValidation { + + public static class DonationAddressException extends Exception { + DonationAddressException(String msg) { + super(msg); + } + } + + public static void validateDonationAddress(TransactionOutput output, + Transaction transaction, + DaoFacade daoFacade, + BtcWalletService btcWalletService) + throws DonationAddressException { + String errorMsg; + // Validate donation address + // Get most recent donation address. + // We do not support past DAO param addresses to avoid that those receive funds (no bond set up anymore). + // Users who have not synced the DAO cannot trade. + + NetworkParameters params = btcWalletService.getParams(); + Address address = output.getAddressFromP2PKHScript(params); + if (address == null) { + // The donation address can be as well be a multisig address. + address = output.getAddressFromP2SH(params); + if (address == null) { + errorMsg = "Donation address cannot be resolved (not of type P2PKHScript or P2SH). Output: " + output; + log.error(errorMsg); + log.error(transaction.toString()); + throw new DonationAddressException(errorMsg); + } + } + + String addressAsString = address.toString(); + + // In case the seller has deactivated the DAO the default address will be used. + String defaultDonationAddressString = Param.RECIPIENT_BTC_ADDRESS.getDefaultValue(); + boolean defaultNotMatching = !defaultDonationAddressString.equals(addressAsString); + String recentDonationAddressString = daoFacade.getParamValue(Param.RECIPIENT_BTC_ADDRESS); + boolean recentFromDaoNotMatching = !recentDonationAddressString.equals(addressAsString); + + // If verifier has DAO deactivated or not synced he will not be able to see recent address used by counterparty, + // so we add it hard coded here. We need to support also the default one as + // FIXME This is a quick fix and should be improved in future. + // We use the default addresses for non mainnet networks. For dev testing it need to be changed here. + // We use a list to gain more flexibility at updates of DAO param, but still might fail if buyer has not updated + // software. Needs a better solution.... + List hardCodedAddresses = Config.baseCurrencyNetwork().isMainnet() ? + List.of("3EtUWqsGThPtjwUczw27YCo6EWvQdaPUyp", "3A8Zc1XioE2HRzYfbb5P8iemCS72M6vRJV") : // mainnet + Config.baseCurrencyNetwork().isDaoBetaNet() ? List.of("1BVxNn3T12veSK6DgqwU4Hdn7QHcDDRag7") : // daoBetaNet + Config.baseCurrencyNetwork().isTestnet() ? List.of("2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV") : // testnet + List.of("2MzBNTJDjjXgViKBGnatDU3yWkJ8pJkEg9w"); // regtest or DAO testnet (regtest) + + boolean noneOfHardCodedMatching = hardCodedAddresses.stream().noneMatch(e -> e.equals(addressAsString)); + + // If counterparty DAO is deactivated as well we get default address + if (recentFromDaoNotMatching && defaultNotMatching && noneOfHardCodedMatching) { + errorMsg = "Donation address is invalid." + + "\nAddress used by BTC seller: " + addressAsString + + "\nRecent donation address:" + recentDonationAddressString + + "\nDefault donation address: " + defaultDonationAddressString; + log.error(errorMsg); + log.error(transaction.toString()); + throw new DonationAddressException(errorMsg); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 31bb507e44f..3ae37d9e478 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -65,8 +65,6 @@ import bisq.common.proto.network.NetworkEnvelope; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.storage.Storage; -import bisq.common.util.Tuple2; -import bisq.common.util.Utilities; import org.bitcoinj.core.AddressFormatException; import org.bitcoinj.core.Coin; @@ -93,7 +91,6 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.UUID; @@ -310,7 +307,7 @@ private void initPendingTrades() { trade.getDelayedPayoutTx(), daoFacade, btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | + } catch (DonationAddressValidation.DonationAddressException | DelayedPayoutTxValidation.InvalidTxException | DelayedPayoutTxValidation.InvalidLockTimeException | DelayedPayoutTxValidation.MissingDelayedPayoutTxException | diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java index fa1aeacdbe6..e7115124c33 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesFinalDelayedPayoutTx.java @@ -18,6 +18,7 @@ package bisq.core.trade.protocol.tasks.buyer; import bisq.core.trade.DelayedPayoutTxValidation; +import bisq.core.trade.DonationAddressValidation; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; @@ -55,7 +56,7 @@ protected void run() { DelayedPayoutTxValidation.validatePayoutTxInput(depositTx, delayedPayoutTx); complete(); - } catch (DelayedPayoutTxValidation.DonationAddressException | + } catch (DonationAddressValidation.DonationAddressException | DelayedPayoutTxValidation.MissingDelayedPayoutTxException | DelayedPayoutTxValidation.InvalidTxException | DelayedPayoutTxValidation.InvalidLockTimeException | diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java index 3242ba8cf6a..eaefb510238 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/buyer/BuyerVerifiesPreparedDelayedPayoutTx.java @@ -18,6 +18,7 @@ package bisq.core.trade.protocol.tasks.buyer; import bisq.core.trade.DelayedPayoutTxValidation; +import bisq.core.trade.DonationAddressValidation; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.TradeTask; @@ -43,7 +44,7 @@ protected void run() { processModel.getBtcWalletService()); complete(); - } catch (DelayedPayoutTxValidation.DonationAddressException | + } catch (DonationAddressValidation.DonationAddressException | DelayedPayoutTxValidation.MissingDelayedPayoutTxException | DelayedPayoutTxValidation.InvalidTxException | DelayedPayoutTxValidation.InvalidLockTimeException | diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java index 184dcd038f9..7525e5366a6 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep1View.java @@ -23,6 +23,7 @@ import bisq.core.locale.Res; import bisq.core.trade.DelayedPayoutTxValidation; +import bisq.core.trade.DonationAddressValidation; public class BuyerStep1View extends TradeStepView { @@ -43,7 +44,7 @@ public void activate() { trade.getDelayedPayoutTx(), model.dataModel.daoFacade, model.dataModel.btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | + } catch (DonationAddressValidation.DonationAddressException | DelayedPayoutTxValidation.InvalidTxException | DelayedPayoutTxValidation.AmountMismatchException | DelayedPayoutTxValidation.InvalidLockTimeException e) { diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java index 80826ccbde4..d4ac2f03855 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/steps/buyer/BuyerStep2View.java @@ -70,6 +70,7 @@ import bisq.core.payment.payload.USPostalMoneyOrderAccountPayload; import bisq.core.payment.payload.WesternUnionAccountPayload; import bisq.core.trade.DelayedPayoutTxValidation; +import bisq.core.trade.DonationAddressValidation; import bisq.core.trade.Trade; import bisq.core.user.DontShowAgainLookup; @@ -120,7 +121,7 @@ public void activate() { trade.getDelayedPayoutTx(), model.dataModel.daoFacade, model.dataModel.btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | + } catch (DonationAddressValidation.DonationAddressException | DelayedPayoutTxValidation.InvalidTxException | DelayedPayoutTxValidation.AmountMismatchException | DelayedPayoutTxValidation.InvalidLockTimeException e) { From 51e0464ad0a9dc8d83d02c55687a21be445984f2 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:12:22 +0200 Subject: [PATCH 02/12] Add atomic account type Add a hidden account type that's automatically used for BSQ trades. This means old orders not using AtomicAccount can still complete the trades as per normal and new offers placed with AtomicAccount can be taken by anyone with an upgraded client, but won't be possible to take by users with older clients. The atomic account is added on startup if not already added. There is no data associated with the account, a new BSQ address will be chosen automatically during the atomic trade process. --- .../java/bisq/core/payment/AtomicAccount.java | 37 ++++++++++++ .../core/payment/PaymentAccountFactory.java | 2 + .../payment/payload/AtomicAccountPayload.java | 57 +++++++++++++++++++ .../core/payment/payload/PaymentMethod.java | 14 ++++- .../bisq/core/proto/CoreProtoResolver.java | 3 + core/src/main/java/bisq/core/user/User.java | 18 ++++++ .../fiataccounts/FiatAccountsDataModel.java | 2 +- .../windows/SignPaymentAccountsWindow.java | 2 +- .../main/java/bisq/desktop/util/GUIUtil.java | 2 +- proto/src/main/proto/pb.proto | 5 ++ 10 files changed, 138 insertions(+), 4 deletions(-) create mode 100644 core/src/main/java/bisq/core/payment/AtomicAccount.java create mode 100644 core/src/main/java/bisq/core/payment/payload/AtomicAccountPayload.java diff --git a/core/src/main/java/bisq/core/payment/AtomicAccount.java b/core/src/main/java/bisq/core/payment/AtomicAccount.java new file mode 100644 index 00000000000..f2ab7ff6484 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/AtomicAccount.java @@ -0,0 +1,37 @@ +/* + * 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 . + */ + +package bisq.core.payment; + +import bisq.core.payment.payload.AtomicAccountPayload; +import bisq.core.payment.payload.PaymentAccountPayload; +import bisq.core.payment.payload.PaymentMethod; + +import lombok.EqualsAndHashCode; + +@EqualsAndHashCode(callSuper = true) +public final class AtomicAccount extends PaymentAccount { + + public AtomicAccount() { + super(PaymentMethod.ATOMIC); + } + + @Override + protected PaymentAccountPayload createPayload() { + return new AtomicAccountPayload(paymentMethod.getId(), id); + } +} diff --git a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java index e858e01e3b0..03c88e0ef46 100644 --- a/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java +++ b/core/src/main/java/bisq/core/payment/PaymentAccountFactory.java @@ -78,6 +78,8 @@ public static PaymentAccount getPaymentAccount(PaymentMethod paymentMethod) { return new AdvancedCashAccount(); case PaymentMethod.BLOCK_CHAINS_INSTANT_ID: return new InstantCryptoCurrencyAccount(); + case PaymentMethod.ATOMIC_ID: + return new AtomicAccount(); // Cannot be deleted as it would break old trade history entries case PaymentMethod.OK_PAY_ID: diff --git a/core/src/main/java/bisq/core/payment/payload/AtomicAccountPayload.java b/core/src/main/java/bisq/core/payment/payload/AtomicAccountPayload.java new file mode 100644 index 00000000000..0f9b6cca035 --- /dev/null +++ b/core/src/main/java/bisq/core/payment/payload/AtomicAccountPayload.java @@ -0,0 +1,57 @@ +/* + * 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 . + */ + +package bisq.core.payment.payload; + +import com.google.protobuf.Message; + +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import lombok.extern.slf4j.Slf4j; + +@EqualsAndHashCode(callSuper = true) +@ToString +@Setter +@Getter +@Slf4j +public final class AtomicAccountPayload extends AssetsAccountPayload { + + public AtomicAccountPayload(String paymentMethod, String id) { + super(paymentMethod, id); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public Message toProtoMessage() { + return getPaymentAccountPayloadBuilder() + .setAtomicAccountPayload(protobuf.AtomicAccountPayload.newBuilder() + .setGenericString("") + ) + .build(); + } + + public static AtomicAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { + return new AtomicAccountPayload(proto.getPaymentMethodId(), + proto.getId()); + } +} diff --git a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java index eecbb0f730d..17aff4beb8e 100644 --- a/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java +++ b/core/src/main/java/bisq/core/payment/payload/PaymentMethod.java @@ -92,6 +92,7 @@ public final class PaymentMethod implements PersistablePayload, Comparable tradeCurrencies) { return tradeCurrencies.stream() .anyMatch(tradeCurrency -> hasChargebackRisk(paymentMethod, tradeCurrency.getCode())); diff --git a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java index 999f4e8e5f1..9adcc48c3d5 100644 --- a/core/src/main/java/bisq/core/proto/CoreProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/CoreProtoResolver.java @@ -23,6 +23,7 @@ import bisq.core.dao.governance.proposal.storage.appendonly.ProposalPayload; import bisq.core.payment.payload.AdvancedCashAccountPayload; import bisq.core.payment.payload.AliPayAccountPayload; +import bisq.core.payment.payload.AtomicAccountPayload; import bisq.core.payment.payload.CashAppAccountPayload; import bisq.core.payment.payload.CashDepositAccountPayload; import bisq.core.payment.payload.ChaseQuickPayAccountPayload; @@ -146,6 +147,8 @@ public PaymentAccountPayload fromProto(protobuf.PaymentAccountPayload proto) { return AdvancedCashAccountPayload.fromProto(proto); case INSTANT_CRYPTO_CURRENCY_ACCOUNT_PAYLOAD: return InstantCryptoCurrencyPayload.fromProto(proto); + case ATOMIC_ACCOUNT_PAYLOAD: + return AtomicAccountPayload.fromProto(proto); // Cannot be deleted as it would break old trade history entries case O_K_PAY_ACCOUNT_PAYLOAD: diff --git a/core/src/main/java/bisq/core/user/User.java b/core/src/main/java/bisq/core/user/User.java index 75891395d22..f41d7e3530f 100644 --- a/core/src/main/java/bisq/core/user/User.java +++ b/core/src/main/java/bisq/core/user/User.java @@ -19,10 +19,13 @@ import bisq.core.alert.Alert; import bisq.core.filter.Filter; +import bisq.core.locale.CryptoCurrency; import bisq.core.locale.LanguageUtil; +import bisq.core.locale.Res; import bisq.core.locale.TradeCurrency; import bisq.core.notifications.alerts.market.MarketAlertFilter; import bisq.core.notifications.alerts.price.PriceAlertFilter; +import bisq.core.payment.AtomicAccount; import bisq.core.payment.PaymentAccount; import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator; import bisq.core.support.dispute.mediation.mediator.Mediator; @@ -114,6 +117,21 @@ public void readPersisted() { userPayload.setCurrentPaymentAccount(currentPaymentAccountProperty.get()); persist(); }); + + addAtomicAccount(); + } + + private void addAtomicAccount() { + checkNotNull(userPayload.getPaymentAccounts(), "userPayload.getPaymentAccounts() must not be null"); + if (userPayload.getPaymentAccounts().stream() + .anyMatch(paymentAccount -> paymentAccount instanceof AtomicAccount)) + return; + + var account = new AtomicAccount(); + account.init(); + account.setAccountName(Res.get("ATOMIC")); + account.setSingleTradeCurrency(new CryptoCurrency("BSQ", "BSQ")); + addPaymentAccount(account); } public void persist() { diff --git a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java index 2cd3dc1ff3d..76580f43e5c 100644 --- a/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/account/content/fiataccounts/FiatAccountsDataModel.java @@ -88,7 +88,7 @@ protected void activate() { private void fillAndSortPaymentAccounts() { if (user.getPaymentAccounts() != null) { List list = user.getPaymentAccounts().stream() - .filter(paymentAccount -> !paymentAccount.getPaymentMethod().isAsset()) + .filter(paymentAccount -> paymentAccount.getPaymentMethod().isFiat()) .collect(Collectors.toList()); paymentAccounts.setAll(list); paymentAccounts.sort(Comparator.comparing(PaymentAccount::getCreationDate)); diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java index 79183a31ba1..98e6260bfc8 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/windows/SignPaymentAccountsWindow.java @@ -164,7 +164,7 @@ public PaymentMethod fromString(String s) { private List getPaymentMethods() { return PaymentMethod.getPaymentMethods().stream() - .filter(paymentMethod -> !paymentMethod.isAsset()) + .filter(PaymentMethod::isFiat) .filter(PaymentMethod::hasChargebackRisk) .collect(Collectors.toList()); } diff --git a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java index 04ae97f7b39..e0bf3f7eb26 100644 --- a/desktop/src/main/java/bisq/desktop/util/GUIUtil.java +++ b/desktop/src/main/java/bisq/desktop/util/GUIUtil.java @@ -542,7 +542,7 @@ protected void updateItem(PaymentMethod method, boolean empty) { HBox box = new HBox(); box.setSpacing(20); Label paymentType = new AutoTooltipLabel( - method.isAsset() ? Res.get("shared.crypto") : Res.get("shared.fiat")); + method.isAsset() || method.isAtomic() ? Res.get("shared.crypto") : Res.get("shared.fiat")); paymentType.getStyleClass().add("currency-label-small"); Label paymentMethod = new AutoTooltipLabel(Res.get(id)); diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 117dbb4cb53..5d3b8e85ad9 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -911,6 +911,7 @@ message PaymentAccountPayload { AdvancedCashAccountPayload advanced_cash_account_payload = 26; InstantCryptoCurrencyAccountPayload instant_crypto_currency_account_payload = 27; JapanBankAccountPayload japan_bank_account_payload = 28; + AtomicAccountPayload atomic_account_payload = 29; } map exclude_from_json_data = 15; } @@ -1036,6 +1037,10 @@ message InstantCryptoCurrencyAccountPayload { string address = 1; } +message AtomicAccountPayload { + string generic_string = 1; +} + message FasterPaymentsAccountPayload { string sort_code = 1; string account_nr = 2; From 2269f1c172676fb86f7bc7223db9110c98fe8c59 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:29:54 +0200 Subject: [PATCH 03/12] Add atomic trade protocol messages --- .../network/CoreNetworkProtoResolver.java | 6 + .../trade/messages/CreateAtomicTxRequest.java | 194 ++++++++++++++++++ .../messages/CreateAtomicTxResponse.java | 89 ++++++++ proto/src/main/proto/pb.proto | 32 +++ 4 files changed, 321 insertions(+) create mode 100644 core/src/main/java/bisq/core/trade/messages/CreateAtomicTxRequest.java create mode 100644 core/src/main/java/bisq/core/trade/messages/CreateAtomicTxResponse.java diff --git a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java index 8d57a75c14b..f249de2f8cd 100644 --- a/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/network/CoreNetworkProtoResolver.java @@ -47,6 +47,8 @@ import bisq.core.support.dispute.refund.refundagent.RefundAgent; import bisq.core.support.messages.ChatMessage; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.CreateAtomicTxRequest; +import bisq.core.trade.messages.CreateAtomicTxResponse; import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; @@ -158,6 +160,10 @@ public NetworkEnvelope fromProto(protobuf.NetworkEnvelope proto) throws Protobuf return DelayedPayoutTxSignatureResponse.fromProto(proto.getDelayedPayoutTxSignatureResponse(), messageVersion); case DEPOSIT_TX_AND_DELAYED_PAYOUT_TX_MESSAGE: return DepositTxAndDelayedPayoutTxMessage.fromProto(proto.getDepositTxAndDelayedPayoutTxMessage(), messageVersion); + case CREATE_ATOMIC_TX_REQUEST: + return CreateAtomicTxRequest.fromProto(proto.getCreateAtomicTxRequest(), messageVersion); + case CREATE_ATOMIC_TX_RESPONSE: + return CreateAtomicTxResponse.fromProto(proto.getCreateAtomicTxResponse(), messageVersion); case COUNTER_CURRENCY_TRANSFER_STARTED_MESSAGE: return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), messageVersion); diff --git a/core/src/main/java/bisq/core/trade/messages/CreateAtomicTxRequest.java b/core/src/main/java/bisq/core/trade/messages/CreateAtomicTxRequest.java new file mode 100644 index 00000000000..b122fb69d7d --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/CreateAtomicTxRequest.java @@ -0,0 +1,194 @@ +/* + * 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 . + */ + +package bisq.core.trade.messages; + +import bisq.core.btc.model.RawTransactionInput; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.crypto.PubKeyRing; + +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class CreateAtomicTxRequest extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final PubKeyRing takerPubKeyRing; + private final long bsqTradeAmount; + private final long btcTradeAmount; + private final long tradePrice; + private final long txFee; + private final long takerFee; + private final boolean isCurrencyForTakerFeeBtc; + private final long takerBsqOutputValue; + private final String takerBsqOutputAddress; + private final long takerBtcOutputValue; + private final String takerBtcOutputAddress; + private final List takerBsqInputs; + private final List takerBtcInputs; + + public CreateAtomicTxRequest(String uid, + String tradeId, + NodeAddress senderNodeAddress, + PubKeyRing takerPubKeyRing, + long bsqTradeAmount, + long btcTradeAmount, + long tradePrice, + long txFee, + long takerFee, + boolean isCurrencyForTakerFeeBtc, + long takerBsqOutputValue, + String takerBsqOutputAddress, + long takerBtcOutputValue, + String takerBtcOutputAddress, + List takerBsqInputs, + List takerBtcInputs) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + takerPubKeyRing, + bsqTradeAmount, + btcTradeAmount, + tradePrice, + txFee, + takerFee, + isCurrencyForTakerFeeBtc, + takerBsqOutputValue, + takerBsqOutputAddress, + takerBtcOutputValue, + takerBtcOutputAddress, + takerBsqInputs, + takerBtcInputs); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CreateAtomicTxRequest(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + PubKeyRing takerPubKeyRing, + long bsqTradeAmount, + long btcTradeAmount, + long tradePrice, + long txFee, + long takerFee, + boolean isCurrencyForTakerFeeBtc, + long takerBsqOutputValue, + String takerBsqOutputAddress, + long takerBtcOutputValue, + String takerBtcOutputAddress, + List takerBsqInputs, + List takerBtcInputs) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.takerPubKeyRing = takerPubKeyRing; + this.bsqTradeAmount = bsqTradeAmount; + this.btcTradeAmount = btcTradeAmount; + this.tradePrice = tradePrice; + this.txFee = txFee; + this.takerFee = takerFee; + this.isCurrencyForTakerFeeBtc = isCurrencyForTakerFeeBtc; + this.takerBsqOutputValue = takerBsqOutputValue; + this.takerBsqOutputAddress = takerBsqOutputAddress; + this.takerBtcOutputValue = takerBtcOutputValue; + this.takerBtcOutputAddress = takerBtcOutputAddress; + this.takerBsqInputs = takerBsqInputs; + this.takerBtcInputs = takerBtcInputs; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setCreateAtomicTxRequest(protobuf.CreateAtomicTxRequest.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setTakerPubKeyRing(takerPubKeyRing.toProtoMessage()) + .setBsqTradeAmount(bsqTradeAmount) + .setBtcTradeAmount(btcTradeAmount) + .setTradePrice(tradePrice) + .setTxFee(txFee) + .setTakerFee(takerFee) + .setIsCurrencyForTakerFeeBtc(isCurrencyForTakerFeeBtc) + .setTakerBsqOutputValue(takerBsqOutputValue) + .setTakerBsqOutputAddress(takerBsqOutputAddress) + .setTakerBtcOutputValue(takerBtcOutputValue) + .setTakerBtcOutputAddress(takerBtcOutputAddress) + .addAllTakerBsqInputs(takerBsqInputs.stream().map(RawTransactionInput::toProtoMessage).collect( + Collectors.toList())) + .addAllTakerBtcInputs(takerBtcInputs.stream().map(RawTransactionInput::toProtoMessage).collect( + Collectors.toList())) + ).build(); + } + + public static CreateAtomicTxRequest fromProto(protobuf.CreateAtomicTxRequest proto, int messageVersion) { + return new CreateAtomicTxRequest(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + PubKeyRing.fromProto(proto.getTakerPubKeyRing()), + proto.getBsqTradeAmount(), + proto.getBtcTradeAmount(), + proto.getTradePrice(), + proto.getTxFee(), + proto.getTakerFee(), + proto.getIsCurrencyForTakerFeeBtc(), + proto.getTakerBsqOutputValue(), + proto.getTakerBsqOutputAddress(), + proto.getTakerBtcOutputValue(), + proto.getTakerBtcOutputAddress(), + proto.getTakerBsqInputsList().stream() + .map(RawTransactionInput::fromProto) + .collect(Collectors.toList()), + proto.getTakerBtcInputsList().stream() + .map(RawTransactionInput::fromProto) + .collect(Collectors.toList()) + ); + } + + @Override + public String toString() { + return "CreateAtomicTxRequest{" + + "\n senderNodeAddress=" + senderNodeAddress + + "\n takerPubKeyRing=" + takerPubKeyRing + + "\n bsqTradeAmount=" + bsqTradeAmount + + "\n btcTradeAmount=" + btcTradeAmount + + "\n tradePrice=" + tradePrice + + "\n txFee=" + txFee + + "\n takerFee=" + takerFee + + "\n isCurrencyForTakerFeeBtc=" + isCurrencyForTakerFeeBtc + + "\n takerBsqOutputValue=" + takerBsqOutputValue + + "\n takerBsqOutputAddress=" + takerBsqOutputAddress + + "\n takerBtcOutputValue=" + takerBtcOutputValue + + "\n takerBtcOutputAddress=" + takerBtcOutputAddress + + "\n takerBsqInputs=" + takerBsqInputs + + "\n takerBtcInputs=" + takerBtcInputs + + "\n} " + super.toString(); + } +} diff --git a/core/src/main/java/bisq/core/trade/messages/CreateAtomicTxResponse.java b/core/src/main/java/bisq/core/trade/messages/CreateAtomicTxResponse.java new file mode 100644 index 00000000000..b67c09c0c8c --- /dev/null +++ b/core/src/main/java/bisq/core/trade/messages/CreateAtomicTxResponse.java @@ -0,0 +1,89 @@ +/* + * 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 . + */ + +package bisq.core.trade.messages; + +import bisq.network.p2p.DirectMessage; +import bisq.network.p2p.NodeAddress; + +import bisq.common.app.Version; +import bisq.common.util.Utilities; + +import com.google.protobuf.ByteString; + +import lombok.EqualsAndHashCode; +import lombok.Value; + +@EqualsAndHashCode(callSuper = true) +@Value +public final class CreateAtomicTxResponse extends TradeMessage implements DirectMessage { + private final NodeAddress senderNodeAddress; + private final byte[] atomicTx; + + public CreateAtomicTxResponse(String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] atomicTx) { + this(Version.getP2PMessageVersion(), + uid, + tradeId, + senderNodeAddress, + atomicTx); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + private CreateAtomicTxResponse(int messageVersion, + String uid, + String tradeId, + NodeAddress senderNodeAddress, + byte[] atomicTx) { + super(messageVersion, tradeId, uid); + this.senderNodeAddress = senderNodeAddress; + this.atomicTx = atomicTx; + } + + @Override + public protobuf.NetworkEnvelope toProtoNetworkEnvelope() { + return getNetworkEnvelopeBuilder() + .setCreateAtomicTxResponse(protobuf.CreateAtomicTxResponse.newBuilder() + .setUid(uid) + .setTradeId(tradeId) + .setSenderNodeAddress(senderNodeAddress.toProtoMessage()) + .setAtomicTx(ByteString.copyFrom(atomicTx)) + ).build(); + } + + public static CreateAtomicTxResponse fromProto(protobuf.CreateAtomicTxResponse proto, int messageVersion) { + return new CreateAtomicTxResponse(messageVersion, + proto.getUid(), + proto.getTradeId(), + NodeAddress.fromProto(proto.getSenderNodeAddress()), + proto.getAtomicTx().toByteArray() + ); + } + + @Override + public String toString() { + return "CreateAtomicTxResponse{" + + "\n senderNodeAddress=" + senderNodeAddress + + "\n depositTx=" + Utilities.bytesAsHexString(atomicTx) + + "\n} " + super.toString(); + } +} diff --git a/proto/src/main/proto/pb.proto b/proto/src/main/proto/pb.proto index 5d3b8e85ad9..06d774c4f38 100644 --- a/proto/src/main/proto/pb.proto +++ b/proto/src/main/proto/pb.proto @@ -78,6 +78,9 @@ message NetworkEnvelope { RefreshTradeStateRequest refresh_trade_state_request = 50; TraderSignedWitnessMessage trader_signed_witness_message = 51; + + CreateAtomicTxRequest create_atomic_tx_request = 52; + CreateAtomicTxResponse create_atomic_tx_response = 53; } } @@ -337,6 +340,34 @@ message TraderSignedWitnessMessage { SignedWitness signed_witness = 4; } +// Atomic trade + +message CreateAtomicTxRequest { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + PubKeyRing taker_pub_key_ring = 4; + int64 bsq_trade_amount = 5; + int64 btc_trade_amount = 6; + int64 trade_price = 7; + int64 tx_fee = 8; + int64 taker_fee = 9; + bool is_currency_for_taker_fee_btc = 10; + int64 taker_bsq_output_value = 11; + string taker_bsq_output_address = 12; + int64 taker_btc_output_value = 13; + string taker_btc_output_address = 14; + repeated RawTransactionInput taker_bsq_inputs = 15; + repeated RawTransactionInput taker_btc_inputs = 16; +} + +message CreateAtomicTxResponse { + string uid = 1; + string trade_id = 2; + NodeAddress sender_node_address = 3; + bytes atomic_tx = 4; +} + // dispute enum SupportType { @@ -1390,6 +1421,7 @@ message Trade { PubKeyRing refund_agent_pub_key_ring = 34; RefundResultState refund_result_state = 35; int64 last_refresh_request_date = 36; + string atomic_tx_id = 37; } message BuyerAsMakerTrade { From e4181e51741f4d15b19e8674583874bc51516c70 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:34:43 +0200 Subject: [PATCH 04/12] Add atomic tx fee estimation --- .../java/bisq/core/btc/TxFeeEstimationService.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java index c0fbfb5b682..9fcb3948e47 100644 --- a/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java +++ b/core/src/main/java/bisq/core/btc/TxFeeEstimationService.java @@ -44,7 +44,10 @@ public class TxFeeEstimationService { public static int TYPICAL_TX_WITH_1_INPUT_SIZE = 260; private static int DEPOSIT_TX_SIZE = 320; private static int PAYOUT_TX_SIZE = 380; + private static int ATOMIC_TX_SIZE = 975; private static int BSQ_INPUT_INCREASE = 150; + private static int EXTRA_INPUT_INCREASE = 225; + private static int EXTRA_OUTPUT_INCREASE = 70; private static int MAX_ITERATIONS = 10; private final FeeService feeService; @@ -80,6 +83,15 @@ public Tuple2 getEstimatedFeeAndTxSizeForMaker(Coin reservedFunds preferences); } + public Tuple2 getEstimatedAtomicFeeAndTxSize(int estimatedInputs, int estimatedOutputs) { + var txFeePerByte = feeService.getTxFeePerByte(); + var extraInputSize = estimatedInputs > 3 ? (estimatedInputs - 3) * EXTRA_INPUT_INCREASE : 0; + var extraOutputSize = estimatedOutputs > 4 ? (estimatedOutputs - 4) * EXTRA_OUTPUT_INCREASE : 0; + var estimatedSize = ATOMIC_TX_SIZE + extraInputSize + extraOutputSize; + var txFee = txFeePerByte.multiply(estimatedSize); + return new Tuple2<>(txFee, estimatedSize); + } + private Tuple2 getEstimatedFeeAndTxSize(boolean isTaker, Coin amount, Coin tradeFee, From b0685cd4c9dcd565ceddda06dfc5ac0b2b467221 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:37:56 +0200 Subject: [PATCH 05/12] Add BSQ specific input signing Refactor signTx and allow for signing of only some inputs --- .../core/btc/wallet/BsqWalletService.java | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index 930a19e2701..586c5db37f4 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -98,6 +98,7 @@ public interface WalletTransactionsChangeListener { private final List walletTransactionsChangeListeners = new ArrayList<>(); private boolean updateBsqWalletTransactionsPending; + // balance of non BSQ satoshis @Getter private Coin availableNonBsqBalance = Coin.ZERO; @@ -485,21 +486,11 @@ public Optional isWalletTransaction(String txId) { public Transaction signTx(Transaction tx) throws WalletException, TransactionVerificationException { for (int i = 0; i < tx.getInputs().size(); i++) { - TransactionInput txIn = tx.getInputs().get(i); - TransactionOutput connectedOutput = txIn.getConnectedOutput(); - if (connectedOutput != null && connectedOutput.isMine(wallet)) { - signTransactionInput(wallet, aesKey, tx, txIn, i); - checkScriptSig(tx, txIn, i); - } + signInput(tx, i); } for (TransactionOutput txo : tx.getOutputs()) { - Coin value = txo.getValue(); - // OpReturn outputs have value 0 - if (value.isPositive()) { - checkArgument(Restrictions.isAboveDust(txo.getValue()), - "An output value is below dust limit. Transaction=" + tx); - } + verifyNonDustTxo(tx, txo); } checkWalletConsistency(wallet); @@ -508,6 +499,41 @@ public Transaction signTx(Transaction tx) throws WalletException, TransactionVer return tx; } + public Transaction signInputs(Transaction tx, List transactionInputs) + throws WalletException, TransactionVerificationException { + for (int i = 0; i < tx.getInputs().size(); i++) { + if (transactionInputs.contains(tx.getInput(i))) + signInput(tx, i); + } + + for (TransactionOutput txo : tx.getOutputs()) { + verifyNonDustTxo(tx, txo); + } + + checkWalletConsistency(wallet); + verifyTransaction(tx); + printTx("BSQ wallet: Signed Tx", tx); + return tx; + } + + private void signInput(Transaction tx, int i) throws TransactionVerificationException{ + TransactionInput txIn = tx.getInputs().get(i); + TransactionOutput connectedOutput = txIn.getConnectedOutput(); + if (connectedOutput != null && connectedOutput.isMine(wallet)) { + signTransactionInput(wallet, aesKey, tx, txIn, i); + checkScriptSig(tx, txIn, i); + } + } + + private void verifyNonDustTxo(Transaction tx, TransactionOutput txo) { + Coin value = txo.getValue(); + // OpReturn outputs have value 0 + if (value.isPositive()) { + checkArgument(Restrictions.isAboveDust(txo.getValue()), + "An output value is below dust limit. Transaction=" + tx); + } + } + /////////////////////////////////////////////////////////////////////////////////////////// // Commit tx From c7170d6edbc68d0d75a7f0107729dfcdc97c50bd Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:39:54 +0200 Subject: [PATCH 06/12] Add walletservice helpers for atomic trade protocol --- .../core/btc/wallet/BsqWalletService.java | 78 +++++++- .../core/btc/wallet/BtcWalletService.java | 64 ++++++- .../core/btc/wallet/TradeWalletService.java | 175 +++++++++++++++++- .../bisq/core/btc/wallet/WalletService.java | 4 + 4 files changed, 310 insertions(+), 11 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index 586c5db37f4..be153bddeb3 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -22,10 +22,13 @@ import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.model.RawTransactionInput; import bisq.core.btc.setup.WalletsSetup; +import bisq.core.dao.DaoFacade; import bisq.core.dao.DaoKillSwitch; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTxOutput; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.Tx; import bisq.core.dao.state.model.blockchain.TxOutput; @@ -34,8 +37,10 @@ import bisq.core.dao.state.unconfirmed.UnconfirmedBsqChangeOutputListService; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; +import bisq.core.util.coin.BsqFormatter; import bisq.common.UserThread; +import bisq.common.util.Tuple2; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; @@ -97,6 +102,8 @@ public interface WalletTransactionsChangeListener { private final CopyOnWriteArraySet bsqBalanceListeners = new CopyOnWriteArraySet<>(); private final List walletTransactionsChangeListeners = new ArrayList<>(); private boolean updateBsqWalletTransactionsPending; + @Getter + private final BsqFormatter bsqFormatter; // balance of non BSQ satoshis @@ -128,7 +135,8 @@ public BsqWalletService(WalletsSetup walletsSetup, UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService, Preferences preferences, FeeService feeService, - DaoKillSwitch daoKillSwitch) { + DaoKillSwitch daoKillSwitch, + BsqFormatter bsqFormatter) { super(walletsSetup, preferences, feeService); @@ -138,6 +146,7 @@ public BsqWalletService(WalletsSetup walletsSetup, this.daoStateService = daoStateService; this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService; this.daoKillSwitch = daoKillSwitch; + this.bsqFormatter = bsqFormatter; walletsSetup.addSetupCompletedHandler(() -> { wallet = walletsSetup.getBsqWallet(); @@ -736,6 +745,31 @@ private void addInputsAndChangeOutputForTx(Transaction tx, } + /////////////////////////////////////////////////////////////////////////////////////////// + // Atomic trade tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Tuple2 prepareAtomicBsqInputs(Coin requiredInput) throws InsufficientBsqException { + daoKillSwitch.assertDaoIsNotDisabled(); + + var dummyTx = new Transaction(params); + var coinSelection = bsqCoinSelector.select(requiredInput, wallet.calculateAllSpendCandidates()); + coinSelection.gathered.forEach(dummyTx::addInput); + + var change = Coin.ZERO; + try { + change = bsqCoinSelector.getChange(requiredInput, coinSelection); + } catch (InsufficientMoneyException e) { + log.error("Missing funds in takerPreparesAtomicBsqInputs"); + throw new InsufficientBsqException(e.missing); + } + checkArgument(Restrictions.isAboveDust(change)); + +// printTx("takerPreparesAtomicBsqInputs", dummyTx); + return new Tuple2<>(dummyTx, change); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Blind vote tx /////////////////////////////////////////////////////////////////////////////////////////// @@ -837,4 +871,46 @@ public String getUnusedBsqAddressAsString() { protected boolean isDustAttackUtxo(TransactionOutput output) { return false; } + + public long getBsqRawInputAmount(List inputs, DaoFacade daoFacade) { + return inputs.stream() + .map(rawInput -> { + var tx = getTxFromSerializedTx(rawInput.parentTransaction); + return daoFacade.getUnspentTxOutputs().stream() + .filter(output -> output.getTxId().equals(tx.getHashAsString())) + .filter(output -> output.getIndex() == rawInput.index) + .map(BaseTxOutput::getValue) + .findAny() + .orElse(0L); + }).reduce(Long::sum) + .orElse(0L); + } + + public long getBsqInputAmount(List inputs, DaoFacade daoFacade) { + return inputs.stream() + .map(input -> { + var txId = input.getOutpoint().getHash().toString(); + return daoFacade.getUnspentTxOutputs().stream() + .filter(output -> output.getTxId().equals(txId)) + .filter(output -> output.getIndex() == input.getOutpoint().getIndex()) + .map(BaseTxOutput::getValue) + .findAny() + .orElse(0L); + }) + .reduce(Long::sum) + .orElse(0L); + } + + public TransactionInput verifyTransactionInput(TransactionInput input, RawTransactionInput rawTransactionInput) { + var connectedOutputTx = getTransaction(input.getOutpoint().getHash()); + checkNotNull(connectedOutputTx); + var outPoint1 = input.getOutpoint(); + var outPoint2 = new TransactionOutPoint(params, + rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)); + if (!outPoint1.equals(outPoint2)) + return null; + var dummyTx = new Transaction(params); + dummyTx.addInput(connectedOutputTx.getOutput(input.getOutpoint().getIndex())); + return dummyTx.getInput(0); + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index 3bb4721b17a..3ba0c65c28f 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -23,6 +23,7 @@ import bisq.core.btc.exceptions.WalletException; import bisq.core.btc.model.AddressEntry; import bisq.core.btc.model.AddressEntryList; +import bisq.core.btc.model.RawTransactionInput; import bisq.core.btc.setup.WalletsSetup; import bisq.core.provider.fee.FeeService; import bisq.core.user.Preferences; @@ -55,6 +56,7 @@ import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -144,6 +146,21 @@ String getWalletAsString(boolean includePrivKeys) { // Public Methods /////////////////////////////////////////////////////////////////////////////////////////// + public Transaction signTx(Transaction tx) throws WalletException, TransactionVerificationException { + for (int i = 0; i < tx.getInputs().size(); i++) { + var input = tx.getInput(i); + if (input.getConnectedOutput() != null && input.getConnectedOutput().isMine(wallet)) { + signTransactionInput(wallet, aesKey, tx, input, i); + checkScriptSig(tx, input, i); + } + } + + checkWalletConsistency(wallet); + verifyTransaction(tx); + printTx("BTC wallet: Signed Tx", tx); + return tx; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Burn BSQ txs (some proposal txs, asset listing fee tx, proof of burn tx) /////////////////////////////////////////////////////////////////////////////////////////// @@ -157,12 +174,18 @@ public Transaction completePreparedBurnBsqTx(Transaction preparedBurnFeeTx, byte // Proposal txs /////////////////////////////////////////////////////////////////////////////////////////// - public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData) + public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, + Address issuanceAddress, + Transaction feeTx, + byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); } - public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData) + public Transaction completePreparedCompensationRequestTx(Coin issuanceAmount, + Address issuanceAddress, + Transaction feeTx, + byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); } @@ -292,7 +315,8 @@ public Transaction completePreparedBlindVoteTx(Transaction preparedTx, byte[] op return completePreparedBsqTxWithBtcFee(preparedTx, opReturnData); } - private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException, TransactionVerificationException, WalletException { + private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, byte[] opReturnData) + throws InsufficientMoneyException, TransactionVerificationException, WalletException { // Remember index for first BTC input int indexOfBtcFirstInput = preparedTx.getInputs().size(); @@ -306,7 +330,8 @@ private Transaction completePreparedBsqTxWithBtcFee(Transaction preparedTx, byte return tx; } - private Transaction addInputsForMinerFee(Transaction preparedTx, byte[] opReturnData) throws InsufficientMoneyException { + private Transaction addInputsForMinerFee(Transaction preparedTx, byte[] opReturnData) + throws InsufficientMoneyException { // safety check counter to avoid endless loops int counter = 0; // estimated size of input sig @@ -376,7 +401,6 @@ private void signAllBtcInputs(int indexOfBtcFirstInput, Transaction tx) throws T } } - /////////////////////////////////////////////////////////////////////////////////////////// // Vote reveal tx /////////////////////////////////////////////////////////////////////////////////////////// @@ -421,8 +445,10 @@ public Transaction completePreparedSendBsqTx(Transaction preparedBsqTx, boolean return completePreparedBsqTx(preparedBsqTx, isSendTx, null); } - public Transaction completePreparedBsqTx(Transaction preparedBsqTx, boolean useCustomTxFee, @Nullable byte[] opReturnData) throws - TransactionVerificationException, WalletException, InsufficientMoneyException { + public Transaction completePreparedBsqTx(Transaction preparedBsqTx, + boolean useCustomTxFee, + @Nullable byte[] opReturnData) + throws TransactionVerificationException, WalletException, InsufficientMoneyException { // preparedBsqTx has following structure: // inputs [1-n] BSQ inputs @@ -545,7 +571,8 @@ public void commitTx(Transaction tx) { // AddressEntry /////////////////////////////////////////////////////////////////////////////////////////// - public Optional getAddressEntry(String offerId, @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { + public Optional getAddressEntry(String offerId, + @SuppressWarnings("SameParameterValue") AddressEntry.Context context) { return getAddressEntryListAsImmutableList().stream() .filter(e -> offerId.equals(e.getOfferId())) .filter(e -> context == e.getContext()) @@ -1175,4 +1202,25 @@ public Transaction createRefundPayoutTx(Coin buyerAmount, return resultTx; } + + // There is currently no way to verify that this tx hasn't been spent already + // If it has, the atomic tx won't confirm, not good but no funds lost + public long getBtcRawInputAmount(List inputs) { + return inputs.stream() + .map(rawInput -> { + var tx = getTxFromSerializedTx(rawInput.parentTransaction); + return tx.getOutput(rawInput.index).getValue().getValue(); + }) + .reduce(Long::sum) + .orElse(0L); + } + + // There is currently no way to verify that this tx hasn't been spent already + // If it has, the atomic tx won't confirm, not good but no funds lost + public long getBtcInputAmount(List inputs) { + return inputs.stream() + .map(input -> Objects.requireNonNull(input.getValue()).getValue()) + .reduce(Long::sum) + .orElse(0L); + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java index 07671fe6871..88c86a39b57 100644 --- a/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/TradeWalletService.java @@ -17,6 +17,7 @@ package bisq.core.btc.wallet; +import bisq.core.btc.TxFeeEstimationService; import bisq.core.btc.exceptions.SigningException; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.WalletException; @@ -30,6 +31,7 @@ import bisq.core.user.Preferences; import bisq.common.config.Config; +import bisq.common.util.Tuple4; import org.bitcoinj.core.Address; import org.bitcoinj.core.AddressFormatException; @@ -58,7 +60,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.stream.Collectors; import org.slf4j.Logger; @@ -77,6 +78,7 @@ public class TradeWalletService { private final WalletsSetup walletsSetup; private final Preferences preferences; private final NetworkParameters params; + private final TxFeeEstimationService txFeeEstimationService; @Nullable private Wallet wallet; @@ -91,9 +93,12 @@ public class TradeWalletService { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public TradeWalletService(WalletsSetup walletsSetup, Preferences preferences) { + public TradeWalletService(WalletsSetup walletsSetup, + Preferences preferences, + TxFeeEstimationService txFeeEstimationService) { this.walletsSetup = walletsSetup; this.preferences = preferences; + this.txFeeEstimationService = txFeeEstimationService; this.params = Config.baseCurrencyNetworkParameters(); walletsSetup.addSetupCompletedHandler(() -> { walletConfig = walletsSetup.getWalletConfig(); @@ -1031,6 +1036,149 @@ public void emergencySignAndPublishPayoutTxFrom2of2MultiSig(String depositTxHex, } + /////////////////////////////////////////////////////////////////////////////////////////// + // Atomic tx + /////////////////////////////////////////////////////////////////////////////////////////// + + /** + * Example of colored coin offline scheme (not used yet): + * https://groups.google.com/forum/#!msg/bitcoinx/pON4XCIBeV4/IvzwkU8Vch0J + * + * Add BTC (for txFee) and BSQ (for trade value and trade fee) inputs from taker, create BSQ change output, + * BTC output (BTC bought + BTC change) + * + * @param preparedAtomicBsqTx Tx with BSQ inputs covering the taker's part of the atomic trade + * @param estimatedTxFee estimated tx fee, a default to guide initial coin selection + * @param btcTradeFee BTC input amount of buyer, excluding txFee which will be calculated here + * @param bsqTradeFee amount of BSQ burnt as trade fee + * @return Tuple of raw inputs, tx fee, btcChange + * @throws AddressFormatException if the taker base58 change address doesn't parse or its checksum is invalid + */ + public Tuple4, ArrayList, Coin, Coin> takerPreparesAtomicTx( + Transaction preparedAtomicBsqTx, + Coin requiredBtc, + Coin estimatedTxFee, + Coin btcTradeFee, + Coin bsqTradeFee) + throws AddressFormatException, InsufficientMoneyException { + + // Gather bsq raw inputs + ArrayList takerRawBsqInputs = new ArrayList<>(); + preparedAtomicBsqTx.getInputs().forEach(input -> takerRawBsqInputs.add(getRawInputFromTransactionInput(input))); + + // Get BTC inputs needed to cover takerInputAmount + estimatedTxFee + var coinSelector = new BtcCoinSelector(walletsSetup.getAddressesByContext(AddressEntry.Context.AVAILABLE), + preferences.getIgnoreDustThreshold()); + checkNotNull(wallet, "Wallet must not be null"); + var estimatedCoinSelection = coinSelector.select(btcTradeFee.add(estimatedTxFee), + wallet.calculateAllSpendCandidates()); + // Estimate one input from maker, add taker BSQ inputs and taker BTC inputs + var numberOfInputs = 1 + preparedAtomicBsqTx.getInputs().size() + + estimatedCoinSelection.gathered.size(); + // Normal case is 4 outputs for a BSQ tradeFee tx and 5 outputs for a BTC tradeFee tx + var numberOfOutputs = 4 + (btcTradeFee.isZero() ? 0 : 1); + // Get final tx fee + var txFee = txFeeEstimationService.getEstimatedAtomicFeeAndTxSize(numberOfInputs, numberOfOutputs).first; + var valueNeededForTxFee = txFee.subtract(bsqTradeFee); + var requiredInput = requiredBtc.add(btcTradeFee).add(valueNeededForTxFee); + var coinSelection = coinSelector.select(requiredInput, wallet.calculateAllSpendCandidates()); + + // Add BTC inputs to dummy tx to gather raw inputs + Transaction dummyTx = new Transaction(params); + ArrayList takerRawBtcInputs = new ArrayList<>(); + coinSelection.gathered.forEach(output -> { + var newInput = dummyTx.addInput(output); + takerRawBtcInputs.add(getRawInputFromTransactionInput(newInput)); + }); + Coin btcChange; + try { + btcChange = coinSelector.getChange(requiredInput, coinSelection); + } catch (InsufficientMoneyException e) { + log.error("Missing funds in takerPrepareAtomicBsqInputs"); + throw e; + } + + return new Tuple4<>(takerRawBsqInputs, takerRawBtcInputs, txFee, btcChange); + } + + + /** + * Maker creates atomic transaction and signs their inputs. + * + * Tx output format: + * At a minimum there will be 1 BSQ out and 1 BTC out + * [0-1] (Maker BSQ) + * [0-1] (Taker BSQ) + * [0-1] (Taker BTC) + * [0-1] (Maker BTC) + * [0-1] (BTCtradeFee) + * + * @param makerBsqPayout BSQ out for maker + * @param makerBtcPayout BTC out for maker + * @param takerBsqPayout BSQ out for taker + * @param takerBtcPayout BTC out for taker + * @param btcTradeFee BTC trade fee + * @param makerBsqAddress BSQ address for maker + * @param makerBtcAddress BTC address for maker + * @param takerBsqAddress BSQ address for taker + * @param takerBtcAddress BTC address for taker + * @param btcTradeFeeAddress donation address + * @param makerBsqInputs BSQ inputs for maker + * @param makerBtcInputs BTC inputs for maker + * @param takerBsqInputs BSQ raw inputs for taker + * @param takerBtcInputs BTC raw inputs for taker + * + * @return the atomic transaction signed by maker + * @throws AddressFormatException if the buyer or seller base58 address doesn't parse or its checksum is invalid + * @throws SigningException if signing failed + */ + public Transaction makerCreatesAndSignsAtomicTx(Coin makerBsqPayout, + Coin makerBtcPayout, + Coin takerBsqPayout, + Coin takerBtcPayout, + Coin btcTradeFee, + String makerBsqAddress, + String makerBtcAddress, + String takerBsqAddress, + String takerBtcAddress, + String btcTradeFeeAddress, + List makerBsqInputs, + List makerBtcInputs, + List takerBsqInputs, + List takerBtcInputs) + throws AddressFormatException, SigningException { + + var atomicTx = new Transaction(params); + + // Add inputs in order, first taker BSQ, BTC inputs, then maker BSQ, BTC inputs + takerBsqInputs.forEach(rawInput -> atomicTx.addInput(getTransactionInput(atomicTx, new byte[]{}, rawInput))); + takerBtcInputs.forEach(rawInput -> atomicTx.addInput(getTransactionInput(atomicTx, new byte[]{}, rawInput))); + makerBsqInputs.forEach(atomicTx::addInput); + makerBtcInputs.forEach(atomicTx::addInput); + + // Add outputs in specific order to follow BSQ protocol + if (makerBsqPayout.isPositive()) + atomicTx.addOutput(makerBsqPayout, Address.fromBase58(params, makerBsqAddress)); + if (takerBsqPayout.isPositive()) + atomicTx.addOutput(takerBsqPayout, Address.fromBase58(params, takerBsqAddress)); + if (takerBtcPayout.isPositive()) + atomicTx.addOutput(takerBtcPayout, Address.fromBase58(params, takerBtcAddress)); + if (makerBtcPayout.isPositive()) + atomicTx.addOutput(makerBtcPayout, Address.fromBase58(params, makerBtcAddress)); + if (btcTradeFee.isPositive()) + atomicTx.addOutput(btcTradeFee, Address.fromBase58(params, btcTradeFeeAddress)); + + // Sign makerinputs + var makerBtcInputIndex = takerBsqInputs.size() + takerBtcInputs.size() + makerBsqInputs.size(); + for (int i = makerBtcInputIndex; i < atomicTx.getInputs().size(); i++) { + var input = atomicTx.getInput(i); + signInput(atomicTx, input, i); + } + + return atomicTx; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Broadcast tx /////////////////////////////////////////////////////////////////////////////////////////// @@ -1069,6 +1217,15 @@ public Transaction getClonedTransaction(Transaction tx) { return new Transaction(params, tx.bitcoinSerialize()); } + // The output from makerFeeTx that will be used for the trade is tracked by address + // Get the output but specifying address and amount to spend (normally tradeamount) + public List getInputsForAddress(Address address, Coin amount) throws WalletException { + Transaction dummyTx = new Transaction(params); + dummyTx.addOutput(new TransactionOutput(params, dummyTx, amount, new ECKey().toAddress(params))); + + addAvailableInputsAndChangeOutputs(dummyTx, address, address); + return dummyTx.getInputs(); + } /////////////////////////////////////////////////////////////////////////////////////////// // Private methods @@ -1241,4 +1398,18 @@ private boolean removeDust(Transaction transaction) { } return false; // no action necessary } + + public TransactionInput verifyTransactionInput(TransactionInput input, RawTransactionInput rawTransactionInput) { + checkNotNull(wallet); + var connectedOutputTx = wallet.getTransaction(input.getOutpoint().getHash()); + checkNotNull(connectedOutputTx); + var outPoint1 = input.getOutpoint(); + var outPoint2 = new TransactionOutPoint(params, + rawTransactionInput.index, new Transaction(params, rawTransactionInput.parentTransaction)); + if (!outPoint1.equals(outPoint2)) + return null; + var dummyTx = new Transaction(params); + dummyTx.addInput(connectedOutputTx.getOutput(input.getOutpoint().getIndex())); + return dummyTx.getInput(0); + } } diff --git a/core/src/main/java/bisq/core/btc/wallet/WalletService.java b/core/src/main/java/bisq/core/btc/wallet/WalletService.java index d56b34a7c95..510b296c4c2 100644 --- a/core/src/main/java/bisq/core/btc/wallet/WalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/WalletService.java @@ -479,6 +479,10 @@ public boolean isAddressUnused(Address address) { return getNumTxOutputsForAddress(address) == 0; } + public boolean isMine(TransactionOutput transactionOutput) { + return transactionOutput.isMine(wallet); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Empty complete Wallet From e77ddff2d21de08735a0637705a4e498b15ee1e6 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:53:36 +0200 Subject: [PATCH 07/12] Add atomic trade protocol Atomic trade protocol is completed in one request -> response interaction between taker and maker. 1. Taker sends a CreateAtomicTxRequest to maker. This includes all the data needed to create and sign the atomic tx from maker's side. 1. Maker verifies all inputs and answers with CreateAtomicTxResponse that includes an atomic tx with maker's inputs signed. 1. Taker verifies the inputs and that their own outputs are paid as expected, signs and publishes the completed atomic tx. 1. Fin There is only one TakerVerifies and one MakerVerifies task as the buy and sell side are very similar, the main difference lies in whether the actor is taker or maker. It's currently not possible to verify BTC inputs, but bad inputs (such as spent ones) would generate a tx that won't broadcast. No funds can be lost as the outputs are verified on both sides. --- .../bisq/core/trade/BuyerAsMakerTrade.java | 9 + .../main/java/bisq/core/trade/MakerTrade.java | 8 +- .../bisq/core/trade/SellerAsMakerTrade.java | 14 +- core/src/main/java/bisq/core/trade/Trade.java | 16 +- .../java/bisq/core/trade/TradeManager.java | 60 ++++++ .../trade/protocol/AtomicMakerProtocol.java | 54 +++++ .../bisq/core/trade/protocol/AtomicModel.java | 144 +++++++++++++ .../trade/protocol/AtomicTakerProtocol.java | 47 ++++ .../trade/protocol/BuyerAsMakerProtocol.java | 3 +- .../trade/protocol/BuyerAsTakerProtocol.java | 32 ++- .../core/trade/protocol/MakerProtocol.java | 4 +- .../core/trade/protocol/ProcessModel.java | 8 + .../trade/protocol/SellerAsMakerProtocol.java | 2 +- .../trade/protocol/SellerAsTakerProtocol.java | 27 ++- .../core/trade/protocol/TradeProtocol.java | 12 +- .../protocol/tasks/AtomicSetupTxListener.java | 105 +++++++++ .../tasks/PublishTradeStatistics.java | 5 +- .../maker/AtomicMakerCreatesAndSignsTx.java | 119 +++++++++++ .../maker/AtomicMakerSetupTxListener.java | 66 ++++++ .../maker/AtomicMakerVerifiesTakerInputs.java | 154 ++++++++++++++ .../taker/AtomicTakerPublishesAtomicTx.java | 87 ++++++++ .../taker/AtomicTakerSendsAtomicRequest.java | 131 ++++++++++++ .../taker/AtomicTakerSetupTxListener.java | 66 ++++++ .../taker/AtomicTakerVerifiesAtomicTx.java | 200 ++++++++++++++++++ .../resources/i18n/displayStrings.properties | 10 +- 25 files changed, 1352 insertions(+), 31 deletions(-) create mode 100644 core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/AtomicModel.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/AtomicTakerProtocol.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/AtomicSetupTxListener.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerCreatesAndSignsTx.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerPublishesAtomicTx.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSendsAtomicRequest.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java create mode 100644 core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java diff --git a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java index e6cecc1e4f9..76ac028894e 100644 --- a/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/BuyerAsMakerTrade.java @@ -20,7 +20,9 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.messages.CreateAtomicTxRequest; import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.protocol.AtomicMakerProtocol; import bisq.core.trade.protocol.BuyerAsMakerProtocol; import bisq.core.trade.protocol.MakerProtocol; @@ -115,4 +117,11 @@ public void handleTakeOfferRequest(InputsForDepositTxRequest message, ErrorMessageHandler errorMessageHandler) { ((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler); } + + @Override + public void handleTakeAtomicRequest(CreateAtomicTxRequest message, + NodeAddress taker, + ErrorMessageHandler errorMessageHandler) { + ((AtomicMakerProtocol) tradeProtocol).handleTakeAtomicRequest(message, taker, errorMessageHandler); + } } diff --git a/core/src/main/java/bisq/core/trade/MakerTrade.java b/core/src/main/java/bisq/core/trade/MakerTrade.java index 5a2fd7dd8d3..706f8d35db3 100644 --- a/core/src/main/java/bisq/core/trade/MakerTrade.java +++ b/core/src/main/java/bisq/core/trade/MakerTrade.java @@ -17,6 +17,7 @@ package bisq.core.trade; +import bisq.core.trade.messages.CreateAtomicTxRequest; import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.network.p2p.NodeAddress; @@ -24,5 +25,10 @@ import bisq.common.handlers.ErrorMessageHandler; public interface MakerTrade { - void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress peerNodeAddress, ErrorMessageHandler errorMessageHandler); + void handleTakeOfferRequest(InputsForDepositTxRequest message, + NodeAddress peerNodeAddress, + ErrorMessageHandler errorMessageHandler); + void handleTakeAtomicRequest(CreateAtomicTxRequest message, + NodeAddress peerNodeAddress, + ErrorMessageHandler errorMessageHandler); } diff --git a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java index 5e9b5883f9d..ae7e9a9576d 100644 --- a/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java +++ b/core/src/main/java/bisq/core/trade/SellerAsMakerTrade.java @@ -20,7 +20,9 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.offer.Offer; import bisq.core.proto.CoreProtoResolver; +import bisq.core.trade.messages.CreateAtomicTxRequest; import bisq.core.trade.messages.InputsForDepositTxRequest; +import bisq.core.trade.protocol.AtomicMakerProtocol; import bisq.core.trade.protocol.MakerProtocol; import bisq.core.trade.protocol.SellerAsMakerProtocol; @@ -111,7 +113,17 @@ protected void createTradeProtocol() { } @Override - public void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress taker, ErrorMessageHandler errorMessageHandler) { + public void handleTakeOfferRequest(InputsForDepositTxRequest message, + NodeAddress taker, + ErrorMessageHandler errorMessageHandler) { ((MakerProtocol) tradeProtocol).handleTakeOfferRequest(message, taker, errorMessageHandler); } + + @Override + public void handleTakeAtomicRequest(CreateAtomicTxRequest message, + NodeAddress taker, + ErrorMessageHandler errorMessageHandler) { + ((AtomicMakerProtocol) tradeProtocol).handleTakeAtomicRequest(message, taker, errorMessageHandler); + } + } diff --git a/core/src/main/java/bisq/core/trade/Trade.java b/core/src/main/java/bisq/core/trade/Trade.java index 6071e59d504..fd98290efed 100644 --- a/core/src/main/java/bisq/core/trade/Trade.java +++ b/core/src/main/java/bisq/core/trade/Trade.java @@ -21,6 +21,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.WalletsManager; import bisq.core.dao.DaoFacade; import bisq.core.filter.FilterManager; import bisq.core.locale.CurrencyUtil; @@ -172,7 +173,6 @@ public enum State { // Alternatively the maker could have seen the payout tx earlier before he received the PAYOUT_TX_PUBLISHED_MSG BUYER_SAW_PAYOUT_TX_IN_NETWORK(Phase.PAYOUT_PUBLISHED), - // #################### Phase WITHDRAWN WITHDRAW_COMPLETED(Phase.WITHDRAWN); @@ -356,6 +356,10 @@ public static protobuf.Trade.TradePeriodState toProtoMessage(Trade.TradePeriodSt private String counterCurrencyTxId; @Getter private final ObservableList chatMessages = FXCollections.observableArrayList(); + @Getter + @Setter + @Nullable + private String atomicTxId; // Transient // Immutable @@ -538,6 +542,7 @@ public Message toProtoMessage() { Optional.ofNullable(mediationResultState).ifPresent(e -> builder.setMediationResultState(MediationResultState.toProtoMessage(mediationResultState))); Optional.ofNullable(refundResultState).ifPresent(e -> builder.setRefundResultState(RefundResultState.toProtoMessage(refundResultState))); Optional.ofNullable(delayedPayoutTxBytes).ifPresent(e -> builder.setDelayedPayoutTxBytes(ByteString.copyFrom(delayedPayoutTxBytes))); + Optional.ofNullable(atomicTxId).ifPresent(builder::setAtomicTxId); return builder.build(); } @@ -570,6 +575,7 @@ public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolv trade.setDelayedPayoutTxBytes(ProtoUtil.byteArrayOrNullFromProto(proto.getDelayedPayoutTxBytes())); trade.setLockTime(proto.getLockTime()); trade.setLastRefreshRequestDate(proto.getLastRefreshRequestDate()); + trade.setAtomicTxId(ProtoUtil.stringOrNullFromProto(proto.getAtomicTxId())); trade.chatMessages.addAll(proto.getChatMessageList().stream() .map(ChatMessage::fromPayloadProto) @@ -592,6 +598,7 @@ public void init(P2PService p2PService, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, TradeWalletService tradeWalletService, + WalletsManager walletsManager, DaoFacade daoFacade, TradeManager tradeManager, OpenOfferManager openOfferManager, @@ -613,6 +620,7 @@ public void init(P2PService p2PService, btcWalletService, bsqWalletService, tradeWalletService, + walletsManager, daoFacade, referralIdService, user, @@ -1078,6 +1086,11 @@ public void logRefresh() { lastRefreshRequestDate = time; } + public boolean isAtomicBsqTrade() { + checkNotNull(getOffer(), "Offer must not be null"); + return getOffer().getPaymentMethod().isAtomic(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// @@ -1187,6 +1200,7 @@ public String toString() { ",\n refundResultState=" + refundResultState + ",\n refundResultStateProperty=" + refundResultStateProperty + ",\n lastRefreshRequestDate=" + lastRefreshRequestDate + + ",\n atomicTxId=" + atomicTxId + "\n}"; } } diff --git a/core/src/main/java/bisq/core/trade/TradeManager.java b/core/src/main/java/bisq/core/trade/TradeManager.java index 3ae37d9e478..706e9f49f1e 100644 --- a/core/src/main/java/bisq/core/trade/TradeManager.java +++ b/core/src/main/java/bisq/core/trade/TradeManager.java @@ -26,6 +26,7 @@ import bisq.core.btc.wallet.TradeWalletService; import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletService; +import bisq.core.btc.wallet.WalletsManager; import bisq.core.dao.DaoFacade; import bisq.core.filter.FilterManager; import bisq.core.locale.Res; @@ -41,6 +42,7 @@ import bisq.core.trade.closed.ClosedTradableManager; import bisq.core.trade.failed.FailedTradesManager; import bisq.core.trade.handlers.TradeResultHandler; +import bisq.core.trade.messages.CreateAtomicTxRequest; import bisq.core.trade.messages.InputsForDepositTxRequest; import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage; import bisq.core.trade.messages.TradeMessage; @@ -117,6 +119,7 @@ public class TradeManager implements PersistedDataHost { private final BtcWalletService btcWalletService; private final BsqWalletService bsqWalletService; private final TradeWalletService tradeWalletService; + private final WalletsManager walletsManager; private final OpenOfferManager openOfferManager; private final ClosedTradableManager closedTradableManager; private final FailedTradesManager failedTradesManager; @@ -158,6 +161,7 @@ public TradeManager(User user, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, TradeWalletService tradeWalletService, + WalletsManager walletsManager, OpenOfferManager openOfferManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, @@ -180,6 +184,7 @@ public TradeManager(User user, this.btcWalletService = btcWalletService; this.bsqWalletService = bsqWalletService; this.tradeWalletService = tradeWalletService; + this.walletsManager = walletsManager; this.openOfferManager = openOfferManager; this.closedTradableManager = closedTradableManager; this.failedTradesManager = failedTradesManager; @@ -205,6 +210,8 @@ public TradeManager(User user, // Handler for incoming initial network_messages from taker if (networkEnvelope instanceof InputsForDepositTxRequest) { handlePayDepositRequest((InputsForDepositTxRequest) networkEnvelope, peerNodeAddress); + } else if (networkEnvelope instanceof CreateAtomicTxRequest) { + handleTakeAtomicRequest((CreateAtomicTxRequest) networkEnvelope, peerNodeAddress); } }); @@ -421,11 +428,64 @@ private void handlePayDepositRequest(InputsForDepositTxRequest inputsForDepositT } } + + private void handleTakeAtomicRequest(CreateAtomicTxRequest createAtomicTxRequest, NodeAddress peer) { + log.info("Received CreateAtomicTxRequest from {} with tradeId {} and uid {}", + peer, createAtomicTxRequest.getTradeId(), createAtomicTxRequest.getUid()); + + try { + Validator.nonEmptyStringOf(createAtomicTxRequest.getTradeId()); + } catch (Throwable t) { + log.warn("Invalid createAtomicTxRequest " + createAtomicTxRequest.toString()); + return; + } + + Optional openOfferOptional = openOfferManager.getOpenOfferById(createAtomicTxRequest.getTradeId()); + if (openOfferOptional.isPresent() && openOfferOptional.get().getState() == OpenOffer.State.AVAILABLE) { + OpenOffer openOffer = openOfferOptional.get(); + Offer offer = openOffer.getOffer(); + openOfferManager.reserveOpenOffer(openOffer); + Trade trade; + if (offer.isBuyOffer()) + trade = new BuyerAsMakerTrade(offer, + Coin.valueOf(createAtomicTxRequest.getTxFee()), + Coin.valueOf(createAtomicTxRequest.getTakerFee()), + createAtomicTxRequest.isCurrencyForTakerFeeBtc(), + null, + null, + null, + tradableListStorage, + btcWalletService); + else + trade = new SellerAsMakerTrade(offer, + Coin.valueOf(createAtomicTxRequest.getTxFee()), + Coin.valueOf(createAtomicTxRequest.getTakerFee()), + createAtomicTxRequest.isCurrencyForTakerFeeBtc(), + null, + null, + null, + tradableListStorage, + btcWalletService); + + initTrade(trade, trade.getProcessModel().isUseSavingsWallet(), + trade.getProcessModel().getFundsNeededForTradeAsLong()); + trade.getProcessModel().getAtomicModel().initFromTrade(trade); + tradableList.add(trade); + ((MakerTrade) trade).handleTakeAtomicRequest(createAtomicTxRequest, peer, errorMessage -> { + if (takeOfferRequestErrorMessageHandler != null) + takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage); + }); + } else { + log.debug("We received a take offer request but don't have that offer anymore."); + } + } + private void initTrade(Trade trade, boolean useSavingsWallet, Coin fundsNeededForTrade) { trade.init(p2PService, btcWalletService, bsqWalletService, tradeWalletService, + walletsManager, daoFacade, this, openOfferManager, diff --git a/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java new file mode 100644 index 00000000000..94b1fb5bcb8 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java @@ -0,0 +1,54 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol; + + +import bisq.core.trade.messages.CreateAtomicTxRequest; +import bisq.core.trade.protocol.tasks.maker.AtomicMakerCreatesAndSignsTx; +import bisq.core.trade.protocol.tasks.maker.AtomicMakerSetupTxListener; +import bisq.core.trade.protocol.tasks.maker.AtomicMakerVerifiesTakerInputs; +import bisq.core.util.Validator; + +import bisq.network.p2p.NodeAddress; + +import bisq.common.handlers.ErrorMessageHandler; + +public interface AtomicMakerProtocol{ + default void handleTakeAtomicRequest(CreateAtomicTxRequest tradeMessage, + NodeAddress sender, + ErrorMessageHandler errorMessageHandler) { + Validator.checkTradeId(((TradeProtocol) this).processModel.getOfferId(), tradeMessage); + ((TradeProtocol) this).processModel.setTradeMessage(tradeMessage); + ((TradeProtocol) this).processModel.setTempTradingPeerNodeAddress(sender); + + TradeTaskRunner taskRunner = new TradeTaskRunner(((TradeProtocol) this).trade, + () -> ((TradeProtocol) this).handleTaskRunnerSuccess(tradeMessage, "handleTakeAtomicRequest"), + errorMessage -> { + errorMessageHandler.handleErrorMessage(errorMessage); + ((TradeProtocol) this).handleTaskRunnerFault(tradeMessage, errorMessage); + }); + + taskRunner.addTasks( + AtomicMakerVerifiesTakerInputs.class, + AtomicMakerCreatesAndSignsTx.class, + AtomicMakerSetupTxListener.class + ); + + taskRunner.run(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java b/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java new file mode 100644 index 00000000000..de6aebd4922 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java @@ -0,0 +1,144 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol; + +// Keep details specific to Atomic trades separate. Normal trade details are still handled by ProcessModel +// Atomic trades are handled in one sequence of interaction without downtime. If the trade negotiation fails +// there is no mediation, just no trade. +// Only atomic Tx Id is persisted with the Trade + +import bisq.core.btc.model.RawTransactionInput; +import bisq.core.monetary.Volume; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CreateAtomicTxRequest; + +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionInput; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.Nullable; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Getter +@Slf4j +public class AtomicModel { + @Setter + private long bsqTradeAmount; + @Setter + private long bsqMaxTradeAmount; + @Setter + private long bsqMinTradeAmount; + @Setter + private long btcTradeAmount; + @Setter + private long btcMaxTradeAmount; + @Setter + private long btcMinTradeAmount; + @Setter + private long tradePrice; + @Setter + private long bsqTradeFee; + @Setter + private long btcTradeFee; + @Setter + private long txFee; + @Setter + private long takerBsqOutputAmount; + @Setter + private long takerBtcOutputAmount; + @Setter + private long makerBsqOutputAmount; + @Setter + private long makerBtcOutputAmount; + @Nullable + @Setter + private String takerBsqAddress; + @Nullable + @Setter + private String takerBtcAddress; + @Nullable + @Setter + private String makerBsqAddress; + @Nullable + @Setter + private String makerBtcAddress; + @Setter + private List rawTakerBsqInputs = new ArrayList<>(); + @Setter + private List rawTakerBtcInputs = new ArrayList<>(); + @Setter + private List makerBsqInputs = new ArrayList<>(); + @Setter + private List makerBtcInputs = new ArrayList<>(); + @Nullable + @Setter + private byte[] atomicTx; + @Nullable + @Setter + private Transaction verifiedAtomicTx; + + + public void initFromTrade(Trade trade) { + var offer = trade.getOffer(); + checkNotNull(offer, "offer must not be null"); + if (trade.getTradeAmount() != null && trade.getTradeAmount().isPositive()) + bsqAmountFromVolume(trade.getTradeVolume()).ifPresent(this::setBsqTradeAmount); + bsqAmountFromVolume(offer.getVolume()).ifPresent(this::setBsqMaxTradeAmount); + bsqAmountFromVolume(offer.getMinVolume()).ifPresent(this::setBsqMinTradeAmount); +// setBsqTradeAmount( +// (Objects.requireNonNull(trade.getTradeVolume()).getMonetary().getValue() + 500_000) / 1_000_000); +// setBsqMaxTradeAmount( +// (Objects.requireNonNull(offer.getVolume()).getMonetary().getValue() + 500_000) / 1_000_000); +// setBsqMinTradeAmount( +// (Objects.requireNonNull(offer.getMinVolume()).getMonetary().getValue() + 500_000) / 1_000_000); + // Atomic trades only allow fixed prices + var price = offer.isUseMarketBasedPrice() ? 0 : Objects.requireNonNull(offer.getPrice()).getValue(); + setTradePrice(price); + if (trade.getTradeAmount() != null && trade.getTradeAmount().isPositive()) + setBtcTradeAmount(trade.getTradeAmount().getValue()); + setBtcMaxTradeAmount(offer.getAmount().getValue()); + setBtcMinTradeAmount(offer.getMinAmount().getValue()); + setBsqTradeFee(trade.isCurrencyForTakerFeeBtc() ? 0 : trade.getTakerFeeAsLong()); + setBtcTradeFee(trade.isCurrencyForTakerFeeBtc() ? trade.getTakerFeeAsLong() : 0); + } + + public void updateFromCreateAtomicTxRequest(CreateAtomicTxRequest message) { + setTakerBsqOutputAmount(message.getTakerBsqOutputValue()); + setTakerBtcOutputAmount(message.getTakerBtcOutputValue()); + setTakerBsqAddress(message.getTakerBsqOutputAddress()); + setTakerBtcAddress(message.getTakerBtcOutputAddress()); + setRawTakerBsqInputs(message.getTakerBsqInputs()); + setRawTakerBtcInputs(message.getTakerBtcInputs()); + setBsqTradeAmount(message.getBsqTradeAmount()); + setBtcTradeAmount(message.getBtcTradeAmount()); + } + + public Optional bsqAmountFromVolume(Volume volume) { + // The Altcoin class have the smallest unit set to 8 decimals, BSQ has the smallest unit at 2 decimals. + return volume == null ? Optional.empty() : Optional.of((volume.getMonetary().getValue() + 500_000) / 1_000_000); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/AtomicTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/AtomicTakerProtocol.java new file mode 100644 index 00000000000..b9034ee6a38 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/AtomicTakerProtocol.java @@ -0,0 +1,47 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol; + + +import bisq.core.trade.messages.CreateAtomicTxResponse; +import bisq.core.trade.protocol.tasks.PublishTradeStatistics; +import bisq.core.trade.protocol.tasks.taker.AtomicTakerPublishesAtomicTx; +import bisq.core.trade.protocol.tasks.taker.AtomicTakerSetupTxListener; +import bisq.core.trade.protocol.tasks.taker.AtomicTakerVerifiesAtomicTx; + +import bisq.network.p2p.NodeAddress; + +public interface AtomicTakerProtocol { + default void handle(CreateAtomicTxResponse tradeMessage, NodeAddress peerNodeAddress) { + ((TradeProtocol) this).getLog().debug("handle CreateAtomicTxResponse called"); + ((TradeProtocol) this).processModel.setTradeMessage(tradeMessage); + ((TradeProtocol) this).processModel.setTempTradingPeerNodeAddress(peerNodeAddress); + + TradeTaskRunner taskRunner = new TradeTaskRunner(((TradeProtocol) this).trade, + () -> ((TradeProtocol) this).handleTaskRunnerSuccess(tradeMessage, "handle CreateAtomicTxResponse"), + errorMessage -> ((TradeProtocol) this).handleTaskRunnerFault(tradeMessage, errorMessage)); + + taskRunner.addTasks( + AtomicTakerVerifiesAtomicTx.class, + AtomicTakerPublishesAtomicTx.class, + AtomicTakerSetupTxListener.class, + PublishTradeStatistics.class + ); + taskRunner.run(); + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java index ec5d5547937..648589a15ee 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsMakerProtocol.java @@ -56,7 +56,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol, MakerProtocol { +public class BuyerAsMakerProtocol extends TradeProtocol implements BuyerProtocol, MakerProtocol, AtomicMakerProtocol { private final BuyerAsMakerTrade buyerAsMakerTrade; @@ -141,7 +141,6 @@ public void handleTakeOfferRequest(InputsForDepositTxRequest tradeMessage, taskRunner.run(); } - /////////////////////////////////////////////////////////////////////////////////////////// // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java index 6665c001ec6..b6e95b7a0ac 100644 --- a/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/BuyerAsTakerProtocol.java @@ -21,6 +21,7 @@ import bisq.core.offer.Offer; import bisq.core.trade.BuyerAsTakerTrade; import bisq.core.trade.Trade; +import bisq.core.trade.messages.CreateAtomicTxResponse; import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest; import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage; import bisq.core.trade.messages.InputsForDepositTxResponse; @@ -44,6 +45,7 @@ import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerCreatesDepositTxInputs; import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSendsDepositTxMessage; import bisq.core.trade.protocol.tasks.buyer_as_taker.BuyerAsTakerSignsDepositTx; +import bisq.core.trade.protocol.tasks.taker.AtomicTakerSendsAtomicRequest; import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; @@ -62,7 +64,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol, TakerProtocol { +public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol, TakerProtocol, AtomicTakerProtocol { private final BuyerAsTakerTrade buyerAsTakerTrade; @@ -74,10 +76,12 @@ public BuyerAsTakerProtocol(BuyerAsTakerTrade trade) { super(trade); this.buyerAsTakerTrade = trade; - Offer offer = checkNotNull(trade.getOffer()); processModel.getTradingPeer().setPubKeyRing(offer.getPubKeyRing()); + // Atomic trades don't have a deposit tx, return early + if (trade.isAtomicBsqTrade()) return; + Trade.Phase phase = trade.getState().getPhase(); if (phase == Trade.Phase.TAKER_FEE_PUBLISHED) { TradeTaskRunner taskRunner = new TradeTaskRunner(trade, @@ -125,21 +129,25 @@ public void takeAvailableOffer() { () -> handleTaskRunnerSuccess("takeAvailableOffer"), this::handleTaskRunnerFault); - taskRunner.addTasks( - TakerVerifyMakerAccount.class, - TakerVerifyMakerFeePayment.class, - CreateTakerFeeTx.class, - BuyerAsTakerCreatesDepositTxInputs.class, - TakerSendInputsForDepositTxRequest.class - ); - + if (trade.isAtomicBsqTrade()) { + taskRunner.addTasks( + AtomicTakerSendsAtomicRequest.class + ); + } else { + taskRunner.addTasks( + TakerVerifyMakerAccount.class, + TakerVerifyMakerFeePayment.class, + CreateTakerFeeTx.class, + BuyerAsTakerCreatesDepositTxInputs.class, + TakerSendInputsForDepositTxRequest.class + ); + } //TODO if peer does get an error he does not respond and all we get is the timeout now knowing why it failed. // We should add an error message the peer sends us in such cases. startTimeout(); taskRunner.run(); } - /////////////////////////////////////////////////////////////////////////////////////////// // Incoming message handling /////////////////////////////////////////////////////////////////////////////////////////// @@ -283,6 +291,8 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s handle((PayoutTxPublishedMessage) tradeMessage, sender); } else if (tradeMessage instanceof RefreshTradeStateRequest) { handle(); + } else if (tradeMessage instanceof CreateAtomicTxResponse) { + handle((CreateAtomicTxResponse) tradeMessage, sender); } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java index d460289e3fc..349e677a702 100644 --- a/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/MakerProtocol.java @@ -25,5 +25,7 @@ import bisq.common.handlers.ErrorMessageHandler; public interface MakerProtocol { - void handleTakeOfferRequest(InputsForDepositTxRequest message, NodeAddress taker, ErrorMessageHandler errorMessageHandler); + void handleTakeOfferRequest(InputsForDepositTxRequest message, + NodeAddress taker, + ErrorMessageHandler errorMessageHandler); } diff --git a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java index 27317d7569c..12d8daf691e 100644 --- a/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/ProcessModel.java @@ -22,6 +22,7 @@ import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.TradeWalletService; +import bisq.core.btc.wallet.WalletsManager; import bisq.core.dao.DaoFacade; import bisq.core.filter.FilterManager; import bisq.core.network.MessageState; @@ -84,6 +85,7 @@ public class ProcessModel implements Model, PersistablePayload { transient private BtcWalletService btcWalletService; transient private BsqWalletService bsqWalletService; transient private TradeWalletService tradeWalletService; + transient private WalletsManager walletsManager; transient private DaoFacade daoFacade; transient private Offer offer; transient private User user; @@ -163,6 +165,10 @@ public class ProcessModel implements Model, PersistablePayload { @Setter private ObjectProperty paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED); + // Added in v.1.3.x + @Setter + private AtomicModel atomicModel = new AtomicModel(); + public ProcessModel() { } @@ -240,6 +246,7 @@ public void onAllServicesInitialized(Offer offer, BtcWalletService walletService, BsqWalletService bsqWalletService, TradeWalletService tradeWalletService, + WalletsManager walletsManager, DaoFacade daoFacade, ReferralIdService referralIdService, User user, @@ -258,6 +265,7 @@ public void onAllServicesInitialized(Offer offer, this.btcWalletService = walletService; this.bsqWalletService = bsqWalletService; this.tradeWalletService = tradeWalletService; + this.walletsManager = walletsManager; this.daoFacade = daoFacade; this.referralIdService = referralIdService; this.user = user; diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java index 3a5ae4df9b8..a1575c587b9 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsMakerProtocol.java @@ -58,7 +58,7 @@ import lombok.extern.slf4j.Slf4j; @Slf4j -public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtocol, MakerProtocol { +public class SellerAsMakerProtocol extends TradeProtocol implements SellerProtocol, MakerProtocol, AtomicMakerProtocol { private final SellerAsMakerTrade sellerAsMakerTrade; diff --git a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java index fd42d66b7c0..194ee497831 100644 --- a/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/SellerAsTakerProtocol.java @@ -22,6 +22,7 @@ import bisq.core.trade.SellerAsTakerTrade; import bisq.core.trade.Trade; import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage; +import bisq.core.trade.messages.CreateAtomicTxResponse; import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse; import bisq.core.trade.messages.InputsForDepositTxResponse; import bisq.core.trade.messages.TradeMessage; @@ -41,6 +42,7 @@ import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx; import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerCreatesDepositTxInputs; import bisq.core.trade.protocol.tasks.seller_as_taker.SellerAsTakerSignsDepositTx; +import bisq.core.trade.protocol.tasks.taker.AtomicTakerSendsAtomicRequest; import bisq.core.trade.protocol.tasks.taker.CreateTakerFeeTx; import bisq.core.trade.protocol.tasks.taker.TakerProcessesInputsForDepositTxResponse; import bisq.core.trade.protocol.tasks.taker.TakerPublishFeeTx; @@ -59,7 +61,7 @@ import static com.google.common.base.Preconditions.checkNotNull; @Slf4j -public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtocol, TakerProtocol { +public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtocol, TakerProtocol, AtomicTakerProtocol { private final SellerAsTakerTrade sellerAsTakerTrade; @@ -101,14 +103,19 @@ public void takeAvailableOffer() { () -> handleTaskRunnerSuccess("takeAvailableOffer"), this::handleTaskRunnerFault); - taskRunner.addTasks( - TakerVerifyMakerAccount.class, - TakerVerifyMakerFeePayment.class, - CreateTakerFeeTx.class, - SellerAsTakerCreatesDepositTxInputs.class, - TakerSendInputsForDepositTxRequest.class - ); - + if (trade.isAtomicBsqTrade()) { + taskRunner.addTasks( + AtomicTakerSendsAtomicRequest.class + ); + } else { + taskRunner.addTasks( + TakerVerifyMakerAccount.class, + TakerVerifyMakerFeePayment.class, + CreateTakerFeeTx.class, + SellerAsTakerCreatesDepositTxInputs.class, + TakerSendInputsForDepositTxRequest.class + ); + } startTimeout(); taskRunner.run(); } @@ -261,6 +268,8 @@ protected void doHandleDecryptedMessage(TradeMessage tradeMessage, NodeAddress s handle((DelayedPayoutTxSignatureResponse) tradeMessage, sender); } else if (tradeMessage instanceof CounterCurrencyTransferStartedMessage) { handle((CounterCurrencyTransferStartedMessage) tradeMessage, sender); + } else if (tradeMessage instanceof CreateAtomicTxResponse) { + handle((CreateAtomicTxResponse) tradeMessage, sender); } } } diff --git a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java index 4cf78045db2..a532d0362f8 100644 --- a/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/TradeProtocol.java @@ -57,6 +57,8 @@ import java.security.PublicKey; +import org.slf4j.Logger; + import lombok.extern.slf4j.Slf4j; import javax.annotation.Nullable; @@ -113,7 +115,11 @@ public TradeProtocol(Trade trade) { processModel.getP2PService().addDecryptedDirectMessageListener(decryptedDirectMessageListener); stateChangeListener = (observable, oldValue, newValue) -> { - if (newValue.getPhase() == Trade.Phase.TAKER_FEE_PUBLISHED && trade instanceof MakerTrade) + if (!(trade instanceof MakerTrade)) + return; + var takerFeePublished = newValue.getPhase() == Trade.Phase.TAKER_FEE_PUBLISHED; + var atomicTxPublished = trade.isAtomicBsqTrade() && newValue.getPhase() == Trade.Phase.WITHDRAWN; + if (takerFeePublished || atomicTxPublished) processModel.getOpenOfferManager().closeOpenOffer(checkNotNull(trade.getOffer())); }; trade.stateProperty().addListener(stateChangeListener); @@ -420,4 +426,8 @@ private void cleanupTradableOnFault() { } } } + + public Logger getLog() { + return log; + } } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/AtomicSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/AtomicSetupTxListener.java new file mode 100644 index 00000000000..e89171584a0 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/AtomicSetupTxListener.java @@ -0,0 +1,105 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks; + +import bisq.core.btc.listeners.AddressConfidenceListener; +import bisq.core.btc.wallet.WalletService; +import bisq.core.trade.Trade; + +import bisq.common.UserThread; +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.core.TransactionConfidence; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public abstract class AtomicSetupTxListener extends TradeTask { + // Use instance fields to not get eaten up by the GC + private AddressConfidenceListener confidenceListener; + protected Address myAddress; + protected WalletService walletService; + + @SuppressWarnings({"unused"}) + public AtomicSetupTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + + @Override + protected void run() { + try { + runInterceptHook(); + + TransactionConfidence confidence = walletService.getConfidenceForAddress(myAddress); + if (isInNetwork(confidence)) { + applyConfidence(confidence); + } else { + confidenceListener = new AddressConfidenceListener(myAddress) { + @Override + public void onTransactionConfidenceChanged(TransactionConfidence confidence) { + if (isInNetwork(confidence)) + applyConfidence(confidence); + } + }; + walletService.addAddressConfidenceListener(confidenceListener); + } + + // we complete immediately, our object stays alive because the balanceListener is stored in the WalletService + complete(); + } catch (Throwable t) { + failed(t); + } + } + + private void applyConfidence(TransactionConfidence confidence) { + Transaction walletTx = walletService.getTransaction(confidence.getTransactionHash()); + checkNotNull(walletTx, "Tx from network should not be null"); + if (processModel.getAtomicModel().getAtomicTx() != null) { + trade.setAtomicTxId(walletTx.getHashAsString()); + WalletService.printTx("atomicTx received from network", walletTx); + setState(); + } else { + log.info("We had the atomic tx already set. tradeId={}, state={}", trade.getId(), trade.getState()); + } + + // need delay as it can be called inside the handler before the listener and tradeStateSubscription are actually set. + UserThread.execute(this::unSubscribe); + } + + private boolean isInNetwork(TransactionConfidence confidence) { + return confidence != null && + (confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.BUILDING) || + confidence.getConfidenceType().equals(TransactionConfidence.ConfidenceType.PENDING)); + } + + private void unSubscribe() { + if (confidenceListener != null) + processModel.getBtcWalletService().removeAddressConfidenceListener(confidenceListener); + } + + protected void setState() { + trade.setState(Trade.State.WITHDRAW_COMPLETED); + processModel.getTradeManager().addTradeToClosedTrades(trade); + } + +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java b/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java index 421b69e1857..19113e04bda 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/PublishTradeStatistics.java @@ -45,7 +45,8 @@ public PublishTradeStatistics(TaskRunner taskHandler, Trade trade) { protected void run() { try { runInterceptHook(); - if (trade.getDepositTx() != null) { + var txId = trade.isAtomicBsqTrade() ? trade.getAtomicTxId() : trade.getDepositTxId(); + if (txId != null) { Map extraDataMap = new HashMap<>(); if (processModel.getReferralIdService().getOptionalReferralId().isPresent()) { extraDataMap.put(OfferPayload.REFERRAL_ID, processModel.getReferralIdService().getOptionalReferralId().get()); @@ -70,7 +71,7 @@ protected void run() { trade.getTradePrice(), trade.getTradeAmount(), trade.getDate(), - trade.getDepositTxId(), + txId, extraDataMap); processModel.getP2PService().addPersistableNetworkPayload(tradeStatistics, true); } diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerCreatesAndSignsTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerCreatesAndSignsTx.java new file mode 100644 index 00000000000..f88a3fef090 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerCreatesAndSignsTx.java @@ -0,0 +1,119 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.maker; + +import bisq.core.dao.governance.param.Param; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CreateAtomicTxResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AtomicMakerCreatesAndSignsTx extends TradeTask { + @SuppressWarnings({"unused"}) + public AtomicMakerCreatesAndSignsTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + var atomicModel = processModel.getAtomicModel(); + var makerBsqAddress = processModel.getBsqWalletService().getUnusedAddress(); + var makerBtcAddress = processModel.getBtcWalletService().getFreshAddressEntry().getAddress(); + checkNotNull(makerBtcAddress, "Maker address must not be null"); + atomicModel.setMakerBsqAddress(makerBsqAddress.toString()); + atomicModel.setMakerBtcAddress(makerBtcAddress.toString()); + var takerBsqAddressInBtcFormat = processModel.getBsqWalletService().getBsqFormatter(). + getAddressFromBsqAddress(atomicModel.getTakerBsqAddress()).toString(); + + // Create atomic tx with maker btc inputs signed + var makerSignedBtcAtomicTx = processModel.getTradeWalletService().makerCreatesAndSignsAtomicTx( + Coin.valueOf(atomicModel.getMakerBsqOutputAmount()), + Coin.valueOf(atomicModel.getMakerBtcOutputAmount()), + Coin.valueOf(atomicModel.getTakerBsqOutputAmount()), + Coin.valueOf(atomicModel.getTakerBtcOutputAmount()), + Coin.valueOf(atomicModel.getBtcTradeFee()), + makerBsqAddress.toString(), + makerBtcAddress.toString(), + takerBsqAddressInBtcFormat, + atomicModel.getTakerBtcAddress(), + processModel.getDaoFacade().getParamValue(Param.RECIPIENT_BTC_ADDRESS), + atomicModel.getMakerBsqInputs(), + atomicModel.getMakerBtcInputs(), + atomicModel.getRawTakerBsqInputs(), + atomicModel.getRawTakerBtcInputs()); + + // Sign maker bsq inputs + var makerSignedAtomicTx = processModel.getBsqWalletService().signInputs( + makerSignedBtcAtomicTx, atomicModel.getMakerBsqInputs()); + + atomicModel.setAtomicTx(makerSignedAtomicTx.bitcoinSerialize()); + var message = new CreateAtomicTxResponse(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + atomicModel.getAtomicTx()); + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), + message.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), + message.getUid(), errorMessage); + + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + + errorMessage); + failed(); + } + } + ); + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java new file mode 100644 index 00000000000..14f20215662 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java @@ -0,0 +1,66 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.maker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.AtomicSetupTxListener; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AtomicMakerSetupTxListener extends AtomicSetupTxListener { + + @SuppressWarnings({"unused"}) + public AtomicMakerSetupTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + var atomicModel = processModel.getAtomicModel(); + checkNotNull(atomicModel, "AtomicModel must not be null"); + + // Find address to listen to + if (atomicModel.getMakerBtcAddress() != null) { + walletService = processModel.getBtcWalletService(); + myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getMakerBtcAddress()); + } else if (atomicModel.getMakerBsqAddress() != null){ + // Listen to BSQ address + walletService = processModel.getBsqWalletService(); + myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getMakerBsqAddress()); + } else { + failed("No maker address set"); + } + + super.run(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java new file mode 100644 index 00000000000..28d163c529f --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java @@ -0,0 +1,154 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.maker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.offer.Offer; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CreateAtomicTxRequest; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.TransactionInput; + +import java.util.ArrayList; +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AtomicMakerVerifiesTakerInputs extends TradeTask { + @SuppressWarnings({"unused"}) + public AtomicMakerVerifiesTakerInputs(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + /* Tx format: + * [1] (BSQtradeAmount, makerBSQAddress) + * [0-1] (BSQchange, takerBSQAddress) (Change from BSQ for tradeAmount and/or tradeFee) + * [1] (BTCtradeAmount + BTCchange, takerBTCAddress) (Change from BTC for txFee and/or tradeFee) + * [0-1] (BTCchange, makerBTCAddress) (Change from BTC for tradeAmount payment) + * [0-1] (BTCtradeFee) + */ + + checkArgument(processModel.getOffer().isMyOffer(processModel.getKeyRing()), "must process own offer"); + var isBuyer = processModel.getOffer().isBuyOffer(); + + if (!(processModel.getTradeMessage() instanceof CreateAtomicTxRequest)) + failed("Expected CreateAtomicTxRequest"); + + var message = (CreateAtomicTxRequest) processModel.getTradeMessage(); + var atomicModel = checkNotNull(processModel.getAtomicModel(), "AtomicModel must not be null"); + atomicModel.updateFromCreateAtomicTxRequest(message); + + Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); + if (message.getTradePrice() != atomicModel.getTradePrice()) + failed("Unexpected trade price"); + trade.setTradePrice(message.getTradePrice()); + processModel.getTradingPeer().setPubKeyRing(checkNotNull(message.getTakerPubKeyRing())); + trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); + trade.setTradeAmount(Coin.valueOf(message.getBtcTradeAmount())); + + if (message.getBsqTradeAmount() < atomicModel.getBsqMinTradeAmount() || + message.getBsqTradeAmount() > atomicModel.getBsqMaxTradeAmount()) + failed("bsqTradeAmount not within range"); + if (message.getBtcTradeAmount() < atomicModel.getBtcMinTradeAmount() || + message.getBtcTradeAmount() > atomicModel.getBtcMaxTradeAmount()) + failed("btcTradeAmount not within range"); + if (message.getTakerFee() != + (message.isCurrencyForTakerFeeBtc() ? atomicModel.getBtcTradeFee() : atomicModel.getBsqTradeFee())) + failed("Taker fee mismatch"); + var bsqAmount = atomicModel.bsqAmountFromVolume(trade.getTradeVolume()).orElse(null); + if (bsqAmount == null || bsqAmount != message.getBsqTradeAmount()) + failed("Amounts don't match price"); + // TODO verify txFee is reasonable + + // Verify taker bsq address + processModel.getBsqWalletService().getBsqFormatter().getAddressFromBsqAddress( + message.getTakerBsqOutputAddress()); + + var makerAddress = processModel.getBtcWalletService().getOrCreateAddressEntry( + trade.getId(), AddressEntry.Context.RESERVED_FOR_TRADE).getAddress(); + List makerBtcInputs = new ArrayList<>(); + if (!isBuyer) { + makerBtcInputs = processModel.getTradeWalletService().getInputsForAddress(makerAddress, + trade.getTradeAmount()); + } + // Inputs + var makerBsqInputAmount = 0L; + var makerBsqOutputAmount = message.getBsqTradeAmount(); + if (isBuyer) { + // TODO Reserve BSQ for trade + // Prepare BSQ as it's not part of the prepared tx + var requiredBsq = atomicModel.getBsqTradeAmount(); + var preparedBsq = processModel.getBsqWalletService().prepareAtomicBsqInputs(Coin.valueOf(requiredBsq)); + makerBsqInputAmount = processModel.getBsqWalletService().getBsqInputAmount( + preparedBsq.first.getInputs(), processModel.getDaoFacade()); + makerBsqOutputAmount = preparedBsq.second.getValue(); + atomicModel.setMakerBsqInputs(preparedBsq.first.getInputs()); + } + + var takerBsqInputAmount = processModel.getBsqWalletService().getBsqRawInputAmount( + message.getTakerBsqInputs(), processModel.getDaoFacade()); + var takerBtcInputAmount = processModel.getBtcWalletService().getBtcRawInputAmount( + message.getTakerBtcInputs()); + var makerBtcInputAmount = processModel.getBtcWalletService().getBtcInputAmount(makerBtcInputs); + + // Outputs and fees + var takerBsqOutputAmount = message.getTakerBsqOutputValue(); + var takerBtcOutputAmount = message.getTakerBtcOutputValue(); + var makerBtcOutputAmount = isBuyer ? message.getBtcTradeAmount() : + makerBtcInputAmount - message.getBtcTradeAmount(); + var btcTradeFeeAmount = atomicModel.getBtcTradeFee(); + var bsqTradeFee = atomicModel.getBsqTradeFee(); + var txFee = message.getTxFee(); + + // Verify input sum equals output sum + var bsqIn = takerBsqInputAmount + makerBsqInputAmount; + var bsqOut = takerBsqOutputAmount + makerBsqOutputAmount; + if (bsqIn != bsqOut + bsqTradeFee) + failed("BSQ in does not match BSQ out"); + var btcIn = takerBtcInputAmount + makerBtcInputAmount + bsqTradeFee; + var btcOut = takerBtcOutputAmount + makerBtcOutputAmount + btcTradeFeeAmount; + if (btcIn != btcOut + txFee) + failed("BTC in does not match BTC out"); + + // Message data is verified as correct, update model with data from message + atomicModel.setMakerBtcInputs(makerBtcInputs); + atomicModel.setMakerBtcOutputAmount(makerBtcOutputAmount); + atomicModel.setMakerBsqOutputAmount(makerBsqOutputAmount); + atomicModel.setTxFee(message.getTxFee()); + + trade.persist(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerPublishesAtomicTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerPublishesAtomicTx.java new file mode 100644 index 00000000000..f92ce8afb3a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerPublishesAtomicTx.java @@ -0,0 +1,87 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.model.AddressEntry; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Transaction; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AtomicTakerPublishesAtomicTx extends TradeTask { + + @SuppressWarnings({"unused"}) + public AtomicTakerPublishesAtomicTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + // Sign and publish atomicTx + var atomicModel = processModel.getAtomicModel(); + var atomicTx = atomicModel.getVerifiedAtomicTx(); + + checkNotNull(atomicTx, "Verified atomictx must not be null"); + processModel.getBsqWalletService().signTx(atomicTx); + processModel.getBtcWalletService().signTx(atomicTx); + + log.info("AtomicTxBytes: {}", Utilities.bytesAsHexString(atomicTx.bitcoinSerialize())); + processModel.getWalletsManager().publishAndCommitBsqTx(atomicTx, TxType.TRANSFER_BSQ, + new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + if (!completed) { + trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX); + + processModel.getBtcWalletService().swapTradeEntryToAvailableEntry(processModel.getOffer().getId(), AddressEntry.Context.RESERVED_FOR_TRADE); + + complete(); + } else { + log.warn("We got the onSuccess callback called after the timeout has been triggered a complete()."); + } + } + + @Override + public void onFailure(TxBroadcastException exception) { + if (!completed) { + failed(exception); + } else { + log.warn("We got the onFailure callback called after the timeout has been triggered a complete()."); + } + } + }); + + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSendsAtomicRequest.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSendsAtomicRequest.java new file mode 100644 index 00000000000..38adbb60083 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSendsAtomicRequest.java @@ -0,0 +1,131 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.taker; + +import bisq.core.btc.model.AddressEntry; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CreateAtomicTxRequest; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.network.p2p.NodeAddress; +import bisq.network.p2p.SendDirectMessageListener; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Coin; + +import java.util.UUID; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +public class AtomicTakerSendsAtomicRequest extends TradeTask { + + @SuppressWarnings({"unused"}) + public AtomicTakerSendsAtomicRequest(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + checkArgument(!processModel.getOffer().isMyOffer(processModel.getKeyRing()), "must not take own offer"); + var isBuyer = !processModel.getOffer().isBuyOffer(); + + var atomicModel = processModel.getAtomicModel(); + atomicModel.initFromTrade(trade); + atomicModel.setTakerBsqAddress(processModel.getBsqWalletService().getUnusedBsqAddressAsString()); + atomicModel.setTakerBtcAddress(processModel.getBtcWalletService(). + getNewAddressEntry(trade.getId(), AddressEntry.Context.TRADE_PAYOUT).getAddressString()); + + // Prepare BSQ inputs + var requiredBsq = atomicModel.getBsqTradeFee() + (isBuyer ? atomicModel.getBsqTradeAmount() : 0L); + var preparedBsq = processModel.getBsqWalletService().prepareAtomicBsqInputs(Coin.valueOf(requiredBsq)); + var takerBsqOutputAmount = preparedBsq.second.getValue() + (isBuyer ? 0L : atomicModel.getBsqTradeAmount()); + atomicModel.setTakerBsqOutputAmount(takerBsqOutputAmount); + + // Prepare BTC inputs + var preparedAtomicTxData = processModel.getTradeWalletService().takerPreparesAtomicTx( + preparedBsq.first, + Coin.valueOf(isBuyer ? 0L : atomicModel.getBtcTradeAmount()), + Coin.valueOf(atomicModel.getTxFee()), + Coin.valueOf(atomicModel.getBtcTradeFee()), + Coin.valueOf(atomicModel.getBsqTradeFee())); + atomicModel.setRawTakerBsqInputs(preparedAtomicTxData.first); + atomicModel.setRawTakerBtcInputs(preparedAtomicTxData.second); + atomicModel.setTxFee(preparedAtomicTxData.third.getValue()); + var takerBtcOutputAmount = preparedAtomicTxData.fourth.getValue() + + (isBuyer ? atomicModel.getBtcTradeAmount() : 0L); + atomicModel.setTakerBtcOutputAmount(takerBtcOutputAmount); + + var message = new CreateAtomicTxRequest(UUID.randomUUID().toString(), + processModel.getOfferId(), + processModel.getMyNodeAddress(), + processModel.getPubKeyRing(), + atomicModel.getBsqTradeAmount(), + atomicModel.getBtcTradeAmount(), + atomicModel.getTradePrice(), + atomicModel.getTxFee(), + trade.getTakerFeeAsLong(), + trade.isCurrencyForTakerFeeBtc(), + atomicModel.getTakerBsqOutputAmount(), + atomicModel.getTakerBsqAddress(), + atomicModel.getTakerBtcOutputAmount(), + atomicModel.getTakerBtcAddress(), + atomicModel.getRawTakerBsqInputs(), + atomicModel.getRawTakerBtcInputs()); + + log.info("atomictxrequest={}", message.toString()); + + NodeAddress peersNodeAddress = trade.getTradingPeerNodeAddress(); + log.info("Send {} to peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), message.getUid()); + processModel.getP2PService().sendEncryptedDirectMessage( + peersNodeAddress, + processModel.getTradingPeer().getPubKeyRing(), + message, + new SendDirectMessageListener() { + @Override + public void onArrived() { + log.info("{} arrived at peer {}. tradeId={}, uid={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), + message.getUid()); + complete(); + } + + @Override + public void onFault(String errorMessage) { + log.error("{} failed: Peer {}. tradeId={}, uid={}, errorMessage={}", + message.getClass().getSimpleName(), peersNodeAddress, message.getTradeId(), + message.getUid(), errorMessage); + + appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + + errorMessage); + failed(); + } + } + ); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java new file mode 100644 index 00000000000..53318fe450a --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java @@ -0,0 +1,66 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.taker; + +import bisq.core.trade.Trade; +import bisq.core.trade.protocol.tasks.AtomicSetupTxListener; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.Address; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AtomicTakerSetupTxListener extends AtomicSetupTxListener { + + @SuppressWarnings({"unused"}) + public AtomicTakerSetupTxListener(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + var atomicModel = processModel.getAtomicModel(); + checkNotNull(atomicModel, "AtomicModel must not be null"); + + // Find address to listen to + if (atomicModel.getTakerBtcAddress() != null) { + walletService = processModel.getBtcWalletService(); + myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getTakerBtcAddress()); + } else if (atomicModel.getTakerBsqAddress() != null){ + // Listen to BSQ address + walletService = processModel.getBsqWalletService(); + myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getTakerBsqAddress()); + } else { + failed("No maker address set"); + } + + super.run(); + + complete(); + } catch (Throwable t) { + failed(t); + } + } +} diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java new file mode 100644 index 00000000000..03c2b1269e3 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java @@ -0,0 +1,200 @@ +/* + * 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 . + */ + +package bisq.core.trade.protocol.tasks.taker; + +import bisq.core.trade.DonationAddressValidation; +import bisq.core.trade.Trade; +import bisq.core.trade.messages.CreateAtomicTxResponse; +import bisq.core.trade.protocol.tasks.TradeTask; + +import bisq.common.taskrunner.TaskRunner; + +import org.bitcoinj.core.TransactionInput; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +@Slf4j +public class AtomicTakerVerifiesAtomicTx extends TradeTask { + + @SuppressWarnings({"unused"}) + public AtomicTakerVerifiesAtomicTx(TaskRunner taskHandler, Trade trade) { + super(taskHandler, trade); + } + + @Override + protected void run() { + try { + runInterceptHook(); + + /* Tx output format: + * At a minimum there will be 1 BSQ out and 1 BTC out + * [0-1] (Maker BSQ) + * [0-1] (Taker BSQ) + * [0-1] (Taker BTC) + * [0-1] (Maker BTC) + * [0-1] (BTCtradeFee) + + * Taker verifies: + * - Number of outputs + * - input vs output BSQ + * - first inputs are taker BSQ inputs + * - other inputs are not owned by taker + * - txFee + * - tradeFee + * - BSQ output to taker BSQ address + * - BTC output to taker BTC address + */ + + checkArgument(!processModel.getOffer().isMyOffer(processModel.getKeyRing()), "must not take own offer"); + var isBuyer = !processModel.getOffer().isBuyOffer(); + + if (!(processModel.getTradeMessage() instanceof CreateAtomicTxResponse)) + failed("Expected CreateAtomicTxResponse"); + + + var serializedAtomicTx = ((CreateAtomicTxResponse) processModel.getTradeMessage()).getAtomicTx(); + var atomicTx = processModel.getBtcWalletService().getTxFromSerializedTx(serializedAtomicTx); + var atomicModel = processModel.getAtomicModel(); + + var inputsSize = atomicTx.getInputs().size(); + var outputsSize = atomicTx.getOutputs().size(); + + // Verify taker inputs are added as expected + int inputIndex = 0; + checkNotNull(atomicModel.getRawTakerBsqInputs(), "Taker BSQ inputs must not be null"); + List atomicTxInputs = new ArrayList<>(); + for (var rawInput : atomicModel.getRawTakerBsqInputs()) { + var verifiedInput = processModel.getBsqWalletService().verifyTransactionInput( + atomicTx.getInput(inputIndex), rawInput); + if (verifiedInput == null) + failed("Taker BSQ input mismatch"); + atomicTxInputs.add(verifiedInput); + inputIndex++; + if (inputIndex > inputsSize) + failed("Not enough inputs"); + } + if (atomicModel.getRawTakerBtcInputs() != null) { + for (var rawInput : atomicModel.getRawTakerBtcInputs()) { + var verifiedInput = processModel.getTradeWalletService().verifyTransactionInput( + atomicTx.getInput(inputIndex), rawInput); + if (verifiedInput == null) + failed("Taker BTC input mismatch"); + atomicTxInputs.add(verifiedInput); + inputIndex++; + if (inputIndex > inputsSize) + failed("Not enough inputs"); + } + } + List makerInputs = new ArrayList<>(); + for (; inputIndex < atomicTx.getInputs().size(); inputIndex++) { + makerInputs.add(atomicTx.getInput(inputIndex)); + atomicTxInputs.add(atomicTx.getInput(inputIndex)); + } + + atomicTx.clearInputs(); + atomicTxInputs.forEach(atomicTx::addInput); + + // Verify makerInputs are not mine + if (makerInputs.stream().anyMatch(this::isMine)) + failed("Maker input must not me mine"); + + var makerBsqInputAmount = + processModel.getBsqWalletService().getBsqInputAmount(makerInputs, processModel.getDaoFacade()); + + var expectedBsqTradeAmount = atomicModel.getBsqTradeAmount(); + var expectedMakerBsqOutAmount = isBuyer ? atomicModel.getBsqTradeAmount() : + makerBsqInputAmount - expectedBsqTradeAmount; + checkArgument(expectedMakerBsqOutAmount >= 0, "Maker BSQ input amount too low"); + var expectedTakerBsqOutAmount = atomicModel.getTakerBsqOutputAmount(); + var expectedBsqTradeFeeAmount = atomicModel.getBsqTradeFee(); + var expectedBtcTradeFee = atomicModel.getBtcTradeFee(); + var expectedTakerBtcAmount = atomicModel.getTakerBtcOutputAmount(); + + // Get BSQ and BTC input amounts + var bsqInputAmount = processModel.getBsqWalletService().getBsqInputAmount( + atomicTx.getInputs(), processModel.getDaoFacade()); + if (expectedBsqTradeFeeAmount + expectedTakerBsqOutAmount + expectedMakerBsqOutAmount != bsqInputAmount) + failed("Unexpected BSQ input amount"); + + var takerBtcOutputIndex = 0; + if (expectedMakerBsqOutAmount > 0) + takerBtcOutputIndex++; + if (expectedTakerBsqOutAmount > 0) { + takerBtcOutputIndex++; + // Verify taker BSQ output, always the output before taker BTC output + var takerBsqOutput = atomicTx.getOutput(takerBtcOutputIndex - 1); + var takerBsqOutputAddressInBtcFormat = Objects.requireNonNull( + takerBsqOutput.getAddressFromP2PKHScript(processModel.getBtcWalletService().getParams())); + var takerBsqOutputAddress = processModel.getBsqWalletService().getBsqFormatter(). + getBsqAddressStringFromAddress(takerBsqOutputAddressInBtcFormat); + if (!takerBsqOutputAddress.equals(atomicModel.getTakerBsqAddress())) + failed("Taker BSQ address mismatch"); + if (expectedTakerBsqOutAmount != takerBsqOutput.getValue().getValue()) + failed("Taker BSQ amount mismatch"); + } + + // Verify taker BTC output (vout index depends on the number of BSQ outputs, as calculated above) + var takerBtcOutput = atomicTx.getOutput(takerBtcOutputIndex); + var takerBtcOutputAddress = Objects.requireNonNull(takerBtcOutput.getAddressFromP2PKHScript( + processModel.getBtcWalletService().getParams())).toString(); + if (!takerBtcOutputAddress.equals(atomicModel.getTakerBtcAddress())) + failed("Taker BTC output address mismatch"); + if (expectedTakerBtcAmount != takerBtcOutput.getValue().getValue()) + failed("Taker BTC output amount mismatch"); + + if (expectedBtcTradeFee > 0) { + var tradeFeeOutput = atomicTx.getOutput(outputsSize - 1); + DonationAddressValidation.validateDonationAddress(tradeFeeOutput, atomicTx, processModel.getDaoFacade(), + processModel.getBtcWalletService()); + if (expectedBtcTradeFee != tradeFeeOutput.getValue().getValue()) + failed("Unexpected trade fee amount"); + } + + atomicModel.setVerifiedAtomicTx(atomicTx); + atomicModel.setAtomicTx(atomicTx.bitcoinSerialize()); + complete(); + } catch (Throwable t) { + failed(t); + } + } + + private boolean isMine(TransactionInput input) { + var result = new AtomicBoolean(false); + var walletServices = Arrays.asList(processModel.getBtcWalletService(), processModel.getBsqWalletService()); + + walletServices.forEach(walletService -> + walletService.getWallet().getWalletTransactions().forEach(tx -> { + if (input.getOutpoint().getHash().toString().equals(tx.getTransaction().getHashAsString())) { + var connectedOutput = tx.getTransaction().getOutput(input.getOutpoint().getIndex()); + if (walletService.isMine(connectedOutput)) + result.set(true); + } + }) + ); + return result.get(); + } +} diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 85aaeb422b6..211ca610e54 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -926,6 +926,8 @@ funds.locked.locked=Locked in multisig for trade with ID: {0} funds.tx.direction.sentTo=Sent to: funds.tx.direction.receivedWith=Received with: funds.tx.direction.genesisTx=From Genesis tx: +funds.tx.direction.atomicBuy=Bought BTC +funds.tx.direction.atomicSell=Sold BTC funds.tx.txFeePaymentForBsqTx=Miner fee for BSQ tx funds.tx.createOfferFee=Maker and tx fee: {0} funds.tx.takeOfferFee=Taker and tx fee: {0} @@ -956,7 +958,7 @@ funds.tx.dustAttackTx.popup=This transaction is sending a very small BTC amount other address as well (coin merge).\n\n\ To protect your privacy the Bisq wallet ignores such dust outputs for spending purposes and in the balance display. \ You can set the threshold amount when an output is considered dust in the settings. - +funds.tx.atomicTx=Atomic trade: {0} #################################################################### # Support @@ -2171,6 +2173,7 @@ dao.tx.type.enum.ASSET_LISTING_FEE=Asset listing fee dao.tx.type.enum.PROOF_OF_BURN=Proof of burn # suppress inspection "UnusedProperty" dao.tx.type.enum.IRREGULAR=Irregular +dao.tx.atomic=Atomic tx dao.tx.withdrawnFromWallet=BTC withdrawn from wallet dao.tx.issuanceFromCompReq=Compensation request/issuance @@ -2179,6 +2182,7 @@ dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance dao.tx.issuanceFromReimbursement=Reimbursement request/issuance dao.tx.issuanceFromReimbursement.tooltip=Reimbursement request which led to an issuance of new BSQ.\n\ Issuance date: {0} +dao.tx.atomicTrade=Atomic trade: {0} dao.proposal.create.missingBsqFunds=You don''t have sufficient BSQ funds for creating the proposal. If you have an \ unconfirmed BSQ transaction you need to wait for a blockchain confirmation because BSQ is validated only if it is \ included in a block.\n\ @@ -3227,6 +3231,8 @@ PROMPT_PAY=PromptPay ADVANCED_CASH=Advanced Cash # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT=Altcoins Instant +# suppress inspection "UnusedProperty" +ATOMIC=Atomic BSQ # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" @@ -3275,6 +3281,8 @@ PROMPT_PAY_SHORT=PromptPay ADVANCED_CASH_SHORT=Advanced Cash # suppress inspection "UnusedProperty" BLOCK_CHAINS_INSTANT_SHORT=Altcoins Instant +# suppress inspection "UnusedProperty" +ATOMIC_SHORT=Atomic BSQ # Deprecated: Cannot be deleted as it would break old trade history entries # suppress inspection "UnusedProperty" From d5560f8db7d8df1196660187f975f836ee51ec13 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 15:57:17 +0200 Subject: [PATCH 08/12] Add UI for atomic trade Show atomic trades as such in transaction views, both BTC and BSQ transaction view. --- desktop/src/main/java/bisq/desktop/bisq.css | 5 ++++ .../main/dao/wallet/tx/BsqTxListItem.java | 17 ++++++++++++- .../desktop/main/dao/wallet/tx/BsqTxView.java | 24 ++++++++++++++++--- .../transactions/TradableRepository.java | 2 +- .../transactions/TransactionAwareTrade.java | 7 +++++- .../transactions/TransactionsListItem.java | 15 ++++++++++-- .../main/offer/takeoffer/TakeOfferView.java | 3 +++ .../offer/takeoffer/TakeOfferViewModel.java | 23 +++++++++++++----- .../pendingtrades/PendingTradesViewModel.java | 1 - 9 files changed, 82 insertions(+), 15 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/bisq.css b/desktop/src/main/java/bisq/desktop/bisq.css index dc2e9ed7c86..7f9c18e04c2 100644 --- a/desktop/src/main/java/bisq/desktop/bisq.css +++ b/desktop/src/main/java/bisq/desktop/bisq.css @@ -1967,6 +1967,11 @@ textfield */ -fx-text-fill: -bs-color-green-3; } +.dao-tx-type-atomic-icon, +.dao-tx-type-atomic-icon:hover { + -fx-text-fill: -bs-color-blue-4; +} + .dao-accepted-icon { -fx-text-fill: -bs-color-primary; } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxListItem.java index 86c8cc47a8d..98c76f5b317 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxListItem.java @@ -18,6 +18,7 @@ package bisq.desktop.main.dao.wallet.tx; import bisq.desktop.components.TxConfidenceListItem; +import bisq.desktop.main.funds.transactions.TradableRepository; import bisq.core.btc.wallet.BsqWalletService; import bisq.core.btc.wallet.BtcWalletService; @@ -25,6 +26,7 @@ import bisq.core.dao.DaoFacade; import bisq.core.dao.state.model.blockchain.TxType; import bisq.core.locale.Res; +import bisq.core.trade.Trade; import bisq.core.util.coin.BsqFormatter; import org.bitcoinj.core.Coin; @@ -55,13 +57,15 @@ class BsqTxListItem extends TxConfidenceListItem { private boolean received; private boolean issuanceTx; + private String atomicTradeId; BsqTxListItem(Transaction transaction, BsqWalletService bsqWalletService, BtcWalletService btcWalletService, DaoFacade daoFacade, Date date, - BsqFormatter bsqFormatter) { + BsqFormatter bsqFormatter, + TradableRepository tradableRepository) { super(transaction, bsqWalletService); this.daoFacade = daoFacade; @@ -123,6 +127,8 @@ class BsqTxListItem extends TxConfidenceListItem { address = received ? receivedWithAddress : sendToAddress; else address = ""; + + setAtomic(tradableRepository); } public TxType getTxType() { @@ -134,5 +140,14 @@ public TxType getTxType() { public boolean isWithdrawalToBTCWallet() { return withdrawalToBTCWallet; } + + private void setAtomic (TradableRepository tradableRepository) { + var tradables = tradableRepository.getAll(); + tradables.forEach(tradable -> { + if (tradable instanceof Trade && txId.equals(((Trade) tradable).getAtomicTxId())) { + atomicTradeId = tradable.getShortId(); + } + }); + } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java index 9afcba9d50d..ba361f20257 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java @@ -25,6 +25,7 @@ import bisq.desktop.components.ExternalHyperlink; import bisq.desktop.components.HyperlinkWithIcon; import bisq.desktop.main.dao.wallet.BsqBalanceUtil; +import bisq.desktop.main.funds.transactions.TradableRepository; import bisq.desktop.util.DisplayUtils; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; @@ -96,6 +97,7 @@ public class BsqTxView extends ActivatableView implements BsqBal private final BtcWalletService btcWalletService; private final BsqBalanceUtil bsqBalanceUtil; private final Preferences preferences; + private final TradableRepository tradableRepository; private final ObservableList observableList = FXCollections.observableArrayList(); // Need to be DoubleProperty as we pass it as reference @@ -121,7 +123,8 @@ private BsqTxView(DaoFacade daoFacade, Preferences preferences, BtcWalletService btcWalletService, BsqBalanceUtil bsqBalanceUtil, - BsqFormatter bsqFormatter) { + BsqFormatter bsqFormatter, + TradableRepository tradableRepository) { this.daoFacade = daoFacade; this.daoStateService = daoStateService; this.bsqFormatter = bsqFormatter; @@ -129,6 +132,7 @@ private BsqTxView(DaoFacade daoFacade, this.preferences = preferences; this.btcWalletService = btcWalletService; this.bsqBalanceUtil = bsqBalanceUtil; + this.tradableRepository = tradableRepository; } @Override @@ -315,7 +319,8 @@ private void updateList() { daoFacade, // Use tx.getIncludedInBestChainAt() when available, otherwise use tx.getUpdateTime() transaction.getIncludedInBestChainAt() != null ? transaction.getIncludedInBestChainAt() : transaction.getUpdateTime(), - bsqFormatter); + bsqFormatter, + tradableRepository); }) .collect(Collectors.toList()); observableList.setAll(items); @@ -441,7 +446,14 @@ public void updateItem(final BsqTxListItem item, boolean empty) { String labelString = Res.get("dao.tx.type.enum." + txType.name()); Label label; if (item.getConfirmations() > 0 && isValidType(txType)) { - if (txType == TxType.COMPENSATION_REQUEST && + if (item.getAtomicTradeId() != null) { + if (field != null) + field.setOnAction(null); + + labelString = Res.get("dao.tx.atomicTrade", item.getAtomicTradeId()); + label = new AutoTooltipLabel(labelString); + setGraphic(label); + } else if (txType == TxType.COMPENSATION_REQUEST && daoFacade.isIssuanceTx(item.getTxId(), IssuanceType.COMPENSATION)) { if (field != null) field.setOnAction(null); @@ -685,6 +697,12 @@ public void updateItem(final BsqTxListItem item, boolean empty) { style = "dao-tx-type-unverified-icon"; break; } + // Atomic tx overrides other characteristics, such as trade fee or transfer BSQ + if (item.getAtomicTradeId() != null) { + awesomeIcon = AwesomeIcon.EXCHANGE; + style = "dao-tx-type-atomic-icon"; + toolTipText = Res.get("dao.tx.atomic"); + } Label label = FormBuilder.getIcon(awesomeIcon); label.getStyleClass().addAll("icon", style); label.setTooltip(new Tooltip(toolTipText)); diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java index f81fece8374..67a61a0f826 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TradableRepository.java @@ -48,7 +48,7 @@ public class TradableRepository { this.failedTradesManager = failedTradesManager; } - Set getAll() { + public Set getAll() { return ImmutableSet.builder() .addAll(openOfferManager.getObservableList()) .addAll(tradeManager.getTradableList()) diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java index f37a4057490..6fd512aaa0a 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionAwareTrade.java @@ -73,9 +73,10 @@ public boolean isRelatedToTransaction(Transaction transaction) { boolean isDisputedPayoutTx = isDisputedPayoutTx(txId); boolean isDelayedPayoutTx = isDelayedPayoutTx(txId); boolean isRefundPayoutTx = isRefundPayoutTx(txId); + boolean isAtomicTx = isAtomicTx(txId); return isTakerOfferFeeTx || isOfferFeeTx || isDepositTx || isPayoutTx || - isDisputedPayoutTx || isDelayedPayoutTx || isRefundPayoutTx; + isDisputedPayoutTx || isDelayedPayoutTx || isRefundPayoutTx || isAtomicTx; } private boolean isPayoutTx(String txId) { @@ -174,6 +175,10 @@ private boolean isRefundPayoutTx(String txId) { return isRefundTx.get() && isDisputeRelatedToThis.get(); } + private boolean isAtomicTx(String txId) { + return (txId.equals(trade.getAtomicTxId())); + } + @Override public Tradable asTradable() { return trade; diff --git a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java index 941bf57ec73..3c1e96e9aea 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/transactions/TransactionsListItem.java @@ -209,7 +209,12 @@ public void onTransactionConfidenceChanged(TransactionConfidence confidence) { } else if (tradable instanceof Trade) { Trade trade = (Trade) tradable; TransactionAwareTrade transactionAwareTrade = (TransactionAwareTrade) transactionAwareTradable; - if (trade.getTakerFeeTxId() != null && trade.getTakerFeeTxId().equals(txId)) { + if (isAtomic(trade)) { + direction = amountAsCoin.isPositive() ? Res.get("funds.tx.direction.atomicBuy") : + Res.get("funds.tx.direction.atomicSell"); + addressString = ""; + details = Res.get("funds.tx.atomicTx", tradeId); + } else if (trade.getTakerFeeTxId() != null && trade.getTakerFeeTxId().equals(txId)) { details = Res.get("funds.tx.takeOfferFee", tradeId); } else { Offer offer = trade.getOffer(); @@ -283,6 +288,10 @@ public void onTransactionConfidenceChanged(TransactionConfidence confidence) { } } + private boolean isAtomic(Trade trade) { + return trade.getAtomicTxId() != null && trade.getAtomicTxId().equals(txId); + } + public void cleanup() { btcWalletService.removeTxConfidenceListener(txConfidenceListener); } @@ -343,6 +352,8 @@ public String getNumConfirmations() { return String.valueOf(confirmations); } - public String getMemo() { return memo; } + public String getMemo() { + return memo; + } } diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java index c921ca34575..e74af578fcc 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java @@ -444,6 +444,9 @@ private void onTakeOffer() { } private void onShowPayFundsScreen() { + if (model.getOffer().getPaymentMethod().isAtomic()) + model.fundFromSavingsWallet(); + scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED); nextButton.setVisible(false); diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java index 685fb6dbe1b..21609f052ae 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferViewModel.java @@ -484,13 +484,16 @@ private void applyTradeErrorMessage(@Nullable String errorMessage) { } private void applyTradeState() { - if (trade.isDepositPublished()) { + if (trade.isAtomicBsqTrade() && trade.isPayoutPublished()) { + if (trade.getProcessModel().getAtomicModel().getVerifiedAtomicTx() != null) { + takeOfferSuccess(); + } else { + final String msg = "verified atomic tx must not be null."; + DevEnv.logErrorAndThrowIfDevMode(msg); + } + } else if (trade.isDepositPublished()) { if (trade.getDepositTx() != null) { - if (takeOfferSucceededHandler != null) - takeOfferSucceededHandler.run(); - - showTransactionPublishedScreen.set(true); - updateSpinnerInfo(); + takeOfferSuccess(); } else { final String msg = "trade.getDepositTx() must not be null."; DevEnv.logErrorAndThrowIfDevMode(msg); @@ -498,6 +501,14 @@ private void applyTradeState() { } } + private void takeOfferSuccess() { + if (takeOfferSucceededHandler != null) + takeOfferSucceededHandler.run(); + + showTransactionPublishedScreen.set(true); + updateSpinnerInfo(); + } + private void updateButtonDisableState() { boolean inputDataValid = isBtcInputValid(amount.get()).isValid && dataModel.isMinAmountLessOrEqualAmount() diff --git a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java index 09b23eb8d13..3495198a8d8 100644 --- a/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/portfolio/pendingtrades/PendingTradesViewModel.java @@ -34,7 +34,6 @@ import bisq.core.trade.Contract; import bisq.core.trade.Trade; import bisq.core.trade.closed.ClosedTradableManager; -import bisq.core.trade.messages.RefreshTradeStateRequest; import bisq.core.trade.messages.TraderSignedWitnessMessage; import bisq.core.user.User; import bisq.core.util.FormattingUtils; From 437273a692339c1cd92d5a3c28cd588def9f5f39 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 17:20:05 +0200 Subject: [PATCH 09/12] Move protocol failed texts to displaystrings --- .../maker/AtomicMakerSetupTxListener.java | 3 ++- .../maker/AtomicMakerVerifiesTakerInputs.java | 18 ++++++------- .../taker/AtomicTakerSetupTxListener.java | 3 ++- .../taker/AtomicTakerVerifiesAtomicTx.java | 27 +++++++++---------- .../resources/i18n/displayStrings.properties | 20 ++++++++++++++ 5 files changed, 46 insertions(+), 25 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java index 14f20215662..e369e7a1f0c 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java @@ -17,6 +17,7 @@ package bisq.core.trade.protocol.tasks.maker; +import bisq.core.locale.Res; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.AtomicSetupTxListener; @@ -53,7 +54,7 @@ protected void run() { walletService = processModel.getBsqWalletService(); myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getMakerBsqAddress()); } else { - failed("No maker address set"); + failed(Res.get("validation.protocol.noMakerAddress")); } super.run(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java index 28d163c529f..d11c403647f 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java @@ -18,6 +18,7 @@ package bisq.core.trade.protocol.tasks.maker; import bisq.core.btc.model.AddressEntry; +import bisq.core.locale.Res; import bisq.core.offer.Offer; import bisq.core.trade.Trade; import bisq.core.trade.messages.CreateAtomicTxRequest; @@ -59,8 +60,7 @@ protected void run() { checkArgument(processModel.getOffer().isMyOffer(processModel.getKeyRing()), "must process own offer"); var isBuyer = processModel.getOffer().isBuyOffer(); - if (!(processModel.getTradeMessage() instanceof CreateAtomicTxRequest)) - failed("Expected CreateAtomicTxRequest"); + checkArgument(processModel.getTradeMessage() instanceof CreateAtomicTxRequest); var message = (CreateAtomicTxRequest) processModel.getTradeMessage(); var atomicModel = checkNotNull(processModel.getAtomicModel(), "AtomicModel must not be null"); @@ -68,7 +68,7 @@ protected void run() { Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); if (message.getTradePrice() != atomicModel.getTradePrice()) - failed("Unexpected trade price"); + failed(Res.get("validation.protocol.badPrice")); trade.setTradePrice(message.getTradePrice()); processModel.getTradingPeer().setPubKeyRing(checkNotNull(message.getTakerPubKeyRing())); trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress()); @@ -76,16 +76,16 @@ protected void run() { if (message.getBsqTradeAmount() < atomicModel.getBsqMinTradeAmount() || message.getBsqTradeAmount() > atomicModel.getBsqMaxTradeAmount()) - failed("bsqTradeAmount not within range"); + failed(Res.get("validation.protocol.badBsqRange")); if (message.getBtcTradeAmount() < atomicModel.getBtcMinTradeAmount() || message.getBtcTradeAmount() > atomicModel.getBtcMaxTradeAmount()) - failed("btcTradeAmount not within range"); + failed(Res.get("validation.protocol.badBtcRange")); if (message.getTakerFee() != (message.isCurrencyForTakerFeeBtc() ? atomicModel.getBtcTradeFee() : atomicModel.getBsqTradeFee())) - failed("Taker fee mismatch"); + failed(Res.get("validation.protocol.badTakerFee")); var bsqAmount = atomicModel.bsqAmountFromVolume(trade.getTradeVolume()).orElse(null); if (bsqAmount == null || bsqAmount != message.getBsqTradeAmount()) - failed("Amounts don't match price"); + failed(Res.get("validation.protocol.badAmountVsPrice")); // TODO verify txFee is reasonable // Verify taker bsq address @@ -132,11 +132,11 @@ protected void run() { var bsqIn = takerBsqInputAmount + makerBsqInputAmount; var bsqOut = takerBsqOutputAmount + makerBsqOutputAmount; if (bsqIn != bsqOut + bsqTradeFee) - failed("BSQ in does not match BSQ out"); + failed(Res.get("validation.protocol.badBsqSum")); var btcIn = takerBtcInputAmount + makerBtcInputAmount + bsqTradeFee; var btcOut = takerBtcOutputAmount + makerBtcOutputAmount + btcTradeFeeAmount; if (btcIn != btcOut + txFee) - failed("BTC in does not match BTC out"); + failed(Res.get("validation.protocol.badBtcSum")); // Message data is verified as correct, update model with data from message atomicModel.setMakerBtcInputs(makerBtcInputs); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java index 53318fe450a..c6a88bafa7a 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java @@ -17,6 +17,7 @@ package bisq.core.trade.protocol.tasks.taker; +import bisq.core.locale.Res; import bisq.core.trade.Trade; import bisq.core.trade.protocol.tasks.AtomicSetupTxListener; @@ -53,7 +54,7 @@ protected void run() { walletService = processModel.getBsqWalletService(); myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getTakerBsqAddress()); } else { - failed("No maker address set"); + failed(Res.get("validation.protocol.missingMakerAddress")); } super.run(); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java index 03c2b1269e3..7b3aaab871b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java @@ -17,6 +17,7 @@ package bisq.core.trade.protocol.tasks.taker; +import bisq.core.locale.Res; import bisq.core.trade.DonationAddressValidation; import bisq.core.trade.Trade; import bisq.core.trade.messages.CreateAtomicTxResponse; @@ -72,9 +73,7 @@ protected void run() { checkArgument(!processModel.getOffer().isMyOffer(processModel.getKeyRing()), "must not take own offer"); var isBuyer = !processModel.getOffer().isBuyOffer(); - if (!(processModel.getTradeMessage() instanceof CreateAtomicTxResponse)) - failed("Expected CreateAtomicTxResponse"); - + checkArgument(processModel.getTradeMessage() instanceof CreateAtomicTxResponse); var serializedAtomicTx = ((CreateAtomicTxResponse) processModel.getTradeMessage()).getAtomicTx(); var atomicTx = processModel.getBtcWalletService().getTxFromSerializedTx(serializedAtomicTx); @@ -91,22 +90,22 @@ protected void run() { var verifiedInput = processModel.getBsqWalletService().verifyTransactionInput( atomicTx.getInput(inputIndex), rawInput); if (verifiedInput == null) - failed("Taker BSQ input mismatch"); + failed(Res.get("validation.protocol.badBsqInput")); atomicTxInputs.add(verifiedInput); inputIndex++; if (inputIndex > inputsSize) - failed("Not enough inputs"); + failed(Res.get("validation.protocol.missngInputs")); } if (atomicModel.getRawTakerBtcInputs() != null) { for (var rawInput : atomicModel.getRawTakerBtcInputs()) { var verifiedInput = processModel.getTradeWalletService().verifyTransactionInput( atomicTx.getInput(inputIndex), rawInput); if (verifiedInput == null) - failed("Taker BTC input mismatch"); + failed(Res.get("validation.protocol.badBtcInput")); atomicTxInputs.add(verifiedInput); inputIndex++; if (inputIndex > inputsSize) - failed("Not enough inputs"); + failed(Res.get("validation.protocol.missngInputs")); } } List makerInputs = new ArrayList<>(); @@ -120,7 +119,7 @@ protected void run() { // Verify makerInputs are not mine if (makerInputs.stream().anyMatch(this::isMine)) - failed("Maker input must not me mine"); + failed(Res.get("validation.protocol.badOwnerInput")); var makerBsqInputAmount = processModel.getBsqWalletService().getBsqInputAmount(makerInputs, processModel.getDaoFacade()); @@ -138,7 +137,7 @@ protected void run() { var bsqInputAmount = processModel.getBsqWalletService().getBsqInputAmount( atomicTx.getInputs(), processModel.getDaoFacade()); if (expectedBsqTradeFeeAmount + expectedTakerBsqOutAmount + expectedMakerBsqOutAmount != bsqInputAmount) - failed("Unexpected BSQ input amount"); + failed(Res.get("validation.protocol.badBsqInputAmount")); var takerBtcOutputIndex = 0; if (expectedMakerBsqOutAmount > 0) @@ -152,9 +151,9 @@ protected void run() { var takerBsqOutputAddress = processModel.getBsqWalletService().getBsqFormatter(). getBsqAddressStringFromAddress(takerBsqOutputAddressInBtcFormat); if (!takerBsqOutputAddress.equals(atomicModel.getTakerBsqAddress())) - failed("Taker BSQ address mismatch"); + failed(Res.get("validation.protocol.badTakerBsqAddress")); if (expectedTakerBsqOutAmount != takerBsqOutput.getValue().getValue()) - failed("Taker BSQ amount mismatch"); + failed(Res.get("validation.protocol.badTakerBsqAmount")); } // Verify taker BTC output (vout index depends on the number of BSQ outputs, as calculated above) @@ -162,16 +161,16 @@ protected void run() { var takerBtcOutputAddress = Objects.requireNonNull(takerBtcOutput.getAddressFromP2PKHScript( processModel.getBtcWalletService().getParams())).toString(); if (!takerBtcOutputAddress.equals(atomicModel.getTakerBtcAddress())) - failed("Taker BTC output address mismatch"); + failed(Res.get("validation.protocol.badTakerBtcAddress")); if (expectedTakerBtcAmount != takerBtcOutput.getValue().getValue()) - failed("Taker BTC output amount mismatch"); + failed("validation.protocol.badTakerBtcAmount"); if (expectedBtcTradeFee > 0) { var tradeFeeOutput = atomicTx.getOutput(outputsSize - 1); DonationAddressValidation.validateDonationAddress(tradeFeeOutput, atomicTx, processModel.getDaoFacade(), processModel.getBtcWalletService()); if (expectedBtcTradeFee != tradeFeeOutput.getValue().getValue()) - failed("Unexpected trade fee amount"); + failed(Res.get("validation.protocol.badTradeFeeAmount")); } atomicModel.setVerifiedAtomicTx(atomicTx); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 211ca610e54..2e3cb0fc208 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -3368,3 +3368,23 @@ validation.phone.tooManyDigits=Too many digits in {0} to be a valid phone number validation.phone.invalidDialingCode=Country dialing code in number {0} is invalid for country {1}. \ The correct dialing code is {2}. validation.invalidAddressList=Must be comma separated list of valid addresses + +validation.protocol.noMakerAddress=No maker address set +validation.protocol.badPrice=Unexpected trade price +validation.protocol.badBsqRange=BSQ trade amount not within range +validation.protocol.badBtcRange=BTC trade amount not within range +validation.protocol.badTakerFee=Taker fee mismatch +validation.protocol.badAmountVsPrice=Amounts don't match price +validation.protocol.badBsqSum=BSQ in does not match BSQ out +validation.protocol.badBtcSum=BTC in does not match BTC out +validation.protocol.missingMakerAddress=No maker address set +validation.protocol.badBsqInput=Taker BSQ input mismatch +validation.protocol.missngInputs=Not enough inputs +validation.protocol.badBtcInput=Taker BTC input mismatch +validation.protocol.badOwnerInput=Maker input must not me mine +validation.protocol.badBsqInputAmount=Unexpected BSQ input amount +validation.protocol.badTakerBsqAddress=Taker BSQ output address mismatch +validation.protocol.badTakerBsqAmount=Taker BSQ output amount mismatch +validation.protocol.badTakerBtcAddress=Taker BTC output address mismatch +validation.protocol.badTakerBtcAmount=Taker BTC output amount mismatch +validation.protocol.badTradeFeeAmount=Unexpected trade fee amount From f7b24bbcf331a1e687e44a4173e0075d5ecb6ecd Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 17 Aug 2020 18:42:28 +0200 Subject: [PATCH 10/12] Fix codacy complaints --- .../main/java/bisq/core/btc/wallet/BsqWalletService.java | 3 +-- .../java/bisq/core/trade/protocol/AtomicMakerProtocol.java | 2 +- .../src/main/java/bisq/core/trade/protocol/AtomicModel.java | 6 ------ .../protocol/tasks/maker/AtomicMakerSetupTxListener.java | 2 +- .../tasks/maker/AtomicMakerVerifiesTakerInputs.java | 2 -- .../protocol/tasks/taker/AtomicTakerSetupTxListener.java | 2 +- 6 files changed, 4 insertions(+), 13 deletions(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index be153bddeb3..8ef9e0b57f2 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -525,7 +525,7 @@ public Transaction signInputs(Transaction tx, List transaction return tx; } - private void signInput(Transaction tx, int i) throws TransactionVerificationException{ + private void signInput(Transaction tx, int i) throws TransactionVerificationException { TransactionInput txIn = tx.getInputs().get(i); TransactionOutput connectedOutput = txIn.getConnectedOutput(); if (connectedOutput != null && connectedOutput.isMine(wallet)) { @@ -765,7 +765,6 @@ public Tuple2 prepareAtomicBsqInputs(Coin requiredInput) thro } checkArgument(Restrictions.isAboveDust(change)); -// printTx("takerPreparesAtomicBsqInputs", dummyTx); return new Tuple2<>(dummyTx, change); } diff --git a/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java b/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java index 94b1fb5bcb8..2b61e4ad595 100644 --- a/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java +++ b/core/src/main/java/bisq/core/trade/protocol/AtomicMakerProtocol.java @@ -28,7 +28,7 @@ import bisq.common.handlers.ErrorMessageHandler; -public interface AtomicMakerProtocol{ +public interface AtomicMakerProtocol { default void handleTakeAtomicRequest(CreateAtomicTxRequest tradeMessage, NodeAddress sender, ErrorMessageHandler errorMessageHandler) { diff --git a/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java b/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java index de6aebd4922..c62f621dca4 100644 --- a/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java +++ b/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java @@ -109,12 +109,6 @@ public void initFromTrade(Trade trade) { bsqAmountFromVolume(trade.getTradeVolume()).ifPresent(this::setBsqTradeAmount); bsqAmountFromVolume(offer.getVolume()).ifPresent(this::setBsqMaxTradeAmount); bsqAmountFromVolume(offer.getMinVolume()).ifPresent(this::setBsqMinTradeAmount); -// setBsqTradeAmount( -// (Objects.requireNonNull(trade.getTradeVolume()).getMonetary().getValue() + 500_000) / 1_000_000); -// setBsqMaxTradeAmount( -// (Objects.requireNonNull(offer.getVolume()).getMonetary().getValue() + 500_000) / 1_000_000); -// setBsqMinTradeAmount( -// (Objects.requireNonNull(offer.getMinVolume()).getMonetary().getValue() + 500_000) / 1_000_000); // Atomic trades only allow fixed prices var price = offer.isUseMarketBasedPrice() ? 0 : Objects.requireNonNull(offer.getPrice()).getValue(); setTradePrice(price); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java index e369e7a1f0c..f2012fa873b 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java @@ -49,7 +49,7 @@ protected void run() { if (atomicModel.getMakerBtcAddress() != null) { walletService = processModel.getBtcWalletService(); myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getMakerBtcAddress()); - } else if (atomicModel.getMakerBsqAddress() != null){ + } else if (atomicModel.getMakerBsqAddress() != null) { // Listen to BSQ address walletService = processModel.getBsqWalletService(); myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getMakerBsqAddress()); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java index d11c403647f..35342fe4369 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java @@ -19,7 +19,6 @@ import bisq.core.btc.model.AddressEntry; import bisq.core.locale.Res; -import bisq.core.offer.Offer; import bisq.core.trade.Trade; import bisq.core.trade.messages.CreateAtomicTxRequest; import bisq.core.trade.protocol.tasks.TradeTask; @@ -66,7 +65,6 @@ protected void run() { var atomicModel = checkNotNull(processModel.getAtomicModel(), "AtomicModel must not be null"); atomicModel.updateFromCreateAtomicTxRequest(message); - Offer offer = checkNotNull(trade.getOffer(), "Offer must not be null"); if (message.getTradePrice() != atomicModel.getTradePrice()) failed(Res.get("validation.protocol.badPrice")); trade.setTradePrice(message.getTradePrice()); diff --git a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java index c6a88bafa7a..4216292f5b5 100644 --- a/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java @@ -49,7 +49,7 @@ protected void run() { if (atomicModel.getTakerBtcAddress() != null) { walletService = processModel.getBtcWalletService(); myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getTakerBtcAddress()); - } else if (atomicModel.getTakerBsqAddress() != null){ + } else if (atomicModel.getTakerBsqAddress() != null) { // Listen to BSQ address walletService = processModel.getBsqWalletService(); myAddress = Address.fromBase58(walletService.getParams(), atomicModel.getTakerBsqAddress()); From 12f51250de940d4858758ab911276a02eb0f1386 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Wed, 19 Aug 2020 13:14:16 +0200 Subject: [PATCH 11/12] Less logging --- core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index 8ef9e0b57f2..dd184ef048e 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -521,7 +521,7 @@ public Transaction signInputs(Transaction tx, List transaction checkWalletConsistency(wallet); verifyTransaction(tx); - printTx("BSQ wallet: Signed Tx", tx); + // printTx("BSQ wallet: Signed Tx", tx); return tx; } From aedea6cfe6f3c53edf5dbe9d20a3a7bf225dd7f3 Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 24 Aug 2020 14:34:56 +0200 Subject: [PATCH 12/12] Allow zero BSQ change for taker --- core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java index dd184ef048e..51efb4323b6 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BsqWalletService.java @@ -763,7 +763,7 @@ public Tuple2 prepareAtomicBsqInputs(Coin requiredInput) thro log.error("Missing funds in takerPreparesAtomicBsqInputs"); throw new InsufficientBsqException(e.missing); } - checkArgument(Restrictions.isAboveDust(change)); + checkArgument(change.isZero() || Restrictions.isAboveDust(change)); return new Tuple2<>(dummyTx, change); }