add mix config persistence and initial usage

This commit is contained in:
Craig Raw 2021-09-01 13:10:46 +02:00
parent 13e01451b7
commit adb77771aa
16 changed files with 171 additions and 38 deletions

2
drongo

@ -1 +1 @@
Subproject commit 71b5778226ef22881240143425325525c1a98d06
Subproject commit 67836b2b557839317316a3e1c8d18b98a51d0e29

View file

@ -899,6 +899,7 @@ public class AppController implements Initializable {
if(wallet.isWhirlpoolMasterWallet()) {
String walletId = storage.getWalletId(wallet);
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
whirlpool.setScode(wallet.getOrCreateMixConfig().getScode());
whirlpool.setHDWallet(storage.getWalletId(wallet), copy);
}

View file

@ -469,7 +469,7 @@ public class AppServices {
Whirlpool whirlpool = whirlpoolMap.get(walletId);
if(whirlpool == null) {
HostAndPort torProxy = AppServices.isTorRunning() ? HostAndPort.fromParts("localhost", TorService.PROXY_PORT) : (Config.get().getProxyServer() == null || Config.get().getProxyServer().isEmpty() || !Config.get().isUseProxy() ? null : HostAndPort.fromString(Config.get().getProxyServer()));
whirlpool = new Whirlpool(Network.get(), torProxy, Config.get().getScode());
whirlpool = new Whirlpool(Network.get(), torProxy);
whirlpoolMap.put(walletId, whirlpool);
}
@ -477,12 +477,15 @@ public class AppServices {
}
private void startAllWhirlpool() {
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(whirlpool -> whirlpool.hasWallet() && !whirlpool.isStarted()).collect(Collectors.toList())) {
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
startupService.setOnFailed(workerStateEvent -> {
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
});
startupService.start();
for(Map.Entry<String, Whirlpool> entry : whirlpoolMap.entrySet().stream().filter(entry -> entry.getValue().hasWallet() && !entry.getValue().isStarted()).collect(Collectors.toList())) {
Wallet wallet = getWallet(entry.getKey());
if(wallet.getMixConfig() != null && wallet.getMixConfig().getMixOnStartup() != Boolean.FALSE) {
Whirlpool.StartupService startupService = new Whirlpool.StartupService(entry.getValue());
startupService.setOnFailed(workerStateEvent -> {
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
});
startupService.start();
}
}
}
@ -974,7 +977,7 @@ public class AppServices {
public void walletOpened(WalletOpenedEvent event) {
String walletId = event.getStorage().getWalletId(event.getWallet());
Whirlpool whirlpool = whirlpoolMap.get(walletId);
if(whirlpool != null && !whirlpool.isStarted() && isConnected()) {
if(whirlpool != null && !whirlpool.isStarted() && isConnected() && event.getWallet().getMixConfig() != null && event.getWallet().getMixConfig().getMixOnStartup() != Boolean.FALSE) {
Whirlpool.StartupService startupService = new Whirlpool.StartupService(whirlpool);
startupService.setOnFailed(workerStateEvent -> {
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());

View file

@ -0,0 +1,9 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletMixConfigChangedEvent extends WalletChangedEvent {
public WalletMixConfigChangedEvent(Wallet wallet) {
super(wallet);
}
}

View file

@ -61,7 +61,6 @@ public class Config {
private String proxyServer;
private Double appWidth;
private Double appHeight;
private String scode;
private static Config INSTANCE;
@ -489,15 +488,6 @@ public class Config {
flush();
}
public String getScode() {
return scode;
}
public void setScode(String scode) {
this.scode = scode;
flush();
}
private synchronized void flush() {
Gson gson = getGson();
try {

View file

@ -272,6 +272,11 @@ public class DbPersistence implements Persistence {
}
}
if(dirtyPersistables.mixConfig) {
MixConfigDao mixConfigDao = handle.attach(MixConfigDao.class);
mixConfigDao.addOrUpdate(wallet, wallet.getMixConfig());
}
if(!dirtyPersistables.changedUtxoMixes.isEmpty()) {
UtxoMixDataDao utxoMixDataDao = handle.attach(UtxoMixDataDao.class);
for(Map.Entry<Sha256Hash, UtxoMixData> utxoMixDataEntry : dirtyPersistables.changedUtxoMixes.entrySet()) {
@ -652,6 +657,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(persistsFor(event.getWallet()) && event.getWallet().getMixConfig() != null) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).mixConfig = true;
}
}
@Subscribe
public void walletUtxoMixesChanged(WalletUtxoMixesChangedEvent event) {
if(persistsFor(event.getWallet())) {
@ -680,6 +692,7 @@ public class DbPersistence implements Persistence {
public Integer blockHeight = null;
public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public boolean mixConfig;
public final Map<Sha256Hash, UtxoMixData> changedUtxoMixes = new HashMap<>();
public final Map<Sha256Hash, UtxoMixData> removedUtxoMixes = new HashMap<>();
public final List<Keystore> labelKeystores = new ArrayList<>();
@ -694,6 +707,7 @@ public class DbPersistence implements Persistence {
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
"\nUTXO statuses:" + utxoStatuses +
"\nMix config:" + mixConfig +
"\nUTXO mixes changed:" + changedUtxoMixes +
"\nUTXO mixes removed:" + removedUtxoMixes +
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +

View file

@ -0,0 +1,41 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.Wallet;
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
import org.jdbi.v3.sqlobject.statement.GetGeneratedKeys;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
public interface MixConfigDao {
@SqlQuery("select id, scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes from mixConfig where wallet = ?")
@RegisterRowMapper(MixConfigMapper.class)
MixConfig getForWalletId(Long id);
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, wallet) values (?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, long wallet);
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, wallet = ? where id = ?")
void updateMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, long wallet, long id);
default void addMixConfig(Wallet wallet) {
if(wallet.getMixConfig() != null) {
addOrUpdate(wallet, wallet.getMixConfig());
}
}
default void addOrUpdate(Wallet wallet, MixConfig mixConfig) {
String mixToWalletFile = null;
if(mixConfig.getMixToWalletFile() != null) {
mixToWalletFile = mixConfig.getMixToWalletFile().getAbsolutePath();
}
if(mixConfig.getId() == null) {
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), wallet.getId());
mixConfig.setId(id);
} else {
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), wallet.getId(), mixConfig.getId());
}
}
}

View file

@ -0,0 +1,33 @@
package com.sparrowwallet.sparrow.io.db;
import com.sparrowwallet.drongo.wallet.MixConfig;
import org.jdbi.v3.core.mapper.RowMapper;
import org.jdbi.v3.core.statement.StatementContext;
import java.io.File;
import java.sql.ResultSet;
import java.sql.SQLException;
public class MixConfigMapper implements RowMapper<MixConfig> {
@Override
public MixConfig map(ResultSet rs, StatementContext ctx) throws SQLException {
String scode = rs.getString("scode");
Boolean mixOnStartup = rs.getBoolean("mixOnStartup");
if(rs.wasNull()) {
mixOnStartup = null;
}
String mixToWalletFile = rs.getString("mixToWalletFile");
String mixToWalletName = rs.getString("mixToWalletName");
Integer minMixes = rs.getInt("minMixes");
if(rs.wasNull()) {
minMixes = null;
}
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes);
mixConfig.setId(rs.getLong("id"));
return mixConfig;
}
}

View file

@ -30,6 +30,9 @@ public interface WalletDao {
@CreateSqlObject
BlockTransactionDao createBlockTransactionDao();
@CreateSqlObject
MixConfigDao createMixConfigDao();
@CreateSqlObject
UtxoMixDataDao createUtxoMixDataDao();
@ -91,6 +94,8 @@ public interface WalletDao {
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId()); //.stream().collect(Collectors.toMap(BlockTransaction::getHash, Function.identity(), (existing, replacement) -> existing, LinkedHashMap::new));
wallet.updateTransactions(blockTransactions);
wallet.setMixConfig(createMixConfigDao().getForWalletId(wallet.getId()));
Map<Sha256Hash, UtxoMixData> utxoMixes = createUtxoMixDataDao().getForWalletId(wallet.getId());
wallet.getUtxoMixes().putAll(utxoMixes);
}
@ -106,6 +111,7 @@ public interface WalletDao {
createKeystoreDao().addKeystores(wallet);
createWalletNodeDao().addWalletNodes(wallet);
createBlockTransactionDao().addBlockTransactions(wallet);
createMixConfigDao().addMixConfig(wallet);
createUtxoMixDataDao().addUtxoMixData(wallet);
} finally {
setSchema(DbPersistence.DEFAULT_SCHEMA);

View file

@ -1110,7 +1110,8 @@ public class SendController extends WalletFormController implements Initializabl
public void broadcastPremix(ActionEvent event) {
//Ensure all child wallets have been saved
for(Wallet childWallet : getWalletForm().getWallet().getChildWallets()) {
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
for(Wallet childWallet : masterWallet.getChildWallets()) {
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
if(!storage.isPersisted(childWallet)) {
try {
@ -1122,7 +1123,7 @@ public class SendController extends WalletFormController implements Initializabl
}
//The WhirlpoolWallet has already been configured for the tx0 preview
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getStorage().getWalletId(masterWallet));
Map<BlockTransactionHashIndex, WalletNode> utxos = walletTransactionProperty.get().getSelectedUtxos();
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, whirlpoolProperty.get(), utxos.keySet());
tx0BroadcastService.setOnRunning(workerStateEvent -> {

View file

@ -170,7 +170,7 @@ public class UtxosController extends WalletFormController implements Initializab
Wallet wallet = getWalletForm().getWallet();
String walletId = walletForm.getWalletId();
if(!wallet.isWhirlpoolMasterWallet() && wallet.isEncrypted()) {
if(wallet.isMasterWallet() && !wallet.isWhirlpoolMasterWallet() && wallet.isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
@ -205,7 +205,7 @@ public class UtxosController extends WalletFormController implements Initializab
keyDerivationService.start();
}
} else {
if(!wallet.isWhirlpoolMasterWallet()) {
if(wallet.isMasterWallet() && !wallet.isWhirlpoolMasterWallet()) {
prepareWhirlpoolWallet(wallet);
}
@ -215,7 +215,7 @@ public class UtxosController extends WalletFormController implements Initializab
private void prepareWhirlpoolWallet(Wallet decryptedWallet) {
Whirlpool whirlpool = AppServices.get().getWhirlpool(getWalletForm().getWalletId());
whirlpool.setScode(Config.get().getScode());
whirlpool.setScode(decryptedWallet.getOrCreateMixConfig().getScode());
whirlpool.setHDWallet(getWalletForm().getWalletId(), decryptedWallet);
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
@ -227,8 +227,10 @@ public class UtxosController extends WalletFormController implements Initializab
}
private void previewPremix(Wallet wallet, Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) {
Wallet premixWallet = wallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX);
Wallet badbankWallet = wallet.getChildWallet(StandardAccount.WHIRLPOOL_BADBANK);
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
Wallet premixWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX);
Wallet badbankWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_BADBANK);
List<Payment> payments = new ArrayList<>();
try {
@ -284,6 +286,10 @@ public class UtxosController extends WalletFormController implements Initializab
});
startupService.start();
}
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
masterWallet.getOrCreateMixConfig().setMixOnStartup(Boolean.TRUE);
EventManager.get().post(new WalletMixConfigChangedEvent(masterWallet));
}
public void stopMixing(ActionEvent event) {
@ -302,6 +308,10 @@ public class UtxosController extends WalletFormController implements Initializab
//Ensure http clients are shutdown
whirlpool.shutdown();
}
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
masterWallet.getOrCreateMixConfig().setMixOnStartup(Boolean.FALSE);
EventManager.get().post(new WalletMixConfigChangedEvent(masterWallet));
}
public void exportUtxos(ActionEvent event) {

View file

@ -469,6 +469,13 @@ public class WalletForm {
}
}
@Subscribe
public void walletMixConfigChanged(WalletMixConfigChangedEvent event) {
if(event.getWallet() == wallet) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe
public void walletUtxoMixesChanged(WalletUtxoMixesChangedEvent event) {
if(event.getWallet() == wallet) {

View file

@ -69,7 +69,7 @@ public class Whirlpool {
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
public Whirlpool(Network network, HostAndPort torProxy, String sCode) {
public Whirlpool(Network network, HostAndPort torProxy) {
this.torProxy = torProxy;
this.whirlpoolServer = WhirlpoolServer.valueOf(network.getName().toUpperCase());
this.httpClientService = new JavaHttpClientService(torProxy);
@ -77,12 +77,12 @@ public class Whirlpool {
this.torClientService = new WhirlpoolTorClientService();
this.whirlpoolWalletService = new WhirlpoolWalletService();
this.config = computeWhirlpoolWalletConfig(sCode);
this.config = computeWhirlpoolWalletConfig();
WhirlpoolEventService.getInstance().register(this);
}
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(String sCode) {
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig() {
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet);
DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, dataPersister) -> new SparrowDataSource(whirlpoolWallet, bip44w, dataPersister);
@ -92,7 +92,6 @@ public class Whirlpool {
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, httpClientService, stompClientService, torClientService, serverApi, whirlpoolServer.getParams(), false);
whirlpoolWalletConfig.setDataPersisterFactory(dataPersisterFactory);
whirlpoolWalletConfig.setScode(sCode);
return whirlpoolWalletConfig;
}
@ -343,6 +342,14 @@ public class Whirlpool {
config.setScode(scode);
}
public void setExternalDestination(ExternalDestination externalDestination) {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
throw new IllegalStateException("Cannot set external destination while WhirlpoolWallet is running");
}
config.setExternalDestination(externalDestination);
}
public boolean isMixing() {
return mixingProperty.get();
}

View file

@ -4,9 +4,12 @@ import com.samourai.whirlpool.client.tx0.Tx0Preview;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.event.WalletMixConfigChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
@ -72,6 +75,7 @@ public class WhirlpoolController {
private String walletId;
private Wallet wallet;
private MixConfig mixConfig;
private List<UtxoEntry> utxoEntries;
private final ObjectProperty<Tx0Preview> tx0PreviewProperty = new SimpleObjectProperty<>(null);
@ -79,6 +83,7 @@ public class WhirlpoolController {
this.walletId = walletId;
this.wallet = wallet;
this.utxoEntries = utxoEntries;
this.mixConfig = wallet.isMasterWallet() ? wallet.getOrCreateMixConfig() : wallet.getMasterWallet().getOrCreateMixConfig();
step1.managedProperty().bind(step1.visibleProperty());
step2.managedProperty().bind(step2.visibleProperty());
@ -89,12 +94,13 @@ public class WhirlpoolController {
step3.setVisible(false);
step4.setVisible(false);
scode.setText(Config.get().getScode() == null ? "" : Config.get().getScode());
scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode());
scode.textProperty().addListener((observable, oldValue, newValue) -> {
Config.get().setScode(newValue);
mixConfig.setScode(newValue);
EventManager.get().post(new WalletMixConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()));
});
if(Config.get().getScode() != null) {
if(mixConfig.getScode() != null) {
step1.setVisible(false);
step3.setVisible(true);
}
@ -193,7 +199,7 @@ public class WhirlpoolController {
List<Pool> availablePools = poolsService.getValue().stream().filter(pool1 -> totalUtxoValue >= (pool1.getPremixValueMin() + pool1.getFeeValue())).toList();
if(availablePools.isEmpty()) {
pool.setVisible(false);
OptionalLong optMinValue = poolsService.getValue().stream().mapToLong(Pool::getMustMixBalanceMin).min();
OptionalLong optMinValue = poolsService.getValue().stream().mapToLong(pool1 -> pool1.getPremixValueMin() + pool1.getFeeValue()).min();
if(optMinValue.isPresent()) {
String satsValue = String.format(Locale.ENGLISH, "%,d", optMinValue.getAsLong()) + " sats";
String btcValue = CoinLabel.BTC_FORMAT.format((double)optMinValue.getAsLong() / Transaction.SATOSHIS_PER_BITCOIN) + " BTC";
@ -224,17 +230,20 @@ public class WhirlpoolController {
}
private void fetchTx0Preview(Pool pool) {
if(Config.get().getScode() == null) {
Config.get().setScode("");
if(mixConfig.getScode() == null) {
mixConfig.setScode("");
EventManager.get().post(new WalletMixConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()));
}
Whirlpool whirlpool = AppServices.get().getWhirlpool(walletId);
whirlpool.setScode(Config.get().getScode());
whirlpool.setScode(mixConfig.getScode());
Whirlpool.Tx0PreviewService tx0PreviewService = new Whirlpool.Tx0PreviewService(whirlpool, wallet, pool, utxoEntries);
tx0PreviewService.setOnRunning(workerStateEvent -> {
nbOutputsBox.setVisible(true);
nbOutputsLoading.setText("Calculating...");
nbOutputs.setVisible(false);
discountFeeBox.setVisible(false);
});
tx0PreviewService.setOnSucceeded(workerStateEvent -> {
Tx0Preview tx0Preview = tx0PreviewService.getValue();

View file

@ -54,7 +54,8 @@ public class WhirlpoolDialog extends Dialog<Tx0Preview> {
backButton.managedProperty().bind(backButton.visibleProperty());
previewButton.managedProperty().bind(previewButton.visibleProperty());
if(Config.get().getScode() == null) {
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
if(masterWallet.getOrCreateMixConfig().getScode() == null) {
backButton.setDisable(true);
}
previewButton.visibleProperty().bind(nextButton.visibleProperty().not());

View file

@ -1 +1,2 @@
create table mixConfig (id identity not null, scode varchar(255), mixOnStartup boolean, mixToWalletFile varchar(1024), mixToWalletName varchar(255), minMixes integer, wallet bigint not null);
create table utxoMixData (id identity not null, hash binary(32) not null, mixesDone integer not null default 0, expired bigint, wallet bigint not null);