move trade wallet management functions from XmrWalletService to Trade

persist security deposits in TradePeer
set deposit tx and reserved key images when deposit tx created
listen to account service in trade manager
This commit is contained in:
woodser 2023-02-05 18:59:33 -05:00
parent 145157f84d
commit 60dc4901e4
17 changed files with 370 additions and 335 deletions

View File

@ -58,7 +58,6 @@ public class CoreDisputesService {
private final CoinFormatter formatter; private final CoinFormatter formatter;
private final KeyRing keyRing; private final KeyRing keyRing;
private final TradeManager tradeManager; private final TradeManager tradeManager;
private final XmrWalletService xmrWalletService;
@Inject @Inject
public CoreDisputesService(ArbitrationManager arbitrationManager, public CoreDisputesService(ArbitrationManager arbitrationManager,
@ -70,7 +69,6 @@ public class CoreDisputesService {
this.formatter = formatter; this.formatter = formatter;
this.keyRing = keyRing; this.keyRing = keyRing;
this.tradeManager = tradeManager; this.tradeManager = tradeManager;
this.xmrWalletService = xmrWalletService;
} }
public List<Dispute> getDisputes() { public List<Dispute> getDisputes() {
@ -144,19 +142,19 @@ public class CoreDisputesService {
// TODO: does not wait for success or error response // TODO: does not wait for success or error response
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) { public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
try {
// get winning dispute // get winning dispute
Dispute winningDispute; Dispute winningDispute;
Trade trade = tradeManager.getTrade(tradeId); Trade trade = tradeManager.getTrade(tradeId);
var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute() var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
.filter(d -> tradeId.equals(d.getTradeId())) .filter(d -> tradeId.equals(d.getTradeId()))
.filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller())) .filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller()))
.findFirst(); .findFirst();
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get(); if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId)); else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (trade) { synchronized (trade) {
try {
var closeDate = new Date(); var closeDate = new Date();
var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate); var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
@ -193,9 +191,10 @@ public class CoreDisputesService {
}, (errMessage, err) -> { }, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err); throw new IllegalStateException(errMessage, err);
}); });
} catch (Exception e) {
e.printStackTrace();
throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage());
} }
} catch (Exception e) {
throw new IllegalStateException(e);
} }
} }

View File

@ -27,6 +27,7 @@ import bisq.core.provider.price.PriceFeedService;
import bisq.core.setup.CorePersistedDataHost; import bisq.core.setup.CorePersistedDataHost;
import bisq.core.setup.CoreSetup; import bisq.core.setup.CoreSetup;
import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager; import bisq.core.support.dispute.arbitration.arbitrator.ArbitratorManager;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.TradeManager; import bisq.core.trade.TradeManager;
import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.trade.txproof.xmr.XmrTxProofService; import bisq.core.trade.txproof.xmr.XmrTxProofService;
@ -50,7 +51,7 @@ import com.google.inject.Guice;
import com.google.inject.Injector; import com.google.inject.Injector;
import java.io.Console; import java.io.Console;
import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -315,12 +316,14 @@ public abstract class HavenoExecutable implements GracefulShutDownHandler, Haven
injector.getInstance(TradeStatisticsManager.class).shutDown(); injector.getInstance(TradeStatisticsManager.class).shutDown();
injector.getInstance(XmrTxProofService.class).shutDown(); injector.getInstance(XmrTxProofService.class).shutDown();
injector.getInstance(AvoidStandbyModeService.class).shutDown(); injector.getInstance(AvoidStandbyModeService.class).shutDown();
injector.getInstance(TradeManager.class).shutDown(); log.info("TradeManager and XmrWalletService shutdown started");
HavenoUtils.executeTasks(Arrays.asList( // shut down trade and main wallets at same time
() -> injector.getInstance(TradeManager.class).shutDown(),
() -> injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly)));
log.info("OpenOfferManager shutdown started"); log.info("OpenOfferManager shutdown started");
injector.getInstance(OpenOfferManager.class).shutDown(() -> { injector.getInstance(OpenOfferManager.class).shutDown(() -> {
log.info("OpenOfferManager shutdown completed"); log.info("OpenOfferManager shutdown completed");
injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly);
injector.getInstance(BtcWalletService.class).shutDown(); injector.getInstance(BtcWalletService.class).shutDown();
// We need to shutdown BitcoinJ before the P2PService as it uses Tor. // We need to shutdown BitcoinJ before the P2PService as it uses Tor.

View File

@ -86,6 +86,7 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
if (injector != null) { if (injector != null) {
JsonFileManager.shutDownAllInstances(); JsonFileManager.shutDownAllInstances();
injector.getInstance(ArbitratorManager.class).shutDown(); injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(true);
injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> { injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> {
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> { injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
module.close(injector); module.close(injector);
@ -97,7 +98,6 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
}); });
}); });
injector.getInstance(WalletsSetup.class).shutDown(); injector.getInstance(WalletsSetup.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(true);
injector.getInstance(BtcWalletService.class).shutDown(); injector.getInstance(BtcWalletService.class).shutDown();
})); }));
// we wait max 5 sec. // we wait max 5 sec.

View File

@ -83,9 +83,8 @@ public class XmrWalletService {
private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user"; private static final String MONERO_WALLET_RPC_USERNAME = "haveno_user";
private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null private static final String MONERO_WALLET_RPC_DEFAULT_PASSWORD = "password"; // only used if account password is null
private static final String MONERO_WALLET_NAME = "haveno_XMR"; private static final String MONERO_WALLET_NAME = "haveno_XMR";
private static final String MONERO_MULTISIG_WALLET_PREFIX = "xmr_multisig_trade_";
public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee public static final double MINER_FEE_TOLERANCE = 0.25; // miner fee must be within percent of estimated fee
private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can abosrb miner fee up to percent private static final double SECURITY_DEPOSIT_TOLERANCE = Config.baseCurrencyNetwork() == BaseCurrencyNetwork.XMR_LOCAL ? 0.25 : 0.05; // security deposit can absorb miner fee up to percent
private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee private static final double DUST_TOLERANCE = 0.01; // max dust as percent of mining fee
private static final int NUM_MAX_BACKUP_WALLETS = 10; private static final int NUM_MAX_BACKUP_WALLETS = 10;
@ -101,8 +100,6 @@ public class XmrWalletService {
private TradeManager tradeManager; private TradeManager tradeManager;
private MoneroWalletRpc wallet; 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>>(); private final Map<String, Optional<MoneroTx>> txCache = new HashMap<String, Optional<MoneroTx>>();
private boolean isShutDown = false; private boolean isShutDown = false;
@ -117,7 +114,6 @@ public class XmrWalletService {
this.connectionsService = connectionsService; this.connectionsService = connectionsService;
this.walletsSetup = walletsSetup; this.walletsSetup = walletsSetup;
this.xmrAddressEntryList = xmrAddressEntryList; this.xmrAddressEntryList = xmrAddressEntryList;
this.multisigWallets = new HashMap<String, MoneroWallet>();
this.walletDir = walletDir; this.walletDir = walletDir;
this.rpcBindPort = rpcBindPort; this.rpcBindPort = rpcBindPort;
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME); this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
@ -133,20 +129,20 @@ public class XmrWalletService {
@Override @Override
public void onAccountCreated() { public void onAccountCreated() {
log.info(getClass() + ".accountService.onAccountCreated()"); log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()");
initialize(); initialize();
} }
@Override @Override
public void onAccountOpened() { public void onAccountOpened() {
log.info(getClass() + ".accountService.onAccountOpened()"); log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()");
initialize(); initialize();
} }
@Override @Override
public void onAccountClosed() { public void onAccountClosed() {
log.info(getClass() + ".accountService.onAccountClosed()"); log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()");
closeAllWallets(true); closeMainWallet(true);
} }
@Override @Override
@ -203,91 +199,68 @@ public class XmrWalletService {
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword(); return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
} }
private synchronized void initWalletLock(String id) { public boolean walletExists(String walletName) {
if (!walletLocks.containsKey(id)) walletLocks.put(id, new Object()); String path = walletDir.toString() + File.separator + walletName;
return new File(path + ".keys").exists();
} }
public boolean multisigWalletExists(String tradeId) { public MoneroWalletRpc createWallet(String walletName) {
initWalletLock(tradeId); log.info("{}.createWallet({})", getClass().getSimpleName(), walletName);
synchronized (walletLocks.get(tradeId)) { if (isShutDown) throw new IllegalStateException("Cannot create wallet because shutting down");
return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId); return createWallet(new MoneroWalletConfig()
} .setPath(walletName)
.setPassword(getWalletPassword()),
null,
true);
} }
public MoneroWallet createMultisigWallet(String tradeId) { public MoneroWalletRpc openWallet(String walletName) {
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId); log.info("{}.openWallet({})", getClass().getSimpleName(), walletName);
initWalletLock(tradeId); if (isShutDown) throw new IllegalStateException("Cannot open wallet because shutting down");
synchronized (walletLocks.get(tradeId)) { return openWallet(new MoneroWalletConfig()
if (multisigWallets.containsKey(tradeId)) return multisigWallets.get(tradeId); .setPath(walletName)
String path = MONERO_MULTISIG_WALLET_PREFIX + tradeId; .setPassword(getWalletPassword()),
MoneroWallet multisigWallet = createWallet(new MoneroWalletConfig().setPath(path).setPassword(getWalletPassword()), null, true); // auto-assign port null);
multisigWallets.put(tradeId, multisigWallet);
return multisigWallet;
}
} }
// TODO (woodser): provide progress notifications during open? public void saveWallet(MoneroWallet wallet, boolean backup) {
public MoneroWallet getMultisigWallet(String tradeId) {
if (isShutDown) throw new RuntimeException(getClass().getName() + " is shut down");
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 saveMultisigWallet(String tradeId) {
log.info("{}.saveMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) {
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
if (!walletExists(walletName)) {
log.warn("Multisig wallet for trade {} does not exist");
return;
}
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to save was not previously opened for trade " + tradeId);
saveWallet(multisigWallets.get(tradeId), true);
}
}
private void saveWallet(MoneroWallet wallet, boolean backup) {
wallet.save(); wallet.save();
if (backup) backupWallet(wallet.getPath()); if (backup) backupWallet(wallet.getPath());
} }
public void closeMultisigWallet(String tradeId) { public void closeWallet(MoneroWallet wallet, boolean save) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId); log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), wallet.getPath(), save);
initWalletLock(tradeId); MoneroError err = null;
synchronized (walletLocks.get(tradeId)) { try {
if (!multisigWallets.containsKey(tradeId)) throw new RuntimeException("Multisig wallet to close was not previously opened for trade " + tradeId); String path = wallet.getPath();
MoneroWallet wallet = multisigWallets.remove(tradeId); wallet.close(save);
closeWallet(wallet, true); if (save) backupWallet(path);
} catch (MoneroError e) {
err = e;
} }
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) wallet);
if (err != null) throw err;
} }
public boolean deleteMultisigWallet(String tradeId) { public void deleteWallet(String walletName) {
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId); log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName);
initWalletLock(tradeId); if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
synchronized (walletLocks.get(tradeId)) { String path = walletDir.toString() + File.separator + walletName;
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; if (!new File(path).delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
if (!walletExists(walletName)) return false; if (!new File(path + ".keys").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId); if (!new File(path + ".address.txt").delete()) throw new RuntimeException("Failed to delete wallet file: " + path);
deleteWallet(walletName);
return true;
}
} }
public void deleteMultisigWalletBackups(String tradeId) { public void backupWallet(String walletName) {
log.info("{}.deleteMultisigWalletBackups({})", getClass().getSimpleName(), tradeId); FileUtil.rollingBackup(walletDir, walletName, NUM_MAX_BACKUP_WALLETS);
initWalletLock(tradeId); FileUtil.rollingBackup(walletDir, walletName + ".keys", NUM_MAX_BACKUP_WALLETS);
synchronized (walletLocks.get(tradeId)) { FileUtil.rollingBackup(walletDir, walletName + ".address.txt", NUM_MAX_BACKUP_WALLETS);
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId; }
deleteWalletBackups(walletName);
} public void deleteWalletBackups(String walletName) {
FileUtil.deleteRollingBackup(walletDir, walletName);
FileUtil.deleteRollingBackup(walletDir, walletName + ".keys");
FileUtil.deleteRollingBackup(walletDir, walletName + ".address.txt");
} }
public MoneroTxWallet createTx(List<MoneroDestination> destinations) { public MoneroTxWallet createTx(List<MoneroDestination> destinations) {
@ -404,56 +377,58 @@ public class XmrWalletService {
public void verifyTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List<String> keyImages) { public void verifyTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List<String> keyImages) {
MoneroDaemonRpc daemon = getDaemon(); MoneroDaemonRpc daemon = getDaemon();
MoneroWallet wallet = getWallet(); MoneroWallet wallet = getWallet();
try { synchronized (daemon) {
// verify tx not submitted to pool
MoneroTx tx = daemon.getTx(txHash);
if (tx != null) throw new RuntimeException("Tx is already submitted");
// submit tx to pool
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
tx = getTx(txHash);
// verify key images
if (keyImages != null) {
Set<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Tx inputs do not match claimed key images");
}
// verify unlock height
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee
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());
// verify miner fee
BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee());
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);
// verify sufficient security deposit
check = wallet.checkTxKey(txHash, txKey, address);
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger();
BigInteger actualSecurityDeposit = check.getReceivedAmount().subtract(sendAmount);
if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit);
// verify deposit amount + miner fee within dust tolerance
BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger());
BigInteger actualDepositAndFee = check.getReceivedAmount().add(tx.getFee());
if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee);
} finally {
try { try {
daemon.flushTxPool(txHash); // flush tx from pool
} catch (MoneroRpcError err) { // verify tx not submitted to pool
System.out.println(daemon.getRpcConnection()); MoneroTx tx = daemon.getTx(txHash);
throw err.getCode() == -32601 ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err; if (tx != null) throw new RuntimeException("Tx is already submitted");
// submit tx to pool
MoneroSubmitTxResult result = daemon.submitTxHex(txHex, true); // TODO (woodser): invert doNotRelay flag to relay for library consistency?
if (!result.isGood()) throw new RuntimeException("Failed to submit tx to daemon: " + JsonUtils.serialize(result));
tx = getTx(txHash);
// verify key images
if (keyImages != null) {
Set<String> txKeyImages = new HashSet<String>();
for (MoneroOutput input : tx.getInputs()) txKeyImages.add(input.getKeyImage().getHex());
if (!txKeyImages.equals(new HashSet<String>(keyImages))) throw new Error("Tx inputs do not match claimed key images");
}
// verify unlock height
if (tx.getUnlockHeight() != 0) throw new RuntimeException("Unlock height must be 0");
// verify trade fee
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());
// verify miner fee
BigInteger feeEstimate = getFeeEstimate(tx.getWeight());
double feeDiff = tx.getFee().subtract(feeEstimate).abs().doubleValue() / feeEstimate.doubleValue(); // TODO: use BigDecimal?
if (feeDiff > MINER_FEE_TOLERANCE) throw new Error("Miner fee is not within " + (MINER_FEE_TOLERANCE * 100) + "% of estimated fee, expected " + feeEstimate + " but was " + tx.getFee());
log.info("Trade tx fee {} is within tolerance, diff%={}", tx.getFee(), feeDiff);
// verify sufficient security deposit
check = wallet.checkTxKey(txHash, txKey, address);
if (!check.isGood()) throw new RuntimeException("Invalid proof of deposit amount");
BigInteger minSecurityDeposit = new BigDecimal(securityDeposit).multiply(new BigDecimal(1.0 - SECURITY_DEPOSIT_TOLERANCE)).toBigInteger();
BigInteger actualSecurityDeposit = check.getReceivedAmount().subtract(sendAmount);
if (actualSecurityDeposit.compareTo(minSecurityDeposit) < 0) throw new RuntimeException("Security deposit amount is not enough, needed " + minSecurityDeposit + " but was " + actualSecurityDeposit);
// verify deposit amount + miner fee within dust tolerance
BigInteger minDepositAndFee = sendAmount.add(securityDeposit).subtract(new BigDecimal(tx.getFee()).multiply(new BigDecimal(1.0 - DUST_TOLERANCE)).toBigInteger());
BigInteger actualDepositAndFee = check.getReceivedAmount().add(tx.getFee());
if (actualDepositAndFee.compareTo(minDepositAndFee) < 0) throw new RuntimeException("Deposit amount + fee is not enough, needed " + minDepositAndFee + " but was " + actualDepositAndFee);
} finally {
try {
daemon.flushTxPool(txHash); // flush tx from pool
} catch (MoneroRpcError err) {
System.out.println(daemon.getRpcConnection());
throw err.getCode() == -32601 ? new RuntimeException("Failed to flush tx from pool. Arbitrator must use trusted, unrestricted daemon") : err;
}
} }
} }
} }
@ -528,9 +503,19 @@ public class XmrWalletService {
} }
} }
private void closeMainWallet(boolean save) {
try {
closeWallet(wallet, true);
wallet = null;
walletListeners.clear();
} catch (Exception e) {
log.warn("Error closing monero-wallet-rpc subprocess. Was Haveno stopped manually with ctrl+c?");
}
}
public void shutDown(boolean save) { public void shutDown(boolean save) {
this.isShutDown = true; this.isShutDown = true;
closeAllWallets(save); closeMainWallet(save);
} }
// ------------------------------ PRIVATE HELPERS ------------------------- // ------------------------------ PRIVATE HELPERS -------------------------
@ -544,11 +529,6 @@ public class XmrWalletService {
connectionsService.addListener(newConnection -> setDaemonConnection(newConnection)); connectionsService.addListener(newConnection -> setDaemonConnection(newConnection));
} }
private boolean walletExists(String walletName) {
String path = walletDir.toString() + File.separator + walletName;
return new File(path + ".keys").exists();
}
private void maybeInitMainWallet() { private void maybeInitMainWallet() {
if (wallet != null) throw new RuntimeException("Main wallet is already initialized"); if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
@ -605,7 +585,7 @@ public class XmrWalletService {
} else { } else {
walletRpc.setDaemonConnection(connection); walletRpc.setDaemonConnection(connection);
} }
log.info("Done creating wallet " + config.getPath()); log.info("Done creating wallet " + walletRpc.getPath());
return walletRpc; return walletRpc;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -624,6 +604,7 @@ public class XmrWalletService {
log.info("Opening wallet " + config.getPath()); log.info("Opening wallet " + config.getPath());
walletRpc.openWallet(config); walletRpc.openWallet(config);
walletRpc.setDaemonConnection(connectionsService.getConnection()); walletRpc.setDaemonConnection(connectionsService.getConnection());
log.info("Done opening wallet " + walletRpc.getPath());
return walletRpc; return walletRpc;
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
@ -718,80 +699,18 @@ public class XmrWalletService {
} }
}); });
// create tasks to change multisig wallet passwords // create tasks to change trade wallet passwords
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList()); List<Trade> trades = tradeManager.getAllTrades();
for (String tradeId : tradeIds) { for (Trade trade : trades) {
tasks.add(() -> { tasks.add(() -> {
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open if (trade.walletExists()) {
if (multisigWallet == null) return; trade.changeWalletPassword(oldPassword, newPassword); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
multisigWallet.changePassword(oldPassword, newPassword); }
saveMultisigWallet(tradeId);
}); });
} }
// excute tasks in parallel // excute tasks in parallel
HavenoUtils.executeTasks(tasks, Math.min(10, 1 + tradeIds.size())); HavenoUtils.executeTasks(tasks, Math.min(10, 1 + trades.size()));
}
private void closeWallet(MoneroWallet walletRpc, boolean save) {
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), walletRpc.getPath(), save);
MoneroError err = null;
try {
String path = walletRpc.getPath();
walletRpc.close(save);
if (save) backupWallet(path);
} catch (MoneroError e) {
err = e;
}
MONERO_WALLET_RPC_MANAGER.stopInstance((MoneroWalletRpc) walletRpc);
if (err != null) throw err;
}
private void deleteWallet(String walletName) {
log.info("{}.deleteWallet({})", getClass().getSimpleName(), walletName);
if (!walletExists(walletName)) throw new Error("Wallet does not exist at path: " + walletName);
String path = walletDir.toString() + File.separator + walletName;
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);
}
private void closeAllWallets(boolean save) {
// collect wallets to shutdown
List<MoneroWallet> openWallets = new ArrayList<MoneroWallet>();
if (wallet != null) openWallets.add(wallet);
for (String multisigWalletKey : multisigWallets.keySet()) {
openWallets.add(multisigWallets.get(multisigWalletKey));
}
// 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.executeTasks(tasks);
// clear wallets
wallet = null;
multisigWallets.clear();
walletListeners.clear();
}
private void backupWallet(String walletName) {
FileUtil.rollingBackup(walletDir, walletName, NUM_MAX_BACKUP_WALLETS);
FileUtil.rollingBackup(walletDir, walletName + ".keys", NUM_MAX_BACKUP_WALLETS);
FileUtil.rollingBackup(walletDir, walletName + ".address.txt", NUM_MAX_BACKUP_WALLETS);
}
private void deleteWalletBackups(String walletName) {
FileUtil.deleteRollingBackup(walletDir, walletName);
FileUtil.deleteRollingBackup(walletDir, walletName + ".keys");
FileUtil.deleteRollingBackup(walletDir, walletName + ".address.txt");
} }
// ----------------------------- LEGACY APP ------------------------------- // ----------------------------- LEGACY APP -------------------------------

View File

@ -445,7 +445,7 @@ public abstract class DisputeManager<T extends DisputeList<Dispute>> extends Sup
PubKeyRing senderPubKeyRing = null; PubKeyRing senderPubKeyRing = null;
try { try {
// intialize // initialize
T disputeList = getDisputeList(); T disputeList = getDisputeList();
if (disputeList == null) { if (disputeList == null) {
log.warn("disputes is null"); log.warn("disputes is null");

View File

@ -86,6 +86,10 @@ public class HavenoUtils {
return atomicUnitsToXmr(centinerosToAtomicUnits(centineros)); return atomicUnitsToXmr(centinerosToAtomicUnits(centineros));
} }
public static Coin centinerosToCoin(long centineros) {
return atomicUnitsToCoin(centinerosToAtomicUnits(centineros));
}
public static long atomicUnitsToCentineros(long atomicUnits) { // TODO: atomic units should be BigInteger; remove this? public static long atomicUnitsToCentineros(long atomicUnits) { // TODO: atomic units should be BigInteger; remove this?
return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue(); return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue();
} }

View File

@ -115,6 +115,10 @@ import monero.wallet.model.MoneroWalletListener;
@Slf4j @Slf4j
public abstract class Trade implements Tradable, Model { public abstract class Trade implements Tradable, Model {
private static final String MONERO_TRADE_WALLET_PREFIX = "xmr_trade_";
private MoneroWallet wallet; // trade wallet
private Object walletLock = new Object();
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// Enums // Enums
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -412,7 +416,6 @@ public abstract class Trade implements Tradable, Model {
@Getter @Getter
@Setter @Setter
private long lockTime; private long lockTime;
@Getter
@Setter @Setter
private long startTime; // added for haveno private long startTime; // added for haveno
@Getter @Getter
@ -583,7 +586,7 @@ public abstract class Trade implements Tradable, Model {
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
// API // INITIALIZATION
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(ProcessModelServiceProvider serviceProvider) { public void initialize(ProcessModelServiceProvider serviceProvider) {
@ -680,11 +683,42 @@ public abstract class Trade implements Tradable, Model {
return getArbitrator() == null ? null : getArbitrator().getNodeAddress(); return getArbitrator() == null ? null : getArbitrator().getNodeAddress();
} }
///////////////////////////////////////////////////////////////////////////////////////////
// WALLET MANAGEMENT
///////////////////////////////////////////////////////////////////////////////////////////
public boolean walletExists() {
synchronized (walletLock) {
return xmrWalletService.walletExists(MONERO_TRADE_WALLET_PREFIX + getId());
}
}
public MoneroWallet createWallet() {
synchronized (walletLock) {
if (walletExists()) throw new RuntimeException("Cannot create trade wallet because it already exists");
wallet = xmrWalletService.createWallet(getWalletName());
return wallet;
}
}
public MoneroWallet getWallet() {
synchronized (walletLock) {
if (wallet != null) return wallet;
if (!walletExists()) return null;
if (isInitialized) wallet = xmrWalletService.openWallet(getWalletName());
return wallet;
}
}
private String getWalletName() {
return MONERO_TRADE_WALLET_PREFIX + getId();
}
public void checkWalletConnection() { public void checkWalletConnection() {
CoreMoneroConnectionsService connectionService = xmrWalletService.getConnectionsService(); CoreMoneroConnectionsService connectionService = xmrWalletService.getConnectionsService();
connectionService.checkConnection(); connectionService.checkConnection();
connectionService.verifyConnection(); connectionService.verifyConnection();
if (!getWallet().isConnectedToDaemon()) throw new RuntimeException("Wallet is not connected to a Monero node"); if (!getWallet().isConnectedToDaemon()) throw new RuntimeException("Trade wallet is not connected to a Monero node");
} }
public boolean isWalletConnected() { public boolean isWalletConnected() {
@ -696,6 +730,88 @@ public abstract class Trade implements Tradable, Model {
} }
} }
public void syncWallet() {
if (getWallet() == null) throw new RuntimeException("Cannot sync trade wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync trade wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
getWallet().sync();
pollWallet();
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
updateWalletRefreshPeriod();
}
private void trySyncWallet() {
try {
syncWallet();
} catch (Exception e) {
if (isInitialized) {
log.warn("Error syncing trade wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
e.printStackTrace();
}
}
}
public void syncWalletNormallyForMs(long syncNormalDuration) {
syncNormalStartTime = System.currentTimeMillis();
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
UserThread.runAfter(() -> {
if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
}, syncNormalDuration);
}
public void changeWalletPassword(String oldPassword, String newPassword) {
synchronized (walletLock) {
getWallet().changePassword(oldPassword, newPassword);
saveWallet();
}
}
public void saveWallet() {
synchronized (walletLock) {
if (wallet == null) throw new RuntimeException("Trade wallet is not open for trade " + getId());
xmrWalletService.saveWallet(wallet, true);
}
}
private void closeWallet() {
synchronized (walletLock) {
if (wallet == null) throw new RuntimeException("Trade wallet to close was not previously opened for trade " + getId());
if (wallet.getPath() == null) log.warn("HOW DID PATH BECOME NULL?");
xmrWalletService.closeWallet(wallet, true);
wallet = null;
}
}
public void deleteWallet() {
synchronized (walletLock) {
if (walletExists()) {
// check if funds deposited but payout not unlocked
if (isDepositsPublished() && !isPayoutUnlocked()) {
log.warn("Refusing to delete wallet for {} {} because it could be funded", getClass().getSimpleName(), getId());
return;
}
// close and delete trade wallet
if (wallet != null) closeWallet();
xmrWalletService.deleteWallet(getWalletName());
// delete trade wallet backups unless deposits requested and payouts not unlocked
if (isDepositRequested() && !isPayoutUnlocked()) {
log.warn("Refusing to delete backup wallet for {} {} in the small chance it becomes funded", getClass().getSimpleName(), getId());
return;
}
xmrWalletService.deleteWalletBackups(getWalletName());
} else {
log.warn("Multisig wallet to delete for trade {} does not exist", getId());
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// PROTOCOL API
///////////////////////////////////////////////////////////////////////////////////////////
/** /**
* Create a contract based on the current state. * Create a contract based on the current state.
* *
@ -959,70 +1075,17 @@ public abstract class Trade implements Tradable, Model {
} }
} }
public MoneroWallet getWallet() {
return xmrWalletService.multisigWalletExists(getId()) ? xmrWalletService.getMultisigWallet(getId()) : null;
}
public void syncWallet() {
if (getWallet() == null) throw new RuntimeException("Cannot sync multisig wallet because it doesn't exist for " + getClass().getSimpleName() + ", " + getId());
if (getWallet().getDaemonConnection() == null) throw new RuntimeException("Cannot sync multisig wallet because it's not connected to a Monero daemon for " + getClass().getSimpleName() + ", " + getId());
log.info("Syncing wallet for {} {}", getClass().getSimpleName(), getId());
getWallet().sync();
pollWallet();
log.info("Done syncing wallet for {} {}", getClass().getSimpleName(), getId());
updateWalletRefreshPeriod();
}
private void trySyncWallet() {
try {
syncWallet();
} catch (Exception e) {
if (isInitialized) log.warn("Error syncing wallet for {} {}: {}", getClass().getSimpleName(), getId(), e.getMessage());
}
}
public void syncWalletNormallyForMs(long syncNormalDuration) {
syncNormalStartTime = System.currentTimeMillis();
setWalletRefreshPeriod(xmrWalletService.getConnectionsService().getDefaultRefreshPeriodMs());
UserThread.runAfter(() -> {
if (isInitialized && System.currentTimeMillis() >= syncNormalStartTime + syncNormalDuration) updateWalletRefreshPeriod();
}, syncNormalDuration);
}
public void saveWallet() {
xmrWalletService.saveMultisigWallet(getId());
}
public void deleteWallet() {
if (xmrWalletService.multisigWalletExists(getId())) {
// delete trade wallet unless funded
if (isDepositsPublished() && !isPayoutUnlocked()) {
log.warn("Refusing to delete wallet for {} {} because it could be funded", getClass().getSimpleName(), getId());
return;
}
xmrWalletService.deleteMultisigWallet(getId());
// delete trade wallet backups unless possibly funded
boolean possiblyFunded = isDepositRequested() && !isPayoutUnlocked();
if (possiblyFunded) {
log.warn("Refusing to delete backup wallet for {} {} in the small chance it becomes funded", getClass().getSimpleName(), getId());
return;
}
xmrWalletService.deleteMultisigWalletBackups(getId());
} else {
log.warn("Multisig wallet to delete for trade {} does not exist", getId());
}
}
public void shutDown() { public void shutDown() {
isInitialized = false; synchronized (walletLock) {
if (txPollLooper != null) { isInitialized = false;
txPollLooper.stop(); if (wallet != null) closeWallet();
txPollLooper = null; if (txPollLooper != null) {
txPollLooper.stop();
txPollLooper = null;
}
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
} }
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
@ -1458,23 +1521,13 @@ public abstract class Trade implements Tradable, Model {
} }
public Coin getBuyerSecurityDeposit() { public Coin getBuyerSecurityDeposit() {
if (this.getBuyer().getDepositTxHash() == null) return null; if (getBuyer().getDepositTxHash() == null) return null;
try { return HavenoUtils.centinerosToCoin(getBuyer().getSecurityDeposit());
MoneroTxWallet depositTx = getWallet().getTx(this.getBuyer().getDepositTxHash()); // TODO (monero-java): return null if tx id not found instead of throw exception
return HavenoUtils.atomicUnitsToCoin(depositTx.getIncomingAmount());
} catch (Exception e) {
return null;
}
} }
public Coin getSellerSecurityDeposit() { public Coin getSellerSecurityDeposit() {
if (this.getSeller().getDepositTxHash() == null) return null; if (getSeller().getDepositTxHash() == null) return null;
try { return HavenoUtils.centinerosToCoin(getSeller().getSecurityDeposit());
MoneroTxWallet depositTx = getWallet().getTx(this.getSeller().getDepositTxHash()); // TODO (monero-java): return null if tx id not found instead of throw exception
return HavenoUtils.atomicUnitsToCoin(depositTx.getIncomingAmount()).subtract(getAmount());
} catch (Exception e) {
return null;
}
} }
@Nullable @Nullable
@ -1603,11 +1656,23 @@ public abstract class Trade implements Tradable, Model {
// check deposit txs // check deposit txs
if (!isDepositsUnlocked()) { if (!isDepositsUnlocked()) {
if (txs.size() == 2) { if (txs.size() == 2) {
setStateDepositsPublished();
// update trader state
boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash()); boolean makerFirst = txs.get(0).getHash().equals(processModel.getMaker().getDepositTxHash());
getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1)); getMaker().setDepositTx(makerFirst ? txs.get(0) : txs.get(1));
getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0)); getTaker().setDepositTx(makerFirst ? txs.get(1) : txs.get(0));
// set security deposits
if (getBuyer().getSecurityDeposit() == 0) {
BigInteger buyerSecurityDeposit = ((MoneroTxWallet) getBuyer().getDepositTx()).getIncomingAmount();
BigInteger sellerSecurityDeposit = ((MoneroTxWallet) getSeller().getDepositTx()).getIncomingAmount().subtract(HavenoUtils.coinToAtomicUnits(getAmount()));
getBuyer().setSecurityDeposit(HavenoUtils.atomicUnitsToCentineros(buyerSecurityDeposit));
getSeller().setSecurityDeposit(HavenoUtils.atomicUnitsToCentineros(sellerSecurityDeposit));
}
// set deposits published state
setStateDepositsPublished();
// check if deposit txs confirmed // check if deposit txs confirmed
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed(); if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked(); if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();

View File

@ -17,6 +17,8 @@
package bisq.core.trade; package bisq.core.trade;
import bisq.core.api.AccountServiceListener;
import bisq.core.api.CoreAccountService;
import bisq.core.api.CoreNotificationService; import bisq.core.api.CoreNotificationService;
import bisq.core.btc.model.XmrAddressEntry; import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService; import bisq.core.btc.wallet.XmrWalletService;
@ -121,6 +123,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private final User user; private final User user;
@Getter @Getter
private final KeyRing keyRing; private final KeyRing keyRing;
private final CoreAccountService accountService;
private final XmrWalletService xmrWalletService; private final XmrWalletService xmrWalletService;
private final CoreNotificationService notificationService; private final CoreNotificationService notificationService;
private final OfferBookService offerBookService; private final OfferBookService offerBookService;
@ -158,6 +161,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
@Inject @Inject
public TradeManager(User user, public TradeManager(User user,
KeyRing keyRing, KeyRing keyRing,
CoreAccountService accountService,
XmrWalletService xmrWalletService, XmrWalletService xmrWalletService,
CoreNotificationService notificationService, CoreNotificationService notificationService,
OfferBookService offerBookService, OfferBookService offerBookService,
@ -177,6 +181,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
ReferralIdService referralIdService) { ReferralIdService referralIdService) {
this.user = user; this.user = user;
this.keyRing = keyRing; this.keyRing = keyRing;
this.accountService = accountService;
this.xmrWalletService = xmrWalletService; this.xmrWalletService = xmrWalletService;
this.notificationService = notificationService; this.notificationService = notificationService;
this.offerBookService = offerBookService; this.offerBookService = offerBookService;
@ -250,6 +255,39 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////
public void onAllServicesInitialized() { public void onAllServicesInitialized() {
// initialize
initialize();
// listen for account updates
accountService.addListener(new AccountServiceListener() {
@Override
public void onAccountCreated() {
log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()");
initialize();
}
@Override
public void onAccountOpened() {
log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()");
initialize();
}
@Override
public void onAccountClosed() {
log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()");
closeAllTrades();
}
@Override
public void onPasswordChanged(String oldPassword, String newPassword) {
// handled in XmrWalletService
}
});
}
private void initialize() {
if (p2PService.isBootstrapped()) { if (p2PService.isBootstrapped()) {
new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread
} else { } else {
@ -272,6 +310,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
public void shutDown() { public void shutDown() {
isShutDown = true; isShutDown = true;
closeAllTrades();
}
private void closeAllTrades() {
// collect trades to shutdown // collect trades to shutdown
Set<Trade> trades = new HashSet<Trade>(); Set<Trade> trades = new HashSet<Trade>();
@ -341,11 +383,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void initPersistedTrades() { private void initPersistedTrades() {
// get all trades // TODO: getAllTrades() // get all trades
List<Trade> trades = new ArrayList<Trade>(); List<Trade> trades = getAllTrades();
trades.addAll(tradableList.getList());
trades.addAll(closedTradableManager.getClosedTrades());
trades.addAll(failedTradesManager.getObservableList());
// open trades in parallel since each may open a multisig wallet // open trades in parallel since each may open a multisig wallet
int threadPoolSize = 10; int threadPoolSize = 10;
@ -1037,6 +1076,14 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
} }
public List<Trade> getAllTrades() {
List<Trade> trades = new ArrayList<Trade>();
trades.addAll(tradableList.getList());
trades.addAll(closedTradableManager.getClosedTrades());
trades.addAll(failedTradesManager.getObservableList());
return trades;
}
public List<Trade> getOpenTrades() { public List<Trade> getOpenTrades() {
synchronized (tradableList) { synchronized (tradableList) {
return ImmutableList.copyOf(getObservableList().stream() return ImmutableList.copyOf(getObservableList().stream()
@ -1085,7 +1132,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
} }
// remove trade if wallet deleted // remove trade if wallet deleted
if (!xmrWalletService.multisigWalletExists(trade.getId())) { if (!trade.walletExists()) {
removeTrade(trade); removeTrade(trade);
return; return;
} }
@ -1093,7 +1140,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// remove trade and wallet unless deposit requested without nack // remove trade and wallet unless deposit requested without nack
if (!trade.isDepositRequested() || trade.isDepositFailed()) { if (!trade.isDepositRequested() || trade.isDepositFailed()) {
removeTrade(trade); removeTrade(trade);
if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet(); if (trade.walletExists()) trade.deleteWallet();
} else { } else {
scheduleDeletionIfUnfunded(trade); scheduleDeletionIfUnfunded(trade);
} }
@ -1115,7 +1162,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId()); log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId());
removeTrade(trade); removeTrade(trade);
failedTradesManager.removeTrade(trade); failedTradesManager.removeTrade(trade);
if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet(); if (trade.walletExists()) trade.deleteWallet();
} else { } else {
log.warn("Refusing to delete {} {} after protocol timeout because its wallet might be funded", trade.getClass().getSimpleName(), trade.getId()); log.warn("Refusing to delete {} {} after protocol timeout because its wallet might be funded", trade.getClass().getSimpleName(), trade.getId());
} }

View File

@ -127,6 +127,7 @@ public final class TradePeer implements PersistablePayload {
private String depositTxHex; private String depositTxHex;
@Nullable @Nullable
private String depositTxKey; private String depositTxKey;
private long securityDeposit;
@Nullable @Nullable
private String updatedMultisigHex; private String updatedMultisigHex;
@ -168,6 +169,7 @@ public final class TradePeer implements PersistablePayload {
Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash)); Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash));
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex)); Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey)); Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex)); Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
builder.setCurrentDate(currentDate); builder.setCurrentDate(currentDate);
@ -218,6 +220,7 @@ public final class TradePeer implements PersistablePayload {
tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash())); tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()));
tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex())); tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey())); tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
tradePeer.setSecurityDeposit(proto.getSecurityDeposit());
tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex())); tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
return tradePeer; return tradePeer;
} }

View File

@ -19,16 +19,11 @@ package bisq.core.trade.protocol.tasks;
import bisq.common.app.Version; import bisq.common.app.Version;
import bisq.common.crypto.Sig;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.InitMultisigRequest; import bisq.core.trade.messages.InitMultisigRequest;
import bisq.core.trade.messages.InitTradeRequest; import bisq.core.trade.messages.InitTradeRequest;
import bisq.core.trade.protocol.TradeListener;
import bisq.network.p2p.AckMessage;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.SendDirectMessageListener; import bisq.network.p2p.SendDirectMessageListener;
import com.google.common.base.Charsets;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
@ -123,7 +118,7 @@ public class ArbitratorSendInitTradeOrMultisigRequests extends TradeTask {
} }
// create wallet for multisig // create wallet for multisig
MoneroWallet multisigWallet = processModel.getXmrWalletService().createMultisigWallet(trade.getId()); MoneroWallet multisigWallet = trade.createWallet();
// prepare multisig // prepare multisig
String preparedHex = multisigWallet.prepareMultisig(); String preparedHex = multisigWallet.prepareMultisig();

View File

@ -17,7 +17,6 @@
package bisq.core.trade.protocol.tasks; package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
@ -67,8 +66,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
checkNotNull(trade.getOffer(), "offer must not be null"); checkNotNull(trade.getOffer(), "offer must not be null");
// get multisig wallet // get multisig wallet
XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); MoneroWallet multisigWallet = trade.getWallet();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
// import multisig hex // import multisig hex
List<String> updatedMultisigHexes = new ArrayList<String>(); List<String> updatedMultisigHexes = new ArrayList<String>();

View File

@ -27,12 +27,16 @@ import bisq.core.trade.Trade;
import bisq.core.trade.Trade.State; import bisq.core.trade.Trade.State;
import bisq.core.trade.messages.SignContractRequest; import bisq.core.trade.messages.SignContractRequest;
import bisq.network.p2p.SendDirectMessageListener; import bisq.network.p2p.SendDirectMessageListener;
import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import com.google.common.base.Charsets; import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet; import monero.wallet.model.MoneroTxWallet;
// TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest // TODO (woodser): separate classes for deposit tx creation and contract request, or combine into ProcessInitMultisigRequest
@ -73,9 +77,15 @@ public class MaybeSendSignContractRequest extends TradeTask {
// create deposit tx and freeze inputs // create deposit tx and freeze inputs
MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade); MoneroTxWallet depositTx = trade.getXmrWalletService().createDepositTx(trade);
// collect reserved key images
List<String> reservedKeyImages = new ArrayList<String>();
for (MoneroOutput input : depositTx.getInputs()) reservedKeyImages.add(input.getKeyImage().getHex());
// save process state // save process state
processModel.setDepositTxXmr(depositTx); // TODO: trade.getSelf().setDepositTx() processModel.setDepositTxXmr(depositTx); // TODO: redundant with trade.getSelf().setDepositTx(), remove?
trade.getSelf().setDepositTx(depositTx);
trade.getSelf().setDepositTxHash(depositTx.getHash()); trade.getSelf().setDepositTxHash(depositTx.getHash());
trade.getSelf().setReserveTxKeyImages(reservedKeyImages);
trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address? trade.getSelf().setPayoutAddressString(trade.getXmrWalletService().getAddressEntry(processModel.getOffer().getId(), XmrAddressEntry.Context.TRADE_PAYOUT).get().getAddressString()); // TODO (woodser): allow custom payout address?
trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade)); trade.getSelf().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade));

View File

@ -82,12 +82,12 @@ public class ProcessInitMultisigRequest extends TradeTask {
boolean updateParticipants = false; boolean updateParticipants = false;
if (trade.getSelf().getPreparedMultisigHex() == null) { if (trade.getSelf().getPreparedMultisigHex() == null) {
log.info("Preparing multisig wallet for trade {}", trade.getId()); log.info("Preparing multisig wallet for trade {}", trade.getId());
multisigWallet = xmrWalletService.createMultisigWallet(trade.getId()); multisigWallet = trade.createWallet();
trade.getSelf().setPreparedMultisigHex(multisigWallet.prepareMultisig()); trade.getSelf().setPreparedMultisigHex(multisigWallet.prepareMultisig());
trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_PREPARED); trade.setStateIfValidTransitionTo(Trade.State.MULTISIG_PREPARED);
updateParticipants = true; updateParticipants = true;
} else if (processModel.getMultisigAddress() == null) { } else if (processModel.getMultisigAddress() == null) {
multisigWallet = xmrWalletService.getMultisigWallet(trade.getId()); multisigWallet = trade.getWallet();
} }
// make multisig if applicable // make multisig if applicable

View File

@ -17,7 +17,6 @@
package bisq.core.trade.protocol.tasks; package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.HavenoUtils; import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade; import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositsConfirmedMessage; import bisq.core.trade.messages.DepositsConfirmedMessage;
@ -27,7 +26,6 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner; import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
/** /**
* Send message on first confirmation to decrypt peer payment account and update multisig hex. * Send message on first confirmation to decrypt peer payment account and update multisig hex.
@ -62,9 +60,7 @@ public abstract class SendDepositsConfirmedMessage extends SendMailboxMessageTas
// export multisig hex once // export multisig hex once
if (trade.getSelf().getUpdatedMultisigHex() == null) { if (trade.getSelf().getUpdatedMultisigHex() == null) {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService(); trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
processModel.getTradeManager().requestPersistence(); processModel.getTradeManager().requestPersistence();
} }

View File

@ -89,9 +89,6 @@ import javax.inject.Named;
import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkNotNull;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
public class PendingTradesDataModel extends ActivatableDataModel { public class PendingTradesDataModel extends ActivatableDataModel {
@Getter @Getter
public final TradeManager tradeManager; public final TradeManager tradeManager;
@ -466,7 +463,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
byte[] payoutTxSerialized = null; byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null; String payoutTxHashAsString = null;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
if (trade.getPayoutTxId() != null) { if (trade.getPayoutTxId() != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr // payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString(); // payoutTxHashAsString = payoutTx.getHashAsString();

View File

@ -1749,7 +1749,8 @@ message TradePeer {
string deposit_tx_hash = 1008; string deposit_tx_hash = 1008;
string deposit_tx_hex = 1009; string deposit_tx_hex = 1009;
string deposit_tx_key = 1010; string deposit_tx_key = 1010;
string updated_multisig_hex = 1011; int64 security_deposit = 1011;
string updated_multisig_hex = 1012;
} }
/////////////////////////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -33,7 +33,6 @@ import bisq.common.UserThread;
import bisq.common.app.AppModule; import bisq.common.app.AppModule;
import bisq.common.app.Capabilities; import bisq.common.app.Capabilities;
import bisq.common.app.Capability; import bisq.common.app.Capability;
import bisq.common.app.DevEnv;
import bisq.common.config.BaseCurrencyNetwork; import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config; import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler; import bisq.common.handlers.ResultHandler;