mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-10-01 01:35:48 -04:00
refactor payout protocol
send payment key & multisig hex on deposit confirm for resilience support payout published, confirmed, unlocked states keep trade wallets open throughout trade close and delete trade wallets when payout unlocks arbitrator idles trade wallets after deposits confirm (1/hour)
This commit is contained in:
parent
45bac8c264
commit
f36dde2857
@ -16,7 +16,7 @@ import org.junit.jupiter.api.TestInfo;
|
||||
import static bisq.cli.table.builder.TableType.TRADE_DETAIL_TBL;
|
||||
import static bisq.core.trade.Trade.Phase.DEPOSITS_UNLOCKED;
|
||||
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.PAYMENT_RECEIVED;
|
||||
import static bisq.core.trade.Trade.State.BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG;
|
||||
import static bisq.core.trade.Trade.State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN;
|
||||
import static bisq.core.trade.Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG;
|
||||
@ -150,7 +150,7 @@ public class AbstractTradeTest extends AbstractOfferTest {
|
||||
String tradeId) {
|
||||
Predicate<TradeInfo> isTradeInPaymentReceiptConfirmedStateAndPhase = (t) ->
|
||||
t.getState().equals(SELLER_RECEIVED_PAYMENT_SENT_MSG.name()) &&
|
||||
(t.getPhase().equals(PAYOUT_PUBLISHED.name()) || t.getPhase().equals(PAYMENT_SENT.name()));
|
||||
t.getPhase().equals(PAYMENT_SENT.name());
|
||||
String userName = toUserName.apply(grpcClient);
|
||||
for (int i = 1; i <= maxTradeStateAndPhaseChecks.get(); i++) {
|
||||
TradeInfo trade = grpcClient.getTrade(tradeId);
|
||||
|
@ -30,13 +30,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import static bisq.apitest.config.ApiTestConfig.BTC;
|
||||
import static bisq.apitest.config.ApiTestConfig.USD;
|
||||
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.State.BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
|
||||
import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static protobuf.OfferDirection.BUY;
|
||||
import static protobuf.OpenOffer.State.AVAILABLE;
|
||||
@ -113,8 +110,8 @@ public class TakeBuyBTCOfferTest extends AbstractTradeTest {
|
||||
trade = bobClient.getTrade(tradeId);
|
||||
// Note: offer.state == available
|
||||
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
|
||||
.setPhase(PAYMENT_RECEIVED)
|
||||
.setPayoutPublished(true)
|
||||
.setPaymentReceivedMessageSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
|
@ -50,8 +50,8 @@ import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import static bisq.apitest.config.ApiTestConfig.BTC;
|
||||
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
|
||||
import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
|
||||
import static protobuf.OfferDirection.BUY;
|
||||
@ -200,8 +200,8 @@ public class TakeBuyBTCOfferWithNationalBankAcctTest extends AbstractTradeTest {
|
||||
trade = bobClient.getTrade(tradeId);
|
||||
// Note: offer.state == available
|
||||
assertEquals(AVAILABLE.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
|
||||
.setPhase(PAYMENT_RECEIVED)
|
||||
.setPayoutPublished(true)
|
||||
.setPaymentReceivedMessageSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
|
@ -29,11 +29,10 @@ import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestInfo;
|
||||
import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import static bisq.apitest.config.ApiTestConfig.BTC;
|
||||
import static bisq.apitest.config.ApiTestConfig.XMR;
|
||||
import static bisq.cli.table.builder.TableType.OFFER_TBL;
|
||||
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
|
||||
import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static protobuf.Offer.State.OFFER_FEE_RESERVED;
|
||||
@ -130,8 +129,8 @@ public class TakeBuyXMROfferTest extends AbstractTradeTest {
|
||||
|
||||
trade = aliceClient.getTrade(tradeId);
|
||||
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
|
||||
.setPhase(PAYMENT_RECEIVED)
|
||||
.setPayoutPublished(true)
|
||||
.setPaymentReceivedMessageSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
|
@ -32,10 +32,10 @@ import org.junit.jupiter.api.TestMethodOrder;
|
||||
|
||||
import static bisq.apitest.config.ApiTestConfig.BTC;
|
||||
import static bisq.apitest.config.ApiTestConfig.USD;
|
||||
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.Phase.WITHDRAWN;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
|
||||
import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED;
|
||||
import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
|
||||
import static bisq.core.trade.Trade.Phase.COMPLETED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
|
||||
import static bisq.core.trade.Trade.State.TRADE_COMPLETED;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
@ -119,8 +119,8 @@ public class TakeSellBTCOfferTest extends AbstractTradeTest {
|
||||
sleep(3_000);
|
||||
trade = aliceClient.getTrade(tradeId);
|
||||
assertEquals(OFFER_FEE_RESERVED.name(), trade.getOffer().getState());
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
|
||||
.setPhase(PAYMENT_RECEIVED)
|
||||
.setPayoutPublished(true)
|
||||
.setPaymentReceivedMessageSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
|
@ -32,12 +32,9 @@ import org.junit.jupiter.api.TestMethodOrder;
|
||||
import static bisq.apitest.config.ApiTestConfig.BTC;
|
||||
import static bisq.apitest.config.ApiTestConfig.XMR;
|
||||
import static bisq.cli.table.builder.TableType.OFFER_TBL;
|
||||
import static bisq.core.trade.Trade.Phase.PAYOUT_PUBLISHED;
|
||||
import static bisq.core.trade.Trade.Phase.WITHDRAWN;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG;
|
||||
import static bisq.core.trade.Trade.State.WITHDRAW_COMPLETED;
|
||||
import static bisq.core.trade.Trade.Phase.PAYMENT_RECEIVED;
|
||||
import static bisq.core.trade.Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.junit.jupiter.api.Assertions.fail;
|
||||
import static protobuf.OfferDirection.BUY;
|
||||
|
||||
@ -139,8 +136,8 @@ public class TakeSellXMROfferTest extends AbstractTradeTest {
|
||||
|
||||
trade = bobClient.getTrade(tradeId);
|
||||
// Warning: trade.getOffer().getState() might be AVAILABLE, not OFFER_FEE_RESERVED.
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG)
|
||||
.setPhase(PAYOUT_PUBLISHED)
|
||||
EXPECTED_PROTOCOL_STATUS.setState(SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG)
|
||||
.setPhase(PAYMENT_RECEIVED)
|
||||
.setPayoutPublished(true)
|
||||
.setPaymentReceivedMessageSent(true);
|
||||
verifyExpectedProtocolStatus(trade);
|
||||
|
@ -105,9 +105,6 @@ public class CoreDisputesService {
|
||||
String updatedMultisigHex = multisigWallet.exportMultisigHex();
|
||||
disputeManager.sendOpenNewDisputeMessage(dispute, false, updatedMultisigHex, resultHandler, faultHandler);
|
||||
tradeManager.requestPersistence();
|
||||
|
||||
// close multisig wallet
|
||||
xmrWalletService.closeMultisigWallet(trade.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@ -159,7 +156,8 @@ public class CoreDisputesService {
|
||||
if (disputeOptional.isPresent()) dispute = disputeOptional.get();
|
||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||
|
||||
synchronized (tradeManager.getTrade(tradeId)) {
|
||||
Trade trade = tradeManager.getTrade(tradeId);
|
||||
synchronized (trade) {
|
||||
var closeDate = new Date();
|
||||
var disputeResult = createDisputeResult(dispute, winner, reason, summaryNotes, closeDate);
|
||||
var contract = dispute.getContract();
|
||||
@ -176,8 +174,8 @@ public class CoreDisputesService {
|
||||
}
|
||||
applyPayoutAmountsToDisputeResult(payout, dispute, disputeResult, customWinnerAmount);
|
||||
|
||||
// resolve the payout
|
||||
resolveDisputePayout(dispute, disputeResult, contract);
|
||||
// apply dispute payout
|
||||
applyDisputePayout(dispute, disputeResult, contract);
|
||||
|
||||
// close dispute ticket
|
||||
closeDispute(arbitrationManager, dispute, disputeResult, false);
|
||||
@ -186,19 +184,17 @@ public class CoreDisputesService {
|
||||
var peersDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream()
|
||||
.filter(d -> tradeId.equals(d.getTradeId()) && dispute.getTraderId() != d.getTraderId())
|
||||
.findFirst();
|
||||
|
||||
if (peersDisputeOptional.isPresent()) {
|
||||
var peerDispute = peersDisputeOptional.get();
|
||||
var peerDisputeResult = createDisputeResult(peerDispute, winner, reason, summaryNotes, closeDate);
|
||||
peerDisputeResult.setBuyerPayoutAmount(disputeResult.getBuyerPayoutAmount());
|
||||
peerDisputeResult.setSellerPayoutAmount(disputeResult.getSellerPayoutAmount());
|
||||
peerDisputeResult.setLoserPublisher(disputeResult.isLoserPublisher());
|
||||
resolveDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
|
||||
applyDisputePayout(peerDispute, peerDisputeResult, peerDispute.getContract());
|
||||
closeDispute(arbitrationManager, peerDispute, peerDisputeResult, false);
|
||||
} else {
|
||||
throw new IllegalStateException("could not find peer dispute");
|
||||
}
|
||||
|
||||
arbitrationManager.requestPersistence();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -250,7 +246,7 @@ public class CoreDisputesService {
|
||||
}
|
||||
}
|
||||
|
||||
public void resolveDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) {
|
||||
public void applyDisputePayout(Dispute dispute, DisputeResult disputeResult, Contract contract) {
|
||||
// TODO (woodser): create disputed payout tx after showing payout tx confirmation, within doCloseIfValid() (see upstream/master)
|
||||
if (!dispute.isMediationDispute()) {
|
||||
try {
|
||||
@ -259,30 +255,25 @@ public class CoreDisputesService {
|
||||
//dispute.getContract().getArbitratorPubKeyRing(); // TODO: support arbitrator pub key ring in contract?
|
||||
//disputeResult.setArbitratorPubKey(arbitratorAddressEntry.getPubKey());
|
||||
|
||||
// TODO (woodser): don't send signed tx if opener is not co-signer?
|
||||
// // determine if opener is co-signer
|
||||
// boolean openerIsWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == Winner.SELLER);
|
||||
// boolean openerIsCosigner = openerIsWinner || disputeResult.isLoserPublisher();
|
||||
// if (!openerIsCosigner) throw new RuntimeException("Need to query non-opener for updated multisig hex before creating tx");
|
||||
// determine if dispute is in context of publisher
|
||||
boolean isOpener = dispute.isOpener();
|
||||
boolean isWinner = (contract.getBuyerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.BUYER) || (contract.getSellerPubKeyRing().equals(dispute.getTraderPubKeyRing()) && disputeResult.getWinner() == DisputeResult.Winner.SELLER);
|
||||
boolean isPublisher = disputeResult.isLoserPublisher() ? !isWinner : isWinner;
|
||||
|
||||
// open multisig wallet
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||
|
||||
// arbitrator creates and signs dispute payout tx if dispute is in context of opener, otherwise opener's peer must request payout tx by providing updated multisig hex
|
||||
boolean isOpener = dispute.isOpener();
|
||||
System.out.println("Is dispute opener: " + isOpener);
|
||||
// if dispute is in context of opener, arbitrator has multisig hex to create and validate payout tx
|
||||
if (isOpener) {
|
||||
MoneroTxWallet arbitratorPayoutTx = ArbitrationManager.arbitratorCreatesDisputedPayoutTx(contract, dispute, disputeResult, multisigWallet);
|
||||
System.out.println("Created arbitrator-signed payout tx: " + arbitratorPayoutTx);
|
||||
if (arbitratorPayoutTx != null)
|
||||
disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
|
||||
|
||||
// if opener is publisher, include signed payout tx in dispute result, otherwise publisher must request payout tx by providing updated multisig hex
|
||||
if (isPublisher) disputeResult.setArbitratorSignedPayoutTxHex(arbitratorPayoutTx.getTxSet().getMultisigTxHex());
|
||||
}
|
||||
|
||||
// send arbitrator's updated multisig hex with dispute result
|
||||
disputeResult.setArbitratorUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
||||
|
||||
// close multisig wallet
|
||||
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
|
||||
}
|
||||
} catch (AddressFormatException e2) {
|
||||
log.error("Error at close dispute", e2);
|
||||
|
@ -5,7 +5,7 @@ import bisq.common.config.Config;
|
||||
import bisq.core.btc.model.EncryptedConnectionList;
|
||||
import bisq.core.btc.setup.DownloadListener;
|
||||
import bisq.core.btc.setup.WalletsSetup;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
@ -239,7 +239,7 @@ public final class CoreMoneroConnectionsService {
|
||||
public long getDefaultRefreshPeriodMs() {
|
||||
if (daemon == null) return REFRESH_PERIOD_LOCAL_MS;
|
||||
else {
|
||||
boolean isLocal = TradeUtils.isLocalHost(daemon.getRpcConnection().getUri());
|
||||
boolean isLocal = HavenoUtils.isLocalHost(daemon.getRpcConnection().getUri());
|
||||
if (isLocal) {
|
||||
updateDaemonInfo();
|
||||
if (lastInfo != null && (lastInfo.isBusySyncing() || (lastInfo.getHeightWithoutBootstrap() != null && lastInfo.getHeightWithoutBootstrap() > 0 && lastInfo.getHeightWithoutBootstrap() < lastInfo.getHeight()))) return REFRESH_PERIOD_REMOTE_MS; // refresh slower if syncing or bootstrapped
|
||||
@ -363,7 +363,7 @@ public final class CoreMoneroConnectionsService {
|
||||
// if offline and last connection is local, start local node if offline
|
||||
currentConnectionUri.ifPresent(uri -> {
|
||||
try {
|
||||
if (!connectionManager.isConnected() && TradeUtils.isLocalHost(uri) && !nodeService.isOnline()) {
|
||||
if (!connectionManager.isConnected() && HavenoUtils.isLocalHost(uri) && !nodeService.isOnline()) {
|
||||
nodeService.startMoneroNode();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
@ -372,7 +372,7 @@ public final class CoreMoneroConnectionsService {
|
||||
});
|
||||
|
||||
// prefer to connect to local node unless prevented by configuration
|
||||
if (("".equals(config.xmrNode) || TradeUtils.isLocalHost(config.xmrNode)) &&
|
||||
if (("".equals(config.xmrNode) || HavenoUtils.isLocalHost(config.xmrNode)) &&
|
||||
(!connectionManager.isConnected() || connectionManager.getAutoSwitch()) &&
|
||||
nodeService.isConnected()) {
|
||||
MoneroRpcConnection connection = connectionManager.getConnectionByUri(nodeService.getDaemon().getRpcConnection().getUri());
|
||||
|
@ -16,7 +16,7 @@
|
||||
*/
|
||||
package bisq.core.api;
|
||||
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.xmr.MoneroNodeSettings;
|
||||
import bisq.common.config.BaseCurrencyNetwork;
|
||||
@ -71,7 +71,7 @@ public class CoreMoneroNodeService {
|
||||
else if (Config.baseCurrencyNetwork().isTestnet()) rpcPort = 28081;
|
||||
else if (Config.baseCurrencyNetwork().isStagenet()) rpcPort = 38081;
|
||||
else throw new RuntimeException("Base network is not local testnet, stagenet, or mainnet");
|
||||
this.daemon = new MoneroDaemonRpc("http://" + TradeUtils.LOOPBACK_HOST + ":" + rpcPort);
|
||||
this.daemon = new MoneroDaemonRpc("http://" + HavenoUtils.LOOPBACK_HOST + ":" + rpcPort);
|
||||
}
|
||||
|
||||
public void addListener(MoneroNodeServiceListener listener) {
|
||||
|
@ -79,12 +79,13 @@ public class TradeInfo implements Payload {
|
||||
private final String state;
|
||||
private final String phase;
|
||||
private final String periodState;
|
||||
private final String payoutState;
|
||||
private final boolean isDepositPublished;
|
||||
private final boolean isDepositUnlocked;
|
||||
private final boolean isPaymentSent;
|
||||
private final boolean isPaymentReceived;
|
||||
private final boolean isPayoutPublished;
|
||||
private final boolean isCompleted;
|
||||
private final boolean isPayoutPublished;
|
||||
private final String contractAsJson;
|
||||
private final ContractInfo contract;
|
||||
|
||||
@ -107,6 +108,7 @@ public class TradeInfo implements Payload {
|
||||
this.state = builder.getState();
|
||||
this.phase = builder.getPhase();
|
||||
this.periodState = builder.getPeriodState();
|
||||
this.payoutState = builder.getPayoutState();
|
||||
this.isDepositPublished = builder.isDepositPublished();
|
||||
this.isDepositUnlocked = builder.isDepositUnlocked();
|
||||
this.isPaymentSent = builder.isPaymentSent();
|
||||
@ -158,6 +160,7 @@ public class TradeInfo implements Payload {
|
||||
.withState(trade.getState().name())
|
||||
.withPhase(trade.getPhase().name())
|
||||
.withPeriodState(trade.getPeriodState().name())
|
||||
.withPayoutState(trade.getPayoutState().name())
|
||||
.withIsDepositPublished(trade.isDepositPublished())
|
||||
.withIsDepositUnlocked(trade.isDepositUnlocked())
|
||||
.withIsPaymentSent(trade.isPaymentSent())
|
||||
@ -195,12 +198,13 @@ public class TradeInfo implements Payload {
|
||||
.setState(state)
|
||||
.setPhase(phase)
|
||||
.setPeriodState(periodState)
|
||||
.setPayoutState(payoutState)
|
||||
.setIsDepositPublished(isDepositPublished)
|
||||
.setIsDepositUnlocked(isDepositUnlocked)
|
||||
.setIsPaymentSent(isPaymentSent)
|
||||
.setIsPaymentReceived(isPaymentReceived)
|
||||
.setIsCompleted(isCompleted)
|
||||
.setIsPayoutPublished(isPayoutPublished)
|
||||
.setIsPayoutPublished(isCompleted)
|
||||
.setContractAsJson(contractAsJson == null ? "" : contractAsJson)
|
||||
.setContract(contract.toProtoMessage())
|
||||
.build();
|
||||
@ -222,6 +226,7 @@ public class TradeInfo implements Payload {
|
||||
.withPrice(proto.getPrice())
|
||||
.withVolume(proto.getTradeVolume())
|
||||
.withPeriodState(proto.getPeriodState())
|
||||
.withPayoutState(proto.getPayoutState())
|
||||
.withState(proto.getState())
|
||||
.withPhase(proto.getPhase())
|
||||
.withArbitratorNodeAddress(proto.getArbitratorNodeAddress())
|
||||
@ -230,8 +235,8 @@ public class TradeInfo implements Payload {
|
||||
.withIsDepositUnlocked(proto.getIsDepositUnlocked())
|
||||
.withIsPaymentSent(proto.getIsPaymentSent())
|
||||
.withIsPaymentReceived(proto.getIsPaymentReceived())
|
||||
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
||||
.withIsCompleted(proto.getIsCompleted())
|
||||
.withIsPayoutPublished(proto.getIsPayoutPublished())
|
||||
.withContractAsJson(proto.getContractAsJson())
|
||||
.withContract((ContractInfo.fromProto(proto.getContract())))
|
||||
.build();
|
||||
@ -256,12 +261,13 @@ public class TradeInfo implements Payload {
|
||||
", state='" + state + '\'' + "\n" +
|
||||
", phase='" + phase + '\'' + "\n" +
|
||||
", periodState='" + periodState + '\'' + "\n" +
|
||||
", payoutState='" + payoutState + '\'' + "\n" +
|
||||
", isDepositPublished=" + isDepositPublished + "\n" +
|
||||
", isDepositConfirmed=" + isDepositUnlocked + "\n" +
|
||||
", isPaymentSent=" + isPaymentSent + "\n" +
|
||||
", isPaymentReceived=" + isPaymentReceived + "\n" +
|
||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||
", isCompleted=" + isCompleted + "\n" +
|
||||
", isPayoutPublished=" + isPayoutPublished + "\n" +
|
||||
", offer=" + offer + "\n" +
|
||||
", contractAsJson=" + contractAsJson + "\n" +
|
||||
", contract=" + contract + "\n" +
|
||||
|
@ -51,6 +51,7 @@ public final class TradeInfoV1Builder {
|
||||
private String state;
|
||||
private String phase;
|
||||
private String periodState;
|
||||
private String payoutState;
|
||||
private boolean isDepositPublished;
|
||||
private boolean isDepositUnlocked;
|
||||
private boolean isPaymentSent;
|
||||
@ -131,11 +132,6 @@ public final class TradeInfoV1Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withPeriodState(String periodState) {
|
||||
this.periodState = periodState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withState(String state) {
|
||||
this.state = state;
|
||||
return this;
|
||||
@ -146,6 +142,16 @@ public final class TradeInfoV1Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withPeriodState(String periodState) {
|
||||
this.periodState = periodState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withPayoutState(String payoutState) {
|
||||
this.payoutState = payoutState;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TradeInfoV1Builder withArbitratorNodeAddress(String arbitratorNodeAddress) {
|
||||
this.arbitratorNodeAddress = arbitratorNodeAddress;
|
||||
return this;
|
||||
|
@ -27,6 +27,7 @@ import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.setup.CorePersistedDataHost;
|
||||
import bisq.core.setup.CoreSetup;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.trade.txproof.xmr.XmrTxProofService;
|
||||
import bisq.network.p2p.P2PService;
|
||||
@ -279,6 +280,7 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
|
||||
injector.getInstance(TradeStatisticsManager.class).shutDown();
|
||||
injector.getInstance(XmrTxProofService.class).shutDown();
|
||||
injector.getInstance(AvoidStandbyModeService.class).shutDown();
|
||||
injector.getInstance(TradeManager.class).shutDown();
|
||||
injector.getInstance(XmrWalletService.class).shutDown(); // TODO: why not shut down BtcWalletService, etc? shutdown CoreMoneroConnectionsService
|
||||
log.info("OpenOfferManager shutdown started");
|
||||
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
|
||||
|
@ -556,7 +556,6 @@ public class HavenoSetup {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static boolean getResyncSpvSemaphore() {
|
||||
File resyncSpvSemaphore = new File(Config.appDataDir(), RESYNC_SPV_FILE_NAME);
|
||||
return resyncSpvSemaphore.exists();
|
||||
|
@ -0,0 +1,13 @@
|
||||
package bisq.core.btc.wallet;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
|
||||
public interface MoneroKeyImageListener {
|
||||
|
||||
/**
|
||||
* Called with changes to the spent status of key images.
|
||||
*/
|
||||
public void onSpentStatusChanged(Map<String, MoneroKeyImageSpentStatus> spentStatuses);
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
package bisq.core.btc.wallet;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import monero.common.MoneroError;
|
||||
import monero.common.TaskLooper;
|
||||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
|
||||
/**
|
||||
* Poll for changes to the spent status of key images.
|
||||
*/
|
||||
public class MoneroKeyImagePoller {
|
||||
|
||||
private MoneroDaemon daemon;
|
||||
private long refreshPeriodMs;
|
||||
private List<String> keyImages = new ArrayList<String>();
|
||||
private Set<MoneroKeyImageListener> listeners = new HashSet<MoneroKeyImageListener>();
|
||||
private TaskLooper looper;
|
||||
private Map<String, MoneroKeyImageSpentStatus> lastStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
|
||||
|
||||
/**
|
||||
* Construct the listener.
|
||||
*
|
||||
* @param refreshPeriodMs - refresh period in milliseconds
|
||||
* @param keyImages - key images to listen to
|
||||
*/
|
||||
public MoneroKeyImagePoller(MoneroDaemon daemon, long refreshPeriodMs, String... keyImages) {
|
||||
looper = new TaskLooper(() -> poll());
|
||||
setDaemon(daemon);
|
||||
setRefreshPeriodMs(refreshPeriodMs);
|
||||
setKeyImages(keyImages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a listener to receive notifications.
|
||||
*
|
||||
* @param listener - the listener to add
|
||||
*/
|
||||
public void addListener(MoneroKeyImageListener listener) {
|
||||
listeners.add(listener);
|
||||
refreshPolling();
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a listener to receive notifications.
|
||||
*
|
||||
* @param listener - the listener to remove
|
||||
*/
|
||||
public void removeListener(MoneroKeyImageListener listener) {
|
||||
if (!listeners.contains(listener)) throw new MoneroError("Listener is not registered");
|
||||
listeners.remove(listener);
|
||||
refreshPolling();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the Monero daemon to fetch key images from.
|
||||
*
|
||||
* @param daemon - the daemon to fetch key images from
|
||||
*/
|
||||
public void setDaemon(MoneroDaemon daemon) {
|
||||
this.daemon = daemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Monero daemon to fetch key images from.
|
||||
*
|
||||
* @return the daemon to fetch key images from
|
||||
*/
|
||||
public MoneroDaemon getDaemon() {
|
||||
return daemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the refresh period in milliseconds.
|
||||
*
|
||||
* @param refreshPeriodMs - the refresh period in milliseconds
|
||||
*/
|
||||
public void setRefreshPeriodMs(long refreshPeriodMs) {
|
||||
this.refreshPeriodMs = refreshPeriodMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the refresh period in milliseconds
|
||||
*
|
||||
* @return the refresh period in milliseconds
|
||||
*/
|
||||
public long getRefreshPeriodMs() {
|
||||
return refreshPeriodMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a copy of the key images being listened to.
|
||||
*
|
||||
* @return the key images to listen to
|
||||
*/
|
||||
public Collection<String> getKeyImages() {
|
||||
return new ArrayList<String>(keyImages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the key images to listen to.
|
||||
*
|
||||
* @return the key images to listen to
|
||||
*/
|
||||
public void setKeyImages(String... keyImages) {
|
||||
synchronized (keyImages) {
|
||||
this.keyImages.clear();
|
||||
this.keyImages.addAll(Arrays.asList(keyImages));
|
||||
refreshPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key image to listen to.
|
||||
*
|
||||
* @param keyImage - the key image to listen to
|
||||
*/
|
||||
public void addKeyImage(String keyImage) {
|
||||
synchronized (keyImages) {
|
||||
addKeyImages(keyImage);
|
||||
refreshPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add key images to listen to.
|
||||
*
|
||||
* @param keyImages - key images to listen to
|
||||
*/
|
||||
public void addKeyImages(String... keyImages) {
|
||||
synchronized (keyImages) {
|
||||
for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) this.keyImages.add(keyImage);
|
||||
refreshPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a key image to listen to.
|
||||
*
|
||||
* @param keyImage - the key image to unlisten to
|
||||
*/
|
||||
public void removeKeyImage(String keyImage) {
|
||||
synchronized (keyImages) {
|
||||
removeKeyImages(keyImage);
|
||||
refreshPolling();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove key images to listen to.
|
||||
*
|
||||
* @param keyImages - key images to unlisten to
|
||||
*/
|
||||
public void removeKeyImages(String... keyImages) {
|
||||
synchronized (keyImages) {
|
||||
for (String keyImage : keyImages) if (!this.keyImages.contains(keyImage)) throw new MoneroError("Key image not registered with poller: " + keyImage);
|
||||
this.keyImages.removeAll(Arrays.asList(keyImages));
|
||||
}
|
||||
}
|
||||
|
||||
public void poll() {
|
||||
synchronized (keyImages) {
|
||||
|
||||
// fetch spent statuses
|
||||
List<MoneroKeyImageSpentStatus> spentStatuses = keyImages.isEmpty() ? new ArrayList<MoneroKeyImageSpentStatus>() : daemon.getKeyImageSpentStatuses(keyImages);
|
||||
|
||||
// collect changed statuses
|
||||
Map<String, MoneroKeyImageSpentStatus> changedStatuses = new HashMap<String, MoneroKeyImageSpentStatus>();
|
||||
for (int i = 0; i < keyImages.size(); i++) {
|
||||
if (lastStatuses.get(keyImages.get(i)) != spentStatuses.get(i)) {
|
||||
lastStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
||||
changedStatuses.put(keyImages.get(i), spentStatuses.get(i));
|
||||
}
|
||||
}
|
||||
|
||||
// announce changes
|
||||
for (MoneroKeyImageListener listener : new ArrayList<MoneroKeyImageListener>(listeners)) listener.onSpentStatusChanged(changedStatuses);
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshPolling() {
|
||||
setIsPolling(listeners.size() > 0);
|
||||
}
|
||||
|
||||
private void setIsPolling(boolean isPolling) {
|
||||
if (isPolling) looper.start(refreshPeriodMs);
|
||||
else looper.stop();
|
||||
}
|
||||
}
|
@ -20,8 +20,10 @@ 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.trade.HavenoUtils;
|
||||
import bisq.core.util.ParsingUtils;
|
||||
|
||||
import com.google.common.collect.TreeMultimap;
|
||||
import com.google.common.util.concurrent.Service.State;
|
||||
import com.google.inject.name.Named;
|
||||
import common.utils.JsonUtils;
|
||||
@ -29,6 +31,7 @@ import java.io.File;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
@ -47,6 +50,7 @@ import monero.common.MoneroRpcConnection;
|
||||
import monero.common.MoneroRpcError;
|
||||
import monero.common.MoneroUtils;
|
||||
import monero.daemon.MoneroDaemonRpc;
|
||||
import monero.daemon.model.MoneroKeyImageSpentStatus;
|
||||
import monero.daemon.model.MoneroNetworkType;
|
||||
import monero.daemon.model.MoneroOutput;
|
||||
import monero.daemon.model.MoneroSubmitTxResult;
|
||||
@ -97,6 +101,7 @@ public class XmrWalletService {
|
||||
private TradeManager tradeManager;
|
||||
private MoneroWalletRpc wallet;
|
||||
private Map<String, MoneroWallet> multisigWallets;
|
||||
private Map<String, Object> walletLocks = new HashMap<String, Object>();
|
||||
private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
|
||||
|
||||
@Inject
|
||||
@ -182,29 +187,40 @@ public class XmrWalletService {
|
||||
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
|
||||
}
|
||||
|
||||
public boolean multisigWalletExists(String tradeId) {
|
||||
return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId);
|
||||
private synchronized void initWalletLock(String id) {
|
||||
if (!walletLocks.containsKey(id)) walletLocks.put(id, new Object());
|
||||
}
|
||||
|
||||
public boolean multisigWalletExists(String tradeId) {
|
||||
initWalletLock(tradeId);
|
||||
synchronized(walletLocks.get(tradeId)) {
|
||||
return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (woodser): test retaking failed trade. create new multisig wallet or replace? cannot reuse
|
||||
public MoneroWallet createMultisigWallet(String tradeId) {
|
||||
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, false); // auto-assign port
|
||||
multisigWallets.put(tradeId, multisigWallet);
|
||||
return multisigWallet;
|
||||
initWalletLock(tradeId);
|
||||
synchronized(walletLocks.get(tradeId)) {
|
||||
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port
|
||||
multisigWallets.put(tradeId, multisigWallet);
|
||||
return multisigWallet;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (woodser): provide progress notifications during open?
|
||||
public MoneroWallet getMultisigWallet(String tradeId) {
|
||||
log.info("{}.getMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||
if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId);
|
||||
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
|
||||
multisigWallets.put(tradeId, multisigWallet);
|
||||
return multisigWallet;
|
||||
initWalletLock(tradeId);
|
||||
synchronized(walletLocks.get(tradeId)) {
|
||||
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId);
|
||||
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||
if (!walletExists(path)) throw new RuntimeException("Multisig wallet does not exist for trade " + tradeId);
|
||||
MoneroWallet multisigWallet = openWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null);
|
||||
multisigWallets.put(tradeId, multisigWallet);
|
||||
return multisigWallet;
|
||||
}
|
||||
}
|
||||
|
||||
public void saveWallet(MoneroWallet wallet) {
|
||||
@ -213,19 +229,25 @@ public class XmrWalletService {
|
||||
}
|
||||
|
||||
public void closeMultisigWallet(String tradeId) {
|
||||
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId);
|
||||
MoneroWallet wallet = multisigWallets.remove(tradeId);
|
||||
closeWallet(wallet, true);
|
||||
initWalletLock(tradeId);
|
||||
synchronized(walletLocks.get(tradeId)) {
|
||||
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId);
|
||||
MoneroWallet wallet = multisigWallets.remove(tradeId);
|
||||
closeWallet(wallet, true);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean deleteMultisigWallet(String tradeId) {
|
||||
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||
if (!walletExists(walletName)) return false;
|
||||
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); // TODO: synchronize
|
||||
deleteWallet(walletName);
|
||||
return true;
|
||||
initWalletLock(tradeId);
|
||||
synchronized(walletLocks.get(tradeId)) {
|
||||
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
|
||||
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
|
||||
if (!walletExists(walletName)) return false;
|
||||
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId);
|
||||
deleteWallet(walletName);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
|
||||
@ -254,21 +276,20 @@ public class XmrWalletService {
|
||||
// get expected mining fee
|
||||
MoneroTxWallet miningFeeTx = wallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.addDestination(TradeUtils.getTradeFeeAddress(), tradeFee)
|
||||
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
||||
.addDestination(returnAddress, depositAmount));
|
||||
BigInteger miningFee = miningFeeTx.getFee();
|
||||
|
||||
// create reserve tx
|
||||
MoneroTxWallet reserveTx = wallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.addDestination(TradeUtils.getTradeFeeAddress(), tradeFee)
|
||||
.addDestination(HavenoUtils.getTradeFeeAddress(), 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
|
||||
if (freezeInputs) {
|
||||
for (MoneroOutput input : reserveTx.getInputs()) {
|
||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
||||
}
|
||||
for (MoneroOutput input : reserveTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
|
||||
wallet.save();
|
||||
}
|
||||
|
||||
return reserveTx;
|
||||
@ -291,13 +312,12 @@ public class XmrWalletService {
|
||||
// create deposit tx
|
||||
MoneroTxWallet depositTx = wallet.createTx(new MoneroTxConfig()
|
||||
.setAccountIndex(0)
|
||||
.addDestination(TradeUtils.getTradeFeeAddress(), tradeFee)
|
||||
.addDestination(HavenoUtils.getTradeFeeAddress(), tradeFee)
|
||||
.addDestination(multisigAddress, depositAmount));
|
||||
|
||||
// freeze deposit inputs
|
||||
for (MoneroOutput input : depositTx.getInputs()) {
|
||||
wallet.freezeOutput(input.getKeyImage().getHex());
|
||||
}
|
||||
for (MoneroOutput input : depositTx.getInputs()) wallet.freezeOutput(input.getKeyImage().getHex());
|
||||
wallet.save();
|
||||
|
||||
return depositTx;
|
||||
}
|
||||
@ -342,7 +362,7 @@ public class XmrWalletService {
|
||||
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
|
||||
|
||||
// verify trade fee
|
||||
String feeAddress = TradeUtils.getTradeFeeAddress();
|
||||
String feeAddress = HavenoUtils.getTradeFeeAddress();
|
||||
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());
|
||||
@ -435,10 +455,8 @@ public class XmrWalletService {
|
||||
// initialize main wallet if connected or previously created
|
||||
maybeInitMainWallet();
|
||||
|
||||
// update wallet connections on change
|
||||
connectionsService.addListener(newConnection -> {
|
||||
setWalletDaemonConnections(newConnection);
|
||||
});
|
||||
// set and listen to daemon connection
|
||||
connectionsService.addListener(newConnection -> setDaemonConnection(newConnection));
|
||||
}
|
||||
|
||||
private boolean walletExists(String walletName) {
|
||||
@ -580,17 +598,13 @@ public class XmrWalletService {
|
||||
return MONERO_WALLET_RPC_MANAGER.startInstance(cmd);
|
||||
}
|
||||
|
||||
private void setWalletDaemonConnections(MoneroRpcConnection connection) {
|
||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||
log.info("Setting wallet daemon connection: " + (connection == null ? null : connection.getUri()));
|
||||
if (wallet == null) maybeInitMainWallet();
|
||||
if (wallet != null) {
|
||||
wallet.setDaemonConnection(connection);
|
||||
wallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs());
|
||||
}
|
||||
for (MoneroWallet multisigWallet : multisigWallets.values()) {
|
||||
multisigWallet.setDaemonConnection(connection);
|
||||
multisigWallet.startSyncing(connectionsService.getDefaultRefreshPeriodMs()); // TODO: optimize when multisig wallets are open and syncing
|
||||
}
|
||||
}
|
||||
|
||||
private void notifyBalanceListeners() {
|
||||
@ -663,7 +677,7 @@ public class XmrWalletService {
|
||||
if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
|
||||
deleteBackupWallets(walletName);
|
||||
deleteBackupWallets(walletName); // TODO: retain backup for some time?
|
||||
}
|
||||
|
||||
private void closeAllWallets() {
|
||||
@ -675,30 +689,16 @@ public class XmrWalletService {
|
||||
openWallets.add(multisigWallets.get(multisigWalletKey));
|
||||
}
|
||||
|
||||
// done if no open wallets
|
||||
if (openWallets.isEmpty()) return;
|
||||
|
||||
// close all wallets in parallel
|
||||
ExecutorService pool = Executors.newFixedThreadPool(Math.min(10, openWallets.size()));
|
||||
for (MoneroWallet openWallet : openWallets) {
|
||||
pool.submit(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
closeWallet(openWallet, true);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
||||
} catch (InterruptedException e) {
|
||||
pool.shutdownNow();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
// close wallets in parallel
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
for (MoneroWallet wallet : openWallets) tasks.add(() -> {
|
||||
try {
|
||||
closeWallet(wallet, true);
|
||||
} catch (Exception e) {
|
||||
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||
}
|
||||
});
|
||||
HavenoUtils.awaitTasks(tasks);
|
||||
|
||||
// clear wallets
|
||||
wallet = null;
|
||||
|
@ -82,13 +82,11 @@ public class TradeEvents {
|
||||
msg = Res.get("account.notifications.trade.message.msg.started", shortId);
|
||||
break;
|
||||
case PAYMENT_RECEIVED:
|
||||
break;
|
||||
case PAYOUT_PUBLISHED:
|
||||
// We only notify the buyer
|
||||
if (trade.getContract() != null && pubKeyRingProvider.get().equals(trade.getContract().getBuyerPubKeyRing()))
|
||||
msg = Res.get("account.notifications.trade.message.msg.completed", shortId);
|
||||
break;
|
||||
case WITHDRAWN:
|
||||
case COMPLETED:
|
||||
break;
|
||||
}
|
||||
if (msg != null) {
|
||||
|
@ -23,12 +23,10 @@ import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.locale.CurrencyUtil;
|
||||
import bisq.core.locale.Res;
|
||||
import bisq.core.monetary.Price;
|
||||
import bisq.core.offer.availability.DisputeAgentSelection;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.payment.PaymentAccountUtil;
|
||||
import bisq.core.provider.price.MarketPrice;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.Preferences;
|
||||
@ -48,9 +46,6 @@ import org.bitcoinj.core.Coin;
|
||||
import javax.inject.Inject;
|
||||
import javax.inject.Singleton;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
@ -22,7 +22,7 @@ import bisq.core.filter.FilterManager;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.payment.PaymentAccountUtil;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.user.User;
|
||||
|
||||
@ -224,6 +224,6 @@ public class OfferFilterService {
|
||||
public boolean hasValidSignature(Offer offer) {
|
||||
Arbitrator arbitrator = user.getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner());
|
||||
if (arbitrator == null) return false; // invalid arbitrator
|
||||
return TradeUtils.isArbitratorSignatureValid(offer, arbitrator);
|
||||
return HavenoUtils.isArbitratorSignatureValid(offer, arbitrator);
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
|
||||
import bisq.core.support.dispute.mediation.mediator.MediatorManager;
|
||||
import bisq.core.trade.ClosedTradableManager;
|
||||
import bisq.core.trade.TradableList;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.trade.handlers.TransactionResultHandler;
|
||||
import bisq.core.trade.statistics.TradeStatisticsManager;
|
||||
import bisq.core.user.Preferences;
|
||||
@ -564,6 +564,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
Offer offer = openOffer.getOffer();
|
||||
if (offer.getOfferPayload().getReserveTxKeyImages() != null) {
|
||||
for (String frozenKeyImage : offer.getOfferPayload().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(frozenKeyImage);
|
||||
xmrWalletService.getWallet().save();
|
||||
}
|
||||
offer.setState(Offer.State.REMOVED);
|
||||
openOffer.setState(OpenOffer.State.CANCELED);
|
||||
@ -634,7 +635,7 @@ public class OpenOfferManager implements PeerManager.Listener, DecryptedDirectMe
|
||||
latch.countDown();
|
||||
errorMessages.add(errorMessage);
|
||||
});
|
||||
TradeUtils.awaitLatch(latch);
|
||||
HavenoUtils.awaitLatch(latch);
|
||||
}
|
||||
requestPersistence();
|
||||
if (errorMessages.size() > 0) errorMessageHandler.handleErrorMessage(errorMessages.toString());
|
||||
|
@ -21,7 +21,7 @@ import bisq.core.offer.AvailabilityResult;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.availability.OfferAvailabilityModel;
|
||||
import bisq.core.offer.messages.OfferAvailabilityResponse;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.common.taskrunner.Task;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
@ -54,7 +54,7 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
|
||||
}
|
||||
|
||||
// verify maker signature for trade request
|
||||
if (!TradeUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) {
|
||||
if (!HavenoUtils.isMakerSignatureValid(model.getTradeRequest(), offerAvailabilityResponse.getMakerSignature(), offer.getPubKeyRing())) {
|
||||
offer.setState(Offer.State.NOT_AVAILABLE);
|
||||
failed("Take offer attempt failed because maker signature is invalid");
|
||||
return;
|
||||
|
@ -62,7 +62,7 @@ public class PlaceOfferProtocol {
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public void placeOffer() {
|
||||
log.debug("placeOffer() " + model.getOffer().getId());
|
||||
log.info("{}.placeOffer() {}", getClass().getSimpleName(), model.getOffer().getId());
|
||||
|
||||
timeoutTimer = UserThread.runAfter(() -> {
|
||||
handleError(Res.get("createOffer.timeoutAtPublishing"));
|
||||
|
@ -20,7 +20,7 @@ package bisq.core.offer.placeoffer.tasks;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.placeoffer.PlaceOfferModel;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@ -42,7 +42,7 @@ public class MakerProcessSignOfferResponse extends Task<PlaceOfferModel> {
|
||||
Arbitrator arbitrator = checkNotNull(model.getUser().getAcceptedArbitratorByAddress(offer.getOfferPayload().getArbitratorSigner()), "user.getAcceptedArbitratorByAddress(arbitratorSigner) must not be null");
|
||||
|
||||
// validate arbitrator signature
|
||||
if (!TradeUtils.isArbitratorSignatureValid(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), arbitrator)) {
|
||||
if (!HavenoUtils.isArbitratorSignatureValid(new Offer(model.getSignOfferResponse().getSignedOfferPayload()), arbitrator)) {
|
||||
throw new RuntimeException("Offer payload has invalid arbitrator signature");
|
||||
}
|
||||
|
||||
|
@ -39,22 +39,18 @@ 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.PaymentSentMessage;
|
||||
import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.DepositRequest;
|
||||
import bisq.core.trade.messages.DepositResponse;
|
||||
import bisq.core.trade.messages.InitMultisigRequest;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.trade.messages.MediatedPayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.MediatedPayoutTxSignatureMessage;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyRequest;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.RefreshTradeStateRequest;
|
||||
import bisq.core.trade.messages.SignContractRequest;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
import bisq.core.trade.messages.TraderSignedWitnessMessage;
|
||||
import bisq.core.trade.messages.UpdateMultisigRequest;
|
||||
import bisq.core.trade.messages.UpdateMultisigResponse;
|
||||
|
||||
import bisq.network.p2p.AckMessage;
|
||||
import bisq.network.p2p.BundleOfEnvelopes;
|
||||
@ -158,21 +154,13 @@ public class CoreNetworkProtoResolver extends CoreProtoResolver implements Netwo
|
||||
return DepositRequest.fromProto(proto.getDepositRequest(), this, messageVersion);
|
||||
case DEPOSIT_RESPONSE:
|
||||
return DepositResponse.fromProto(proto.getDepositResponse(), this, messageVersion);
|
||||
case PAYMENT_ACCOUNT_KEY_REQUEST:
|
||||
return PaymentAccountKeyRequest.fromProto(proto.getPaymentAccountKeyRequest(), this, messageVersion);
|
||||
case PAYMENT_ACCOUNT_KEY_RESPONSE:
|
||||
return PaymentAccountKeyResponse.fromProto(proto.getPaymentAccountKeyResponse(), this, messageVersion);
|
||||
case UPDATE_MULTISIG_REQUEST:
|
||||
return UpdateMultisigRequest.fromProto(proto.getUpdateMultisigRequest(), this, messageVersion);
|
||||
case UPDATE_MULTISIG_RESPONSE:
|
||||
return UpdateMultisigResponse.fromProto(proto.getUpdateMultisigResponse(), this, messageVersion);
|
||||
case DEPOSITS_CONFIRMED_MESSAGE:
|
||||
return DepositsConfirmedMessage.fromProto(proto.getDepositsConfirmedMessage(), this, 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 TRADER_SIGNED_WITNESS_MESSAGE:
|
||||
return TraderSignedWitnessMessage.fromProto(proto.getTraderSignedWitnessMessage(), messageVersion);
|
||||
|
@ -333,13 +333,10 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
if (isAgent(dispute)) {
|
||||
|
||||
// update arbitrator's multisig wallet
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||
multisigWallet.importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex());
|
||||
trade.syncWallet();
|
||||
trade.getWallet().importMultisigHex(openNewDisputeMessage.getUpdatedMultisigHex());
|
||||
trade.saveWallet();
|
||||
log.info("Arbitrator multisig wallet updated on new dispute message for trade " + dispute.getTradeId());
|
||||
|
||||
// close multisig wallet
|
||||
xmrWalletService.closeMultisigWallet(dispute.getTradeId());
|
||||
|
||||
synchronized (disputeList) {
|
||||
if (!disputeList.contains(dispute)) {
|
||||
Optional<Dispute> storedDisputeOptional = findDispute(dispute);
|
||||
@ -748,6 +745,15 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
|
||||
disputeResultMessage.getTradeId(), disputeResultMessage.getUid(),
|
||||
chatMessage.getUid());
|
||||
|
||||
// TODO: hack to sync wallet after dispute message received in order to detect payout published
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
UserThread.runAfter(() -> {
|
||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||
}, defaultRefreshPeriod / 1000 * (i + 1));
|
||||
}
|
||||
|
||||
// We use the chatMessage wrapped inside the disputeResultMessage for
|
||||
// the state, as that is displayed to the user and we only persist that msg
|
||||
chatMessage.setArrived(true);
|
||||
|
@ -301,7 +301,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
log.trace("We don't publish the tx as we are not the winning party.");
|
||||
// Clean up tangling trades
|
||||
if (dispute.disputeResultProperty().get() != null && dispute.isClosed()) {
|
||||
updateTradeOrOpenOfferManager(tradeId);
|
||||
closeTradeOrOffer(tradeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -324,7 +324,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// We prefer to close the dispute in that case. If there was no deposit tx and a random tx was used
|
||||
// we get a TransactionVerificationException. No reason to keep that dispute open...
|
||||
updateTradeOrOpenOfferManager(tradeId); // TODO (woodser): only close in case of verification exception?
|
||||
closeTradeOrOffer(tradeId); // TODO (woodser): only close in case of verification exception?
|
||||
|
||||
throw new RuntimeException(errorMessage);
|
||||
} finally {
|
||||
@ -338,7 +338,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
synchronized (trade) {
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(tradeId); // TODO (woodser): this is closed after sending ArbitratorPayoutTxRequest to arbitrator which opens and syncs multisig and responds with signed dispute tx. more efficient way is to include with arbitrator-signed dispute tx with dispute result?
|
||||
sendArbitratorPayoutTxRequest(multisigWallet.exportMultisigHex(), dispute, contract);
|
||||
xmrWalletService.closeMultisigWallet(tradeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -376,12 +375,13 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
cleanupRetryMap(uid);
|
||||
|
||||
// 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(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex());
|
||||
MoneroTxWallet parsedPayoutTx = multisigWallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
|
||||
xmrWalletService.closeMultisigWallet(tradeId);
|
||||
// update trade wallet
|
||||
MoneroWallet wallet = trade.getWallet();
|
||||
if (wallet != null) { // TODO: multisig wallet may already be deleted if peer completed trade with arbitrator. refactor trade completion?
|
||||
trade.syncWallet();
|
||||
wallet.importMultisigHex(peerPublishedDisputePayoutTxMessage.getUpdatedMultisigHex());
|
||||
trade.saveWallet();
|
||||
MoneroTxWallet parsedPayoutTx = wallet.describeTxSet(new MoneroTxSet().setMultisigTxHex(peerPublishedDisputePayoutTxMessage.getPayoutTxHex())).getTxs().get(0);
|
||||
dispute.setDisputePayoutTxId(parsedPayoutTx.getHash());
|
||||
XmrWalletService.printTxs("Disputed payoutTx received from peer", parsedPayoutTx);
|
||||
}
|
||||
@ -397,6 +397,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
}
|
||||
|
||||
// Arbitrator receives updated multisig hex from dispute opener's peer (if co-signer) and returns updated payout tx to be signed and published
|
||||
// TODO: this should be invoked from mailbox message and send mailbox message response to support offline arbitrator
|
||||
private void onArbitratorPayoutTxRequest(ArbitratorPayoutTxRequest request) {
|
||||
log.info("{}.onArbitratorPayoutTxRequest()", getClass().getSimpleName());
|
||||
String tradeId = request.getTradeId();
|
||||
@ -426,9 +427,11 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
}
|
||||
|
||||
// update arbitrator's multisig wallet with co-signer's multisig hex
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(dispute.getTradeId());
|
||||
trade.syncWallet();
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
try {
|
||||
multisigWallet.importMultisigHex(request.getUpdatedMultisigHex());
|
||||
trade.saveWallet();
|
||||
} catch (Exception e) {
|
||||
log.warn("Failed to import multisig hex from payout co-signer for trade id " + tradeId);
|
||||
return;
|
||||
@ -439,9 +442,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
System.out.println("Arbitrator created updated payout tx for co-signer!!!");
|
||||
System.out.println(payoutTx);
|
||||
|
||||
// close multisig wallet
|
||||
xmrWalletService.closeMultisigWallet(tradeId);
|
||||
|
||||
// send updated payout tx to sender
|
||||
PubKeyRing senderPubKeyRing = contract.getBuyerNodeAddress().equals(request.getSenderNodeAddress()) ? contract.getBuyerPubKeyRing() : contract.getSellerPubKeyRing();
|
||||
ArbitratorPayoutTxResponse response = new ArbitratorPayoutTxResponse(
|
||||
@ -455,10 +455,19 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
senderPubKeyRing,
|
||||
response,
|
||||
new SendDirectMessageListener() {
|
||||
@Override
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at peer {}. tradeId={}, uid={}",
|
||||
response.getClass().getSimpleName(), request.getSenderNodeAddress(), dispute.getTradeId(), response.getUid());
|
||||
|
||||
// TODO: hack to sync wallet after dispute message received in order to detect payout published
|
||||
Trade trade = tradeManager.getTrade(dispute.getTradeId());
|
||||
long defaultRefreshPeriod = xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs();
|
||||
for (int i = 0; i < 3; i++) {
|
||||
UserThread.runAfter(() -> {
|
||||
if (!trade.isPayoutUnlocked()) trade.syncWallet();
|
||||
}, defaultRefreshPeriod / 1000 * (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -546,6 +555,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
|
||||
// update multisig wallet from arbitrator
|
||||
multisigWallet.importMultisigHex(disputeResult.getArbitratorUpdatedMultisigHex());
|
||||
xmrWalletService.saveWallet(multisigWallet);
|
||||
|
||||
// sign arbitrator-signed payout tx
|
||||
MoneroMultisigSignResult result = multisigWallet.signMultisigTxHex(payoutTxHex);
|
||||
@ -575,10 +585,10 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
// update state
|
||||
trade.setPayoutTx(txSet.getTxs().get(0)); // TODO (woodser): is trade.payoutTx() mutually exclusive from dispute payout tx?
|
||||
trade.setPayoutTxId(txSet.getTxs().get(0).getHash());
|
||||
trade.setState(Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
|
||||
trade.setPayoutState(Trade.PayoutState.PUBLISHED);
|
||||
dispute.setDisputePayoutTxId(txSet.getTxs().get(0).getHash());
|
||||
sendPeerPublishedPayoutTxMessage(multisigWallet.exportMultisigHex(), txSet.getMultisigTxHex(), dispute, contract);
|
||||
updateTradeOrOpenOfferManager(tradeId);
|
||||
closeTradeOrOffer(tradeId);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -623,7 +633,7 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
);
|
||||
}
|
||||
|
||||
private void updateTradeOrOpenOfferManager(String tradeId) {
|
||||
public void closeTradeOrOffer(String tradeId) {
|
||||
// set state after payout as we call swapTradeEntryToAvailableEntry
|
||||
if (tradeManager.getOpenTrade(tradeId).isPresent()) {
|
||||
tradeManager.closeDisputedTrade(tradeId, Trade.DisputeState.DISPUTE_CLOSED);
|
||||
@ -632,7 +642,6 @@ public final class ArbitrationManager extends DisputeManager<ArbitrationDisputeL
|
||||
openOfferOptional.ifPresent(openOffer -> openOfferManager.closeOpenOffer(openOffer.getOffer()));
|
||||
}
|
||||
}
|
||||
|
||||
// dispute opener's peer signs payout tx by sending updated multisig hex to arbitrator who returns updated payout tx
|
||||
private void sendArbitratorPayoutTxRequest(String updatedMultisigHex, Dispute dispute, Contract contract) {
|
||||
ArbitratorPayoutTxRequest request = new ArbitratorPayoutTxRequest(
|
||||
|
@ -64,7 +64,7 @@ public class TradeChatSession extends SupportSession {
|
||||
|
||||
@Override
|
||||
public boolean chatIsOpen() {
|
||||
return trade != null && trade.getState() != Trade.State.WITHDRAW_COMPLETED;
|
||||
return trade != null && trade.getState() != Trade.State.TRADE_COMPLETED;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -158,7 +158,7 @@ public class ClosedTradableFormatter {
|
||||
|
||||
if (isBisqV1Trade(tradable)) {
|
||||
Trade trade = castToTrade(tradable);
|
||||
if (trade.isWithdrawn() || trade.isPayoutPublished()) {
|
||||
if (trade.isCompleted() || trade.isPayoutPublished()) {
|
||||
return Res.get("portfolio.closed.completed");
|
||||
} else if (trade.getDisputeState() == DISPUTE_CLOSED) {
|
||||
return Res.get("portfolio.closed.ticketClosed");
|
||||
|
@ -18,24 +18,24 @@
|
||||
package bisq.core.trade;
|
||||
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.crypto.Sig;
|
||||
import bisq.common.util.Tuple2;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferPayload;
|
||||
import bisq.core.support.dispute.arbitration.arbitrator.Arbitrator;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.util.JsonUtil;
|
||||
import java.net.URI;
|
||||
import java.util.Objects;
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Collection of utilities for trading.
|
||||
* Collection of utilities.
|
||||
*/
|
||||
public class TradeUtils {
|
||||
public class HavenoUtils {
|
||||
|
||||
public static final String LOOPBACK_HOST = "127.0.0.1"; // local loopback address to host Monero node
|
||||
public static final String LOCALHOST = "localhost";
|
||||
@ -149,61 +149,6 @@ public class TradeUtils {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO (woodser): remove the following utitilites?
|
||||
|
||||
// Returns <MULTI_SIG, TRADE_PAYOUT> if both are AVAILABLE, otherwise null
|
||||
static Tuple2<String, String> getAvailableAddresses(Trade trade, XmrWalletService xmrWalletService,
|
||||
KeyRing keyRing) {
|
||||
var addresses = getTradeAddresses(trade, xmrWalletService, keyRing);
|
||||
if (addresses == null)
|
||||
return null;
|
||||
|
||||
if (xmrWalletService.getAvailableAddressEntries().stream()
|
||||
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.first)))
|
||||
return null;
|
||||
if (xmrWalletService.getAvailableAddressEntries().stream()
|
||||
.noneMatch(e -> Objects.equals(e.getAddressString(), addresses.second)))
|
||||
return null;
|
||||
|
||||
return new Tuple2<>(addresses.first, addresses.second);
|
||||
}
|
||||
|
||||
// Returns <MULTI_SIG, TRADE_PAYOUT> addresses as strings if they're known by the wallet
|
||||
public static Tuple2<String, String> getTradeAddresses(Trade trade, XmrWalletService xmrWalletService,
|
||||
KeyRing keyRing) {
|
||||
var contract = trade.getContract();
|
||||
if (contract == null)
|
||||
return null;
|
||||
|
||||
// TODO (woodser): xmr multisig does not use pub key
|
||||
throw new RuntimeException("need to replace btc multisig pub key with xmr");
|
||||
|
||||
// Get multisig address
|
||||
// var isMyRoleBuyer = contract.isMyRoleBuyer(keyRing.getPubKeyRing());
|
||||
// var multiSigPubKey = isMyRoleBuyer ? contract.getBuyerMultiSigPubKey() : contract.getSellerMultiSigPubKey();
|
||||
// if (multiSigPubKey == null)
|
||||
// return null;
|
||||
// var multiSigPubKeyString = Utilities.bytesAsHexString(multiSigPubKey);
|
||||
// var multiSigAddress = xmrWalletService.getAddressEntryListAsImmutableList().stream()
|
||||
// .filter(e -> e.getKeyPair().getPublicKeyAsHex().equals(multiSigPubKeyString))
|
||||
// .findAny()
|
||||
// .orElse(null);
|
||||
// if (multiSigAddress == null)
|
||||
// return null;
|
||||
//
|
||||
// // Get payout address
|
||||
// var payoutAddress = isMyRoleBuyer ?
|
||||
// contract.getBuyerPayoutAddressString() : contract.getSellerPayoutAddressString();
|
||||
// var payoutAddressEntry = xmrWalletService.getAddressEntryListAsImmutableList().stream()
|
||||
// .filter(e -> Objects.equals(e.getAddressString(), payoutAddress))
|
||||
// .findAny()
|
||||
// .orElse(null);
|
||||
// if (payoutAddressEntry == null)
|
||||
// return null;
|
||||
//
|
||||
// return new Tuple2<>(multiSigAddress.getAddressString(), payoutAddress);
|
||||
}
|
||||
|
||||
public static void awaitLatch(CountDownLatch latch) {
|
||||
try {
|
||||
latch.await();
|
||||
@ -211,4 +156,17 @@ public class TradeUtils {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void awaitTasks(Collection<Runnable> tasks) {
|
||||
if (tasks.isEmpty()) return;
|
||||
ExecutorService pool = Executors.newFixedThreadPool(tasks.size());
|
||||
for (Runnable task : tasks) pool.submit(task);
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (!pool.awaitTermination(60000, TimeUnit.SECONDS)) pool.shutdownNow();
|
||||
} catch (InterruptedException e) {
|
||||
pool.shutdownNow();
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
@ -50,9 +50,9 @@ import bisq.common.util.Utilities;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.protobuf.ByteString;
|
||||
import com.google.protobuf.Message;
|
||||
import common.utils.GenUtils;
|
||||
import org.bitcoinj.core.Coin;
|
||||
import org.bitcoinj.core.Transaction;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@ -88,13 +88,14 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
import monero.common.MoneroError;
|
||||
import monero.common.MoneroRpcConnection;
|
||||
import monero.common.TaskLooper;
|
||||
import monero.daemon.MoneroDaemon;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroCheckTx;
|
||||
import monero.wallet.model.MoneroDestination;
|
||||
import monero.wallet.model.MoneroMultisigSignResult;
|
||||
import monero.wallet.model.MoneroTransferQuery;
|
||||
import monero.wallet.model.MoneroOutputWallet;
|
||||
import monero.wallet.model.MoneroTxConfig;
|
||||
import monero.wallet.model.MoneroTxQuery;
|
||||
import monero.wallet.model.MoneroTxSet;
|
||||
@ -125,9 +126,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
|
||||
// deposit requested
|
||||
SENT_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
|
||||
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
|
||||
STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED), // not a mailbox msg, not used... remove
|
||||
SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
|
||||
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST(Phase.DEPOSIT_REQUESTED),
|
||||
|
||||
// deposit published
|
||||
ARBITRATOR_PUBLISHED_DEPOSIT_TXS(Phase.DEPOSITS_PUBLISHED),
|
||||
@ -142,30 +142,20 @@ public abstract class Trade implements Tradable, Model {
|
||||
// payment sent
|
||||
BUYER_CONFIRMED_IN_UI_PAYMENT_SENT(Phase.PAYMENT_SENT),
|
||||
BUYER_SENT_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
BUYER_SEND_FAILED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
SELLER_RECEIVED_PAYMENT_SENT_MSG(Phase.PAYMENT_SENT),
|
||||
|
||||
// payment received
|
||||
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT(Phase.PAYMENT_RECEIVED),
|
||||
SELLER_SENT_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
|
||||
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
|
||||
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
|
||||
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
|
||||
|
||||
// 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),
|
||||
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
|
||||
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
|
||||
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG(Phase.PAYOUT_PUBLISHED),
|
||||
BUYER_PUBLISHED_PAYOUT_TX(Phase.PAYOUT_PUBLISHED),
|
||||
PAYOUT_TX_SEEN_IN_NETWORK(Phase.PAYOUT_PUBLISHED),
|
||||
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
|
||||
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG(Phase.PAYMENT_RECEIVED),
|
||||
|
||||
// trade completed
|
||||
WITHDRAW_COMPLETED(Phase.WITHDRAWN);
|
||||
TRADE_COMPLETED(Phase.COMPLETED);
|
||||
|
||||
@NotNull
|
||||
public Phase getPhase() {
|
||||
@ -199,14 +189,13 @@ public abstract class Trade implements Tradable, Model {
|
||||
|
||||
public enum Phase {
|
||||
INIT,
|
||||
DEPOSIT_REQUESTED, // TODO (woodser): remove unused phases
|
||||
DEPOSIT_REQUESTED,
|
||||
DEPOSITS_PUBLISHED,
|
||||
DEPOSITS_CONFIRMED,
|
||||
DEPOSITS_UNLOCKED,
|
||||
PAYMENT_SENT,
|
||||
PAYMENT_RECEIVED,
|
||||
PAYOUT_PUBLISHED,
|
||||
WITHDRAWN;
|
||||
COMPLETED;
|
||||
|
||||
public static Trade.Phase fromProto(protobuf.Trade.Phase phase) {
|
||||
return ProtoUtil.enumFromProto(Trade.Phase.class, phase.name());
|
||||
@ -224,6 +213,25 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
public enum PayoutState {
|
||||
UNPUBLISHED,
|
||||
PUBLISHED,
|
||||
CONFIRMED,
|
||||
UNLOCKED;
|
||||
|
||||
public static Trade.PayoutState fromProto(protobuf.Trade.PayoutState state) {
|
||||
return ProtoUtil.enumFromProto(Trade.PayoutState.class, state.name());
|
||||
}
|
||||
|
||||
public static protobuf.Trade.PayoutState toProtoMessage(Trade.PayoutState state) {
|
||||
return protobuf.Trade.PayoutState.valueOf(state.name());
|
||||
}
|
||||
|
||||
public boolean isValidTransitionTo(PayoutState newState) {
|
||||
return newState.ordinal() > this.ordinal();
|
||||
}
|
||||
}
|
||||
|
||||
public enum DisputeState {
|
||||
NO_DISPUTE,
|
||||
// arbitration
|
||||
@ -307,7 +315,6 @@ public abstract class Trade implements Tradable, Model {
|
||||
private long takeOfferDate;
|
||||
|
||||
// Mutable
|
||||
@Nullable
|
||||
@Getter
|
||||
@Setter
|
||||
private long amountAsLong;
|
||||
@ -317,6 +324,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
@Getter
|
||||
private State state = State.PREPARATION;
|
||||
@Getter
|
||||
private PayoutState payoutState = PayoutState.UNPUBLISHED;
|
||||
@Getter
|
||||
private DisputeState disputeState = DisputeState.NO_DISPUTE;
|
||||
@Getter
|
||||
private TradePeriodState periodState = TradePeriodState.FIRST_HALF;
|
||||
@ -351,10 +360,16 @@ public abstract class Trade implements Tradable, Model {
|
||||
transient final private XmrWalletService xmrWalletService;
|
||||
|
||||
transient final private ObjectProperty<State> stateProperty = new SimpleObjectProperty<>(state);
|
||||
transient final private ObjectProperty<Phase> statePhaseProperty = new SimpleObjectProperty<>(state.phase);
|
||||
transient final private ObjectProperty<Phase> phaseProperty = new SimpleObjectProperty<>(state.phase);
|
||||
transient final private ObjectProperty<PayoutState> payoutStateProperty = new SimpleObjectProperty<>(payoutState);
|
||||
transient final private ObjectProperty<DisputeState> disputeStateProperty = new SimpleObjectProperty<>(disputeState);
|
||||
transient final private ObjectProperty<TradePeriodState> tradePeriodStateProperty = new SimpleObjectProperty<>(periodState);
|
||||
transient final private StringProperty errorMessageProperty = new SimpleStringProperty();
|
||||
transient private Subscription tradePhaseSubscription = null;
|
||||
transient private Subscription payoutStateSubscription = null;
|
||||
transient private TaskLooper tradeTxsLooper;
|
||||
transient private Long lastWalletRefreshPeriod;
|
||||
private static final long IDLE_SYNC_PERIOD_MS = 3600000; // 1 hour
|
||||
|
||||
// Mutable
|
||||
@Getter
|
||||
@ -530,6 +545,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
.setAmountAsLong(amountAsLong)
|
||||
.setPrice(price)
|
||||
.setState(Trade.State.toProtoMessage(state))
|
||||
.setPayoutState(Trade.PayoutState.toProtoMessage(payoutState))
|
||||
.setDisputeState(Trade.DisputeState.toProtoMessage(disputeState))
|
||||
.setPeriodState(Trade.TradePeriodState.toProtoMessage(periodState))
|
||||
.addAllChatMessage(chatMessages.stream()
|
||||
@ -556,6 +572,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
public static Trade fromProto(Trade trade, protobuf.Trade proto, CoreProtoResolver coreProtoResolver) {
|
||||
trade.setTakeOfferDate(proto.getTakeOfferDate());
|
||||
trade.setState(State.fromProto(proto.getState()));
|
||||
trade.setPayoutState(PayoutState.fromProto(proto.getPayoutState()));
|
||||
trade.setDisputeState(DisputeState.fromProto(proto.getDisputeState()));
|
||||
trade.setPeriodState(TradePeriodState.fromProto(proto.getPeriodState()));
|
||||
trade.setPayoutTxId(ProtoUtil.stringOrNullFromProto(proto.getPayoutTxId()));
|
||||
@ -590,7 +607,56 @@ public abstract class Trade implements Tradable, Model {
|
||||
getArbitrator().setPubKeyRing(arbitrator.getPubKeyRing());
|
||||
});
|
||||
|
||||
isInitialized = true;
|
||||
isInitialized = true; // TODO: move to end?
|
||||
|
||||
// listen to daemon connection
|
||||
xmrWalletService.getConnectionsService().addListener(newConnection -> setDaemonConnection(newConnection));
|
||||
|
||||
// done if payout unlocked
|
||||
if (isPayoutUnlocked()) return;
|
||||
|
||||
// handle trade state events
|
||||
if (isDepositPublished()) listenToTradeTxs();
|
||||
tradePhaseSubscription = EasyBind.subscribe(phaseProperty, newValue -> {
|
||||
updateTxListenerRefreshPeriod();
|
||||
if (isDepositPublished()) listenToTradeTxs();
|
||||
if (isCompleted()) {
|
||||
UserThread.execute(() -> {
|
||||
if (tradePhaseSubscription != null) {
|
||||
tradePhaseSubscription.unsubscribe();
|
||||
tradePhaseSubscription = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// handle payout state events
|
||||
payoutStateSubscription = EasyBind.subscribe(payoutStateProperty, newValue -> {
|
||||
updateTxListenerRefreshPeriod();
|
||||
|
||||
// cleanup when payout published
|
||||
if (isPayoutPublished()) {
|
||||
log.info("Payout published for {} {}", getClass().getSimpleName(), getId());
|
||||
if (isArbitrator() && !isCompleted()) processModel.getTradeManager().onTradeCompleted(this); // complete arbitrator trade when payout published
|
||||
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(getId());
|
||||
}
|
||||
|
||||
// cleanup when payout unlocks
|
||||
if (isPayoutUnlocked()) {
|
||||
log.info("Payout unlocked for {} {}, deleting multisig wallet", getClass().getSimpleName(), getId()); // TODO: retain backup for some time?
|
||||
deleteWallet();
|
||||
if (tradeTxsLooper != null) {
|
||||
tradeTxsLooper.stop();
|
||||
tradeTxsLooper = null;
|
||||
}
|
||||
UserThread.execute(() -> {
|
||||
if (payoutStateSubscription != null) {
|
||||
payoutStateSubscription.unsubscribe();
|
||||
payoutStateSubscription = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -603,12 +669,12 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
public NodeAddress getTradingPeerNodeAddress() {
|
||||
return getTradingPeer() == null ? null : getTradingPeer().getNodeAddress();
|
||||
return getTradingPeer() == null ? null : getTradingPeer().getNodeAddress();
|
||||
}
|
||||
|
||||
public NodeAddress getArbitratorNodeAddress() {
|
||||
return getArbitrator() == null ? null : getArbitrator().getNodeAddress();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a contract based on the current state.
|
||||
@ -761,9 +827,8 @@ public abstract class Trade implements Tradable, Model {
|
||||
// submit payout tx
|
||||
if (publish) {
|
||||
multisigWallet.submitMultisigTxHex(payoutTxHex);
|
||||
setState(isArbitrator() ? Trade.State.WITHDRAW_COMPLETED : isBuyer() ? Trade.State.BUYER_PUBLISHED_PAYOUT_TX : Trade.State.SELLER_PUBLISHED_PAYOUT_TX);
|
||||
setPayoutState(Trade.PayoutState.PUBLISHED);
|
||||
}
|
||||
walletService.closeMultisigWallet(getId());
|
||||
}
|
||||
|
||||
/**
|
||||
@ -771,7 +836,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
*
|
||||
* @param paymentAccountKey is the key to decrypt the payment account payload
|
||||
*/
|
||||
public void decryptPeersPaymentAccountPayload(byte[] paymentAccountKey) {
|
||||
public void decryptPeerPaymentAccountPayload(byte[] paymentAccountKey) {
|
||||
try {
|
||||
|
||||
// decrypt payment account payload
|
||||
@ -792,139 +857,6 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Listen for deposit transactions to unlock and then apply the transactions.
|
||||
*
|
||||
* TODO: adopt for general purpose scheduling
|
||||
* TODO: check and notify if deposits are dropped due to re-org
|
||||
*/
|
||||
public void listenForDepositTxs() {
|
||||
log.info("Listening for deposit txs to unlock for trade {}", getId());
|
||||
|
||||
// ignore if already listening
|
||||
if (depositTxListener != null) {
|
||||
log.warn("Trade {} already listening for deposit txs", getId());
|
||||
return;
|
||||
}
|
||||
|
||||
// get daemon and primary wallet
|
||||
MoneroWallet havenoWallet = processModel.getXmrWalletService().getWallet();
|
||||
|
||||
// fetch deposit txs from daemon
|
||||
List<MoneroTx> txs = xmrWalletService.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()));
|
||||
|
||||
// handle deposit txs seen
|
||||
if (txs.size() == 2) {
|
||||
setStateDepositsPublished();
|
||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
|
||||
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
|
||||
|
||||
// check if deposit txs unlocked
|
||||
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) {
|
||||
setStateDepositsConfirmed();
|
||||
long unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK;
|
||||
if (havenoWallet.getHeight() >= unlockHeight) {
|
||||
setStateDepositsUnlocked();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// create block listener
|
||||
depositTxListener = new MoneroWalletListener() {
|
||||
Long unlockHeight = null;
|
||||
|
||||
@Override
|
||||
public void onNewBlock(long height) {
|
||||
|
||||
// skip if no longer listening
|
||||
if (depositTxListener == null) return;
|
||||
|
||||
// use latest height
|
||||
height = havenoWallet.getHeight();
|
||||
|
||||
// skip if before unlock height
|
||||
if (unlockHeight != null && height < unlockHeight) return;
|
||||
|
||||
// fetch txs from daemon
|
||||
List<MoneroTx> txs = xmrWalletService.getTxs(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()));
|
||||
|
||||
// skip if deposit txs not seen
|
||||
if (txs.size() != 2) return;
|
||||
setStateDepositsPublished();
|
||||
|
||||
// update deposit txs
|
||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
|
||||
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
|
||||
|
||||
// check if deposit txs confirmed and compute unlock height
|
||||
if (txs.size() == 2 && txs.get(0).isConfirmed() && txs.get(1).isConfirmed() && unlockHeight == null) {
|
||||
log.info("Multisig deposits confirmed for trade {}", getId());
|
||||
setStateDepositsConfirmed();
|
||||
unlockHeight = Math.max(txs.get(0).getHeight(), txs.get(1).getHeight()) + XmrWalletService.NUM_BLOCKS_UNLOCK;
|
||||
}
|
||||
|
||||
// check if deposit txs unlocked
|
||||
if (unlockHeight != null && height >= unlockHeight) {
|
||||
log.info("Multisig deposits unlocked for trade {}", getId());
|
||||
xmrWalletService.removeWalletListener(depositTxListener); // remove listener when notified
|
||||
depositTxListener = null; // prevent re-applying trade state in subsequent requests
|
||||
setStateDepositsUnlocked();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// register wallet listener
|
||||
xmrWalletService.addWalletListener(depositTxListener);
|
||||
}
|
||||
|
||||
public void listenForPayoutTx() {
|
||||
log.info("Listening for payout tx for trade {}", getId());
|
||||
|
||||
// check if payout tx already seen
|
||||
if (getState().ordinal() >= Trade.State.PAYOUT_TX_SEEN_IN_NETWORK.ordinal()) {
|
||||
log.warn("We had a payout tx already set. tradeId={}, state={}", getId(), getState());
|
||||
return;
|
||||
}
|
||||
|
||||
// get payout address entry
|
||||
Optional<XmrAddressEntry> optionalPayoutEntry = xmrWalletService.getAddressEntry(getId(), XmrAddressEntry.Context.TRADE_PAYOUT);
|
||||
if (!optionalPayoutEntry.isPresent()) throw new RuntimeException("Trade does not have address entry for payout");
|
||||
XmrAddressEntry payoutEntry = optionalPayoutEntry.get();
|
||||
|
||||
// watch for payout tx on loop
|
||||
new Thread(() -> { // TODO: use thread manager
|
||||
boolean found = false;
|
||||
while (!found) {
|
||||
if (getPayoutTxKey() != null) {
|
||||
|
||||
// get txs to payout address
|
||||
List<MoneroTxWallet> txs = xmrWalletService.getWallet().getTxs(new MoneroTxQuery()
|
||||
.setTransferQuery(new MoneroTransferQuery()
|
||||
.setAccountIndex(0)
|
||||
.setSubaddressIndex(payoutEntry.getSubaddressIndex())
|
||||
.setIsIncoming(true)));
|
||||
|
||||
// check for payout tx
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
MoneroCheckTx txCheck = xmrWalletService.getWallet().checkTxKey(tx.getHash(), getPayoutTxKey(), payoutEntry.getAddressString());
|
||||
if (txCheck.isGood() && txCheck.receivedAmount.compareTo(new BigInteger("0")) > 0) {
|
||||
found = true;
|
||||
setPayoutTx(tx);
|
||||
setStateIfValidTransitionTo(Trade.State.PAYOUT_TX_SEEN_IN_NETWORK);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// wait to loop
|
||||
GenUtils.waitFor(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public MoneroTx getTakerDepositTx() {
|
||||
String depositTxHash = getProcessModel().getTaker().getDepositTxHash();
|
||||
@ -984,6 +916,30 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
}
|
||||
|
||||
public MoneroWallet getWallet() {
|
||||
return xmrWalletService.multisigWalletExists(getId()) ? xmrWalletService.getMultisigWallet(getId()) : null;
|
||||
}
|
||||
|
||||
public void syncWallet() {
|
||||
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
getWallet().sync();
|
||||
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
|
||||
pollWallet();
|
||||
}
|
||||
|
||||
public void saveWallet() {
|
||||
xmrWalletService.saveWallet(getWallet());
|
||||
}
|
||||
|
||||
public void deleteWallet() {
|
||||
if (xmrWalletService.multisigWalletExists(getId())) xmrWalletService.deleteMultisigWallet(getId());
|
||||
else log.warn("Multisig wallet to delete for trade {} does not exist", getId());
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
isInitialized = false;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Model implementation
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -1055,7 +1011,37 @@ public abstract class Trade implements Tradable, Model {
|
||||
this.state = state;
|
||||
UserThread.execute(() -> {
|
||||
stateProperty.set(state);
|
||||
statePhaseProperty.set(state.getPhase());
|
||||
phaseProperty.set(state.getPhase());
|
||||
});
|
||||
}
|
||||
|
||||
public void setStateIfProgress(State state) {
|
||||
if (state.ordinal() > getState().ordinal()) setState(state);
|
||||
}
|
||||
|
||||
public void setPayoutStateIfValidTransitionTo(PayoutState newPayoutState) {
|
||||
if (payoutState.isValidTransitionTo(newPayoutState)) {
|
||||
setPayoutState(newPayoutState);
|
||||
} else {
|
||||
log.warn("Payout state change is not getting applied because it would cause an invalid transition. " +
|
||||
"Trade payout state={}, intended payout state={}", payoutState, newPayoutState);
|
||||
}
|
||||
}
|
||||
|
||||
public void setPayoutState(PayoutState payoutState) {
|
||||
if (isInitialized) {
|
||||
// We don't want to log at startup the setState calls from all persisted trades
|
||||
log.info("Set new payout state at {} (id={}): {}", this.getClass().getSimpleName(), getShortId(), payoutState);
|
||||
}
|
||||
if (payoutState.ordinal() < this.payoutState.ordinal()) {
|
||||
String message = "We got a payout state change to a previous phase (id=" + getShortId() + ").\n" +
|
||||
"Old payout state is: " + this.state + ". New payout state is: " + payoutState;
|
||||
log.warn(message);
|
||||
}
|
||||
|
||||
this.payoutState = payoutState;
|
||||
UserThread.execute(() -> {
|
||||
payoutStateProperty.set(payoutState);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1264,7 +1250,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
return getState().getPhase().ordinal() == Phase.INIT.ordinal();
|
||||
}
|
||||
|
||||
public boolean isTakerFeePublished() {
|
||||
public boolean isDepositRequested() {
|
||||
return getState().getPhase().ordinal() >= Phase.DEPOSIT_REQUESTED.ordinal();
|
||||
}
|
||||
|
||||
@ -1319,16 +1305,20 @@ public abstract class Trade implements Tradable, Model {
|
||||
return getState().getPhase().ordinal() >= Phase.PAYMENT_RECEIVED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isPayoutPublished() {
|
||||
return getState().getPhase().ordinal() >= Phase.PAYOUT_PUBLISHED.ordinal() || isWithdrawn();
|
||||
}
|
||||
|
||||
public boolean isCompleted() {
|
||||
return isPayoutPublished();
|
||||
return getState().getPhase().ordinal() >= Phase.COMPLETED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isWithdrawn() {
|
||||
return getState().getPhase().ordinal() == Phase.WITHDRAWN.ordinal();
|
||||
public boolean isPayoutPublished() {
|
||||
return getPayoutState().ordinal() >= PayoutState.PUBLISHED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isPayoutConfirmed() {
|
||||
return getPayoutState().ordinal() >= PayoutState.CONFIRMED.ordinal();
|
||||
}
|
||||
|
||||
public boolean isPayoutUnlocked() {
|
||||
return getPayoutState().ordinal() >= PayoutState.UNLOCKED.ordinal();
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<State> stateProperty() {
|
||||
@ -1336,7 +1326,11 @@ public abstract class Trade implements Tradable, Model {
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<Phase> statePhaseProperty() {
|
||||
return statePhaseProperty;
|
||||
return phaseProperty;
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<PayoutState> payoutStateProperty() {
|
||||
return payoutStateProperty;
|
||||
}
|
||||
|
||||
public ReadOnlyObjectProperty<DisputeState> disputeStateProperty() {
|
||||
@ -1439,6 +1433,98 @@ public abstract class Trade implements Tradable, Model {
|
||||
return tradeVolumeProperty;
|
||||
}
|
||||
|
||||
private void listenToTradeTxs() {
|
||||
if (tradeTxsLooper != null) return;
|
||||
log.info("Listening for payout tx for {} {}", getClass().getSimpleName(), getId());
|
||||
|
||||
// poll wallet for tx state
|
||||
pollWallet();
|
||||
tradeTxsLooper = new TaskLooper(() -> {
|
||||
try {
|
||||
pollWallet();
|
||||
} catch (Exception e) {
|
||||
if (isInitialized) log.warn("Error checking trade txs in background: " + e.getMessage());
|
||||
}
|
||||
});
|
||||
tradeTxsLooper.start(getWalletRefreshPeriod());
|
||||
}
|
||||
|
||||
private void pollWallet() {
|
||||
|
||||
// skip if payout unlocked
|
||||
if (isPayoutUnlocked()) return;
|
||||
|
||||
// rescan spent if deposits unlocked
|
||||
if (isDepositUnlocked()) getWallet().rescanSpent();
|
||||
|
||||
// get txs with outputs
|
||||
List<MoneroTxWallet> txs = getWallet().getTxs(new MoneroTxQuery()
|
||||
.setHashes(Arrays.asList(processModel.getMaker().getDepositTxHash(), processModel.getTaker().getDepositTxHash()))
|
||||
.setIncludeOutputs(true));
|
||||
|
||||
// check deposit txs
|
||||
if (!isDepositUnlocked()) {
|
||||
if (txs.size() == 2) {
|
||||
setStateDepositsPublished();
|
||||
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
|
||||
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
|
||||
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
|
||||
|
||||
// check if deposit txs confirmed
|
||||
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
|
||||
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();
|
||||
}
|
||||
}
|
||||
|
||||
// check payout tx
|
||||
else {
|
||||
|
||||
// check if deposit txs spent (appears on payout published)
|
||||
for (MoneroTxWallet tx : txs) {
|
||||
for (MoneroOutputWallet output : tx.getOutputsWallet()) {
|
||||
if (Boolean.TRUE.equals(output.isSpent())) {
|
||||
setPayoutStatePublished();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check for outgoing txs (appears on payout confirmed)
|
||||
List<MoneroTxWallet> outgoingTxs = getWallet().getTxs(new MoneroTxQuery().setIsOutgoing(true));
|
||||
if (!outgoingTxs.isEmpty()) {
|
||||
MoneroTxWallet payoutTx = outgoingTxs.get(0);
|
||||
setPayoutTx(payoutTx);
|
||||
setPayoutStatePublished();
|
||||
if (payoutTx.isConfirmed()) setPayoutStateConfirmed();
|
||||
if (!payoutTx.isLocked()) setPayoutStateUnlocked();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setDaemonConnection(MoneroRpcConnection connection) {
|
||||
log.info("Setting daemon connection for trade wallet {}: {}: ", getId() , connection == null ? null : connection.getUri());
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(getId());
|
||||
multisigWallet.setDaemonConnection(connection);
|
||||
multisigWallet.startSyncing(getWalletRefreshPeriod());
|
||||
updateTxListenerRefreshPeriod();
|
||||
}
|
||||
|
||||
private void updateTxListenerRefreshPeriod() {
|
||||
long walletRefreshPeriod = getWalletRefreshPeriod();
|
||||
if (lastWalletRefreshPeriod != null && lastWalletRefreshPeriod == walletRefreshPeriod) return;
|
||||
log.info("Setting wallet refresh rate for {} to {}", getClass().getSimpleName(), walletRefreshPeriod);
|
||||
lastWalletRefreshPeriod = walletRefreshPeriod;
|
||||
if (tradeTxsLooper != null) {
|
||||
tradeTxsLooper.stop();
|
||||
tradeTxsLooper = null;
|
||||
listenToTradeTxs();
|
||||
}
|
||||
}
|
||||
|
||||
private long getWalletRefreshPeriod() {
|
||||
if (this instanceof ArbitratorTrade && isDepositConfirmed()) return IDLE_SYNC_PERIOD_MS; // arbitrator slows trade wallet after deposits confirm since messages are expected so this is only backup
|
||||
return xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs(); // otherwise sync at default refresh rate
|
||||
}
|
||||
|
||||
private void setStateDepositsPublished() {
|
||||
if (!isDepositPublished()) setState(State.DEPOSIT_TXS_SEEN_IN_NETWORK);
|
||||
}
|
||||
@ -1451,6 +1537,18 @@ public abstract class Trade implements Tradable, Model {
|
||||
if (!isDepositUnlocked()) setState(State.DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN);
|
||||
}
|
||||
|
||||
private void setPayoutStatePublished() {
|
||||
if (!isPayoutPublished()) setPayoutState(PayoutState.PUBLISHED);
|
||||
}
|
||||
|
||||
private void setPayoutStateConfirmed() {
|
||||
if (!isPayoutConfirmed()) setPayoutState(PayoutState.CONFIRMED);
|
||||
}
|
||||
|
||||
private void setPayoutStateUnlocked() {
|
||||
if (!isPayoutUnlocked()) setPayoutState(PayoutState.UNLOCKED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Trade{" +
|
||||
@ -1463,6 +1561,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
",\n tradeAmountAsLong=" + amountAsLong +
|
||||
",\n tradePrice=" + price +
|
||||
",\n state=" + state +
|
||||
",\n payoutState=" + payoutState +
|
||||
",\n disputeState=" + disputeState +
|
||||
",\n tradePeriodState=" + periodState +
|
||||
",\n contract=" + contract +
|
||||
@ -1477,7 +1576,7 @@ public abstract class Trade implements Tradable, Model {
|
||||
",\n takerFee=" + takerFee +
|
||||
",\n xmrWalletService=" + xmrWalletService +
|
||||
",\n stateProperty=" + stateProperty +
|
||||
",\n statePhaseProperty=" + statePhaseProperty +
|
||||
",\n statePhaseProperty=" + phaseProperty +
|
||||
",\n disputeStateProperty=" + disputeStateProperty +
|
||||
",\n tradePeriodStateProperty=" + tradePeriodStateProperty +
|
||||
",\n errorMessageProperty=" + errorMessageProperty +
|
||||
|
@ -41,10 +41,8 @@ import bisq.core.trade.messages.DepositRequest;
|
||||
import bisq.core.trade.messages.DepositResponse;
|
||||
import bisq.core.trade.messages.InitMultisigRequest;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyRequest;
|
||||
import bisq.core.trade.messages.SignContractRequest;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
import bisq.core.trade.messages.UpdateMultisigRequest;
|
||||
import bisq.core.trade.protocol.ArbitratorProtocol;
|
||||
import bisq.core.trade.protocol.MakerProtocol;
|
||||
import bisq.core.trade.protocol.ProcessModel;
|
||||
@ -66,7 +64,6 @@ import bisq.network.p2p.P2PService;
|
||||
import bisq.network.p2p.network.TorNetworkNode;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import bisq.common.ClockWatcher;
|
||||
import bisq.common.config.Config;
|
||||
import bisq.common.crypto.KeyRing;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.FaultHandler;
|
||||
@ -245,10 +242,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
handleDepositRequest((DepositRequest) networkEnvelope, peer);
|
||||
} else if (networkEnvelope instanceof DepositResponse) {
|
||||
handleDepositResponse((DepositResponse) networkEnvelope, peer);
|
||||
} else if (networkEnvelope instanceof PaymentAccountKeyRequest) {
|
||||
handlePaymentAccountKeyRequest((PaymentAccountKeyRequest) networkEnvelope, peer);
|
||||
} else if (networkEnvelope instanceof UpdateMultisigRequest) {
|
||||
handleUpdateMultisigRequest((UpdateMultisigRequest) networkEnvelope, peer);
|
||||
}
|
||||
}
|
||||
|
||||
@ -284,6 +277,26 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
thawUnreservedOutputs();
|
||||
}
|
||||
|
||||
public void shutDown() {
|
||||
|
||||
// collect trades to shutdown
|
||||
Set<Trade> trades = new HashSet<Trade>();
|
||||
trades.addAll(tradableList.getList());
|
||||
trades.addAll(closedTradableManager.getClosedTrades());
|
||||
trades.addAll(failedTradesManager.getObservableList());
|
||||
|
||||
// shut down trades in parallel
|
||||
Set<Runnable> tasks = new HashSet<Runnable>();
|
||||
for (Trade trade : trades) tasks.add(() -> {
|
||||
try {
|
||||
trade.shutDown();
|
||||
} catch (Exception e) {
|
||||
log.warn("Error closing trade subprocess. Was Haveno stopped manually with ctrl+c?");
|
||||
}
|
||||
});
|
||||
HavenoUtils.awaitTasks(tasks);
|
||||
}
|
||||
|
||||
private void thawUnreservedOutputs() {
|
||||
if (xmrWalletService.getWallet() == null) return;
|
||||
|
||||
@ -301,13 +314,15 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
Set<String> frozenKeyImages = xmrWalletService.getWallet().getOutputs(new MoneroOutputQuery()
|
||||
.setIsFrozen(true)
|
||||
.setIsSpent(false))
|
||||
.stream().map(output -> output.getKeyImage().getHex())
|
||||
.stream()
|
||||
.map(output -> output.getKeyImage().getHex())
|
||||
.collect(Collectors.toSet());
|
||||
frozenKeyImages.removeAll(reservedKeyImages);
|
||||
for (String unreservedFrozenKeyImage : frozenKeyImages) {
|
||||
log.info("Thawing output which is not reserved for offer or trade: " + unreservedFrozenKeyImage);
|
||||
xmrWalletService.getWallet().thawOutput(unreservedFrozenKeyImage);
|
||||
}
|
||||
xmrWalletService.getWallet().save();
|
||||
}
|
||||
|
||||
public TradeProtocol getTradeProtocol(Trade trade) {
|
||||
@ -369,7 +384,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
|
||||
private void initTradeAndProtocol(Trade trade, TradeProtocol tradeProtocol) {
|
||||
tradeProtocol.initialize(processModelServiceProvider, this, trade.getOffer());
|
||||
trade.initialize(processModelServiceProvider);
|
||||
requestPersistence(); // TODO requesting persistence twice with initPersistedTrade()
|
||||
}
|
||||
|
||||
@ -470,7 +484,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
|
||||
((ArbitratorProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||
log.warn("Arbitrator error during trade initialization for trade {}: {}", trade.getId(), errorMessage);
|
||||
maybeRemoveTrade(trade);
|
||||
removeTrade(trade);
|
||||
});
|
||||
|
||||
requestPersistence();
|
||||
@ -555,7 +569,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
((MakerProtocol) getTradeProtocol(trade)).handleInitTradeRequest(request, sender, errorMessage -> {
|
||||
log.warn("Maker error during trade initialization: " + errorMessage);
|
||||
openOfferManager.unreserveOpenOffer(openOffer); // offer remains available // TODO: only unreserve if funds not deposited to multisig
|
||||
maybeRemoveTrade(trade);
|
||||
removeTrade(trade);
|
||||
if (takeOfferRequestErrorMessageHandler != null) takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||
});
|
||||
|
||||
@ -658,45 +672,6 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
((TraderProtocol) getTradeProtocol(trade)).handleDepositResponse(response, peer);
|
||||
}
|
||||
|
||||
private void handlePaymentAccountKeyRequest(PaymentAccountKeyRequest request, NodeAddress peer) {
|
||||
log.info("Received PaymentAccountKeyRequest from {} with tradeId {} and uid {}", peer, request.getTradeId(), request.getUid());
|
||||
|
||||
try {
|
||||
Validator.nonEmptyStringOf(request.getTradeId());
|
||||
} catch (Throwable t) {
|
||||
log.warn("Invalid PaymentAccountKeyRequest message " + request.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||
if (!tradeOptional.isPresent()) {
|
||||
log.warn("No trade with id " + request.getTradeId());
|
||||
return;
|
||||
}
|
||||
Trade trade = tradeOptional.get();
|
||||
((ArbitratorProtocol) getTradeProtocol(trade)).handlePaymentAccountKeyRequest(request, peer);
|
||||
}
|
||||
|
||||
private void handleUpdateMultisigRequest(UpdateMultisigRequest request, NodeAddress peer) {
|
||||
log.info("Received UpdateMultisigRequest from {} with tradeId {} and uid {}", peer, request.getTradeId(), request.getUid());
|
||||
|
||||
try {
|
||||
Validator.nonEmptyStringOf(request.getTradeId());
|
||||
} catch (Throwable t) {
|
||||
log.warn("Invalid UpdateMultisigRequest message " + request.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
Optional<Trade> tradeOptional = getOpenTrade(request.getTradeId());
|
||||
if (!tradeOptional.isPresent()) throw new RuntimeException("No trade with id " + request.getTradeId()); // TODO (woodser): error handling
|
||||
Trade trade = tradeOptional.get();
|
||||
getTradeProtocol(trade).handleUpdateMultisigRequest(request, peer, errorMessage -> {
|
||||
log.warn("Error handling UpdateMultisigRequest: " + errorMessage);
|
||||
if (takeOfferRequestErrorMessageHandler != null)
|
||||
takeOfferRequestErrorMessageHandler.handleErrorMessage(errorMessage);
|
||||
});
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Take offer
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -777,7 +752,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
}, errorMessage -> {
|
||||
log.warn("Taker error during trade initialization: " + errorMessage);
|
||||
errorMessageHandler.handleErrorMessage(errorMessage);
|
||||
maybeRemoveTrade(trade);
|
||||
removeTrade(trade);
|
||||
});
|
||||
requestPersistence();
|
||||
}
|
||||
@ -830,8 +805,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
// If trade was completed (closed without fault but might be closed by a dispute) we move it to the closed trades
|
||||
public void onTradeCompleted(Trade trade) {
|
||||
closedTradableManager.add(trade);
|
||||
trade.setState(Trade.State.WITHDRAW_COMPLETED);
|
||||
maybeRemoveTrade(trade);
|
||||
trade.setState(Trade.State.TRADE_COMPLETED);
|
||||
removeTrade(trade);
|
||||
|
||||
// TODO The address entry should have been removed already. Check and if its the case remove that.
|
||||
xmrWalletService.resetAddressEntriesForPendingTrade(trade.getId());
|
||||
@ -899,7 +874,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
// If trade is in already in critical state (if taker role: taker fee; both roles: after deposit published)
|
||||
// we move the trade to failedTradesManager
|
||||
public void onMoveInvalidTradeToFailedTrades(Trade trade) {
|
||||
maybeRemoveTrade(trade);
|
||||
removeTrade(trade);
|
||||
failedTradesManager.add(trade);
|
||||
}
|
||||
|
||||
@ -1050,34 +1025,28 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
return closedTradableManager.getClosedTrades().stream().filter(e -> e.getId().equals(tradeId)).findFirst();
|
||||
}
|
||||
|
||||
private synchronized void maybeRemoveTrade(Trade trade) {
|
||||
log.info("TradeManager.maybeRemoveTrade()");
|
||||
private synchronized void removeTrade(Trade trade) {
|
||||
log.info("TradeManager.removeTrade()");
|
||||
synchronized(tradableList) {
|
||||
if (!tradableList.contains(trade)) return;
|
||||
|
||||
// delete trade if not possibly funded
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSIT_REQUESTED.ordinal() || trade.getPhase().ordinal() >= Trade.Phase.PAYOUT_PUBLISHED.ordinal()) { // TODO: delete after payout unlocked
|
||||
// remove trade
|
||||
tradableList.remove(trade);
|
||||
|
||||
// remove trade
|
||||
tradableList.remove(trade);
|
||||
|
||||
// unreserve trade key images
|
||||
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
|
||||
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) {
|
||||
xmrWalletService.getWallet().thawOutput(keyImage);
|
||||
}
|
||||
}
|
||||
|
||||
// delete multisig wallet
|
||||
deleteTradeWallet(trade);
|
||||
|
||||
// unregister and persist
|
||||
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
||||
requestPersistence();
|
||||
} else {
|
||||
log.warn("Not deleting trade " + trade.getId() + " because its trade wallet might be funded");
|
||||
// TODO: schedule wallet for deletion after unlock
|
||||
// unreserve trade key images
|
||||
if (trade instanceof TakerTrade && trade.getSelf().getReserveTxKeyImages() != null) {
|
||||
for (String keyImage : trade.getSelf().getReserveTxKeyImages()) xmrWalletService.getWallet().thawOutput(keyImage);
|
||||
xmrWalletService.getWallet().save();
|
||||
}
|
||||
|
||||
// delete trade wallet if before funded
|
||||
if (xmrWalletService.multisigWalletExists(trade.getId()) && !trade.isDepositRequested()) {
|
||||
trade.deleteWallet();
|
||||
}
|
||||
|
||||
// unregister and persist
|
||||
p2PService.removeDecryptedDirectMessageListener(getTradeProtocol(trade));
|
||||
requestPersistence();
|
||||
}
|
||||
}
|
||||
|
||||
@ -1094,9 +1063,4 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
|
||||
private void onTradesChanged() {
|
||||
this.numPendingTrades.set(getObservableList().size());
|
||||
}
|
||||
|
||||
private void deleteTradeWallet(Trade trade) {
|
||||
if (xmrWalletService.multisigWalletExists(trade.getId())) xmrWalletService.deleteMultisigWallet(trade.getId());
|
||||
else log.warn("Multisig wallet to delete for trade {} does not exist", trade.getId());
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,6 @@ import bisq.core.proto.CoreProtoResolver;
|
||||
|
||||
import bisq.network.p2p.DirectMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import com.google.protobuf.ByteString;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
@ -24,6 +24,8 @@ import bisq.network.p2p.NodeAddress;
|
||||
import com.google.protobuf.ByteString;
|
||||
import java.util.Optional;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import lombok.EqualsAndHashCode;
|
||||
@ -31,25 +33,24 @@ import lombok.Value;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
public final class PaymentAccountKeyResponse extends TradeMailboxMessage implements DirectMessage {
|
||||
public final class DepositsConfirmedMessage extends TradeMailboxMessage implements DirectMessage {
|
||||
private final NodeAddress senderNodeAddress;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
@Nullable
|
||||
private final byte[] paymentAccountKey;
|
||||
private final byte[] sellerPaymentAccountKey;
|
||||
@Nullable
|
||||
private final String updatedMultisigHex;
|
||||
|
||||
public PaymentAccountKeyResponse(String tradeId,
|
||||
public DepositsConfirmedMessage(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
PubKeyRing pubKeyRing,
|
||||
String uid,
|
||||
String messageVersion,
|
||||
@Nullable byte[] paymentAccountKey,
|
||||
@Nullable byte[] sellerPaymentAccountKey,
|
||||
@Nullable String updatedMultisigHex) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
super(Version.getP2PMessageVersion(), tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.paymentAccountKey = paymentAccountKey;
|
||||
this.sellerPaymentAccountKey = sellerPaymentAccountKey;
|
||||
this.updatedMultisigHex = updatedMultisigHex;
|
||||
}
|
||||
|
||||
@ -60,34 +61,34 @@ public final class PaymentAccountKeyResponse extends TradeMailboxMessage impleme
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.PaymentAccountKeyResponse.Builder builder = protobuf.PaymentAccountKeyResponse.newBuilder()
|
||||
protobuf.DepositsConfirmedMessage.Builder builder = protobuf.DepositsConfirmedMessage.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||
.setUid(uid);
|
||||
Optional.ofNullable(paymentAccountKey).ifPresent(e -> builder.setPaymentAccountKey(ByteString.copyFrom(e)));
|
||||
Optional.ofNullable(sellerPaymentAccountKey).ifPresent(e -> builder.setSellerPaymentAccountKey(ByteString.copyFrom(e)));
|
||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||
return getNetworkEnvelopeBuilder().setPaymentAccountKeyResponse(builder).build();
|
||||
return getNetworkEnvelopeBuilder().setDepositsConfirmedMessage(builder).build();
|
||||
}
|
||||
|
||||
public static PaymentAccountKeyResponse fromProto(protobuf.PaymentAccountKeyResponse proto,
|
||||
public static DepositsConfirmedMessage fromProto(protobuf.DepositsConfirmedMessage proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
String messageVersion) {
|
||||
return new PaymentAccountKeyResponse(proto.getTradeId(),
|
||||
return new DepositsConfirmedMessage(proto.getTradeId(),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getUid(),
|
||||
messageVersion,
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getPaymentAccountKey()),
|
||||
ProtoUtil.byteArrayOrNullFromProto(proto.getSellerPaymentAccountKey()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PaymentAccountKeyResponse {" +
|
||||
return "DepositsConfirmedMessage {" +
|
||||
"\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n pubKeyRing=" + pubKeyRing +
|
||||
",\n paymentAccountKey=" + paymentAccountKey +
|
||||
",\n sellerPaymentAccountKey=" + sellerPaymentAccountKey +
|
||||
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -1,77 +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.messages;
|
||||
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
|
||||
import bisq.network.p2p.DirectMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
public final class PaymentAccountKeyRequest extends TradeMessage implements DirectMessage {
|
||||
private final NodeAddress senderNodeAddress;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
|
||||
public PaymentAccountKeyRequest(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
PubKeyRing pubKeyRing,
|
||||
String uid,
|
||||
String messageVersion) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.PaymentAccountKeyRequest.Builder builder = protobuf.PaymentAccountKeyRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||
.setUid(uid);
|
||||
return getNetworkEnvelopeBuilder().setPaymentAccountKeyRequest(builder).build();
|
||||
}
|
||||
|
||||
public static PaymentAccountKeyRequest fromProto(protobuf.PaymentAccountKeyRequest proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
String messageVersion) {
|
||||
return new PaymentAccountKeyRequest(proto.getTradeId(),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getUid(),
|
||||
messageVersion);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PaymentAccountKeyRequest {" +
|
||||
"\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n pubKeyRing=" + pubKeyRing +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -22,6 +22,7 @@ import bisq.core.account.sign.SignedWitness;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
import bisq.common.proto.network.NetworkEnvelope;
|
||||
|
||||
import java.util.Optional;
|
||||
@ -38,7 +39,12 @@ import javax.annotation.Nullable;
|
||||
@Value
|
||||
public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
private final NodeAddress senderNodeAddress;
|
||||
private final String payoutTxHex;
|
||||
@Nullable
|
||||
private final String unsignedPayoutTxHex;
|
||||
@Nullable
|
||||
private final String signedPayoutTxHex;
|
||||
private final String updatedMultisigHex;
|
||||
private final boolean sawArrivedPaymentReceivedMsg;
|
||||
|
||||
// Added in v1.4.0
|
||||
@Nullable
|
||||
@ -47,13 +53,19 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
public PaymentReceivedMessage(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
@Nullable SignedWitness signedWitness,
|
||||
String signedPayoutTxHex) {
|
||||
String unsignedPayoutTxHex,
|
||||
String signedPayoutTxHex,
|
||||
String updatedMultisigHex,
|
||||
boolean sawArrivedPaymentReceivedMsg) {
|
||||
this(tradeId,
|
||||
senderNodeAddress,
|
||||
signedWitness,
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion(),
|
||||
signedPayoutTxHex);
|
||||
unsignedPayoutTxHex,
|
||||
signedPayoutTxHex,
|
||||
updatedMultisigHex,
|
||||
sawArrivedPaymentReceivedMsg);
|
||||
}
|
||||
|
||||
|
||||
@ -66,11 +78,17 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
@Nullable SignedWitness signedWitness,
|
||||
String uid,
|
||||
String messageVersion,
|
||||
String signedPayoutTxHex) {
|
||||
String unsignedPayoutTxHex,
|
||||
String signedPayoutTxHex,
|
||||
String updatedMultisigHex,
|
||||
boolean sawArrivedPaymentReceivedMsg) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.signedWitness = signedWitness;
|
||||
this.payoutTxHex = signedPayoutTxHex;
|
||||
this.unsignedPayoutTxHex = unsignedPayoutTxHex;
|
||||
this.signedPayoutTxHex = signedPayoutTxHex;
|
||||
this.updatedMultisigHex = updatedMultisigHex;
|
||||
this.sawArrivedPaymentReceivedMsg = sawArrivedPaymentReceivedMsg;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -79,8 +97,11 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setUid(uid)
|
||||
.setPayoutTxHex(payoutTxHex);
|
||||
.setSawArrivedPaymentReceivedMsg(sawArrivedPaymentReceivedMsg);
|
||||
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
|
||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||
Optional.ofNullable(unsignedPayoutTxHex).ifPresent(e -> builder.setUnsignedPayoutTxHex(unsignedPayoutTxHex));
|
||||
Optional.ofNullable(signedPayoutTxHex).ifPresent(e -> builder.setSignedPayoutTxHex(signedPayoutTxHex));
|
||||
return getNetworkEnvelopeBuilder().setPaymentReceivedMessage(builder).build();
|
||||
}
|
||||
|
||||
@ -96,7 +117,10 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
signedWitness,
|
||||
proto.getUid(),
|
||||
messageVersion,
|
||||
proto.getPayoutTxHex());
|
||||
ProtoUtil.stringOrNullFromProto(proto.getUnsignedPayoutTxHex()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getSignedPayoutTxHex()),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()),
|
||||
proto.getSawArrivedPaymentReceivedMsg());
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -104,7 +128,10 @@ public final class PaymentReceivedMessage extends TradeMailboxMessage {
|
||||
return "SellerReceivedPaymentMessage{" +
|
||||
"\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n signedWitness=" + signedWitness +
|
||||
",\n payoutTxHex=" + payoutTxHex +
|
||||
",\n unsignedPayoutTxHex=" + unsignedPayoutTxHex +
|
||||
",\n signedPayoutTxHex=" + signedPayoutTxHex +
|
||||
",\n updatedMultisigHex=" + (updatedMultisigHex == null ? null : updatedMultisigHex.substring(0, Math.max(updatedMultisigHex.length(), 1000))) +
|
||||
",\n sawArrivedPaymentReceivedMsg=" + sawArrivedPaymentReceivedMsg +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
||||
|
@ -52,8 +52,8 @@ public final class PaymentSentMessage extends TradeMailboxMessage {
|
||||
@Nullable String counterCurrencyTxId,
|
||||
@Nullable String counterCurrencyExtraData,
|
||||
String uid,
|
||||
String signedPayoutTxHex,
|
||||
String updatedMultisigHex,
|
||||
@Nullable String signedPayoutTxHex,
|
||||
@Nullable String updatedMultisigHex,
|
||||
@Nullable byte[] paymentAccountKey) {
|
||||
this(tradeId,
|
||||
senderNodeAddress,
|
||||
|
@ -1,118 +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.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 PayoutTxPublishedMessage extends TradeMailboxMessage {
|
||||
private final NodeAddress senderNodeAddress;
|
||||
private final boolean isMaker;
|
||||
private final String signedPayoutTxHex;
|
||||
|
||||
// Added in v1.4.0
|
||||
@Nullable
|
||||
private final SignedWitness signedWitness;
|
||||
|
||||
public PayoutTxPublishedMessage(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
boolean isMaker,
|
||||
@Nullable SignedWitness signedWitness,
|
||||
String signedPayoutTxHex) {
|
||||
this(tradeId,
|
||||
senderNodeAddress,
|
||||
isMaker,
|
||||
signedWitness,
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion(),
|
||||
signedPayoutTxHex);
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private PayoutTxPublishedMessage(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
boolean isMaker,
|
||||
@Nullable SignedWitness signedWitness,
|
||||
String uid,
|
||||
String messageVersion,
|
||||
String signedPayoutTxHex) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.isMaker = isMaker;
|
||||
this.signedWitness = signedWitness;
|
||||
this.signedPayoutTxHex = signedPayoutTxHex;
|
||||
}
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.PayoutTxPublishedMessage.Builder builder = protobuf.PayoutTxPublishedMessage.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setIsMaker(isMaker)
|
||||
.setUid(uid)
|
||||
.setSignedPayoutTxHex(signedPayoutTxHex);
|
||||
Optional.ofNullable(signedWitness).ifPresent(signedWitness -> builder.setSignedWitness(signedWitness.toProtoSignedWitness()));
|
||||
return getNetworkEnvelopeBuilder().setPayoutTxPublishedMessage(builder).build();
|
||||
}
|
||||
|
||||
public static NetworkEnvelope fromProto(protobuf.PayoutTxPublishedMessage 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 PayoutTxPublishedMessage(proto.getTradeId(),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
proto.getIsMaker(),
|
||||
signedWitness,
|
||||
proto.getUid(),
|
||||
messageVersion,
|
||||
proto.getSignedPayoutTxHex());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "PayoutTxPublishedMessage{" +
|
||||
"\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n isMaker=" + isMaker +
|
||||
",\n signedWitness=" + signedWitness +
|
||||
",\n signedPayoutTxHex=" + signedPayoutTxHex +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -1,99 +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.messages;
|
||||
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
|
||||
import bisq.network.p2p.DirectMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
public final class UpdateMultisigRequest extends TradeMessage implements DirectMessage {
|
||||
private final NodeAddress senderNodeAddress;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final long currentDate;
|
||||
@Nullable
|
||||
private final String updatedMultisigHex;
|
||||
|
||||
public UpdateMultisigRequest(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
PubKeyRing pubKeyRing,
|
||||
String uid,
|
||||
String messageVersion,
|
||||
long currentDate,
|
||||
String updatedMultisigHex) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.currentDate = currentDate;
|
||||
this.updatedMultisigHex = updatedMultisigHex;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.UpdateMultisigRequest.Builder builder = protobuf.UpdateMultisigRequest.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||
.setUid(uid);
|
||||
|
||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||
|
||||
builder.setCurrentDate(currentDate);
|
||||
|
||||
return getNetworkEnvelopeBuilder().setUpdateMultisigRequest(builder).build();
|
||||
}
|
||||
|
||||
public static UpdateMultisigRequest fromProto(protobuf.UpdateMultisigRequest proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
String messageVersion) {
|
||||
return new UpdateMultisigRequest(proto.getTradeId(),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getUid(),
|
||||
messageVersion,
|
||||
proto.getCurrentDate(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UpdateMultisigRequest {" +
|
||||
"\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n pubKeyRing=" + pubKeyRing +
|
||||
",\n currentDate=" + currentDate +
|
||||
",\n updatedMultisigHex='" + updatedMultisigHex +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -1,99 +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.messages;
|
||||
|
||||
import bisq.core.proto.CoreProtoResolver;
|
||||
|
||||
import bisq.network.p2p.DirectMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.proto.ProtoUtil;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Value;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Value
|
||||
public final class UpdateMultisigResponse extends TradeMessage implements DirectMessage {
|
||||
private final NodeAddress senderNodeAddress;
|
||||
private final PubKeyRing pubKeyRing;
|
||||
private final long currentDate;
|
||||
@Nullable
|
||||
private final String updatedMultisigHex;
|
||||
|
||||
public UpdateMultisigResponse(String tradeId,
|
||||
NodeAddress senderNodeAddress,
|
||||
PubKeyRing pubKeyRing,
|
||||
String uid,
|
||||
String messageVersion,
|
||||
long currentDate,
|
||||
String updatedMultisigHex) {
|
||||
super(messageVersion, tradeId, uid);
|
||||
this.senderNodeAddress = senderNodeAddress;
|
||||
this.pubKeyRing = pubKeyRing;
|
||||
this.currentDate = currentDate;
|
||||
this.updatedMultisigHex = updatedMultisigHex;
|
||||
}
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// PROTO BUFFER
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@Override
|
||||
public protobuf.NetworkEnvelope toProtoNetworkEnvelope() {
|
||||
protobuf.UpdateMultisigResponse.Builder builder = protobuf.UpdateMultisigResponse.newBuilder()
|
||||
.setTradeId(tradeId)
|
||||
.setSenderNodeAddress(senderNodeAddress.toProtoMessage())
|
||||
.setPubKeyRing(pubKeyRing.toProtoMessage())
|
||||
.setUid(uid);
|
||||
|
||||
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
|
||||
|
||||
builder.setCurrentDate(currentDate);
|
||||
|
||||
return getNetworkEnvelopeBuilder().setUpdateMultisigResponse(builder).build();
|
||||
}
|
||||
|
||||
public static UpdateMultisigResponse fromProto(protobuf.UpdateMultisigResponse proto,
|
||||
CoreProtoResolver coreProtoResolver,
|
||||
String messageVersion) {
|
||||
return new UpdateMultisigResponse(proto.getTradeId(),
|
||||
NodeAddress.fromProto(proto.getSenderNodeAddress()),
|
||||
PubKeyRing.fromProto(proto.getPubKeyRing()),
|
||||
proto.getUid(),
|
||||
messageVersion,
|
||||
proto.getCurrentDate(),
|
||||
ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "UpdateMultisigResponse {" +
|
||||
"\n senderNodeAddress=" + senderNodeAddress +
|
||||
",\n pubKeyRing=" + pubKeyRing +
|
||||
",\n currentDate=" + currentDate +
|
||||
",\n updatedMultisigHex='" + updatedMultisigHex +
|
||||
"\n} " + super.toString();
|
||||
}
|
||||
}
|
@ -6,18 +6,16 @@ import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.DepositRequest;
|
||||
import bisq.core.trade.messages.DepositResponse;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyRequest;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.core.trade.protocol.FluentProtocol.Condition;
|
||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||
import bisq.core.trade.protocol.tasks.ArbitratorProcessDepositRequest;
|
||||
import bisq.core.trade.protocol.tasks.ArbitratorProcessPaymentAccountKeyRequest;
|
||||
import bisq.core.trade.protocol.tasks.ArbitratorProcessReserveTx;
|
||||
import bisq.core.trade.protocol.tasks.ArbitratorProcessPayoutTxPublishedMessage;
|
||||
import bisq.core.trade.protocol.tasks.ArbitratorSendInitTradeOrMultisigRequests;
|
||||
import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
||||
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
|
||||
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToSeller;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
import bisq.core.util.Validator;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@ -32,17 +30,11 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
||||
@Override
|
||||
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
||||
super.onTradeMessage(message, peer);
|
||||
if (message instanceof PayoutTxPublishedMessage) {
|
||||
handle((PayoutTxPublishedMessage) message, peer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMailboxMessage(TradeMessage message, NodeAddress peer) {
|
||||
super.onMailboxMessage(message, peer);
|
||||
if (message instanceof PayoutTxPublishedMessage) {
|
||||
handle((PayoutTxPublishedMessage) message, peer);
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -119,56 +111,9 @@ public class ArbitratorProtocol extends DisputeProtocol {
|
||||
log.warn("Arbitrator ignoring DepositResponse for trade " + response.getTradeId());
|
||||
}
|
||||
|
||||
public void handlePaymentAccountKeyRequest(PaymentAccountKeyRequest request, NodeAddress sender) {
|
||||
System.out.println("ArbitratorProtocol.handlePaymentAccountKeyRequest() " + trade.getId());
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||
processModel.setTradeMessage(request);
|
||||
expect(new Condition(trade)
|
||||
.with(request)
|
||||
.from(sender))
|
||||
.setup(tasks(
|
||||
ArbitratorProcessPaymentAccountKeyRequest.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
stopTimeout();
|
||||
handleTaskRunnerSuccess(sender, request);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, request, errorMessage);
|
||||
}))
|
||||
.withTimeout(TRADE_TIMEOUT))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
protected void handle(PayoutTxPublishedMessage request, NodeAddress peer) {
|
||||
System.out.println("ArbitratorProtocol.handle(PayoutTxPublishedMessage)");
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
if (trade.isCompleted()) return; // ignore subsequent requests
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), request);
|
||||
processModel.setTradeMessage(request);
|
||||
expect(anyPhase(Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED, Trade.Phase.DEPOSITS_UNLOCKED)
|
||||
.with(request)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
ArbitratorProcessPayoutTxPublishedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, request);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, request, errorMessage);
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
|
||||
return new Class[] { SendDepositsConfirmedMessageToBuyer.class, SendDepositsConfirmedMessageToSeller.class };
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.DepositResponse;
|
||||
import bisq.core.trade.messages.InitMultisigRequest;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.SignContractRequest;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
@ -31,7 +31,7 @@ import bisq.core.trade.protocol.tasks.ProcessInitTradeRequest;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
import bisq.common.taskrunner.Task;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@ -102,7 +102,7 @@ public class BuyerAsMakerProtocol extends BuyerProtocol implements MakerProtocol
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(PaymentAccountKeyResponse request, NodeAddress sender) {
|
||||
public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
|
||||
super.handle(request, sender);
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,7 @@ import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.handlers.TradeResultHandler;
|
||||
import bisq.core.trade.messages.DepositResponse;
|
||||
import bisq.core.trade.messages.InitMultisigRequest;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.SignContractRequest;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
@ -113,7 +113,7 @@ public class BuyerAsTakerProtocol extends BuyerProtocol implements TakerProtocol
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handle(PaymentAccountKeyResponse request, NodeAddress sender) {
|
||||
public void handle(DepositsConfirmedMessage request, NodeAddress sender) {
|
||||
super.handle(request, sender);
|
||||
}
|
||||
|
||||
|
@ -17,35 +17,23 @@
|
||||
|
||||
package bisq.core.trade.protocol;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
import bisq.core.trade.BuyerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.core.trade.protocol.FluentProtocol.Condition;
|
||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
|
||||
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentAccountKeyResponse;
|
||||
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.BuyerSendPaymentAccountKeyRequestToArbitrator;
|
||||
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
|
||||
import bisq.core.trade.protocol.tasks.BuyerSendPayoutTxPublishedMessage;
|
||||
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener;
|
||||
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener;
|
||||
import bisq.core.util.Validator;
|
||||
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToArbitrator;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
|
||||
@Slf4j
|
||||
public abstract class BuyerProtocol extends DisputeProtocol {
|
||||
public class BuyerProtocol extends DisputeProtocol {
|
||||
|
||||
private boolean listeningToSendPaymentAccountKey;
|
||||
private boolean paymentAccountPayloadKeyRequestSent;
|
||||
enum BuyerEvent implements FluentProtocol.Event {
|
||||
STARTUP,
|
||||
DEPOSIT_TXS_CONFIRMED,
|
||||
@ -66,23 +54,8 @@ public abstract class BuyerProtocol extends DisputeProtocol {
|
||||
|
||||
// TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades
|
||||
|
||||
// request key to decrypt seller's payment account payload after first confirmation
|
||||
sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent.STARTUP, false);
|
||||
|
||||
// listen for deposit txs
|
||||
given(anyPhase(Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED)
|
||||
.with(BuyerEvent.STARTUP))
|
||||
.setup(tasks(SetupDepositTxsListener.class))
|
||||
.executeTasks();
|
||||
|
||||
// listen for payout tx
|
||||
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(BuyerEvent.STARTUP))
|
||||
.setup(tasks(SetupPayoutTxListener.class))
|
||||
.executeTasks();
|
||||
|
||||
// send payment sent message
|
||||
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED) // TODO: remove payment received phase?
|
||||
given(anyPhase(Trade.Phase.PAYMENT_SENT)
|
||||
.anyState(Trade.State.BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG, Trade.State.BUYER_SEND_FAILED_PAYMENT_SENT_MSG)
|
||||
.with(BuyerEvent.STARTUP))
|
||||
.setup(tasks(BuyerSendPaymentSentMessage.class))
|
||||
@ -92,49 +65,16 @@ public abstract class BuyerProtocol extends DisputeProtocol {
|
||||
@Override
|
||||
protected void onTradeMessage(TradeMessage message, NodeAddress peer) {
|
||||
super.onTradeMessage(message, peer);
|
||||
if (message instanceof PaymentReceivedMessage) {
|
||||
handle((PaymentReceivedMessage) message, peer);
|
||||
} if (message instanceof PaymentAccountKeyResponse) {
|
||||
handle((PaymentAccountKeyResponse) message, peer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMailboxMessage(TradeMessage message, NodeAddress peer) {
|
||||
super.onMailboxMessage(message, peer);
|
||||
if (message instanceof PaymentReceivedMessage) {
|
||||
handle((PaymentReceivedMessage) message, peer);
|
||||
} else if (message instanceof PaymentAccountKeyResponse) {
|
||||
handle((PaymentAccountKeyResponse) message, peer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) {
|
||||
super.handleSignContractResponse(response, sender);
|
||||
sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent.DEPOSIT_TXS_CONFIRMED, true);
|
||||
}
|
||||
|
||||
public void handle(PaymentAccountKeyResponse response, NodeAddress sender) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handlePaymentAccountKeyResponse()");
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
expect(new Condition(trade)
|
||||
.with(response)
|
||||
.from(sender))
|
||||
.setup(tasks(BuyerProcessPaymentAccountKeyResponse.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(sender, response);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, response, errorMessage);
|
||||
})))
|
||||
.executeTasks();
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -177,84 +117,9 @@ public abstract class BuyerProtocol extends DisputeProtocol {
|
||||
}).start();
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
// Incoming message Payout tx
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
||||
System.out.println("BuyerProtocol.handle(PaymentReceivedMessage)");
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
expect(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
BuyerProcessPaymentReceivedMessage.class,
|
||||
BuyerSendPayoutTxPublishedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void sendPaymentAccountKeyRequestIfWhenNeeded(BuyerEvent event, boolean waitForSellerOnConfirm) {
|
||||
|
||||
// skip if payment account payload already decrypted or not enough progress
|
||||
if (trade.getSeller().getPaymentAccountPayload() != null) return;
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSIT_REQUESTED.ordinal()) return;
|
||||
|
||||
// if confirmed and waiting for seller, recheck later
|
||||
if (trade.getState() == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN && waitForSellerOnConfirm) {
|
||||
UserThread.runAfter(() -> {
|
||||
sendPaymentAccountKeyRequestIfWhenNeeded(event, false);
|
||||
}, TRADE_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
// else if confirmed send request and return
|
||||
else if (trade.getState().ordinal() >= Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN.ordinal()) {
|
||||
sendPaymentAccountKeyRequest(event);
|
||||
return;
|
||||
}
|
||||
|
||||
// register for state changes once
|
||||
if (!listeningToSendPaymentAccountKey) {
|
||||
listeningToSendPaymentAccountKey = true;
|
||||
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||
sendPaymentAccountKeyRequestIfWhenNeeded(event, waitForSellerOnConfirm);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void sendPaymentAccountKeyRequest(BuyerEvent event) {
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
if (paymentAccountPayloadKeyRequestSent) return;
|
||||
if (trade.getSeller().getPaymentAccountPayload() != null) return; // skip if initialized
|
||||
latchTrade();
|
||||
expect(new Condition(trade))
|
||||
.setup(tasks(BuyerSendPaymentAccountKeyRequestToArbitrator.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(event);
|
||||
},
|
||||
(errorMessage) -> {
|
||||
handleTaskRunnerFault(event, errorMessage);
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
paymentAccountPayloadKeyRequestSent = true;
|
||||
}
|
||||
}).start();
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
|
||||
return new Class[] { SendDepositsConfirmedMessageToArbitrator.class };
|
||||
}
|
||||
}
|
||||
|
@ -22,25 +22,20 @@ import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentSentMessage;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.core.trade.protocol.BuyerProtocol.BuyerEvent;
|
||||
import bisq.core.trade.protocol.FluentProtocol.Condition;
|
||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||
import bisq.core.trade.protocol.tasks.SellerMaybeSendPayoutTxPublishedMessage;
|
||||
import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
|
||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentAccountPayloadKey;
|
||||
import bisq.core.trade.protocol.tasks.SetupDepositTxsListener;
|
||||
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener;
|
||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToArbitrator;
|
||||
import bisq.core.trade.protocol.tasks.SendDepositsConfirmedMessageToBuyer;
|
||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.handlers.ErrorMessageHandler;
|
||||
import bisq.common.handlers.ResultHandler;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
|
||||
@Slf4j
|
||||
public abstract class SellerProtocol extends DisputeProtocol {
|
||||
public class SellerProtocol extends DisputeProtocol {
|
||||
enum SellerEvent implements FluentProtocol.Event {
|
||||
STARTUP,
|
||||
DEPOSIT_TXS_CONFIRMED,
|
||||
@ -54,25 +49,6 @@ public abstract class SellerProtocol extends DisputeProtocol {
|
||||
@Override
|
||||
protected void onInitialized() {
|
||||
super.onInitialized();
|
||||
|
||||
// TODO: run with trade lock and latch, otherwise getting invalid transition warnings on startup after offline trades
|
||||
|
||||
// send payment account payload key when trade state is confirmed
|
||||
if (trade.getPhase() == Trade.Phase.DEPOSIT_REQUESTED || trade.getPhase() == Trade.Phase.DEPOSITS_PUBLISHED) {
|
||||
sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent.STARTUP);
|
||||
}
|
||||
|
||||
// listen for deposit txs
|
||||
given(anyPhase(Trade.Phase.DEPOSIT_REQUESTED, Trade.Phase.DEPOSITS_PUBLISHED, Trade.Phase.DEPOSITS_CONFIRMED)
|
||||
.with(SellerEvent.STARTUP))
|
||||
.setup(tasks(SetupDepositTxsListener.class))
|
||||
.executeTasks();
|
||||
|
||||
// listen for payout tx
|
||||
given(anyPhase(Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED)
|
||||
.with(BuyerEvent.STARTUP))
|
||||
.setup(tasks(SetupPayoutTxListener.class))
|
||||
.executeTasks();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -94,7 +70,6 @@ public abstract class SellerProtocol extends DisputeProtocol {
|
||||
|
||||
@Override
|
||||
public void handleSignContractResponse(SignContractResponse response, NodeAddress sender) {
|
||||
sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent.DEPOSIT_TXS_CONFIRMED);
|
||||
super.handleSignContractResponse(response, sender);
|
||||
}
|
||||
|
||||
@ -163,8 +138,8 @@ public abstract class SellerProtocol extends DisputeProtocol {
|
||||
.setup(tasks(
|
||||
ApplyFilter.class,
|
||||
SellerPreparePaymentReceivedMessage.class,
|
||||
SellerMaybeSendPayoutTxPublishedMessage.class,
|
||||
SellerSendPaymentReceivedMessage.class)
|
||||
SellerSendPaymentReceivedMessageToBuyer.class,
|
||||
SellerSendPaymentReceivedMessageToArbitrator.class)
|
||||
.using(new TradeTaskRunner(trade, () -> {
|
||||
this.errorMessageHandler = null;
|
||||
handleTaskRunnerSuccess(event);
|
||||
@ -183,26 +158,9 @@ public abstract class SellerProtocol extends DisputeProtocol {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void sendPaymentAccountPayloadKeyWhenConfirmed(SellerEvent event) {
|
||||
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) {
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
expect(new Condition(trade))
|
||||
.setup(tasks(SellerSendPaymentAccountPayloadKey.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(event);
|
||||
},
|
||||
(errorMessage) -> {
|
||||
handleTaskRunnerFault(event, errorMessage);
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
});
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Class<? extends TradeTask>[] getDepsitsConfirmedTasks() {
|
||||
return new Class[] { SendDepositsConfirmedMessageToBuyer.class };
|
||||
}
|
||||
}
|
||||
|
@ -18,24 +18,30 @@
|
||||
package bisq.core.trade.protocol;
|
||||
|
||||
import bisq.core.offer.Offer;
|
||||
import bisq.core.trade.ArbitratorTrade;
|
||||
import bisq.core.trade.BuyerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeManager;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.trade.handlers.TradeResultHandler;
|
||||
import bisq.core.trade.messages.PaymentSentMessage;
|
||||
import bisq.core.trade.messages.DepositResponse;
|
||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
||||
import bisq.core.trade.messages.InitMultisigRequest;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.SignContractRequest;
|
||||
import bisq.core.trade.messages.SignContractResponse;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.core.trade.messages.UpdateMultisigRequest;
|
||||
import bisq.core.trade.protocol.tasks.RemoveOffer;
|
||||
import bisq.core.trade.protocol.tasks.TradeTask;
|
||||
import bisq.core.trade.protocol.FluentProtocol.Condition;
|
||||
import bisq.core.trade.protocol.tasks.MaybeSendSignContractRequest;
|
||||
import bisq.core.trade.protocol.tasks.ProcessDepositResponse;
|
||||
import bisq.core.trade.protocol.tasks.ProcessDepositsConfirmedMessage;
|
||||
import bisq.core.trade.protocol.tasks.ProcessInitMultisigRequest;
|
||||
import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.ProcessSignContractRequest;
|
||||
import bisq.core.trade.protocol.tasks.ProcessSignContractResponse;
|
||||
import bisq.core.trade.protocol.tasks.ProcessUpdateMultisigRequest;
|
||||
import bisq.core.util.Validator;
|
||||
|
||||
import bisq.network.p2p.AckMessage;
|
||||
@ -47,7 +53,6 @@ import bisq.network.p2p.SendMailboxMessageListener;
|
||||
import bisq.network.p2p.mailbox.MailboxMessage;
|
||||
import bisq.network.p2p.mailbox.MailboxMessageService;
|
||||
import bisq.network.p2p.messaging.DecryptedMailboxListener;
|
||||
|
||||
import bisq.common.Timer;
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
@ -58,7 +63,6 @@ import bisq.common.taskrunner.Task;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.CountDownLatch;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
@ -93,10 +97,20 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
|
||||
protected void onTradeMessage(TradeMessage message, NodeAddress peerNodeAddress) {
|
||||
log.info("Received {} as TradeMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
|
||||
if (message instanceof DepositsConfirmedMessage) {
|
||||
handle((DepositsConfirmedMessage) message, peerNodeAddress);
|
||||
} else if (message instanceof PaymentReceivedMessage) {
|
||||
handle((PaymentReceivedMessage) message, peerNodeAddress);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onMailboxMessage(TradeMessage message, NodeAddress peerNodeAddress) {
|
||||
log.info("Received {} as MailboxMessage from {} with tradeId {} and uid {}", message.getClass().getSimpleName(), peerNodeAddress, message.getTradeId(), message.getUid());
|
||||
if (message instanceof DepositsConfirmedMessage) {
|
||||
handle((DepositsConfirmedMessage) message, peerNodeAddress);
|
||||
} else if (message instanceof PaymentReceivedMessage) {
|
||||
handle((PaymentReceivedMessage) message, peerNodeAddress);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -110,20 +124,22 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}
|
||||
|
||||
protected void onInitialized() {
|
||||
if (!trade.isWithdrawn()) {
|
||||
if (!trade.isCompleted()) {
|
||||
processModel.getP2PService().addDecryptedDirectMessageListener(this);
|
||||
}
|
||||
|
||||
// handle trade events
|
||||
EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||
if (state == Trade.State.DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN) sendDepositsConfirmedMessage();
|
||||
});
|
||||
|
||||
// initialize trade
|
||||
trade.initialize(processModel.getProvider());
|
||||
|
||||
// process mailbox messages
|
||||
MailboxMessageService mailboxMessageService = processModel.getP2PService().getMailboxMessageService();
|
||||
// We delay a bit here as the trade gets updated from the wallet to update the trade
|
||||
// state (deposit confirmed) and that happens after our method is called.
|
||||
// TODO To fix that in a better way we would need to change the order of some routines
|
||||
// from the TradeManager, but as we are close to a release I dont want to risk a bigger
|
||||
// change and leave that for a later PR
|
||||
UserThread.runAfter(() -> {
|
||||
mailboxMessageService.addDecryptedMailboxListener(this);
|
||||
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
||||
}, 100, TimeUnit.MILLISECONDS);
|
||||
mailboxMessageService.addDecryptedMailboxListener(this);
|
||||
handleMailboxCollection(mailboxMessageService.getMyDecryptedMailboxMessages());
|
||||
}
|
||||
|
||||
public void onWithdrawCompleted() {
|
||||
@ -196,7 +212,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
TradeMessage tradeMessage = (TradeMessage) mailboxMessage;
|
||||
// We only remove here if we have already completed the trade.
|
||||
// Otherwise removal is done after successfully applied the task runner.
|
||||
if (trade.isWithdrawn()) {
|
||||
if (trade.isCompleted()) {
|
||||
processModel.getP2PService().getMailboxMessageService().removeMailboxMsg(mailboxMessage);
|
||||
log.info("Remove {} from the P2P network as trade is already completed.",
|
||||
tradeMessage.getClass().getSimpleName());
|
||||
@ -205,7 +221,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
onMailboxMessage(tradeMessage, mailboxMessage.getSenderNodeAddress());
|
||||
} else if (mailboxMessage instanceof AckMessage) {
|
||||
AckMessage ackMessage = (AckMessage) mailboxMessage;
|
||||
if (!trade.isWithdrawn()) {
|
||||
if (!trade.isCompleted()) {
|
||||
// We only apply the msg if we have not already completed the trade
|
||||
onAckMessage(ackMessage, mailboxMessage.getSenderNodeAddress());
|
||||
}
|
||||
@ -227,8 +243,10 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
// Abstract
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
public abstract Class<? extends TradeTask>[] getDepsitsConfirmedTasks();
|
||||
|
||||
public void handleInitMultisigRequest(InitMultisigRequest request, NodeAddress sender) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleInitMultisigRequest()");
|
||||
System.out.println(getClass().getSimpleName() + ".handleInitMultisigRequest()");
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
@ -256,7 +274,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}
|
||||
|
||||
public void handleSignContractRequest(SignContractRequest message, NodeAddress sender) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleSignContractRequest() " + trade.getId());
|
||||
System.out.println(getClass().getSimpleName() + ".handleSignContractRequest() " + trade.getId());
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
@ -292,7 +310,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}
|
||||
|
||||
public void handleSignContractResponse(SignContractResponse message, NodeAddress sender) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleSignContractResponse() " + trade.getId());
|
||||
System.out.println(getClass().getSimpleName() + ".handleSignContractResponse() " + trade.getId());
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
@ -329,7 +347,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}
|
||||
|
||||
public void handleDepositResponse(DepositResponse response, NodeAddress sender) {
|
||||
System.out.println(getClass().getCanonicalName() + ".handleDepositResponse()");
|
||||
System.out.println(getClass().getSimpleName() + ".handleDepositResponse()");
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
@ -358,25 +376,55 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
}).start();
|
||||
}
|
||||
|
||||
// TODO (woodser): update to use fluent for consistency
|
||||
public void handleUpdateMultisigRequest(UpdateMultisigRequest message, NodeAddress peer, ErrorMessageHandler errorMessageHandler) {
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
TradeTaskRunner taskRunner = new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
stopTimeout();
|
||||
handleTaskRunnerSuccess(peer, message, "handleUpdateMultisigRequest");
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
});
|
||||
taskRunner.addTasks(
|
||||
ProcessUpdateMultisigRequest.class
|
||||
);
|
||||
startTimeout(TRADE_TIMEOUT);
|
||||
taskRunner.run();
|
||||
awaitTradeLatch();
|
||||
public void handle(DepositsConfirmedMessage response, NodeAddress sender) {
|
||||
System.out.println(getClass().getSimpleName() + ".handle(DepositsConfirmedMessage)");
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
expect(new Condition(trade)
|
||||
.with(response)
|
||||
.from(sender))
|
||||
.setup(tasks(ProcessDepositsConfirmedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(sender, response);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(sender, response, errorMessage);
|
||||
})))
|
||||
.executeTasks();
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
// received by buyer and arbitrator
|
||||
protected void handle(PaymentReceivedMessage message, NodeAddress peer) {
|
||||
System.out.println(getClass().getSimpleName() + ".handle(PaymentReceivedMessage)");
|
||||
if (!(trade instanceof BuyerTrade || trade instanceof ArbitratorTrade)) {
|
||||
log.warn("Ignoring PaymentReceivedMessage since not buyer or arbitrator");
|
||||
return;
|
||||
}
|
||||
if (trade instanceof ArbitratorTrade && !trade.isPayoutUnlocked()) trade.syncWallet(); // arbitrator syncs slowly after deposits confirmed
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
processModel.setTradeMessage(message);
|
||||
expect(anyPhase(trade instanceof ArbitratorTrade ? new Trade.Phase[] { Trade.Phase.DEPOSITS_UNLOCKED } : new Trade.Phase[] { Trade.Phase.PAYMENT_SENT, Trade.Phase.PAYMENT_RECEIVED })
|
||||
.with(message)
|
||||
.from(peer))
|
||||
.setup(tasks(
|
||||
ProcessPaymentReceivedMessage.class)
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(peer, message);
|
||||
},
|
||||
errorMessage -> {
|
||||
handleTaskRunnerFault(peer, message, errorMessage);
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
@ -591,7 +639,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
// Private
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
private void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) {
|
||||
protected void handleTaskRunnerSuccess(NodeAddress sender, @Nullable TradeMessage message, String source) {
|
||||
log.info("TaskRunner successfully completed. Triggered from {}, tradeId={}", source, trade.getId());
|
||||
if (message != null) {
|
||||
sendAckMessage(sender, message, true, null);
|
||||
@ -638,7 +686,7 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
|
||||
protected void awaitTradeLatch() {
|
||||
if (tradeLatch == null) return;
|
||||
TradeUtils.awaitLatch(tradeLatch);
|
||||
HavenoUtils.awaitLatch(tradeLatch);
|
||||
}
|
||||
|
||||
private boolean isMyMessage(NetworkEnvelope message) {
|
||||
@ -653,4 +701,23 @@ public abstract class TradeProtocol implements DecryptedDirectMessageListener, D
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void sendDepositsConfirmedMessage() {
|
||||
new Thread(() -> {
|
||||
synchronized (trade) {
|
||||
latchTrade();
|
||||
expect(new Condition(trade))
|
||||
.setup(tasks(getDepsitsConfirmedTasks())
|
||||
.using(new TradeTaskRunner(trade,
|
||||
() -> {
|
||||
handleTaskRunnerSuccess(null, null, "SendDepositsConfirmedMessages");
|
||||
},
|
||||
(errorMessage) -> {
|
||||
handleTaskRunnerFault(null, null, errorMessage);
|
||||
})))
|
||||
.executeTasks(true);
|
||||
awaitTradeLatch();
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
}
|
||||
|
@ -36,7 +36,6 @@ import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.daemon.model.MoneroTx;
|
||||
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
|
||||
|
@ -1,81 +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;
|
||||
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.SendDirectMessageListener;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ArbitratorProcessPaymentAccountKeyRequest extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public ArbitratorProcessPaymentAccountKeyRequest(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// ensure deposit txs confirmed
|
||||
trade.listenForDepositTxs();
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
|
||||
throw new RuntimeException("Arbitrator refusing payment account key request for trade " + trade.getId() + " because the deposit txs have not confirmed");
|
||||
}
|
||||
|
||||
// create response for buyer with key to decrypt seller's payment account payload
|
||||
PaymentAccountKeyResponse response = new PaymentAccountKeyResponse(
|
||||
trade.getId(),
|
||||
processModel.getMyNodeAddress(),
|
||||
processModel.getPubKeyRing(),
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion(),
|
||||
trade.getSeller().getPaymentAccountKey(),
|
||||
null
|
||||
);
|
||||
|
||||
// send response to buyer
|
||||
NodeAddress buyerAddress = trade.getBuyer().getNodeAddress();
|
||||
log.info("Arbitrator sending PaymentAccountKeyResponse to buyer={}; offerId={}", buyerAddress, trade.getId());
|
||||
processModel.getP2PService().sendEncryptedDirectMessage(buyerAddress, trade.getBuyer().getPubKeyRing(), response, new SendDirectMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived: trading peer={}; offerId={}; uid={}", response.getClass().getSimpleName(), buyerAddress, trade.getId());
|
||||
complete();
|
||||
}
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), buyerAddress, trade.getId(), errorMessage);
|
||||
appendToErrorMessage("Sending message failed: message=" + response + "\nerrorMessage=" + errorMessage);
|
||||
failed();
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,57 +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;
|
||||
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ArbitratorProcessPayoutTxPublishedMessage extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public ArbitratorProcessPayoutTxPublishedMessage(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
PayoutTxPublishedMessage request = (PayoutTxPublishedMessage) processModel.getTradeMessage();
|
||||
|
||||
// verify and publish payout tx
|
||||
trade.verifyPayoutTx(request.getSignedPayoutTxHex(), false, true);
|
||||
|
||||
// update latest peer address
|
||||
if (request.isMaker()) trade.getMaker().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
else trade.getTaker().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
|
||||
// TODO: publish signed witness data?
|
||||
//request.getSignedWitness()
|
||||
|
||||
// close arbitrator trade
|
||||
processModel.getTradeManager().onTradeCompleted(trade);
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -63,24 +63,25 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
|
||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
|
||||
|
||||
// create payout tx if we have seller's updated multisig hex
|
||||
if (trade.getTradingPeer().getUpdatedMultisigHex() != null) {
|
||||
// import multisig hex
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getSeller().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getSeller().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) {
|
||||
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0])); // TODO (monero-project): fails if multisig hex imported individually
|
||||
trade.saveWallet();
|
||||
}
|
||||
|
||||
// create payout tx
|
||||
// create payout tx if we have seller's updated multisig hex
|
||||
if (trade.getSeller().getUpdatedMultisigHex() != null) {
|
||||
|
||||
// create payout tx
|
||||
log.info("Buyer creating unsigned payout tx");
|
||||
multisigWallet.importMultisigHex(trade.getTradingPeer().getUpdatedMultisigHex());
|
||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||
trade.setPayoutTx(payoutTx);
|
||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
|
||||
// start listening for published payout tx
|
||||
trade.listenForPayoutTx();
|
||||
} else {
|
||||
if (trade.getSelf().getUpdatedMultisigHex() == null) trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); // only export multisig hex once
|
||||
}
|
||||
|
||||
// close multisig wallet
|
||||
walletService.closeMultisigWallet(trade.getId());
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
@ -1,60 +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;
|
||||
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class BuyerProcessPaymentAccountKeyResponse extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public BuyerProcessPaymentAccountKeyResponse(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// update peer node address if not from arbitrator
|
||||
if (!processModel.getTempTradingPeerNodeAddress().equals(trade.getArbitrator().getNodeAddress())) {
|
||||
trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
}
|
||||
|
||||
// decrypt peer's payment account payload
|
||||
PaymentAccountKeyResponse request = (PaymentAccountKeyResponse) processModel.getTradeMessage();
|
||||
if (trade.getTradingPeer().getPaymentAccountPayload() == null) {
|
||||
trade.decryptPeersPaymentAccountPayload(request.getPaymentAccountKey());
|
||||
}
|
||||
|
||||
// store updated multisig hex for processing on payment sent
|
||||
if (request.getUpdatedMultisigHex() != null) trade.getTradingPeer().setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||
|
||||
// persist and complete
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,73 +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;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyRequest;
|
||||
import bisq.network.p2p.SendDirectMessageListener;
|
||||
import java.util.UUID;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class BuyerSendPaymentAccountKeyRequestToArbitrator extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public BuyerSendPaymentAccountKeyRequestToArbitrator(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// create request to arbitrator
|
||||
PaymentAccountKeyRequest request = new PaymentAccountKeyRequest(
|
||||
trade.getId(),
|
||||
processModel.getMyNodeAddress(),
|
||||
processModel.getPubKeyRing(),
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion()
|
||||
);
|
||||
|
||||
// send request to arbitrator
|
||||
log.info("Sending {} with offerId {} and uid {} to arbitrator {} with pub key ring {}", request.getClass().getSimpleName(), request.getTradeId(), request.getUid(), trade.getArbitrator().getNodeAddress(), trade.getArbitrator().getPubKeyRing());
|
||||
processModel.getP2PService().sendEncryptedDirectMessage(
|
||||
trade.getArbitrator().getNodeAddress(),
|
||||
trade.getArbitrator().getPubKeyRing(),
|
||||
request,
|
||||
new SendDirectMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at arbitrator: offerId={}", PaymentAccountKeyRequest.class.getSimpleName(), trade.getId());
|
||||
complete();
|
||||
}
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.warn("Failed to send {} to arbitrator, error={}.", PaymentAccountKeyRequest.class.getSimpleName(), errorMessage);
|
||||
failed();
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -63,7 +63,7 @@ public class BuyerSendPaymentSentMessage extends SendMailboxMessageTask {
|
||||
trade.getCounterCurrencyExtraData(),
|
||||
deterministicId,
|
||||
trade.getPayoutTxHex(),
|
||||
trade.getBuyer().getUpdatedMultisigHex(),
|
||||
trade.getSelf().getUpdatedMultisigHex(),
|
||||
trade.getSelf().getPaymentAccountKey()
|
||||
);
|
||||
}
|
||||
|
@ -1,82 +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;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.payment.PaymentAccount;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import java.util.UUID;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
public class BuyerSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
|
||||
|
||||
public BuyerSendPayoutTxPublishedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getArbitrator().getNodeAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getArbitrator().getPubKeyRing();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
|
||||
return new PayoutTxPublishedMessage(
|
||||
tradeId,
|
||||
processModel.getMyNodeAddress(),
|
||||
trade.isMaker(),
|
||||
null, // TODO: send witness data?
|
||||
trade.getPayoutTxHex()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateSent() {
|
||||
log.info("Buyer sent PayoutTxPublishedMessage: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateArrived() {
|
||||
log.info("Buyer's PayoutTxPublishedMessage arrived: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateStoredInMailbox() {
|
||||
log.info("Buyer's PayoutTxPublishedMessage stored in mailbox: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateFault() {
|
||||
log.error("Buyer's PayoutTxPublishedMessage failed: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
||||
import bisq.core.trade.protocol.TradingPeer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public class ProcessDepositsConfirmedMessage extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public ProcessDepositsConfirmedMessage(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// get sender based on the pub key
|
||||
// TODO: trade.getTradingPeer(PubKeyRing)
|
||||
DepositsConfirmedMessage request = (DepositsConfirmedMessage) processModel.getTradeMessage();
|
||||
TradingPeer sender;
|
||||
if (trade.getArbitrator().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getArbitrator();
|
||||
else if (trade.getBuyer().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getBuyer();
|
||||
else if (trade.getSeller().getPubKeyRing().equals(request.getPubKeyRing())) sender = trade.getSeller();
|
||||
else throw new RuntimeException("Pub key ring is not from arbitrator, buyer, or seller");
|
||||
|
||||
// update peer node address
|
||||
sender.setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
|
||||
// decrypt seller payment account payload if key given
|
||||
if (request.getSellerPaymentAccountKey() != null && trade.getTradingPeer().getPaymentAccountPayload() == null) {
|
||||
log.info(trade.getClass().getSimpleName() + " decryping using seller payment account key: " + request.getSellerPaymentAccountKey());
|
||||
trade.decryptPeerPaymentAccountPayload(request.getSellerPaymentAccountKey());
|
||||
}
|
||||
|
||||
// store updated multisig hex for processing on payment sent
|
||||
sender.setUpdatedMultisigHex(request.getUpdatedMultisigHex());
|
||||
|
||||
// persist and complete
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -122,7 +122,7 @@ public class ProcessInitMultisigRequest extends TradeTask {
|
||||
log.info("Importing exchanged multisig hex for trade {}", trade.getId());
|
||||
MoneroMultisigInitResult result = multisigWallet.exchangeMultisigKeys(Arrays.asList(peers[0].getExchangedMultisigHex(), peers[1].getExchangedMultisigHex()), xmrWalletService.getWalletPassword());
|
||||
processModel.setMultisigAddress(result.getAddress());
|
||||
processModel.getProvider().getXmrWalletService().closeMultisigWallet(trade.getId()); // save and close multisig wallet once it's created
|
||||
processModel.getProvider().getXmrWalletService().saveWallet(multisigWallet); // save multisig wallet once it's created
|
||||
trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_COMPLETED);
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ import bisq.core.offer.Offer;
|
||||
import bisq.core.trade.ArbitratorTrade;
|
||||
import bisq.core.trade.MakerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.trade.messages.InitTradeRequest;
|
||||
import bisq.core.trade.protocol.TradingPeer;
|
||||
|
||||
@ -71,7 +71,7 @@ public class ProcessInitTradeRequest extends TradeTask {
|
||||
if (!trade.getTaker().getNodeAddress().equals(request.getTakerNodeAddress())) throw new RuntimeException("Init trade requests from maker and taker do not agree");
|
||||
if (trade.getTaker().getPubKeyRing() != null) throw new RuntimeException("Pub key ring should not be initialized before processing InitTradeRequest");
|
||||
trade.getTaker().setPubKeyRing(request.getPubKeyRing());
|
||||
if (!TradeUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature
|
||||
if (!HavenoUtils.isMakerSignatureValid(request, request.getMakerSignature(), offer.getPubKeyRing())) throw new RuntimeException("Maker signature is invalid for the trade request"); // verify maker signature
|
||||
|
||||
// check trade price
|
||||
try {
|
||||
|
@ -19,26 +19,22 @@ package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.core.account.sign.SignedWitness;
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.ArbitratorTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.util.Validator;
|
||||
|
||||
import common.utils.GenUtils;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
@Slf4j
|
||||
public class BuyerProcessPaymentReceivedMessage extends TradeTask {
|
||||
public BuyerProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
public class ProcessPaymentReceivedMessage extends TradeTask {
|
||||
public ProcessPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@ -50,39 +46,44 @@ public class BuyerProcessPaymentReceivedMessage extends TradeTask {
|
||||
PaymentReceivedMessage message = (PaymentReceivedMessage) processModel.getTradeMessage();
|
||||
Validator.checkTradeId(processModel.getOfferId(), message);
|
||||
checkNotNull(message);
|
||||
checkArgument(message.getPayoutTxHex() != null);
|
||||
checkArgument(message.getUnsignedPayoutTxHex() != null || message.getSignedPayoutTxHex() != null, "No payout tx hex provided");
|
||||
|
||||
// update to the latest peer address of our peer if the message is correct
|
||||
trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
trade.getSeller().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
if (trade.getSeller().getNodeAddress().equals(trade.getBuyer().getNodeAddress())) trade.getBuyer().setNodeAddress(null); // tests sometimes reuse addresses
|
||||
|
||||
// handle if payout tx is not seen on network
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.PAYOUT_PUBLISHED.ordinal()) {
|
||||
// handle if payout tx not published
|
||||
if (!trade.isPayoutPublished()) {
|
||||
|
||||
// publish payout tx if signed. otherwise verify, sign, and publish payout tx
|
||||
boolean previouslySigned = trade.getPayoutTxHex() != null;
|
||||
if (previouslySigned) {
|
||||
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.setStateIfValidTransitionTo(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.verifyPayoutTx(message.getPayoutTxHex(), true, true);
|
||||
trade.setStateIfValidTransitionTo(Trade.State.BUYER_PUBLISHED_PAYOUT_TX);
|
||||
// TODO (woodser): send PayoutTxPublishedMessage to seller
|
||||
// import multisig hex
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
if (message.getUpdatedMultisigHex() != null) {
|
||||
multisigWallet.importMultisigHex(message.getUpdatedMultisigHex());
|
||||
trade.saveWallet();
|
||||
}
|
||||
|
||||
// mark address entries as available
|
||||
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId());
|
||||
// arbitrator waits for buyer to sign and broadcast payout tx if message arrived
|
||||
boolean isSigned = message.getSignedPayoutTxHex() != null;
|
||||
if (trade instanceof ArbitratorTrade && !isSigned && message.isSawArrivedPaymentReceivedMsg()) {
|
||||
log.info("{} waiting for buyer to sign and broadcast payout tx", trade.getClass().getSimpleName());
|
||||
GenUtils.waitFor(30000);
|
||||
multisigWallet.rescanSpent();
|
||||
}
|
||||
|
||||
// verify and publish payout tx
|
||||
if (!trade.isPayoutPublished()) {
|
||||
if (isSigned) {
|
||||
log.info("{} publishing signed payout tx from seller", trade.getClass().getSimpleName());
|
||||
trade.verifyPayoutTx(message.getSignedPayoutTxHex(), false, true);
|
||||
} else {
|
||||
log.info("{} verifying, signing, and publishing seller's payout tx", trade.getClass().getSimpleName());
|
||||
trade.verifyPayoutTx(message.getUnsignedPayoutTxHex(), true, true);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.info("We got the payout tx already set from BuyerSetupPayoutTxListener and do nothing here. trade ID={}", trade.getId());
|
||||
log.info("We got the payout tx already set from the payout listener and do nothing here. trade ID={}", trade.getId());
|
||||
}
|
||||
|
||||
// TODO: remove witness
|
||||
SignedWitness signedWitness = message.getSignedWitness();
|
||||
if (signedWitness != null) {
|
||||
// We received the signedWitness from the seller and publish the data to the network.
|
||||
@ -91,6 +92,8 @@ public class BuyerProcessPaymentReceivedMessage extends TradeTask {
|
||||
processModel.getAccountAgeWitnessService().publishOwnSignedWitness(signedWitness);
|
||||
}
|
||||
|
||||
// complete
|
||||
if (!trade.isArbitrator()) trade.setStateIfValidTransitionTo(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG); // arbitrator trade completes on payout published
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
@ -72,9 +72,6 @@ public class ProcessSignContractResponse extends TradeTask {
|
||||
// send deposit request when all contract signatures received
|
||||
if (processModel.getArbitrator().getContractSignature() != null && processModel.getMaker().getContractSignature() != null && processModel.getTaker().getContractSignature() != null) {
|
||||
|
||||
// start listening for deposit txs
|
||||
trade.listenForDepositTxs();
|
||||
|
||||
// create request for arbitrator to deposit funds to multisig
|
||||
DepositRequest request = new DepositRequest(
|
||||
trade.getOffer().getId(),
|
||||
|
@ -1,109 +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;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.UpdateMultisigRequest;
|
||||
import bisq.core.trade.messages.UpdateMultisigResponse;
|
||||
|
||||
import bisq.network.p2p.SendDirectMessageListener;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import static bisq.core.util.Validator.checkTradeId;
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
@Slf4j
|
||||
public class ProcessUpdateMultisigRequest extends TradeTask {
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public ProcessUpdateMultisigRequest(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
log.debug("current trade state " + trade.getState());
|
||||
UpdateMultisigRequest request = (UpdateMultisigRequest) processModel.getTradeMessage();
|
||||
checkNotNull(request);
|
||||
checkTradeId(processModel.getOfferId(), request);
|
||||
MoneroWallet multisigWallet = processModel.getProvider().getXmrWalletService().getMultisigWallet(trade.getId());
|
||||
|
||||
System.out.println("PROCESS UPDATE MULTISIG REQUEST");
|
||||
System.out.println(request);
|
||||
|
||||
// check if multisig wallet needs updated
|
||||
if (!multisigWallet.isMultisigImportNeeded()) {
|
||||
log.warn("Multisig wallet does not need updated, so request is unexpected");
|
||||
failed(); // TODO (woodser): ignore instead fail
|
||||
return;
|
||||
}
|
||||
|
||||
// get updated multisig hex
|
||||
multisigWallet.sync();
|
||||
String updatedMultisigHex = multisigWallet.exportMultisigHex();
|
||||
|
||||
// import the multisig hex
|
||||
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());
|
||||
|
||||
// respond with updated multisig hex
|
||||
UpdateMultisigResponse response = new UpdateMultisigResponse(
|
||||
processModel.getOffer().getId(),
|
||||
processModel.getMyNodeAddress(),
|
||||
processModel.getPubKeyRing(),
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion(),
|
||||
new Date().getTime(),
|
||||
updatedMultisigHex);
|
||||
|
||||
log.info("Send {} with offerId {} and uid {} to peer {}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid(), trade.getTradingPeer().getNodeAddress());
|
||||
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), response, new SendDirectMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at trading peer: offerId={}; uid={}", response.getClass().getSimpleName(), response.getTradeId(), response.getUid());
|
||||
complete();
|
||||
}
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("Sending {} failed: uid={}; peer={}; error={}", response.getClass().getSimpleName(), response.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage);
|
||||
appendToErrorMessage("Sending response failed: response=" + response + "\nerrorMessage=" + errorMessage);
|
||||
failed();
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,97 +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;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PayoutTxPublishedMessage;
|
||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
public class SellerMaybeSendPayoutTxPublishedMessage extends SendMailboxMessageTask {
|
||||
|
||||
public SellerMaybeSendPayoutTxPublishedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// skip if payout tx not published
|
||||
if (trade.getPhase().ordinal() < Trade.Phase.PAYOUT_PUBLISHED.ordinal()) {
|
||||
complete();
|
||||
return;
|
||||
}
|
||||
|
||||
super.run();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getArbitrator().getNodeAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getArbitrator().getPubKeyRing();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
|
||||
return new PayoutTxPublishedMessage(
|
||||
tradeId,
|
||||
processModel.getMyNodeAddress(),
|
||||
trade.isMaker(),
|
||||
null, // TODO: send witness data?
|
||||
trade.getPayoutTxHex()
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateSent() {
|
||||
log.info("Seller sent PayoutTxPublishedMessage: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateArrived() {
|
||||
log.info("Seller's PayoutTxPublishedMessage arrived: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateStoredInMailbox() {
|
||||
log.info("Seller's PayoutTxPublishedMessage stored in mailbox: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateFault() {
|
||||
log.error("Seller's PayoutTxPublishedMessage failed: tradeId={} at arbitrator {}", trade.getId(), getReceiverNodeAddress());
|
||||
}
|
||||
}
|
@ -17,11 +17,16 @@
|
||||
|
||||
package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.Trade;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import monero.wallet.MoneroWallet;
|
||||
import monero.wallet.model.MoneroTxWallet;
|
||||
|
||||
@Slf4j
|
||||
@ -37,25 +42,35 @@ public class SellerPreparePaymentReceivedMessage extends TradeTask {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// import multisig hex
|
||||
MoneroWallet multisigWallet = trade.getWallet();
|
||||
List<String> updatedMultisigHexes = new ArrayList<String>();
|
||||
if (trade.getBuyer().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getBuyer().getUpdatedMultisigHex());
|
||||
if (trade.getArbitrator().getUpdatedMultisigHex() != null) updatedMultisigHexes.add(trade.getArbitrator().getUpdatedMultisigHex());
|
||||
if (!updatedMultisigHexes.isEmpty()) {
|
||||
multisigWallet.importMultisigHex(updatedMultisigHexes.toArray(new String[0]));
|
||||
trade.saveWallet();
|
||||
}
|
||||
|
||||
// verify, sign, and publish payout tx if given. otherwise create payout tx
|
||||
if (trade.getPayoutTxHex() != null) {
|
||||
log.info("Seller verifying, signing, and publishing payout tx");
|
||||
log.info("Seller verifying, signing, and publishing payout tx for trade {}", trade.getId());
|
||||
trade.verifyPayoutTx(trade.getPayoutTxHex(), true, true);
|
||||
|
||||
// mark address entries as available
|
||||
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId());
|
||||
} else {
|
||||
|
||||
// create unsigned payout tx
|
||||
log.info("Seller creating unsigned payout tx");
|
||||
log.info("Seller creating unsigned payout tx for trade {}", trade.getId());
|
||||
MoneroTxWallet payoutTx = trade.createPayoutTx();
|
||||
System.out.println("created payout tx: " + payoutTx);
|
||||
trade.setPayoutTx(payoutTx);
|
||||
trade.setPayoutTxHex(payoutTx.getTxSet().getMultisigTxHex());
|
||||
|
||||
// start listening for published payout tx
|
||||
trade.listenForPayoutTx();
|
||||
// export multisig hex once
|
||||
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
||||
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
||||
}
|
||||
}
|
||||
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
@ -20,12 +20,10 @@ package bisq.core.trade.protocol.tasks;
|
||||
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.util.Validator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
@Slf4j
|
||||
public class SellerProcessPaymentSentMessage extends TradeTask {
|
||||
@ -47,18 +45,10 @@ public class SellerProcessPaymentSentMessage extends TradeTask {
|
||||
trade.getBuyer().setUpdatedMultisigHex(message.getUpdatedMultisigHex());
|
||||
|
||||
// decrypt buyer's payment account payload
|
||||
trade.decryptPeersPaymentAccountPayload(message.getPaymentAccountKey());
|
||||
|
||||
// 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());
|
||||
}
|
||||
trade.decryptPeerPaymentAccountPayload(message.getPaymentAccountKey());
|
||||
|
||||
// update latest peer address
|
||||
trade.getTradingPeer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
trade.getBuyer().setNodeAddress(processModel.getTempTradingPeerNodeAddress());
|
||||
|
||||
String counterCurrencyTxId = message.getCounterCurrencyTxId();
|
||||
if (counterCurrencyTxId != null && counterCurrencyTxId.length() < 100) {
|
||||
@ -73,7 +63,6 @@ public class SellerProcessPaymentSentMessage extends TradeTask {
|
||||
trade.setState(Trade.State.SELLER_RECEIVED_PAYMENT_SENT_MSG);
|
||||
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
|
@ -21,6 +21,8 @@ import bisq.core.account.sign.SignedWitness;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentReceivedMessage;
|
||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
@ -30,13 +32,17 @@ import static com.google.common.base.Preconditions.checkNotNull;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
public class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||
public abstract class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||
SignedWitness signedWitness = null;
|
||||
|
||||
public SellerSendPaymentReceivedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
protected abstract NodeAddress getReceiverNodeAddress();
|
||||
|
||||
protected abstract PubKeyRing getReceiverPubKeyRing();
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
@ -55,47 +61,52 @@ public class SellerSendPaymentReceivedMessage extends SendMailboxMessageTask {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String id) {
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||
checkNotNull(trade.getPayoutTxHex(), "Payout tx must not be null");
|
||||
|
||||
// TODO: sign witness
|
||||
// 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,
|
||||
tradeId,
|
||||
processModel.getMyNodeAddress(),
|
||||
signedWitness,
|
||||
trade.getPayoutTxHex()
|
||||
trade.isPayoutPublished() ? null : trade.getPayoutTxHex(), // unsigned
|
||||
trade.isPayoutPublished() ? trade.getPayoutTxHex() : null, // signed
|
||||
trade.getSelf().getUpdatedMultisigHex(),
|
||||
trade.getState().ordinal() >= Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG.ordinal() // informs to expect payout
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: using PAYOUT_TX_PUBLISHED_MSG to represent PAYMENT_RECEIVED_MSG after payout, but PAYOUT_TX_PUBLISHED_MSG is specifically for arbitrator. delete *PAYOUT_TX_PUBLISHED* messages and check payout field manually?
|
||||
|
||||
@Override
|
||||
protected void setStateSent() {
|
||||
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
|
||||
log.info("Sent SellerReceivedPaymentMessage: tradeId={} at peer {} SignedWitness {}",
|
||||
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateArrived() {
|
||||
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG);
|
||||
log.info("Seller's PaymentReceivedMessage arrived: tradeId={} at peer {} SignedWitness {}",
|
||||
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateStoredInMailbox() {
|
||||
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG);
|
||||
log.info("Seller's PaymentReceivedMessage stored in mailbox: tradeId={} at peer {} SignedWitness {}",
|
||||
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
|
||||
trade.setStateIfProgress(Trade.State.SELLER_SENT_PAYMENT_RECEIVED_MSG);
|
||||
log.info("{} sent: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateFault() {
|
||||
trade.setState(trade.getState().ordinal() >= Trade.State.SELLER_PUBLISHED_PAYOUT_TX.ordinal() ? Trade.State.SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG : Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG);
|
||||
log.error("SellerReceivedPaymentMessage failed: tradeId={} at peer {} SignedWitness {}",
|
||||
trade.getId(), trade.getTradingPeer().getNodeAddress(), signedWitness);
|
||||
trade.setStateIfProgress(Trade.State.SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG);
|
||||
log.error("{} failed: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateStoredInMailbox() {
|
||||
trade.setStateIfProgress(Trade.State.SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG);
|
||||
log.info("{} stored in mailbox: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setStateArrived() {
|
||||
trade.setStateIfProgress(Trade.State.SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG);
|
||||
log.info("{} arrived: tradeId={} at peer {} SignedWitness {}", getClass().getSimpleName(), trade.getId(), getReceiverNodeAddress(), signedWitness);
|
||||
processModel.getTradeManager().requestPersistence();
|
||||
}
|
||||
}
|
||||
|
@ -17,26 +17,27 @@
|
||||
|
||||
package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
public class SetupDepositTxsListener extends TradeTask {
|
||||
public class SellerSendPaymentReceivedMessageToArbitrator extends SellerSendPaymentReceivedMessage {
|
||||
|
||||
@SuppressWarnings({ "unused" })
|
||||
public SetupDepositTxsListener(TaskRunner taskHandler, Trade trade) {
|
||||
public SellerSendPaymentReceivedMessageToArbitrator(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
trade.listenForDepositTxs();
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
protected NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getArbitrator().getNodeAddress();
|
||||
}
|
||||
|
||||
protected PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getArbitrator().getPubKeyRing();
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Slf4j
|
||||
public class SellerSendPaymentReceivedMessageToBuyer extends SellerSendPaymentReceivedMessage {
|
||||
|
||||
public SellerSendPaymentReceivedMessageToBuyer(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
protected NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getBuyer().getNodeAddress();
|
||||
}
|
||||
|
||||
protected PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getBuyer().getPubKeyRing();
|
||||
}
|
||||
|
||||
// continue execution on fault so payment received message is sent to arbitrator
|
||||
@Override
|
||||
protected void onFault(String errorMessage, TradeMessage message) {
|
||||
setStateFault();
|
||||
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
|
||||
complete();
|
||||
}
|
||||
}
|
@ -18,23 +18,25 @@
|
||||
package bisq.core.trade.protocol.tasks;
|
||||
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.BuyerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.PaymentAccountKeyResponse;
|
||||
import bisq.core.trade.messages.DepositsConfirmedMessage;
|
||||
import bisq.core.trade.messages.TradeMailboxMessage;
|
||||
import bisq.common.app.Version;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
/**
|
||||
* Allow sender's payment account info to be decrypted when trade state is confirmed.
|
||||
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask {
|
||||
private PaymentAccountKeyResponse message;
|
||||
public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTask {
|
||||
private DepositsConfirmedMessage message;
|
||||
|
||||
public SellerSendPaymentAccountPayloadKey(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
public SendDepositsConfirmedMessage(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@ -48,15 +50,21 @@ public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected abstract NodeAddress getReceiverNodeAddress();
|
||||
|
||||
@Override
|
||||
protected abstract PubKeyRing getReceiverPubKeyRing();
|
||||
|
||||
@Override
|
||||
protected TradeMailboxMessage getTradeMailboxMessage(String tradeId) {
|
||||
if (message == null) {
|
||||
|
||||
// get updated multisig hex
|
||||
// export multisig hex once
|
||||
if (trade.getSelf().getUpdatedMultisigHex() == null) {
|
||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
|
||||
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex()); // only export multisig hex once
|
||||
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -64,13 +72,12 @@ public class SellerSendPaymentAccountPayloadKey extends SendMailboxMessageTask {
|
||||
// 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 PaymentAccountKeyResponse(
|
||||
message = new DepositsConfirmedMessage(
|
||||
trade.getOffer().getId(),
|
||||
processModel.getMyNodeAddress(),
|
||||
processModel.getPubKeyRing(),
|
||||
deterministicId,
|
||||
Version.getP2PMessageVersion(),
|
||||
trade.getSelf().getPaymentAccountKey(),
|
||||
getReceiverNodeAddress().equals(trade.getBuyer().getNodeAddress()) ? trade.getSeller().getPaymentAccountKey() : null, // buyer receives seller's payment account decryption key
|
||||
trade.getSelf().getUpdatedMultisigHex());
|
||||
}
|
||||
return message;
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SendDepositsConfirmedMessageToArbitrator extends SendDepositsConfirmedMessage {
|
||||
|
||||
public SendDepositsConfirmedMessageToArbitrator(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getArbitrator().getNodeAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getArbitrator().getPubKeyRing();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SendDepositsConfirmedMessageToBuyer extends SendDepositsConfirmedMessage {
|
||||
|
||||
public SendDepositsConfirmedMessageToBuyer(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getBuyer().getNodeAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getBuyer().getPubKeyRing();
|
||||
}
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.common.crypto.PubKeyRing;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Send message on first confirmation to decrypt peer payment account and update multisig hex.
|
||||
*/
|
||||
@Slf4j
|
||||
public class SendDepositsConfirmedMessageToSeller extends SendDepositsConfirmedMessage {
|
||||
|
||||
public SendDepositsConfirmedMessageToSeller(TaskRunner<Trade> taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeAddress getReceiverNodeAddress() {
|
||||
return trade.getSeller().getNodeAddress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PubKeyRing getReceiverPubKeyRing() {
|
||||
return trade.getSeller().getPubKeyRing();
|
||||
}
|
||||
}
|
@ -1,66 +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;
|
||||
|
||||
import bisq.common.UserThread;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
import bisq.core.trade.Trade;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.fxmisc.easybind.EasyBind;
|
||||
import org.fxmisc.easybind.Subscription;
|
||||
|
||||
@Slf4j
|
||||
public class SetupPayoutTxListener extends TradeTask {
|
||||
|
||||
private Subscription tradeStateSubscription;
|
||||
|
||||
@SuppressWarnings({ "unused" })
|
||||
public SetupPayoutTxListener(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// skip if payout already published
|
||||
if (!trade.isPayoutPublished()) {
|
||||
|
||||
// listen for payout tx
|
||||
trade.listenForPayoutTx();
|
||||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), newValue -> {
|
||||
if (trade.isPayoutPublished()) {
|
||||
|
||||
// cleanup on trade completion
|
||||
processModel.getXmrWalletService().resetAddressEntriesForPendingTrade(trade.getId());
|
||||
UserThread.execute(this::unSubscribe); // unsubscribe
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
complete();
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
|
||||
private void unSubscribe() {
|
||||
if (tradeStateSubscription != null) tradeStateSubscription.unsubscribe();
|
||||
}
|
||||
}
|
@ -1,116 +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;
|
||||
|
||||
import bisq.core.btc.wallet.XmrWalletService;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.messages.TradeMessage;
|
||||
import bisq.core.trade.messages.UpdateMultisigRequest;
|
||||
import bisq.core.trade.messages.UpdateMultisigResponse;
|
||||
import bisq.core.trade.protocol.TradeListener;
|
||||
|
||||
import bisq.network.p2p.NodeAddress;
|
||||
import bisq.network.p2p.SendDirectMessageListener;
|
||||
|
||||
import bisq.common.app.Version;
|
||||
import bisq.common.taskrunner.TaskRunner;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
|
||||
|
||||
import monero.wallet.MoneroWallet;
|
||||
|
||||
@Slf4j
|
||||
public class UpdateMultisigWithTradingPeer extends TradeTask {
|
||||
|
||||
private TradeListener updateMultisigResponseListener;
|
||||
|
||||
@SuppressWarnings({"unused"})
|
||||
public UpdateMultisigWithTradingPeer(TaskRunner taskHandler, Trade trade) {
|
||||
super(taskHandler, trade);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void run() {
|
||||
try {
|
||||
runInterceptHook();
|
||||
|
||||
// fetch relevant trade info
|
||||
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
|
||||
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId()); // closed in BuyerPreparesPaymentStartedMessage
|
||||
|
||||
// skip if multisig wallet does not need updated
|
||||
if (!multisigWallet.isMultisigImportNeeded()) {
|
||||
log.warn("Multisig wallet does not need updated, this should not happen");
|
||||
failed();
|
||||
return;
|
||||
}
|
||||
|
||||
// register listener to receive updated multisig response
|
||||
updateMultisigResponseListener = new TradeListener() {
|
||||
@Override
|
||||
public void onVerifiedTradeMessage(TradeMessage message, NodeAddress sender) {
|
||||
if (!(message instanceof UpdateMultisigResponse)) return;
|
||||
UpdateMultisigResponse response = (UpdateMultisigResponse) message;
|
||||
multisigWallet.sync();
|
||||
multisigWallet.importMultisigHex(response.getUpdatedMultisigHex());
|
||||
trade.removeListener(updateMultisigResponseListener);
|
||||
complete();
|
||||
}
|
||||
};
|
||||
trade.addListener(updateMultisigResponseListener);
|
||||
|
||||
// get updated multisig hex
|
||||
multisigWallet.sync();
|
||||
String updatedMultisigHex = multisigWallet.exportMultisigHex();
|
||||
|
||||
// message trading peer with updated multisig hex
|
||||
UpdateMultisigRequest message = new UpdateMultisigRequest(
|
||||
processModel.getOffer().getId(),
|
||||
processModel.getMyNodeAddress(),
|
||||
processModel.getPubKeyRing(),
|
||||
UUID.randomUUID().toString(),
|
||||
Version.getP2PMessageVersion(),
|
||||
new Date().getTime(),
|
||||
updatedMultisigHex);
|
||||
|
||||
System.out.println("Sending message: " + message);
|
||||
|
||||
// TODO (woodser): trade.getTradingPeer().getNodeAddress() and/or trade.getTradingPeer().getPubKeyRing() are null on restart of application, so cannot send payment to complete trade
|
||||
log.info("Send {} with offerId {} and uid {} to peer {}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid(), trade.getTradingPeer().getNodeAddress());
|
||||
processModel.getP2PService().sendEncryptedDirectMessage(trade.getTradingPeer().getNodeAddress(), trade.getTradingPeer().getPubKeyRing(), message, new SendDirectMessageListener() {
|
||||
@Override
|
||||
public void onArrived() {
|
||||
log.info("{} arrived at trading peer: offerId={}; uid={}", message.getClass().getSimpleName(), message.getTradeId(), message.getUid());
|
||||
}
|
||||
@Override
|
||||
public void onFault(String errorMessage) {
|
||||
log.error("Sending {} failed: uid={}; peer={}; error={}", message.getClass().getSimpleName(), message.getUid(), trade.getArbitrator().getNodeAddress(), errorMessage);
|
||||
appendToErrorMessage("Sending message failed: message=" + message + "\nerrorMessage=" + errorMessage);
|
||||
failed();
|
||||
}
|
||||
});
|
||||
} catch (Throwable t) {
|
||||
failed(t);
|
||||
}
|
||||
}
|
||||
}
|
@ -31,8 +31,8 @@ import bisq.proto.grpc.GetTradeReply;
|
||||
import bisq.proto.grpc.GetTradeRequest;
|
||||
import bisq.proto.grpc.GetTradesReply;
|
||||
import bisq.proto.grpc.GetTradesRequest;
|
||||
import bisq.proto.grpc.KeepFundsReply;
|
||||
import bisq.proto.grpc.KeepFundsRequest;
|
||||
import bisq.proto.grpc.CompleteTradeReply;
|
||||
import bisq.proto.grpc.CompleteTradeRequest;
|
||||
import bisq.proto.grpc.SendChatMessageReply;
|
||||
import bisq.proto.grpc.SendChatMessageRequest;
|
||||
import bisq.proto.grpc.TakeOfferReply;
|
||||
@ -176,13 +176,13 @@ class GrpcTradesService extends TradesImplBase {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: rename KeepFundsRequest to CloseTradeRequest
|
||||
// TODO: rename CompleteTradeRequest to CloseTradeRequest
|
||||
@Override
|
||||
public void keepFunds(KeepFundsRequest req,
|
||||
StreamObserver<KeepFundsReply> responseObserver) {
|
||||
public void completeTrade(CompleteTradeRequest req,
|
||||
StreamObserver<CompleteTradeReply> responseObserver) {
|
||||
try {
|
||||
coreApi.closeTrade(req.getTradeId());
|
||||
var reply = KeepFundsReply.newBuilder().build();
|
||||
var reply = CompleteTradeReply.newBuilder().build();
|
||||
responseObserver.onNext(reply);
|
||||
responseObserver.onCompleted();
|
||||
} catch (Throwable cause) {
|
||||
@ -244,12 +244,12 @@ class GrpcTradesService extends TradesImplBase {
|
||||
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
||||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||
new HashMap<>() {{
|
||||
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getGetTradeMethod().getFullMethodName(), new GrpcCallRateMeter(20, SECONDS));
|
||||
put(getGetTradesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getTakeOfferMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getConfirmPaymentStartedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getConfirmPaymentReceivedMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getKeepFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
put(getCompleteTradeMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getWithdrawFundsMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
put(getGetChatMessagesMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
put(getSendChatMessageMethod().getFullMethodName(), new GrpcCallRateMeter(10, SECONDS));
|
||||
|
@ -415,7 +415,7 @@ class GrpcWalletsService extends WalletsImplBase {
|
||||
return getCustomRateMeteringInterceptor(coreApi.getConfig().appDataDir, this.getClass())
|
||||
.or(() -> Optional.of(CallRateMeteringInterceptor.valueOf(
|
||||
new HashMap<>() {{
|
||||
put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(50, SECONDS));
|
||||
put(getGetBalancesMethod().getFullMethodName(), new GrpcCallRateMeter(100, SECONDS)); // TODO: why do tests make so many calls to get balances?
|
||||
put(getGetAddressBalanceMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||
put(getGetFundingAddressesMethod().getFullMethodName(), new GrpcCallRateMeter(1, SECONDS));
|
||||
put(getSendBtcMethod().getFullMethodName(), new GrpcCallRateMeter(1, MINUTES));
|
||||
|
@ -28,7 +28,7 @@ import bisq.core.offer.placeoffer.tasks.MakerReserveOfferFunds;
|
||||
import bisq.core.offer.placeoffer.tasks.ValidateOffer;
|
||||
import bisq.core.trade.protocol.tasks.ApplyFilter;
|
||||
import bisq.core.trade.protocol.tasks.BuyerPreparePaymentSentMessage;
|
||||
import bisq.core.trade.protocol.tasks.BuyerProcessPaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.ProcessPaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.BuyerSendPaymentSentMessage;
|
||||
import bisq.core.trade.protocol.tasks.MakerSetLockTime;
|
||||
import bisq.core.trade.protocol.tasks.RemoveOffer;
|
||||
@ -36,8 +36,7 @@ import bisq.core.trade.protocol.tasks.SellerPreparePaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.SellerProcessPaymentSentMessage;
|
||||
import bisq.core.trade.protocol.tasks.SellerPublishDepositTx;
|
||||
import bisq.core.trade.protocol.tasks.SellerPublishTradeStatistics;
|
||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessage;
|
||||
import bisq.core.trade.protocol.tasks.SetupPayoutTxListener;
|
||||
import bisq.core.trade.protocol.tasks.SellerSendPaymentReceivedMessageToBuyer;
|
||||
import bisq.core.trade.protocol.tasks.TakerVerifyMakerFeePayment;
|
||||
import bisq.core.trade.protocol.tasks.VerifyPeersAccountAgeWitness;
|
||||
import bisq.common.taskrunner.Task;
|
||||
@ -109,7 +108,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||
TakerVerifyMakerFeePayment.class,
|
||||
SellerPreparePaymentReceivedMessage.class,
|
||||
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
|
||||
SellerSendPaymentReceivedMessage.class
|
||||
SellerSendPaymentReceivedMessageToBuyer.class
|
||||
|
||||
)
|
||||
));
|
||||
@ -123,10 +122,9 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||
|
||||
ApplyFilter.class,
|
||||
BuyerPreparePaymentSentMessage.class,
|
||||
SetupPayoutTxListener.class,
|
||||
BuyerSendPaymentSentMessage.class,
|
||||
|
||||
BuyerProcessPaymentReceivedMessage.class
|
||||
ProcessPaymentReceivedMessage.class
|
||||
)
|
||||
));
|
||||
|
||||
@ -142,10 +140,9 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||
ApplyFilter.class,
|
||||
TakerVerifyMakerFeePayment.class,
|
||||
BuyerPreparePaymentSentMessage.class,
|
||||
SetupPayoutTxListener.class,
|
||||
BuyerSendPaymentSentMessage.class,
|
||||
|
||||
BuyerProcessPaymentReceivedMessage.class)
|
||||
ProcessPaymentReceivedMessage.class)
|
||||
));
|
||||
addGroup("SellerAsMakerProtocol",
|
||||
FXCollections.observableArrayList(Arrays.asList(
|
||||
@ -166,7 +163,7 @@ public class DebugView extends InitializableView<GridPane, Void> {
|
||||
ApplyFilter.class,
|
||||
SellerPreparePaymentReceivedMessage.class,
|
||||
//SellerBroadcastPayoutTx.class, // TODO (woodser): removed from main pipeline; debug view?
|
||||
SellerSendPaymentReceivedMessage.class
|
||||
SellerSendPaymentReceivedMessageToBuyer.class
|
||||
)
|
||||
));
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferDirection;
|
||||
import bisq.core.offer.OpenOfferManager;
|
||||
import bisq.core.provider.price.PriceFeedService;
|
||||
import bisq.core.trade.TradeUtils;
|
||||
import bisq.core.trade.HavenoUtils;
|
||||
import bisq.core.user.Preferences;
|
||||
import bisq.core.util.VolumeUtil;
|
||||
|
||||
|
@ -424,8 +424,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
|
||||
case PAYMENT_RECEIVED:
|
||||
appendMsg = Res.get("takeOffer.error.depositPublished");
|
||||
break;
|
||||
case PAYOUT_PUBLISHED:
|
||||
case WITHDRAWN:
|
||||
case COMPLETED:
|
||||
appendMsg = Res.get("takeOffer.error.payoutPublished");
|
||||
break;
|
||||
default:
|
||||
@ -444,7 +443,7 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
|
||||
}
|
||||
|
||||
private void applyTradeState() {
|
||||
if (trade.isTakerFeePublished()) {
|
||||
if (trade.isDepositRequested()) {
|
||||
if (takeOfferResultHandler != null)
|
||||
takeOfferResultHandler.run();
|
||||
|
||||
|
@ -183,12 +183,11 @@ public class NotificationCenter {
|
||||
|
||||
private void onTradePhaseChanged(Trade trade, Trade.Phase phase) {
|
||||
String message = null;
|
||||
if (trade.isPayoutPublished() && !trade.isWithdrawn()) {
|
||||
if (trade.isPayoutPublished() && !trade.isCompleted()) {
|
||||
message = Res.get("notification.trade.completed");
|
||||
} else {
|
||||
if (trade instanceof MakerTrade &&
|
||||
phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal() ||
|
||||
phase.ordinal() == Trade.Phase.DEPOSITS_CONFIRMED.ordinal()) {
|
||||
phase.ordinal() == Trade.Phase.DEPOSITS_PUBLISHED.ordinal()) {
|
||||
final String role = trade instanceof BuyerTrade ? Res.get("shared.seller") : Res.get("shared.buyer");
|
||||
message = Res.get("notification.trade.accepted", role);
|
||||
}
|
||||
|
@ -590,7 +590,7 @@ public class DisputeSummaryWindow extends Overlay<DisputeSummaryWindow> {
|
||||
Button cancelButton = tuple.second;
|
||||
|
||||
closeTicketButton.setOnAction(e -> {
|
||||
disputesService.resolveDisputePayout(dispute, disputeResult, contract);
|
||||
disputesService.applyDisputePayout(dispute, disputeResult, contract);
|
||||
doClose(closeTicketButton);
|
||||
|
||||
// if (dispute.getDepositTxSerialized() == null) {
|
||||
|
@ -200,7 +200,7 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
((BuyerProtocol) tradeManager.getTradeProtocol(trade)).onPaymentStarted(resultHandler, errorMessageHandler);
|
||||
}
|
||||
|
||||
public void onFiatPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
public void onPaymentReceived(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
|
||||
Trade trade = getTrade();
|
||||
checkNotNull(trade, "trade must not be null");
|
||||
checkArgument(trade instanceof SellerTrade, "Trade must be instance of SellerTrade");
|
||||
@ -466,7 +466,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
|
||||
String payoutTxHashAsString = null;
|
||||
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
|
||||
String updatedMultisigHex = multisigWallet.exportMultisigHex();
|
||||
xmrWalletService.closeMultisigWallet(trade.getId()); // close multisig wallet
|
||||
if (trade.getPayoutTxId() != null) {
|
||||
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
|
||||
// payoutTxHashAsString = payoutTx.getHashAsString();
|
||||
|
@ -30,8 +30,11 @@ import bisq.core.offer.Offer;
|
||||
import bisq.core.offer.OfferUtil;
|
||||
import bisq.core.provider.fee.FeeService;
|
||||
import bisq.core.provider.mempool.MempoolService;
|
||||
import bisq.core.trade.ArbitratorTrade;
|
||||
import bisq.core.trade.BuyerTrade;
|
||||
import bisq.core.trade.ClosedTradableManager;
|
||||
import bisq.core.trade.Contract;
|
||||
import bisq.core.trade.SellerTrade;
|
||||
import bisq.core.trade.Trade;
|
||||
import bisq.core.trade.TradeUtil;
|
||||
import bisq.core.user.User;
|
||||
@ -115,6 +118,7 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
@Getter
|
||||
private final ObjectProperty<MessageState> messageStateProperty = new SimpleObjectProperty<>(MessageState.UNDEFINED);
|
||||
private Subscription tradeStateSubscription;
|
||||
private Subscription payoutStateSubscription;
|
||||
private Subscription messageStateSubscription;
|
||||
@Getter
|
||||
protected final IntegerProperty mempoolStatus = new SimpleIntegerProperty();
|
||||
@ -160,6 +164,11 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
tradeStateSubscription = null;
|
||||
}
|
||||
|
||||
if (payoutStateSubscription != null) {
|
||||
payoutStateSubscription.unsubscribe();
|
||||
payoutStateSubscription = null;
|
||||
}
|
||||
|
||||
if (messageStateSubscription != null) {
|
||||
messageStateSubscription.unsubscribe();
|
||||
messageStateSubscription = null;
|
||||
@ -174,6 +183,12 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
buyerState.set(BuyerState.UNDEFINED);
|
||||
}
|
||||
|
||||
if (payoutStateSubscription != null) {
|
||||
payoutStateSubscription.unsubscribe();
|
||||
sellerState.set(SellerState.UNDEFINED);
|
||||
buyerState.set(BuyerState.UNDEFINED);
|
||||
}
|
||||
|
||||
if (messageStateSubscription != null) {
|
||||
messageStateSubscription.unsubscribe();
|
||||
messageStateProperty.set(MessageState.UNDEFINED);
|
||||
@ -184,6 +199,9 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
tradeStateSubscription = EasyBind.subscribe(trade.stateProperty(), state -> {
|
||||
UserThread.execute(() -> onTradeStateChanged(state));
|
||||
});
|
||||
payoutStateSubscription = EasyBind.subscribe(trade.payoutStateProperty(), state -> {
|
||||
UserThread.execute(() -> onPayoutStateChanged(state));
|
||||
});
|
||||
messageStateSubscription = EasyBind.subscribe(trade.getProcessModel().getPaymentStartedMessageStateProperty(), this::onMessageStateChanged);
|
||||
}
|
||||
}
|
||||
@ -399,6 +417,13 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
tradeState,
|
||||
trade != null ? trade.getShortId() : "trade is null");
|
||||
|
||||
// arbitrator trade view only shows tx status
|
||||
if (trade instanceof ArbitratorTrade) {
|
||||
buyerState.set(BuyerState.STEP1);
|
||||
sellerState.set(SellerState.STEP1);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (tradeState) {
|
||||
// preparation
|
||||
case PREPARATION:
|
||||
@ -414,9 +439,8 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
|
||||
// deposit requested
|
||||
case SENT_PUBLISH_DEPOSIT_TX_REQUEST:
|
||||
case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST:
|
||||
case STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST:
|
||||
case SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST:
|
||||
case SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST:
|
||||
|
||||
// deposit published
|
||||
case ARBITRATOR_PUBLISHED_DEPOSIT_TXS:
|
||||
@ -456,29 +480,16 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
// seller step 4
|
||||
case SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT: // UI action
|
||||
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_PUBLISHED_PAYOUT_TX: // payout tx broadcasted
|
||||
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG sent
|
||||
sellerState.set(SellerState.STEP3);
|
||||
if (trade instanceof BuyerTrade) buyerState.set(BuyerState.STEP4);
|
||||
else if (trade instanceof SellerTrade) sellerState.set(SellerState.STEP3);
|
||||
break;
|
||||
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG arrived
|
||||
case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG mailbox
|
||||
case SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG: // PAYOUT_TX_PUBLISHED_MSG failed - payout tx is published, peer will see it in network so we ignore failure and complete
|
||||
sellerState.set(SellerState.STEP4);
|
||||
break;
|
||||
|
||||
// buyer step 4
|
||||
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 PAYOUT_TX_SEEN_IN_NETWORK:
|
||||
// Alternatively the buyer could fully sign and publish the payout tx
|
||||
case BUYER_PUBLISHED_PAYOUT_TX:
|
||||
buyerState.set(BuyerState.STEP4);
|
||||
break;
|
||||
|
||||
case WITHDRAW_COMPLETED:
|
||||
case TRADE_COMPLETED:
|
||||
sellerState.set(UNDEFINED);
|
||||
buyerState.set(BuyerState.UNDEFINED);
|
||||
break;
|
||||
@ -491,4 +502,21 @@ public class PendingTradesViewModel extends ActivatableWithDataModel<PendingTrad
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void onPayoutStateChanged(Trade.PayoutState payoutState) {
|
||||
log.info("UI payoutState={}, id={}",
|
||||
payoutState,
|
||||
trade != null ? trade.getShortId() : "trade is null");
|
||||
|
||||
if (trade instanceof ArbitratorTrade) return;
|
||||
|
||||
switch (payoutState) {
|
||||
case PUBLISHED:
|
||||
sellerState.set(SellerState.STEP4);
|
||||
buyerState.set(BuyerState.STEP4);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -123,8 +123,6 @@ public class SellerStep3View extends TradeStepView {
|
||||
busyAnimation.play();
|
||||
statusLabel.setText(Res.get("Confirming payment received. This can take up to a few minutes. Please wait..."));
|
||||
break;
|
||||
case SELLER_PUBLISHED_PAYOUT_TX:
|
||||
case SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG:
|
||||
case SELLER_SENT_PAYMENT_RECEIVED_MSG:
|
||||
busyAnimation.play();
|
||||
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
||||
@ -135,16 +133,14 @@ public class SellerStep3View extends TradeStepView {
|
||||
}, 10);
|
||||
break;
|
||||
case SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG:
|
||||
busyAnimation.stop();
|
||||
statusLabel.setText(Res.get("shared.messageArrived"));
|
||||
break;
|
||||
case SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG:
|
||||
case SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG:
|
||||
busyAnimation.stop();
|
||||
statusLabel.setText(Res.get("shared.messageStoredInMailbox"));
|
||||
break;
|
||||
case SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG:
|
||||
case SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG:
|
||||
// We get a popup and the trade closed, so we dont need to show anything here
|
||||
busyAnimation.stop();
|
||||
statusLabel.setText("");
|
||||
@ -464,7 +460,7 @@ public class SellerStep3View extends TradeStepView {
|
||||
busyAnimation.play();
|
||||
statusLabel.setText(Res.get("shared.sendingConfirmation"));
|
||||
|
||||
model.dataModel.onFiatPaymentReceived(() -> {
|
||||
model.dataModel.onPaymentReceived(() -> {
|
||||
}, errorMessage -> {
|
||||
busyAnimation.stop();
|
||||
new Popup().warning(Res.get("popup.warning.sendMsgFailed")).show();
|
||||
|
@ -731,7 +731,7 @@ service Trades {
|
||||
}
|
||||
rpc ConfirmPaymentReceived (ConfirmPaymentReceivedRequest) returns (ConfirmPaymentReceivedReply) {
|
||||
}
|
||||
rpc KeepFunds (KeepFundsRequest) returns (KeepFundsReply) {
|
||||
rpc CompleteTrade (CompleteTradeRequest) returns (CompleteTradeReply) {
|
||||
}
|
||||
rpc WithdrawFunds (WithdrawFundsRequest) returns (WithdrawFundsReply) {
|
||||
}
|
||||
@ -787,11 +787,11 @@ message GetTradesReply {
|
||||
repeated TradeInfo trades = 1;
|
||||
}
|
||||
|
||||
message KeepFundsRequest {
|
||||
message CompleteTradeRequest {
|
||||
string trade_id = 1;
|
||||
}
|
||||
|
||||
message KeepFundsReply {
|
||||
message CompleteTradeReply {
|
||||
}
|
||||
|
||||
message WithdrawFundsRequest {
|
||||
@ -837,15 +837,16 @@ message TradeInfo {
|
||||
string state = 16;
|
||||
string phase = 17;
|
||||
string period_state = 18;
|
||||
bool is_deposit_published = 19;
|
||||
bool is_deposit_unlocked = 20;
|
||||
bool is_payment_sent = 21;
|
||||
bool is_payment_received = 22;
|
||||
bool is_payout_published = 23;
|
||||
bool is_completed = 24;
|
||||
string contract_as_json = 25;
|
||||
ContractInfo contract = 26;
|
||||
string trade_volume = 27;
|
||||
string payout_state = 19;
|
||||
bool is_deposit_published = 20;
|
||||
bool is_deposit_unlocked = 21;
|
||||
bool is_payment_sent = 22;
|
||||
bool is_payment_received = 23;
|
||||
bool is_payout_published = 24;
|
||||
bool is_completed = 25;
|
||||
string contract_as_json = 26;
|
||||
ContractInfo contract = 27;
|
||||
string trade_volume = 28;
|
||||
|
||||
string maker_deposit_tx_id = 100;
|
||||
string taker_deposit_tx_id = 101;
|
||||
|
@ -74,17 +74,11 @@ message NetworkEnvelope {
|
||||
SignContractResponse sign_contract_response = 1006;
|
||||
DepositRequest deposit_request = 1007;
|
||||
DepositResponse deposit_response = 1008;
|
||||
PaymentAccountKeyRequest payment_account_key_request = 1009;
|
||||
PaymentAccountKeyResponse payment_account_key_response = 1010;
|
||||
PaymentSentMessage payment_sent_message = 1011;
|
||||
PaymentReceivedMessage payment_received_message = 1012;
|
||||
PayoutTxPublishedMessage payout_tx_published_message = 1013;
|
||||
ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1016;
|
||||
ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1017;
|
||||
|
||||
// TODO: delete these
|
||||
UpdateMultisigRequest update_multisig_request = 1018;
|
||||
UpdateMultisigResponse update_multisig_response = 1019;
|
||||
DepositsConfirmedMessage deposits_confirmed_message = 1009;
|
||||
PaymentSentMessage payment_sent_message = 1010;
|
||||
PaymentReceivedMessage payment_received_message = 1011;
|
||||
ArbitratorPayoutTxRequest arbitrator_payout_tx_request = 1012;
|
||||
ArbitratorPayoutTxResponse arbitrator_payout_tx_response = 1013;
|
||||
}
|
||||
}
|
||||
|
||||
@ -355,37 +349,12 @@ message DepositResponse {
|
||||
int64 current_date = 5;
|
||||
}
|
||||
|
||||
message PaymentAccountKeyRequest {
|
||||
message DepositsConfirmedMessage {
|
||||
string trade_id = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
PubKeyRing pub_key_ring = 3;
|
||||
string uid = 4;
|
||||
}
|
||||
|
||||
message PaymentAccountKeyResponse {
|
||||
string trade_id = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
PubKeyRing pub_key_ring = 3;
|
||||
string uid = 4;
|
||||
bytes payment_account_key = 5;
|
||||
string updated_multisig_hex = 6;
|
||||
}
|
||||
|
||||
message UpdateMultisigRequest {
|
||||
string trade_id = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
PubKeyRing pub_key_ring = 3;
|
||||
string uid = 4;
|
||||
int64 current_date = 5;
|
||||
string updated_multisig_hex = 6;
|
||||
}
|
||||
|
||||
message UpdateMultisigResponse {
|
||||
string trade_id = 1;
|
||||
NodeAddress sender_node_address = 2;
|
||||
PubKeyRing pub_key_ring = 3;
|
||||
string uid = 4;
|
||||
int64 current_date = 5;
|
||||
bytes seller_payment_account_key = 5;
|
||||
string updated_multisig_hex = 6;
|
||||
}
|
||||
|
||||
@ -454,16 +423,10 @@ message PaymentReceivedMessage {
|
||||
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;
|
||||
bool is_maker = 3;
|
||||
string uid = 4;
|
||||
SignedWitness signed_witness = 5;
|
||||
string unsigned_payout_tx_hex = 5;
|
||||
string signed_payout_tx_hex = 6;
|
||||
string updated_multisig_hex = 7;
|
||||
bool saw_arrived_payment_received_msg = 8;
|
||||
}
|
||||
|
||||
message ArbitratorPayoutTxRequest {
|
||||
@ -1644,33 +1607,24 @@ message Trade {
|
||||
CONTRACT_SIGNATURE_REQUESTED = 6;
|
||||
CONTRACT_SIGNED = 7;
|
||||
SENT_PUBLISH_DEPOSIT_TX_REQUEST = 8;
|
||||
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 9;
|
||||
STORED_IN_MAILBOX_PUBLISH_DEPOSIT_TX_REQUEST = 10;
|
||||
SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 11;
|
||||
ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 12;
|
||||
DEPOSIT_TXS_SEEN_IN_NETWORK = 13;
|
||||
DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 14;
|
||||
DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 15;
|
||||
BUYER_CONFIRMED_IN_UI_PAYMENT_SENT = 16;
|
||||
BUYER_SENT_PAYMENT_SENT_MSG = 17;
|
||||
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 18;
|
||||
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 19;
|
||||
BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 20;
|
||||
SELLER_RECEIVED_PAYMENT_SENT_MSG = 21;
|
||||
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 22;
|
||||
SELLER_SENT_PAYMENT_RECEIVED_MSG = 23;
|
||||
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 24;
|
||||
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 25;
|
||||
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 26;
|
||||
SELLER_PUBLISHED_PAYOUT_TX = 27;
|
||||
SELLER_SENT_PAYOUT_TX_PUBLISHED_MSG = 28;
|
||||
SELLER_SAW_ARRIVED_PAYOUT_TX_PUBLISHED_MSG = 29;
|
||||
SELLER_STORED_IN_MAILBOX_PAYOUT_TX_PUBLISHED_MSG = 30;
|
||||
SELLER_SEND_FAILED_PAYOUT_TX_PUBLISHED_MSG = 31;
|
||||
BUYER_RECEIVED_PAYOUT_TX_PUBLISHED_MSG = 32;
|
||||
BUYER_PUBLISHED_PAYOUT_TX = 33;
|
||||
PAYOUT_TX_SEEN_IN_NETWORK = 34;
|
||||
WITHDRAW_COMPLETED = 35;
|
||||
SEND_FAILED_PUBLISH_DEPOSIT_TX_REQUEST = 9;
|
||||
SAW_ARRIVED_PUBLISH_DEPOSIT_TX_REQUEST = 10;
|
||||
ARBITRATOR_PUBLISHED_DEPOSIT_TXS = 11;
|
||||
DEPOSIT_TXS_SEEN_IN_NETWORK = 12;
|
||||
DEPOSIT_TXS_CONFIRMED_IN_BLOCKCHAIN = 13;
|
||||
DEPOSIT_TXS_UNLOCKED_IN_BLOCKCHAIN = 14;
|
||||
BUYER_CONFIRMED_IN_UI_PAYMENT_SENT = 15;
|
||||
BUYER_SENT_PAYMENT_SENT_MSG = 16;
|
||||
BUYER_SEND_FAILED_PAYMENT_SENT_MSG = 17;
|
||||
BUYER_STORED_IN_MAILBOX_PAYMENT_SENT_MSG = 18;
|
||||
BUYER_SAW_ARRIVED_PAYMENT_SENT_MSG = 19;
|
||||
SELLER_RECEIVED_PAYMENT_SENT_MSG = 20;
|
||||
SELLER_CONFIRMED_IN_UI_PAYMENT_RECEIPT = 21;
|
||||
SELLER_SENT_PAYMENT_RECEIVED_MSG = 22;
|
||||
SELLER_SEND_FAILED_PAYMENT_RECEIVED_MSG = 23;
|
||||
SELLER_STORED_IN_MAILBOX_PAYMENT_RECEIVED_MSG = 24;
|
||||
SELLER_SAW_ARRIVED_PAYMENT_RECEIVED_MSG = 25;
|
||||
TRADE_COMPLETED = 26;
|
||||
}
|
||||
|
||||
enum Phase {
|
||||
@ -1682,8 +1636,14 @@ message Trade {
|
||||
DEPOSITS_UNLOCKED = 5;
|
||||
PAYMENT_SENT = 6;
|
||||
PAYMENT_RECEIVED = 7;
|
||||
PAYOUT_PUBLISHED = 8;
|
||||
WITHDRAWN = 9;
|
||||
COMPLETED = 8;
|
||||
}
|
||||
|
||||
enum PayoutState {
|
||||
UNPUBLISHED = 0;
|
||||
PUBLISHED = 1;
|
||||
CONFIRMED = 2;
|
||||
UNLOCKED = 3;
|
||||
}
|
||||
|
||||
enum DisputeState {
|
||||
@ -1718,23 +1678,24 @@ message Trade {
|
||||
int64 take_offer_date = 9;
|
||||
int64 price = 10;
|
||||
State state = 11;
|
||||
DisputeState dispute_state = 12;
|
||||
TradePeriodState period_state = 13;
|
||||
Contract contract = 14;
|
||||
string contract_as_json = 15;
|
||||
bytes contract_hash = 16;
|
||||
NodeAddress arbitrator_node_address = 17;
|
||||
NodeAddress mediator_node_address = 18;
|
||||
string error_message = 19;
|
||||
string counter_currency_tx_id = 20;
|
||||
repeated ChatMessage chat_message = 21;
|
||||
MediationResultState mediation_result_state = 22;
|
||||
int64 lock_time = 23;
|
||||
NodeAddress refund_agent_node_address = 24;
|
||||
RefundResultState refund_result_state = 25;
|
||||
string counter_currency_extra_data = 26;
|
||||
string asset_tx_proof_result = 27; // name of AssetTxProofResult enum
|
||||
string uid = 28;
|
||||
PayoutState payout_state = 12;
|
||||
DisputeState dispute_state = 13;
|
||||
TradePeriodState period_state = 14;
|
||||
Contract contract = 15;
|
||||
string contract_as_json = 16;
|
||||
bytes contract_hash = 17;
|
||||
NodeAddress arbitrator_node_address = 18;
|
||||
NodeAddress mediator_node_address = 19;
|
||||
string error_message = 20;
|
||||
string counter_currency_tx_id = 21;
|
||||
repeated ChatMessage chat_message = 22;
|
||||
MediationResultState mediation_result_state = 23;
|
||||
int64 lock_time = 24;
|
||||
NodeAddress refund_agent_node_address = 25;
|
||||
RefundResultState refund_result_state = 26;
|
||||
string counter_currency_extra_data = 27;
|
||||
string asset_tx_proof_result = 28; // name of AssetTxProofResult enum
|
||||
string uid = 29;
|
||||
}
|
||||
|
||||
message BuyerAsMakerTrade {
|
||||
|
Loading…
Reference in New Issue
Block a user