refactor payout protocol to work with or without updated multisig

This commit is contained in:
woodser 2022-03-31 14:23:58 -04:00
parent bb95b4b1d6
commit 32070fbafb
50 changed files with 1026 additions and 899 deletions

View file

@ -15,7 +15,7 @@ haveno:
./gradlew build
# build haveno without tests
no-tests:
skip-tests:
./gradlew build -x test
# quick build desktop and daemon apps without tests

View file

@ -37,7 +37,7 @@ import org.junit.jupiter.api.TestMethodOrder;
import static bisq.cli.TableFormat.formatBalancesTbls;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED;
import static bisq.core.trade.Trade.Phase.FIAT_SENT;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.State.*;
import static java.lang.String.format;
@ -170,8 +170,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
continue;
} else {
assertEquals(OFFER_FEE_PAID.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG)
.setPhase(FIAT_SENT)
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG)
.setPhase(PAYMENT_SENT)
.setFiatSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Alice's view after confirming fiat payment sent", trade);
@ -190,8 +190,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
var trade = bobClient.getTrade(tradeId);
Predicate<TradeInfo> tradeStateAndPhaseCorrect = (t) ->
t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name())
&& (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name()));
t.getState().equals(SELLER_RECEIVED_PAYMENT_INITIATED_MSG.name())
&& (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name()));
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
if (!tradeStateAndPhaseCorrect.test(trade)) {

View file

@ -38,7 +38,7 @@ import static bisq.apitest.config.ApiTestConfig.BTC;
import static bisq.cli.TableFormat.formatBalancesTbls;
import static bisq.core.btc.wallet.Restrictions.getDefaultBuyerSecurityDepositAsPercent;
import static bisq.core.trade.Trade.Phase.DEPOSIT_CONFIRMED;
import static bisq.core.trade.Trade.Phase.FIAT_SENT;
import static bisq.core.trade.Trade.Phase.PAYMENT_SENT;
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
import static bisq.core.trade.Trade.Phase.WITHDRAWN;
import static bisq.core.trade.Trade.State.*;
@ -173,8 +173,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
} else {
// Note: offer.state == available
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG)
.setPhase(FIAT_SENT)
EXPECTED_PROTOCOL_STATUS.setState(BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG)
.setPhase(PAYMENT_SENT)
.setFiatSent(true);
verifyExpectedProtocolStatus(trade);
logTrade(log, testInfo, "Bob's view after confirming fiat payment sent", trade);
@ -193,8 +193,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
var trade = aliceClient.getTrade(tradeId);
Predicate<TradeInfo> tradeStateAndPhaseCorrect = (t) ->
t.getState().equals(SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG.name())
&& (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(FIAT_SENT.name()));
t.getState().equals(SELLER_RECEIVED_PAYMENT_INITIATED_MSG.name())
&& (t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name()));
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
if (!tradeStateAndPhaseCorrect.test(trade)) {
log.warn("INVALID_PHASE for Alice's trade {} in STATE={} PHASE={}, cannot confirm payment received yet.",

View file

@ -13,18 +13,26 @@ import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.model.XmrAddressEntryList;
import bisq.core.btc.setup.MoneroWalletRpcManager;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.offer.Offer;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.trade.TradeUtils;
import bisq.core.util.ParsingUtils;
import com.google.common.util.concurrent.Service.State;
import com.google.inject.name.Named;
import common.utils.JsonUtils;
import java.io.File;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@ -36,8 +44,12 @@ import monero.common.MoneroRpcConnection;
import monero.common.MoneroUtils;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroNetworkType;
import monero.daemon.model.MoneroOutput;
import monero.daemon.model.MoneroSubmitTxResult;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.MoneroWalletRpc;
import monero.wallet.model.MoneroCheckTx;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroOutputWallet;
import monero.wallet.model.MoneroSubaddress;
@ -217,6 +229,140 @@ public class XmrWalletService {
}
}
/**
* Create the reserve tx and freeze its inputs. The deposit amount is returned
* to the sender's payout address. Additional funds are reserved to allow
* fluctuations in the mining fee.
*
* @param tradeFee is the trade fee
* @param depositAmount the amount needed for the trade minus the trade fee
* @return a transaction to reserve a trade
*/
public MoneroTxWallet createReserveTx(BigInteger tradeFee, String returnAddress, BigInteger depositAmount) {
MoneroWallet wallet = getWallet();
synchronized (wallet) {
// get expected mining fee
MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
.addDestination(returnAddress, depositAmount));
BigInteger miningFee = miningFeeTx.getFee();
// create reserve tx
MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
.addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit?
// freeze inputs
for (MoneroOutput input : reserveTx.getInputs()) {
wallet.freezeOutput(input.getKeyImage().getHex());
}
return reserveTx;
}
}
/**
* Create the multisig deposit tx and freeze its inputs.
*
* @return MoneroTxWallet the multisig deposit tx
*/
public MoneroTxWallet createDepositTx(Trade trade) {
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee());
Offer offer = trade.getProcessModel().getOffer();
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit());
String multisigAddress = trade.getProcessModel().getMultisigAddress();
MoneroWallet wallet = getWallet();
synchronized (wallet) {
// create deposit tx
MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
.addDestination(multisigAddress, depositAmount));
// freeze deposit inputs
for (MoneroOutput input : depositTx.getInputs()) {
wallet.freezeOutput(input.getKeyImage().getHex());
}
return depositTx;
}
}
/**
* Verify a reserve or deposit transaction used during trading.
* Checks double spends, deposit amount and destination, trade fee, and mining fee.
* The transaction is submitted but not relayed to the pool then flushed.
*
* @param depositAddress is the expected destination address for the deposit amount
* @param depositAmount is the expected amount deposited to multisig
* @param tradeFee is the expected fee for trading
* @param txHash is the transaction hash
* @param txHex is the transaction hex
* @param txKey is the transaction key
* @param keyImages are expected key images of inputs, ignored if null
* @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase
*/
public void verifyTradeTx(String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean miningFeePadding) {
boolean submittedToPool = false;
MoneroDaemon daemon = getDaemon();
MoneroWallet wallet = getWallet();
try {
// get tx from daemon
MoneroTx tx = daemon.getTx(txHash);
// if tx is not submitted, submit but do not relay
if (tx == null) {
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
submittedToPool = true;
tx = daemon.getTx(txHash);
} else if (tx.isRelayed()) {
throw new RuntimeException("Trade tx must not be relayed");
}
// verify reserved key images
if (keyImages != null) {
Set<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images");
}
// verify the unlock height
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee
String feeAddress = TradeUtils.FEE_ADDRESS;
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee");
if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount());
// verify mining fee
BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate
BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee
tx = daemon.getTx(txHash);
if (tx.getFee().compareTo(feeThreshold) < 0) {
throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee());
}
// verify deposit amount
check = wallet.checkTxKey(txHash, txKey, depositAddress);
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
BigInteger depositThreshold = depositAmount;
if (miningFeePadding) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee)
if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount());
} finally {
// flush tx from pool if we added it
if (submittedToPool) daemon.flushTxPool(txHash);
}
}
public void shutDown() {
closeAllWallets();
}

View file

@ -76,12 +76,12 @@ public class TradeEvents {
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
break;
case FIAT_SENT:
case PAYMENT_SENT:
// We only notify the seller
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getSellerPubKeyRing()))
msg = Res.get("account.notifications.trade.message.msg.started", shortId);
break;
case FIAT_RECEIVED:
case PAYMENT_RECEIVED:
break;
case PAYOUT_PUBLISHED:
// We only notify the buyer

View file

@ -664,9 +664,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
Offer offer = new Offer(request.getOfferPayload());
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(offer.getDirection() == OfferPayload.Direction.BUY ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
TradeUtils.processTradeTx(
xmrWalletService.getDaemon(),
xmrWalletService.getWallet(),
xmrWalletService.verifyTradeTx(
request.getPayoutAddress(),
depositAmount,
tradeFee,

View file

@ -22,13 +22,11 @@ import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.Offer;
import bisq.core.offer.placeoffer.PlaceOfferModel;
import bisq.core.trade.TradeUtils;
import bisq.core.util.ParsingUtils;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import monero.daemon.model.MoneroOutput;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
@ -45,26 +43,22 @@ public class MakerReservesTradeFunds extends Task<PlaceOfferModel> {
try {
runInterceptHook();
// synchronize on wallet to reserve key images
synchronized (model.getXmrWalletService().getWallet()) {
// freeze trade funds and get reserve tx
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(makerFee, returnAddress, depositAmount);
// create transaction to reserve trade
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger makerFee = ParsingUtils.coinToAtomicUnits(offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(model.getReservedFundsForOffer());
MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), offer.getId(), makerFee, returnAddress, depositAmount);
// collect reserved key images // TODO (woodser): switch to proof of reserve?
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// collect reserved key images // TODO (woodser): switch to proof of reserve?
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save offer state
// TODO (woodser): persist
model.setReserveTx(reserveTx);
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field
complete();
}
// save offer state
// TODO (woodser): persist
model.setReserveTx(reserveTx);
offer.getOfferPayload().setReserveTxKeyImages(reservedKeyImages);
offer.setOfferFeePaymentTxId(reserveTx.getHash()); // TODO (woodser): don't use this field
complete();
} catch (Throwable t) {
offer.setErrorMessage("An error occurred.\n" +
"Error message:\n"

View file

@ -38,7 +38,8 @@ import bisq.core.support.dispute.messages.OpenNewDisputeMessage;
import bisq.core.support.dispute.messages.PeerOpenedDisputeMessage;
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.PaymentSentMessage;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
import bisq.core.trade.messages.DelayedPayoutTxSignatureResponse;
import bisq.core.trade.messages.DepositRequest;
@ -52,7 +53,7 @@ import bisq.core.trade.messages.InputsForDepositTxResponse;
import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage;
import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage;
import bisq.core.trade.messages.PaymentAccountPayloadRequest;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.PeerPublishedDelayedPayoutTxMessage;
import bisq.core.trade.messages.RefreshTradeStateRequest;
import bisq.core.trade.messages.SignContractRequest;
@ -182,11 +183,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
case DEPOSIT_TX_AND_DELAYED_PAYOUT_TX_MESSAGE:
return DepositTxAndDelayedPayoutTxMessage.fromProto(proto.getDepositTxAndDelayedPayoutTxMessage(), messageVersion);
case COUNTER_CURRENCY_TRANSFER_STARTED_MESSAGE:
return CounterCurrencyTransferStartedMessage.fromProto(proto.getCounterCurrencyTransferStartedMessage(), messageVersion);
case PAYMENT_SENT_MESSAGE:
return PaymentSentMessage.fromProto(proto.getPaymentSentMessage(), messageVersion);
case PAYMENT_RECEIVED_MESSAGE:
return PaymentReceivedMessage.fromProto(proto.getPaymentReceivedMessage(), messageVersion);
case PAYOUT_TX_PUBLISHED_MESSAGE:
return PayoutTxPublishedMessage.fromProto(proto.getPayoutTxPublishedMessage(), messageVersion);
case PEER_PUBLISHED_DELAYED_PAYOUT_TX_MESSAGE:
return PeerPublishedDelayedPayoutTxMessage.fromProto(proto.getPeerPublishedDelayedPayoutTxMessage(), messageVersion);
case TRADER_SIGNED_WITNESS_MESSAGE:

View file

@ -326,7 +326,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
// update arbitrator's multisig wallet
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
multisigWallet.importMultisigHex(Arrays.asList(openNewDisputeMessage.getUpdatedMultisigHex()));
multisigWallet.importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex());
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
// close multisig wallet

View file

@ -380,7 +380,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// update multisig wallet
if (xmrWalletService.multisigWalletExists(tradeId)) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
multisigWallet.importMultisigHex(Arrays.asList(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex()));
multisigWallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex());
MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
xmrWalletService.closeMultisigWallet(tradeId);
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
@ -434,7 +434,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
// update arbitrator's multisig wallet with co-signer's multisig hex
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
try {
multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
multisigWallet.importMultisigHex(request.getUpdatedMultisigHex());
} catch (Exception e) {
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
return;
@ -550,7 +550,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
if (!expectedLoserAmount.equals(actualLoserAmount)) throw new RuntimeException("Unexpected loser payout: " + expectedLoserAmount + " vs " + actualLoserAmount);
// update multisig wallet from arbitrator
multisigWallet.importMultisigHex(Arrays.asList(disputeResult.getArbitratorUpdatedMultisigHex()));
multisigWallet.importMultisigHex(disputeResult.getArbitratorUpdatedMultisigHex());
// sign arbitrator-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);

View file

@ -17,6 +17,7 @@
package bisq.core.trade;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.locale.CurrencyUtil;
import bisq.core.monetary.Price;
@ -35,6 +36,7 @@ import bisq.core.trade.protocol.ProcessModelServiceProvider;
import bisq.core.trade.protocol.TradeListener;
import bisq.core.trade.protocol.TradingPeer;
import bisq.core.trade.txproof.AssetTxProofResult;
import bisq.core.util.ParsingUtils;
import bisq.core.util.VolumeUtil;
import bisq.network.p2p.AckMessage;
import bisq.network.p2p.NodeAddress;
@ -43,10 +45,9 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.proto.ProtoUtil;
import bisq.common.taskrunner.Model;
import bisq.common.util.Utilities;
import com.google.common.base.Preconditions;
import com.google.protobuf.ByteString;
import com.google.protobuf.Message;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.Transaction;
@ -61,7 +62,7 @@ import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
@ -85,6 +86,10 @@ import monero.common.MoneroError;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxSet;
import monero.wallet.model.MoneroTxWallet;
import monero.wallet.model.MoneroWalletListener;
@ -148,21 +153,21 @@ public abstract class Trade implements Tradable, Model {
DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN(Phase.DEPOSIT_CONFIRMED),
// #################### Phase FIAT_SENT
BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED(Phase.FIAT_SENT),
BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
// #################### Phase PAYMENT_SENT
BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED(Phase.PAYMENT_SENT),
BUYER_SENT_PAYMENT_INITIATED_MSG(Phase.PAYMENT_SENT),
BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG(Phase.PAYMENT_SENT),
BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG(Phase.PAYMENT_SENT),
BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG(Phase.PAYMENT_SENT),
SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG(Phase.FIAT_SENT),
SELLER_RECEIVED_PAYMENT_INITIATED_MSG(Phase.PAYMENT_SENT),
// #################### Phase FIAT_RECEIVED
// #################### Phase PAYMENT_RECEIVED
// note that this state can also be triggered by auto confirmation feature
SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT(Phase.FIAT_RECEIVED),
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT(Phase.PAYMENT_RECEIVED),
// #################### Phase PAYOUT_PUBLISHED
SELLER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED),
SELLER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED), // TODO (woodser): this enum is over used, like during arbitration
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
@ -172,6 +177,7 @@ public abstract class Trade implements Tradable, Model {
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
// 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),
BUYER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED),
// #################### Phase WITHDRAWN
@ -212,8 +218,8 @@ public abstract class Trade implements Tradable, Model {
TAKER_FEE_PUBLISHED, // TODO (woodser): remove unused phases
DEPOSIT_PUBLISHED,
DEPOSIT_CONFIRMED, // TODO (woodser): rename to or add DEPOSIT_UNLOCKED
FIAT_SENT,
FIAT_RECEIVED,
PAYMENT_SENT,
PAYMENT_RECEIVED,
PAYOUT_PUBLISHED,
WITHDRAWN;
@ -699,6 +705,157 @@ public abstract class Trade implements Tradable, Model {
else throw new RuntimeException("Unknown trade type: " + this.getClass().getName());
}
/**
* Create a contract based on the current state.
*
* @param trade is the trade to create the contract from
* @return the contract
*/
public Contract createContract() {
boolean isBuyerMakerAndSellerTaker = getOffer().getDirection() == Direction.BUY;
Contract contract = new Contract(
getOffer().getOfferPayload(),
checkNotNull(getTradeAmount()).value,
getTradePrice().getValue(),
isBuyerMakerAndSellerTaker ? getMakerNodeAddress() : getTakerNodeAddress(), // buyer node address // TODO (woodser): use maker and taker node address instead of buyer and seller node address for consistency
isBuyerMakerAndSellerTaker ? getTakerNodeAddress() : getMakerNodeAddress(), // seller node address
getArbitratorNodeAddress(),
isBuyerMakerAndSellerTaker,
this instanceof MakerTrade ? processModel.getAccountId() : getMaker().getAccountId(), // maker account id
this instanceof TakerTrade ? processModel.getAccountId() : getTaker().getAccountId(), // taker account id
checkNotNull(this instanceof MakerTrade ? processModel.getPaymentAccountPayload(this).getPaymentMethodId() : getOffer().getOfferPayload().getPaymentMethodId()), // maker payment method id
checkNotNull(this instanceof TakerTrade ? processModel.getPaymentAccountPayload(this).getPaymentMethodId() : getTaker().getPaymentMethodId()), // taker payment method id
this instanceof MakerTrade ? processModel.getPaymentAccountPayload(this).getHash() : getMaker().getPaymentAccountPayloadHash(), // maker payment account payload hash
this instanceof TakerTrade ? processModel.getPaymentAccountPayload(this).getHash() : getTaker().getPaymentAccountPayloadHash(), // maker payment account payload hash
getMakerPubKeyRing(),
getTakerPubKeyRing(),
this instanceof MakerTrade ? xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : getMaker().getPayoutAddressString(), // maker payout address
this instanceof TakerTrade ? xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : getTaker().getPayoutAddressString(), // taker payout address
getLockTime(),
getMaker().getDepositTxHash(),
getTaker().getDepositTxHash()
);
return contract;
}
/**
* Create the payout tx.
*
* @return MoneroTxWallet the payout tx when the trade is successfully completed
*/
public MoneroTxWallet createPayoutTx() {
// gather relevant info
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(this.getId());
String sellerPayoutAddress = this.getSeller().getPayoutAddressString();
String buyerPayoutAddress = this.getBuyer().getPayoutAddressString();
Preconditions.checkNotNull(sellerPayoutAddress, "Seller payout address must not be null");
Preconditions.checkNotNull(buyerPayoutAddress, "Buyer payout address must not be null");
BigInteger sellerDepositAmount = multisigWallet.getTx(this.getSeller().getDepositTxHash()).getIncomingAmount();
BigInteger buyerDepositAmount = multisigWallet.getTx(this.getBuyer().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(this.getTradeAmount());
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
// create transaction to get fee estimate
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Cannot create payout tx because multisig import is needed");
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)))
.setRelay(false)
);
// attempt to create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10 of fee until tx is successful
try {
numAttempts++;
payoutTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount
.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))
.setRelay(false));
} catch (MoneroError e) {
// exception expected
}
}
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
return payoutTx;
}
/**
* Verify and sign a payout tx.
*
* @param payoutTxHex is the payout tx hex to verify
* @return String the signed payout tx hex
*/
public void verifySignAndPublishPayoutTx(String payoutTxHex) {
log.info("Verifying payout tx");
// gather relevant info
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(getId());
Contract contract = getContract();
BigInteger sellerDepositAmount = multisigWallet.getTx(getSeller().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs this.getDepositTxId() necessary or avoidable?
BigInteger buyerDepositAmount = multisigWallet.getTx(getBuyer().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(getTradeAmount());
// parse payout tx
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(payoutTxHex));
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad payout tx"); // TODO (woodser): test nack
MoneroTxWallet payoutTx = parsedTxSet.getTxs().get(0);
// verify payout tx has exactly 2 destinations
if (payoutTx.getOutgoingTransfer() == null || payoutTx.getOutgoingTransfer().getDestinations() == null || payoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Payout tx does not have exactly two destinations");
// get buyer and seller destinations (order not preserved)
boolean buyerFirst = payoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1);
MoneroDestination sellerPayoutDestination = payoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
// verify payout addresses
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
// verify change address is multisig's primary address
if (!payoutTx.getChangeAmount().equals(BigInteger.ZERO) && !payoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount
if (!payoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(payoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// verify buyer destination amount is deposit amount + this amount - 1/2 tx costs
BigInteger txCost = payoutTx.getFee().add(payoutTx.getChangeAmount());
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
// verify seller destination amount is deposit amount - this amount - 1/2 tx costs
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
// sign payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing payout tx");
String signedPayoutTxHex = result.getSignedMultisigTxHex();
// submit payout tx
multisigWallet.submitMultisigTxHex(signedPayoutTxHex);
walletService.closeMultisigWallet(getId());
// update trade state
this.getSelf().setPayoutTxHex(signedPayoutTxHex);
this.setPayoutTx(parsedTxSet.getTxs().get(0));
this.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash());
this.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
}
/**
* Listen for deposit transactions to unlock and then apply the transactions.
*
@ -822,32 +979,6 @@ public abstract class Trade implements Tradable, Model {
this.delayedPayoutTxBytes = delayedPayoutTxBytes;
}
// @Nullable
// public Transaction getDelayedPayoutTx() {
// return getDelayedPayoutTx(processModel.getBtcWalletService());
// }
//
// // If called from a not initialized trade (or a closed or failed trade)
// // we need to pass the xmrWalletService
// @Nullable
// public Transaction getDelayedPayoutTx(XmrWalletService xmrWalletService) {
// if (delayedPayoutTx == null) {
// if (xmrWalletService == null) {
// log.warn("xmrWalletService is null. You might call that method before the tradeManager has " +
// "initialized all trades");
// return null;
// }
//
// if (delayedPayoutTxBytes == null) {
// log.warn("delayedPayoutTxBytes are null");
// return null;
// }
//
// delayedPayoutTx = xmrWalletService.getTxFromSerializedTx(delayedPayoutTxBytes);
// }
// return delayedPayoutTx;
// }
public void addAndPersistChatMessage(ChatMessage chatMessage) {
if (!chatMessages.contains(chatMessage)) {
chatMessages.add(chatMessage);
@ -1163,11 +1294,11 @@ public abstract class Trade implements Tradable, Model {
}
public boolean isFiatSent() {
return getState().getPhase().ordinal() >= Phase.FIAT_SENT.ordinal();
return getState().getPhase().ordinal() >= Phase.PAYMENT_SENT.ordinal();
}
public boolean isFiatReceived() {
return getState().getPhase().ordinal() >= Phase.FIAT_RECEIVED.ordinal();
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal();
}
public boolean isPayoutPublished() {

View file

@ -17,33 +17,16 @@
package bisq.core.trade;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.crypto.KeyRing;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.util.Tuple2;
import bisq.common.util.Utilities;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.offer.OfferPayload;
import bisq.core.offer.OfferPayload.Direction;
import bisq.core.support.dispute.mediation.mediator.Mediator;
import bisq.core.trade.messages.InitTradeRequest;
import common.utils.JsonUtils;
import java.math.BigInteger;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import monero.daemon.MoneroDaemon;
import monero.daemon.model.MoneroOutput;
import monero.daemon.model.MoneroSubmitTxResult;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroCheckTx;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet;
/**
* Collection of utilities for trading.
@ -136,162 +119,6 @@ public class TradeUtils {
}
}
/**
* Create a transaction to reserve a trade and freeze its funds. The deposit
* amount is returned to the sender's payout address. Additional funds are
* reserved to allow fluctuations in the mining fee.
*
* @param xmrWalletService
* @param offerId
* @param tradeFee
* @param depositAmount
* @return a transaction to reserve a trade
*/
public static MoneroTxWallet reserveTradeFunds(XmrWalletService xmrWalletService, String offerId, BigInteger tradeFee, String returnAddress, BigInteger depositAmount) {
// get expected mining fee
MoneroWallet wallet = xmrWalletService.getWallet();
MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
.addDestination(returnAddress, depositAmount));
BigInteger miningFee = miningFeeTx.getFee();
// create reserve tx
MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
.addDestination(returnAddress, depositAmount.add(miningFee.multiply(BigInteger.valueOf(3l))))); // add thrice the mining fee // TODO (woodser): really require more funds on top of security deposit?
// freeze trade funds
for (MoneroOutput input : reserveTx.getInputs()) {
wallet.freezeOutput(input.getKeyImage().getHex());
}
return reserveTx;
}
/**
* Create a transaction to deposit funds to the multisig wallet.
*
* @param xmrWalletService
* @param tradeFee
* @param destinationAddress
* @param depositAddress
* @return MoneroTxWallet
*/
public static MoneroTxWallet createDepositTx(XmrWalletService xmrWalletService, BigInteger tradeFee, String depositAddress, BigInteger depositAmount) {
return xmrWalletService.getWallet().createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(TradeUtils.FEE_ADDRESS, tradeFee)
.addDestination(depositAddress, depositAmount));
}
/**
* Process a reserve or deposit transaction used during trading.
* Checks double spends, deposit amount and destination, trade fee, and mining fee.
* The transaction is submitted but not relayed to the pool then flushed.
*
* @param daemon is the Monero daemon to check for double spends
* @param wallet is the Monero wallet to verify the tx
* @param depositAddress is the expected destination address for the deposit amount
* @param depositAmount is the expected amount deposited to multisig
* @param tradeFee is the expected fee for trading
* @param txHash is the transaction hash
* @param txHex is the transaction hex
* @param txKey is the transaction key
* @param keyImages are expected key images of inputs, ignored if null
* @param miningFeePadding verifies depositAmount has additional funds to cover mining fee increase
*/
public static void processTradeTx(MoneroDaemon daemon, MoneroWallet wallet, String depositAddress, BigInteger depositAmount, BigInteger tradeFee, String txHash, String txHex, String txKey, List<String> keyImages, boolean miningFeePadding) {
boolean submittedToPool = false;
try {
// get tx from daemon
MoneroTx tx = daemon.getTx(txHash);
// if tx is not submitted, submit but do not relay
if (tx == null) {
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
submittedToPool = true;
tx = daemon.getTx(txHash);
} else if (tx.isRelayed()) {
throw new RuntimeException("Trade tx must not be relayed");
}
// verify reserved key images
if (keyImages != null) {
Set<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Reserve tx's inputs do not match claimed key images");
}
// verify the unlock height
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee
String feeAddress = TradeUtils.FEE_ADDRESS;
MoneroCheckTx check = wallet.checkTxKey(txHash, txKey, feeAddress);
if (!check.isGood()) throw new RuntimeException("Invalid proof of trade fee");
if (!check.getReceivedAmount().equals(tradeFee)) throw new RuntimeException("Trade fee is incorrect amount, expected " + tradeFee + " but was " + check.getReceivedAmount());
// verify mining fee
BigInteger feeEstimate = daemon.getFeeEstimate().multiply(BigInteger.valueOf(txHex.length())); // TODO (woodser): fee estimates are too high, use more accurate estimate
BigInteger feeThreshold = feeEstimate.multiply(BigInteger.valueOf(1l)).divide(BigInteger.valueOf(2l)); // must be at least 50% of estimated fee
tx = daemon.getTx(txHash);
if (tx.getFee().compareTo(feeThreshold) < 0) {
throw new RuntimeException("Mining fee is not enough, needed " + feeThreshold + " but was " + tx.getFee());
}
// verify deposit amount
check = wallet.checkTxKey(txHash, txKey, depositAddress);
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
BigInteger depositThreshold = depositAmount;
if (miningFeePadding) depositThreshold = depositThreshold.add(feeThreshold.multiply(BigInteger.valueOf(3l))); // prove reserve of at least deposit amount + (3 * min mining fee)
if (check.getReceivedAmount().compareTo(depositThreshold) < 0) throw new RuntimeException("Deposit amount is not enough, needed " + depositThreshold + " but was " + check.getReceivedAmount());
} finally {
// flush tx from pool if we added it
if (submittedToPool) daemon.flushTxPool(txHash);
}
}
/**
* Create a contract from a trade.
*
* TODO (woodser): refactor/reduce trade, process model, and trading peer models
*
* @param trade is the trade to create the contract from
* @return the contract
*/
public static Contract createContract(Trade trade) {
boolean isBuyerMakerAndSellerTaker = trade.getOffer().getDirection() == Direction.BUY;
Contract contract = new Contract(
trade.getOffer().getOfferPayload(),
checkNotNull(trade.getTradeAmount()).value,
trade.getTradePrice().getValue(),
isBuyerMakerAndSellerTaker ? trade.getMakerNodeAddress() : trade.getTakerNodeAddress(), // buyer node address // TODO (woodser): use maker and taker node address instead of buyer and seller node address for consistency
isBuyerMakerAndSellerTaker ? trade.getTakerNodeAddress() : trade.getMakerNodeAddress(), // seller node address
trade.getArbitratorNodeAddress(),
isBuyerMakerAndSellerTaker,
trade instanceof MakerTrade ? trade.getProcessModel().getAccountId() : trade.getMaker().getAccountId(), // maker account id
trade instanceof TakerTrade ? trade.getProcessModel().getAccountId() : trade.getTaker().getAccountId(), // taker account id
checkNotNull(trade instanceof MakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getPaymentMethodId() : trade.getOffer().getOfferPayload().getPaymentMethodId()), // maker payment method id
checkNotNull(trade instanceof TakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getPaymentMethodId() : trade.getTaker().getPaymentMethodId()), // taker payment method id
trade instanceof MakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getHash() : trade.getMaker().getPaymentAccountPayloadHash(), // maker payment account payload hash
trade instanceof TakerTrade ? trade.getProcessModel().getPaymentAccountPayload(trade).getHash() : trade.getTaker().getPaymentAccountPayloadHash(), // maker payment account payload hash
trade.getMakerPubKeyRing(),
trade.getTakerPubKeyRing(),
trade instanceof MakerTrade ? trade.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : trade.getMaker().getPayoutAddressString(), // maker payout address
trade instanceof TakerTrade ? trade.getXmrWalletService().getAddressEntry(trade.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString() : trade.getTaker().getPayoutAddressString(), // taker payout address
trade.getLockTime(),
trade.getMaker().getDepositTxHash(),
trade.getTaker().getDepositTxHash()
);
return contract;
}
// TODO (woodser): remove the following utitilites?
// Returns <MULTI_SIG, TRADE_PAYOUT> if both are AVAILABLE, otherwise null

View file

@ -0,0 +1,110 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.messages;
import bisq.core.account.sign.SignedWitness;
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
import bisq.common.proto.network.NetworkEnvelope;
import java.util.Optional;
import java.util.UUID;
import lombok.EqualsAndHashCode;
import lombok.Value;
import lombok.extern.slf4j.Slf4j;
import javax.annotation.Nullable;
@Slf4j
@EqualsAndHashCode(callSuper = true)
@Value
public final class PaymentReceivedMessage extends TradeMailboxMessage {
private final NodeAddress senderNodeAddress;
private final String payoutTxHex;
// Added in v1.4.0
@Nullable
private final SignedWitness signedWitness;
public PaymentReceivedMessage(String tradeId,
NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness,
String signedPayoutTxHex) {
this(tradeId,
senderNodeAddress,
signedWitness,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
signedPayoutTxHex);
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private PaymentReceivedMessage(String tradeId,
NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness,
String uid,
String messageVersion,
String signedPayoutTxHex) {
super(messageVersion, tradeId, uid);
this.senderNodeAddress = senderNodeAddress;
this.signedWitness = signedWitness;
this.payoutTxHex = signedPayoutTxHex;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.PaymentReceivedMessage.Builder builder = protobuf.PaymentReceivedMessage.newBuilder()
.setTradeId(tradeId)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid)
.setPayoutTxHex(payoutTxHex);
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
}
public static NetworkEnvelope fromProto(protobuf.PaymentReceivedMessage proto, String messageVersion) {
// There is no method to check for a nullable non-primitive data type object but we know that all fields
// are empty/null, so we check for the signature to see if we got a valid signedWitness.
protobuf.SignedWitness protoSignedWitness = proto.getSignedWitness();
SignedWitness signedWitness = !protoSignedWitness.getSignature().isEmpty() ?
SignedWitness.fromProto(protoSignedWitness) :
null;
return new PaymentReceivedMessage(proto.getTradeId(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
signedWitness,
proto.getUid(),
messageVersion,
proto.getPayoutTxHex());
}
@Override
public String toString() {
return "SellerReceivedPaymentMessage{" +
"\n senderNodeAddress=" + senderNodeAddress +
",\n signedWitness=" + signedWitness +
",\n payoutTxHex=" + payoutTxHex +
"\n} " + super.toString();
}
}

View file

@ -31,33 +31,38 @@ import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Value
public final class CounterCurrencyTransferStartedMessage extends TradeMailboxMessage {
public final class PaymentSentMessage extends TradeMailboxMessage {
private final String buyerPayoutAddress;
private final NodeAddress senderNodeAddress;
private final String buyerPayoutTxSigned;
@Nullable
private final String counterCurrencyTxId;
@Nullable
private final String payoutTxHex;
@Nullable
private final String updatedMultisigHex;
// Added after v1.3.7
// We use that for the XMR txKey but want to keep it generic to be flexible for data of other payment methods or assets.
@Nullable
private String counterCurrencyExtraData;
public CounterCurrencyTransferStartedMessage(String tradeId,
public PaymentSentMessage(String tradeId,
String buyerPayoutAddress,
NodeAddress senderNodeAddress,
String buyerPayoutTxSigned,
@Nullable String counterCurrencyTxId,
@Nullable String counterCurrencyExtraData,
String uid) {
String uid,
String signedPayoutTxHex,
String updatedMultisigHex) {
this(tradeId,
buyerPayoutAddress,
senderNodeAddress,
buyerPayoutTxSigned,
counterCurrencyTxId,
counterCurrencyExtraData,
uid,
Version.getP2PMessageVersion());
Version.getP2PMessageVersion(),
signedPayoutTxHex,
updatedMultisigHex);
}
@ -65,59 +70,64 @@ public final class CounterCurrencyTransferStartedMessage extends TradeMailboxMes
// PROTO BUFFER
///////////////////////////////////////////////////////////////////////////////////////////
private CounterCurrencyTransferStartedMessage(String tradeId,
private PaymentSentMessage(String tradeId,
String buyerPayoutAddress,
NodeAddress senderNodeAddress,
String buyerPayoutTxSigned,
@Nullable String counterCurrencyTxId,
@Nullable String counterCurrencyExtraData,
String uid,
String messageVersion) {
String messageVersion,
@Nullable String signedPayoutTxHex,
@Nullable String updatedMultisigHex) {
super(messageVersion, tradeId, uid);
this.buyerPayoutAddress = buyerPayoutAddress;
this.senderNodeAddress = senderNodeAddress;
this.buyerPayoutTxSigned = buyerPayoutTxSigned;
this.counterCurrencyTxId = counterCurrencyTxId;
this.counterCurrencyExtraData = counterCurrencyExtraData;
this.payoutTxHex = signedPayoutTxHex;
this.updatedMultisigHex = updatedMultisigHex;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
final protobuf.CounterCurrencyTransferStartedMessage.Builder builder = protobuf.CounterCurrencyTransferStartedMessage.newBuilder();
final protobuf.PaymentSentMessage.Builder builder = protobuf.PaymentSentMessage.newBuilder();
builder.setTradeId(tradeId)
.setBuyerPayoutAddress(buyerPayoutAddress)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setBuyerPayoutTxSigned(buyerPayoutTxSigned)
.setUid(uid);
Optional.ofNullable(counterCurrencyTxId).ifPresent(e -> builder.setCounterCurrencyTxId(counterCurrencyTxId));
Optional.ofNullable(counterCurrencyExtraData).ifPresent(e -> builder.setCounterCurrencyExtraData(counterCurrencyExtraData));
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
return getNetworkEnvelopeBuilder().setCounterCurrencyTransferStartedMessage(builder).build();
return getNetworkEnvelopeBuilder().setPaymentSentMessage(builder).build();
}
public static CounterCurrencyTransferStartedMessage fromProto(protobuf.CounterCurrencyTransferStartedMessage proto,
public static PaymentSentMessage fromProto(protobuf.PaymentSentMessage proto,
String messageVersion) {
return new CounterCurrencyTransferStartedMessage(proto.getTradeId(),
return new PaymentSentMessage(proto.getTradeId(),
proto.getBuyerPayoutAddress(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
proto.getBuyerPayoutTxSigned(),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyTxId()),
ProtoUtil.stringOrNullFromProto(proto.getCounterCurrencyExtraData()),
proto.getUid(),
messageVersion);
messageVersion,
ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()),
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
}
@Override
public String toString() {
return "CounterCurrencyTransferStartedMessage{" +
return "PaymentSentMessage{" +
"\n buyerPayoutAddress='" + buyerPayoutAddress + '\'' +
",\n senderNodeAddress=" + senderNodeAddress +
",\n counterCurrencyTxId=" + counterCurrencyTxId +
",\n counterCurrencyExtraData=" + counterCurrencyExtraData +
",\n uid='" + uid + '\'' +
",\n buyerPayoutTxSigned=" + buyerPayoutTxSigned +
",\n payoutTxHex=" + payoutTxHex +
",\n updatedMultisigHex=" + updatedMultisigHex +
"\n} " + super.toString();
}
}

View file

@ -37,23 +37,23 @@ import javax.annotation.Nullable;
@EqualsAndHashCode(callSuper = true)
@Value
public final class PayoutTxPublishedMessage extends TradeMailboxMessage {
private final String signedMultisigTxHex;
private final NodeAddress senderNodeAddress;
private final String payoutTxHex;
// Added in v1.4.0
@Nullable
private final SignedWitness signedWitness;
public PayoutTxPublishedMessage(String tradeId,
String signedMultisigTxHex,
NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness) {
@Nullable SignedWitness signedWitness,
String payoutTxHex) {
this(tradeId,
signedMultisigTxHex,
senderNodeAddress,
signedWitness,
UUID.randomUUID().toString(),
Version.getP2PMessageVersion());
Version.getP2PMessageVersion(),
payoutTxHex);
}
@ -62,24 +62,24 @@ public final class PayoutTxPublishedMessage extends TradeMailboxMessage {
///////////////////////////////////////////////////////////////////////////////////////////
private PayoutTxPublishedMessage(String tradeId,
String signedMultisigTxHex,
NodeAddress senderNodeAddress,
@Nullable SignedWitness signedWitness,
String uid,
String messageVersion) {
String messageVersion,
String payoutTxHex) {
super(messageVersion, tradeId, uid);
this.signedMultisigTxHex = signedMultisigTxHex;
this.senderNodeAddress = senderNodeAddress;
this.signedWitness = signedWitness;
this.payoutTxHex = payoutTxHex;
}
@Override
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
protobuf.PayoutTxPublishedMessage.Builder builder = protobuf.PayoutTxPublishedMessage.newBuilder()
.setTradeId(tradeId)
.setSignedMultisigTxHex(signedMultisigTxHex)
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
.setUid(uid);
.setUid(uid)
.setPayoutTxHex(payoutTxHex);
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
return getNetworkEnvelopeBuilder().setPayoutTxPublishedMessage(builder).build();
}
@ -92,19 +92,19 @@ public final class PayoutTxPublishedMessage extends TradeMailboxMessage {
SignedWitness.fromProto(protoSignedWitness) :
null;
return new PayoutTxPublishedMessage(proto.getTradeId(),
proto.getSignedMultisigTxHex(),
NodeAddress.fromProto(proto.getSenderNodeAddress()),
signedWitness,
proto.getUid(),
messageVersion);
messageVersion,
proto.getPayoutTxHex());
}
@Override
public String toString() {
return "PayoutTxPublishedMessage{" +
"\n signedMultisigTxHex=" + signedMultisigTxHex +
",\n senderNodeAddress=" + senderNodeAddress +
"\n senderNodeAddress=" + senderNodeAddress +
",\n signedWitness=" + signedWitness +
",\n payoutTxHex=" + payoutTxHex +
"\n} " + super.toString();
}
}

View file

@ -24,7 +24,7 @@ import lombok.Value;
/**
* Not used anymore since v1.4.0
* We do the re-sending of the payment sent message via the BuyerSendCounterCurrencyTransferStartedMessage task on the
* We do the re-sending of the payment sent message via the BuyerSendPaymentSentMessage task on the
* buyer side, so seller do not need to do anything interactively.
*/
@Deprecated

View file

@ -26,7 +26,7 @@ import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.messages.PaymentAccountPayloadRequest;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
@ -279,7 +279,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
super.handle(message, peer);
}

View file

@ -29,7 +29,7 @@ import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InputsForDepositTxResponse;
import bisq.core.trade.messages.PaymentAccountPayloadRequest;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.messages.TradeMessage;
@ -296,7 +296,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
super.handle(message, peer);
}

View file

@ -21,15 +21,15 @@ import bisq.core.trade.BuyerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.DelayedPayoutTxSignatureRequest;
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.UpdateMultisigWithTradingPeer;
import bisq.core.trade.protocol.tasks.buyer.BuyerCreateAndSignPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerPreparesPaymentSentMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessesPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsPaymentSentMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener;
import bisq.network.p2p.NodeAddress;
@ -63,16 +63,16 @@ public abstract class BuyerProtocol extends DisputeProtocol {
.setup(tasks(SetupDepositTxsListener.class))
.executeTasks();
given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED)
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
.with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSetupPayoutTxListener.class)) // TODO (woodser): mirror deposit listener setup?
.executeTasks();
given(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.FIAT_RECEIVED)
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG,
Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG)
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG,
Trade.State.BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG)
.with(BuyerEvent.STARTUP))
.setup(tasks(BuyerSendCounterCurrencyTransferStartedMessage.class))
.setup(tasks(BuyerSendsPaymentSentMessage.class))
.executeTasks();
}
@ -82,8 +82,8 @@ public abstract class BuyerProtocol extends DisputeProtocol {
if (message instanceof DepositTxAndDelayedPayoutTxMessage) {
handle((DepositTxAndDelayedPayoutTxMessage) message, peer);
} else if (message instanceof PayoutTxPublishedMessage) {
handle((PayoutTxPublishedMessage) message, peer);
} else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peer);
}
}
@ -131,25 +131,28 @@ public abstract class BuyerProtocol extends DisputeProtocol {
synchronized (trade) { // TODO (woodser): UpdateMultisigWithTradingPeer sends UpdateMultisigRequest and waits for UpdateMultisigResponse which is new thread, so synchronized (trade) in subsequent pipeline blocks forever if we hold on with countdown latch in this function
System.out.println("BuyerProtocol.onPaymentStarted() has the lock!!!");
BuyerEvent event = BuyerEvent.PAYMENT_SENT;
CountDownLatch latch = new CountDownLatch(1);
expect(phase(Trade.Phase.DEPOSIT_CONFIRMED)
.with(event)
.preCondition(trade.confirmPermitted()))
.setup(tasks(ApplyFilter.class,
getVerifyPeersFeePaymentClass(),
UpdateMultisigWithTradingPeer.class,
BuyerCreateAndSignPayoutTx.class,
BuyerSetupPayoutTxListener.class,
BuyerSendCounterCurrencyTransferStartedMessage.class)
//UpdateMultisigWithTradingPeer.class, // TODO (woodser): can use this to test protocol with updated multisig from peer. peer should attempt to send updated multisig hex earlier as part of protocol. cannot use with countdown latch because response comes back in a separate thread and blocks on trade
BuyerPreparesPaymentSentMessage.class,
//BuyerSetupPayoutTxListener.class,
BuyerSendsPaymentSentMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
resultHandler.handleResult();
handleTaskRunnerSuccess(event);
},
(errorMessage) -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED))
.run(() -> trade.setState(Trade.State.BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED))
.executeTasks();
}
}
@ -158,18 +161,18 @@ public abstract class BuyerProtocol extends DisputeProtocol {
// Incoming message Payout tx
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(PayoutTxPublishedMessage message, NodeAddress peer) {
log.info("BuyerProtocol.handle(PayoutTxPublishedMessage)");
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
log.info("BuyerProtocol.handle(SellerReceivedPaymentMessage)");
synchronized (trade) {
processModel.setTradeMessage(message);
processModel.setTempTradingPeerNodeAddress(peer);
CountDownLatch latch = new CountDownLatch(1);
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
.with(message)
.from(peer))
.setup(tasks(
getVerifyPeersFeePaymentClass(),
BuyerProcessPayoutTxPublishedMessage.class)
BuyerProcessesPaymentReceivedMessage.class)
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
@ -200,8 +203,8 @@ public abstract class BuyerProtocol extends DisputeProtocol {
handle((DelayedPayoutTxSignatureRequest) message, peer);
} else if (message instanceof DepositTxAndDelayedPayoutTxMessage) {
handle((DepositTxAndDelayedPayoutTxMessage) message, peer);
} else if (message instanceof PayoutTxPublishedMessage) {
handle((PayoutTxPublishedMessage) message, peer);
} else if (message instanceof PaymentReceivedMessage) {
handle((PaymentReceivedMessage) message, peer);
}
}

View file

@ -62,8 +62,8 @@ public abstract class DisputeProtocol extends TradeProtocol {
public void onAcceptMediationResult(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED;
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED,
Trade.Phase.FIAT_SENT,
Trade.Phase.FIAT_RECEIVED)
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(event)
.preCondition(trade.getTradingPeer().getMediatedPayoutTxSignature() == null,
() -> errorMessageHandler.handleErrorMessage("We have received already the signature from the peer."))
@ -89,8 +89,8 @@ public abstract class DisputeProtocol extends TradeProtocol {
public void onFinalizeMediationResultPayout(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
DisputeEvent event = DisputeEvent.MEDIATION_RESULT_ACCEPTED;
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED,
Trade.Phase.FIAT_SENT,
Trade.Phase.FIAT_RECEIVED)
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(event)
.preCondition(trade.getPayoutTx() == null,
() -> errorMessageHandler.handleErrorMessage("Payout tx is already published.")))
@ -118,8 +118,8 @@ public abstract class DisputeProtocol extends TradeProtocol {
protected void handle(MediatedPayoutTxSignatureMessage message, NodeAddress peer) {
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED,
Trade.Phase.FIAT_SENT,
Trade.Phase.FIAT_RECEIVED)
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(message)
.from(peer))
.setup(tasks(ProcessMediatedPayoutSignatureMessage.class))
@ -128,8 +128,8 @@ public abstract class DisputeProtocol extends TradeProtocol {
protected void handle(MediatedPayoutTxPublishedMessage message, NodeAddress peer) {
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED,
Trade.Phase.FIAT_SENT,
Trade.Phase.FIAT_RECEIVED)
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(message)
.from(peer))
.setup(tasks(ProcessMediatedPayoutTxPublishedMessage.class))
@ -168,8 +168,8 @@ public abstract class DisputeProtocol extends TradeProtocol {
private void handle(PeerPublishedDelayedPayoutTxMessage message, NodeAddress peer) {
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED,
Trade.Phase.FIAT_SENT,
Trade.Phase.FIAT_RECEIVED)
Trade.Phase.PAYMENT_SENT,
Trade.Phase.PAYMENT_RECEIVED)
.with(message)
.from(peer))
.setup(tasks(ProcessPeerPublishedDelayedPayoutTxMessage.class))

View file

@ -289,7 +289,7 @@ public class FluentProtocol {
return Result.VALID.info(info);
} else {
String info = MessageFormat.format("We received a {0} but we are are not in the expected phase.\n" +
"This can be an expected case if we get a repeated CounterCurrencyTransferStartedMessage " +
"This can be an expected case if we get a repeated PaymentSentMessage " +
"after we have already received one as the peer re-sends that message at each startup.\n" +
"Expected phases={1},\nTrade phase={2},\nTrade state= {3},\ntradeId={4}",
trigger,

View file

@ -186,12 +186,10 @@ public class ProcessModel implements Model, PersistablePayload {
@Getter
@Setter
private boolean multisigSetupComplete; // TODO (woodser): redundant with multisigAddress existing, remove
@Nullable
transient private MoneroTxWallet buyerSignedPayoutTx; // TODO (woodser): remove
// We want to indicate the user the state of the message delivery of the
// CounterCurrencyTransferStartedMessage. As well we do an automatic re-send in case it was not ACKed yet.
// PaymentSentMessage. As well we do an automatic re-send in case it was not ACKed yet.
// To enable that even after restart we persist the state.
@Setter
private ObjectProperty<MessageState> paymentStartedMessageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
@ -421,13 +419,4 @@ public class ProcessModel implements Model, PersistablePayload {
public KeyRing getKeyRing() {
return provider.getKeyRing();
}
public void setBuyerSignedPayoutTx(MoneroTxWallet buyerSignedPayoutTx) {
this.buyerSignedPayoutTx = buyerSignedPayoutTx;
}
@Nullable
public MoneroTxWallet getBuyerSignedPayoutTx() {
return buyerSignedPayoutTx;
}
}

View file

@ -21,7 +21,7 @@ package bisq.core.trade.protocol;
import bisq.core.trade.SellerAsMakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.DepositTxMessage;
import bisq.core.trade.messages.InitMultisigRequest;
@ -322,7 +322,7 @@ public class SellerAsMakerProtocol extends SellerProtocol implements MakerProtoc
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) {
protected void handle(PaymentSentMessage message, NodeAddress peer) {
super.handle(message, peer);
}
}

View file

@ -23,7 +23,7 @@ import bisq.core.trade.SellerAsTakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InputsForDepositTxResponse;
@ -279,7 +279,7 @@ public class SellerAsTakerProtocol extends SellerProtocol implements TakerProtoc
// We keep the handler here in as well to make it more transparent which messages we expect
@Override
protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) {
protected void handle(PaymentSentMessage message, NodeAddress peer) {
super.handle(message, peer);
}

View file

@ -19,15 +19,15 @@ package bisq.core.trade.protocol;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.BuyerProtocol.BuyerEvent;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerProcessesPaymentSentMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSendsPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerPreparesPaymentReceivedMessage;
import bisq.network.p2p.NodeAddress;
import java.util.concurrent.CountDownLatch;
@ -66,8 +66,8 @@ public abstract class SellerProtocol extends DisputeProtocol {
public void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
super.onMailboxMessage(message, peerNodeAddress);
if (message instanceof CounterCurrencyTransferStartedMessage) {
handle((CounterCurrencyTransferStartedMessage) message, peerNodeAddress);
if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peerNodeAddress);
}
}
@ -76,39 +76,31 @@ public abstract class SellerProtocol extends DisputeProtocol {
// Incoming message when buyer has clicked payment started button
///////////////////////////////////////////////////////////////////////////////////////////
protected void handle(CounterCurrencyTransferStartedMessage message, NodeAddress peer) {
log.info("SellerProtocol.handle(CounterCurrencyTransferStartedMessage)");
protected void handle(PaymentSentMessage message, NodeAddress peer) {
log.info("SellerProtocol.handle(PaymentSentMessage)");
// We are more tolerant with expected phase and allow also DEPOSIT_PUBLISHED as it can be the case
// that the wallet is still syncing and so the DEPOSIT_CONFIRMED state to yet triggered when we received
// a mailbox message with CounterCurrencyTransferStartedMessage.
// a mailbox message with PaymentSentMessage.
// TODO A better fix would be to add a listener for the wallet sync state and process
// the mailbox msg once wallet is ready and trade state set.
synchronized (trade) {
CountDownLatch latch = new CountDownLatch(1);
//CountDownLatch latch = new CountDownLatch(1); // TODO: apply latch countdown
expect(anyPhase(Trade.Phase.DEPOSIT_CONFIRMED, Trade.Phase.DEPOSIT_PUBLISHED)
.with(message)
.from(peer)
.preCondition(trade.getPayoutTx() == null,
() -> {
log.warn("We received a CounterCurrencyTransferStartedMessage but we have already created the payout tx " +
log.warn("We received a PaymentSentMessage but we have already created the payout tx " +
"so we ignore the message. This can happen if the ACK message to the peer did not " +
"arrive and the peer repeats sending us the message. We send another ACK msg.");
sendAckMessage(peer, message, true, null);
removeMailboxMessageAfterProcessing(message);
}))
.setup(tasks(
SellerProcessCounterCurrencyTransferStartedMessage.class,
SellerProcessesPaymentSentMessage.class,
ApplyFilter.class,
getVerifyPeersFeePaymentClass())
.using(new TradeTaskRunner(trade,
() -> {
latch.countDown();
},
(errorMessage) -> {
latch.countDown();
})))
getVerifyPeersFeePaymentClass()))
.executeTasks();
wait(latch);
}
}
@ -119,40 +111,34 @@ public abstract class SellerProtocol extends DisputeProtocol {
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
log.info("SellerProtocol.onPaymentReceived()");
synchronized (trade) {
CountDownLatch latch = new CountDownLatch(1);
SellerEvent event = SellerEvent.PAYMENT_RECEIVED;
expect(anyPhase(Trade.Phase.FIAT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
// CountDownLatch latch = new CountDownLatch(1); // TODO (woodser): user countdown latch, but freezes legacy app
expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYOUT_PUBLISHED)
.with(event)
.preCondition(trade.confirmPermitted()))
.setup(tasks(
ApplyFilter.class,
getVerifyPeersFeePaymentClass(),
SellerSignAndPublishPayoutTx.class,
// SellerSignAndFinalizePayoutTx.class,
// SellerBroadcastPayoutTx.class,
SellerSendPayoutTxPublishedMessage.class)
SellerPreparesPaymentReceivedMessage.class,
SellerSendsPaymentReceivedMessage.class)
.using(new TradeTaskRunner(trade, () -> {
latch.countDown();
resultHandler.handleResult();
handleTaskRunnerSuccess(event);
}, (errorMessage) -> {
latch.countDown();
errorMessageHandler.handleErrorMessage(errorMessage);
handleTaskRunnerFault(event, errorMessage);
})))
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT))
.run(() -> trade.setState(Trade.State.SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT))
.executeTasks();
wait(latch);
}
}
@Override
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
super.onTradeMessage(message, peer);
if (message instanceof CounterCurrencyTransferStartedMessage) {
handle((CounterCurrencyTransferStartedMessage) message, peer);
if (message instanceof PaymentSentMessage) {
handle((PaymentSentMessage) message, peer);
}
}

View file

@ -21,7 +21,7 @@ import bisq.core.offer.Offer;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeManager;
import bisq.core.trade.handlers.TradeResultHandler;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.DepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.SignContractRequest;
@ -294,10 +294,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
// TODO (woodser): support notifications of ack messages
private void onAckMessage(AckMessage ackMessage, NodeAddress peer) {
// We handle the ack for CounterCurrencyTransferStartedMessage and DepositTxAndDelayedPayoutTxMessage
// We handle the ack for PaymentSentMessage and DepositTxAndDelayedPayoutTxMessage
// as we support automatic re-send of the msg in case it was not ACKed after a certain time
// TODO (woodser): add AckMessage for InitTradeRequest and support automatic re-send ?
if (ackMessage.getSourceMsgClassName().equals(CounterCurrencyTransferStartedMessage.class.getSimpleName())) {
if (ackMessage.getSourceMsgClassName().equals(PaymentSentMessage.class.getSimpleName())) {
processModel.setPaymentStartedAckMessage(ackMessage);
} else if (ackMessage.getSourceMsgClassName().equals(DepositTxAndDelayedPayoutTxMessage.class.getSimpleName())) {
processModel.setDepositTxSentAckMessage(ackMessage);

View file

@ -35,7 +35,7 @@ import java.util.stream.Collectors;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.model.MoneroTxWallet;
import javax.annotation.Nullable;
// Fields marked as transient are only used during protocol execution which are based on directMessages so we do not
@ -108,13 +108,17 @@ public final class TradingPeer implements PersistablePayload {
@Nullable
private String madeMultisigHex;
@Nullable
private String signedPayoutTxHex;
@Nullable
private String depositTxHash;
@Nullable
private String depositTxHex;
@Nullable
private String depositTxKey;
@Nullable
transient private MoneroTxWallet payoutTx;
@Nullable
private String payoutTxHex;
@Nullable
private String updatedMultisigHex;
public TradingPeer() {
}
@ -146,10 +150,11 @@ public final class TradingPeer implements PersistablePayload {
Optional.ofNullable(reserveTxKey).ifPresent(e -> builder.setReserveTxKey(reserveTxKey));
Optional.ofNullable(preparedMultisigHex).ifPresent(e -> builder.setPreparedMultisigHex(preparedMultisigHex));
Optional.ofNullable(madeMultisigHex).ifPresent(e -> builder.setMadeMultisigHex(madeMultisigHex));
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex));
Optional.ofNullable(payoutTxHex).ifPresent(e -> builder.setPayoutTxHex(payoutTxHex));
Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash));
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
builder.setCurrentDate(currentDate);
return builder.build();
@ -189,10 +194,11 @@ public final class TradingPeer implements PersistablePayload {
tradingPeer.setReserveTxKeyImages(proto.getReserveTxKeyImagesList());
tradingPeer.setPreparedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getPreparedMultisigHex()));
tradingPeer.setMadeMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getMadeMultisigHex()));
tradingPeer.setSignedPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()));
tradingPeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()));
tradingPeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
tradingPeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
tradingPeer.setPayoutTxHex(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxHex()));
tradingPeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
return tradingPeer;
}
}

View file

@ -22,10 +22,10 @@ import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferPayload;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.DepositRequest;
import bisq.core.trade.messages.DepositResponse;
import bisq.core.trade.protocol.TradingPeer;
@ -37,7 +37,6 @@ import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.MoneroDaemon;
import monero.wallet.MoneroWallet;
@Slf4j
public class ArbitratorProcessesDepositRequest extends TradeTask {
@ -86,14 +85,12 @@ public class ArbitratorProcessesDepositRequest extends TradeTask {
else throw new RuntimeException("DepositRequest is not from maker or taker");
// flush reserve tx from pool
MoneroDaemon daemon = trade.getXmrWalletService().getDaemon();
XmrWalletService xmrWalletService = trade.getXmrWalletService();
MoneroDaemon daemon = xmrWalletService.getDaemon();
daemon.flushTxPool(trader.getReserveTxHash());
// process and verify deposit tx
TradeUtils.processTradeTx(
daemon,
trade.getXmrWalletService().getWallet(),
depositAddress,
// verify deposit tx
xmrWalletService.verifyTradeTx(depositAddress,
depositAmount,
tradeFee,
trader.getDepositTxHash(),

View file

@ -55,9 +55,7 @@ public class ArbitratorProcessesReserveTx extends TradeTask {
// process reserve tx with expected terms
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(isFromTaker ? trade.getTakerFee() : offer.getMakerFee());
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(isFromBuyer ? offer.getBuyerSecurityDeposit() : offer.getAmount().add(offer.getSellerSecurityDeposit()));
TradeUtils.processTradeTx(
processModel.getXmrWalletService().getDaemon(),
processModel.getXmrWalletService().getWallet(),
trade.getXmrWalletService().verifyTradeTx(
request.getPayoutAddress(),
depositAmount,
tradeFee,

View file

@ -27,7 +27,6 @@ import bisq.core.trade.ArbitratorTrade;
import bisq.core.trade.Contract;
import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.messages.SignContractResponse;
import bisq.core.trade.protocol.TradingPeer;
@ -71,7 +70,7 @@ public class ProcessSignContractRequest extends TradeTask {
}
// create and sign contract
Contract contract = TradeUtils.createContract(trade);
Contract contract = trade.createContract();
String contractAsJson = Utilities.objectToJson(contract);
String signature = Sig.sign(processModel.getKeyRing().getSignatureKeyPair().getPrivate(), contractAsJson);

View file

@ -72,9 +72,9 @@ public class ProcessUpdateMultisigRequest extends TradeTask {
String updatedMultisigHex = multisigWallet.getMultisigHex();
// import the multisig hex
int numOutputsSigned = multisigWallet.importMultisigHex(Arrays.asList(request.getUpdatedMultisigHex()));
int numOutputsSigned = multisigWallet.importMultisigHex(request.getUpdatedMultisigHex());
System.out.println("Num outputs signed by imported multisig hex: " + numOutputsSigned);
// close multisig wallet
processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId());

View file

@ -18,25 +18,14 @@
package bisq.core.trade.protocol.tasks;
import bisq.common.app.Version;
import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.offer.Offer;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.SellerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.messages.SignContractRequest;
import bisq.core.trade.protocol.TradeListener;
import bisq.core.util.ParsingUtils;
import bisq.network.p2p.AckMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener;
import java.math.BigInteger;
import java.util.Date;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
@ -57,88 +46,78 @@ public class SendSignContractRequestAfterMultisig extends TradeTask {
try {
runInterceptHook();
synchronized (trade.getXmrWalletService().getWallet()) { // synchronize on wallet to create deposit tx and freeze funds
// skip if multisig wallet not complete
if (!processModel.isMultisigSetupComplete()) {
complete();
return; // TODO: woodser: this does not ack original request?
}
// skip if deposit tx already created
if (processModel.getDepositTxXmr() != null) {
complete();
return;
}
// thaw reserved outputs
MoneroWallet wallet = trade.getXmrWalletService().getWallet();
for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) {
wallet.thawOutput(reserveTxKeyImage);
}
// create deposit tx
BigInteger tradeFee = ParsingUtils.coinToAtomicUnits(trade instanceof MakerTrade ? trade.getOffer().getMakerFee() : trade.getTakerFee());
Offer offer = processModel.getOffer();
BigInteger depositAmount = ParsingUtils.coinToAtomicUnits(trade instanceof SellerTrade ? offer.getAmount().add(offer.getSellerSecurityDeposit()) : offer.getBuyerSecurityDeposit());
String multisigAddress = processModel.getMultisigAddress();
MoneroTxWallet depositTx = TradeUtils.createDepositTx(trade.getXmrWalletService(), tradeFee, multisigAddress, depositAmount);
// freeze deposit outputs
// TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig
for (MoneroOutput input : depositTx.getInputs()) {
wallet.freezeOutput(input.getKeyImage().getHex());
}
// save process state
processModel.setDepositTxXmr(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
// create request for peer and arbitrator to sign contract
SignContractRequest request = new SignContractRequest(
trade.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
trade.getProcessModel().getAccountId(),
trade.getProcessModel().getPaymentAccountPayload(trade).getHash(),
trade.getXmrWalletService().getAddressEntry(offer.getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString(),
depositTx.getHash());
// send request to trading peer
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
ack1 = true;
if (ack1 && ack2) completeAux();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
});
// send request to arbitrator
processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId());
ack2 = true;
if (ack1 && ack2) completeAux();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
});
// skip if multisig wallet not complete
if (!processModel.isMultisigSetupComplete()) {
complete();
return; // TODO: woodser: this does not ack original request?
}
// skip if deposit tx already created
if (processModel.getDepositTxXmr() != null) {
complete();
return;
}
// thaw reserved outputs
MoneroWallet wallet = trade.getXmrWalletService().getWallet();
for (String reserveTxKeyImage : trade.getSelf().getReserveTxKeyImages()) {
wallet.thawOutput(reserveTxKeyImage);
}
// create deposit tx and freeze inputs
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade);
// TODO (woodser): save frozen key images and unfreeze if trade fails before deposited to multisig
// save process state
processModel.setDepositTxXmr(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address?
// create request for peer and arbitrator to sign contract
SignContractRequest request = new SignContractRequest(
trade.getOffer().getId(),
processModel.getMyNodeAddress(),
processModel.getPubKeyRing(),
UUID.randomUUID().toString(),
Version.getP2PMessageVersion(),
new Date().getTime(),
trade.getProcessModel().getAccountId(),
trade.getProcessModel().getPaymentAccountPayload(trade).getHash(),
trade.getSelf().getPayoutAddressString(),
depositTx.getHash());
// send request to trading peer
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeerNodeAddress(), trade.getTradingPeerPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId());
ack1 = true;
if (ack1 && ack2) completeAux();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getTradingPeerNodeAddress(), trade.getId(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
});
// send request to arbitrator
processModel.getP2PService().sendEncryptedDirectMessage(trade.getArbitratorNodeAddress(), trade.getArbitratorPubKeyRing(), request, new SendDirectMessageListener() {
@Override
public void onArrived() {
log.info("{} arrived: trading peer={}; offerId={}; uid={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId());
ack2 = true;
if (ack1 && ack2) completeAux();
}
@Override
public void onFault(String errorMessage) {
log.error("Sending {} failed: uid={}; peer={}; error={}", request.getClass().getSimpleName(), trade.getArbitratorNodeAddress(), trade.getId(), errorMessage);
appendToErrorMessage("Sending message failed: message=" + request + "\nerrorMessage=" + errorMessage);
failed();
}
});
} catch (Throwable t) {
failed(t);
}

View file

@ -57,7 +57,7 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
// fetch relevant trade info
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerCreateAndSignPayoutTx
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerPreparesPaymentStartedMessage
// skip if multisig wallet does not need updated
if (!multisigWallet.isMultisigImportNeeded()) {
@ -72,8 +72,8 @@ public class UpdateMultisigWithTradingPeer extends TradeTask {
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
if (!(message instanceof UpdateMultisigResponse)) return;
UpdateMultisigResponse response = (UpdateMultisigResponse) message;
multisigWallet.importMultisigHex(Arrays.asList(response.getUpdatedMultisigHex()));
multisigWallet.sync();
multisigWallet.importMultisigHex(response.getUpdatedMultisigHex());
trade.removeListener(updateMultisigResponseListener);
complete();
}

View file

@ -18,10 +18,8 @@
package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.ParsingUtils;
import bisq.common.taskrunner.TaskRunner;
@ -38,18 +36,16 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.common.MoneroError;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroAccount;
import monero.wallet.model.MoneroSubaddress;
import monero.wallet.model.MoneroTxConfig;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
public class BuyerCreateAndSignPayoutTx extends TradeTask {
public class BuyerPreparesPaymentSentMessage extends TradeTask {
@SuppressWarnings({"unused"})
public BuyerCreateAndSignPayoutTx(TaskRunner taskHandler, Trade trade) {
public BuyerPreparesPaymentSentMessage(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -57,61 +53,36 @@ public class BuyerCreateAndSignPayoutTx extends TradeTask {
protected void run() {
try {
runInterceptHook();
// validate state
Preconditions.checkNotNull(trade.getTradeAmount(), "trade.getTradeAmount() must not be null");
Preconditions.checkNotNull(trade.getMakerDepositTx(), "trade.getMakerDepositTx() must not be null");
Preconditions.checkNotNull(trade.getTakerDepositTx(), "trade.getTakerDepositTx() must not be null");
checkNotNull(trade.getOffer(), "offer must not be null");
// gather relevant trade info
// get multisig wallet
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
String sellerPayoutAddress = trade.getTradingPeer().getPayoutAddressString();
String buyerPayoutAddress = trade instanceof MakerTrade ? trade.getContract().getMakerPayoutAddressString() : trade.getContract().getTakerPayoutAddressString();
Preconditions.checkNotNull(sellerPayoutAddress, "sellerPayoutAddress must not be null");
Preconditions.checkNotNull(buyerPayoutAddress, "buyerPayoutAddress must not be null");
BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount();
BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount());
BigInteger buyerPayoutAmount = buyerDepositAmount.add(tradeAmount);
BigInteger sellerPayoutAmount = sellerDepositAmount.subtract(tradeAmount);
// create transaction to get fee estimate
if (multisigWallet.isMultisigImportNeeded()) throw new RuntimeException("Multisig import is still needed!!!");
MoneroTxWallet feeEstimateTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10))) // reduce payment amount to compute fee of similar tx
.addDestination(sellerPayoutAddress, sellerPayoutAmount.multiply(BigInteger.valueOf(9)).divide(BigInteger.valueOf(10)))
.setRelay(false)
);
// attempt to create payout tx by increasing estimated fee until successful
MoneroTxWallet payoutTx = null;
int numAttempts = 0;
while (payoutTx == null && numAttempts < 50) {
BigInteger feeEstimate = feeEstimateTx.getFee().add(feeEstimateTx.getFee().multiply(BigInteger.valueOf(numAttempts)).divide(BigInteger.valueOf(10))); // add 1/10 of fee until tx is successful
try {
numAttempts++;
payoutTx = multisigWallet.createTx(new MoneroTxConfig()
.setAccountIndex(0)
.addDestination(buyerPayoutAddress, buyerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2)))) // split fee subtracted from each payout amount
.addDestination(sellerPayoutAddress, sellerPayoutAmount.subtract(feeEstimate.divide(BigInteger.valueOf(2))))
.setRelay(false));
} catch (MoneroError e) {
// exception expected
}
// create payout tx if we have seller's updated multisig hex
if (!multisigWallet.isMultisigImportNeeded()) {
log.info("Buyer creating unsigned payout tx");
MoneroTxWallet payoutTx = trade.createPayoutTx();
trade.getBuyer().setPayoutTx(payoutTx);
trade.getBuyer().setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
} else {
trade.getSelf().setUpdatedMultisigHex(multisigWallet.getMultisigHex());
}
if (payoutTx == null) throw new RuntimeException("Failed to generate payout tx after " + numAttempts + " attempts");
log.info("Payout transaction generated on attempt {}: {}", numAttempts, payoutTx);
processModel.setBuyerSignedPayoutTx(payoutTx);
// close multisig wallet
walletService.closeMultisigWallet(trade.getId());
complete();
} catch (Throwable t) {
failed(t);
}
}
// TODO (woodser): move these to gen utils
/**
* Generic parameterized pair.

View file

@ -20,7 +20,7 @@ package bisq.core.trade.protocol.tasks.buyer;
import bisq.core.account.sign.SignedWitness;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.Validator;
@ -38,8 +38,8 @@ import static com.google.common.base.Preconditions.checkNotNull;
import monero.wallet.MoneroWallet;
@Slf4j
public class BuyerProcessPayoutTxPublishedMessage extends TradeTask {
public BuyerProcessPayoutTxPublishedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
public class BuyerProcessesPaymentReceivedMessage extends TradeTask {
public BuyerProcessesPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -48,23 +48,34 @@ public class BuyerProcessPayoutTxPublishedMessage extends TradeTask {
try {
runInterceptHook();
log.debug("current trade state " + trade.getState());
PayoutTxPublishedMessage message = (PayoutTxPublishedMessage) processModel.getTradeMessage();
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
Validator.checkTradeId(processModel.getOfferId(), message);
checkNotNull(message);
checkArgument(message.getSignedMultisigTxHex() != null);
checkArgument(message.getPayoutTxHex() != null);
// update to the latest peer address of our peer if the message is correct
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
// handle if payout tx is not seen on network
if (trade.getPayoutTx() == null) {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
List<String> txHashes = multisigWallet.submitMultisigTxHex(message.getSignedMultisigTxHex());
trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0)));
XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx());
trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG);
walletService.closeMultisigWallet(trade.getId());
//processModel.getBtcWalletService().resetCoinLockedInMultiSigAddressEntry(trade.getId());
// publish payout tx if signed. otherwise verify, sign, and publish payout tx
boolean fullySigned = trade.getSelf().getPayoutTx() != null;
if (fullySigned) {
log.info("Buyer publishing signed payout tx from seller");
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
List<String> txHashes = multisigWallet.submitMultisigTxHex(message.getPayoutTxHex());
trade.setPayoutTx(multisigWallet.getTx(txHashes.get(0)));
XmrWalletService.printTxs("payoutTx received from peer", trade.getPayoutTx());
trade.setState(Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG);
walletService.closeMultisigWallet(trade.getId());
} else {
log.info("Buyer verifying, signing, and publishing seller's payout tx");
trade.verifySignAndPublishPayoutTx(message.getPayoutTxHex());
trade.setState(Trade.State.BUYER_PUBLISHED_PAYOUT_TX);
// TODO (woodser): send PayoutTxPublishedMessage to arbitrator and seller
}
} else {
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId());
}

View file

@ -21,7 +21,7 @@ import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.network.MessageState;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.core.trade.messages.TradeMessage;
import bisq.core.trade.protocol.tasks.SendMailboxMessageTask;
@ -35,9 +35,10 @@ import javafx.beans.value.ChangeListener;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
/**
* We send the seller the BuyerSendCounterCurrencyTransferStartedMessage.
* We send the seller the BuyerSendPaymentSentMessage.
* We wait to receive a ACK message back and resend the message
* in case that does not happen in 10 minutes or if the message was stored in mailbox or failed. We keep repeating that
* with doubling the interval each time and until the MAX_RESEND_ATTEMPTS is reached.
@ -46,15 +47,15 @@ import lombok.extern.slf4j.Slf4j;
* online he will process it.
*/
@Slf4j
public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxMessageTask {
public class BuyerSendsPaymentSentMessage extends SendMailboxMessageTask {
private static final int MAX_RESEND_ATTEMPTS = 10;
private int delayInMin = 15;
private int resendCounter = 0;
private CounterCurrencyTransferStartedMessage message;
private PaymentSentMessage message;
private ChangeListener<MessageState> listener;
private Timer timer;
public BuyerSendCounterCurrencyTransferStartedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
public BuyerSendsPaymentSentMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -66,21 +67,21 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
final String id = processModel.getOfferId();
XmrAddressEntry payoutAddressEntry = walletService.getOrCreateAddressEntry(id, XmrAddressEntry.Context.TRADE_PAYOUT);
String payoutTxHex = processModel.getBuyerSignedPayoutTx().getTxSet().getMultisigTxHex();
// We do not use a real unique ID here as we want to be able to re-send the exact same message in case the
// peer does not respond with an ACK msg in a certain time interval. To avoid that we get dangling mailbox
// messages where only the one which gets processed by the peer would be removed we use the same uid. All
// other data stays the same when we re-send the message at any time later.
String deterministicId = tradeId + processModel.getMyNodeAddress().getFullAddress();
message = new CounterCurrencyTransferStartedMessage(
message = new PaymentSentMessage(
tradeId,
payoutAddressEntry.getAddressString(),
processModel.getMyNodeAddress(),
payoutTxHex,
trade.getCounterCurrencyTxId(),
trade.getCounterCurrencyExtraData(),
deterministicId
deterministicId,
trade.getBuyer().getPayoutTxHex(),
trade.getBuyer().getUpdatedMultisigHex()
);
}
return message;
@ -88,8 +89,8 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
@Override
protected void setStateSent() {
if (trade.getState().ordinal() < Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG.ordinal()) {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG);
if (trade.getState().ordinal() < Trade.State.BUYER_SENT_PAYMENT_INITIATED_MSG.ordinal()) {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SENT_PAYMENT_INITIATED_MSG);
}
processModel.getTradeManager().requestPersistence();
@ -111,7 +112,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
@Override
protected void setStateStoredInMailbox() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG);
if (!trade.isPayoutPublished()) {
tryToSendAgainLater();
}
@ -126,7 +127,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
@Override
protected void setStateFault() {
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG);
if (!trade.isPayoutPublished()) {
tryToSendAgainLater();
}
@ -165,7 +166,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
private void tryToSendAgainLater() {
if (resendCounter >= MAX_RESEND_ATTEMPTS) {
cleanup();
log.warn("We never received an ACK message when sending the CounterCurrencyTransferStartedMessage to the peer. " +
log.warn("We never received an ACK message when sending the PaymentSentMessage to the peer. " +
"We stop now and complete the protocol task.");
complete();
return;
@ -192,7 +193,7 @@ public class BuyerSendCounterCurrencyTransferStartedMessage extends SendMailboxM
// Once we receive an ACK from our msg we know the peer has received the msg and we stop.
if (newValue == MessageState.ACKNOWLEDGED) {
// We treat a ACK like BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG);
trade.setStateIfValidTransitionTo(Trade.State.BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG);
processModel.getTradeManager().requestPersistence();

View file

@ -0,0 +1,59 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks.seller;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
public class SellerPreparesPaymentReceivedMessage extends TradeTask {
@SuppressWarnings({"unused"})
public SellerPreparesPaymentReceivedMessage(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// verify, sign, and publish payout tx if given. otherwise create payout tx
if (trade.getBuyer().getPayoutTxHex() != null) {
log.info("Seller verifying, signing, and publishing payout tx");
trade.verifySignAndPublishPayoutTx(trade.getBuyer().getPayoutTxHex());
} else {
log.info("Seller creating unsigned payout tx");
MoneroTxWallet payoutTx = trade.createPayoutTx();
System.out.println("created payout tx: " + payoutTx);
trade.getSeller().setPayoutTx(payoutTx);
trade.getSeller().setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
}
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -17,20 +17,21 @@
package bisq.core.trade.protocol.tasks.seller;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.CounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.Validator;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkNotNull;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PaymentSentMessage;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.Validator;
import java.util.Arrays;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
@Slf4j
public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTask {
public SellerProcessCounterCurrencyTransferStartedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
public class SellerProcessesPaymentSentMessage extends TradeTask {
public SellerProcessesPaymentSentMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@ -39,12 +40,21 @@ public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTas
try {
runInterceptHook();
log.debug("current trade state " + trade.getState());
CounterCurrencyTransferStartedMessage message = (CounterCurrencyTransferStartedMessage) processModel.getTradeMessage();
PaymentSentMessage message = (PaymentSentMessage) processModel.getTradeMessage();
Validator.checkTradeId(processModel.getOfferId(), message);
checkNotNull(message);
trade.getTradingPeer().setPayoutAddressString(Validator.nonEmptyStringOf(message.getBuyerPayoutAddress())); // TODO (woodser): verify against contract
trade.getTradingPeer().setSignedPayoutTxHex(message.getBuyerPayoutTxSigned());
trade.getBuyer().setPayoutAddressString(Validator.nonEmptyStringOf(message.getBuyerPayoutAddress())); // TODO (woodser): verify against contract
trade.getBuyer().setPayoutTxHex(message.getPayoutTxHex());
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
// sync and update multisig wallet
if (trade.getBuyer().getUpdatedMultisigHex() != null) {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // TODO: ensure sync() always called before importMultisigHex()
multisigWallet.importMultisigHex(trade.getBuyer().getUpdatedMultisigHex());
walletService.closeMultisigWallet(trade.getId());
}
// update to the latest peer address of our peer if the message is correct // TODO (woodser): update to latest peer addresses where needed
trade.setTradingPeerNodeAddress(processModel.getTempTradingPeerNodeAddress());
@ -59,7 +69,7 @@ public class SellerProcessCounterCurrencyTransferStartedMessage extends TradeTas
trade.setCounterCurrencyExtraData(counterCurrencyExtraData);
}
trade.setState(Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG);
trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_INITIATED_MSG);
processModel.getTradeManager().requestPersistence();

View file

@ -20,7 +20,7 @@ package bisq.core.trade.protocol.tasks.seller;
import bisq.core.account.sign.SignedWitness;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.PayoutTxPublishedMessage;
import bisq.core.trade.messages.PaymentReceivedMessage;
import bisq.core.trade.messages.TradeMailboxMessage;
import bisq.core.trade.protocol.tasks.SendMailboxMessageTask;
@ -33,71 +33,21 @@ import static com.google.common.base.Preconditions.checkNotNull;
@EqualsAndHashCode(callSuper = true)
@Slf4j
public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
public class SellerSendsPaymentReceivedMessage extends SendMailboxMessageTask {
SignedWitness signedWitness = null;
public SellerSendPayoutTxPublishedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
public SellerSendsPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String id) {
checkNotNull(trade.getPayoutTx(), "trade.getPayoutTx() must not be null");
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
// Broadcast is done in accountAgeWitness domain.
accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
}
return new PayoutTxPublishedMessage(
id,
trade.getPayoutTx().getTxSet().getMultisigTxHex(),
processModel.getMyNodeAddress(),
signedWitness
);
}
@Override
protected void setStateSent() {
trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG);
log.info("Sent PayoutTxPublishedMessage: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG);
log.info("PayoutTxPublishedMessage arrived: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG);
log.info("PayoutTxPublishedMessage storedInMailbox: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateFault() {
trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG);
log.error("PayoutTxPublishedMessage failed: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void run() {
try {
runInterceptHook();
if (trade.getPayoutTx() == null) {
log.error("PayoutTx is null");
failed("PayoutTx is null");
if (trade.getSeller().getPayoutTxHex() == null) {
log.error("Payout tx is null");
failed("Payout tx is null");
return;
}
@ -106,4 +56,54 @@ public class SellerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
failed(t);
}
}
@Override
protected TradeMailboxMessage getTradeMailboxMessage(String id) {
checkNotNull(trade.getSeller().getPayoutTxHex(), "Payout tx must not be null");
AccountAgeWitnessService accountAgeWitnessService = processModel.getAccountAgeWitnessService();
if (accountAgeWitnessService.isSignWitnessTrade(trade)) {
// Broadcast is done in accountAgeWitness domain.
accountAgeWitnessService.traderSignAndPublishPeersAccountAgeWitness(trade).ifPresent(witness -> signedWitness = witness);
}
return new PaymentReceivedMessage(
id,
processModel.getMyNodeAddress(),
signedWitness,
trade.getSeller().getPayoutTxHex()
);
}
@Override
protected void setStateSent() {
trade.setState(Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG);
log.info("Sent SellerReceivedPaymentMessage: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateArrived() {
trade.setState(Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG);
log.info("SellerReceivedPaymentMessage arrived: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateStoredInMailbox() {
trade.setState(Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG);
log.info("SellerReceivedPaymentMessage storedInMailbox: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
@Override
protected void setStateFault() {
trade.setState(Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG);
log.error("SellerReceivedPaymentMessage failed: tradeId={} at peer {} SignedWitness {}",
trade.getId(), trade.getTradingPeerNodeAddress(), signedWitness);
processModel.getTradeManager().requestPersistence();
}
}

View file

@ -1,117 +0,0 @@
/*
* This file is part of Haveno.
*
* Haveno 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.
*
* Haveno 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 Haveno. If not, see <http://www.gnu.org/licenses/>.
*/
package bisq.core.trade.protocol.tasks.seller;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Contract;
import bisq.core.trade.MakerTrade;
import bisq.core.trade.Trade;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.ParsingUtils;
import bisq.common.taskrunner.TaskRunner;
import java.math.BigInteger;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroDestination;
import monero.wallet.model.MoneroMultisigSignResult;
import monero.wallet.model.MoneroTxSet;
import monero.wallet.model.MoneroTxWallet;
@Slf4j
public class SellerSignAndPublishPayoutTx extends TradeTask {
@SuppressWarnings({"unused"})
public SellerSignAndPublishPayoutTx(TaskRunner taskHandler, Trade trade) {
super(taskHandler, trade);
}
@Override
protected void run() {
try {
runInterceptHook();
// gather relevant trade info
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
String buyerSignedPayoutTxHex = trade.getTradingPeer().getSignedPayoutTxHex();
Contract contract = trade.getContract();
BigInteger sellerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getMaker().getDepositTxHash() : processModel.getTaker().getDepositTxHash()).getIncomingAmount(); // TODO (woodser): redundancy of processModel.getPreparedDepositTxId() vs trade.getDepositTxId() necessary or avoidable?
BigInteger buyerDepositAmount = multisigWallet.getTx(trade instanceof MakerTrade ? processModel.getTaker().getDepositTxHash() : processModel.getMaker().getDepositTxHash()).getIncomingAmount();
BigInteger tradeAmount = ParsingUtils.coinToAtomicUnits(trade.getTradeAmount());
// parse buyer-signed payout tx
MoneroTxSet parsedTxSet = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(buyerSignedPayoutTxHex));
if (parsedTxSet.getTxs() == null || parsedTxSet.getTxs().size() != 1) throw new RuntimeException("Bad buyer-signed payout tx"); // TODO (woodser): test nack
MoneroTxWallet buyerSignedPayoutTx = parsedTxSet.getTxs().get(0);
// verify payout tx has exactly 2 destinations
log.info("Seller verifying buyer-signed payout tx");
if (buyerSignedPayoutTx.getOutgoingTransfer() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations() == null || buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().size() != 2) throw new RuntimeException("Buyer-signed payout tx does not have exactly two destinations");
// get buyer and seller destinations (order not preserved)
boolean buyerFirst = buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().get(0).getAddress().equals(contract.getBuyerPayoutAddressString());
MoneroDestination buyerPayoutDestination = buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 0 : 1);
MoneroDestination sellerPayoutDestination = buyerSignedPayoutTx.getOutgoingTransfer().getDestinations().get(buyerFirst ? 1 : 0);
// verify payout addresses
if (!buyerPayoutDestination.getAddress().equals(contract.getBuyerPayoutAddressString())) throw new RuntimeException("Buyer payout address does not match contract");
if (!sellerPayoutDestination.getAddress().equals(contract.getSellerPayoutAddressString())) throw new RuntimeException("Seller payout address does not match contract");
// verify change address is multisig's primary address
if (!buyerSignedPayoutTx.getChangeAmount().equals(BigInteger.ZERO) && !buyerSignedPayoutTx.getChangeAddress().equals(multisigWallet.getPrimaryAddress())) throw new RuntimeException("Change address is not multisig wallet's primary address");
// verify sum of outputs = destination amounts + change amount
if (!buyerSignedPayoutTx.getOutputSum().equals(buyerPayoutDestination.getAmount().add(sellerPayoutDestination.getAmount()).add(buyerSignedPayoutTx.getChangeAmount()))) throw new RuntimeException("Sum of outputs != destination amounts + change amount");
// verify buyer destination amount is deposit amount + trade amount - 1/2 tx costs
BigInteger txCost = buyerSignedPayoutTx.getFee().add(buyerSignedPayoutTx.getChangeAmount());
BigInteger expectedBuyerPayout = buyerDepositAmount.add(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
if (!buyerPayoutDestination.getAmount().equals(expectedBuyerPayout)) throw new RuntimeException("Buyer destination amount is not deposit amount + trade amount - 1/2 tx costs, " + buyerPayoutDestination.getAmount() + " vs " + expectedBuyerPayout);
// verify seller destination amount is deposit amount - trade amount - 1/2 tx costs
BigInteger expectedSellerPayout = sellerDepositAmount.subtract(tradeAmount).subtract(txCost.divide(BigInteger.valueOf(2)));
if (!sellerPayoutDestination.getAmount().equals(expectedSellerPayout)) throw new RuntimeException("Seller destination amount is not deposit amount - trade amount - 1/2 tx costs, " + sellerPayoutDestination.getAmount() + " vs " + expectedSellerPayout);
// TODO (woodser): verify fee is reasonable (e.g. within 2x of fee estimate tx)
// sign buyer-signed payout tx
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(buyerSignedPayoutTxHex);
if (result.getSignedMultisigTxHex() == null) throw new RuntimeException("Error signing buyer-signed payout tx");
String signedMultisigTxHex = result.getSignedMultisigTxHex();
// submit fully signed payout tx to the network
multisigWallet.submitMultisigTxHex(signedMultisigTxHex);
// close multisig wallet
walletService.closeMultisigWallet(trade.getId());
// update trade state
parsedTxSet.setMultisigTxHex(signedMultisigTxHex); // TODO (woodser): better place to store this?
trade.setPayoutTx(parsedTxSet.getTxs().get(0));
trade.setPayoutTxId(parsedTxSet.getTxs().get(0).getHash());
trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
complete();
} catch (Throwable t) {
failed(t);
}
}
}

View file

@ -20,14 +20,12 @@ package bisq.core.trade.protocol.tasks.taker;
import bisq.common.taskrunner.TaskRunner;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.trade.Trade;
import bisq.core.trade.TradeUtils;
import bisq.core.trade.protocol.tasks.TradeTask;
import bisq.core.util.ParsingUtils;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;
import monero.daemon.model.MoneroOutput;
import monero.wallet.MoneroWallet;
import monero.wallet.model.MoneroTxWallet;
public class TakerReservesTradeFunds extends TradeTask {
@ -41,27 +39,23 @@ public class TakerReservesTradeFunds extends TradeTask {
try {
runInterceptHook();
// synchronize on wallet to reserve key images
synchronized (model.getXmrWalletService().getWallet()) {
// create transaction to reserve trade
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong());
MoneroTxWallet reserveTx = TradeUtils.reserveTradeFunds(model.getXmrWalletService(), trade.getId(), takerFee, returnAddress, depositAmount);
// collect reserved key images // TODO (woodser): switch to proof of reserve?
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save process state
// TODO (woodser): persist
processModel.setReserveTx(reserveTx);
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used?
//trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states
complete();
}
// freeze trade funds and get reserve tx
String returnAddress = model.getXmrWalletService().getOrCreateAddressEntry(trade.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).getAddressString();
BigInteger takerFee = ParsingUtils.coinToAtomicUnits(trade.getTakerFee());
BigInteger depositAmount = ParsingUtils.centinerosToAtomicUnits(processModel.getFundsNeededForTradeAsLong());
MoneroTxWallet reserveTx = model.getXmrWalletService().createReserveTx(takerFee, returnAddress, depositAmount);
// collect reserved key images // TODO (woodser): switch to proof of reserve?
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : reserveTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save process state
// TODO (woodser): persist
processModel.setReserveTx(reserveTx);
processModel.getTaker().setReserveTxKeyImages(reservedKeyImages);
trade.setTakerFeeTxId(reserveTx.getHash()); // TODO (woodser): this should be multisig deposit tx id? how is it used?
//trade.setState(Trade.State.TAKER_PUBLISHED_TAKER_FEE_TX); // TODO (woodser): fee tx is not broadcast separate, update states
complete();
} catch (Throwable t) {
trade.setErrorMessage("An error occurred.\n" +
"Error message:\n"

View file

@ -346,7 +346,7 @@ public class XmrTxProofService implements AssetTxProofService {
}
private boolean isExpectedTradeState(Trade.State newValue) {
return newValue == Trade.State.SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG;
return newValue == Trade.State.SELLER_RECEIVED_PAYMENT_INITIATED_MSG;
}
private boolean is32BitHexStringInValid(String hexString) {

View file

@ -28,11 +28,11 @@ import bisq.core.offer.placeoffer.tasks.MakerReservesTradeFunds;
import bisq.core.offer.placeoffer.tasks.ValidateOffer;
import bisq.core.trade.protocol.tasks.ApplyFilter;
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
import bisq.core.trade.protocol.tasks.buyer.BuyerCreateAndSignPayoutTx;
import bisq.core.trade.protocol.tasks.buyer.BuyerPreparesPaymentSentMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessDepositTxAndDelayedPayoutTxMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendCounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerProcessesPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsPaymentSentMessage;
import bisq.core.trade.protocol.tasks.buyer.BuyerSendsDelayedPayoutTxSignatureResponse;
import bisq.core.trade.protocol.tasks.buyer.BuyerSetupPayoutTxListener;
import bisq.core.trade.protocol.tasks.buyer.BuyerSignsDelayedPayoutTx;
@ -48,13 +48,13 @@ import bisq.core.trade.protocol.tasks.maker.MakerSetsLockTime;
import bisq.core.trade.protocol.tasks.maker.MakerVerifyTakerFeePayment;
import bisq.core.trade.protocol.tasks.seller.SellerCreatesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerFinalizesDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerProcessCounterCurrencyTransferStartedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerProcessesPaymentSentMessage;
import bisq.core.trade.protocol.tasks.seller.SellerProcessDelayedPayoutTxSignatureResponse;
import bisq.core.trade.protocol.tasks.seller.SellerPublishesDepositTx;
import bisq.core.trade.protocol.tasks.seller.SellerPublishesTradeStatistics;
import bisq.core.trade.protocol.tasks.seller.SellerSendDelayedPayoutTxSignatureRequest;
import bisq.core.trade.protocol.tasks.seller.SellerSendPayoutTxPublishedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignAndPublishPayoutTx;
import bisq.core.trade.protocol.tasks.seller.SellerSendsPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerPreparesPaymentReceivedMessage;
import bisq.core.trade.protocol.tasks.seller.SellerSignsDelayedPayoutTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerCreatesUnsignedDepositTx;
import bisq.core.trade.protocol.tasks.seller_as_maker.SellerAsMakerFinalizesDepositTx;
@ -135,15 +135,15 @@ public class DebugView extends InitializableView<GridPane, Void> {
SellerPublishesDepositTx.class,
SellerPublishesTradeStatistics.class,
SellerProcessCounterCurrencyTransferStartedMessage.class,
SellerProcessesPaymentSentMessage.class,
ApplyFilter.class,
TakerVerifyMakerFeePayment.class,
ApplyFilter.class,
TakerVerifyMakerFeePayment.class,
SellerSignAndPublishPayoutTx.class,
SellerPreparesPaymentReceivedMessage.class,
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
SellerSendPayoutTxPublishedMessage.class
SellerSendsPaymentReceivedMessage.class
)
));
@ -167,11 +167,11 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class,
MakerVerifyTakerFeePayment.class,
BuyerCreateAndSignPayoutTx.class,
BuyerPreparesPaymentSentMessage.class,
BuyerSetupPayoutTxListener.class,
BuyerSendCounterCurrencyTransferStartedMessage.class,
BuyerSendsPaymentSentMessage.class,
BuyerProcessPayoutTxPublishedMessage.class
BuyerProcessesPaymentReceivedMessage.class
)
));
@ -199,11 +199,11 @@ public class DebugView extends InitializableView<GridPane, Void> {
ApplyFilter.class,
TakerVerifyMakerFeePayment.class,
BuyerCreateAndSignPayoutTx.class,
BuyerPreparesPaymentSentMessage.class,
BuyerSetupPayoutTxListener.class,
BuyerSendCounterCurrencyTransferStartedMessage.class,
BuyerSendsPaymentSentMessage.class,
BuyerProcessPayoutTxPublishedMessage.class)
BuyerProcessesPaymentReceivedMessage.class)
));
addGroup("SellerAsMakerProtocol",
FXCollections.observableArrayList(Arrays.asList(
@ -227,15 +227,15 @@ public class DebugView extends InitializableView<GridPane, Void> {
SellerPublishesDepositTx.class,
SellerPublishesTradeStatistics.class,
SellerProcessCounterCurrencyTransferStartedMessage.class,
SellerProcessesPaymentSentMessage.class,
ApplyFilter.class,
MakerVerifyTakerFeePayment.class,
ApplyFilter.class,
MakerVerifyTakerFeePayment.class,
SellerSignAndPublishPayoutTx.class,
SellerPreparesPaymentReceivedMessage.class,
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
SellerSendPayoutTxPublishedMessage.class
SellerSendsPaymentReceivedMessage.class
)
));
}

View file

@ -409,8 +409,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
appendMsg = Res.get("takeOffer.error.feePaid");
break;
case DEPOSIT_PUBLISHED:
case FIAT_SENT:
case FIAT_RECEIVED:
case PAYMENT_SENT:
case PAYMENT_RECEIVED:
appendMsg = Res.get("takeOffer.error.depositPublished");
break;
case PAYOUT_PUBLISHED:

View file

@ -194,7 +194,7 @@ public class NotificationCenter {
if (trade instanceof BuyerTrade && phase.ordinal() == Trade.Phase.DEPOSIT_CONFIRMED.ordinal())
message = Res.get("notification.trade.confirmed");
else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.FIAT_SENT.ordinal())
else if (trade instanceof SellerTrade && phase.ordinal() == Trade.Phase.PAYMENT_SENT.ordinal())
message = Res.get("notification.trade.paymentStarted");
}

View file

@ -468,27 +468,33 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
TradeChatSession tradeChatSession = new TradeChatSession(trade, isTaker);
tradeStateListener = (observable, oldValue, newValue) -> {
if (trade.isPayoutPublished()) {
if (chatPopupStage.isShowing()) {
chatPopupStage.hide();
UserThread.execute(() -> {
if (trade.isPayoutPublished()) {
if (chatPopupStage.isShowing()) {
chatPopupStage.hide();
}
}
}
});
};
trade.stateProperty().addListener(tradeStateListener);
disputeStateListener = (observable, oldValue, newValue) -> {
if (newValue == Trade.DisputeState.DISPUTE_CLOSED || newValue == Trade.DisputeState.REFUND_REQUEST_CLOSED) {
chatPopupStage.hide();
}
UserThread.execute(() -> {
if (newValue == Trade.DisputeState.DISPUTE_CLOSED || newValue == Trade.DisputeState.REFUND_REQUEST_CLOSED) {
chatPopupStage.hide();
}
});
};
trade.disputeStateProperty().addListener(disputeStateListener);
mediationResultStateListener = (observable, oldValue, newValue) -> {
if (newValue == MediationResultState.PAYOUT_TX_PUBLISHED ||
newValue == MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG ||
newValue == MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK) {
chatPopupStage.hide();
}
UserThread.execute(() -> {
if (newValue == MediationResultState.PAYOUT_TX_PUBLISHED ||
newValue == MediationResultState.RECEIVED_PAYOUT_TX_PUBLISHED_MSG ||
newValue == MediationResultState.PAYOUT_TX_SEEN_IN_NETWORK) {
chatPopupStage.hide();
}
});
};
trade.mediationResultStateProperty().addListener(mediationResultStateListener);
@ -559,21 +565,23 @@ public class PendingTradesView extends ActivatableViewAndModel<VBox, PendingTrad
}
private void updateChatMessageCount(Trade trade, JFXBadge badge) {
if (!trade.getId().equals(tradeIdOfOpenChat)) {
updateNewChatMessagesByTradeMap();
long num = newChatMessagesByTradeMap.get(trade.getId());
if (num > 0) {
badge.setText(String.valueOf(num));
badge.setEnabled(true);
UserThread.execute(() -> {
if (!trade.getId().equals(tradeIdOfOpenChat)) {
updateNewChatMessagesByTradeMap();
long num = newChatMessagesByTradeMap.get(trade.getId());
if (num > 0) {
badge.setText(String.valueOf(num));
badge.setEnabled(true);
} else {
badge.setText("");
badge.setEnabled(false);
}
} else {
badge.setText("");
badge.setEnabled(false);
}
} else {
badge.setText("");
badge.setEnabled(false);
}
badge.refreshBadge();
badge.refreshBadge();
});
}
///////////////////////////////////////////////////////////////////////////////////////////

View file

@ -452,27 +452,27 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
break;
// buyer step 3
case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED: // UI action
case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG sent
case BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED: // UI action
case BUYER_SENT_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG sent
// We don't switch the UI before we got the feedback of the msg delivery
buyerState.set(BuyerState.STEP2);
break;
case BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG arrived
case BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG in mailbox
case BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG arrived
case BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG in mailbox
buyerState.set(BuyerState.STEP3);
break;
case BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG failed
case BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG failed
// if failed we need to repeat sending so back to step 2
buyerState.set(BuyerState.STEP2);
break;
// seller step 3
case SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG received
case SELLER_RECEIVED_PAYMENT_INITIATED_MSG: // FIAT_PAYMENT_INITIATED_MSG received
sellerState.set(SellerState.STEP3);
break;
// seller step 4
case SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT: // UI action
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: // UI action
case SELLER_PUBLISHED_PAYOUT_TX: // payout tx broad casted
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG sent
sellerState.set(SellerState.STEP3);
@ -487,6 +487,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
case BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG:
// Alternatively the maker could have seen the payout tx earlier before he received the PAYOUT_TX_PUBLISHED_MSG:
case BUYER_SAW_PAYOUT_TX_IN_NETWORK:
// Alternatively the buyer could fully sign and publish the payout tx
case BUYER_PUBLISHED_PAYOUT_TX:
buyerState.set(BuyerState.STEP4);
break;

View file

@ -135,12 +135,12 @@ public class BuyerStep2View extends TradeStepView {
if (trade.isDepositConfirmed() && !trade.isFiatSent()) {
showPopup();
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG.ordinal()) {
} else if (state.ordinal() <= Trade.State.BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG.ordinal()) {
if (!trade.hasFailed()) {
UserThread.execute(() -> {
switch (state) {
case BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED:
case BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG:
case BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED:
case BUYER_SENT_PAYMENT_INITIATED_MSG:
busyAnimation.play();
statusLabel.setText(Res.get("shared.sendingConfirmation"));
model.setMessageStateProperty(MessageState.SENT);
@ -149,17 +149,17 @@ public class BuyerStep2View extends TradeStepView {
statusLabel.setText(Res.get("shared.sendingConfirmationAgain"));
}, 10);
break;
case BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG:
case BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageArrived"));
model.setMessageStateProperty(MessageState.ARRIVED);
break;
case BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG:
case BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG:
busyAnimation.stop();
statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
model.setMessageStateProperty(MessageState.STORED_IN_MAILBOX);
break;
case BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG:
case BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG:
// We get a popup and the trade closed, so we dont need to show anything here
busyAnimation.stop();
statusLabel.setText("");

View file

@ -116,7 +116,7 @@ public class SellerStep3View extends TradeStepView {
} else if (trade.isFiatReceived()) {
if (!trade.hasFailed()) {
switch (state) {
case SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT:
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT:
case SELLER_PUBLISHED_PAYOUT_TX:
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG:
busyAnimation.play();

View file

@ -39,34 +39,32 @@ message NetworkEnvelope {
InputsForDepositTxRequest inputs_for_deposit_tx_request = 17;
InputsForDepositTxResponse inputs_for_deposit_tx_response = 18;
DepositTxMessage deposit_tx_message = 19;
CounterCurrencyTransferStartedMessage counter_currency_transfer_started_message = 20;
PayoutTxPublishedMessage payout_tx_published_message = 21;
OpenNewDisputeMessage open_new_dispute_message = 22;
PeerOpenedDisputeMessage peer_opened_dispute_message = 23;
ChatMessage chat_message = 24;
DisputeResultMessage dispute_result_message = 25;
PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 26;
OpenNewDisputeMessage open_new_dispute_message = 20;
PeerOpenedDisputeMessage peer_opened_dispute_message = 21;
ChatMessage chat_message = 22;
DisputeResultMessage dispute_result_message = 23;
PeerPublishedDisputePayoutTxMessage peer_published_dispute_payout_tx_message = 24;
PrivateNotificationMessage private_notification_message = 27;
PrivateNotificationMessage private_notification_message = 25;
AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 28;
AckMessage ack_message = 29;
AddPersistableNetworkPayloadMessage add_persistable_network_payload_message = 26;
AckMessage ack_message = 27;
BundleOfEnvelopes bundle_of_envelopes = 30;
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 31;
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 32;
BundleOfEnvelopes bundle_of_envelopes = 28;
MediatedPayoutTxSignatureMessage mediated_payout_tx_signature_message = 29;
MediatedPayoutTxPublishedMessage mediated_payout_tx_published_message = 30;
DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 33;
DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 34;
DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 35;
PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 36;
DelayedPayoutTxSignatureRequest delayed_payout_tx_signature_request = 31;
DelayedPayoutTxSignatureResponse delayed_payout_tx_signature_response = 32;
DepositTxAndDelayedPayoutTxMessage deposit_tx_and_delayed_payout_tx_message = 33;
PeerPublishedDelayedPayoutTxMessage peer_published_delayed_payout_tx_message = 34;
RefreshTradeStateRequest refresh_trade_state_request = 37 [deprecated = true];
TraderSignedWitnessMessage trader_signed_witness_message = 38 [deprecated = true];
RefreshTradeStateRequest refresh_trade_state_request = 35 [deprecated = true];
TraderSignedWitnessMessage trader_signed_witness_message = 36 [deprecated = true];
GetInventoryRequest get_inventory_request = 39;
GetInventoryResponse get_inventory_response = 40;
GetInventoryRequest get_inventory_request = 37;
GetInventoryResponse get_inventory_response = 38;
SignOfferRequest sign_offer_request = 1001;
SignOfferResponse sign_offer_response = 1002;
@ -77,10 +75,13 @@ message NetworkEnvelope {
DepositRequest deposit_request = 1007;
DepositResponse deposit_response = 1008;
PaymentAccountPayloadRequest payment_account_payload_request = 1009;
UpdateMultisigRequest update_multisig_request = 1010;
UpdateMultisigResponse update_multisig_response = 1011;
ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012;
ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013;
PaymentSentMessage payment_sent_message = 1010;
PaymentReceivedMessage payment_received_message = 1011;
PayoutTxPublishedMessage payout_tx_published_message = 1012;
UpdateMultisigRequest update_multisig_request = 1013;
UpdateMultisigResponse update_multisig_response = 1014;
ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1015;
ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1016;
}
}
@ -428,16 +429,6 @@ message PeerPublishedDelayedPayoutTxMessage {
NodeAddress sender_node_address = 3;
}
message CounterCurrencyTransferStartedMessage {
string trade_id = 1;
string buyer_payout_address = 2;
NodeAddress sender_node_address = 3;
string buyer_payout_tx_signed = 4;
string counter_currency_tx_id = 5;
string uid = 6;
string counter_currency_extra_data = 7;
}
message FinalizePayoutTxRequest {
string trade_id = 1;
bytes seller_signature = 2;
@ -446,6 +437,33 @@ message FinalizePayoutTxRequest {
string uid = 5;
}
message PaymentSentMessage {
string trade_id = 1;
string buyer_payout_address = 2;
NodeAddress sender_node_address = 3;
string counter_currency_tx_id = 4;
string uid = 5;
string counter_currency_extra_data = 6;
string payout_tx_hex = 7;
string updated_multisig_hex = 8;
}
message PaymentReceivedMessage {
string trade_id = 1;
NodeAddress sender_node_address = 2;
string uid = 3;
SignedWitness signed_witness = 4; // Added in v1.4.0
string payout_tx_hex = 5;
}
message PayoutTxPublishedMessage {
string trade_id = 1;
NodeAddress sender_node_address = 2;
string uid = 3;
SignedWitness signed_witness = 4; // Added in v1.4.0
string payout_tx_hex = 5;
}
message ArbitratorPayoutTxRequest {
Dispute dispute = 1; // TODO (woodser): replace with trade id
NodeAddress sender_node_address = 2;
@ -462,14 +480,6 @@ message ArbitratorPayoutTxResponse {
string arbitrator_signed_payout_tx_hex = 5;
}
message PayoutTxPublishedMessage {
string trade_id = 1;
string signed_multisig_tx_hex = 2;
NodeAddress sender_node_address = 3;
string uid = 4;
SignedWitness signed_witness = 5; // Added in v1.4.0
}
message MediatedPayoutTxPublishedMessage {
string trade_id = 1;
bytes payout_tx = 2;
@ -1514,13 +1524,13 @@ message Trade {
MAKER_RECEIVED_DEPOSIT_TX_PUBLISHED_MSG = 16;
MAKER_SAW_DEPOSIT_TX_IN_NETWORK = 17;
DEPOSIT_CONFIRMED_IN_BLOCK_CHAIN = 18;
BUYER_CONFIRMED_IN_UI_FIAT_PAYMENT_INITIATED = 19;
BUYER_SENT_FIAT_PAYMENT_INITIATED_MSG = 20;
BUYER_SAW_ARRIVED_FIAT_PAYMENT_INITIATED_MSG = 21;
BUYER_STORED_IN_MAILBOX_FIAT_PAYMENT_INITIATED_MSG = 22;
BUYER_SEND_FAILED_FIAT_PAYMENT_INITIATED_MSG = 23;
SELLER_RECEIVED_FIAT_PAYMENT_INITIATED_MSG = 24;
SELLER_CONFIRMED_IN_UI_FIAT_PAYMENT_RECEIPT = 25;
BUYER_CONFIRMED_IN_UI_PAYMENT_INITIATED = 19;
BUYER_SENT_PAYMENT_INITIATED_MSG = 20;
BUYER_SAW_ARRIVED_PAYMENT_INITIATED_MSG = 21;
BUYER_STORED_IN_MAILBOX_PAYMENT_INITIATED_MSG = 22;
BUYER_SEND_FAILED_PAYMENT_INITIATED_MSG = 23;
SELLER_RECEIVED_PAYMENT_INITIATED_MSG = 24;
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 25;
SELLER_PUBLISHED_PAYOUT_TX = 26;
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 27;
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 28;
@ -1528,7 +1538,8 @@ message Trade {
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 30;
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 31;
BUYER_SAW_PAYOUT_TX_IN_NETWORK = 32;
WITHDRAW_COMPLETED = 33;
BUYER_PUBLISHED_PAYOUT_TX = 33;
WITHDRAW_COMPLETED = 34;
}
enum Phase {
@ -1537,8 +1548,8 @@ message Trade {
TAKER_FEE_PUBLISHED = 2;
DEPOSIT_PUBLISHED = 3;
DEPOSIT_CONFIRMED = 4;
FIAT_SENT = 5;
FIAT_RECEIVED = 6;
PAYMENT_SENT = 5;
PAYMENT_RECEIVED = 6;
PAYOUT_PUBLISHED = 7;
WITHDRAWN = 8;
}
@ -1686,10 +1697,11 @@ message TradingPeer {
repeated string reserve_tx_key_images = 1004;
string prepared_multisig_hex = 1005;
string made_multisig_hex = 1006;
string signed_payout_tx_hex = 1007;
string payout_tx_hex = 1007;
string deposit_tx_hash = 1008;
string deposit_tx_hex = 1009;
string deposit_tx_key = 1010;
string updated_multisig_hex = 1011;
}
///////////////////////////////////////////////////////////////////////////////////////////