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 KeyRing keyRing;
private final TradeManager tradeManager;
private final XmrWalletService xmrWalletService;
@Inject
public CoreDisputesService(ArbitrationManager arbitrationManager,
@ -70,7 +69,6 @@ public class CoreDisputesService {
this.formatter = formatter;
this.keyRing = keyRing;
this.tradeManager = tradeManager;
this.xmrWalletService = xmrWalletService;
}
public List<Dispute> getDisputes() {
@ -144,7 +142,6 @@ public class CoreDisputesService {
// TODO: does not wait for success or error response
public void resolveDispute(String tradeId, DisputeResult.Winner winner, DisputeResult.Reason reason, String summaryNotes, long customWinnerAmount) {
try {
// get winning dispute
Dispute winningDispute;
@ -157,6 +154,7 @@ public class CoreDisputesService {
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
synchronized (trade) {
try {
var closeDate = new Date();
var disputeResult = createDisputeResult(winningDispute, winner, reason, summaryNotes, closeDate);
@ -193,9 +191,10 @@ public class CoreDisputesService {
}, (errMessage, err) -> {
throw new IllegalStateException(errMessage, err);
});
}
} catch (Exception e) {
throw new IllegalStateException(e);
e.printStackTrace();
throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage());
}
}
}

View File

@ -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.HavenoUtils;
import bisq.core.trade.TradeManager;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.trade.txproof.xmr.XmrTxProofService;
@ -50,7 +51,7 @@ import com.google.inject.Guice;
import com.google.inject.Injector;
import java.io.Console;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
@ -315,12 +316,14 @@ 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();
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");
injector.getInstance(OpenOfferManager.class).shutDown(() -> {
log.info("OpenOfferManager shutdown completed");
injector.getInstance(XmrWalletService.class).shutDown(!isReadOnly);
injector.getInstance(BtcWalletService.class).shutDown();
// 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) {
JsonFileManager.shutDownAllInstances();
injector.getInstance(ArbitratorManager.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(true);
injector.getInstance(OpenOfferManager.class).shutDown(() -> injector.getInstance(P2PService.class).shutDown(() -> {
injector.getInstance(WalletsSetup.class).shutDownComplete.addListener((ov, o, n) -> {
module.close(injector);
@ -97,7 +98,6 @@ public abstract class ExecutableForAppWithP2p extends HavenoExecutable {
});
});
injector.getInstance(WalletsSetup.class).shutDown();
injector.getInstance(XmrWalletService.class).shutDown(true);
injector.getInstance(BtcWalletService.class).shutDown();
}));
// 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_DEFAULT_PASSWORD = "password"; // only used if account password is null
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
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 int NUM_MAX_BACKUP_WALLETS = 10;
@ -101,8 +100,6 @@ 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>>();
private boolean isShutDown = false;
@ -117,7 +114,6 @@ public class XmrWalletService {
this.connectionsService = connectionsService;
this.walletsSetup = walletsSetup;
this.xmrAddressEntryList = xmrAddressEntryList;
this.multisigWallets = new HashMap<String, MoneroWallet>();
this.walletDir = walletDir;
this.rpcBindPort = rpcBindPort;
this.xmrWalletFile = new File(walletDir, MONERO_WALLET_NAME);
@ -133,20 +129,20 @@ public class XmrWalletService {
@Override
public void onAccountCreated() {
log.info(getClass() + ".accountService.onAccountCreated()");
log.info(getClass().getSimpleName() + ".accountService.onAccountCreated()");
initialize();
}
@Override
public void onAccountOpened() {
log.info(getClass() + ".accountService.onAccountOpened()");
log.info(getClass().getSimpleName() + ".accountService.onAccountOpened()");
initialize();
}
@Override
public void onAccountClosed() {
log.info(getClass() + ".accountService.onAccountClosed()");
closeAllWallets(true);
log.info(getClass().getSimpleName() + ".accountService.onAccountClosed()");
closeMainWallet(true);
}
@Override
@ -203,91 +199,68 @@ public class XmrWalletService {
return accountService.getPassword() == null ? MONERO_WALLET_RPC_DEFAULT_PASSWORD : accountService.getPassword();
}
private synchronized void initWalletLock(String id) {
if (!walletLocks.containsKey(id)) walletLocks.put(id, new Object());
public boolean walletExists(String walletName) {
String path = walletDir.toString() + File.separator + walletName;
return new File(path + ".keys").exists();
}
public boolean multisigWalletExists(String tradeId) {
initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) {
return walletExists(MONERO_MULTISIG_WALLET_PREFIX + tradeId);
}
public MoneroWalletRpc createWallet(String walletName) {
log.info("{}.createWallet({})", getClass().getSimpleName(), walletName);
if (isShutDown) throw new IllegalStateException("Cannot create wallet because shutting down");
return createWallet(new MoneroWalletConfig()
.setPath(walletName)
.setPassword(getWalletPassword()),
null,
true);
}
public MoneroWallet createMultisigWallet(String tradeId) {
log.info("{}.createMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId);
synchronized (walletLocks.get(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;
}
public MoneroWalletRpc openWallet(String walletName) {
log.info("{}.openWallet({})", getClass().getSimpleName(), walletName);
if (isShutDown) throw new IllegalStateException("Cannot open wallet because shutting down");
return openWallet(new MoneroWalletConfig()
.setPath(walletName)
.setPassword(getWalletPassword()),
null);
}
// TODO (woodser): provide progress notifications during open?
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) {
public void saveWallet(MoneroWallet wallet, boolean backup) {
wallet.save();
if (backup) backupWallet(wallet.getPath());
}
public void closeMultisigWallet(String tradeId) {
log.info("{}.closeMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId);
synchronized (walletLocks.get(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 void closeWallet(MoneroWallet wallet, boolean save) {
log.info("{}.closeWallet({}, {})", getClass().getSimpleName(), wallet.getPath(), save);
MoneroError err = null;
try {
String path = wallet.getPath();
wallet.close(save);
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) {
log.info("{}.deleteMultisigWallet({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) {
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
if (!walletExists(walletName)) return false;
if (multisigWallets.containsKey(tradeId)) closeMultisigWallet(tradeId);
deleteWallet(walletName);
return true;
}
public 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);
}
public void deleteMultisigWalletBackups(String tradeId) {
log.info("{}.deleteMultisigWalletBackups({})", getClass().getSimpleName(), tradeId);
initWalletLock(tradeId);
synchronized (walletLocks.get(tradeId)) {
String walletName = MONERO_MULTISIG_WALLET_PREFIX + tradeId;
deleteWalletBackups(walletName);
public 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);
}
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) {
@ -404,6 +377,7 @@ public class XmrWalletService {
public void verifyTradeTx(BigInteger tradeFee, BigInteger sendAmount, BigInteger securityDeposit, String address, String txHash, String txHex, String txKey, List<String> keyImages) {
MoneroDaemonRpc daemon = getDaemon();
MoneroWallet wallet = getWallet();
synchronized (daemon) {
try {
// verify tx not submitted to pool
@ -457,6 +431,7 @@ public class XmrWalletService {
}
}
}
}
/**
* Get the tx fee estimate based on its weight.
@ -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) {
this.isShutDown = true;
closeAllWallets(save);
closeMainWallet(save);
}
// ------------------------------ PRIVATE HELPERS -------------------------
@ -544,11 +529,6 @@ public class XmrWalletService {
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() {
if (wallet != null) throw new RuntimeException("Main wallet is already initialized");
@ -605,7 +585,7 @@ public class XmrWalletService {
} else {
walletRpc.setDaemonConnection(connection);
}
log.info("Done creating wallet " + config.getPath());
log.info("Done creating wallet " + walletRpc.getPath());
return walletRpc;
} catch (Exception e) {
e.printStackTrace();
@ -624,6 +604,7 @@ public class XmrWalletService {
log.info("Opening wallet " + config.getPath());
walletRpc.openWallet(config);
walletRpc.setDaemonConnection(connectionsService.getConnection());
log.info("Done opening wallet " + walletRpc.getPath());
return walletRpc;
} catch (Exception e) {
e.printStackTrace();
@ -718,80 +699,18 @@ public class XmrWalletService {
}
});
// create tasks to change multisig wallet passwords
List<String> tradeIds = tradeManager.getOpenTrades().stream().map(Trade::getId).collect(Collectors.toList());
for (String tradeId : tradeIds) {
// create tasks to change trade wallet passwords
List<Trade> trades = tradeManager.getAllTrades();
for (Trade trade : trades) {
tasks.add(() -> {
MoneroWallet multisigWallet = getMultisigWallet(tradeId); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
if (multisigWallet == null) return;
multisigWallet.changePassword(oldPassword, newPassword);
saveMultisigWallet(tradeId);
if (trade.walletExists()) {
trade.changeWalletPassword(oldPassword, newPassword); // TODO (woodser): this unnecessarily connects and syncs unopen wallets and leaves open
}
});
}
// excute tasks in parallel
HavenoUtils.executeTasks(tasks, Math.min(10, 1 + tradeIds.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");
HavenoUtils.executeTasks(tasks, Math.min(10, 1 + trades.size()));
}
// ----------------------------- LEGACY APP -------------------------------

View File

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

View File

@ -86,6 +86,10 @@ public class HavenoUtils {
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?
return atomicUnits / CENTINEROS_AU_MULTIPLIER.longValue();
}

View File

@ -115,6 +115,10 @@ import monero.wallet.model.MoneroWalletListener;
@Slf4j
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
///////////////////////////////////////////////////////////////////////////////////////////
@ -412,7 +416,6 @@ public abstract class Trade implements Tradable, Model {
@Getter
@Setter
private long lockTime;
@Getter
@Setter
private long startTime; // added for haveno
@Getter
@ -583,7 +586,7 @@ public abstract class Trade implements Tradable, Model {
///////////////////////////////////////////////////////////////////////////////////////////
// API
// INITIALIZATION
///////////////////////////////////////////////////////////////////////////////////////////
public void initialize(ProcessModelServiceProvider serviceProvider) {
@ -680,11 +683,42 @@ public abstract class Trade implements Tradable, Model {
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() {
CoreMoneroConnectionsService connectionService = xmrWalletService.getConnectionsService();
connectionService.checkConnection();
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() {
@ -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.
*
@ -959,64 +1075,10 @@ 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() {
synchronized (walletLock) {
isInitialized = false;
if (wallet != null) closeWallet();
if (txPollLooper != null) {
txPollLooper.stop();
txPollLooper = null;
@ -1024,6 +1086,7 @@ public abstract class Trade implements Tradable, Model {
if (tradePhaseSubscription != null) tradePhaseSubscription.unsubscribe();
if (payoutStateSubscription != null) payoutStateSubscription.unsubscribe();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Model implementation
@ -1458,23 +1521,13 @@ public abstract class Trade implements Tradable, Model {
}
public Coin getBuyerSecurityDeposit() {
if (this.getBuyer().getDepositTxHash() == null) return null;
try {
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;
}
if (getBuyer().getDepositTxHash() == null) return null;
return HavenoUtils.centinerosToCoin(getBuyer().getSecurityDeposit());
}
public Coin getSellerSecurityDeposit() {
if (this.getSeller().getDepositTxHash() == null) return null;
try {
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;
}
if (getSeller().getDepositTxHash() == null) return null;
return HavenoUtils.centinerosToCoin(getSeller().getSecurityDeposit());
}
@Nullable
@ -1603,11 +1656,23 @@ public abstract class Trade implements Tradable, Model {
// check deposit txs
if (!isDepositsUnlocked()) {
if (txs.size() == 2) {
setStateDepositsPublished();
// update trader state
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));
// 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
if (txs.get(0).isConfirmed() && txs.get(1).isConfirmed()) setStateDepositsConfirmed();
if (!txs.get(0).isLocked() && !txs.get(1).isLocked()) setStateDepositsUnlocked();

View File

@ -17,6 +17,8 @@
package bisq.core.trade;
import bisq.core.api.AccountServiceListener;
import bisq.core.api.CoreAccountService;
import bisq.core.api.CoreNotificationService;
import bisq.core.btc.model.XmrAddressEntry;
import bisq.core.btc.wallet.XmrWalletService;
@ -121,6 +123,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private final User user;
@Getter
private final KeyRing keyRing;
private final CoreAccountService accountService;
private final XmrWalletService xmrWalletService;
private final CoreNotificationService notificationService;
private final OfferBookService offerBookService;
@ -158,6 +161,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
@Inject
public TradeManager(User user,
KeyRing keyRing,
CoreAccountService accountService,
XmrWalletService xmrWalletService,
CoreNotificationService notificationService,
OfferBookService offerBookService,
@ -177,6 +181,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
ReferralIdService referralIdService) {
this.user = user;
this.keyRing = keyRing;
this.accountService = accountService;
this.xmrWalletService = xmrWalletService;
this.notificationService = notificationService;
this.offerBookService = offerBookService;
@ -250,6 +255,39 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
///////////////////////////////////////////////////////////////////////////////////////////
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()) {
new Thread(() -> initPersistedTrades()).start(); // initialize trades off main thread
} else {
@ -272,6 +310,10 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
public void shutDown() {
isShutDown = true;
closeAllTrades();
}
private void closeAllTrades() {
// collect trades to shutdown
Set<Trade> trades = new HashSet<Trade>();
@ -341,11 +383,8 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
private void initPersistedTrades() {
// get all trades // TODO: getAllTrades()
List<Trade> trades = new ArrayList<Trade>();
trades.addAll(tradableList.getList());
trades.addAll(closedTradableManager.getClosedTrades());
trades.addAll(failedTradesManager.getObservableList());
// get all trades
List<Trade> trades = getAllTrades();
// open trades in parallel since each may open a multisig wallet
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() {
synchronized (tradableList) {
return ImmutableList.copyOf(getObservableList().stream()
@ -1085,7 +1132,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
}
// remove trade if wallet deleted
if (!xmrWalletService.multisigWalletExists(trade.getId())) {
if (!trade.walletExists()) {
removeTrade(trade);
return;
}
@ -1093,7 +1140,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
// remove trade and wallet unless deposit requested without nack
if (!trade.isDepositRequested() || trade.isDepositFailed()) {
removeTrade(trade);
if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet();
if (trade.walletExists()) trade.deleteWallet();
} else {
scheduleDeletionIfUnfunded(trade);
}
@ -1115,7 +1162,7 @@ public class TradeManager implements PersistedDataHost, DecryptedDirectMessageLi
log.warn("Deleting {} {} after protocol timeout", trade.getClass().getSimpleName(), trade.getId());
removeTrade(trade);
failedTradesManager.removeTrade(trade);
if (xmrWalletService.multisigWalletExists(trade.getId())) trade.deleteWallet();
if (trade.walletExists()) trade.deleteWallet();
} else {
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;
@Nullable
private String depositTxKey;
private long securityDeposit;
@Nullable
private String updatedMultisigHex;
@ -168,6 +169,7 @@ public final class TradePeer implements PersistablePayload {
Optional.ofNullable(depositTxHash).ifPresent(e -> builder.setDepositTxHash(depositTxHash));
Optional.ofNullable(depositTxHex).ifPresent(e -> builder.setDepositTxHex(depositTxHex));
Optional.ofNullable(depositTxKey).ifPresent(e -> builder.setDepositTxKey(depositTxKey));
Optional.ofNullable(securityDeposit).ifPresent(e -> builder.setSecurityDeposit(securityDeposit));
Optional.ofNullable(updatedMultisigHex).ifPresent(e -> builder.setUpdatedMultisigHex(updatedMultisigHex));
builder.setCurrentDate(currentDate);
@ -218,6 +220,7 @@ public final class TradePeer implements PersistablePayload {
tradePeer.setDepositTxHash(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHash()));
tradePeer.setDepositTxHex(ProtoUtil.stringOrNullFromProto(proto.getDepositTxHex()));
tradePeer.setDepositTxKey(ProtoUtil.stringOrNullFromProto(proto.getDepositTxKey()));
tradePeer.setSecurityDeposit(proto.getSecurityDeposit());
tradePeer.setUpdatedMultisigHex(ProtoUtil.stringOrNullFromProto(proto.getUpdatedMultisigHex()));
return tradePeer;
}

View File

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

View File

@ -17,7 +17,6 @@
package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.Trade;
import bisq.common.taskrunner.TaskRunner;
@ -67,8 +66,7 @@ public class BuyerPreparePaymentSentMessage extends TradeTask {
checkNotNull(trade.getOffer(), "offer must not be null");
// get multisig wallet
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(trade.getId());
MoneroWallet multisigWallet = trade.getWallet();
// import multisig hex
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.messages.SignContractRequest;
import bisq.network.p2p.SendDirectMessageListener;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import com.google.common.base.Charsets;
import lombok.extern.slf4j.Slf4j;
import monero.daemon.model.MoneroOutput;
import monero.wallet.model.MoneroTxWallet;
// 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
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
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().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().setPaymentAccountPayload(trade.getProcessModel().getPaymentAccountPayload(trade));

View File

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

View File

@ -17,7 +17,6 @@
package bisq.core.trade.protocol.tasks;
import bisq.core.btc.wallet.XmrWalletService;
import bisq.core.trade.HavenoUtils;
import bisq.core.trade.Trade;
import bisq.core.trade.messages.DepositsConfirmedMessage;
@ -27,7 +26,6 @@ import bisq.common.crypto.PubKeyRing;
import bisq.common.taskrunner.TaskRunner;
import lombok.extern.slf4j.Slf4j;
import monero.wallet.MoneroWallet;
/**
* 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
if (trade.getSelf().getUpdatedMultisigHex() == null) {
XmrWalletService walletService = processModel.getProvider().getXmrWalletService();
MoneroWallet multisigWallet = walletService.getMultisigWallet(tradeId);
trade.getSelf().setUpdatedMultisigHex(multisigWallet.exportMultisigHex());
trade.getSelf().setUpdatedMultisigHex(trade.getWallet().exportMultisigHex());
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.checkNotNull;
import monero.daemon.model.MoneroTx;
import monero.wallet.MoneroWallet;
public class PendingTradesDataModel extends ActivatableDataModel {
@Getter
public final TradeManager tradeManager;
@ -466,7 +463,6 @@ public class PendingTradesDataModel extends ActivatableDataModel {
byte[] payoutTxSerialized = null;
String payoutTxHashAsString = null;
MoneroWallet multisigWallet = xmrWalletService.getMultisigWallet(trade.getId());
if (trade.getPayoutTxId() != null) {
// payoutTxSerialized = payoutTx.bitcoinSerialize(); // TODO (woodser): no need to pass serialized txs for xmr
// payoutTxHashAsString = payoutTx.getHashAsString();

View File

@ -1749,7 +1749,8 @@ message TradePeer {
string deposit_tx_hash = 1008;
string deposit_tx_hex = 1009;
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.Capabilities;
import bisq.common.app.Capability;
import bisq.common.app.DevEnv;
import bisq.common.config.BaseCurrencyNetwork;
import bisq.common.config.Config;
import bisq.common.handlers.ResultHandler;