mirror of
https://github.com/haveno-dex/haveno.git
synced 2024-11-17 00:07:49 +00:00
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:
parent
145157f84d
commit
60dc4901e4
17 changed files with 370 additions and 335 deletions
|
@ -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,19 +142,19 @@ 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;
|
||||
Trade trade = tradeManager.getTrade(tradeId);
|
||||
var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
|
||||
.filter(d -> tradeId.equals(d.getTradeId()))
|
||||
.filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller()))
|
||||
.findFirst();
|
||||
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||
// get winning dispute
|
||||
Dispute winningDispute;
|
||||
Trade trade = tradeManager.getTrade(tradeId);
|
||||
var winningDisputeOptional = arbitrationManager.getDisputesAsObservableList().stream() // TODO (woodser): use getDispute()
|
||||
.filter(d -> tradeId.equals(d.getTradeId()))
|
||||
.filter(d -> trade.getTradePeer(d.getTraderPubKeyRing()) == (winner == DisputeResult.Winner.BUYER ? trade.getBuyer() : trade.getSeller()))
|
||||
.findFirst();
|
||||
if (winningDisputeOptional.isPresent()) winningDispute = winningDisputeOptional.get();
|
||||
else throw new IllegalStateException(format("dispute for tradeId '%s' not found", tradeId));
|
||||
|
||||
synchronized (trade) {
|
||||
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) {
|
||||
e.printStackTrace();
|
||||
throw new IllegalStateException(e.getMessage() == null ? ("Error resolving dispute for trade " + trade.getId()) : e.getMessage());
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,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) {
|
||||
MoneroDaemonRpc daemon = getDaemon();
|
||||
MoneroWallet wallet = getWallet();
|
||||
try {
|
||||
|
||||
// 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 {
|
||||
synchronized (daemon) {
|
||||
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;
|
||||
|
||||
// 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 {
|
||||
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) {
|
||||
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 -------------------------------
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,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() {
|
||||
isInitialized = false;
|
||||
if (txPollLooper != null) {
|
||||
txPollLooper.stop();
|
||||
txPollLooper = null;
|
||||
synchronized (walletLock) {
|
||||
isInitialized = false;
|
||||
if (wallet != null) closeWallet();
|
||||
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() {
|
||||
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();
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>();
|
||||
|
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue