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, 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..51efb4323b6 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,9 @@ 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 @Getter @@ -127,7 +135,8 @@ public BsqWalletService(WalletsSetup walletsSetup, UnconfirmedBsqChangeOutputListService unconfirmedBsqChangeOutputListService, Preferences preferences, FeeService feeService, - DaoKillSwitch daoKillSwitch) { + DaoKillSwitch daoKillSwitch, + BsqFormatter bsqFormatter) { super(walletsSetup, preferences, feeService); @@ -137,6 +146,7 @@ public BsqWalletService(WalletsSetup walletsSetup, this.daoStateService = daoStateService; this.unconfirmedBsqChangeOutputListService = unconfirmedBsqChangeOutputListService; this.daoKillSwitch = daoKillSwitch; + this.bsqFormatter = bsqFormatter; walletsSetup.addSetupCompletedHandler(() -> { wallet = walletsSetup.getBsqWallet(); @@ -485,21 +495,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 +508,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 @@ -710,6 +745,30 @@ 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(change.isZero() || Restrictions.isAboveDust(change)); + + return new Tuple2<>(dummyTx, change); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Blind vote tx /////////////////////////////////////////////////////////////////////////////////////////// @@ -811,4 +870,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 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/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/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/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/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 31bb507e44f..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; @@ -65,8 +67,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 +93,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; @@ -120,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; @@ -161,6 +161,7 @@ public TradeManager(User user, BtcWalletService btcWalletService, BsqWalletService bsqWalletService, TradeWalletService tradeWalletService, + WalletsManager walletsManager, OpenOfferManager openOfferManager, ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, @@ -183,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; @@ -208,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); } }); @@ -310,7 +314,7 @@ private void initPendingTrades() { trade.getDelayedPayoutTx(), daoFacade, btcWalletService); - } catch (DelayedPayoutTxValidation.DonationAddressException | + } catch (DonationAddressValidation.DonationAddressException | DelayedPayoutTxValidation.InvalidTxException | DelayedPayoutTxValidation.InvalidLockTimeException | DelayedPayoutTxValidation.MissingDelayedPayoutTxException | @@ -424,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/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/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..2b61e4ad595 --- /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..c62f621dca4 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/AtomicModel.java @@ -0,0 +1,138 @@ +/* + * 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); + // 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/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/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..f2012fa873b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerSetupTxListener.java @@ -0,0 +1,67 @@ +/* + * 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.locale.Res; +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(Res.get("validation.protocol.noMakerAddress")); + } + + 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..35342fe4369 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/maker/AtomicMakerVerifiesTakerInputs.java @@ -0,0 +1,152 @@ +/* + * 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.locale.Res; +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(); + + checkArgument(processModel.getTradeMessage() instanceof CreateAtomicTxRequest); + + var message = (CreateAtomicTxRequest) processModel.getTradeMessage(); + var atomicModel = checkNotNull(processModel.getAtomicModel(), "AtomicModel must not be null"); + atomicModel.updateFromCreateAtomicTxRequest(message); + + if (message.getTradePrice() != atomicModel.getTradePrice()) + failed(Res.get("validation.protocol.badPrice")); + 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(Res.get("validation.protocol.badBsqRange")); + if (message.getBtcTradeAmount() < atomicModel.getBtcMinTradeAmount() || + message.getBtcTradeAmount() > atomicModel.getBtcMaxTradeAmount()) + failed(Res.get("validation.protocol.badBtcRange")); + if (message.getTakerFee() != + (message.isCurrencyForTakerFeeBtc() ? atomicModel.getBtcTradeFee() : atomicModel.getBsqTradeFee())) + failed(Res.get("validation.protocol.badTakerFee")); + var bsqAmount = atomicModel.bsqAmountFromVolume(trade.getTradeVolume()).orElse(null); + if (bsqAmount == null || bsqAmount != message.getBsqTradeAmount()) + failed(Res.get("validation.protocol.badAmountVsPrice")); + // 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(Res.get("validation.protocol.badBsqSum")); + var btcIn = takerBtcInputAmount + makerBtcInputAmount + bsqTradeFee; + var btcOut = takerBtcOutputAmount + makerBtcOutputAmount + btcTradeFeeAmount; + if (btcIn != btcOut + txFee) + failed(Res.get("validation.protocol.badBtcSum")); + + // 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..4216292f5b5 --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerSetupTxListener.java @@ -0,0 +1,67 @@ +/* + * 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.locale.Res; +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(Res.get("validation.protocol.missingMakerAddress")); + } + + 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..7b3aaab871b --- /dev/null +++ b/core/src/main/java/bisq/core/trade/protocol/tasks/taker/AtomicTakerVerifiesAtomicTx.java @@ -0,0 +1,199 @@ +/* + * 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.locale.Res; +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(); + + checkArgument(processModel.getTradeMessage() instanceof 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(Res.get("validation.protocol.badBsqInput")); + atomicTxInputs.add(verifiedInput); + inputIndex++; + if (inputIndex > inputsSize) + 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(Res.get("validation.protocol.badBtcInput")); + atomicTxInputs.add(verifiedInput); + inputIndex++; + if (inputIndex > inputsSize) + failed(Res.get("validation.protocol.missngInputs")); + } + } + 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(Res.get("validation.protocol.badOwnerInput")); + + 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(Res.get("validation.protocol.badBsqInputAmount")); + + 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(Res.get("validation.protocol.badTakerBsqAddress")); + if (expectedTakerBsqOutAmount != takerBsqOutput.getValue().getValue()) + failed(Res.get("validation.protocol.badTakerBsqAmount")); + } + + // 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(Res.get("validation.protocol.badTakerBtcAddress")); + if (expectedTakerBtcAmount != takerBtcOutput.getValue().getValue()) + 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(Res.get("validation.protocol.badTradeFeeAmount")); + } + + 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/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/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 85aaeb422b6..2e3cb0fc208 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" @@ -3360,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 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/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/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/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/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; 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) { 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..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 { @@ -911,6 +942,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 +1068,10 @@ message InstantCryptoCurrencyAccountPayload { string address = 1; } +message AtomicAccountPayload { + string generic_string = 1; +} + message FasterPaymentsAccountPayload { string sort_code = 1; string account_nr = 2; @@ -1385,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 {