mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 03:56:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
599880ea5c | ||
|
|
d625bab02e | ||
|
|
1676676e06 | ||
|
|
f7e603118f | ||
|
|
f6fd889712 | ||
|
|
21d91d3d10 | ||
|
|
f1cddc28e7 | ||
|
|
1887e1c7b0 | ||
|
|
3e870f362d | ||
|
|
665d70b845 | ||
|
|
c2cbe62a5a | ||
|
|
c6b6e74515 | ||
|
|
8ddc494b53 | ||
|
|
33f439f49a | ||
|
|
d68ab40c94 | ||
|
|
31346e2afa | ||
|
|
c407a41475 | ||
|
|
8baa8e2e96 | ||
|
|
72adbe44a7 | ||
|
|
dd64c18c21 | ||
|
|
2354b061a9 | ||
|
|
a5050117a3 | ||
|
|
f245b57022 | ||
|
|
d3752a856b | ||
|
|
fe7dba6d83 | ||
|
|
2d0a94d024 | ||
|
|
41146310d6 | ||
|
|
a167f6aedb | ||
|
|
0fed7c45ee | ||
|
|
5a0df265bc | ||
|
|
646b8b0e65 | ||
|
|
c9b40b1973 | ||
|
|
9ec5b6ce26 | ||
|
|
93893111c6 | ||
|
|
3600d32ffd |
+12
-3
@@ -7,7 +7,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.8.5'
|
||||
def sparrowVersion = '1.9.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -124,8 +124,7 @@ dependencies {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.6')
|
||||
implementation('io.samourai.code.wallet:java-http-client:2.0.2')
|
||||
implementation('org.eclipse.jetty:jetty-client:9.4.54.v20240208')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
@@ -291,6 +290,16 @@ jlink {
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jlink.finalizedBy "addUserWritePermission"
|
||||
|
||||
task addUserWritePermission(type: Exec) {
|
||||
if(os.windows) {
|
||||
commandLine 'icacls', "$buildDir\\image\\legal", '/grant', 'Users:(OI)(CI)F', '/T'
|
||||
} else {
|
||||
commandLine 'chmod', '-R', 'u+w', "$buildDir/image/legal"
|
||||
}
|
||||
}
|
||||
|
||||
task removeGroupWritePermission(type: Exec) {
|
||||
commandLine 'chmod', '-R', 'g-w', "$buildDir/jpackage/Sparrow"
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.8.4"
|
||||
GIT_TAG="1.8.6"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: b128bb895d...143d28166a
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.8.5</string>
|
||||
<string>1.9.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -27,10 +27,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
@@ -187,9 +184,6 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem sweepPrivateKey;
|
||||
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@@ -215,10 +209,14 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private UnlabeledToggleSwitch serverToggle;
|
||||
|
||||
private Storage.KeyDerivationService keyDerivationService;
|
||||
|
||||
private PauseTransition wait;
|
||||
|
||||
private Timeline statusTimeline;
|
||||
|
||||
private SearchWalletDialog searchWalletDialog;
|
||||
|
||||
private SendToManyDialog sendToManyDialog;
|
||||
|
||||
private DownloadVerifierDialog downloadVerifierDialog;
|
||||
@@ -280,6 +278,15 @@ public class AppController implements Initializable {
|
||||
void initializeView() {
|
||||
setPlatformApplicationMenu();
|
||||
|
||||
rootStack.getScene().getWindow().setOnHiding(windowEvent -> {
|
||||
if(searchWalletDialog != null && searchWalletDialog.isShowing()) {
|
||||
searchWalletDialog.close();
|
||||
}
|
||||
if(sendToManyDialog != null && sendToManyDialog.isShowing()) {
|
||||
sendToManyDialog.close();
|
||||
}
|
||||
});
|
||||
|
||||
rootStack.setOnDragOver(event -> {
|
||||
if(event.getGestureSource() != rootStack && event.getDragboard().hasFiles()) {
|
||||
event.acceptTransferModes(TransferMode.LINK);
|
||||
@@ -410,10 +417,6 @@ public class AppController implements Initializable {
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
@@ -1311,7 +1314,7 @@ public class AppController implements Initializable {
|
||||
log.error("Error saving imported wallet", e);
|
||||
}
|
||||
} else {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get());
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
@@ -1342,11 +1345,13 @@ public class AppController implements Initializable {
|
||||
if(key != null) {
|
||||
key.clear();
|
||||
}
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.END, "Failed"));
|
||||
showErrorDialog("Error encrypting wallet", keyDerivationService.getException().getMessage());
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(Storage.getWalletFile(wallet.getName()).getAbsolutePath(), TimedEvent.Action.START, "Encrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
@@ -1419,7 +1424,6 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit);
|
||||
sendToManyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
@@ -1445,74 +1449,6 @@ public class AppController implements Initializable {
|
||||
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||
}
|
||||
|
||||
public void findMixingPartner(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = selectedWalletForm.getStorage();
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> findMixingPartner(null));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
} else {
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
@@ -1605,13 +1541,46 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void searchWallets(List<WalletForm> walletForms) {
|
||||
SearchWalletDialog searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
searchWalletDialog.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
if(searchWalletDialog != null) {
|
||||
if(!searchWalletDialog.getWalletForms().equals(walletForms)) {
|
||||
searchWalletDialog.close();
|
||||
} else {
|
||||
Stage stage = (Stage)searchWalletDialog.getDialogPane().getScene().getWindow();
|
||||
stage.setAlwaysOnTop(true);
|
||||
stage.setAlwaysOnTop(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
searchWalletDialog = new SearchWalletDialog(walletForms);
|
||||
searchWalletDialog.initModality(Modality.NONE);
|
||||
Optional<Entry> optEntry = searchWalletDialog.showAndWait();
|
||||
if(optEntry.isPresent()) {
|
||||
Entry entry = optEntry.get();
|
||||
EventManager.get().post(new FunctionActionEvent(entry.getWalletFunction(), entry.getWallet()));
|
||||
Platform.runLater(() -> EventManager.get().post(new SelectEntryEvent(entry)));
|
||||
}
|
||||
searchWalletDialog = null;
|
||||
});
|
||||
}
|
||||
|
||||
public void showAllWalletsSummary(ActionEvent event) {
|
||||
List<List<WalletForm>> allWalletForms = new ArrayList<>();
|
||||
for(Tab tab : tabs.getTabs()) {
|
||||
if(tab.getUserData() instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)tab.getContent();
|
||||
allWalletForms.add(subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm())
|
||||
.filter(walletForm -> walletForm.getWallet().isValid() && !walletForm.isLocked()).collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
|
||||
if(allWalletForms.isEmpty() || allWalletForms.stream().allMatch(List::isEmpty)) {
|
||||
showErrorDialog("No wallets", "There are no open and unlocked wallets to summarize.");
|
||||
} else {
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(allWalletForms);
|
||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||
walletSummaryDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1623,7 +1592,7 @@ public class AppController implements Initializable {
|
||||
TabPane subTabs = (TabPane) selectedTab.getContent();
|
||||
List<WalletForm> walletForms = subTabs.getTabs().stream().map(subTab -> ((WalletTabData)subTab.getUserData()).getWalletForm()).collect(Collectors.toList());
|
||||
if(!walletForms.isEmpty()) {
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(walletForms);
|
||||
WalletSummaryDialog walletSummaryDialog = new WalletSummaryDialog(List.of(walletForms));
|
||||
walletSummaryDialog.initOwner(rootStack.getScene().getWindow());
|
||||
walletSummaryDialog.showAndWait();
|
||||
}
|
||||
@@ -1679,14 +1648,6 @@ public class AppController implements Initializable {
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setClosable(true);
|
||||
tab.setOnCloseRequest(event -> {
|
||||
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TabPane subTabs = new TabPane();
|
||||
subTabs.setSide(Side.LEFT);
|
||||
@@ -2180,7 +2141,7 @@ public class AppController implements Initializable {
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
@@ -2190,7 +2151,7 @@ public class AppController implements Initializable {
|
||||
deleteStorage(storage, true);
|
||||
} finally {
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
@@ -2203,6 +2164,7 @@ public class AppController implements Initializable {
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
@@ -2505,7 +2467,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
@@ -2517,7 +2478,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2543,7 +2503,6 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,8 +24,6 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -95,11 +93,7 @@ public class AppServices {
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
private final InteractionServices interactionServices;
|
||||
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
@@ -188,8 +182,6 @@ public class AppServices {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -502,7 +494,7 @@ public class AppServices {
|
||||
public static Proxy getProxy(String proxyCircuitId) {
|
||||
Config config = Config.get();
|
||||
Proxy proxy = null;
|
||||
if(config.isUseProxy()) {
|
||||
if(config.isUseProxy() && config.getProxyServer() != null) {
|
||||
HostAndPort proxyHostAndPort = HostAndPort.fromString(config.getProxyServer());
|
||||
InetSocketAddress proxyAddress = new InetSocketAddress(proxyHostAndPort.getHost(), proxyHostAndPort.getPortOrDefault(ProxyTcpOverTlsTransport.DEFAULT_PROXY_PORT));
|
||||
proxy = new Proxy(Proxy.Type.SOCKS, proxyAddress);
|
||||
@@ -534,14 +526,6 @@ public class AppServices {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WhirlpoolServices getWhirlpoolServices() {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
@@ -923,6 +907,7 @@ public class AppServices {
|
||||
}
|
||||
|
||||
if(openWindow instanceof Stage) {
|
||||
((Stage)openWindow).setIconified(false);
|
||||
((Stage)openWindow).setAlwaysOnTop(true);
|
||||
((Stage)openWindow).setAlwaysOnTop(false);
|
||||
}
|
||||
@@ -1094,6 +1079,37 @@ public class AppServices {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getStandardAccountType() != null
|
||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
||||
}
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1;
|
||||
}
|
||||
|
||||
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||
List<Wallet> childWallets = new ArrayList<>();
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
childWallets.add(childWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.8.5";
|
||||
public static final String APP_VERSION = "1.9.0";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@@ -5,17 +5,13 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.*;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
@@ -46,12 +42,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
standardAccountCombo = new ComboBox<>();
|
||||
standardAccountCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
List<Integer> existingIndexes = new ArrayList<>();
|
||||
Set<Integer> existingIndexes = new LinkedHashSet<>();
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
existingIndexes.add(masterWallet.getAccountIndex());
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
existingIndexes.add(childWallet.getAccountIndex());
|
||||
Optional<StandardAccount> optStdAcc = Arrays.stream(StandardAccount.values()).filter(stdacc -> stdacc.getName().equals(childWallet.getName())).findFirst();
|
||||
optStdAcc.ifPresent(standardAccount -> existingIndexes.add(standardAccount.getAccountNumber()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,15 +60,14 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
||||
}
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
|
||||
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
|
||||
@@ -10,6 +10,7 @@ import javafx.beans.value.ChangeListener;
|
||||
import javafx.event.EventHandler;
|
||||
import javafx.scene.Cursor;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
@@ -52,6 +53,7 @@ public class CopyableTextField extends CustomTextField {
|
||||
selectedTextProperty().removeListener(selectionListener);
|
||||
}
|
||||
});
|
||||
setContextMenu(new ContextMenu());
|
||||
}
|
||||
|
||||
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
|
||||
|
||||
@@ -606,7 +606,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
}
|
||||
|
||||
private static class TransactionContextMenu extends ContextMenu {
|
||||
protected static class TransactionContextMenu extends ContextMenu {
|
||||
public TransactionContextMenu(String date, BlockTransaction blockTransaction) {
|
||||
MenuItem viewTransaction = new MenuItem("View Transaction");
|
||||
viewTransaction.setGraphic(getViewTransactionGlyph());
|
||||
|
||||
@@ -13,6 +13,8 @@ import java.util.stream.Collectors;
|
||||
import static com.sparrowwallet.sparrow.AppServices.*;
|
||||
|
||||
public class FeeRangeSlider extends Slider {
|
||||
private static final double FEE_RATE_SCROLL_INCREMENT = 0.01;
|
||||
|
||||
public FeeRangeSlider() {
|
||||
super(0, FEE_RATES_RANGE.size() - 1, 0);
|
||||
setMajorTickUnit(1);
|
||||
@@ -45,6 +47,18 @@ public class FeeRangeSlider extends Slider {
|
||||
updateMaxFeeRange(newValue.doubleValue());
|
||||
}
|
||||
});
|
||||
|
||||
setOnScroll(event -> {
|
||||
if(event.getDeltaY() != 0) {
|
||||
double newFeeRate = getFeeRate() + (event.getDeltaY() > 0 ? FEE_RATE_SCROLL_INCREMENT : -FEE_RATE_SCROLL_INCREMENT);
|
||||
if(newFeeRate < LONG_FEE_RATES_RANGE.get(0)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(0);
|
||||
} else if(newFeeRate > LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1)) {
|
||||
newFeeRate = LONG_FEE_RATES_RANGE.get(LONG_FEE_RATES_RANGE.size() - 1);
|
||||
}
|
||||
setFeeRate(newFeeRate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public double getFeeRate() {
|
||||
|
||||
@@ -240,6 +240,8 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
javafx.application.Platform.runLater(passwordField::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletImportEvent;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreFileImport;
|
||||
import javafx.application.Platform;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
@@ -38,6 +39,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private final KeystoreFileImport importer;
|
||||
private String fileName;
|
||||
private byte[] fileBytes;
|
||||
private String password;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
@@ -46,6 +48,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
|
||||
protected void importFile(String fileName, InputStream inputStream, String password) throws ImportException {
|
||||
this.fileName = fileName;
|
||||
this.password = password;
|
||||
|
||||
List<ScriptType> scriptTypes = ScriptType.getAddressableScriptTypes(PolicyType.SINGLE);
|
||||
if(wallets != null && !wallets.isEmpty()) {
|
||||
@@ -83,7 +86,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
EventManager.get().post(new WalletImportEvent(wallet));
|
||||
} else {
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(fileBytes);
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, "");
|
||||
Keystore keystore = importer.getKeystore(scriptType, bais, password);
|
||||
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(Files.getNameWithoutExtension(fileName));
|
||||
@@ -151,6 +154,8 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
Platform.runLater(scriptTypeComboBox::requestFocus);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -41,162 +25,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason.isError()) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
|
||||
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,10 +79,6 @@ public class PayNymAvatar extends StackPane {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
|
||||
@@ -81,10 +81,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@ public class PaymentCodeTextField extends CopyableTextField {
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
|
||||
@@ -42,7 +42,7 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
|
||||
private static final int MIN_FRAGMENT_LENGTH = 10;
|
||||
|
||||
private static final int ANIMATION_PERIOD_MILLIS = 200;
|
||||
private static final double ANIMATION_PERIOD_MILLIS = 200d;
|
||||
|
||||
private static final int DEFAULT_QR_SIZE = 580;
|
||||
private static final int REDUCED_QR_SIZE = 520;
|
||||
@@ -100,6 +100,16 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
qrImageView = new ImageView();
|
||||
stackPane.getChildren().add(qrImageView);
|
||||
|
||||
qrImageView.setOnScroll(scrollEvent -> {
|
||||
if(animateQRService != null && animateQRService.isRunning() && scrollEvent.getDeltaY() != 0) {
|
||||
Duration duration = animateQRService.getPeriod();
|
||||
Duration newDuration = scrollEvent.getDeltaY() > 0 ? duration.multiply(1.1) : duration.multiply(0.9);
|
||||
if(newDuration.lessThan(Duration.millis(ANIMATION_PERIOD_MILLIS*10)) && newDuration.greaterThan(Duration.millis(ANIMATION_PERIOD_MILLIS/2))) {
|
||||
animateQRService.setPeriod(newDuration);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
|
||||
nextPart();
|
||||
|
||||
@@ -1,18 +1,27 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ReadOnlyObjectWrapper;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.textfield.TextFields;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -20,10 +29,11 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
|
||||
public class SearchWalletDialog extends Dialog<Entry> {
|
||||
private static final Logger log = LoggerFactory.getLogger(SearchWalletDialog.class);
|
||||
@@ -48,7 +58,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/wallet.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("search.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet");
|
||||
dialogPane.setHeaderText(showWallet ? "Search All Wallets" : "Search Wallet " + walletForms.get(0).getMasterWallet().getName());
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
@@ -81,6 +91,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
results.setEditable(true);
|
||||
|
||||
if(showWallet) {
|
||||
TreeTableColumn<Entry, String> walletColumn = new TreeTableColumn<>("Wallet");
|
||||
@@ -117,7 +128,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
|
||||
return param.getValue().getValue().labelProperty();
|
||||
});
|
||||
labelCol.setCellFactory(p -> new SearchLabelCell());
|
||||
labelCol.setCellFactory(p -> new LabelCell());
|
||||
results.getColumns().add(labelCol);
|
||||
|
||||
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
|
||||
@@ -130,12 +141,20 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
vBox.getChildren().addAll(form, results);
|
||||
dialogPane.setContent(vBox);
|
||||
|
||||
ButtonType exportButtonType = new ButtonType("Export CSV", ButtonBar.ButtonData.LEFT);
|
||||
ButtonType showButtonType = new javafx.scene.control.ButtonType("Show", ButtonBar.ButtonData.APPLY);
|
||||
ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType, showButtonType);
|
||||
dialogPane.getButtonTypes().addAll(exportButtonType, cancelButtonType, showButtonType);
|
||||
|
||||
Button showButton = (Button) dialogPane.lookupButton(showButtonType);
|
||||
Button exportButton = (Button)dialogPane.lookupButton(exportButtonType);
|
||||
exportButton.setGraphic(GlyphUtils.getDownArrowGlyph());
|
||||
exportButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
exportResults(showWallet);
|
||||
});
|
||||
|
||||
Button showButton = (Button)dialogPane.lookupButton(showButtonType);
|
||||
showButton.setDefaultButton(true);
|
||||
showButton.setDisable(true);
|
||||
|
||||
@@ -147,7 +166,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
});
|
||||
|
||||
search.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
searchWallets(newValue.toLowerCase(Locale.ROOT));
|
||||
searchWallets(newValue);
|
||||
});
|
||||
|
||||
setResizable(true);
|
||||
@@ -157,42 +176,41 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
Platform.runLater(search::requestFocus);
|
||||
}
|
||||
|
||||
private void searchWallets(String searchText) {
|
||||
List<Entry> matchingEntries = new ArrayList<>();
|
||||
public List<WalletForm> getWalletForms() {
|
||||
return walletForms;
|
||||
}
|
||||
|
||||
if(!searchText.isEmpty()) {
|
||||
Long searchValue = getSearchValue(searchText);
|
||||
Address searchAddress = getSearchAddress(searchText);
|
||||
private void searchWallets(String searchPhrase) {
|
||||
Set<Entry> matchingEntries = new LinkedHashSet<>();
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(!searchPhrase.isEmpty()) {
|
||||
Set<String> searchWords = new LinkedHashSet<>(Arrays.stream(searchPhrase.split("\\s+"))
|
||||
.filter(text -> isAddress(text) || isHash(text) || isHashIndex(text)).toList());
|
||||
String freeText = removeOccurrences(searchPhrase, searchWords).trim();
|
||||
if(!freeText.isEmpty()) {
|
||||
searchWords.add(freeText);
|
||||
}
|
||||
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
for(String searchText : searchWords) {
|
||||
Long searchValue = getSearchValue(searchText);
|
||||
Address searchAddress = getSearchAddress(searchText);
|
||||
searchText = searchText.toLowerCase(Locale.ROOT);
|
||||
|
||||
for(WalletForm walletForm : walletForms) {
|
||||
WalletTransactionsEntry walletTransactionsEntry = walletForm.getWalletTransactionsEntry();
|
||||
for(Entry entry : walletTransactionsEntry.getChildren()) {
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
if(transactionEntry.getBlockTransaction().getHash().toString().equals(searchText) ||
|
||||
(transactionEntry.getLabel() != null && transactionEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(transactionEntry.getValue() != null && searchValue != null && Math.abs(transactionEntry.getValue()) == searchValue) ||
|
||||
(searchAddress != null && transactionEntry.getBlockTransaction().getTransaction().getOutputs().stream().map(output -> output.getScript().getToAddress()).filter(Objects::nonNull).anyMatch(address -> address.equals(searchAddress)))) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
|
||||
NodeEntry purposeEntry = walletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
@@ -203,22 +221,38 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
for(WalletForm nestedWalletForm : walletForm.getNestedWalletForms()) {
|
||||
for(KeyPurpose keyPurpose : nestedWalletForm.getWallet().getWalletKeyPurposes()) {
|
||||
NodeEntry purposeEntry = nestedWalletForm.getNodeEntry(keyPurpose);
|
||||
for(Entry entry : purposeEntry.getChildren()) {
|
||||
if(entry instanceof NodeEntry nodeEntry) {
|
||||
if(nodeEntry.getAddress().toString().toLowerCase(Locale.ROOT).contains(searchText) ||
|
||||
(nodeEntry.getLabel() != null && nodeEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(nodeEntry.getValue() != null && searchValue != null && Math.abs(nodeEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WalletUtxosEntry walletUtxosEntry = walletForm.getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
if(hashIndexEntry.getBlockTransaction().getHash().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
hashIndexEntry.getHashIndex().toString().toLowerCase(Locale.ROOT).equals(searchText) ||
|
||||
(hashIndexEntry.getLabel() != null && hashIndexEntry.getLabel().toLowerCase(Locale.ROOT).contains(searchText)) ||
|
||||
(hashIndexEntry.getValue() != null && searchValue != null && Math.abs(hashIndexEntry.getValue()) == searchValue)) {
|
||||
matchingEntries.add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), matchingEntries);
|
||||
SearchWalletEntry rootEntry = new SearchWalletEntry(walletForms.iterator().next().getWallet(), new ArrayList<>(matchingEntries));
|
||||
RecursiveTreeItem<Entry> rootItem = new RecursiveTreeItem<>(rootEntry, Entry::getChildren);
|
||||
results.setRoot(rootItem);
|
||||
}
|
||||
@@ -239,6 +273,95 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isAddress(String text) {
|
||||
try {
|
||||
Address.fromString(text);
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isHash(String text) {
|
||||
return text.length() == 64 && Utils.isHex(text);
|
||||
}
|
||||
|
||||
private boolean isHashIndex(String text) {
|
||||
String[] parts = text.split(":");
|
||||
if(parts.length == 2 && isHash(parts[0])) {
|
||||
try {
|
||||
Integer.parseInt(parts[1]);
|
||||
return true;
|
||||
} catch(NumberFormatException e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private String removeOccurrences(String inputString, Collection<String> stringsToRemove) {
|
||||
for(String str : stringsToRemove) {
|
||||
inputString = inputString.replaceAll("(?i)" + str, "");
|
||||
}
|
||||
|
||||
return inputString;
|
||||
}
|
||||
|
||||
public void exportResults(boolean showWallet) {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Export search results to CSV");
|
||||
fileChooser.setInitialFileName(getDialogPane().getHeaderText() + ".csv");
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
List<String> headers = new ArrayList<>(List.of("Wallet", "Account", "Type", "Date", "Txid / Address / Output", "Label", "Value"));
|
||||
if(!showWallet) {
|
||||
headers.remove(0);
|
||||
}
|
||||
writer.writeRecord(headers.toArray(new String[0]));
|
||||
for(TreeItem<Entry> item : results.getRoot().getChildren()) {
|
||||
Entry entry = item.getValue();
|
||||
if(showWallet) {
|
||||
writer.write(entry.getWallet().getMasterName());
|
||||
}
|
||||
writer.write(entry.getWallet().getDisplayName());
|
||||
writer.write(entry.getEntryType());
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
writer.write(transactionEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(transactionEntry.getBlockTransaction().getDate()));
|
||||
writer.write(transactionEntry.getBlockTransaction().getHash().toString());
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
writer.write("");
|
||||
writer.write(nodeEntry.getAddress().toString());
|
||||
} else if(entry instanceof HashIndexEntry hashIndexEntry) {
|
||||
writer.write(hashIndexEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : EntryCell.DATE_FORMAT.format(hashIndexEntry.getBlockTransaction().getDate()));
|
||||
writer.write(hashIndexEntry.getHashIndex().toString());
|
||||
} else {
|
||||
writer.write("");
|
||||
writer.write("");
|
||||
}
|
||||
writer.write(entry.getLabel());
|
||||
writer.write(getCoinValue(entry.getValue() == null ? 0 : entry.getValue()));
|
||||
writer.endRecord();
|
||||
}
|
||||
writer.close();
|
||||
} catch(IOException e) {
|
||||
log.error("Error exporting search results as CSV", e);
|
||||
AppServices.showErrorDialog("Error exporting search results as CSV", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getCoinValue(Long value) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return BitcoinUnit.BTC.equals(results.getBitcoinUnit()) ? format.tableFormatBtcValue(value) : String.format(Locale.ENGLISH, "%d", value);
|
||||
}
|
||||
|
||||
private static class SearchWalletEntry extends Entry {
|
||||
public SearchWalletEntry(Wallet wallet, List<Entry> entries) {
|
||||
super(wallet, wallet.getName(), entries);
|
||||
@@ -264,15 +387,19 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
@Override
|
||||
protected void updateItem(Entry entry, boolean empty) {
|
||||
super.updateItem(entry, empty);
|
||||
setContextMenu(null);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SearchLabelCell extends LabelCell {
|
||||
@Override
|
||||
public void updateItem(String label, boolean empty) {
|
||||
super.updateItem(label, empty);
|
||||
setContextMenu(null);
|
||||
ContextMenu copyMenu;
|
||||
if(entry instanceof TransactionEntry transactionEntry) {
|
||||
copyMenu = new TransactionContextMenu(getText(), transactionEntry.getBlockTransaction());
|
||||
} else if(entry instanceof NodeEntry nodeEntry) {
|
||||
copyMenu = new AddressContextMenu(nodeEntry.getAddress(), nodeEntry.getOutputDescriptor(), null, false, null);
|
||||
} else if(entry instanceof UtxoEntry utxoEntry) {
|
||||
copyMenu = new HashIndexEntryContextMenu(null, utxoEntry);
|
||||
} else {
|
||||
copyMenu = new ContextMenu();
|
||||
}
|
||||
copyMenu.getItems().removeIf(menuItem -> !menuItem.getText().startsWith("Copy"));
|
||||
setContextMenu(copyMenu);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import javafx.scene.control.ContextMenu;
|
||||
import javafx.scene.control.MenuItem;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.ClipboardContent;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
|
||||
public class SelectableCodeArea extends CodeArea {
|
||||
public SelectableCodeArea() {
|
||||
super();
|
||||
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem copy = new MenuItem("Copy");
|
||||
copy.setDisable(true);
|
||||
copy.setOnAction(event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getSelectedText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
MenuItem copyAll = new MenuItem("Copy All");
|
||||
copyAll.setOnAction(event -> {
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(getText());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
contextMenu.getItems().addAll(copy, copyAll);
|
||||
setContextMenu(contextMenu);
|
||||
|
||||
selectedTextProperty().addListener((observable, oldValue, newValue) -> {
|
||||
copy.setDisable(newValue.isEmpty());
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -12,11 +12,9 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -246,19 +244,9 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
if(addUserSet && displayedUtxoSets.size() == 1) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
|
||||
displayedUtxoSets.add(addUserUtxoSet);
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||
@@ -339,11 +327,9 @@ public class TransactionDiagram extends GridPane {
|
||||
double setHeight = (height / numSets) - 5;
|
||||
for(int set = 0; set < numSets; set++) {
|
||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||
if(externalUserSet || addUserSet) {
|
||||
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||
String tooltipText = addUserSet ? "Click to add a mix partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mix partner" : ""));
|
||||
if(externalUserSet) {
|
||||
Glyph bracketGlyph = walletTx.isCoinControlUsed() ? getLockGlyph() : getCoinsGlyph();
|
||||
String tooltipText = walletTx.getWallet().getFullDisplayName();
|
||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
} else {
|
||||
@@ -474,14 +460,6 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
@@ -570,7 +548,7 @@ public class TransactionDiagram extends GridPane {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||
continue;
|
||||
@@ -952,46 +930,10 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
userAddGlyph.setFontSize(12);
|
||||
userAddGlyph.setOnMouseEntered(event -> {
|
||||
userAddGlyph.setFontSize(18);
|
||||
});
|
||||
userAddGlyph.setOnMouseExited(event -> {
|
||||
userAddGlyph.setFontSize(12);
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
|
||||
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||
private Glyph getCoinsGlyph() {
|
||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
if(allowReplacement) {
|
||||
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||
coinsGlyph.setOnMouseEntered(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||
coinsGlyph.setFontSize(18);
|
||||
});
|
||||
coinsGlyph.setOnMouseExited(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
}
|
||||
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
@@ -1094,20 +1036,6 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final boolean required;
|
||||
|
||||
public AddUserBlockTransactionHashIndex(boolean required) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Add Mix Partner" + (required ? "" : "?");
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
|
||||
@@ -19,9 +19,10 @@ import javafx.scene.shape.Circle;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.girod.javafx.svgimage.SVGImage;
|
||||
import org.girod.javafx.svgimage.SVGLoader;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.net.URLStreamHandler;
|
||||
@@ -137,7 +138,7 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
|
||||
private void addWalletIcon(String walletId) {
|
||||
Image image = new Image(PROTOCOL + ":" + walletId + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
Image image = new Image(PROTOCOL + ":" + walletId.replaceAll(" ", "%20").replaceAll("#", "%23") + "?" + QUERY, WIDTH, HEIGHT, true, false);
|
||||
getChildren().clear();
|
||||
Circle circle = new Circle(getPrefWidth() / 2,getPrefHeight() / 2,getPrefWidth() / 2);
|
||||
circle.setFill(new ImagePattern(image));
|
||||
@@ -176,6 +177,8 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
|
||||
public static class WalletIconStreamHandler extends URLStreamHandler {
|
||||
private static final Logger log = LoggerFactory.getLogger(WalletIconStreamHandler.class);
|
||||
|
||||
@Override
|
||||
protected URLConnection openConnection(URL url) throws IOException {
|
||||
return new URLConnection(url) {
|
||||
@@ -186,17 +189,19 @@ public class WalletIcon extends StackPane {
|
||||
|
||||
@Override
|
||||
public InputStream getInputStream() throws IOException {
|
||||
String walletId = url.getPath();
|
||||
String walletId = url.getPath().replaceAll("%20", " ").replaceAll("%23", "#");
|
||||
String query = url.getQuery();
|
||||
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet == null) {
|
||||
throw new IOException("Cannot find wallet for wallet id " + walletId);
|
||||
log.warn("Cannot find wallet for wallet id " + walletId);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
if(query.startsWith(QUERY)) {
|
||||
if(wallet.getWalletConfig() == null || wallet.getWalletConfig().getIconData() == null) {
|
||||
throw new IOException("No icon data for " + walletId);
|
||||
log.warn("No icon data for " + walletId);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
ByteArrayInputStream bais = new ByteArrayInputStream(wallet.getWalletConfig().getIconData());
|
||||
@@ -207,7 +212,12 @@ public class WalletIcon extends StackPane {
|
||||
}
|
||||
}
|
||||
|
||||
throw new MalformedURLException("Cannot load url " + url);
|
||||
log.warn("Cannot load url " + url);
|
||||
return getFallbackIconStream();
|
||||
}
|
||||
|
||||
private static InputStream getFallbackIconStream() {
|
||||
return WalletIconStreamHandler.class.getResourceAsStream("/image/sparrow.png");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -51,7 +51,8 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(), new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new Jade(), new KeystoneSinglesig(), new PassportSinglesig(),
|
||||
new GordianSeedTool(), new SeedSigner(), new SpecterDIY(), new Krux(), new AirGapVault(), new Samourai());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
if(!importer.isDeprecated() || Config.get().isShowDeprecatedImportExport()) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
@@ -59,7 +60,8 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
}
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig()));
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(),
|
||||
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig()));
|
||||
if(!selectedWalletForms.isEmpty()) {
|
||||
walletImporters.add(new WalletLabels(selectedWalletForms));
|
||||
}
|
||||
|
||||
@@ -18,17 +18,21 @@ import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class WalletSummaryDialog extends Dialog<Void> {
|
||||
public WalletSummaryDialog(List<WalletForm> walletForms) {
|
||||
if(walletForms.isEmpty()) {
|
||||
public WalletSummaryDialog(List<List<WalletForm>> summaryWalletFormsList) {
|
||||
List<List<WalletForm>> walletFormsList = new ArrayList<>(summaryWalletFormsList);
|
||||
walletFormsList.removeIf(List::isEmpty);
|
||||
if(walletFormsList.isEmpty()) {
|
||||
throw new IllegalArgumentException("No wallets selected to summarize");
|
||||
}
|
||||
|
||||
Wallet masterWallet = walletForms.get(0).getMasterWallet();
|
||||
boolean allOpenWallets = walletFormsList.size() > 1;
|
||||
List<Wallet> masterWallets = walletFormsList.stream().map(walletForms -> walletForms.get(0).getMasterWallet()).toList();
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
@@ -37,7 +41,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("wallet/transactions.css").toExternalForm());
|
||||
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
dialogPane.setHeaderText("Wallet Summary for " + masterWallet.getName());
|
||||
dialogPane.setHeaderText("Wallet Summary for " + (allOpenWallets ? "All Open Wallets" : masterWallets.get(0).getName()));
|
||||
|
||||
Image image = new Image("image/sparrow-small.png", 50, 50, false, false);
|
||||
if(!image.isError()) {
|
||||
@@ -64,7 +68,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
});
|
||||
balanceColumn.setCellFactory(p -> new CoinCell());
|
||||
table.getColumns().add(balanceColumn);
|
||||
table.setUnitFormat(masterWallet);
|
||||
table.setUnitFormat(masterWallets.get(0));
|
||||
|
||||
CurrencyRate currencyRate = AppServices.getFiatCurrencyExchangeRate();
|
||||
if(currencyRate != null && currencyRate.isAvailable() && Config.get().getExchangeSource() != ExchangeSource.NONE) {
|
||||
@@ -77,12 +81,20 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
table.setCurrencyRate(currencyRate);
|
||||
}
|
||||
|
||||
SummaryEntry rootEntry = new SummaryEntry(walletForms);
|
||||
Entry rootEntry = allOpenWallets ? new AllSummaryEntry(walletFormsList) : new SummaryEntry(walletFormsList.get(0));
|
||||
TreeItem<Entry> rootItem = new TreeItem<>(rootEntry);
|
||||
for(Entry childEntry : rootEntry.getChildren()) {
|
||||
TreeItem<Entry> childItem = new TreeItem<>(childEntry);
|
||||
rootItem.getChildren().add(childItem);
|
||||
childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry)));
|
||||
if(allOpenWallets) {
|
||||
for(Entry walletEntry : childEntry.getChildren()) {
|
||||
TreeItem<Entry> walletItem = new TreeItem<>(walletEntry);
|
||||
childItem.getChildren().add(walletItem);
|
||||
walletItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)walletEntry)));
|
||||
}
|
||||
} else {
|
||||
childItem.getChildren().add(new TreeItem<>(new UnconfirmedEntry((WalletTransactionsEntry)childEntry)));
|
||||
}
|
||||
}
|
||||
|
||||
table.setShowRoot(true);
|
||||
@@ -97,6 +109,14 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
|
||||
hBox.getChildren().add(vBox);
|
||||
|
||||
Wallet balanceWallet;
|
||||
if(allOpenWallets) {
|
||||
balanceWallet = new Wallet();
|
||||
balanceWallet.getChildWallets().addAll(masterWallets.stream().flatMap(mws -> mws.getAllWallets().stream()).toList());
|
||||
} else {
|
||||
balanceWallet = masterWallets.get(0);
|
||||
}
|
||||
|
||||
NumberAxis xAxis = new NumberAxis();
|
||||
xAxis.setSide(Side.BOTTOM);
|
||||
xAxis.setForceZeroInRange(false);
|
||||
@@ -104,7 +124,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
NumberAxis yAxis = new NumberAxis();
|
||||
yAxis.setSide(Side.LEFT);
|
||||
BalanceChart balanceChart = new BalanceChart(xAxis, yAxis);
|
||||
balanceChart.initialize(new WalletTransactionsEntry(masterWallet, true));
|
||||
balanceChart.initialize(new WalletTransactionsEntry(balanceWallet, true));
|
||||
balanceChart.setAnimated(false);
|
||||
balanceChart.setLegendVisible(false);
|
||||
balanceChart.setVerticalGridLinesVisible(false);
|
||||
@@ -120,6 +140,32 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
}
|
||||
|
||||
public static class AllSummaryEntry extends Entry {
|
||||
private AllSummaryEntry(List<List<WalletForm>> walletFormsList) {
|
||||
super(null, "All Wallets", walletFormsList.stream().map(SummaryEntry::new).collect(Collectors.toList()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Long getValue() {
|
||||
long value = 0;
|
||||
for(Entry entry : getChildren()) {
|
||||
value += entry.getValue();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getEntryType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Function getWalletFunction() {
|
||||
return Function.TRANSACTIONS;
|
||||
}
|
||||
}
|
||||
|
||||
public static class SummaryEntry extends Entry {
|
||||
private SummaryEntry(List<WalletForm> walletForms) {
|
||||
super(walletForms.get(0).getWallet(), walletForms.get(0).getWallet().getName(), walletForms.stream().map(WalletForm::getWalletTransactionsEntry).collect(Collectors.toList()));
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class SorobanInitiatedEvent {
|
||||
private Wallet wallet;
|
||||
|
||||
public SorobanInitiatedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
@@ -17,7 +16,6 @@ public class SpendUtxoEvent {
|
||||
private final Long fee;
|
||||
private final boolean requireAllUtxos;
|
||||
private final BlockTransaction replacedTransaction;
|
||||
private final Pool pool;
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
@@ -32,19 +30,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = requireAllUtxos;
|
||||
this.replacedTransaction = replacedTransaction;
|
||||
this.pool = null;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = pool;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
@@ -56,7 +41,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = null;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = null;
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
@@ -88,10 +72,6 @@ public class SpendUtxoEvent {
|
||||
return replacedTransaction;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolIndexHighFrequencyEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WhirlpoolIndexHighFrequencyEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolMixEvent {
|
||||
private final Wallet wallet;
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
private final MixProgress mixProgress;
|
||||
private final Utxo nextUtxo;
|
||||
private final MixFailReason mixFailReason;
|
||||
private final String mixError;
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = mixProgress;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = nextUtxo;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = mixFailReason;
|
||||
this.mixError = mixError;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress() {
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
public Utxo getNextUtxo() {
|
||||
return nextUtxo;
|
||||
}
|
||||
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
|
||||
super(wallet, utxo, nextUtxo);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@@ -203,4 +203,11 @@ public class GlyphUtils {
|
||||
busyGlyph.setFontSize(12);
|
||||
return busyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getDownArrowGlyph() {
|
||||
Glyph downGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
|
||||
downGlyph.getStyleClass().add("arrow-down");
|
||||
downGlyph.setFontSize(12);
|
||||
return downGlyph;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,31 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
inputStream.transferTo(baos);
|
||||
InputStream firstClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
InputStream secondClone = new ByteArrayInputStream(baos.toByteArray());
|
||||
|
||||
Keystore keystore;
|
||||
try {
|
||||
keystore = getKeystoreMultisig(scriptType, firstClone, password);
|
||||
} catch(Exception e) {
|
||||
keystore = getKeystoreSinglesig(scriptType, secondClone, password);
|
||||
}
|
||||
|
||||
return keystore;
|
||||
} catch(IOException e) {
|
||||
throw new ImportException("Error importing keystore for " + scriptType, e);
|
||||
}
|
||||
}
|
||||
|
||||
private Keystore getKeystoreSinglesig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
ColdcardSinglesig coldcardSinglesig = new ColdcardSinglesig();
|
||||
return coldcardSinglesig.getKeystore(scriptType, inputStream, password);
|
||||
}
|
||||
|
||||
public Keystore getKeystoreMultisig(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8);
|
||||
ColdcardKeystore cck = JsonPersistence.getGson().fromJson(reader, ColdcardKeystore.class);
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ public class Config {
|
||||
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
|
||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
private boolean sameAppMixing;
|
||||
private boolean mempoolFullRbf;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
@@ -662,15 +661,6 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isSameAppMixing() {
|
||||
return sameAppMixing;
|
||||
}
|
||||
|
||||
public void setSameAppMixing(boolean sameAppMixing) {
|
||||
this.sameAppMixing = sameAppMixing;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isMempoolFullRbf() {
|
||||
return mempoolFullRbf;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.SamouraiUtil;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class Samourai implements KeystoreFileImport {
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import the wallet backup file samourai.txt exported from the Samourai app. Note that see the full balance, several script types may need to imported in separate wallets.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
try {
|
||||
String input = CharStreams.toString(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
|
||||
|
||||
Gson gson = new Gson();
|
||||
Type stringStringMap = new TypeToken<Map<String, JsonElement>>() {
|
||||
}.getType();
|
||||
Map<String, JsonElement> map = gson.fromJson(input, stringStringMap);
|
||||
|
||||
String payload = input;
|
||||
if(map.containsKey("payload")) {
|
||||
payload = map.get("payload").getAsString();
|
||||
}
|
||||
|
||||
int version = 1;
|
||||
if(map.containsKey("version")) {
|
||||
version = map.get("version").getAsInt();
|
||||
}
|
||||
|
||||
String decrypted;
|
||||
if(version == 1) {
|
||||
decrypted = SamouraiUtil.decrypt(payload, password, SamouraiUtil.DefaultPBKDF2Iterations);
|
||||
} else if(version == 2) {
|
||||
decrypted = SamouraiUtil.decryptSHA256(payload, password);
|
||||
} else {
|
||||
throw new ImportException("Unsupported backup version: " + version);
|
||||
}
|
||||
|
||||
SamouraiBackup backup = gson.fromJson(decrypted, SamouraiBackup.class);
|
||||
DeterministicSeed seed = new DeterministicSeed(Utils.hexToBytes(backup.wallet.seed), password, 0);
|
||||
Keystore keystore = Keystore.fromSeed(seed, scriptType.getDefaultDerivation());
|
||||
keystore.setLabel(getWalletModel().toDisplayString());
|
||||
return keystore;
|
||||
} catch(JsonParseException e) {
|
||||
throw new ImportException("Failed to decrypt the wallet backup file, check if the password is correct.");
|
||||
} catch(ImportException e) {
|
||||
throw e;
|
||||
} catch(Exception e) {
|
||||
throw new ImportException("Error importing backup", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Samourai Backup";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SAMOURAI;
|
||||
}
|
||||
|
||||
private static class SamouraiBackup {
|
||||
public SamouraiWallet wallet;
|
||||
}
|
||||
|
||||
private static class SamouraiWallet {
|
||||
public boolean testnet;
|
||||
public String seed;
|
||||
public String fingerprint;
|
||||
}
|
||||
}
|
||||
@@ -8,8 +8,6 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -174,15 +172,6 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
|
||||
@@ -31,6 +31,10 @@ public class WalletTransactions implements WalletExport {
|
||||
|
||||
private final WalletForm walletForm;
|
||||
|
||||
static {
|
||||
DATE_FORMAT.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||
}
|
||||
|
||||
public WalletTransactions(WalletForm walletForm) {
|
||||
this.walletForm = walletForm;
|
||||
}
|
||||
@@ -72,7 +76,7 @@ public class WalletTransactions implements WalletExport {
|
||||
try {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
|
||||
writer.write("Date");
|
||||
writer.write("Date (UTC)");
|
||||
writer.write("Label");
|
||||
writer.write("Value");
|
||||
writer.write("Balance");
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.wallet.httpClient.HttpResponseException;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.samourai.wallet.httpClient.HttpResponseException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.JettyHttpClientService;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.samourai.wallet.httpClient.IHttpClient;
|
||||
import com.samourai.wallet.util.AsyncUtil;
|
||||
import com.samourai.wallet.util.ThreadUtil;
|
||||
import com.samourai.whirlpool.client.utils.ClientUtils;
|
||||
import com.sparrowwallet.sparrow.net.http.client.AsyncUtil;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.IHttpClient;
|
||||
import com.sparrowwallet.sparrow.net.http.client.JettyHttpClientService;
|
||||
import io.reactivex.Observable;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -22,15 +20,15 @@ public class HttpClientService extends JettyHttpClientService {
|
||||
}
|
||||
|
||||
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
|
||||
return getHttpClient(HttpUsage.BACKEND).getJson(url, responseType, headers);
|
||||
return getHttpClient(HttpUsage.DEFAULT).getJson(url, responseType, headers);
|
||||
}
|
||||
|
||||
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
|
||||
return getHttpClient(HttpUsage.BACKEND).postJson(url, responseType, headers, body).toObservable();
|
||||
return getHttpClient(HttpUsage.DEFAULT).postJson(url, responseType, headers, body).toObservable();
|
||||
}
|
||||
|
||||
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
|
||||
IHttpClient httpClient = getHttpClient(HttpUsage.BACKEND);
|
||||
IHttpClient httpClient = getHttpClient(HttpUsage.DEFAULT);
|
||||
return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.IHttpProxySupplier;
|
||||
import com.samourai.wallet.httpClient.HttpProxy;
|
||||
import com.samourai.wallet.httpClient.HttpProxyProtocol;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpProxy;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpProxyProtocol;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.IHttpProxySupplier;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class AsyncUtil {
|
||||
private static final ThreadUtil threadUtil = ThreadUtil.getInstance();
|
||||
private static AsyncUtil instance;
|
||||
|
||||
public static AsyncUtil getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new AsyncUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public <T> T unwrapException(Callable<T> c) throws Exception {
|
||||
try {
|
||||
return c.call();
|
||||
} catch(RuntimeException e) {
|
||||
// blockingXXX wraps errors with RuntimeException, unwrap it
|
||||
throw unwrapException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Exception unwrapException(Exception e) throws Exception {
|
||||
if(e.getCause() != null && e.getCause() instanceof Exception) {
|
||||
throw (Exception) e.getCause();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
public <T> T blockingGet(Single<T> o) throws Exception {
|
||||
try {
|
||||
return unwrapException(o::blockingGet);
|
||||
} catch(ExecutionException e) {
|
||||
// blockingGet(threadUtil.runWithTimeoutAndRetry()) wraps InterruptedException("exit (done)")
|
||||
// with ExecutionException, unwrap it
|
||||
throw unwrapException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T blockingGet(Single<T> o, long timeoutMs) throws Exception {
|
||||
Callable<T> callable = () -> blockingGet(o);
|
||||
return blockingGet(runAsync(callable, timeoutMs));
|
||||
}
|
||||
|
||||
public <T> T blockingGet(Future<T> o, long timeoutMs) throws Exception {
|
||||
return o.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public <T> T blockingLast(Observable<T> o) throws Exception {
|
||||
return unwrapException(o::blockingLast);
|
||||
}
|
||||
|
||||
public void blockingAwait(Completable o) throws Exception {
|
||||
Callable<Optional> callable = () -> {
|
||||
o.blockingAwait();
|
||||
return Optional.empty();
|
||||
};
|
||||
unwrapException(callable);
|
||||
}
|
||||
|
||||
public void blockingAwait(Completable o, long timeoutMs) throws Exception {
|
||||
Callable<Optional> callable = () -> {
|
||||
o.blockingAwait();
|
||||
return Optional.empty();
|
||||
};
|
||||
blockingGet(runAsync(callable, timeoutMs));
|
||||
}
|
||||
|
||||
public <T> Single<T> timeout(Single<T> o, long timeoutMs) {
|
||||
try {
|
||||
return Single.just(blockingGet(o, timeoutMs));
|
||||
} catch(Exception e) {
|
||||
return Single.error(e);
|
||||
}
|
||||
}/*
|
||||
|
||||
public Completable timeout(Completable o, long timeoutMs) {
|
||||
try {
|
||||
return Completable.fromCallable(() -> {
|
||||
blockingAwait(o, timeoutMs);
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return Completable.error(e);
|
||||
}
|
||||
}*/
|
||||
|
||||
public <T> Single<T> runIOAsync(final Callable<T> callable) {
|
||||
return Single.fromCallable(callable).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable runIOAsyncCompletable(final Action action) {
|
||||
return Completable.fromAction(action).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public <T> T runIO(final Callable<T> callable) throws Exception {
|
||||
return blockingGet(runIOAsync(callable));
|
||||
}
|
||||
|
||||
public void runIO(final Action action) throws Exception {
|
||||
blockingAwait(runIOAsyncCompletable(action));
|
||||
}
|
||||
|
||||
public Completable runAsync(Runnable runnable, long timeoutMs) {
|
||||
Future<?> future = runAsync(() -> {
|
||||
runnable.run();
|
||||
return Optional.empty(); // must return an object for using Completable.fromSingle()
|
||||
});
|
||||
return Completable.fromSingle(Single.fromFuture(future, timeoutMs, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
public <T> Future<T> runAsync(Callable<T> callable) {
|
||||
// preserve logging context
|
||||
String mdc = mdcAppend("runAsync=" + System.currentTimeMillis());
|
||||
return threadUtil.runAsync(() -> {
|
||||
MDC.put("mdc", mdc);
|
||||
return callable.call();
|
||||
});
|
||||
}
|
||||
|
||||
public <T> Single<T> runAsync(Callable<T> callable, long timeoutMs) {
|
||||
Future<T> future = runAsync(callable);
|
||||
return Single.fromFuture(future, timeoutMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private static String mdcAppend(String info) {
|
||||
String mdc = MDC.get("mdc");
|
||||
if(mdc == null) {
|
||||
mdc = "";
|
||||
} else {
|
||||
mdc += ",";
|
||||
}
|
||||
mdc += info;
|
||||
return mdc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public abstract class HttpException extends Exception {
|
||||
|
||||
public HttpException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public HttpException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public class HttpNetworkException extends HttpException {
|
||||
public HttpNetworkException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpNetworkException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class HttpProxy {
|
||||
private final HttpProxyProtocol protocol;
|
||||
private final String host;
|
||||
private final int port;
|
||||
|
||||
public HttpProxy(HttpProxyProtocol protocol, String host, int port) {
|
||||
this.protocol = protocol;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public static boolean validate(String proxy) {
|
||||
// check protocol
|
||||
String[] protocols = Arrays.stream(HttpProxyProtocol.values()).map(p -> p.name()).toArray(String[]::new);
|
||||
String regex = "^(" + StringUtils.join(protocols, "|").toLowerCase() + ")://(.+?):([0-9]+)";
|
||||
return proxy.trim().toLowerCase().matches(regex);
|
||||
}
|
||||
|
||||
public HttpProxyProtocol getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return protocol + "://" + host + ":" + port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public enum HttpProxyProtocol {
|
||||
HTTP,
|
||||
SOCKS,
|
||||
SOCKS5;
|
||||
|
||||
public static Optional<HttpProxyProtocol> find(String value) {
|
||||
try {
|
||||
return Optional.of(valueOf(value));
|
||||
} catch(Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public class HttpResponseException extends HttpException {
|
||||
private final String responseBody;
|
||||
private final int statusCode;
|
||||
|
||||
public HttpResponseException(Exception cause, String responseBody, int statusCode) {
|
||||
super(cause);
|
||||
this.responseBody = responseBody;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public HttpResponseException(String message, String responseBody, int statusCode) {
|
||||
super(message);
|
||||
this.responseBody = responseBody;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public HttpResponseException(String responseBody, int statusCode) {
|
||||
this("response statusCode=" + statusCode, responseBody, statusCode);
|
||||
}
|
||||
|
||||
public String getResponseBody() {
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpResponseException{" +
|
||||
"message=" + getMessage() + ", " +
|
||||
"responseBody='" + responseBody + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public class HttpSystemException extends HttpException {
|
||||
public HttpSystemException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpSystemException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class HttpUsage {
|
||||
public static final HttpUsage DEFAULT = new HttpUsage("Default");
|
||||
|
||||
private final String name;
|
||||
|
||||
public HttpUsage(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
HttpUsage httpUsage = (HttpUsage) o;
|
||||
return Objects.equals(name, httpUsage.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IBackendClient {
|
||||
<T> T getJson(String url, Class<T> responseType, Map<String, String> headers) throws HttpException;
|
||||
|
||||
<T> T getJson(String url, Class<T> responseType, Map<String, String> headers, boolean async) throws HttpException;
|
||||
|
||||
<T> T postUrlEncoded(String url, Class<T> responseType, Map<String, String> headers, Map<String, String> body) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import io.reactivex.Single;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IHttpClient extends IBackendClient {
|
||||
void connect() throws Exception;
|
||||
|
||||
<T> Single<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body);
|
||||
|
||||
Single<Optional<String>> postString(String urlStr, Map<String, String> headers, String contentType, String content);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public interface IHttpClientService {
|
||||
IHttpClient getHttpClient(HttpUsage httpUsage);
|
||||
|
||||
void changeIdentity(); // change Tor identity if any
|
||||
|
||||
void stop();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IHttpProxySupplier {
|
||||
Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage);
|
||||
|
||||
void changeIdentity();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
public class JSONUtils {
|
||||
private static JSONUtils instance;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public JSONUtils() {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
}
|
||||
|
||||
public static final JSONUtils getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new JSONUtils();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ObjectMapper getObjectMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public abstract class JacksonHttpClient implements IHttpClient {
|
||||
private static final Logger log = LoggerFactory.getLogger(JacksonHttpClient.class);
|
||||
|
||||
private final Consumer<Exception> onNetworkError;
|
||||
|
||||
public JacksonHttpClient(Consumer<Exception> onNetworkError) {
|
||||
this.onNetworkError = onNetworkError;
|
||||
}
|
||||
|
||||
protected abstract String requestJsonGet(String urlStr, Map<String, String> headers, boolean async) throws HttpException;
|
||||
|
||||
protected abstract String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody) throws HttpException;
|
||||
|
||||
protected abstract String requestStringPost(String urlStr, Map<String, String> headers, String contentType, String content) throws HttpException;
|
||||
|
||||
protected abstract String requestJsonPostUrlEncoded(String urlStr, Map<String, String> headers, Map<String, String> body) throws HttpException;
|
||||
|
||||
@Override
|
||||
public <T> T getJson(String urlStr, Class<T> responseType, Map<String, String> headers) throws HttpException {
|
||||
return getJson(urlStr, responseType, headers, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getJson(String urlStr, Class<T> responseType, Map<String, String> headers, boolean async) throws HttpException {
|
||||
return httpObservableBlockingSingle(() -> { // run on ioThread
|
||||
try {
|
||||
String responseContent = handleNetworkError("getJson " + urlStr, () -> requestJsonGet(urlStr, headers, async));
|
||||
return parseJson(responseContent, responseType, 200);
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("getJson failed: " + urlStr + ": " + e.toString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Single<Optional<T>> postJson(final String urlStr, final Class<T> responseType, final Map<String, String> headers, final Object bodyObj) {
|
||||
return httpObservable(
|
||||
() -> {
|
||||
try {
|
||||
String jsonBody = getObjectMapper().writeValueAsString(bodyObj);
|
||||
String responseContent = handleNetworkError("postJson " + urlStr, () -> requestJsonPost(urlStr, headers, jsonBody));
|
||||
return parseJson(responseContent, responseType, 200);
|
||||
} catch(HttpException e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("postJson failed: " + urlStr + ": " + e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<Optional<String>> postString(String urlStr, Map<String, String> headers, String contentType, String content) {
|
||||
return httpObservable(
|
||||
() -> {
|
||||
try {
|
||||
return handleNetworkError("postString " + urlStr, () -> requestStringPost(urlStr, headers, contentType, content));
|
||||
} catch(HttpException e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("postJson failed: " + urlStr + ": " + e.toString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T postUrlEncoded(String urlStr, Class<T> responseType, Map<String, String> headers, Map<String, String> body) throws HttpException {
|
||||
return httpObservableBlockingSingle(() -> { // run on ioThread
|
||||
try {
|
||||
String responseContent = handleNetworkError("postUrlEncoded " + urlStr, () -> requestJsonPostUrlEncoded(urlStr, headers, body));
|
||||
return parseJson(responseContent, responseType, 200);
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("postUrlEncoded failed: " + urlStr + ": " + e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <T> T parseJson(String responseContent, Class<T> responseType, int statusCode) throws HttpException {
|
||||
T result;
|
||||
if(log.isTraceEnabled()) {
|
||||
String responseStr = (responseContent != null ? responseContent : "null");
|
||||
if(responseStr.length() > 500) {
|
||||
responseStr = responseStr.substring(0, 500) + "...";
|
||||
}
|
||||
log.trace("response[" + (responseType != null ? responseType.getCanonicalName() : "null") + "]: " + responseStr);
|
||||
}
|
||||
if(String.class.equals(responseType)) {
|
||||
result = (T) responseContent;
|
||||
} else {
|
||||
try {
|
||||
result = getObjectMapper().readValue(responseContent, responseType);
|
||||
} catch(Exception e) {
|
||||
throw new HttpResponseException(e, responseContent, statusCode);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected String handleNetworkError(String logInfo, Callable<String> doHttpRequest) throws HttpException {
|
||||
try {
|
||||
try {
|
||||
// first attempt
|
||||
return doHttpRequest.call();
|
||||
} catch(HttpNetworkException e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("HTTP_ERROR_NETWORK " + logInfo + ", retrying: " + e.getMessage());
|
||||
}
|
||||
// change tor proxy
|
||||
onNetworkError(e);
|
||||
|
||||
// retry second attempt
|
||||
return doHttpRequest.call();
|
||||
}
|
||||
} catch(HttpException e) { // forward
|
||||
throw e;
|
||||
} catch(Exception e) { // should never happen
|
||||
throw new HttpSystemException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onNetworkError(HttpNetworkException e) {
|
||||
if(onNetworkError != null) {
|
||||
synchronized(JacksonHttpClient.class) { // avoid overlapping Tor restarts between httpClients
|
||||
onNetworkError.accept(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> Single<Optional<T>> httpObservable(final Callable<T> supplier) {
|
||||
return Single.fromCallable(() -> Optional.ofNullable(supplier.call())).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
protected <T> T httpObservableBlockingSingle(final Callable<T> supplier) throws HttpException {
|
||||
try {
|
||||
Optional<T> opt = AsyncUtil.getInstance().blockingGet(httpObservable(supplier));
|
||||
return opt.orElse(null);
|
||||
} catch(HttpException e) { // forward
|
||||
throw e;
|
||||
} catch(Exception e) { // should never happen
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected ObjectMapper getObjectMapper() {
|
||||
return JSONUtils.getInstance().getObjectMapper();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||
import org.eclipse.jetty.client.util.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class JettyHttpClient extends JacksonHttpClient {
|
||||
protected static Logger log = LoggerFactory.getLogger(JettyHttpClient.class);
|
||||
public static final String CONTENTTYPE_APPLICATION_JSON = "application/json";
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final long requestTimeout;
|
||||
private final HttpUsage httpUsage;
|
||||
|
||||
public JettyHttpClient(Consumer<Exception> onNetworkError, HttpClient httpClient, long requestTimeout, HttpUsage httpUsage) {
|
||||
super(onNetworkError);
|
||||
this.httpClient = httpClient;
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.httpUsage = httpUsage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws HttpException {
|
||||
try {
|
||||
if(!httpClient.isRunning()) {
|
||||
httpClient.start();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
try {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("restart");
|
||||
}
|
||||
if(httpClient.isRunning()) {
|
||||
httpClient.stop();
|
||||
}
|
||||
httpClient.start();
|
||||
} catch(Exception e) {
|
||||
log.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
if(httpClient.isRunning()) {
|
||||
httpClient.stop();
|
||||
Executor executor = httpClient.getExecutor();
|
||||
if(executor instanceof LifeCycle) {
|
||||
((LifeCycle) executor).stop();
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error stopping client", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestJsonGet(String urlStr, Map<String, String> headers, boolean async) throws HttpException {
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.GET, headers);
|
||||
return makeRequest(req, async);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody) throws HttpException {
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
||||
req.content(new StringContentProvider(CONTENTTYPE_APPLICATION_JSON, jsonBody, StandardCharsets.UTF_8));
|
||||
return makeRequest(req, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestStringPost(String urlStr, Map<String, String> headers, String contentType, String content) throws HttpException {
|
||||
log.debug("POST " + urlStr);
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
||||
req.content(new StringContentProvider(content), contentType);
|
||||
return makeRequest(req, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestJsonPostUrlEncoded(String urlStr, Map<String, String> headers, Map<String, String> body) throws HttpException {
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
||||
req.content(new FormContentProvider(computeBodyFields(body)));
|
||||
return makeRequest(req, false);
|
||||
}
|
||||
|
||||
private Fields computeBodyFields(Map<String, String> body) {
|
||||
Fields fields = new Fields();
|
||||
for(Map.Entry<String, String> entry : body.entrySet()) {
|
||||
fields.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
protected String makeRequest(Request req, boolean async) throws HttpException {
|
||||
String responseContent;
|
||||
if(async) {
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
req.send(listener);
|
||||
|
||||
// Call to the listener's get() blocks until the headers arrived
|
||||
Response response;
|
||||
try {
|
||||
response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch(Exception e) {
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
|
||||
// Read content
|
||||
InputStream is = listener.getInputStream();
|
||||
Scanner s = new Scanner(is).useDelimiter("\\A");
|
||||
responseContent = s.hasNext() ? s.next() : null;
|
||||
|
||||
// check status
|
||||
checkResponseStatus(response.getStatus(), responseContent);
|
||||
} else {
|
||||
ContentResponse response;
|
||||
try {
|
||||
response = req.send();
|
||||
} catch(Exception e) {
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
checkResponseStatus(response.getStatus(), response.getContentAsString());
|
||||
responseContent = response.getContentAsString();
|
||||
}
|
||||
return responseContent;
|
||||
}
|
||||
|
||||
private void checkResponseStatus(int status, String responseBody) throws HttpResponseException {
|
||||
if(!HttpStatus.isSuccess(status)) {
|
||||
log.error("Http query failed: status=" + status + ", responseBody=" + responseBody);
|
||||
throw new HttpResponseException(responseBody, status);
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient getJettyHttpClient() throws HttpException {
|
||||
connect();
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private Request computeHttpRequest(String url, HttpMethod method, Map<String, String> headers) throws HttpException {
|
||||
if(url.endsWith("/rpc")) {
|
||||
// log RPC as TRACE
|
||||
if(log.isTraceEnabled()) {
|
||||
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
|
||||
log.trace("+" + method + ": " + url + headersStr);
|
||||
}
|
||||
} else {
|
||||
if(log.isDebugEnabled()) {
|
||||
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
|
||||
log.debug("+" + method + ": " + url + headersStr);
|
||||
}
|
||||
}
|
||||
Request req = getJettyHttpClient().newRequest(url);
|
||||
req.method(method);
|
||||
if(headers != null) {
|
||||
for(Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
req.header(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
req.timeout(requestTimeout, TimeUnit.MILLISECONDS);
|
||||
return req;
|
||||
}
|
||||
|
||||
public HttpUsage getHttpUsage() {
|
||||
return httpUsage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.sparrowwallet.sparrow.net.http.client.socks5.Socks5Proxy;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.ProxyConfiguration;
|
||||
import org.eclipse.jetty.client.Socks4Proxy;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class JettyHttpClientService implements IHttpClientService {
|
||||
private static final Logger log = LoggerFactory.getLogger(JettyHttpClientService.class);
|
||||
private static final String NAME = "HttpClient";
|
||||
public static final long DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
// limit changing Tor identity on network error every 4 minutes
|
||||
private static final double RATE_CHANGE_IDENTITY_ON_NETWORK_ERROR = 1.0 / 240;
|
||||
|
||||
protected Map<HttpUsage, JettyHttpClient> httpClients; // used by Sparrow
|
||||
private final IHttpProxySupplier httpProxySupplier;
|
||||
private final long requestTimeout;
|
||||
|
||||
public JettyHttpClientService(long requestTimeout, IHttpProxySupplier httpProxySupplier) {
|
||||
this.httpProxySupplier = httpProxySupplier != null ? httpProxySupplier : computeHttpProxySupplierDefault();
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.httpClients = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public JettyHttpClientService(long requestTimeout) {
|
||||
this(requestTimeout, null);
|
||||
}
|
||||
|
||||
public JettyHttpClientService() {
|
||||
this(DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
protected static IHttpProxySupplier computeHttpProxySupplierDefault() {
|
||||
return new IHttpProxySupplier() {
|
||||
@Override
|
||||
public Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeIdentity() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyHttpClient getHttpClient(HttpUsage httpUsage) {
|
||||
JettyHttpClient httpClient = httpClients.get(httpUsage);
|
||||
if(httpClient == null) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("+httpClient[" + httpUsage + "]");
|
||||
}
|
||||
httpClient = computeHttpClient(httpUsage);
|
||||
httpClients.put(httpUsage, httpClient);
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
protected JettyHttpClient computeHttpClient(HttpUsage httpUsage) {
|
||||
Consumer<Exception> onNetworkError = computeOnNetworkError();
|
||||
HttpClient httpClient = computeJettyClient(httpUsage);
|
||||
return new JettyHttpClient(onNetworkError, httpClient, requestTimeout, httpUsage);
|
||||
}
|
||||
|
||||
protected HttpClient computeJettyClient(HttpUsage httpUsage) {
|
||||
// we use jetty for proxy SOCKS support
|
||||
HttpClient jettyHttpClient = new HttpClient(new SslContextFactory());
|
||||
// jettyHttpClient.setSocketAddressResolver(new MySocketAddressResolver());
|
||||
|
||||
// prevent user-agent tracking
|
||||
jettyHttpClient.setUserAgentField(null);
|
||||
|
||||
// configure
|
||||
configureProxy(jettyHttpClient, httpUsage);
|
||||
configureThread(jettyHttpClient, httpUsage);
|
||||
|
||||
return jettyHttpClient;
|
||||
}
|
||||
|
||||
protected Consumer<Exception> computeOnNetworkError() {
|
||||
RateLimiter rateLimiter = RateLimiter.create(RATE_CHANGE_IDENTITY_ON_NETWORK_ERROR);
|
||||
return e -> {
|
||||
if(!rateLimiter.tryAcquire()) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("onNetworkError: not changing Tor identity (too many recent attempts)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// change Tor identity on network error
|
||||
httpProxySupplier.changeIdentity();
|
||||
};
|
||||
}
|
||||
|
||||
protected void configureProxy(HttpClient jettyHttpClient, HttpUsage httpUsage) {
|
||||
Optional<HttpProxy> httpProxyOptional = httpProxySupplier.getHttpProxy(httpUsage);
|
||||
if(httpProxyOptional != null && httpProxyOptional.isPresent()) {
|
||||
HttpProxy httpProxy = httpProxyOptional.get();
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("+httpClient: proxy=" + httpProxy);
|
||||
}
|
||||
ProxyConfiguration.Proxy jettyProxy = computeJettyProxy(httpProxy);
|
||||
jettyHttpClient.getProxyConfiguration().getProxies().add(jettyProxy);
|
||||
} else {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("+httpClient: no proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void configureThread(HttpClient jettyHttpClient, HttpUsage httpUsage) {
|
||||
String name = NAME + "-" + httpUsage.toString();
|
||||
|
||||
QueuedThreadPool threadPool = new QueuedThreadPool();
|
||||
threadPool.setName(name);
|
||||
threadPool.setDaemon(true);
|
||||
jettyHttpClient.setExecutor(threadPool);
|
||||
jettyHttpClient.setScheduler(new ScheduledExecutorScheduler(name + "-scheduler", true));
|
||||
}
|
||||
|
||||
protected ProxyConfiguration.Proxy computeJettyProxy(HttpProxy httpProxy) {
|
||||
ProxyConfiguration.Proxy jettyProxy = null;
|
||||
switch(httpProxy.getProtocol()) {
|
||||
case SOCKS:
|
||||
jettyProxy = new Socks4Proxy(httpProxy.getHost(), httpProxy.getPort());
|
||||
break;
|
||||
case SOCKS5:
|
||||
jettyProxy = new Socks5Proxy(httpProxy.getHost(), httpProxy.getPort());
|
||||
break;
|
||||
case HTTP:
|
||||
jettyProxy = new org.eclipse.jetty.client.HttpProxy(httpProxy.getHost(), httpProxy.getPort());
|
||||
break;
|
||||
}
|
||||
return jettyProxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
for(JettyHttpClient httpClient : httpClients.values()) {
|
||||
httpClient.stop();
|
||||
}
|
||||
httpClients.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeIdentity() {
|
||||
stop();
|
||||
httpProxySupplier.changeIdentity();
|
||||
}
|
||||
|
||||
public IHttpProxySupplier getHttpProxySupplier() {
|
||||
return httpProxySupplier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class ThreadUtil {
|
||||
private static final Logger log = LoggerFactory.getLogger(ThreadUtil.class);
|
||||
|
||||
private static ThreadUtil instance;
|
||||
|
||||
private ExecutorService executorService;
|
||||
|
||||
protected ThreadUtil() {
|
||||
this.executorService = computeExecutorService();
|
||||
}
|
||||
|
||||
public static ThreadUtil getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new ThreadUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected ExecutorService computeExecutorService() {
|
||||
return Executors.newFixedThreadPool(5,
|
||||
r -> {
|
||||
Thread t = Executors.defaultThreadFactory().newThread(r);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
public void setExecutorService(ScheduledExecutorService executorService) {
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
public <T> Future<T> runAsync(Callable<T> callable) {
|
||||
return executorService.submit(callable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Socks5 backported from Jetty12 - we still use Jetty9 for JDK8 compatibility.
|
||||
*/
|
||||
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package com.sparrowwallet.sparrow.net.http.client.socks5;
|
||||
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Helper class for SOCKS5 proxying.
|
||||
*
|
||||
* @see Socks5Proxy
|
||||
*/
|
||||
public class Socks5 {
|
||||
/** The SOCKS protocol version: {@value}. */
|
||||
public static final byte VERSION = 0x05;
|
||||
|
||||
/** The SOCKS5 {@code CONNECT} command used in SOCKS5 connect requests. */
|
||||
public static final byte COMMAND_CONNECT = 0x01;
|
||||
|
||||
/** The reserved byte value: {@value}. */
|
||||
public static final byte RESERVED = 0x00;
|
||||
|
||||
/** The address type for IPv4 used in SOCKS5 connect requests and responses. */
|
||||
public static final byte ADDRESS_TYPE_IPV4 = 0x01;
|
||||
|
||||
/** The address type for domain names used in SOCKS5 connect requests and responses. */
|
||||
public static final byte ADDRESS_TYPE_DOMAIN = 0x03;
|
||||
|
||||
/** The address type for IPv6 used in SOCKS5 connect requests and responses. */
|
||||
public static final byte ADDRESS_TYPE_IPV6 = 0x04;
|
||||
|
||||
private Socks5() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A SOCKS5 authentication method.
|
||||
*
|
||||
* <p>Implementations should send and receive the bytes that are specific to the particular
|
||||
* authentication method.
|
||||
*/
|
||||
public interface Authentication {
|
||||
/**
|
||||
* Performs the authentication send and receive bytes exchanges specific for this {@link
|
||||
* Authentication}.
|
||||
*
|
||||
* @param endPoint the {@link EndPoint} to send to and receive from the SOCKS5 server
|
||||
* @param callback the callback to complete when the authentication is complete
|
||||
*/
|
||||
void authenticate(EndPoint endPoint, Callback callback);
|
||||
|
||||
/** A factory for {@link Authentication}s. */
|
||||
interface Factory {
|
||||
/**
|
||||
* @return the authentication method defined by RFC 1928
|
||||
*/
|
||||
byte getMethod();
|
||||
|
||||
/**
|
||||
* @return a new {@link Authentication}
|
||||
*/
|
||||
Authentication newAuthentication();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation of the {@code NO AUTH} authentication method defined in <a
|
||||
* href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.
|
||||
*/
|
||||
public static class NoAuthenticationFactory implements Authentication.Factory {
|
||||
public static final byte METHOD = 0x00;
|
||||
|
||||
@Override
|
||||
public byte getMethod() {
|
||||
return METHOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication newAuthentication() {
|
||||
return (endPoint, callback) -> callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation of the {@code USERNAME/PASSWORD} authentication method defined in <a
|
||||
* href="https://datatracker.ietf.org/doc/html/rfc1929">RFC 1929</a>.
|
||||
*/
|
||||
public static class UsernamePasswordAuthenticationFactory implements Authentication.Factory {
|
||||
public static final byte METHOD = 0x02;
|
||||
public static final byte VERSION = 0x01;
|
||||
private static final Logger LOG =
|
||||
LoggerFactory.getLogger(UsernamePasswordAuthenticationFactory.class);
|
||||
|
||||
private final String userName;
|
||||
private final String password;
|
||||
private final Charset charset;
|
||||
|
||||
public UsernamePasswordAuthenticationFactory(String userName, String password) {
|
||||
this(userName, password, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public UsernamePasswordAuthenticationFactory(
|
||||
String userName, String password, Charset charset) {
|
||||
this.userName = Objects.requireNonNull(userName);
|
||||
this.password = Objects.requireNonNull(password);
|
||||
this.charset = Objects.requireNonNull(charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getMethod() {
|
||||
return METHOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication newAuthentication() {
|
||||
return new UsernamePasswordAuthentication(this);
|
||||
}
|
||||
|
||||
private static class UsernamePasswordAuthentication implements Authentication, Callback {
|
||||
private final ByteBuffer byteBuffer = BufferUtil.allocate(2);
|
||||
private final UsernamePasswordAuthenticationFactory factory;
|
||||
private EndPoint endPoint;
|
||||
private Callback callback;
|
||||
|
||||
private UsernamePasswordAuthentication(UsernamePasswordAuthenticationFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(EndPoint endPoint, Callback callback) {
|
||||
this.endPoint = endPoint;
|
||||
this.callback = callback;
|
||||
|
||||
byte[] userNameBytes = factory.userName.getBytes(factory.charset);
|
||||
byte[] passwordBytes = factory.password.getBytes(factory.charset);
|
||||
ByteBuffer byteBuffer =
|
||||
(ByteBuffer)
|
||||
ByteBuffer.allocate(3 + userNameBytes.length + passwordBytes.length)
|
||||
.put(VERSION)
|
||||
.put((byte) userNameBytes.length)
|
||||
.put(userNameBytes)
|
||||
.put((byte) passwordBytes.length)
|
||||
.put(passwordBytes)
|
||||
.flip();
|
||||
endPoint.write(Callback.from(this::authenticationSent, this::failed), byteBuffer);
|
||||
}
|
||||
|
||||
private void authenticationSent() {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Written SOCKS5 username/password authentication request");
|
||||
}
|
||||
endPoint.fillInterested(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded() {
|
||||
try {
|
||||
int filled = endPoint.fill(byteBuffer);
|
||||
if(filled < 0) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
if(byteBuffer.remaining() < 2) {
|
||||
endPoint.fillInterested(this);
|
||||
return;
|
||||
}
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Received SOCKS5 username/password authentication response");
|
||||
}
|
||||
byte version = byteBuffer.get();
|
||||
if(version != VERSION) {
|
||||
throw new IOException(
|
||||
"Unsupported username/password authentication version: " + version);
|
||||
}
|
||||
byte status = byteBuffer.get();
|
||||
if(status != 0) {
|
||||
throw new IOException("SOCK5 username/password authentication failure");
|
||||
}
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("SOCKS5 username/password authentication succeeded");
|
||||
}
|
||||
callback.succeeded();
|
||||
} catch(Throwable x) {
|
||||
failed(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x) {
|
||||
callback.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType() {
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package com.sparrowwallet.sparrow.net.http.client.socks5;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.HttpDestination;
|
||||
import org.eclipse.jetty.client.Origin;
|
||||
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Client-side proxy configuration for SOCKS5, defined by <a
|
||||
* href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.
|
||||
*
|
||||
* <p>Multiple authentication methods are supported via {@link
|
||||
* #putAuthenticationFactory(Socks5.Authentication.Factory)}. By default only the {@link
|
||||
* Socks5.NoAuthenticationFactory NO AUTH} authentication method is configured. The {@link
|
||||
* Socks5.UsernamePasswordAuthenticationFactory USERNAME/PASSWORD} is available to applications but
|
||||
* must be explicitly configured and added.
|
||||
*/
|
||||
public class Socks5Proxy extends Proxy {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
|
||||
|
||||
private final Map<Byte, Socks5.Authentication.Factory> authentications = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given SOCKS5 proxy host and port.
|
||||
*
|
||||
* @param host the SOCKS5 proxy host name
|
||||
* @param port the SOCKS5 proxy port
|
||||
*/
|
||||
public Socks5Proxy(String host, int port) {
|
||||
this(new Origin.Address(host, port), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given SOCKS5 proxy address.
|
||||
*
|
||||
* <p>When {@code secure=true} the communication between the client and the proxy will be
|
||||
* encrypted (using this proxy {@link #getSslContextFactory()} which typically defaults to that of
|
||||
* {@link HttpClient}.
|
||||
*
|
||||
* @param address the SOCKS5 proxy address (host and port)
|
||||
* @param secure whether the communication between the client and the SOCKS5 proxy should be
|
||||
* secure
|
||||
*/
|
||||
public Socks5Proxy(Origin.Address address, boolean secure) {
|
||||
super(address, secure);
|
||||
putAuthenticationFactory(new Socks5.NoAuthenticationFactory());
|
||||
}
|
||||
|
||||
protected static ClientConnectionFactory newSslClientConnectionFactory(HttpClient httpClient, SslContextFactory sslContextFactory, ClientConnectionFactory connectionFactory) {
|
||||
if(sslContextFactory == null) {
|
||||
sslContextFactory = httpClient.getSslContextFactory();
|
||||
}
|
||||
return new SslClientConnectionFactory(sslContextFactory, httpClient.getByteBufferPool(), httpClient.getExecutor(), connectionFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides this class with the given SOCKS5 authentication method.
|
||||
*
|
||||
* @param authenticationFactory the SOCKS5 authentication factory
|
||||
* @return the previous authentication method of the same type, or {@code null} if there was none
|
||||
* of that type already present
|
||||
*/
|
||||
public Socks5.Authentication.Factory putAuthenticationFactory(Socks5.Authentication.Factory authenticationFactory) {
|
||||
return authentications.put(authenticationFactory.getMethod(), authenticationFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the authentication of the given {@code method}.
|
||||
*
|
||||
* @param method the authentication method to remove
|
||||
*/
|
||||
public Socks5.Authentication.Factory removeAuthenticationFactory(byte method) {
|
||||
return authentications.remove(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory) {
|
||||
return new Socks5ProxyClientConnectionFactory(connectionFactory);
|
||||
}
|
||||
|
||||
private static class Socks5ProxyConnection extends AbstractConnection implements Connection.UpgradeFrom {
|
||||
private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
||||
|
||||
// SOCKS5 response max length is 262 bytes.
|
||||
private final ByteBuffer byteBuffer = BufferUtil.allocate(512);
|
||||
private final ClientConnectionFactory connectionFactory;
|
||||
private final Map<String, Object> context;
|
||||
private final Map<Byte, Socks5.Authentication.Factory> authentications;
|
||||
private State state = State.HANDSHAKE;
|
||||
|
||||
private Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context, Map<Byte, Socks5.Authentication.Factory> authentications) {
|
||||
super(endPoint, executor);
|
||||
this.connectionFactory = connectionFactory;
|
||||
this.context = context;
|
||||
this.authentications = new LinkedHashMap<>(authentications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer onUpgradeFrom() {
|
||||
return BufferUtil.copy(byteBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen() {
|
||||
super.onOpen();
|
||||
sendHandshake();
|
||||
}
|
||||
|
||||
private void sendHandshake() {
|
||||
try {
|
||||
// +-------------+--------------------+------------------+
|
||||
// | version (1) | num of methods (1) | methods (1..255) |
|
||||
// +-------------+--------------------+------------------+
|
||||
int size = authentications.size();
|
||||
ByteBuffer byteBuffer =
|
||||
ByteBuffer.allocate(1 + 1 + size).put(Socks5.VERSION).put((byte) size);
|
||||
authentications.keySet().forEach(byteBuffer::put);
|
||||
byteBuffer.flip();
|
||||
getEndPoint().write(Callback.from(this::handshakeSent, this::fail), byteBuffer);
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void handshakeSent() {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Written SOCKS5 handshake request");
|
||||
}
|
||||
state = State.HANDSHAKE;
|
||||
fillInterested();
|
||||
}
|
||||
|
||||
private void fail(Throwable x) {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("SOCKS5 failure", x);
|
||||
}
|
||||
getEndPoint().close();
|
||||
@SuppressWarnings("unchecked")
|
||||
Promise<Connection> promise =
|
||||
(Promise<Connection>)
|
||||
this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
promise.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFillable() {
|
||||
try {
|
||||
switch(state) {
|
||||
case HANDSHAKE:
|
||||
receiveHandshake();
|
||||
break;
|
||||
case CONNECT:
|
||||
receiveConnect();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void receiveHandshake() throws IOException {
|
||||
// +-------------+------------+
|
||||
// | version (1) | method (1) |
|
||||
// +-------------+------------+
|
||||
int filled = getEndPoint().fill(byteBuffer);
|
||||
if(filled < 0) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
if(byteBuffer.remaining() < 2) {
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Received SOCKS5 handshake response {}", BufferUtil.toDetailString(byteBuffer));
|
||||
}
|
||||
|
||||
byte version = byteBuffer.get();
|
||||
if(version != Socks5.VERSION) {
|
||||
throw new IOException("Unsupported SOCKS5 version: " + version);
|
||||
}
|
||||
|
||||
byte method = byteBuffer.get();
|
||||
if(method == -1) {
|
||||
throw new IOException("Unacceptable SOCKS5 authentication methods");
|
||||
}
|
||||
|
||||
Socks5.Authentication.Factory factory = authentications.get(method);
|
||||
if(factory == null) {
|
||||
throw new IOException("Unknown SOCKS5 authentication method: " + method);
|
||||
}
|
||||
|
||||
factory
|
||||
.newAuthentication()
|
||||
.authenticate(getEndPoint(), Callback.from(this::sendConnect, this::fail));
|
||||
}
|
||||
|
||||
private void sendConnect() {
|
||||
try {
|
||||
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
||||
// | version (1) | command (1) | reserved (1) | address type (1) | address bytes (4..255) |
|
||||
// port (2) |
|
||||
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
||||
HttpDestination destination = (HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||
Origin.Address address = destination.getOrigin().getAddress();
|
||||
String host = address.getHost();
|
||||
short port = (short) address.getPort();
|
||||
|
||||
ByteBuffer byteBuffer;
|
||||
Matcher matcher = IPv4_PATTERN.matcher(host);
|
||||
if(matcher.matches()) {
|
||||
byteBuffer =
|
||||
ByteBuffer.allocate(10)
|
||||
.put(Socks5.VERSION)
|
||||
.put(Socks5.COMMAND_CONNECT)
|
||||
.put(Socks5.RESERVED)
|
||||
.put(Socks5.ADDRESS_TYPE_IPV4);
|
||||
for(int i = 1; i <= 4; ++i) {
|
||||
byteBuffer.put(Byte.parseByte(matcher.group(i)));
|
||||
}
|
||||
byteBuffer.putShort(port).flip();
|
||||
} else if(true /*URIUtil.isValidHostRegisteredName(host)*/) {
|
||||
byte[] bytes = host.getBytes(StandardCharsets.US_ASCII);
|
||||
if(bytes.length > 255) {
|
||||
throw new IOException("Invalid host name: " + host);
|
||||
}
|
||||
byteBuffer =
|
||||
(ByteBuffer)
|
||||
ByteBuffer.allocate(7 + bytes.length)
|
||||
.put(Socks5.VERSION)
|
||||
.put(Socks5.COMMAND_CONNECT)
|
||||
.put(Socks5.RESERVED)
|
||||
.put(Socks5.ADDRESS_TYPE_DOMAIN)
|
||||
.put((byte) bytes.length)
|
||||
.put(bytes)
|
||||
.putShort(port)
|
||||
.flip();
|
||||
} else {
|
||||
// Assume IPv6.
|
||||
byte[] bytes = InetAddress.getByName(host).getAddress();
|
||||
byteBuffer =
|
||||
(ByteBuffer)
|
||||
ByteBuffer.allocate(22)
|
||||
.put(Socks5.VERSION)
|
||||
.put(Socks5.COMMAND_CONNECT)
|
||||
.put(Socks5.RESERVED)
|
||||
.put(Socks5.ADDRESS_TYPE_IPV6)
|
||||
.put(bytes)
|
||||
.putShort(port)
|
||||
.flip();
|
||||
}
|
||||
|
||||
getEndPoint().write(Callback.from(this::connectSent, this::fail), byteBuffer);
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectSent() {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Written SOCKS5 connect request");
|
||||
}
|
||||
state = State.CONNECT;
|
||||
fillInterested();
|
||||
}
|
||||
|
||||
private void receiveConnect() throws IOException {
|
||||
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
||||
// | version (1) | reply (1) | reserved (1) | address type (1) | address bytes (4..255) | port
|
||||
// (2) |
|
||||
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
||||
int filled = getEndPoint().fill(byteBuffer);
|
||||
if(filled < 0) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
if(byteBuffer.remaining() < 5) {
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
byte addressType = byteBuffer.get(3);
|
||||
int length = 6;
|
||||
if(addressType == Socks5.ADDRESS_TYPE_IPV4) {
|
||||
length += 4;
|
||||
} else if(addressType == Socks5.ADDRESS_TYPE_DOMAIN) {
|
||||
length += 1 + (byteBuffer.get(4) & 0xFF);
|
||||
} else if(addressType == Socks5.ADDRESS_TYPE_IPV6) {
|
||||
length += 16;
|
||||
} else {
|
||||
throw new IOException("Invalid SOCKS5 address type: " + addressType);
|
||||
}
|
||||
if(byteBuffer.remaining() < length) {
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Received SOCKS5 connect response {}", BufferUtil.toDetailString(byteBuffer));
|
||||
}
|
||||
|
||||
// We have all the SOCKS5 bytes.
|
||||
byte version = byteBuffer.get();
|
||||
if(version != Socks5.VERSION) {
|
||||
throw new IOException("Unsupported SOCKS5 version: " + version);
|
||||
}
|
||||
|
||||
byte status = byteBuffer.get();
|
||||
switch(status) {
|
||||
case 0: {
|
||||
// Consume the buffer before upgrading to the tunnel.
|
||||
byteBuffer.position(length);
|
||||
tunnel();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
throw new IOException("SOCKS5 general failure");
|
||||
case 2:
|
||||
throw new IOException("SOCKS5 connection not allowed");
|
||||
case 3:
|
||||
throw new IOException("SOCKS5 network unreachable");
|
||||
case 4:
|
||||
throw new IOException("SOCKS5 host unreachable");
|
||||
case 5:
|
||||
throw new IOException("SOCKS5 connection refused");
|
||||
case 6:
|
||||
throw new IOException("SOCKS5 timeout expired");
|
||||
case 7:
|
||||
throw new IOException("SOCKS5 unsupported command");
|
||||
case 8:
|
||||
throw new IOException("SOCKS5 unsupported address");
|
||||
default:
|
||||
throw new IOException("SOCKS5 unknown status: " + status);
|
||||
}
|
||||
}
|
||||
|
||||
private void tunnel() {
|
||||
try {
|
||||
HttpDestination destination =
|
||||
(HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||
this.context.put("ssl.peer.host", destination.getHost());
|
||||
this.context.put("ssl.peer.port", destination.getPort());
|
||||
// Origin.Address address = destination.getOrigin().getAddress();
|
||||
// Don't want to do DNS resolution here.
|
||||
// InetSocketAddress inet = InetSocketAddress.createUnresolved(address.getHost(),
|
||||
// address.getPort());
|
||||
// context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, inet);
|
||||
ClientConnectionFactory connectionFactory = this.connectionFactory;
|
||||
if(destination.isSecure()) {
|
||||
connectionFactory =
|
||||
newSslClientConnectionFactory(destination.getHttpClient(), null, connectionFactory);
|
||||
}
|
||||
Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
|
||||
getEndPoint().upgrade(newConnection);
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("SOCKS5 tunnel established: {} over {}", this, newConnection);
|
||||
}
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private enum State {
|
||||
HANDSHAKE,
|
||||
CONNECT
|
||||
}
|
||||
}
|
||||
|
||||
private class Socks5ProxyClientConnectionFactory implements ClientConnectionFactory {
|
||||
private final ClientConnectionFactory connectionFactory;
|
||||
|
||||
private Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory) {
|
||||
this.connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public Connection newConnection(EndPoint endPoint, Map<String, Object> context) {
|
||||
HttpDestination destination = (HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||
Executor executor = destination.getHttpClient().getExecutor();
|
||||
Socks5ProxyConnection connection = new Socks5ProxyConnection(endPoint, executor, connectionFactory, context, authentications);
|
||||
return customize(connection, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.payjoin;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.samourai.wallet.httpClient.HttpResponseException;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
@@ -17,6 +16,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.HttpClientService;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
@@ -58,14 +58,6 @@ public class PayNym {
|
||||
return followers;
|
||||
}
|
||||
|
||||
public boolean isCollaborativeSend() {
|
||||
return collaborativeSend;
|
||||
}
|
||||
|
||||
public void setCollaborativeSend(boolean collaborativeSend) {
|
||||
this.collaborativeSend = collaborativeSend;
|
||||
}
|
||||
|
||||
public List<ScriptType> getScriptTypes() {
|
||||
return segwit ? getSegwitScriptTypes() : getV1ScriptTypes();
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.paynym;
|
||||
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
|
||||
public final class PayNymAddress extends P2WPKHAddress {
|
||||
private final PayNym payNym;
|
||||
|
||||
public PayNymAddress(PayNym payNym) {
|
||||
super(new byte[20]);
|
||||
this.payNym = payNym;
|
||||
}
|
||||
|
||||
public PayNym getPayNym() {
|
||||
return payNym;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return payNym.nymName();
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,8 @@ public class PayNymController {
|
||||
public static final String INVALID_PAYMENT_CODE_LABEL = "Invalid Payment Code";
|
||||
|
||||
private String walletId;
|
||||
private boolean selectLinkedOnly;
|
||||
private PayNym walletPayNym;
|
||||
private boolean requestingPassword;
|
||||
|
||||
@FXML
|
||||
private CopyableTextField payNymName;
|
||||
@@ -87,9 +87,8 @@ public class PayNymController {
|
||||
|
||||
private final BooleanProperty closeProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public void initializeView(String walletId, boolean selectLinkedOnly) {
|
||||
public void initializeView(String walletId) {
|
||||
this.walletId = walletId;
|
||||
this.selectLinkedOnly = selectLinkedOnly;
|
||||
|
||||
payNymName.managedProperty().bind(payNymName.visibleProperty());
|
||||
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
|
||||
@@ -353,6 +352,43 @@ public class PayNymController {
|
||||
}
|
||||
|
||||
private void addWalletIfNotificationTransactionPresent(List<PayNym> following) {
|
||||
Unlinked unlinked = getUnlinkedPayNyms(following);
|
||||
|
||||
Wallet wallet = getMasterWallet();
|
||||
if(!unlinked.unlinkedNotifications.isEmpty()) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
requestingPassword = true;
|
||||
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link contacts?", "Some contacts were found that may be already linked. Link these contacts? Your password is required to check.", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(payNymName.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
addWalletIfNotificationTransactionPresent(decryptedWallet, getUnlinkedPayNyms(following));
|
||||
decryptedWallet.clearPrivate();
|
||||
requestingPassword = false;
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
requestingPassword = false;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addWalletIfNotificationTransactionPresent(wallet, unlinked);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Unlinked getUnlinkedPayNyms(List<PayNym> following) {
|
||||
Map<BlockTransaction, PayNym> unlinkedPayNyms = new HashMap<>();
|
||||
Map<BlockTransaction, WalletNode> unlinkedNotifications = new HashMap<>();
|
||||
for(PayNym payNym : following) {
|
||||
@@ -366,44 +402,16 @@ public class PayNymController {
|
||||
}
|
||||
}
|
||||
|
||||
Wallet wallet = getMasterWallet();
|
||||
if(!unlinkedNotifications.isEmpty()) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link contacts?", "Some contacts were found that may be already linked. Link these contacts? Your password is required to check.", Alert.AlertType.CONFIRMATION, ButtonType.NO, ButtonType.YES);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(payNymName.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.DecryptWalletService decryptWalletService = new Storage.DecryptWalletService(wallet.copy(), password.get());
|
||||
decryptWalletService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Done"));
|
||||
Wallet decryptedWallet = decryptWalletService.getValue();
|
||||
addWalletIfNotificationTransactionPresent(decryptedWallet, unlinkedPayNyms, unlinkedNotifications);
|
||||
decryptedWallet.clearPrivate();
|
||||
});
|
||||
decryptWalletService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.END, "Failed"));
|
||||
AppServices.showErrorDialog("Incorrect Password", decryptWalletService.getException().getMessage());
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(storage.getWalletId(wallet), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
addWalletIfNotificationTransactionPresent(wallet, unlinkedPayNyms, unlinkedNotifications);
|
||||
}
|
||||
}
|
||||
return new Unlinked(unlinkedPayNyms, unlinkedNotifications);
|
||||
}
|
||||
|
||||
private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Map<BlockTransaction, PayNym> unlinkedPayNyms, Map<BlockTransaction, WalletNode> unlinkedNotifications) {
|
||||
private void addWalletIfNotificationTransactionPresent(Wallet decryptedWallet, Unlinked unlinked) {
|
||||
List<Wallet> addedWallets = new ArrayList<>();
|
||||
for(BlockTransaction blockTransaction : unlinkedNotifications.keySet()) {
|
||||
for(BlockTransaction blockTransaction : unlinked.unlinkedNotifications.keySet()) {
|
||||
try {
|
||||
PayNym payNym = unlinkedPayNyms.get(blockTransaction);
|
||||
PayNym payNym = unlinked.unlinkedPayNyms.get(blockTransaction);
|
||||
PaymentCode externalPaymentCode = payNym.paymentCode();
|
||||
WalletNode input0Node = unlinkedNotifications.get(blockTransaction);
|
||||
WalletNode input0Node = unlinked.unlinkedNotifications.get(blockTransaction);
|
||||
Keystore keystore = input0Node.getWallet().isNested() ? decryptedWallet.getChildWallet(input0Node.getWallet().getName()).getKeystores().get(0) : decryptedWallet.getKeystores().get(0);
|
||||
ECKey input0Key = keystore.getKey(input0Node);
|
||||
TransactionOutPoint input0Outpoint = PaymentCode.getDesignatedInput(blockTransaction.getTransaction()).getOutpoint();
|
||||
@@ -658,10 +666,6 @@ public class PayNymController {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSelectLinkedOnly() {
|
||||
return selectLinkedOnly;
|
||||
}
|
||||
|
||||
public PayNym getPayNym() {
|
||||
return payNymProperty.get();
|
||||
}
|
||||
@@ -689,7 +693,7 @@ public class PayNymController {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
|
||||
}
|
||||
|
||||
if(walletPayNym != null) {
|
||||
if(walletPayNym != null && !requestingPassword) {
|
||||
//If we have just linked a PayNym wallet that paid for another notification transaction, attempt to link
|
||||
Platform.runLater(() -> addWalletIfNotificationTransactionPresent(walletPayNym.following()));
|
||||
}
|
||||
@@ -761,4 +765,6 @@ public class PayNymController {
|
||||
public void selectNext() {
|
||||
}
|
||||
}
|
||||
|
||||
private record Unlinked(Map<BlockTransaction, PayNym> unlinkedPayNyms, Map<BlockTransaction, WalletNode> unlinkedNotifications) {}
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import java.io.IOException;
|
||||
|
||||
public class PayNymDialog extends Dialog<PayNym> {
|
||||
public PayNymDialog(String walletId) {
|
||||
this(walletId, Operation.SHOW, false);
|
||||
this(walletId, Operation.SHOW);
|
||||
}
|
||||
|
||||
public PayNymDialog(String walletId, Operation operation, boolean selectLinkedOnly) {
|
||||
public PayNymDialog(String walletId, Operation operation) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
@@ -21,7 +21,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("paynym/paynym.fxml"));
|
||||
dialogPane.setContent(payNymLoader.load());
|
||||
PayNymController payNymController = payNymLoader.getController();
|
||||
payNymController.initializeView(walletId, selectLinkedOnly);
|
||||
payNymController.initializeView(walletId);
|
||||
|
||||
EventManager.get().register(payNymController);
|
||||
|
||||
@@ -33,26 +33,13 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.css").toExternalForm());
|
||||
|
||||
final ButtonType sendDirectlyButtonType = new javafx.scene.control.ButtonType("Send Directly", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType sendCollaborativelyButtonType = new javafx.scene.control.ButtonType("Send Collaboratively", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType sendDirectlyButtonType = new javafx.scene.control.ButtonType("Send To Contact", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
if(operation == Operation.SEND) {
|
||||
if(selectLinkedOnly) {
|
||||
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, cancelButtonType);
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, sendCollaborativelyButtonType, cancelButtonType);
|
||||
Button sendCollaborativelyButton = (Button)dialogPane.lookupButton(sendCollaborativelyButtonType);
|
||||
sendCollaborativelyButton.setDisable(true);
|
||||
sendCollaborativelyButton.setDefaultButton(false);
|
||||
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
||||
sendCollaborativelyButton.setDisable(payNym == null);
|
||||
sendCollaborativelyButton.setDefaultButton(payNym != null && !payNymController.isLinked(payNym));
|
||||
});
|
||||
}
|
||||
|
||||
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, cancelButtonType);
|
||||
Button sendDirectlyButton = (Button)dialogPane.lookupButton(sendDirectlyButtonType);
|
||||
sendDirectlyButton.setDisable(true);
|
||||
sendDirectlyButton.setDefaultButton(true);
|
||||
@@ -66,7 +53,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
selectButton.setDisable(true);
|
||||
selectButton.setDefaultButton(true);
|
||||
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
||||
selectButton.setDisable(payNym == null || (selectLinkedOnly && !payNymController.isLinked(payNym)));
|
||||
selectButton.setDisable(payNym == null || !payNymController.isLinked(payNym));
|
||||
});
|
||||
} else {
|
||||
dialogPane.getButtonTypes().add(doneButtonType);
|
||||
@@ -83,14 +70,8 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> {
|
||||
if(dialogButton == sendCollaborativelyButtonType) {
|
||||
PayNym payNym = payNymController.getPayNym();
|
||||
payNym.setCollaborativeSend(true);
|
||||
return payNym;
|
||||
} else if(dialogButton == sendDirectlyButtonType || dialogButton == selectButtonType) {
|
||||
PayNym payNym = payNymController.getPayNym();
|
||||
payNym.setCollaborativeSend(false);
|
||||
return payNym;
|
||||
if(dialogButton == sendDirectlyButtonType || dialogButton == selectButtonType) {
|
||||
return payNymController.getPayNym();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -155,10 +155,6 @@ public class PayNymService {
|
||||
.map(o -> o.get());
|
||||
}
|
||||
|
||||
public static Observable<Map<String, Object>> followPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode, String authToken, String signature) {
|
||||
return followPaymentCode(PaymentCode.fromString(paymentCode.toString()), authToken, signature);
|
||||
}
|
||||
|
||||
public static Observable<Map<String, Object>> followPaymentCode(PaymentCode paymentCode, String authToken, String signature) {
|
||||
Map<String, String> headers = new HashMap<>();
|
||||
headers.put("content-type", "application/json");
|
||||
|
||||
@@ -1,465 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.soroban.client.meeting.SorobanRequestMessage;
|
||||
import com.samourai.soroban.client.wallet.SorobanWalletService;
|
||||
import com.samourai.soroban.client.wallet.counterparty.SorobanWalletCounterparty;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.CahootsContext;
|
||||
import com.samourai.wallet.cahoots.CahootsType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import io.reactivex.functions.Consumer;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.soroban.Soroban.TIMEOUT_MS;
|
||||
|
||||
public class CounterpartyController extends SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(CounterpartyController.class);
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private CopyableTextField payNym;
|
||||
|
||||
@FXML
|
||||
private Button showPayNym;
|
||||
|
||||
@FXML
|
||||
private PayNymAvatar payNymAvatar;
|
||||
|
||||
@FXML
|
||||
private Button payNymButton;
|
||||
|
||||
@FXML
|
||||
private PaymentCodeTextField paymentCode;
|
||||
|
||||
@FXML
|
||||
private Button paymentCodeQR;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Wallet> mixWallet;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step2Timer;
|
||||
|
||||
@FXML
|
||||
private Label step2Desc;
|
||||
|
||||
@FXML
|
||||
private Label mixingPartner;
|
||||
|
||||
@FXML
|
||||
private PayNymAvatar mixPartnerAvatar;
|
||||
|
||||
@FXML
|
||||
private Hyperlink meetingFail;
|
||||
|
||||
@FXML
|
||||
private VBox mixDetails;
|
||||
|
||||
@FXML
|
||||
private Label mixType;
|
||||
|
||||
@FXML
|
||||
private Label mixFee;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step3Timer;
|
||||
|
||||
@FXML
|
||||
private Label step3Desc;
|
||||
|
||||
@FXML
|
||||
private ProgressBar sorobanProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label sorobanProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph mixDeclined;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
private final ObjectProperty<Boolean> meetingReceived = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Boolean> meetingAccepted = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
mixWallet.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Wallet wallet) {
|
||||
return wallet == null ? "" : wallet.getFullDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
mixWallet.setItems(FXCollections.observableList(wallet.getAllWallets()));
|
||||
mixWallet.setValue(wallet);
|
||||
mixWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> setWallet(selectedWallet));
|
||||
|
||||
sorobanProgressBar.managedProperty().bind(sorobanProgressBar.visibleProperty());
|
||||
sorobanProgressLabel.managedProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.managedProperty().bind(mixDeclined.visibleProperty());
|
||||
sorobanProgressBar.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.visibleProperty().bind(sorobanProgressLabel.visibleProperty().not());
|
||||
step2Timer.visibleProperty().bind(mixingPartner.visibleProperty());
|
||||
step3Timer.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
if(soroban.getHdWallet() == null) {
|
||||
throw new IllegalStateException("Soroban HD wallet must be set");
|
||||
}
|
||||
|
||||
payNym.managedProperty().bind(payNym.visibleProperty());
|
||||
showPayNym.managedProperty().bind(showPayNym.visibleProperty());
|
||||
showPayNym.visibleProperty().bind(payNym.visibleProperty());
|
||||
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
||||
payNymAvatar.visibleProperty().bind(payNym.visibleProperty());
|
||||
payNymButton.managedProperty().bind(payNymButton.visibleProperty());
|
||||
payNymButton.visibleProperty().bind(payNym.visibleProperty().not());
|
||||
if(isUsePayNym(wallet)) {
|
||||
retrievePayNym(null);
|
||||
} else {
|
||||
payNym.setVisible(false);
|
||||
}
|
||||
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
paymentCode.setPaymentCode(masterWallet.getPaymentCode());
|
||||
paymentCodeQR.prefHeightProperty().bind(paymentCode.heightProperty());
|
||||
paymentCodeQR.prefWidthProperty().bind(showPayNym.widthProperty());
|
||||
|
||||
mixingPartner.managedProperty().bind(mixingPartner.visibleProperty());
|
||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||
meetingFail.visibleProperty().bind(mixingPartner.visibleProperty().not());
|
||||
meetingFail.setOnAction(event -> {
|
||||
step2Desc.setText("Ask your mix partner to initiate the Soroban communication.");
|
||||
mixingPartner.setVisible(true);
|
||||
startCounterpartyMeetingReceive();
|
||||
step2Timer.start(e -> {
|
||||
step2Desc.setText("Mix declined due to timeout.");
|
||||
meetingReceived.set(Boolean.FALSE);
|
||||
});
|
||||
});
|
||||
|
||||
mixDetails.managedProperty().bind(mixDetails.visibleProperty());
|
||||
mixDetails.setVisible(false);
|
||||
|
||||
meetingAccepted.addListener((observable, oldValue, accepted) -> {
|
||||
Platform.exitNestedEventLoop(meetingAccepted, accepted);
|
||||
meetingReceived.set(null);
|
||||
});
|
||||
|
||||
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
startCounterpartyMeetingReceive();
|
||||
step2Timer.start(e -> {
|
||||
step2Desc.setText("Mix declined due to timeout.");
|
||||
meetingReceived.set(Boolean.FALSE);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
step3.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
meetingAccepted.set(Boolean.TRUE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setWallet(Wallet wallet) {
|
||||
this.walletId = AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
private void startCounterpartyMeetingReceive() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService();
|
||||
|
||||
SparrowCahootsWallet cahootsWallet = soroban.getCahootsWallet(wallet);
|
||||
SorobanWalletCounterparty sorobanWalletCounterparty = sorobanWalletService.getSorobanWalletCounterparty(cahootsWallet);
|
||||
sorobanWalletCounterparty.setTimeoutMeetingMs(TIMEOUT_MS);
|
||||
|
||||
Service<SorobanRequestMessage> receiveMeetingService = new Service<>() {
|
||||
@Override
|
||||
protected Task<SorobanRequestMessage> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected SorobanRequestMessage call() throws Exception {
|
||||
return sorobanWalletCounterparty.receiveMeetingRequest();
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
receiveMeetingService.setOnSucceeded(event -> {
|
||||
SorobanRequestMessage requestMessage = receiveMeetingService.getValue();
|
||||
PaymentCode paymentCodeInitiator = requestMessage.getSender();
|
||||
CahootsType cahootsType = requestMessage.getType();
|
||||
updateMixPartner(paymentCodeInitiator, cahootsType);
|
||||
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(meetingAccepted);
|
||||
|
||||
sorobanWalletCounterparty.sendMeetingResponse(requestMessage, accepted)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(JavaFxScheduler.platform())
|
||||
.subscribe(responseMessage -> {
|
||||
requestUserAttention();
|
||||
if(accepted) {
|
||||
startCounterpartyCollaboration(sorobanWalletCounterparty, paymentCodeInitiator, cahootsType, cahootsWallet.getAccount());
|
||||
followPaymentCode(paymentCodeInitiator);
|
||||
}
|
||||
}, error -> {
|
||||
log.error("Error sending meeting response", error);
|
||||
mixingPartner.setVisible(false);
|
||||
requestUserAttention();
|
||||
});
|
||||
});
|
||||
receiveMeetingService.setOnFailed(event -> {
|
||||
Throwable e = event.getSource().getException();
|
||||
log.error("Failed to receive meeting request", e);
|
||||
mixingPartner.setVisible(false);
|
||||
requestUserAttention();
|
||||
});
|
||||
receiveMeetingService.start();
|
||||
}
|
||||
|
||||
private void updateMixPartner(PaymentCode paymentCodeInitiator, CahootsType cahootsType) {
|
||||
String code = paymentCodeInitiator.toString();
|
||||
mixingPartner.setText(code.substring(0, 12) + "..." + code.substring(code.length() - 5));
|
||||
if(isUsePayNym(wallet)) {
|
||||
mixPartnerAvatar.setPaymentCode(paymentCodeInitiator);
|
||||
PayNymService.getPayNym(paymentCodeInitiator.toString()).subscribe(payNym -> {
|
||||
mixingPartner.setText(payNym.nymName());
|
||||
}, error -> {
|
||||
//ignore, may not be a PayNym
|
||||
});
|
||||
}
|
||||
|
||||
if(cahootsType == CahootsType.STONEWALLX2) {
|
||||
mixType.setText("Two person coinjoin (" + cahootsType.getLabel() + ")");
|
||||
mixFee.setText("You pay half the miner fee");
|
||||
} else if(cahootsType == CahootsType.STOWAWAY) {
|
||||
mixType.setText("Payjoin (" + cahootsType.getLabel() + ")");
|
||||
mixFee.setText("None");
|
||||
} else {
|
||||
mixType.setText(cahootsType.getLabel());
|
||||
mixFee.setText("None");
|
||||
}
|
||||
|
||||
mixDetails.setVisible(true);
|
||||
meetingReceived.set(Boolean.TRUE);
|
||||
}
|
||||
|
||||
private void startCounterpartyCollaboration(SorobanWalletCounterparty sorobanWalletCounterparty, PaymentCode initiatorPaymentCode, CahootsType cahootsType, int account) {
|
||||
sorobanProgressLabel.setText("Creating mix transaction...");
|
||||
SparrowCahootsWallet cahootsWallet = (SparrowCahootsWallet)sorobanWalletCounterparty.getCahootsWallet();
|
||||
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getSpendableUtxos();
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
|
||||
cahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||
}
|
||||
|
||||
try {
|
||||
CahootsContext cahootsContext = CahootsContext.newCounterparty(cahootsWallet, cahootsType, account);
|
||||
Consumer<OnlineCahootsMessage> onProgress = cahootsMessage -> {
|
||||
if(cahootsMessage != null) {
|
||||
Platform.runLater(() -> {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
if(cahoots.getStep() == 3) {
|
||||
sorobanProgressLabel.setText("Your mix partner is reviewing the transaction...");
|
||||
step3Timer.start();
|
||||
} else if(cahoots.getStep() >= 4) {
|
||||
try {
|
||||
Transaction transaction = getTransaction(cahoots);
|
||||
if(transaction != null) {
|
||||
transactionProperty.set(transaction);
|
||||
updateTransactionDiagram(transactionDiagram, wallet, null, transaction);
|
||||
next();
|
||||
}
|
||||
} catch(PSBTParseException e) {
|
||||
log.error("Invalid collaborative PSBT created", e);
|
||||
step3Desc.setText("Invalid transaction created.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Service<Cahoots> cahootsService = new Service<>() {
|
||||
@Override
|
||||
protected Task<Cahoots> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Cahoots call() throws Exception {
|
||||
return sorobanWalletCounterparty.counterparty(cahootsContext, initiatorPaymentCode, onProgress);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
cahootsService.setOnFailed(event -> {
|
||||
Throwable error = Throwables.getRootCause(event.getSource().getException());
|
||||
log.error("Error creating mix transaction", error);
|
||||
String cutFrom = "Exception: ";
|
||||
String message = error.getMessage() == null ? (error instanceof TimeoutException || step3Timer.getProgress() == 0d ? "Timed out receiving response" : "Error receiving response") : error.getMessage();
|
||||
int index = message.lastIndexOf(cutFrom);
|
||||
String msg = index < 0 ? message : message.substring(index + cutFrom.length());
|
||||
msg = msg.replace("#Cahoots", "mix transaction");
|
||||
step3Desc.setText(msg);
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
cahootsService.start();
|
||||
} catch(Exception e) {
|
||||
log.error("Error creating mix transaction", e);
|
||||
sorobanProgressLabel.setText(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private void followPaymentCode(PaymentCode paymentCodeInitiator) {
|
||||
if(isUsePayNym(wallet)) {
|
||||
PayNymService.getAuthToken(wallet, new HashMap<>()).subscribe(authToken -> {
|
||||
String signature = PayNymService.getSignature(wallet, authToken);
|
||||
PayNymService.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
|
||||
log.debug("Followed payment code " + followMap.get("following"));
|
||||
}, error -> {
|
||||
log.warn("Could not follow payment code", error);
|
||||
});
|
||||
}, error -> {
|
||||
log.warn("Could not follow payment code", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
meetingAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
|
||||
public void retrievePayNym(ActionEvent event) {
|
||||
setUsePayNym(wallet, true);
|
||||
|
||||
PayNymService.createPayNym(wallet).subscribe(createMap -> {
|
||||
payNym.setText((String)createMap.get("nymName"));
|
||||
payNymAvatar.setPaymentCode(wallet.isMasterWallet() ? wallet.getPaymentCode() : wallet.getMasterWallet().getPaymentCode());
|
||||
payNym.setVisible(true);
|
||||
|
||||
PayNymService.claimPayNym(wallet, createMap, true);
|
||||
}, error -> {
|
||||
log.error("Error retrieving PayNym", error);
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Error retrieving PayNym", "Could not retrieve PayNym. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
retrievePayNym(null);
|
||||
} else {
|
||||
payNym.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
PayNymDialog payNymDialog = new PayNymDialog(walletId);
|
||||
payNymDialog.initOwner(payNym.getScene().getWindow());
|
||||
payNymDialog.showAndWait();
|
||||
}
|
||||
|
||||
public void showPayNymQR(ActionEvent event) {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(masterWallet.getPaymentCode().toString());
|
||||
qrDisplayDialog.initOwner(payNym.getScene().getWindow());
|
||||
qrDisplayDialog.showAndWait();
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> meetingReceivedProperty() {
|
||||
return meetingReceived;
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> meetingAcceptedProperty() {
|
||||
return meetingAccepted;
|
||||
}
|
||||
|
||||
public ObjectProperty<Transaction> transactionProperty() {
|
||||
return transactionProperty;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CounterpartyDialog extends Dialog<Boolean> {
|
||||
public CounterpartyDialog(String walletId, Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader counterpartyLoader = new FXMLLoader(AppServices.class.getResource("soroban/counterparty.fxml"));
|
||||
dialogPane.setContent(counterpartyLoader.load());
|
||||
CounterpartyController counterpartyController = counterpartyLoader.getController();
|
||||
counterpartyController.initializeView(walletId, wallet);
|
||||
|
||||
dialogPane.setPrefWidth(730);
|
||||
dialogPane.setPrefHeight(520);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/counterparty.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, doneButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
|
||||
Button doneButton = (Button)dialogPane.lookupButton(doneButtonType);
|
||||
doneButton.setDisable(true);
|
||||
counterpartyController.meetingReceivedProperty().addListener((observable, oldValue, newValue) -> {
|
||||
nextButton.setDisable(newValue != Boolean.TRUE);
|
||||
});
|
||||
counterpartyController.transactionProperty().addListener((observable, oldValue, newValue) -> {
|
||||
nextButton.setVisible(false);
|
||||
doneButton.setDisable(newValue == null);
|
||||
cancelButton.setDisable(newValue != null);
|
||||
});
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
doneButton.managedProperty().bind(doneButton.visibleProperty());
|
||||
|
||||
doneButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!counterpartyController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
doneButton.setDefaultButton(true);
|
||||
}
|
||||
nextButton.setDisable(counterpartyController.meetingReceivedProperty().get() != Boolean.TRUE);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
cancelButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(counterpartyController.meetingReceivedProperty().get() == Boolean.TRUE) {
|
||||
counterpartyController.cancel();
|
||||
}
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY));
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.soroban.client.OnlineSorobanInteraction;
|
||||
import com.samourai.soroban.client.meeting.SorobanResponseMessage;
|
||||
import com.samourai.soroban.client.wallet.SorobanWalletService;
|
||||
import com.samourai.soroban.client.wallet.sender.CahootsSorobanInitiatorListener;
|
||||
import com.samourai.wallet.cahoots.CahootsContext;
|
||||
import com.samourai.soroban.client.cahoots.OnlineCahootsMessage;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.samourai.wallet.cahoots.CahootsType;
|
||||
import com.samourai.wallet.cahoots.TxBroadcastInteraction;
|
||||
import com.samourai.wallet.sorobanClient.SorobanInteraction;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.crypto.EncryptionType;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.crypto.Key;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.PayNymAvatar;
|
||||
import com.sparrowwallet.sparrow.control.ProgressTimer;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletNodeHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import io.reactivex.Observable;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.beans.property.StringProperty;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.function.UnaryOperator;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
import static com.sparrowwallet.sparrow.paynym.PayNymController.PAYNYM_REGEX;
|
||||
|
||||
public class InitiatorController extends SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(InitiatorController.class);
|
||||
|
||||
private static final PayNym FIND_FOLLOWERS = new PayNym(null, null, "Retrieve Contacts...", false, Collections.emptyList(), Collections.emptyList());
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
private WalletTransaction walletTransaction;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private ComboBox<PayNym> payNymFollowers;
|
||||
|
||||
@FXML
|
||||
private TextField counterparty;
|
||||
|
||||
@FXML
|
||||
private ProgressIndicator payNymLoading;
|
||||
|
||||
@FXML
|
||||
private Button findPayNym;
|
||||
|
||||
@FXML
|
||||
private PayNymAvatar payNymAvatar;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step2Timer;
|
||||
|
||||
@FXML
|
||||
private Label step2Desc;
|
||||
|
||||
@FXML
|
||||
private Hyperlink meetingFail;
|
||||
|
||||
@FXML
|
||||
private ProgressBar sorobanProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label sorobanProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph mixDeclined;
|
||||
|
||||
@FXML
|
||||
private ProgressTimer step3Timer;
|
||||
|
||||
@FXML
|
||||
private Label step3Desc;
|
||||
|
||||
@FXML
|
||||
private TransactionDiagram transactionDiagram;
|
||||
|
||||
@FXML
|
||||
private Label step4Desc;
|
||||
|
||||
@FXML
|
||||
private ProgressBar broadcastProgressBar;
|
||||
|
||||
@FXML
|
||||
private Label broadcastProgressLabel;
|
||||
|
||||
@FXML
|
||||
private Glyph broadcastSuccessful;
|
||||
|
||||
private final StringProperty counterpartyPayNymName = new SimpleStringProperty();
|
||||
|
||||
private final ObjectProperty<PaymentCode> counterpartyPaymentCode = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Step> stepProperty = new SimpleObjectProperty<>(Step.SETUP);
|
||||
|
||||
private final ObjectProperty<Boolean> transactionAccepted = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Transaction> transactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private CahootsType cahootsType = CahootsType.STONEWALLX2;
|
||||
|
||||
private ElectrumServer.TransactionMempoolService transactionMempoolService;
|
||||
|
||||
private boolean closed;
|
||||
|
||||
private final ChangeListener<String> counterpartyListener = (observable, oldValue, newValue) -> {
|
||||
if(newValue != null) {
|
||||
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) {
|
||||
//Assumed valid payment code
|
||||
} else if(isUsePayNym(wallet) && PAYNYM_REGEX.matcher(newValue).matches()) {
|
||||
if(!newValue.equals(counterpartyPayNymName.get())) {
|
||||
searchPayNyms(newValue);
|
||||
}
|
||||
} else if(!newValue.equals(counterpartyPayNymName.get())) {
|
||||
counterpartyPayNymName.set(null);
|
||||
counterpartyPaymentCode.set(null);
|
||||
payNymAvatar.clearPaymentCode();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
this.walletTransaction = walletTransaction;
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
sorobanProgressBar.managedProperty().bind(sorobanProgressBar.visibleProperty());
|
||||
sorobanProgressLabel.managedProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.managedProperty().bind(mixDeclined.visibleProperty());
|
||||
sorobanProgressBar.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
mixDeclined.visibleProperty().bind(sorobanProgressLabel.visibleProperty().not());
|
||||
step2Timer.visibleProperty().bind(sorobanProgressLabel.visibleProperty());
|
||||
broadcastProgressBar.managedProperty().bind(broadcastProgressBar.visibleProperty());
|
||||
broadcastProgressLabel.managedProperty().bind(broadcastProgressLabel.visibleProperty());
|
||||
broadcastSuccessful.managedProperty().bind(broadcastSuccessful.visibleProperty());
|
||||
broadcastSuccessful.setVisible(false);
|
||||
|
||||
meetingFail.managedProperty().bind(meetingFail.visibleProperty());
|
||||
meetingFail.setVisited(true);
|
||||
meetingFail.setVisible(false);
|
||||
meetingFail.setOnAction(event -> {
|
||||
meetingFail.setVisible(false);
|
||||
step2Desc.setText("Retrying...");
|
||||
sorobanProgressLabel.setVisible(true);
|
||||
startInitiatorMeetAndInitiate(AppServices.getSorobanServices().getSoroban(walletId), wallet);
|
||||
step2Timer.start();
|
||||
});
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
transactionAccepted.addListener((observable, oldValue, accepted) -> {
|
||||
if(transactionProperty.get() != null && stepProperty.get() != Step.REBROADCAST) {
|
||||
Platform.exitNestedEventLoop(transactionAccepted, accepted);
|
||||
}
|
||||
});
|
||||
|
||||
transactionProperty.addListener((observable, oldValue, transaction) -> {
|
||||
if(transaction != null) {
|
||||
updateTransactionDiagram(transactionDiagram, wallet, walletTransaction, transaction);
|
||||
}
|
||||
});
|
||||
|
||||
step2.visibleProperty().addListener((observable, oldValue, visible) -> {
|
||||
if(visible) {
|
||||
startInitiatorMeetAndInitiate();
|
||||
step2Timer.start();
|
||||
}
|
||||
});
|
||||
|
||||
payNymLoading.managedProperty().bind(payNymLoading.visibleProperty());
|
||||
payNymLoading.maxHeightProperty().bind(counterparty.heightProperty());
|
||||
payNymLoading.setVisible(false);
|
||||
|
||||
payNymAvatar.managedProperty().bind(payNymAvatar.visibleProperty());
|
||||
payNymFollowers.prefWidthProperty().bind(counterparty.widthProperty());
|
||||
payNymFollowers.valueProperty().addListener((observable, oldValue, payNym) -> {
|
||||
if(payNym == FIND_FOLLOWERS) {
|
||||
setUsePayNym(wallet, true);
|
||||
setPayNymFollowers();
|
||||
} else if(payNym != null) {
|
||||
counterpartyPayNymName.set(payNym.nymName());
|
||||
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
counterparty.setText(payNym.nymName());
|
||||
step1.requestFocus();
|
||||
}
|
||||
});
|
||||
payNymFollowers.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(PayNym payNym) {
|
||||
return payNym == null ? "" : payNym.nymName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public PayNym fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
UnaryOperator<TextFormatter.Change> paymentCodeFilter = change -> {
|
||||
String input = change.getControlNewText();
|
||||
if(input.startsWith("P") && !input.contains("...")) {
|
||||
try {
|
||||
PaymentCode paymentCode = new PaymentCode(input);
|
||||
if(paymentCode.isValid()) {
|
||||
counterpartyPaymentCode.set(paymentCode);
|
||||
if(payNymAvatar.getPaymentCode() == null || !input.equals(payNymAvatar.getPaymentCode().toString())) {
|
||||
payNymAvatar.setPaymentCode(paymentCode);
|
||||
}
|
||||
|
||||
TextInputControl control = (TextInputControl)change.getControl();
|
||||
change.setText(input.substring(0, 12) + "..." + input.substring(input.length() - 5));
|
||||
change.setRange(0, control.getLength());
|
||||
change.setAnchor(change.getText().length());
|
||||
change.setCaretPosition(change.getText().length());
|
||||
}
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
return change;
|
||||
};
|
||||
counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
|
||||
counterparty.textProperty().addListener(counterpartyListener);
|
||||
counterparty.addEventFilter(KeyEvent.ANY, event -> {
|
||||
if(counterparty.isEditable() && event.getCode() == KeyCode.ENTER) {
|
||||
searchPayNyms(counterparty.getText());
|
||||
event.consume();
|
||||
}
|
||||
});
|
||||
|
||||
stepProperty.addListener((observable, oldValue, step) -> {
|
||||
if(step == Step.BROADCAST) {
|
||||
step4Desc.setText("Broadcasting the mix transaction...");
|
||||
broadcastProgressLabel.setVisible(true);
|
||||
} else if(step == Step.REBROADCAST) {
|
||||
step4Desc.setText("Rebroadcast the mix transaction.");
|
||||
broadcastProgressLabel.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
Payment payment = walletTransaction.getPayments().get(0);
|
||||
if(payment.getAddress() instanceof PayNymAddress payNymAddress) {
|
||||
PayNym payNym = payNymAddress.getPayNym();
|
||||
counterpartyPayNymName.set(payNym.nymName());
|
||||
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
counterparty.setText(payNym.nymName());
|
||||
counterparty.setEditable(false);
|
||||
findPayNym.setVisible(false);
|
||||
cahootsType = CahootsType.STOWAWAY;
|
||||
} else if(Config.get().isUsePayNym()) {
|
||||
setPayNymFollowers();
|
||||
} else {
|
||||
List<PayNym> defaultList = new ArrayList<>();
|
||||
defaultList.add(FIND_FOLLOWERS);
|
||||
payNymFollowers.setItems(FXCollections.observableList(defaultList));
|
||||
}
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
Platform.runLater(() -> {
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(counterparty, (Control c, String newValue) -> ValidationResult.fromErrorIf(c, "Invalid counterparty", !isValidCounterparty()));
|
||||
});
|
||||
}
|
||||
|
||||
private void searchPayNyms(String identifier) {
|
||||
payNymLoading.setVisible(true);
|
||||
PayNymService.getPayNym(identifier).subscribe(payNym -> {
|
||||
payNymLoading.setVisible(false);
|
||||
counterpartyPayNymName.set(payNym.nymName());
|
||||
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
counterparty.textProperty().removeListener(counterpartyListener);
|
||||
int caret = counterparty.getCaretPosition();
|
||||
counterparty.setText("");
|
||||
counterparty.setText(payNym.nymName());
|
||||
counterparty.positionCaret(caret);
|
||||
counterparty.textProperty().addListener(counterpartyListener);
|
||||
}, error -> {
|
||||
payNymLoading.setVisible(false);
|
||||
//ignore, probably doesn't exist but will try again on meeting request
|
||||
});
|
||||
}
|
||||
|
||||
private void setPayNymFollowers() {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
PayNymService.getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
|
||||
findPayNym.setVisible(true);
|
||||
payNymFollowers.setItems(FXCollections.observableList(followerPayNyms));
|
||||
}, error -> {
|
||||
if(error.getMessage().endsWith("404")) {
|
||||
setUsePayNym(masterWallet, false);
|
||||
AppServices.showErrorDialog("Could not retrieve PayNym", "This wallet does not have an associated PayNym or any followers yet. You can retrieve the PayNym using the Find PayNym button.");
|
||||
} else {
|
||||
log.warn("Could not retrieve followers: ", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void startInitiatorMeetAndInitiate() {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(payNymFollowers.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
startInitiatorMeetAndInitiate(soroban, wallet);
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(this::startInitiatorMeetAndInitiate);
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
} else {
|
||||
step2.setVisible(false);
|
||||
step1.setVisible(true);
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
startInitiatorMeetAndInitiate(soroban, wallet);
|
||||
}
|
||||
} else {
|
||||
startInitiatorMeetAndInitiate(soroban, wallet);
|
||||
}
|
||||
}
|
||||
|
||||
private void startInitiatorMeetAndInitiate(Soroban soroban, Wallet wallet) {
|
||||
getPaymentCodeCounterparty().subscribe(paymentCodeCounterparty -> {
|
||||
SparrowCahootsWallet cahootsWallet = soroban.getCahootsWallet(wallet);
|
||||
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getSpendableUtxos();
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
|
||||
cahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
|
||||
}
|
||||
|
||||
Payment payment = walletTransaction.getPayments().get(0);
|
||||
long feePerB = (long)walletTransaction.getFeeRate();
|
||||
CahootsContext cahootsContext = CahootsContext.newInitiator(cahootsWallet, cahootsType, cahootsWallet.getAccount(), feePerB, payment.getAmount(), payment.getAddress().getAddress(), paymentCodeCounterparty.toString());
|
||||
|
||||
CahootsSorobanInitiatorListener listener = new CahootsSorobanInitiatorListener() {
|
||||
@Override
|
||||
public void onResponse(SorobanResponseMessage sorobanResponse) throws Exception {
|
||||
super.onResponse(sorobanResponse);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
requestUserAttention();
|
||||
if(sorobanResponse.isAccept()) {
|
||||
sorobanProgressBar.setProgress(0.1);
|
||||
sorobanProgressLabel.setText("Mix partner accepted!");
|
||||
} else {
|
||||
step2Desc.setText("Mix partner declined.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInteraction(OnlineSorobanInteraction interaction) throws Exception {
|
||||
SorobanInteraction originInteraction = interaction.getInteraction();
|
||||
if(originInteraction instanceof TxBroadcastInteraction) {
|
||||
Platform.runLater(() -> {
|
||||
try {
|
||||
Boolean accepted = (Boolean)Platform.enterNestedEventLoop(transactionAccepted);
|
||||
if(accepted) {
|
||||
interaction.sorobanAccept();
|
||||
} else {
|
||||
interaction.sorobanReject("Mix partner declined to broadcast the transaction.");
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error accepting Soroban interaction", e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
throw new Exception("Unknown interaction: "+originInteraction.getTypeInteraction());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void progress(OnlineCahootsMessage cahootsMessage) {
|
||||
super.progress(cahootsMessage);
|
||||
|
||||
if(cahootsMessage != null) {
|
||||
Platform.runLater(() -> {
|
||||
Cahoots cahoots = cahootsMessage.getCahoots();
|
||||
sorobanProgressBar.setProgress((double)(cahoots.getStep() + 1) / 5);
|
||||
|
||||
if(cahoots.getStep() >= 3) {
|
||||
try {
|
||||
Transaction transaction = getTransaction(cahoots);
|
||||
if(transaction != null) {
|
||||
transactionProperty.set(transaction);
|
||||
if(cahoots.getStep() == 3) {
|
||||
next();
|
||||
step3Timer.start(e -> {
|
||||
if(stepProperty.get() != Step.BROADCAST && stepProperty.get() != Step.REBROADCAST) {
|
||||
step3Desc.setText("Transaction declined due to timeout.");
|
||||
transactionAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
});
|
||||
} else if(cahoots.getStep() == 4) {
|
||||
next();
|
||||
broadcastTransaction();
|
||||
}
|
||||
}
|
||||
} catch(PSBTParseException e) {
|
||||
log.error("Invalid collaborative PSBT created", e);
|
||||
step2Desc.setText("Invalid transaction created.");
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
SorobanWalletService sorobanWalletService = soroban.getSorobanWalletService();
|
||||
sorobanProgressLabel.setText("Waiting for mix partner...");
|
||||
|
||||
Service<Cahoots> cahootsService = new Service<>() {
|
||||
@Override
|
||||
protected Task<Cahoots> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Cahoots call() throws Exception {
|
||||
return sorobanWalletService.getSorobanWalletInitiator(cahootsWallet).meetAndInitiate(cahootsContext, paymentCodeCounterparty, listener);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
cahootsService.setOnFailed(event -> {
|
||||
Throwable error = event.getSource().getException();
|
||||
log.error("Error receiving meeting response", error);
|
||||
step2Desc.setText(getErrorMessage(error));
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
meetingFail.setVisible(true);
|
||||
requestUserAttention();
|
||||
});
|
||||
cahootsService.start();
|
||||
}, error -> {
|
||||
log.error("Could not retrieve payment code", error);
|
||||
if(error.getMessage().endsWith("404")) {
|
||||
step2Desc.setText("PayNym not found");
|
||||
} else if(error.getMessage().endsWith("400")) {
|
||||
step2Desc.setText("Could not retrieve PayNym");
|
||||
} else {
|
||||
step2Desc.setText(error.getMessage());
|
||||
}
|
||||
sorobanProgressLabel.setVisible(false);
|
||||
});
|
||||
}
|
||||
|
||||
public void broadcastTransaction() {
|
||||
stepProperty.set(Step.BROADCAST);
|
||||
|
||||
ElectrumServer.BroadcastTransactionService broadcastTransactionService = new ElectrumServer.BroadcastTransactionService(getTransaction());
|
||||
broadcastTransactionService.setOnRunning(workerStateEvent -> {
|
||||
broadcastProgressBar.setProgress(-1);
|
||||
});
|
||||
broadcastTransactionService.setOnSucceeded(workerStateEvent -> {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new HashMap<>();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
|
||||
for(TransactionInput txInput : getTransaction().getInputs()) {
|
||||
Optional<BlockTransactionHashIndex> optSelectedUtxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
|
||||
.findFirst();
|
||||
optSelectedUtxo.ifPresent(blockTransactionHashIndex -> selectedUtxos.put(blockTransactionHashIndex, walletTxos.get(blockTransactionHashIndex)));
|
||||
}
|
||||
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
|
||||
transactionMempoolService = new ElectrumServer.TransactionMempoolService(wallet, getTransaction().getTxId(), new HashSet<>(selectedUtxos.values()));
|
||||
transactionMempoolService.setDelay(Duration.seconds(3));
|
||||
transactionMempoolService.setPeriod(Duration.seconds(10));
|
||||
transactionMempoolService.setRestartOnFailure(false);
|
||||
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
|
||||
Set<String> scriptHashes = transactionMempoolService.getValue();
|
||||
if(!scriptHashes.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
|
||||
}
|
||||
|
||||
if(transactionMempoolService.getIterationCount() > 3 && transactionMempoolService.isRunning()) {
|
||||
transactionMempoolService.cancel();
|
||||
broadcastProgressBar.setProgress(0);
|
||||
log.error("Timeout searching for broadcasted transaction");
|
||||
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not register it in the mempool. It is safe to try broadcasting again.");
|
||||
stepProperty.set(Step.REBROADCAST);
|
||||
}
|
||||
});
|
||||
transactionMempoolService.setOnFailed(mempoolWorkerStateEvent -> {
|
||||
transactionMempoolService.cancel();
|
||||
broadcastProgressBar.setProgress(0);
|
||||
log.error("Timeout searching for broadcasted transaction");
|
||||
AppServices.showErrorDialog("Timeout searching for broadcasted transaction", "The transaction was broadcast but the server did not indicate it had entered the mempool. It is safe to try broadcasting again.");
|
||||
stepProperty.set(Step.REBROADCAST);
|
||||
});
|
||||
|
||||
if(!closed) {
|
||||
transactionMempoolService.start();
|
||||
}
|
||||
});
|
||||
broadcastTransactionService.setOnFailed(workerStateEvent -> {
|
||||
broadcastProgressBar.setProgress(0);
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
log.error("Error broadcasting transaction", exception);
|
||||
AppServices.showErrorDialog("Error broadcasting transaction", exception.getMessage());
|
||||
stepProperty.set(Step.REBROADCAST);
|
||||
});
|
||||
broadcastTransactionService.start();
|
||||
}
|
||||
|
||||
public void next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
stepProperty.set(Step.COMMUNICATE);
|
||||
return;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
stepProperty.set(Step.REVIEW);
|
||||
return;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
stepProperty.set(Step.BROADCAST);
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<PaymentCode> getPaymentCodeCounterparty() {
|
||||
if(counterpartyPaymentCode.get() != null) {
|
||||
return Observable.just(counterpartyPaymentCode.get());
|
||||
} else {
|
||||
return PayNymService.getPayNym(counterparty.getText()).map(payNym -> new PaymentCode(payNym.paymentCode().toString()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidCounterparty() {
|
||||
if(counterpartyPaymentCode.get() != null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if(counterparty.getText().startsWith("P") && counterparty.getText().contains("...") && counterparty.getText().length() == 20) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return PAYNYM_REGEX.matcher(counterparty.getText()).matches();
|
||||
}
|
||||
|
||||
public void accept() {
|
||||
transactionAccepted.set(Boolean.TRUE);
|
||||
}
|
||||
|
||||
public void cancel() {
|
||||
transactionAccepted.set(Boolean.FALSE);
|
||||
}
|
||||
|
||||
public void findPayNym(ActionEvent event) {
|
||||
PayNymDialog payNymDialog = new PayNymDialog(walletId, PayNymDialog.Operation.SELECT, false);
|
||||
payNymDialog.initOwner(payNymFollowers.getScene().getWindow());
|
||||
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||
optPayNym.ifPresent(payNym -> {
|
||||
counterpartyPayNymName.set(payNym.nymName());
|
||||
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
|
||||
payNymAvatar.setPaymentCode(payNym.paymentCode());
|
||||
counterparty.setText(payNym.nymName());
|
||||
step1.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
public ObjectProperty<PaymentCode> counterpartyPaymentCodeProperty() {
|
||||
return counterpartyPaymentCode;
|
||||
}
|
||||
|
||||
public ObjectProperty<Step> stepProperty() {
|
||||
return stepProperty;
|
||||
}
|
||||
|
||||
public Transaction getTransaction() {
|
||||
return transactionProperty.get();
|
||||
}
|
||||
|
||||
public void close() {
|
||||
closed = true;
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private static String getErrorMessage(Throwable error) {
|
||||
String message = error.getMessage() == null ? (error instanceof TimeoutException ? "Timed out receiving meeting response" : "Error receiving meeting response") : error.getMessage();
|
||||
String cutFrom = "Exception: ";
|
||||
int index = message.lastIndexOf(cutFrom);
|
||||
String msg = index < 0 ? message : message.substring(index + cutFrom.length());
|
||||
msg = msg.replace("#Cahoots", "mix transaction");
|
||||
msg = msg.endsWith(".") ? msg : msg + ".";
|
||||
return msg;
|
||||
}
|
||||
|
||||
public boolean isTransactionAccepted() {
|
||||
return transactionAccepted.get() == Boolean.TRUE;
|
||||
}
|
||||
|
||||
public ObjectProperty<Boolean> transactionAcceptedProperty() {
|
||||
return transactionAccepted;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletNodeHistoryChanged(WalletNodeHistoryChangedEvent event) {
|
||||
if(event.getWalletNode(wallet) != null) {
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
|
||||
broadcastProgressBar.setVisible(false);
|
||||
broadcastProgressLabel.setVisible(false);
|
||||
step4Desc.setText("Transaction broadcasted.");
|
||||
broadcastSuccessful.setVisible(true);
|
||||
}
|
||||
}
|
||||
|
||||
public enum Step {
|
||||
SETUP, COMMUNICATE, REVIEW, BROADCAST, REBROADCAST
|
||||
}
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.crypto.InvalidPasswordException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
|
||||
import com.sparrowwallet.sparrow.event.StorageEvent;
|
||||
import com.sparrowwallet.sparrow.event.TimedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class InitiatorDialog extends Dialog<Transaction> {
|
||||
private static final Logger log = LoggerFactory.getLogger(InitiatorDialog.class);
|
||||
|
||||
private final boolean confirmationRequired;
|
||||
|
||||
public InitiatorDialog(String walletId, Wallet wallet, WalletTransaction walletTransaction) {
|
||||
this.confirmationRequired = AppServices.getSorobanServices().getSoroban(walletId).getHdWallet() != null;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader initiatorLoader = new FXMLLoader(AppServices.class.getResource("soroban/initiator.fxml"));
|
||||
dialogPane.setContent(initiatorLoader.load());
|
||||
InitiatorController initiatorController = initiatorLoader.getController();
|
||||
initiatorController.initializeView(walletId, wallet, walletTransaction);
|
||||
|
||||
EventManager.get().register(initiatorController);
|
||||
|
||||
dialogPane.setPrefWidth(730);
|
||||
dialogPane.setPrefHeight(530);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("soroban/initiator.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType broadcastButtonType = new javafx.scene.control.ButtonType("Sign & Broadcast", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, cancelButtonType, broadcastButtonType, doneButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button cancelButton = (Button)dialogPane.lookupButton(cancelButtonType);
|
||||
Button broadcastButton = (Button)dialogPane.lookupButton(broadcastButtonType);
|
||||
Button doneButton = (Button)dialogPane.lookupButton(doneButtonType);
|
||||
nextButton.setDisable(initiatorController.counterpartyPaymentCodeProperty().get() == null);
|
||||
broadcastButton.setDisable(true);
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
cancelButton.managedProperty().bind(cancelButton.visibleProperty());
|
||||
broadcastButton.managedProperty().bind(broadcastButton.visibleProperty());
|
||||
doneButton.managedProperty().bind(doneButton.visibleProperty());
|
||||
|
||||
broadcastButton.setVisible(false);
|
||||
doneButton.setVisible(false);
|
||||
|
||||
initiatorController.counterpartyPaymentCodeProperty().addListener((observable, oldValue, paymentCode) -> {
|
||||
nextButton.setDisable(paymentCode == null || !AppServices.isConnected());
|
||||
});
|
||||
|
||||
initiatorController.stepProperty().addListener((observable, oldValue, step) -> {
|
||||
if(step == InitiatorController.Step.SETUP) {
|
||||
nextButton.setDisable(false);
|
||||
nextButton.setVisible(true);
|
||||
} else if(step == InitiatorController.Step.COMMUNICATE) {
|
||||
nextButton.setDisable(true);
|
||||
nextButton.setVisible(true);
|
||||
} else if(step == InitiatorController.Step.REVIEW) {
|
||||
nextButton.setVisible(false);
|
||||
broadcastButton.setVisible(true);
|
||||
broadcastButton.setDefaultButton(true);
|
||||
broadcastButton.setDisable(false);
|
||||
} else if(step == InitiatorController.Step.BROADCAST) {
|
||||
cancelButton.setVisible(false);
|
||||
broadcastButton.setVisible(false);
|
||||
doneButton.setVisible(true);
|
||||
doneButton.setDefaultButton(true);
|
||||
} else if(step == InitiatorController.Step.REBROADCAST) {
|
||||
cancelButton.setVisible(true);
|
||||
broadcastButton.setVisible(true);
|
||||
broadcastButton.setDisable(false);
|
||||
doneButton.setVisible(false);
|
||||
}
|
||||
});
|
||||
|
||||
initiatorController.transactionAcceptedProperty().addListener((observable, oldValue, accepted) -> {
|
||||
broadcastButton.setDisable(accepted != Boolean.TRUE);
|
||||
});
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
initiatorController.next();
|
||||
event.consume();
|
||||
});
|
||||
|
||||
cancelButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
initiatorController.cancel();
|
||||
});
|
||||
|
||||
broadcastButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(initiatorController.isTransactionAccepted()) {
|
||||
initiatorController.broadcastTransaction();
|
||||
} else {
|
||||
acceptAndBroadcast(initiatorController, walletId, wallet);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
initiatorController.close();
|
||||
EventManager.get().unregister(initiatorController);
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY) ? initiatorController.getTransaction() : null);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void acceptAndBroadcast(InitiatorController initiatorController, String walletId, Wallet wallet) {
|
||||
if(confirmationRequired && wallet.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(getDialogPane().getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
initiatorController.accept();
|
||||
password.get().clear();
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> acceptAndBroadcast(initiatorController, walletId, wallet));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
initiatorController.accept();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.soroban.client.SorobanConfig;
|
||||
import com.samourai.soroban.client.wallet.SorobanWalletService;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.util.ExtLibJConfig;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Soroban {
|
||||
private static final Logger log = LoggerFactory.getLogger(Soroban.class);
|
||||
|
||||
protected static final int TIMEOUT_MS = 60000;
|
||||
public static final List<Network> SOROBAN_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
private final SorobanWalletService sorobanWalletService;
|
||||
|
||||
private HD_Wallet hdWallet;
|
||||
private int bip47Account;
|
||||
private SparrowChainSupplier chainSupplier;
|
||||
|
||||
public Soroban() {
|
||||
SorobanConfig sorobanConfig = AppServices.getWhirlpoolServices().getSorobanConfig();
|
||||
this.sorobanWalletService = sorobanConfig.getSorobanWalletService();
|
||||
}
|
||||
|
||||
public HD_Wallet getHdWallet() {
|
||||
return hdWallet;
|
||||
}
|
||||
|
||||
public void setHDWallet(Wallet wallet) {
|
||||
ExtLibJConfig extLibJConfig = sorobanWalletService.getSorobanService().getSorobanConfig().getExtLibJConfig();
|
||||
NetworkParameters params = extLibJConfig.getSamouraiNetwork().getParams();
|
||||
hdWallet = Whirlpool.computeHdWallet(wallet, params);
|
||||
bip47Account = wallet.isMasterWallet() ? wallet.getAccountIndex() : wallet.getMasterWallet().getAccountIndex();
|
||||
}
|
||||
|
||||
public SparrowCahootsWallet getCahootsWallet(Wallet wallet) {
|
||||
if(wallet.getScriptType() != ScriptType.P2WPKH) {
|
||||
throw new IllegalArgumentException("Wallet must be P2WPKH");
|
||||
}
|
||||
|
||||
if(hdWallet == null) {
|
||||
for(Wallet associatedWallet : wallet.getAllWallets()) {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(associatedWallet);
|
||||
if(soroban != null && soroban.getHdWallet() != null) {
|
||||
hdWallet = soroban.hdWallet;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if(hdWallet == null) {
|
||||
throw new IllegalStateException("HD wallet is not set");
|
||||
}
|
||||
|
||||
try {
|
||||
if(chainSupplier == null) {
|
||||
chainSupplier = new SparrowChainSupplier(wallet.getStoredBlockHeight());
|
||||
chainSupplier.open();
|
||||
}
|
||||
return new SparrowCahootsWallet(chainSupplier, wallet, hdWallet, bip47Account);
|
||||
} catch(Exception e) {
|
||||
log.error("Could not create cahoots wallet", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getBip47Account() {
|
||||
return bip47Account;
|
||||
}
|
||||
|
||||
public SorobanWalletService getSorobanWalletService() {
|
||||
return sorobanWalletService;
|
||||
}
|
||||
|
||||
public void close() {
|
||||
if(chainSupplier != null) {
|
||||
chainSupplier.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.wallet.cahoots.Cahoots;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.TransactionDiagram;
|
||||
import com.sparrowwallet.sparrow.event.WalletConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.awt.Taskbar;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SorobanController {
|
||||
private static final Logger log = LoggerFactory.getLogger(SorobanController.class);
|
||||
|
||||
protected Transaction getTransaction(Cahoots cahoots) throws PSBTParseException {
|
||||
if(cahoots.getPSBT() != null) {
|
||||
PSBT psbt = new PSBT(cahoots.getPSBT().toBytes());
|
||||
return psbt.getTransaction();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected void updateTransactionDiagram(TransactionDiagram transactionDiagram, Wallet wallet, WalletTransaction walletTransaction, Transaction transaction) {
|
||||
WalletTransaction txWalletTransaction = getWalletTransaction(wallet, walletTransaction, transaction, null);
|
||||
transactionDiagram.update(txWalletTransaction);
|
||||
|
||||
if(txWalletTransaction.getSelectedUtxoSets().size() == 2) {
|
||||
Set<Sha256Hash> references = txWalletTransaction.getSelectedUtxoSets().get(1).keySet().stream().map(BlockTransactionHash::getHash).collect(Collectors.toSet());
|
||||
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
|
||||
transactionReferenceService.setOnSucceeded(successEvent -> {
|
||||
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
|
||||
transactionDiagram.update(getWalletTransaction(wallet, walletTransaction, transaction, transactionMap));
|
||||
});
|
||||
transactionReferenceService.setOnFailed(failedEvent -> {
|
||||
log.error("Failed to retrieve referenced transactions", failedEvent.getSource().getException());
|
||||
});
|
||||
transactionReferenceService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private WalletTransaction getWalletTransaction(Wallet wallet, WalletTransaction walletTransaction, Transaction transaction, Map<Sha256Hash, BlockTransaction> inputTransactions) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> allWalletUtxos = wallet.getWalletTxos();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = new LinkedHashMap<>();
|
||||
Map<BlockTransactionHashIndex, WalletNode> externalUtxos = new LinkedHashMap<>();
|
||||
|
||||
for(TransactionInput txInput : transaction.getInputs()) {
|
||||
Optional<BlockTransactionHashIndex> optWalletUtxo = allWalletUtxos.keySet().stream().filter(txo -> txo.getHash().equals(txInput.getOutpoint().getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()).findFirst();
|
||||
if(optWalletUtxo.isPresent()) {
|
||||
walletUtxos.put(optWalletUtxo.get(), allWalletUtxos.get(optWalletUtxo.get()));
|
||||
} else {
|
||||
BlockTransactionHashIndex externalUtxo;
|
||||
if(inputTransactions != null && inputTransactions.containsKey(txInput.getOutpoint().getHash())) {
|
||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
externalUtxo = new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
|
||||
} else {
|
||||
externalUtxo = new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
|
||||
}
|
||||
externalUtxos.put(externalUtxo, null);
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets = new ArrayList<>();
|
||||
selectedUtxoSets.add(walletUtxos);
|
||||
selectedUtxoSets.add(externalUtxos);
|
||||
|
||||
Map<Address, WalletNode> walletAddresses = wallet.getWalletAddresses();
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
|
||||
for(TransactionOutput txOutput : transaction.getOutputs()) {
|
||||
Address address = txOutput.getScript().getToAddress();
|
||||
if(address != null) {
|
||||
Optional<Payment> optPayment = walletTransaction == null ? Optional.empty() :
|
||||
walletTransaction.getPayments().stream().filter(payment -> payment.getAddress().equals(address) && payment.getAmount() == txOutput.getValue()).findFirst();
|
||||
if(optPayment.isPresent()) {
|
||||
payments.add(optPayment.get());
|
||||
} else if(walletAddresses.containsKey(address) && walletAddresses.get(address).getKeyPurpose() == KeyPurpose.CHANGE) {
|
||||
changeMap.put(walletAddresses.get(address), txOutput.getValue());
|
||||
} else {
|
||||
Payment payment = new Payment(address, null, txOutput.getValue(), false);
|
||||
if(transaction.getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
|
||||
payment.setType(Payment.Type.MIX);
|
||||
}
|
||||
payments.add(payment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
long fee = calculateFee(walletTransaction, selectedUtxoSets, transaction);
|
||||
return new WalletTransaction(wallet, transaction, Collections.emptyList(), selectedUtxoSets, payments, changeMap, fee, inputTransactions);
|
||||
}
|
||||
|
||||
private long calculateFee(WalletTransaction walletTransaction, List<Map<BlockTransactionHashIndex, WalletNode>> selectedUtxoSets, Transaction transaction) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = new LinkedHashMap<>();
|
||||
selectedUtxoSets.forEach(selectedUtxos::putAll);
|
||||
|
||||
long feeAmt = 0L;
|
||||
for(BlockTransactionHashIndex utxo : selectedUtxos.keySet()) {
|
||||
if(utxo.getValue() == 0) {
|
||||
return walletTransaction == null ? -1 : walletTransaction.getFee();
|
||||
}
|
||||
|
||||
feeAmt += utxo.getValue();
|
||||
}
|
||||
|
||||
for(TransactionOutput txOutput : transaction.getOutputs()) {
|
||||
feeAmt -= txOutput.getValue();
|
||||
}
|
||||
|
||||
return feeAmt;
|
||||
}
|
||||
|
||||
protected void requestUserAttention() {
|
||||
if(Taskbar.isTaskbarSupported() && Taskbar.getTaskbar().isSupported(Taskbar.Feature.USER_ATTENTION)) {
|
||||
Taskbar.getTaskbar().requestUserAttention(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isUsePayNym(Wallet wallet) {
|
||||
//TODO: Remove config setting
|
||||
boolean usePayNym = Config.get().isUsePayNym();
|
||||
if(usePayNym && wallet != null) {
|
||||
setUsePayNym(wallet, true);
|
||||
}
|
||||
|
||||
return usePayNym;
|
||||
}
|
||||
|
||||
protected void setUsePayNym(Wallet wallet, boolean usePayNym) {
|
||||
//TODO: Remove config setting
|
||||
if(Config.get().isUsePayNym() != usePayNym) {
|
||||
Config.get().setUsePayNym(usePayNym);
|
||||
}
|
||||
|
||||
if(wallet != null) {
|
||||
WalletConfig walletConfig = wallet.getMasterWalletConfig();
|
||||
if(walletConfig.isUsePayNym() != usePayNym) {
|
||||
walletConfig.setUsePayNym(usePayNym);
|
||||
EventManager.get().post(new WalletConfigChangedEvent(wallet.isMasterWallet() ? wallet : wallet.getMasterWallet()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.WalletTabsClosedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class SorobanServices {
|
||||
private static final Logger log = LoggerFactory.getLogger(SorobanServices.class);
|
||||
|
||||
private final Map<String, Soroban> sorobanMap = new HashMap<>();
|
||||
|
||||
public Soroban getSoroban(Wallet wallet) {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
for(Map.Entry<Wallet, Storage> entry : AppServices.get().getOpenWallets().entrySet()) {
|
||||
if(entry.getKey() == masterWallet) {
|
||||
return sorobanMap.get(entry.getValue().getWalletId(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Soroban getSoroban(String walletId) {
|
||||
Soroban soroban = sorobanMap.get(walletId);
|
||||
if(soroban == null) {
|
||||
soroban = new Soroban();
|
||||
sorobanMap.put(walletId, soroban);
|
||||
}
|
||||
|
||||
return soroban;
|
||||
}
|
||||
|
||||
public static boolean canWalletMix(Wallet wallet) {
|
||||
return Soroban.SOROBAN_NETWORKS.contains(Network.get())
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getScriptType() == ScriptType.P2WPKH;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData walletTabData : event.getClosedWalletTabData()) {
|
||||
String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet());
|
||||
Soroban soroban = sorobanMap.remove(walletId);
|
||||
if(soroban != null) {
|
||||
soroban.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.soroban;
|
||||
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.bip47.rpc.BIP47Wallet;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentAddress;
|
||||
import com.samourai.wallet.bip47.rpc.PaymentCode;
|
||||
import com.samourai.wallet.bip47.rpc.java.Bip47UtilJava;
|
||||
import com.samourai.wallet.bipFormat.BipFormat;
|
||||
import com.samourai.wallet.cahoots.AbstractCahootsWallet;
|
||||
import com.samourai.wallet.cahoots.CahootsUtxo;
|
||||
import com.samourai.wallet.chain.ChainSupplier;
|
||||
import com.samourai.wallet.hd.HD_Address;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.send.MyTransactionOutPoint;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class SparrowCahootsWallet extends AbstractCahootsWallet {
|
||||
private final Wallet wallet;
|
||||
private final HD_Wallet bip84w;
|
||||
private final int account;
|
||||
private final List<CahootsUtxo> utxos;
|
||||
private final Map<KeyPurpose, WalletNode> lastWalletNodes = new HashMap<>();
|
||||
|
||||
public SparrowCahootsWallet(ChainSupplier chainSupplier, Wallet wallet, HD_Wallet bip84w, int bip47Account) {
|
||||
super(chainSupplier, bip84w.getFingerprint(), new BIP47Wallet(bip84w).getAccount(bip47Account));
|
||||
this.wallet = wallet;
|
||||
this.bip84w = bip84w;
|
||||
this.account = wallet.getAccountIndex();
|
||||
this.utxos = new LinkedList<>();
|
||||
|
||||
bip84w.getAccount(account).getReceive().setAddrIdx(wallet.getFreshNode(KeyPurpose.RECEIVE).getIndex());
|
||||
bip84w.getAccount(account).getChange().setAddrIdx(wallet.getFreshNode(KeyPurpose.CHANGE).getIndex());
|
||||
|
||||
if(!wallet.isMasterWallet() && account != 0) {
|
||||
Wallet masterWallet = wallet.getMasterWallet();
|
||||
bip84w.getAccount(0).getReceive().setAddrIdx(masterWallet.getFreshNode(KeyPurpose.RECEIVE).getIndex());
|
||||
bip84w.getAccount(0).getChange().setAddrIdx(masterWallet.getFreshNode(KeyPurpose.CHANGE).getIndex());
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public int getAccount() {
|
||||
return account;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doFetchAddressReceive(int account, boolean increment, BipFormat bipFormat) throws Exception {
|
||||
if(account == StandardAccount.WHIRLPOOL_POSTMIX.getAccountNumber()) {
|
||||
// force change chain
|
||||
return getAddress(account, increment, KeyPurpose.CHANGE);
|
||||
}
|
||||
|
||||
return getAddress(account, increment, KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String doFetchAddressChange(int account, boolean increment, BipFormat bipFormat) throws Exception {
|
||||
return getAddress(account, increment, KeyPurpose.CHANGE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<CahootsUtxo> getUtxosWpkhByAccount(int account) {
|
||||
return utxos;
|
||||
}
|
||||
|
||||
private String getAddress(int account, boolean increment, KeyPurpose keyPurpose) {
|
||||
WalletNode addressNode = getWallet(account).getFreshNode(keyPurpose, increment ? lastWalletNodes.get(keyPurpose) : null);
|
||||
lastWalletNodes.put(keyPurpose, addressNode);
|
||||
return addressNode.getAddress().getAddress();
|
||||
}
|
||||
|
||||
private Wallet getWallet(int account) {
|
||||
if(account != this.account && account == 0 && !wallet.isMasterWallet()) {
|
||||
return wallet.getMasterWallet();
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
public void addUtxo(WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
if(node.getWallet().getScriptType() != ScriptType.P2WPKH) {
|
||||
return;
|
||||
}
|
||||
|
||||
NetworkParameters params = getBip47Account().getParams();
|
||||
UnspentOutput unspentOutput = Whirlpool.getUnspentOutput(node, blockTransaction, index);
|
||||
MyTransactionOutPoint myTransactionOutPoint = unspentOutput.computeOutpoint(params);
|
||||
|
||||
CahootsUtxo cahootsUtxo;
|
||||
if(node.getWallet().isBip47()) {
|
||||
try {
|
||||
String strPaymentCode = node.getWallet().getKeystores().get(0).getExternalPaymentCode().toString();
|
||||
HD_Address hdAddress = getBip47Account().addressAt(node.getIndex());
|
||||
PaymentAddress paymentAddress = Bip47UtilJava.getInstance().getPaymentAddress(new PaymentCode(strPaymentCode), 0, hdAddress, params);
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, paymentAddress.getReceiveECKey().getPrivKeyBytes());
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Cannot add BIP47 UTXO", e);
|
||||
}
|
||||
} else {
|
||||
HD_Address hdAddress = bip84w.getAddressAt(account, unspentOutput);
|
||||
cahootsUtxo = new CahootsUtxo(myTransactionOutPoint, node.getDerivationPath(), null, hdAddress.getECKey().getPrivKeyBytes());
|
||||
}
|
||||
|
||||
utxos.add(cahootsUtxo);
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import com.googlecode.lanterna.gui2.*;
|
||||
import com.googlecode.lanterna.gui2.dialogs.DialogWindow;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -47,7 +47,7 @@ final class AddAccountDialog extends DialogWindow {
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletMasterMixConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixDialog extends WalletDialog {
|
||||
private static final List<FeePriority> FEE_PRIORITIES = List.of(new FeePriority("Low", Tx0FeeTarget.MIN), new FeePriority("Normal", Tx0FeeTarget.BLOCKS_4), new FeePriority("High", Tx0FeeTarget.BLOCKS_2));
|
||||
|
||||
private final String walletId;
|
||||
private final List<UtxoEntry> utxoEntries;
|
||||
|
||||
private final TextBox scode;
|
||||
private final ComboBox<FeePriority> premixPriority;
|
||||
private final Label premixFeeRate;
|
||||
|
||||
private Pool mixPool;
|
||||
|
||||
public MixDialog(String walletId, WalletForm walletForm, List<UtxoEntry> utxoEntries) {
|
||||
super(walletForm.getWallet().getFullDisplayName() + " Premix Config", walletForm);
|
||||
|
||||
this.walletId = walletId;
|
||||
this.utxoEntries = utxoEntries;
|
||||
|
||||
setHints(List.of(Hint.CENTERED));
|
||||
|
||||
Wallet wallet = walletForm.getWallet();
|
||||
MixConfig mixConfig = wallet.getMasterMixConfig();
|
||||
|
||||
Panel mainPanel = new Panel();
|
||||
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
mainPanel.addComponent(new Label("SCODE"));
|
||||
scode = new TextBox(new TerminalSize(20, 1));
|
||||
mainPanel.addComponent(scode);
|
||||
|
||||
mainPanel.addComponent(new Label("Premix priority"));
|
||||
premixPriority = new ComboBox<>();
|
||||
FEE_PRIORITIES.forEach(premixPriority::addItem);
|
||||
mainPanel.addComponent(premixPriority);
|
||||
|
||||
mainPanel.addComponent(new Label("Premix fee rate"));
|
||||
premixFeeRate = new Label("");
|
||||
mainPanel.addComponent(premixFeeRate);
|
||||
|
||||
Panel buttonPanel = new Panel();
|
||||
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
|
||||
Button next = new Button("Next", this::onNext).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false));
|
||||
buttonPanel.addComponent(next);
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||
setComponent(mainPanel);
|
||||
|
||||
scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode());
|
||||
|
||||
premixPriority.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||
FeePriority feePriority = premixPriority.getItem(selectedIndex);
|
||||
premixFeeRate.setText(SparrowMinerFeeSupplier.getFee(Integer.parseInt(feePriority.getTx0FeeTarget().getFeeTarget().getValue())) + " sats/vB");
|
||||
});
|
||||
premixPriority.setSelectedIndex(1);
|
||||
|
||||
scode.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||
if(changedByUserInteraction) {
|
||||
scode.setText(newText.toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
mixConfig.setScode(newText.toUpperCase(Locale.ROOT));
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(wallet));
|
||||
});
|
||||
}
|
||||
|
||||
private void onNext() {
|
||||
MixPoolDialog mixPoolDialog = new MixPoolDialog(walletId, getWalletForm(), utxoEntries, premixPriority.getSelectedItem().getTx0FeeTarget());
|
||||
mixPool = mixPoolDialog.showDialog(SparrowTerminal.get().getGui());
|
||||
close();
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pool showDialog(WindowBasedTextGUI textGUI) {
|
||||
super.showDialog(textGUI);
|
||||
return mixPool;
|
||||
}
|
||||
|
||||
private static class FeePriority {
|
||||
private final String name;
|
||||
private final Tx0FeeTarget tx0FeeTarget;
|
||||
|
||||
public FeePriority(String name, Tx0FeeTarget tx0FeeTarget) {
|
||||
this.name = name;
|
||||
this.tx0FeeTarget = tx0FeeTarget;
|
||||
}
|
||||
|
||||
public Tx0FeeTarget getTx0FeeTarget() {
|
||||
return tx0FeeTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Previews;
|
||||
import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.event.WalletMasterMixConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.scene.control.ButtonBar;
|
||||
import javafx.scene.control.ButtonType;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.OptionalLong;
|
||||
|
||||
public class MixPoolDialog extends WalletDialog {
|
||||
private static final DisplayPool NULL_POOL = new DisplayPool(null);
|
||||
|
||||
private final String walletId;
|
||||
private final List<UtxoEntry> utxoEntries;
|
||||
private final Tx0FeeTarget tx0FeeTarget;
|
||||
|
||||
private final ComboBox<DisplayPool> pool;
|
||||
private final Label poolFeeLabel;
|
||||
private final Label poolFee;
|
||||
private final Label premixOutputs;
|
||||
private final Button broadcast;
|
||||
|
||||
private Tx0Previews tx0Previews;
|
||||
private final ObjectProperty<Tx0Preview> tx0PreviewProperty = new SimpleObjectProperty<>(null);
|
||||
private Pool mixPool;
|
||||
|
||||
public MixPoolDialog(String walletId, WalletForm walletForm, List<UtxoEntry> utxoEntries, Tx0FeeTarget tx0FeeTarget) {
|
||||
super(walletForm.getWallet().getFullDisplayName() + " Premix Pool", walletForm);
|
||||
|
||||
this.walletId = walletId;
|
||||
this.utxoEntries = utxoEntries;
|
||||
this.tx0FeeTarget = tx0FeeTarget;
|
||||
|
||||
setHints(List.of(Hint.CENTERED));
|
||||
|
||||
Panel mainPanel = new Panel();
|
||||
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
mainPanel.addComponent(new Label("Pool"));
|
||||
pool = new ComboBox<>();
|
||||
pool.addItem(NULL_POOL);
|
||||
pool.setEnabled(false);
|
||||
mainPanel.addComponent(pool);
|
||||
|
||||
poolFeeLabel = new Label("Pool fee");
|
||||
poolFeeLabel.setPreferredSize(new TerminalSize(21, 1));
|
||||
mainPanel.addComponent(poolFeeLabel);
|
||||
poolFee = new Label("");
|
||||
mainPanel.addComponent(poolFee);
|
||||
|
||||
mainPanel.addComponent(new Label("Premix outputs"));
|
||||
premixOutputs = new Label("");
|
||||
mainPanel.addComponent(premixOutputs);
|
||||
|
||||
Panel buttonPanel = new Panel();
|
||||
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
|
||||
broadcast = new Button("Broadcast", this::onBroadcast).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false));
|
||||
buttonPanel.addComponent(broadcast);
|
||||
broadcast.setEnabled(false);
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||
setComponent(mainPanel);
|
||||
|
||||
pool.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||
DisplayPool selectedPool = pool.getSelectedItem();
|
||||
if(selectedPool != NULL_POOL) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
poolFee.setText(format.formatSatsValue(selectedPool.pool.getFeeValue()) + " sats");
|
||||
fetchTx0Preview(selectedPool.pool);
|
||||
}
|
||||
});
|
||||
|
||||
tx0PreviewProperty.addListener((observable, oldValue, tx0Preview) -> {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
if(tx0Preview == null) {
|
||||
premixOutputs.setText("Calculating...");
|
||||
broadcast.setEnabled(false);
|
||||
} else {
|
||||
if(tx0Preview.getPool().getFeeValue() != tx0Preview.getTx0Data().getFeeValue()) {
|
||||
poolFeeLabel.setText("Pool fee (discounted)");
|
||||
} else {
|
||||
poolFeeLabel.setText("Pool fee");
|
||||
}
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
poolFee.setText(format.formatSatsValue(tx0Preview.getTx0Data().getFeeValue()) + " sats");
|
||||
premixOutputs.setText(tx0Preview.getNbPremix() + " UTXOs");
|
||||
broadcast.setEnabled(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Platform.runLater(this::fetchPools);
|
||||
}
|
||||
|
||||
private void onBroadcast() {
|
||||
mixPool = tx0PreviewProperty.get() == null ? null : tx0PreviewProperty.get().getPool();
|
||||
close();
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pool showDialog(WindowBasedTextGUI textGUI) {
|
||||
super.showDialog(textGUI);
|
||||
return mixPool;
|
||||
}
|
||||
|
||||
private void fetchPools() {
|
||||
long totalUtxoValue = utxoEntries.stream().mapToLong(Entry::getValue).sum();
|
||||
Whirlpool.PoolsService poolsService = new Whirlpool.PoolsService(AppServices.getWhirlpoolServices().getWhirlpool(walletId), totalUtxoValue);
|
||||
poolsService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Pool> availablePools = poolsService.getValue().stream().toList();
|
||||
if(availablePools.isEmpty()) {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> pool.setEnabled(false));
|
||||
|
||||
Whirlpool.PoolsService allPoolsService = new Whirlpool.PoolsService(AppServices.getWhirlpoolServices().getWhirlpool(walletId), null);
|
||||
allPoolsService.setOnSucceeded(poolsStateEvent -> {
|
||||
OptionalLong optMinValue = allPoolsService.getValue().stream().mapToLong(pool1 -> pool1.getPremixValueMin() + pool1.getFeeValue()).min();
|
||||
if(optMinValue.isPresent() && totalUtxoValue < optMinValue.getAsLong()) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
String satsValue = format.formatSatsValue(optMinValue.getAsLong()) + " sats";
|
||||
String btcValue = format.formatBtcValue(optMinValue.getAsLong()) + " BTC";
|
||||
AppServices.showErrorDialog("Insufficient UTXO Value", "No available pools. Select a value over " + (Config.get().getBitcoinUnit() == BitcoinUnit.BTC ? btcValue : satsValue) + ".");
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(this::close);
|
||||
}
|
||||
});
|
||||
allPoolsService.start();
|
||||
} else {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
pool.setEnabled(true);
|
||||
pool.clearItems();
|
||||
availablePools.stream().map(DisplayPool::new).forEach(pool::addItem);
|
||||
pool.setSelectedIndex(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
poolsService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("Error fetching pools", exception.getMessage(), ButtonType.CANCEL, new ButtonType("Retry", ButtonBar.ButtonData.APPLY));
|
||||
if(optButton.isPresent()) {
|
||||
if(optButton.get().getButtonData().equals(ButtonBar.ButtonData.APPLY)) {
|
||||
fetchPools();
|
||||
} else {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> pool.setEnabled(false));
|
||||
}
|
||||
}
|
||||
});
|
||||
poolsService.start();
|
||||
}
|
||||
|
||||
private void fetchTx0Preview(Pool pool) {
|
||||
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||
if(mixConfig.getScode() == null) {
|
||||
mixConfig.setScode("");
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
}
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
if(tx0Previews != null && mixConfig.getScode().equals(whirlpool.getScode()) && tx0FeeTarget == whirlpool.getTx0FeeTarget()) {
|
||||
Tx0Preview tx0Preview = tx0Previews.getTx0Preview(pool.getPoolId());
|
||||
tx0PreviewProperty.set(tx0Preview);
|
||||
} else {
|
||||
tx0Previews = null;
|
||||
whirlpool.setScode(mixConfig.getScode());
|
||||
whirlpool.setTx0FeeTarget(tx0FeeTarget);
|
||||
whirlpool.setMixFeeTarget(tx0FeeTarget);
|
||||
|
||||
Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, utxoEntries);
|
||||
tx0PreviewsService.setOnRunning(workerStateEvent -> {
|
||||
premixOutputs.setText("Calculating...");
|
||||
tx0PreviewProperty.set(null);
|
||||
});
|
||||
tx0PreviewsService.setOnSucceeded(workerStateEvent -> {
|
||||
tx0Previews = tx0PreviewsService.getValue();
|
||||
Tx0Preview tx0Preview = tx0Previews.getTx0Preview(this.pool.getSelectedItem() == null ? pool.getPoolId() : this.pool.getSelectedItem().pool.getPoolId());
|
||||
tx0PreviewProperty.set(tx0Preview);
|
||||
});
|
||||
tx0PreviewsService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Error fetching Tx0","Error fetching Tx0: " + exception.getMessage());
|
||||
});
|
||||
tx0PreviewsService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DisplayPool {
|
||||
private final Pool pool;
|
||||
|
||||
public DisplayPool(Pool pool) {
|
||||
this.pool = pool;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
if(pool == null) {
|
||||
return "Fetching pools...";
|
||||
}
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
return format.formatSatsValue(pool.getDenomination()) + " sats";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.terminal.wallet;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.WHIRLPOOL_BADBANK;
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.WHIRLPOOL_PREMIX;
|
||||
|
||||
public class MixToDialog extends WalletDialog {
|
||||
private static final DisplayWallet NONE_DISPLAY_WALLET = new DisplayWallet(null);
|
||||
|
||||
private MixConfig mixConfig;
|
||||
|
||||
private final ComboBox<DisplayWallet> mixToWallet;
|
||||
private final TextBox minimumMixes;
|
||||
private final ComboBox<DisplayIndexRange> indexRange;
|
||||
private final Button apply;
|
||||
|
||||
public MixToDialog(WalletForm walletForm) {
|
||||
super(walletForm.getWallet().getFullDisplayName() + " Mix To", walletForm);
|
||||
|
||||
setHints(List.of(Hint.CENTERED));
|
||||
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
this.mixConfig = wallet.getMasterMixConfig().copy();
|
||||
|
||||
Panel mainPanel = new Panel();
|
||||
mainPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(5).setVerticalSpacing(1));
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
mainPanel.addComponent(new Label("Mix to wallet"));
|
||||
mixToWallet = new ComboBox<>();
|
||||
mainPanel.addComponent(mixToWallet);
|
||||
|
||||
mainPanel.addComponent(new Label("Minimum mixes"));
|
||||
minimumMixes = new TextBox().setValidationPattern(Pattern.compile("[0-9]*"));
|
||||
mainPanel.addComponent(minimumMixes);
|
||||
|
||||
mainPanel.addComponent(new Label("Postmix index range"));
|
||||
indexRange = new ComboBox<>();
|
||||
mainPanel.addComponent(indexRange);
|
||||
|
||||
Panel buttonPanel = new Panel();
|
||||
buttonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||
buttonPanel.addComponent(new Button("Cancel", this::onCancel));
|
||||
apply = new Button("Apply", this::onApply).setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.CENTER, GridLayout.Alignment.CENTER, true, false));
|
||||
apply.setEnabled(false);
|
||||
buttonPanel.addComponent(apply);
|
||||
|
||||
mainPanel.addComponent(new EmptySpace(TerminalSize.ONE));
|
||||
|
||||
buttonPanel.setLayoutData(GridLayout.createLayoutData(GridLayout.Alignment.END, GridLayout.Alignment.CENTER,false,false)).addTo(mainPanel);
|
||||
setComponent(mainPanel);
|
||||
|
||||
List<DisplayWallet> allWallets = new ArrayList<>();
|
||||
allWallets.add(NONE_DISPLAY_WALLET);
|
||||
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
|
||||
&& (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH)
|
||||
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
|
||||
&& (openWallet.getStandardAccountType() == null || !List.of(WHIRLPOOL_PREMIX, WHIRLPOOL_BADBANK).contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
|
||||
allWallets.addAll(destinationWallets.stream().map(DisplayWallet::new).collect(Collectors.toList()));
|
||||
allWallets.forEach(mixToWallet::addItem);
|
||||
|
||||
String mixToWalletId = null;
|
||||
try {
|
||||
mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||
} catch(NoSuchElementException e) {
|
||||
//ignore, mix to wallet is not open
|
||||
}
|
||||
|
||||
if(mixToWalletId != null) {
|
||||
mixToWallet.setSelectedItem(new DisplayWallet(AppServices.get().getWallet(mixToWalletId)));
|
||||
} else {
|
||||
mixToWallet.setSelectedItem(NONE_DISPLAY_WALLET);
|
||||
}
|
||||
|
||||
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
|
||||
minimumMixes.setText(Integer.toString(initialMinMixes));
|
||||
|
||||
List<DisplayIndexRange> indexRanges = Arrays.stream(IndexRange.values()).map(DisplayIndexRange::new).collect(Collectors.toList());
|
||||
indexRanges.forEach(indexRange::addItem);
|
||||
|
||||
indexRange.setSelectedItem(new DisplayIndexRange(IndexRange.FULL));
|
||||
if(mixConfig.getIndexRange() != null) {
|
||||
try {
|
||||
indexRange.setSelectedItem(new DisplayIndexRange(IndexRange.valueOf(mixConfig.getIndexRange())));
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
|
||||
mixToWallet.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||
DisplayWallet newValue = mixToWallet.getSelectedItem();
|
||||
if(newValue == NONE_DISPLAY_WALLET) {
|
||||
mixConfig.setMixToWalletName(null);
|
||||
mixConfig.setMixToWalletFile(null);
|
||||
} else {
|
||||
mixConfig.setMixToWalletName(newValue.getWallet().getName());
|
||||
mixConfig.setMixToWalletFile(AppServices.get().getOpenWallets().get(newValue.getWallet()).getWalletFile());
|
||||
}
|
||||
apply.setEnabled(apply.isEnabled() || selectedIndex != previousSelection);
|
||||
});
|
||||
|
||||
minimumMixes.setTextChangeListener((newText, changedByUserInteraction) -> {
|
||||
try {
|
||||
int newValue = Integer.parseInt(newText);
|
||||
if(newValue < 2 || newValue > 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
mixConfig.setMinMixes(newValue);
|
||||
apply.setEnabled(true);
|
||||
} catch(NumberFormatException e) {
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
indexRange.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||
DisplayIndexRange newValue = indexRange.getSelectedItem();
|
||||
mixConfig.setIndexRange(newValue.getIndexRange().toString());
|
||||
apply.setEnabled(apply.isEnabled() || selectedIndex != previousSelection);
|
||||
});
|
||||
}
|
||||
|
||||
private void onCancel() {
|
||||
mixConfig = null;
|
||||
close();
|
||||
}
|
||||
|
||||
private void onApply() {
|
||||
close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object showDialog(WindowBasedTextGUI textGUI) {
|
||||
super.showDialog(textGUI);
|
||||
return mixConfig;
|
||||
}
|
||||
|
||||
private static class DisplayWallet {
|
||||
private final Wallet wallet;
|
||||
|
||||
public DisplayWallet(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return wallet == null ? "None" : wallet.getFullDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DisplayWallet that = (DisplayWallet) o;
|
||||
return Objects.equals(wallet, that.wallet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return wallet != null ? wallet.hashCode() : 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DisplayIndexRange {
|
||||
private final IndexRange indexRange;
|
||||
|
||||
public DisplayIndexRange(IndexRange indexRange) {
|
||||
this.indexRange = indexRange;
|
||||
}
|
||||
|
||||
public IndexRange getIndexRange() {
|
||||
return indexRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return indexRange.toString().charAt(0) + indexRange.toString().substring(1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
DisplayIndexRange that = (DisplayIndexRange) o;
|
||||
return indexRange == that.indexRange;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return indexRange.hashCode();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,28 +5,18 @@ import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.googlecode.lanterna.gui2.table.Table;
|
||||
import com.googlecode.lanterna.gui2.table.TableModel;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.terminal.ModalDialog;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
import com.sparrowwallet.sparrow.terminal.wallet.table.*;
|
||||
import com.sparrowwallet.sparrow.wallet.*;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UtxosDialog extends WalletDialog {
|
||||
@@ -37,59 +27,6 @@ public class UtxosDialog extends WalletDialog {
|
||||
private final Label utxoCount;
|
||||
private final Table<TableCell> utxos;
|
||||
|
||||
private Button startMix;
|
||||
private Button mixTo;
|
||||
private Button mixSelected;
|
||||
|
||||
private final ChangeListener<Boolean> mixingOnlineListener = (observable, oldValue, newValue) -> {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> startMix.setEnabled(newValue));
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingStartingListener = (observable, oldValue, newValue) -> {
|
||||
try {
|
||||
SparrowTerminal.get().getGuiThread().invokeAndWait(() -> {
|
||||
startMix.setEnabled(!newValue && AppServices.onlineProperty().get());
|
||||
startMix.setLabel(newValue && AppServices.onlineProperty().get() ? "Starting Mixing..." : isMixing() ? "Stop Mixing" : "Start Mixing");
|
||||
mixTo.setEnabled(!newValue);
|
||||
});
|
||||
} catch(InterruptedException e) {
|
||||
//ignore
|
||||
}
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingStoppingListener = (observable, oldValue, newValue) -> {
|
||||
try {
|
||||
SparrowTerminal.get().getGuiThread().invokeAndWait(() -> {
|
||||
startMix.setEnabled(!newValue && AppServices.onlineProperty().get());
|
||||
startMix.setLabel(newValue ? "Stopping Mixing..." : isMixing() ? "Stop Mixing" : "Start Mixing");
|
||||
mixTo.setEnabled(!newValue);
|
||||
});
|
||||
} catch(InterruptedException e) {
|
||||
//ignore
|
||||
}
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingListener = (observable, oldValue, newValue) -> {
|
||||
if(!newValue) {
|
||||
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getMixStatus() != null && utxoEntry.getMixStatus().getMixProgress() != null
|
||||
&& utxoEntry.getMixStatus().getMixProgress().getMixStep() != null
|
||||
&& utxoEntry.getMixStatus().getMixProgress().getMixStep().isInterruptable()) {
|
||||
whirlpoolMix(new WhirlpoolMixEvent(getWalletForm().getWallet(), utxoEntry.getHashIndex(), (MixProgress)null));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(newValue) {
|
||||
startMix.setLabel("Stop Mixing");
|
||||
} else if(whirlpool == null || !whirlpool.stoppingProperty().get()) {
|
||||
startMix.setLabel("Start Mixing");
|
||||
}
|
||||
};
|
||||
|
||||
public UtxosDialog(WalletForm walletForm) {
|
||||
super(walletForm.getWallet().getFullDisplayName() + " UTXOs", walletForm);
|
||||
|
||||
@@ -116,7 +53,6 @@ public class UtxosDialog extends WalletDialog {
|
||||
if(utxos.getTableModel().getRowCount() > utxos.getSelectedRow()) {
|
||||
TableCell dateCell = utxos.getTableModel().getRow(utxos.getSelectedRow()).get(0);
|
||||
dateCell.setSelected(!dateCell.isSelected());
|
||||
updateMixSelectedButton();
|
||||
}
|
||||
});
|
||||
utxos.setInputFilter((interactable, keyStroke) -> {
|
||||
@@ -138,51 +74,12 @@ public class UtxosDialog extends WalletDialog {
|
||||
updateHistory(getWalletForm().getWalletUtxosEntry());
|
||||
|
||||
Panel buttonPanel = new Panel(new GridLayout(5).setHorizontalSpacing(2).setVerticalSpacing(0));
|
||||
if(getWalletForm().getWallet().isWhirlpoolMixWallet()) {
|
||||
startMix = new Button("Start Mixing", this::toggleMixing).setSize(new TerminalSize(20, 1)).addTo(buttonPanel);
|
||||
startMix.setEnabled(AppServices.onlineProperty().get());
|
||||
|
||||
mixTo = new Button("Mix to...", this::showMixToDialog);
|
||||
if(getWalletForm().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX) {
|
||||
buttonPanel.addComponent(mixTo);
|
||||
} else {
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
}
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool != null) {
|
||||
startMix.setLabel(whirlpool.isMixing() ? "Stop Mixing" : "Start Mixing");
|
||||
if(whirlpool.startingProperty().getValue()) {
|
||||
mixingStartingListener.changed(whirlpool.startingProperty(), null, whirlpool.startingProperty().getValue());
|
||||
}
|
||||
whirlpool.startingProperty().addListener(new WeakChangeListener<>(mixingStartingListener));
|
||||
if(whirlpool.stoppingProperty().getValue()) {
|
||||
mixingStoppingListener.changed(whirlpool.stoppingProperty(), null, whirlpool.stoppingProperty().getValue());
|
||||
}
|
||||
whirlpool.stoppingProperty().addListener(new WeakChangeListener<>(mixingStoppingListener));
|
||||
whirlpool.mixingProperty().addListener(new WeakChangeListener<>(mixingListener));
|
||||
updateMixToButton();
|
||||
}
|
||||
|
||||
AppServices.onlineProperty().addListener(new WeakChangeListener<>(mixingOnlineListener));
|
||||
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
|
||||
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||
} else {
|
||||
if(WhirlpoolServices.canWalletMix(getWalletForm().getWallet())) {
|
||||
mixSelected = new Button("Mix Selected", this::mixSelected);
|
||||
mixSelected.setEnabled(false);
|
||||
buttonPanel.addComponent(mixSelected);
|
||||
} else {
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
}
|
||||
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
|
||||
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||
}
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
buttonPanel.addComponent(new EmptySpace(new TerminalSize(15, 1)));
|
||||
buttonPanel.addComponent(new Button("Back", () -> onBack(Function.UTXOS)));
|
||||
buttonPanel.addComponent(new Button("Refresh", this::onRefresh));
|
||||
|
||||
Panel mainPanel = new Panel();
|
||||
mainPanel.setLayoutManager(new LinearLayout(Direction.VERTICAL).setSpacing(1));
|
||||
@@ -250,161 +147,10 @@ public class UtxosDialog extends WalletDialog {
|
||||
utxoCount.setText(walletUtxosEntry.getChildren() != null ? Integer.toString(walletUtxosEntry.getChildren().size()) : "0");
|
||||
}
|
||||
|
||||
private boolean isMixing() {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
return whirlpool != null && whirlpool.isMixing();
|
||||
}
|
||||
|
||||
public void toggleMixing() {
|
||||
if(isMixing()) {
|
||||
stopMixing();
|
||||
} else {
|
||||
startMixing();
|
||||
}
|
||||
}
|
||||
|
||||
public void startMixing() {
|
||||
startMix.setEnabled(false);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) {
|
||||
AppServices.getWhirlpoolServices().startWhirlpool(getWalletForm().getWallet(), whirlpool, true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void stopMixing() {
|
||||
startMix.setEnabled(AppServices.onlineProperty().get());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool.isStarted()) {
|
||||
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, true);
|
||||
} else {
|
||||
//Ensure http clients are shutdown
|
||||
whirlpool.shutdown();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void showMixToDialog() {
|
||||
MixToDialog mixToDialog = new MixToDialog(getWalletForm());
|
||||
MixConfig changedMixConfig = (MixConfig)mixToDialog.showDialog(SparrowTerminal.get().getGui());
|
||||
|
||||
if(changedMixConfig != null) {
|
||||
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||
|
||||
mixConfig.setMixToWalletName(changedMixConfig.getMixToWalletName());
|
||||
mixConfig.setMixToWalletFile(changedMixConfig.getMixToWalletFile());
|
||||
mixConfig.setMinMixes(changedMixConfig.getMinMixes());
|
||||
mixConfig.setIndexRange(changedMixConfig.getIndexRange());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
whirlpool.setPostmixIndexRange(mixConfig.getIndexRange());
|
||||
try {
|
||||
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||
whirlpool.setMixToWallet(mixToWalletId, mixConfig.getMinMixes());
|
||||
} catch(NoSuchElementException e) {
|
||||
mixConfig.setMixToWalletName(null);
|
||||
mixConfig.setMixToWalletFile(null);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
whirlpool.setMixToWallet(null, null);
|
||||
}
|
||||
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(this::updateMixToButton);
|
||||
if(whirlpool.isStarted()) {
|
||||
//Will automatically restart
|
||||
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMixToButton() {
|
||||
if(mixTo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||
if(mixConfig != null && mixConfig.getMixToWalletName() != null) {
|
||||
mixTo.setLabel("Mixing to " + mixConfig.getMixToWalletName());
|
||||
try {
|
||||
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||
String mixToName = AppServices.get().getWallet(mixToWalletId).getFullDisplayName();
|
||||
mixTo.setLabel("Mixing to " + mixToName);
|
||||
} catch(NoSuchElementException e) {
|
||||
mixTo.setLabel("! Not Open");
|
||||
}
|
||||
} else {
|
||||
mixTo.setLabel("Mix to...");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMixSelectedButton() {
|
||||
if(mixSelected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
mixSelected.setEnabled(!getSelectedEntries().isEmpty());
|
||||
}
|
||||
|
||||
private List<UtxoEntry> getSelectedEntries() {
|
||||
return utxos.getTableModel().getRows().stream().map(row -> row.get(0)).filter(TableCell::isSelected).map(dateCell -> (UtxoEntry)dateCell.getEntry()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private void mixSelected() {
|
||||
MixDialog mixDialog = new MixDialog(getWalletForm().getMasterWalletId(), getWalletForm(), getSelectedEntries());
|
||||
Pool pool = mixDialog.showDialog(SparrowTerminal.get().getGui());
|
||||
|
||||
if(pool != null) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
if(wallet.isMasterWallet() && !wallet.isWhirlpoolMasterWallet()) {
|
||||
addAccount(wallet, StandardAccount.WHIRLPOOL_PREMIX, () -> broadcastPremix(pool));
|
||||
} else {
|
||||
Platform.runLater(() -> broadcastPremix(pool));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPremix(Pool pool) {
|
||||
ModalDialog broadcastingDialog = new ModalDialog(getWalletForm().getWallet().getFullDisplayName(), "Broadcasting premix...");
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> SparrowTerminal.get().getGui().addWindow(broadcastingDialog));
|
||||
|
||||
//The WhirlpoolWallet has already been configured
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getStorage().getWalletId(getWalletForm().getMasterWallet()));
|
||||
List<BlockTransactionHashIndex> utxos = getSelectedEntries().stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||
Whirlpool.Tx0BroadcastService tx0BroadcastService = new Whirlpool.Tx0BroadcastService(whirlpool, pool, utxos);
|
||||
tx0BroadcastService.setOnSucceeded(workerStateEvent -> {
|
||||
Sha256Hash txid = tx0BroadcastService.getValue();
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
SparrowTerminal.get().getGui().removeWindow(broadcastingDialog);
|
||||
AppServices.showSuccessDialog("Broadcast Successful", "Premix transaction id:\n" + txid.toString());
|
||||
});
|
||||
});
|
||||
tx0BroadcastService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
String message = exception.getMessage();
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
SparrowTerminal.get().getGui().removeWindow(broadcastingDialog);
|
||||
AppServices.showErrorDialog("Error broadcasting premix transaction", message);
|
||||
});
|
||||
});
|
||||
tx0BroadcastService.start();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletNodesChanged(WalletNodesChangedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
@@ -424,60 +170,6 @@ public class UtxosDialog extends WalletDialog {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void whirlpoolMix(WhirlpoolMixEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().equals(event.getUtxo())) {
|
||||
if(event.getNextUtxo() != null) {
|
||||
utxoEntry.setNextMixUtxo(event.getNextUtxo());
|
||||
} else if(event.getMixFailReason() != null) {
|
||||
utxoEntry.setMixFailReason(event.getMixFailReason(), event.getMixError());
|
||||
} else {
|
||||
utxoEntry.setMixProgress(event.getMixProgress());
|
||||
}
|
||||
|
||||
TableModel<TableCell> tableModel = utxos.getTableModel();
|
||||
for(int row = 0; row < tableModel.getRowCount(); row++) {
|
||||
UtxoEntry tableEntry = (UtxoEntry)tableModel.getRow(row).get(0).getEntry();
|
||||
if(tableEntry.getHashIndex().equals(event.getUtxo())) {
|
||||
final int utxoRow = row;
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(() -> {
|
||||
tableModel.setCell(2, utxoRow, new MixTableCell(utxoEntry));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool != null) {
|
||||
for(Entry entry : getWalletForm().getWalletUtxosEntry().getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
MixProgress mixProgress = whirlpool.getMixProgress(utxoEntry.getHashIndex());
|
||||
if(mixProgress != null || utxoEntry.getMixStatus() == null || (utxoEntry.getMixStatus().getMixFailReason() == null && utxoEntry.getMixStatus().getNextMixUtxo() == null)) {
|
||||
whirlpoolMix(new WhirlpoolMixEvent(getWalletForm().getWallet(), utxoEntry.getHashIndex(), mixProgress));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(this::updateMixToButton);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletLabelChanged(WalletLabelChangedEvent event) {
|
||||
SparrowTerminal.get().getGuiThread().invokeLater(this::updateMixToButton);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void includeMempoolOutputsChangedEvent(IncludeMempoolOutputsChangedEvent event) {
|
||||
updateHistory(getWalletForm().getWalletUtxosEntry());
|
||||
|
||||
@@ -22,7 +22,6 @@ import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.terminal.SparrowTerminal;
|
||||
import com.sparrowwallet.sparrow.wallet.Function;
|
||||
import com.sparrowwallet.sparrow.wallet.WalletForm;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Platform;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -125,7 +124,7 @@ public class WalletDialog extends DialogWindow {
|
||||
private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
|
||||
List<Wallet> childWallets;
|
||||
if(StandardAccount.isWhirlpoolAccount(standardAccount)) {
|
||||
childWallets = WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
|
||||
childWallets = AppServices.addWhirlpoolWallets(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
|
||||
} else {
|
||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.terminal.wallet.table;
|
||||
|
||||
import com.google.common.base.Strings;
|
||||
import com.googlecode.lanterna.Symbols;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
|
||||
public class MixTableCell extends TableCell {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
public static final int WIDTH = 18;
|
||||
|
||||
public MixTableCell(Entry entry) {
|
||||
@@ -18,48 +14,12 @@ public class MixTableCell extends TableCell {
|
||||
@Override
|
||||
public String formatCell() {
|
||||
if(entry instanceof UtxoEntry utxoEntry) {
|
||||
if(utxoEntry.getMixStatus() != null) {
|
||||
UtxoEntry.MixStatus mixStatus = utxoEntry.getMixStatus();
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
return getMixSuccess(mixStatus);
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
return getMixFail(mixStatus);
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
return getMixProgress(mixStatus, mixStatus.getMixProgress());
|
||||
}
|
||||
}
|
||||
|
||||
return getMixCountOnly(utxoEntry.getMixStatus());
|
||||
return getMixCountOnly(utxoEntry.mixStatusProperty().get());
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
private String getMixSuccess(UtxoEntry.MixStatus mixStatus) {
|
||||
String msg = "Success!";
|
||||
String mixesDone = Strings.padStart(Integer.toString(mixStatus.getMixesDone()), WIDTH - msg.length(), ' ');
|
||||
return msg + mixesDone;
|
||||
}
|
||||
|
||||
private String getMixFail(UtxoEntry.MixStatus mixStatus) {
|
||||
long elapsed = mixStatus.getMixErrorTimestamp() == null ? 0L : System.currentTimeMillis() - mixStatus.getMixErrorTimestamp();
|
||||
if(!mixStatus.getMixFailReason().isError() || elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
return getMixCountOnly(mixStatus);
|
||||
}
|
||||
|
||||
String msg = mixStatus.getMixFailReason().getMessage();
|
||||
msg = msg.length() > 14 ? msg.substring(0, 14) : msg;
|
||||
String mixesDone = Strings.padStart(Integer.toString(mixStatus.getMixesDone()), WIDTH - msg.length(), ' ');
|
||||
return msg + mixesDone;
|
||||
}
|
||||
|
||||
private String getMixProgress(UtxoEntry.MixStatus mixStatus, MixProgress mixProgress) {
|
||||
int progress = mixProgress.getMixStep().getProgressPercent();
|
||||
String progressBar = Strings.padEnd(Strings.repeat(Character.toString(Symbols.BLOCK_SOLID), progress / 10), 10, ' ');
|
||||
String mixesDone = Strings.padStart(Integer.toString(mixStatus.getMixesDone()), WIDTH - 10, ' ');
|
||||
return progressBar + mixesDone;
|
||||
}
|
||||
|
||||
private String getMixCountOnly(UtxoEntry.MixStatus mixStatus) {
|
||||
return Strings.padStart(Integer.toString(mixStatus == null ? 0 : mixStatus.getMixesDone()), WIDTH, ' ');
|
||||
}
|
||||
|
||||
@@ -1386,7 +1386,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
if(id.getScene().getWindow().equals(event.getWindow()) && headersForm.getPsbt() != null && headersForm.getBlockTransaction() == null) {
|
||||
List<Wallet> availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSign(headersForm.getPsbt())).sorted(new WalletSignComparator()).collect(Collectors.toList());
|
||||
List<Wallet> availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSignAllInputs(headersForm.getPsbt())).sorted(new WalletSignComparator()).collect(Collectors.toList());
|
||||
if(availableWallets.isEmpty()) {
|
||||
for(Wallet wallet : event.getWalletsMap().keySet()) {
|
||||
if(wallet.isValid() && !wallet.getSigningKeystores(headersForm.getPsbt()).isEmpty()) {
|
||||
|
||||
@@ -1,142 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.IntegerSpinner;
|
||||
import com.sparrowwallet.sparrow.event.MixToConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.ComboBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.net.URL;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.drongo.wallet.StandardAccount.*;
|
||||
|
||||
public class MixToController implements Initializable {
|
||||
private static final Wallet NONE_WALLET = new Wallet("None");
|
||||
|
||||
@FXML
|
||||
private ComboBox<Wallet> mixToWallets;
|
||||
|
||||
@FXML
|
||||
private IntegerSpinner minMixes;
|
||||
|
||||
@FXML
|
||||
private ComboBox<IndexRange> indexRange;
|
||||
|
||||
private MixConfig mixConfig;
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
|
||||
}
|
||||
|
||||
public void initializeView(Wallet wallet) {
|
||||
mixConfig = wallet.getMasterMixConfig().copy();
|
||||
|
||||
List<Wallet> allWallets = new ArrayList<>();
|
||||
allWallets.add(NONE_WALLET);
|
||||
|
||||
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
|
||||
&& (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH)
|
||||
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
|
||||
&& (openWallet.getStandardAccountType() == null || !List.of(WHIRLPOOL_PREMIX, WHIRLPOOL_BADBANK).contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
|
||||
allWallets.addAll(destinationWallets);
|
||||
|
||||
mixToWallets.setItems(FXCollections.observableList(allWallets));
|
||||
mixToWallets.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Wallet wallet) {
|
||||
return wallet == null ? "" : wallet.getFullDisplayName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
String mixToWalletId = null;
|
||||
try {
|
||||
mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||
} catch(NoSuchElementException e) {
|
||||
//ignore, mix to wallet is not open
|
||||
}
|
||||
|
||||
if(mixToWalletId != null) {
|
||||
mixToWallets.setValue(AppServices.get().getWallet(mixToWalletId));
|
||||
} else {
|
||||
mixToWallets.setValue(NONE_WALLET);
|
||||
}
|
||||
|
||||
mixToWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == NONE_WALLET) {
|
||||
mixConfig.setMixToWalletName(null);
|
||||
mixConfig.setMixToWalletFile(null);
|
||||
} else {
|
||||
mixConfig.setMixToWalletName(newValue.getName());
|
||||
mixConfig.setMixToWalletFile(AppServices.get().getOpenWallets().get(newValue).getWalletFile());
|
||||
}
|
||||
|
||||
EventManager.get().post(new MixToConfigChangedEvent(wallet));
|
||||
});
|
||||
|
||||
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
|
||||
minMixes.setValueFactory(new IntegerSpinner.ValueFactory(2, 10000, initialMinMixes));
|
||||
minMixes.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == null || newValue < 2 || newValue > 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
mixConfig.setMinMixes(newValue);
|
||||
EventManager.get().post(new MixToConfigChangedEvent(wallet));
|
||||
});
|
||||
|
||||
indexRange.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(IndexRange indexRange) {
|
||||
if(indexRange == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return indexRange.toString().charAt(0) + indexRange.toString().substring(1).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public IndexRange fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
indexRange.setValue(IndexRange.FULL);
|
||||
if(mixConfig.getIndexRange() != null) {
|
||||
try {
|
||||
indexRange.setValue(IndexRange.valueOf(mixConfig.getIndexRange()));
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
indexRange.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
mixConfig.setIndexRange(newValue.toString());
|
||||
EventManager.get().post(new MixToConfigChangedEvent(wallet));
|
||||
});
|
||||
}
|
||||
|
||||
public void close() {
|
||||
minMixes.commitValue();
|
||||
}
|
||||
|
||||
public MixConfig getMixConfig() {
|
||||
return mixConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
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.event.MixToConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
import org.controlsfx.tools.Borders;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.NoSuchElementException;
|
||||
|
||||
public class MixToDialog extends Dialog<MixConfig> {
|
||||
private final Wallet wallet;
|
||||
private final Button applyButton;
|
||||
|
||||
public MixToDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
this.wallet = wallet;
|
||||
|
||||
try {
|
||||
FXMLLoader mixToLoader = new FXMLLoader(AppServices.class.getResource("wallet/mixto.fxml"));
|
||||
dialogPane.setContent(Borders.wrap(mixToLoader.load()).emptyBorder().buildAll());
|
||||
MixToController mixToController = mixToLoader.getController();
|
||||
mixToController.initializeView(wallet);
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(wallet);
|
||||
final ButtonType closeButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType applyButtonType = new javafx.scene.control.ButtonType(whirlpool.isStarted() ? "Restart Whirlpool" : "Apply", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(closeButtonType, applyButtonType);
|
||||
|
||||
applyButton = (Button)dialogPane.lookupButton(applyButtonType);
|
||||
applyButton.setDisable(true);
|
||||
applyButton.setDefaultButton(true);
|
||||
|
||||
try {
|
||||
AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(wallet.getMasterMixConfig());
|
||||
} catch(NoSuchElementException e) {
|
||||
applyButton.setDisable(false);
|
||||
}
|
||||
|
||||
dialogPane.setPrefWidth(400);
|
||||
dialogPane.setPrefHeight(300);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton == applyButtonType ? mixToController.getMixConfig() : null);
|
||||
|
||||
setOnCloseRequest(event -> {
|
||||
mixToController.close();
|
||||
EventManager.get().unregister(this);
|
||||
});
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void mixToConfigChanged(MixToConfigChangedEvent event) {
|
||||
if(event.getWallet() == wallet) {
|
||||
applyButton.setDisable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,9 +26,7 @@ import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.*;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -52,7 +50,6 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.URL;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.util.*;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -128,8 +125,6 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
emptyAmountProperty.set(true);
|
||||
}
|
||||
|
||||
updateMixOnlyStatus();
|
||||
|
||||
sendController.updateTransaction();
|
||||
}
|
||||
};
|
||||
@@ -166,8 +161,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
openWallets.prefWidthProperty().bind(address.widthProperty());
|
||||
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == payNymWallet) {
|
||||
boolean selectLinkedOnly = sendController.getPaymentTabs().getTabs().size() > 1 || !SorobanServices.canWalletMix(sendController.getWalletForm().getWallet());
|
||||
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly);
|
||||
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND);
|
||||
payNymDialog.initOwner(scanQrButton.getScene().getWindow());
|
||||
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
|
||||
optPayNym.ifPresent(this::setPayNym);
|
||||
@@ -208,7 +202,6 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
});
|
||||
|
||||
payNymProperty.addListener((observable, oldValue, payNym) -> {
|
||||
updateMixOnlyStatus(payNym);
|
||||
revalidateAmount();
|
||||
});
|
||||
|
||||
@@ -325,29 +318,11 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
address.setText(payNym.nymName());
|
||||
address.leftProperty().set(getPayNymGlyph());
|
||||
label.requestFocus();
|
||||
if(existingPayNym != null && payNym.nymName().equals(existingPayNym.nymName()) && payNym.isCollaborativeSend() != existingPayNym.isCollaborativeSend()) {
|
||||
if(existingPayNym != null && payNym.nymName().equals(existingPayNym.nymName())) {
|
||||
sendController.updateTransaction();
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMixOnlyStatus() {
|
||||
updateMixOnlyStatus(payNymProperty.get());
|
||||
}
|
||||
|
||||
public void updateMixOnlyStatus(PayNym payNym) {
|
||||
boolean mixOnly = false;
|
||||
try {
|
||||
mixOnly = payNym != null && getRecipientAddress() instanceof PayNymAddress;
|
||||
} catch(InvalidAddressException e) {
|
||||
log.error("Error creating payment code from PayNym", e);
|
||||
}
|
||||
|
||||
addPaymentButton.setDisable(mixOnly);
|
||||
if(mixOnly) {
|
||||
sendController.setPayNymMixOnlyPayment();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateOpenWallets() {
|
||||
updateOpenWallets(AppServices.get().getOpenWallets().keySet());
|
||||
}
|
||||
@@ -424,27 +399,22 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
return Address.fromString(address.getText());
|
||||
}
|
||||
|
||||
if(!payNym.isCollaborativeSend()) {
|
||||
try {
|
||||
Wallet recipientBip47Wallet = getWalletForPayNym(payNym);
|
||||
if(recipientBip47Wallet != null) {
|
||||
int index = sendController.getPayNymSendIndex(this);
|
||||
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
|
||||
for(int i = 0; i < index; i++) {
|
||||
sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode);
|
||||
}
|
||||
ECKey pubKey = sendNode.getPubKey();
|
||||
Address address = recipientBip47Wallet.getScriptType().getAddress(pubKey);
|
||||
if(sendController.getPaymentTabs().getTabs().size() > 1 || (getRecipientValueSats() != null && getRecipientValueSats() > getRecipientDustThreshold(address)) || maxButton.isSelected()) {
|
||||
return address;
|
||||
}
|
||||
try {
|
||||
Wallet recipientBip47Wallet = getWalletForPayNym(payNym);
|
||||
if(recipientBip47Wallet != null) {
|
||||
int index = sendController.getPayNymSendIndex(this);
|
||||
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
|
||||
for(int i = 0; i < index; i++) {
|
||||
sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND, sendNode);
|
||||
}
|
||||
} catch(InvalidPaymentCodeException e) {
|
||||
log.error("Error creating payment code from PayNym", e);
|
||||
ECKey pubKey = sendNode.getPubKey();
|
||||
return recipientBip47Wallet.getScriptType().getAddress(pubKey);
|
||||
}
|
||||
} catch(InvalidPaymentCodeException e) {
|
||||
log.error("Error creating payment code from PayNym", e);
|
||||
}
|
||||
|
||||
return new PayNymAddress(payNym);
|
||||
throw new InvalidAddressException();
|
||||
}
|
||||
|
||||
private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException {
|
||||
@@ -453,8 +423,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
}
|
||||
|
||||
boolean isSentToSamePayNym(PaymentController paymentController) {
|
||||
return (this != paymentController && payNymProperty.get() != null && !payNymProperty.get().isCollaborativeSend()
|
||||
&& payNymProperty.get().paymentCode().equals(paymentController.payNymProperty.get().paymentCode()));
|
||||
return (this != paymentController && payNymProperty.get() != null && payNymProperty.get().paymentCode().equals(paymentController.payNymProperty.get().paymentCode()));
|
||||
}
|
||||
|
||||
private Long getRecipientValueSats() {
|
||||
@@ -492,10 +461,6 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
address = new P2PKHAddress(new byte[20]);
|
||||
}
|
||||
|
||||
if(address instanceof PayNymAddress && SorobanServices.canWalletMix(sendController.getWalletForm().getWallet())) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return getRecipientDustThreshold(address);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.fxmisc.richtext.CodeArea;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -63,7 +62,7 @@ public class ReceiveController extends WalletFormController implements Initializ
|
||||
private ScriptArea scriptPubKeyArea;
|
||||
|
||||
@FXML
|
||||
private CodeArea outputDescriptor;
|
||||
private SelectableCodeArea outputDescriptor;
|
||||
|
||||
@FXML
|
||||
private Button displayAddress;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
@@ -26,14 +25,9 @@ import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNym;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymService;
|
||||
import com.sparrowwallet.sparrow.soroban.InitiatorDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymAddress;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.animation.KeyFrame;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.binding.Bindings;
|
||||
import javafx.beans.property.*;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
@@ -49,7 +43,6 @@ import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.util.Duration;
|
||||
import javafx.util.StringConverter;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
@@ -148,9 +141,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
@FXML
|
||||
private Button createButton;
|
||||
|
||||
@FXML
|
||||
private Button premixButton;
|
||||
|
||||
@FXML
|
||||
private Button notificationButton;
|
||||
|
||||
@@ -162,8 +152,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
private final ObjectProperty<TxoFilter> txoFilterProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<Pool> whirlpoolProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<PaymentCode> paymentCodeProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
@@ -234,7 +222,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> broadcastButtonsOnlineListener = (observable, oldValue, newValue) -> {
|
||||
premixButton.setDisable(!newValue);
|
||||
notificationButton.setDisable(walletTransactionProperty.get() == null || isInsufficientFeeRate() || !newValue);
|
||||
};
|
||||
|
||||
@@ -269,7 +256,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
paymentTabs.getTabs().forEach(tab -> {
|
||||
tab.setClosable(true);
|
||||
((PaymentController)tab.getUserData()).updateMixOnlyStatus();
|
||||
});
|
||||
} else {
|
||||
paymentTabs.getStyleClass().remove("multiple-tabs");
|
||||
@@ -400,7 +386,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
transactionDiagram.update(walletTransaction);
|
||||
updatePrivacyAnalysis(walletTransaction);
|
||||
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || isPayNymMixOnlyPayment(walletTransaction.getPayments()));
|
||||
createButton.setDisable(walletTransaction == null || isInsufficientFeeRate());
|
||||
notificationButton.setDisable(walletTransaction == null || isInsufficientFeeRate() || !AppServices.isConnected());
|
||||
});
|
||||
|
||||
@@ -432,10 +418,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
optimizationHelp.visibleProperty().bind(privacyAnalysis.visibleProperty().not());
|
||||
|
||||
createButton.managedProperty().bind(createButton.visibleProperty());
|
||||
premixButton.managedProperty().bind(premixButton.visibleProperty());
|
||||
notificationButton.managedProperty().bind(notificationButton.visibleProperty());
|
||||
createButton.visibleProperty().bind(Bindings.and(premixButton.visibleProperty().not(), notificationButton.visibleProperty().not()));
|
||||
premixButton.setVisible(false);
|
||||
createButton.visibleProperty().bind(notificationButton.visibleProperty().not());
|
||||
notificationButton.setVisible(false);
|
||||
AppServices.onlineProperty().addListener(new WeakChangeListener<>(broadcastButtonsOnlineListener));
|
||||
}
|
||||
@@ -634,8 +618,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
OptimizationStrategy optimizationStrategy = (OptimizationStrategy)optimizationToggleGroup.getSelectedToggle().getUserData();
|
||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY
|
||||
&& payments.size() == 1
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType())
|
||||
&& !(payments.get(0).getAddress() instanceof PayNymAddress)) {
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType())) {
|
||||
selectors.add(new StonewallUtxoSelector(payments.get(0).getAddress().getScriptType(), noInputsFee));
|
||||
}
|
||||
|
||||
@@ -991,28 +974,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isPayNymMixOnlyPayment(List<Payment> payments) {
|
||||
return payments.size() == 1 && payments.get(0).getAddress() instanceof PayNymAddress;
|
||||
}
|
||||
|
||||
public void setPayNymMixOnlyPayment() {
|
||||
optimizationToggleGroup.selectToggle(privacyToggle);
|
||||
transactionDiagram.setOptimizationStrategy(OptimizationStrategy.PRIVACY);
|
||||
efficiencyToggle.setDisable(true);
|
||||
privacyToggle.setDisable(false);
|
||||
}
|
||||
|
||||
private boolean isMixPossible(List<Payment> payments) {
|
||||
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
|
||||
&& payments.size() == 1
|
||||
private boolean isFakeMixPossible(List<Payment> payments) {
|
||||
return utxoSelectorProperty.get() == null && payments.size() == 1
|
||||
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType())
|
||||
&& AppServices.getPayjoinURI(payments.get(0).getAddress()) == null;
|
||||
}
|
||||
|
||||
private void updateOptimizationButtons(List<Payment> payments) {
|
||||
if(isPayNymMixOnlyPayment(payments)) {
|
||||
setPayNymMixOnlyPayment();
|
||||
} else if(isMixPossible(payments)) {
|
||||
if(isFakeMixPossible(payments)) {
|
||||
setPreferredOptimizationStrategy();
|
||||
efficiencyToggle.setDisable(false);
|
||||
privacyToggle.setDisable(false);
|
||||
@@ -1091,11 +1060,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
efficiencyToggle.setDisable(false);
|
||||
privacyToggle.setDisable(false);
|
||||
|
||||
premixButton.setVisible(false);
|
||||
notificationButton.setVisible(false);
|
||||
createButton.setDefaultButton(true);
|
||||
|
||||
whirlpoolProperty.set(null);
|
||||
paymentCodeProperty.set(null);
|
||||
|
||||
addressNodeMap.clear();
|
||||
@@ -1183,51 +1150,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
walletForm.addWalletTransactionNodes(nodes);
|
||||
}
|
||||
|
||||
public void broadcastPremix(ActionEvent event) {
|
||||
//Ensure all child wallets have been saved
|
||||
Wallet masterWallet = getWalletForm().getWallet().isMasterWallet() ? getWalletForm().getWallet() : getWalletForm().getWallet().getMasterWallet();
|
||||
for(Wallet childWallet : masterWallet.getChildWallets()) {
|
||||
if(!childWallet.isNested()) {
|
||||
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
|
||||
if(!storage.isPersisted(childWallet)) {
|
||||
try {
|
||||
storage.saveWallet(childWallet);
|
||||
EventManager.get().post(new NewChildWalletSavedEvent(storage, masterWallet, childWallet));
|
||||
} catch(Exception e) {
|
||||
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//The WhirlpoolWallet has already been configured for the tx0 preview
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().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 -> {
|
||||
premixButton.setDisable(true);
|
||||
addWalletTransactionNodes();
|
||||
});
|
||||
tx0BroadcastService.setOnSucceeded(workerStateEvent -> {
|
||||
premixButton.setDisable(false);
|
||||
Sha256Hash txid = tx0BroadcastService.getValue();
|
||||
clear(null);
|
||||
});
|
||||
tx0BroadcastService.setOnFailed(workerStateEvent -> {
|
||||
premixButton.setDisable(false);
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Error broadcasting premix transaction", exception.getMessage());
|
||||
});
|
||||
ServiceProgressDialog progressDialog = new ServiceProgressDialog("Whirlpool", "Broadcast Premix Transaction", "/image/whirlpool.png", tx0BroadcastService);
|
||||
progressDialog.initOwner(premixButton.getScene().getWindow());
|
||||
AppServices.moveToActiveWindowScreen(progressDialog);
|
||||
tx0BroadcastService.start();
|
||||
}
|
||||
|
||||
public void broadcastNotification(ActionEvent event) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
Storage storage = AppServices.get().getOpenWallets().get(wallet);
|
||||
@@ -1487,7 +1409,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
@Subscribe
|
||||
public void spendUtxos(SpendUtxoEvent event) {
|
||||
if((event.getUtxos() == null || !event.getUtxos().isEmpty()) && event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if(whirlpoolProperty.get() != null || paymentCodeProperty.get() != null) {
|
||||
if(paymentCodeProperty.get() != null) {
|
||||
clear(null);
|
||||
}
|
||||
|
||||
@@ -1516,19 +1438,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
txoFilterProperty.set(null);
|
||||
whirlpoolProperty.set(event.getPool());
|
||||
paymentCodeProperty.set(event.getPaymentCode());
|
||||
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
||||
|
||||
boolean isWhirlpoolPremix = (event.getPool() != null);
|
||||
premixButton.setVisible(isWhirlpoolPremix);
|
||||
premixButton.setDefaultButton(isWhirlpoolPremix);
|
||||
|
||||
boolean isNotificationTransaction = (event.getPaymentCode() != null);
|
||||
notificationButton.setVisible(isNotificationTransaction);
|
||||
notificationButton.setDefaultButton(isNotificationTransaction);
|
||||
|
||||
setInputFieldsDisabled(isWhirlpoolPremix || isNotificationTransaction, isWhirlpoolPremix);
|
||||
setInputFieldsDisabled(isNotificationTransaction, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1646,37 +1563,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void sorobanInitiated(SorobanInitiatedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if(!AppServices.onlineProperty().get()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("Cannot Mix Offline", "Sparrow needs to be connected to a server to perform collaborative mixes. Try to connect?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
|
||||
AppServices.onlineProperty().set(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Platform.runLater(() -> {
|
||||
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
|
||||
initiatorDialog.initOwner(paymentTabs.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
initiatorDialog.initModality(Modality.NONE);
|
||||
}
|
||||
Optional<Transaction> optTransaction = initiatorDialog.showAndWait();
|
||||
if(optTransaction.isPresent()) {
|
||||
BlockTransaction blockTransaction = walletForm.getWallet().getWalletTransaction(optTransaction.get().getTxId());
|
||||
if(blockTransaction != null && blockTransaction.getLabel() == null && walletTransactionProperty.get() != null) {
|
||||
blockTransaction.setLabel(walletTransactionProperty.get().getPayments().stream().map(Payment::getLabel).findFirst().orElse(null));
|
||||
TransactionEntry transactionEntry = new TransactionEntry(walletForm.getWallet(), blockTransaction, Collections.emptyMap(), Collections.emptyMap());
|
||||
EventManager.get().post(new WalletEntryLabelsChangedEvent(walletForm.getWallet(), List.of(transactionEntry)));
|
||||
}
|
||||
clear(null);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private class PrivacyAnalysisTooltip extends VBox {
|
||||
private final List<Label> analysisLabels = new ArrayList<>();
|
||||
|
||||
@@ -1685,7 +1571,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
List<Payment> userPayments = payments.stream().filter(payment -> payment.getType() != Payment.Type.FAKE_MIX).collect(Collectors.toList());
|
||||
Map<Address, WalletNode> walletAddresses = walletTransaction.getAddressNodeMap();
|
||||
OptimizationStrategy optimizationStrategy = getPreferredOptimizationStrategy();
|
||||
boolean payNymPresent = isPayNymMixOnlyPayment(payments);
|
||||
boolean fakeMixPresent = payments.stream().anyMatch(payment -> payment.getType() == Payment.Type.FAKE_MIX);
|
||||
boolean roundPaymentAmounts = userPayments.stream().anyMatch(payment -> payment.getAmount() % 100 == 0);
|
||||
boolean mixedAddressTypes = userPayments.stream().anyMatch(payment -> payment.getAddress().getScriptType() != getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
@@ -1693,29 +1578,21 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
|
||||
|
||||
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
|
||||
if(payNymPresent) {
|
||||
addLabel("Appears as a normal transaction, but actual value transferred is hidden", getPlusGlyph());
|
||||
} else if(fakeMixPresent) {
|
||||
if(fakeMixPresent) {
|
||||
addLabel("Appears as a two person coinjoin", getPlusGlyph());
|
||||
} else {
|
||||
if(mixedAddressTypes) {
|
||||
addLabel("Cannot coinjoin due to mixed address types", getInfoGlyph());
|
||||
addLabel("Cannot fake coinjoin due to mixed address types", getInfoGlyph());
|
||||
} else if(userPayments.size() > 1) {
|
||||
addLabel("Cannot coinjoin due to multiple payments", getInfoGlyph());
|
||||
addLabel("Cannot fake coinjoin due to multiple payments", getInfoGlyph());
|
||||
} else if(payjoinPresent) {
|
||||
addLabel("Cannot coinjoin due to payjoin", getInfoGlyph());
|
||||
addLabel("Cannot fake coinjoin due to payjoin", getInfoGlyph());
|
||||
} else {
|
||||
if(utxoSelectorProperty().get() != null) {
|
||||
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
|
||||
} else {
|
||||
addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());
|
||||
}
|
||||
|
||||
if(!SorobanServices.canWalletMix(getWalletForm().getWallet())) {
|
||||
addLabel("Can only add mix partner to Native Segwit software wallets", getInfoGlyph());
|
||||
} else {
|
||||
addLabel("Add a mix partner to create a two person coinjoin", getInfoGlyph());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1724,7 +1601,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
addLabel("Address types different to the wallet indicate external payments", getMinusGlyph());
|
||||
}
|
||||
|
||||
if(roundPaymentAmounts && !fakeMixPresent && !payNymPresent) {
|
||||
if(roundPaymentAmounts && !fakeMixPresent) {
|
||||
addLabel("Rounded payment amounts indicate external payments", getMinusGlyph());
|
||||
}
|
||||
|
||||
|
||||
@@ -15,10 +15,11 @@ import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.StorageException;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -462,6 +463,11 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
|
||||
private void rederiveAndReplaceWallet(Wallet editedWallet) {
|
||||
if(!walletForm.getWallet().isMasterWallet() && (editedWallet.getPolicyType() != walletForm.getMasterWallet().getPolicyType() || editedWallet.getScriptType() != walletForm.getMasterWallet().getScriptType())) {
|
||||
AppServices.showErrorDialog("Policy or Script Type Mismatch", "The provided output descriptor does not match the policy or script type of this wallet.");
|
||||
return;
|
||||
}
|
||||
|
||||
boolean rederive = false;
|
||||
for(Keystore keystore : editedWallet.getKeystores()) {
|
||||
Optional<Keystore> optExisting = walletForm.getWallet().getKeystores().stream()
|
||||
@@ -629,7 +635,9 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
accountDiscoveryService.setOnSucceeded(event -> {
|
||||
addAndEncryptAccounts(masterWallet, accountDiscoveryService.getValue(), key);
|
||||
if(accountDiscoveryService.getValue().isEmpty()) {
|
||||
AppServices.showAlertDialog("No Accounts Found", "No new accounts with existing transactions were found. Note only the first 10 accounts are scanned.", Alert.AlertType.INFORMATION, ButtonType.OK);
|
||||
AppServices.showAlertDialog("No Accounts Found", "No new accounts with existing transactions were found. " +
|
||||
(Config.get().getServerType() == ServerType.BITCOIN_CORE ? "Note the configured server type is Bitcoin Core, which does not support account discovery." : "Note only the next 10 accounts are scanned."),
|
||||
Alert.AlertType.INFORMATION, ButtonType.OK);
|
||||
}
|
||||
});
|
||||
accountDiscoveryService.setOnFailed(event -> {
|
||||
@@ -667,7 +675,9 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
accountDiscoveryService.setOnSucceeded(event -> {
|
||||
addAndSaveAccounts(masterWallet, accountDiscoveryService.getValue(), null);
|
||||
if(accountDiscoveryService.getValue().isEmpty()) {
|
||||
AppServices.showAlertDialog("No Accounts Found", "No new accounts with existing transactions were found. Note only the first 10 accounts are scanned.", Alert.AlertType.INFORMATION, ButtonType.OK);
|
||||
AppServices.showAlertDialog("No Accounts Found", "No new accounts with existing transactions were found. " +
|
||||
(Config.get().getServerType() == ServerType.BITCOIN_CORE ? "Note the configured server type is Bitcoin Core, which does not support account discovery." : "Note only the next 10 accounts are scanned."),
|
||||
Alert.AlertType.INFORMATION, ButtonType.OK);
|
||||
}
|
||||
});
|
||||
accountDiscoveryService.setOnFailed(event -> {
|
||||
@@ -726,7 +736,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
private void addAndSaveAccount(Wallet masterWallet, StandardAccount standardAccount, Key key) {
|
||||
List<Wallet> childWallets;
|
||||
if(standardAccount == StandardAccount.WHIRLPOOL_PREMIX) {
|
||||
childWallets = WhirlpoolServices.prepareWhirlpoolWallet(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
|
||||
childWallets = AppServices.addWhirlpoolWallets(masterWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
|
||||
} else {
|
||||
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
|
||||
@@ -763,7 +773,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
policyType.setDisable(disabled);
|
||||
scriptType.setDisable(disabled);
|
||||
multisigControl.setDisable(disabled);
|
||||
editDescriptor.setVisible(!disabled);
|
||||
editDescriptor.setVisible(!disabled || (!walletForm.getWallet().isValid() && walletForm.getMasterWallet().getKeystores().stream().allMatch(k -> k.getSource() == KeystoreSource.SW_WATCH)));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.collections.ObservableList;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class UtxoEntry extends HashIndexEntry {
|
||||
private final WalletNode node;
|
||||
|
||||
@@ -47,10 +47,6 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
return Function.UTXOS;
|
||||
}
|
||||
|
||||
public boolean isMixing() {
|
||||
return mixStatusProperty != null && ((mixStatusProperty.get().getMixProgress() != null && mixStatusProperty.get().getMixProgress().getMixStep() != MixStep.FAIL) || mixStatusProperty.get().getNextMixUtxo() != null);
|
||||
}
|
||||
|
||||
public Address getAddress() {
|
||||
return node.getAddress();
|
||||
}
|
||||
@@ -121,51 +117,19 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
*/
|
||||
private ObjectProperty<MixStatus> mixStatusProperty;
|
||||
|
||||
public void setMixProgress(MixProgress mixProgress) {
|
||||
mixStatusProperty().set(new MixStatus(mixProgress));
|
||||
}
|
||||
|
||||
public void setMixFailReason(MixFailReason mixFailReason, String mixError) {
|
||||
mixStatusProperty().set(new MixStatus(mixFailReason, mixError));
|
||||
}
|
||||
|
||||
public void setNextMixUtxo(Utxo nextMixUtxo) {
|
||||
mixStatusProperty().set(new MixStatus(nextMixUtxo));
|
||||
}
|
||||
|
||||
public final MixStatus getMixStatus() {
|
||||
return mixStatusProperty == null ? null : mixStatusProperty.get();
|
||||
}
|
||||
|
||||
public final ObjectProperty<MixStatus> mixStatusProperty() {
|
||||
if(mixStatusProperty == null) {
|
||||
mixStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "mixStatus", null);
|
||||
mixStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "mixStatus", new MixStatus());
|
||||
}
|
||||
|
||||
return mixStatusProperty;
|
||||
}
|
||||
|
||||
public class MixStatus {
|
||||
private MixProgress mixProgress;
|
||||
private Utxo nextMixUtxo;
|
||||
private MixFailReason mixFailReason;
|
||||
private String mixError;
|
||||
private Long mixErrorTimestamp;
|
||||
|
||||
public MixStatus(MixProgress mixProgress) {
|
||||
this.mixProgress = mixProgress;
|
||||
}
|
||||
|
||||
public MixStatus(Utxo nextMixUtxo) {
|
||||
this.nextMixUtxo = nextMixUtxo;
|
||||
}
|
||||
|
||||
public MixStatus(MixFailReason mixFailReason, String mixError) {
|
||||
this.mixFailReason = mixFailReason;
|
||||
this.mixError = mixError;
|
||||
this.mixErrorTimestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public UtxoEntry getUtxoEntry() {
|
||||
return UtxoEntry.this;
|
||||
}
|
||||
@@ -177,38 +141,37 @@ public class UtxoEntry extends HashIndexEntry {
|
||||
}
|
||||
|
||||
//Mix data not available - recount (and store if WhirlpoolWallet is running)
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(wallet);
|
||||
if(whirlpool != null && getUtxoEntry().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && node.getKeyPurpose() == KeyPurpose.RECEIVE) {
|
||||
int mixesDone = whirlpool.recountMixesDone(getUtxoEntry().getWallet(), getHashIndex());
|
||||
whirlpool.setMixesDone(getHashIndex(), mixesDone);
|
||||
if(getUtxoEntry().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && node.getKeyPurpose() == KeyPurpose.RECEIVE) {
|
||||
int mixesDone = recountMixesDone(getUtxoEntry().getWallet(), getHashIndex());
|
||||
return new UtxoMixData(mixesDone, null);
|
||||
}
|
||||
|
||||
return new UtxoMixData(getUtxoEntry().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX ? 1 : 0, null);
|
||||
}
|
||||
|
||||
public int recountMixesDone(Wallet postmixWallet, BlockTransactionHashIndex postmixUtxo) {
|
||||
int mixesDone = 0;
|
||||
Set<BlockTransactionHashIndex> walletTxos = postmixWallet.getWalletTxos().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE).map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
BlockTransaction blkTx = postmixWallet.getTransactions().get(postmixUtxo.getHash());
|
||||
|
||||
while(blkTx != null) {
|
||||
mixesDone++;
|
||||
List<TransactionInput> inputs = blkTx.getTransaction().getInputs();
|
||||
blkTx = null;
|
||||
for(TransactionInput txInput : inputs) {
|
||||
BlockTransaction inputTx = postmixWallet.getTransactions().get(txInput.getOutpoint().getHash());
|
||||
if(inputTx != null && walletTxos.stream().anyMatch(txo -> txo.getHash().equals(inputTx.getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()) && inputTx.getTransaction() != null) {
|
||||
blkTx = inputTx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mixesDone;
|
||||
}
|
||||
|
||||
public int getMixesDone() {
|
||||
return getUtxoMixData().getMixesDone();
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress() {
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
public Utxo getNextMixUtxo() {
|
||||
return nextMixUtxo;
|
||||
}
|
||||
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
|
||||
public Long getMixErrorTimestamp() {
|
||||
return mixErrorTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.csvreader.CsvWriter;
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.InvalidAddressException;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -17,24 +11,16 @@ import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.io.WalletTransactions;
|
||||
import com.sparrowwallet.sparrow.net.ExchangeSource;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.WeakChangeListener;
|
||||
import javafx.collections.ListChangeListener;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.Initializable;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.ButtonType;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.control.TreeItem;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
@@ -49,8 +35,6 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class UtxosController extends WalletFormController implements Initializable {
|
||||
private static final Logger log = LoggerFactory.getLogger(UtxosController.class);
|
||||
|
||||
@@ -72,18 +56,6 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
@FXML
|
||||
private UtxosTreeTable utxosTable;
|
||||
|
||||
@FXML
|
||||
private HBox mixButtonsBox;
|
||||
|
||||
@FXML
|
||||
private Button startMix;
|
||||
|
||||
@FXML
|
||||
private Button stopMix;
|
||||
|
||||
@FXML
|
||||
private Button mixTo;
|
||||
|
||||
@FXML
|
||||
private Button selectAll;
|
||||
|
||||
@@ -93,44 +65,9 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
@FXML
|
||||
private Button sendSelected;
|
||||
|
||||
@FXML
|
||||
private Button mixSelected;
|
||||
|
||||
@FXML
|
||||
private UtxosChart utxosChart;
|
||||
|
||||
private final ChangeListener<Boolean> mixingOnlineListener = (observable, oldValue, newValue) -> {
|
||||
mixSelected.setDisable(getSelectedEntries().isEmpty() || !newValue);
|
||||
startMix.setDisable(!newValue);
|
||||
stopMix.setDisable(!newValue);
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingStartingListener = (observable, oldValue, newValue) -> {
|
||||
startMix.setDisable(newValue || !AppServices.onlineProperty().get());
|
||||
Platform.runLater(() -> startMix.setText(newValue && AppServices.onlineProperty().get() ? "Starting Mixing..." : "Start Mixing"));
|
||||
mixTo.setDisable(newValue);
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingStoppingListener = (observable, oldValue, newValue) -> {
|
||||
startMix.setDisable(newValue || !AppServices.onlineProperty().get());
|
||||
Platform.runLater(() -> startMix.setText(newValue ? "Stopping Mixing..." : "Start Mixing"));
|
||||
mixTo.setDisable(newValue);
|
||||
};
|
||||
|
||||
private final ChangeListener<Boolean> mixingListener = (observable, oldValue, newValue) -> {
|
||||
if(!newValue) {
|
||||
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getMixStatus() != null && utxoEntry.getMixStatus().getMixProgress() != null
|
||||
&& utxoEntry.getMixStatus().getMixProgress().getMixStep() != null
|
||||
&& utxoEntry.getMixStatus().getMixProgress().getMixStep().isInterruptable()) {
|
||||
utxoEntry.setMixProgress(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -150,42 +87,9 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
utxosTable.initialize(walletUtxosEntry);
|
||||
utxosChart.initialize(walletUtxosEntry);
|
||||
|
||||
mixButtonsBox.managedProperty().bind(mixButtonsBox.visibleProperty());
|
||||
mixButtonsBox.setVisible(getWalletForm().getWallet().isWhirlpoolMixWallet());
|
||||
startMix.managedProperty().bind(startMix.visibleProperty());
|
||||
startMix.setDisable(!AppServices.isConnected());
|
||||
stopMix.managedProperty().bind(stopMix.visibleProperty());
|
||||
startMix.visibleProperty().bind(stopMix.visibleProperty().not());
|
||||
stopMix.visibleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
stopMix.setDisable(!newValue);
|
||||
});
|
||||
mixTo.managedProperty().bind(mixTo.visibleProperty());
|
||||
mixTo.setVisible(getWalletForm().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX);
|
||||
|
||||
if(mixButtonsBox.isVisible()) {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool != null) {
|
||||
stopMix.visibleProperty().bind(whirlpool.mixingProperty());
|
||||
if(whirlpool.startingProperty().getValue()) {
|
||||
mixingStartingListener.changed(whirlpool.startingProperty(), null, whirlpool.startingProperty().getValue());
|
||||
}
|
||||
whirlpool.startingProperty().addListener(new WeakChangeListener<>(mixingStartingListener));
|
||||
if(whirlpool.stoppingProperty().getValue()) {
|
||||
mixingStoppingListener.changed(whirlpool.stoppingProperty(), null, whirlpool.stoppingProperty().getValue());
|
||||
}
|
||||
whirlpool.stoppingProperty().addListener(new WeakChangeListener<>(mixingStoppingListener));
|
||||
whirlpool.mixingProperty().addListener(new WeakChangeListener<>(mixingListener));
|
||||
updateMixToButton();
|
||||
}
|
||||
}
|
||||
|
||||
clear.setDisable(true);
|
||||
sendSelected.setDisable(true);
|
||||
sendSelected.setTooltip(new Tooltip("Send selected UTXOs. Use " + (org.controlsfx.tools.Platform.getCurrent() == org.controlsfx.tools.Platform.OSX ? "Cmd" : "Ctrl") + "+click to select multiple." ));
|
||||
mixSelected.managedProperty().bind(mixSelected.visibleProperty());
|
||||
mixSelected.setVisible(canWalletMix());
|
||||
mixSelected.setDisable(true);
|
||||
AppServices.onlineProperty().addListener(new WeakChangeListener<>(mixingOnlineListener));
|
||||
|
||||
utxosTable.getSelectionModel().getSelectedIndices().addListener((ListChangeListener<Integer>) c -> {
|
||||
List<Entry> selectedEntries = utxosTable.getSelectionModel().getSelectedCells().stream().filter(tp -> tp.getTreeItem() != null).map(tp -> tp.getTreeItem().getValue()).collect(Collectors.toList());
|
||||
@@ -207,17 +111,12 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
utxoCount.setText((selectedCount > 0 ? selectedCount + "/" : "") + (walletUtxosEntry.getChildren() != null ? Integer.toString(walletUtxosEntry.getChildren().size()) : "0"));
|
||||
}
|
||||
|
||||
private boolean canWalletMix() {
|
||||
return WhirlpoolServices.canWalletMix(getWalletForm().getWallet());
|
||||
}
|
||||
|
||||
private void updateButtons(UnitFormat format, BitcoinUnit unit) {
|
||||
List<Entry> selectedEntries = getSelectedEntries();
|
||||
|
||||
selectAll.setDisable(utxosTable.getRoot().getChildren().size() == utxosTable.getSelectionModel().getSelectedCells().size());
|
||||
clear.setDisable(selectedEntries.isEmpty());
|
||||
sendSelected.setDisable(selectedEntries.isEmpty());
|
||||
mixSelected.setDisable(selectedEntries.isEmpty() || !AppServices.isConnected());
|
||||
|
||||
long selectedTotal = selectedEntries.stream().mapToLong(Entry::getValue).sum();
|
||||
if(selectedTotal > 0) {
|
||||
@@ -231,35 +130,11 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
|
||||
if(unit.equals(BitcoinUnit.BTC)) {
|
||||
sendSelected.setText("Send Selected (" + format.formatBtcValue(selectedTotal) + " BTC)");
|
||||
mixSelected.setText("Mix Selected (" + format.formatBtcValue(selectedTotal) + " BTC)");
|
||||
} else {
|
||||
sendSelected.setText("Send Selected (" + format.formatSatsValue(selectedTotal) + " sats)");
|
||||
mixSelected.setText("Mix Selected (" + format.formatSatsValue(selectedTotal) + " sats)");
|
||||
}
|
||||
} else {
|
||||
sendSelected.setText("Send Selected");
|
||||
mixSelected.setText("Mix Selected");
|
||||
}
|
||||
}
|
||||
|
||||
private void updateMixToButton() {
|
||||
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||
if(mixConfig != null && mixConfig.getMixToWalletName() != null) {
|
||||
mixTo.setText("Mixing to " + mixConfig.getMixToWalletName());
|
||||
try {
|
||||
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||
String mixToName = AppServices.get().getWallet(mixToWalletId).getFullDisplayName();
|
||||
mixTo.setText("Mixing to " + mixToName);
|
||||
mixTo.setGraphic(getExternalGlyph());
|
||||
mixTo.setTooltip(new Tooltip("Mixing to " + mixToName + " after at least " + (mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes()) + " mixes"));
|
||||
} catch(NoSuchElementException e) {
|
||||
mixTo.setGraphic(getErrorGlyph());
|
||||
mixTo.setTooltip(new Tooltip(mixConfig.getMixToWalletName() + " is not open - open this wallet to mix to it!"));
|
||||
}
|
||||
} else {
|
||||
mixTo.setText("Mix to...");
|
||||
mixTo.setGraphic(null);
|
||||
mixTo.setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,116 +153,6 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), spendingUtxos)));
|
||||
}
|
||||
|
||||
public void mixSelected(ActionEvent event) {
|
||||
if(!getWalletForm().getWallet().isMasterWallet() && getWalletForm().getWallet().getStandardAccountType() == StandardAccount.ACCOUNT_0) {
|
||||
showErrorDialog("Invalid Whirlpool wallet", "Create a new wallet with Account #0 as the first account.");
|
||||
return;
|
||||
}
|
||||
|
||||
List<UtxoEntry> selectedEntries = getSelectedUtxos();
|
||||
WhirlpoolDialog whirlpoolDialog = new WhirlpoolDialog(getWalletForm().getMasterWalletId(), getWalletForm().getWallet(), selectedEntries);
|
||||
whirlpoolDialog.initOwner(utxosTable.getScene().getWindow());
|
||||
Optional<Tx0Preview> optTx0Preview = whirlpoolDialog.showAndWait();
|
||||
optTx0Preview.ifPresent(tx0Preview -> previewPremix(tx0Preview, selectedEntries));
|
||||
}
|
||||
|
||||
public void previewPremix(Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) {
|
||||
Wallet wallet = getWalletForm().getWallet();
|
||||
String walletId = walletForm.getWalletId();
|
||||
|
||||
if(wallet.isMasterWallet() && !wallet.isWhirlpoolMasterWallet() && wallet.isEncrypted()) {
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(wallet.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(utxosTable.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
wallet.decrypt(key);
|
||||
|
||||
try {
|
||||
prepareWhirlpoolWallet(wallet);
|
||||
} finally {
|
||||
wallet.encrypt(key);
|
||||
for(Wallet childWallet : wallet.getChildWallets()) {
|
||||
if(!childWallet.isNested() && !childWallet.isEncrypted()) {
|
||||
childWallet.encrypt(key);
|
||||
}
|
||||
}
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
password.get().clear();
|
||||
}
|
||||
|
||||
previewPremix(wallet, tx0Preview, utxoEntries);
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> previewPremix(tx0Preview, utxoEntries));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
if(wallet.isMasterWallet() && !wallet.isWhirlpoolMasterWallet()) {
|
||||
prepareWhirlpoolWallet(wallet);
|
||||
}
|
||||
|
||||
previewPremix(wallet, tx0Preview, utxoEntries);
|
||||
}
|
||||
}
|
||||
|
||||
private void prepareWhirlpoolWallet(Wallet decryptedWallet) {
|
||||
WhirlpoolServices.prepareWhirlpoolWallet(decryptedWallet, getWalletForm().getWalletId(), getWalletForm().getStorage());
|
||||
}
|
||||
|
||||
private void previewPremix(Wallet wallet, Tx0Preview tx0Preview, List<UtxoEntry> utxoEntries) {
|
||||
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<>();
|
||||
if(tx0Preview.getTx0Data().getFeeAddress() != null) {
|
||||
try {
|
||||
Address whirlpoolFeeAddress = Address.fromString(tx0Preview.getTx0Data().getFeeAddress());
|
||||
Payment whirlpoolFeePayment = new Payment(whirlpoolFeeAddress, "Whirlpool Fee", tx0Preview.getFeeValue(), false);
|
||||
whirlpoolFeePayment.setType(Payment.Type.WHIRLPOOL_FEE);
|
||||
payments.add(whirlpoolFeePayment);
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalStateException("Cannot parse whirlpool fee address " + tx0Preview.getTx0Data().getFeeAddress(), e);
|
||||
}
|
||||
}
|
||||
|
||||
WalletNode badbankNode = badbankWallet.getFreshNode(KeyPurpose.RECEIVE);
|
||||
Payment changePayment = new Payment(badbankNode.getAddress(), "Badbank Change", tx0Preview.getChangeValue(), false);
|
||||
payments.add(changePayment);
|
||||
|
||||
WalletNode premixNode = null;
|
||||
for(int i = 0; i < tx0Preview.getNbPremix(); i++) {
|
||||
premixNode = premixWallet.getFreshNode(KeyPurpose.RECEIVE, premixNode);
|
||||
Address premixAddress = premixNode.getAddress();
|
||||
payments.add(new Payment(premixAddress, "Premix #" + i, tx0Preview.getPremixValue(), false));
|
||||
}
|
||||
|
||||
List<byte[]> opReturns = List.of(new byte[64]);
|
||||
|
||||
final List<BlockTransactionHashIndex> utxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
|
||||
Platform.runLater(() -> {
|
||||
EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), utxos));
|
||||
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), utxos, payments, opReturns, tx0Preview.getTx0MinerFee(), tx0Preview.getPool())));
|
||||
});
|
||||
}
|
||||
|
||||
private List<UtxoEntry> getSelectedUtxos() {
|
||||
return utxosTable.getSelectionModel().getSelectedCells().stream()
|
||||
.map(tp -> tp.getTreeItem().getValue())
|
||||
@@ -405,69 +170,6 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
utxosTable.getSelectionModel().clearSelection();
|
||||
}
|
||||
|
||||
public void startMixing(ActionEvent event) {
|
||||
startMix.setDisable(true);
|
||||
stopMix.setDisable(false);
|
||||
|
||||
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.TRUE);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) {
|
||||
AppServices.getWhirlpoolServices().startWhirlpool(getWalletForm().getWallet(), whirlpool, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopMixing(ActionEvent event) {
|
||||
stopMix.setDisable(true);
|
||||
startMix.setDisable(!AppServices.onlineProperty().get());
|
||||
|
||||
getWalletForm().getWallet().getMasterMixConfig().setMixOnStartup(Boolean.FALSE);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
if(whirlpool.isStarted()) {
|
||||
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, true);
|
||||
} else {
|
||||
//Ensure http clients are shutdown
|
||||
whirlpool.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
public void showMixToDialog(ActionEvent event) {
|
||||
MixToDialog mixToDialog = new MixToDialog(getWalletForm().getWallet());
|
||||
mixToDialog.initOwner(mixTo.getScene().getWindow());
|
||||
Optional<MixConfig> optMixConfig = mixToDialog.showAndWait();
|
||||
if(optMixConfig.isPresent()) {
|
||||
MixConfig changedMixConfig = optMixConfig.get();
|
||||
MixConfig mixConfig = getWalletForm().getWallet().getMasterMixConfig();
|
||||
|
||||
mixConfig.setMixToWalletName(changedMixConfig.getMixToWalletName());
|
||||
mixConfig.setMixToWalletFile(changedMixConfig.getMixToWalletFile());
|
||||
mixConfig.setMinMixes(changedMixConfig.getMinMixes());
|
||||
mixConfig.setIndexRange(changedMixConfig.getIndexRange());
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
|
||||
whirlpool.setPostmixIndexRange(mixConfig.getIndexRange());
|
||||
try {
|
||||
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
|
||||
whirlpool.setMixToWallet(mixToWalletId, mixConfig.getMinMixes());
|
||||
} catch(NoSuchElementException e) {
|
||||
mixConfig.setMixToWalletName(null);
|
||||
mixConfig.setMixToWalletFile(null);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
|
||||
whirlpool.setMixToWallet(null, null);
|
||||
}
|
||||
|
||||
updateMixToButton();
|
||||
if(whirlpool.isStarted()) {
|
||||
//Will automatically restart
|
||||
AppServices.getWhirlpoolServices().stopWhirlpool(whirlpool, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void exportUtxos(ActionEvent event) {
|
||||
Stage window = new Stage();
|
||||
|
||||
@@ -480,7 +182,7 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
if(file != null) {
|
||||
try(FileOutputStream outputStream = new FileOutputStream(file)) {
|
||||
CsvWriter writer = new CsvWriter(outputStream, ',', StandardCharsets.UTF_8);
|
||||
writer.writeRecord(new String[] {"Date", "Output", "Address", "Label", "Value"});
|
||||
writer.writeRecord(new String[] {"Date (UTC)", "Output", "Address", "Label", "Value"});
|
||||
for(Entry entry : getWalletForm().getWalletUtxosEntry().getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
writer.write(utxoEntry.getBlockTransaction().getDate() == null ? "Unconfirmed" : WalletTransactions.DATE_FORMAT.format(utxoEntry.getBlockTransaction().getDate()));
|
||||
@@ -523,7 +225,6 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
updateFields(walletUtxosEntry);
|
||||
utxosTable.updateAll(walletUtxosEntry);
|
||||
utxosChart.update(walletUtxosEntry);
|
||||
mixSelected.setVisible(canWalletMix());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,11 +274,6 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
utxosTable.updateHistoryStatus(event);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
getWalletForm().getWalletUtxosEntry().updateMixProgress();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void cormorantStatus(CormorantStatusEvent event) {
|
||||
if(event.isFor(walletForm.getWallet())) {
|
||||
@@ -623,35 +319,6 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
utxosChart.setVisible(event.isVisible() && !getWalletForm().getWallet().isWhirlpoolMixWallet());
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void openWallets(OpenWalletsEvent event) {
|
||||
Platform.runLater(this::updateMixToButton);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletLabelChanged(WalletLabelChangedEvent event) {
|
||||
Platform.runLater(this::updateMixToButton);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void whirlpoolMix(WhirlpoolMixEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet())) {
|
||||
WalletUtxosEntry walletUtxosEntry = getWalletForm().getWalletUtxosEntry();
|
||||
for(Entry entry : walletUtxosEntry.getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
if(utxoEntry.getHashIndex().equals(event.getUtxo())) {
|
||||
if(event.getNextUtxo() != null) {
|
||||
utxoEntry.setNextMixUtxo(event.getNextUtxo());
|
||||
} else if(event.getMixFailReason() != null) {
|
||||
utxoEntry.setMixFailReason(event.getMixFailReason(), event.getMixError());
|
||||
} else {
|
||||
utxoEntry.setMixProgress(event.getMixProgress());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void selectEntry(SelectEntryEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet()) && event.getEntry().getWalletFunction() == Function.UTXOS) {
|
||||
|
||||
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@@ -21,7 +20,6 @@ import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -626,31 +624,6 @@ public class WalletForm {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void whirlpoolMixSuccess(WhirlpoolMixSuccessEvent event) {
|
||||
if(event.getWallet() == wallet && event.getWalletNode() != null) {
|
||||
if(transactionMempoolService != null) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
|
||||
transactionMempoolService = new ElectrumServer.TransactionMempoolService(event.getWallet(), Sha256Hash.wrap(event.getNextUtxo().getHash()), Set.of(event.getWalletNode()));
|
||||
transactionMempoolService.setDelay(Duration.seconds(5));
|
||||
transactionMempoolService.setPeriod(Duration.seconds(5));
|
||||
transactionMempoolService.setRestartOnFailure(false);
|
||||
transactionMempoolService.setOnSucceeded(mempoolWorkerStateEvent -> {
|
||||
Set<String> scriptHashes = transactionMempoolService.getValue();
|
||||
if(!scriptHashes.isEmpty()) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
|
||||
}
|
||||
|
||||
if(transactionMempoolService.getIterationCount() > 10) {
|
||||
transactionMempoolService.cancel();
|
||||
}
|
||||
});
|
||||
transactionMempoolService.start();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.application.Platform;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -18,7 +14,6 @@ public class WalletUtxosEntry extends Entry {
|
||||
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
||||
calculateDuplicates();
|
||||
calculateDust();
|
||||
updateMixProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,19 +61,6 @@ public class WalletUtxosEntry extends Entry {
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMixProgress() {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWallet());
|
||||
if(whirlpool != null) {
|
||||
for(Entry entry : getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry)entry;
|
||||
MixProgress mixProgress = whirlpool.getMixProgress(utxoEntry.getHashIndex());
|
||||
if(mixProgress != null || utxoEntry.getMixStatus() == null || (utxoEntry.getMixStatus().getMixFailReason() == null && utxoEntry.getMixStatus().getNextMixUtxo() == null)) {
|
||||
utxoEntry.setMixProgress(mixProgress);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateUtxos() {
|
||||
List<Entry> current = getWallet().getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList());
|
||||
List<Entry> previous = new ArrayList<>(getChildren());
|
||||
@@ -93,8 +75,6 @@ public class WalletUtxosEntry extends Entry {
|
||||
|
||||
calculateDuplicates();
|
||||
calculateDust();
|
||||
//Update mix status after SparrowUtxoSupplier has refreshed
|
||||
Platform.runLater(this::updateMixProgress);
|
||||
}
|
||||
|
||||
public long getBalance() {
|
||||
|
||||
@@ -1,788 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.soroban.client.SorobanConfig;
|
||||
import com.samourai.wallet.api.backend.beans.UnspentOutput;
|
||||
import com.samourai.wallet.bipFormat.BIP_FORMAT;
|
||||
import com.samourai.wallet.bipWallet.WalletSupplier;
|
||||
import com.samourai.wallet.constants.BIP_WALLETS;
|
||||
import com.samourai.wallet.constants.SamouraiAccount;
|
||||
import com.samourai.wallet.constants.SamouraiNetwork;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.hd.HD_WalletFactoryGeneric;
|
||||
import com.samourai.wallet.util.AsyncUtil;
|
||||
import com.samourai.wallet.util.FormatsUtilGeneric;
|
||||
import com.samourai.whirlpool.client.event.*;
|
||||
import com.samourai.whirlpool.client.mix.handler.IPostmixHandler;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Config;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Info;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Previews;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletService;
|
||||
import com.samourai.whirlpool.client.wallet.beans.*;
|
||||
import com.samourai.whirlpool.client.wallet.data.WhirlpoolInfo;
|
||||
import com.samourai.whirlpool.client.wallet.data.coordinator.CoordinatorSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersisterFactory;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceFactory;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier;
|
||||
import com.samourai.whirlpool.client.whirlpool.WhirlpoolClientConfig;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WhirlpoolMixEvent;
|
||||
import com.sparrowwallet.sparrow.event.WhirlpoolMixSuccessEvent;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataPersister.SparrowDataPersister;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowChainSupplier;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowDataSource;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowPostmixHandler;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.rxjavafx.schedulers.JavaFxScheduler;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.util.Duration;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class Whirlpool {
|
||||
private static final Logger log = LoggerFactory.getLogger(Whirlpool.class);
|
||||
|
||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
public static final int DEFAULT_MIXTO_MIN_MIXES = 3;
|
||||
public static final int DEFAULT_MIXTO_RANDOM_FACTOR = 4;
|
||||
|
||||
private final WhirlpoolWalletService whirlpoolWalletService;
|
||||
private final WhirlpoolWalletConfig config;
|
||||
private WhirlpoolInfo whirlpoolInfo;
|
||||
private Tx0Info tx0Info;
|
||||
private Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
private Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4;
|
||||
private HD_Wallet hdWallet;
|
||||
private String walletId;
|
||||
private String mixToWalletId;
|
||||
private boolean resyncMixesDone;
|
||||
|
||||
private StartupService startupService;
|
||||
private Duration startupServiceDelay;
|
||||
|
||||
private final BooleanProperty startingProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty stoppingProperty = new SimpleBooleanProperty(false);
|
||||
private final BooleanProperty mixingProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public Whirlpool(Integer storedBlockHeight) {
|
||||
this.whirlpoolWalletService = new WhirlpoolWalletService();
|
||||
this.config = computeWhirlpoolWalletConfig(storedBlockHeight);
|
||||
this.tx0Info = null; // instantiated by getTx0Info()
|
||||
this.whirlpoolInfo = null; // instantiated by getWhirlpoolInfo()
|
||||
|
||||
WhirlpoolEventService.getInstance().register(this);
|
||||
}
|
||||
|
||||
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(Integer storedBlockHeight) {
|
||||
SorobanConfig sorobanConfig = AppServices.getWhirlpoolServices().getSorobanConfig();
|
||||
DataSourceConfig dataSourceConfig = computeDataSourceConfig(storedBlockHeight);
|
||||
DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, passphrase, walletStateSupplier, utxoConfigSupplier) -> new SparrowDataSource(whirlpoolWallet, bip44w, walletStateSupplier, utxoConfigSupplier, dataSourceConfig);
|
||||
|
||||
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, sorobanConfig, false);
|
||||
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet, whirlpoolWalletConfig.getPersistDelaySeconds());
|
||||
whirlpoolWalletConfig.setDataPersisterFactory(dataPersisterFactory);
|
||||
whirlpoolWalletConfig.setPartner("SPARROW");
|
||||
whirlpoolWalletConfig.setIndexRangePostmix(IndexRange.FULL);
|
||||
return whirlpoolWalletConfig;
|
||||
}
|
||||
|
||||
private DataSourceConfig computeDataSourceConfig(Integer storedBlockHeight) {
|
||||
return new DataSourceConfig(SparrowMinerFeeSupplier.getInstance(), new SparrowChainSupplier(storedBlockHeight), BIP_FORMAT.PROVIDER, BIP_WALLETS.WHIRLPOOL);
|
||||
}
|
||||
|
||||
private WhirlpoolInfo getWhirlpoolInfo() {
|
||||
if(whirlpoolInfo == null) {
|
||||
whirlpoolInfo = new WhirlpoolInfo(SparrowMinerFeeSupplier.getInstance(), config);
|
||||
}
|
||||
|
||||
return whirlpoolInfo;
|
||||
}
|
||||
|
||||
public Collection<Pool> getPools(Long totalUtxoValue) throws Exception {
|
||||
CoordinatorSupplier coordinatorSupplier = getWhirlpoolInfo().getCoordinatorSupplier();
|
||||
coordinatorSupplier.load();
|
||||
if(totalUtxoValue == null) {
|
||||
return coordinatorSupplier.getPools();
|
||||
}
|
||||
|
||||
return coordinatorSupplier.findPoolsForTx0(totalUtxoValue);
|
||||
}
|
||||
|
||||
public Tx0Previews getTx0Previews(Collection<UnspentOutput> utxos) throws Exception {
|
||||
Tx0Info tx0Info = getTx0Info();
|
||||
|
||||
// preview all pools
|
||||
Tx0Config tx0Config = computeTx0Config(tx0Info);
|
||||
return tx0Info.tx0Previews(tx0Config, utxos);
|
||||
}
|
||||
|
||||
public Tx0 broadcastTx0(Pool pool, Collection<BlockTransactionHashIndex> utxos) throws Exception {
|
||||
WhirlpoolWallet whirlpoolWallet = getWhirlpoolWallet();
|
||||
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform());
|
||||
UtxoSupplier utxoSupplier = whirlpoolWallet.getUtxoSupplier();
|
||||
List<WhirlpoolUtxo> whirlpoolUtxos = utxos.stream().map(ref -> utxoSupplier.findUtxo(ref.getHashAsString(), (int)ref.getIndex())).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
|
||||
if(whirlpoolUtxos.size() != utxos.size()) {
|
||||
throw new IllegalStateException("Failed to find UTXOs in Whirlpool wallet");
|
||||
}
|
||||
|
||||
Tx0Info tx0Info = getTx0Info();
|
||||
|
||||
WalletSupplier walletSupplier = whirlpoolWallet.getWalletSupplier();
|
||||
Tx0Config tx0Config = computeTx0Config(tx0Info);
|
||||
Tx0 tx0 = tx0Info.tx0(walletSupplier, utxoSupplier, whirlpoolUtxos, pool, tx0Config);
|
||||
|
||||
//Clear tx0 for new fee addresses
|
||||
clearTx0Info();
|
||||
return tx0;
|
||||
}
|
||||
|
||||
private Tx0Info getTx0Info() throws Exception {
|
||||
if(tx0Info == null) {
|
||||
tx0Info = fetchTx0Info();
|
||||
}
|
||||
|
||||
return tx0Info;
|
||||
}
|
||||
|
||||
private Tx0Info fetchTx0Info() throws Exception {
|
||||
return AsyncUtil.getInstance().blockingGet(
|
||||
Single.fromCallable(() -> getWhirlpoolInfo().fetchTx0Info(getScode()))
|
||||
.subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()));
|
||||
}
|
||||
|
||||
private void clearTx0Info() {
|
||||
tx0Info = null;
|
||||
}
|
||||
|
||||
private Tx0Config computeTx0Config(Tx0Info tx0Info) {
|
||||
Tx0Config tx0Config = tx0Info.getTx0Config(tx0FeeTarget, mixFeeTarget);
|
||||
tx0Config.setChangeWallet(SamouraiAccount.BADBANK);
|
||||
return tx0Config;
|
||||
}
|
||||
|
||||
public void setHDWallet(String walletId, Wallet wallet) {
|
||||
NetworkParameters params = config.getSamouraiNetwork().getParams();
|
||||
this.hdWallet = computeHdWallet(wallet, params);
|
||||
this.walletId = walletId;
|
||||
}
|
||||
|
||||
public static HD_Wallet computeHdWallet(Wallet wallet, NetworkParameters params) {
|
||||
if(wallet.isEncrypted()) {
|
||||
throw new IllegalStateException("Wallet cannot be encrypted");
|
||||
}
|
||||
|
||||
try {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
ScriptType scriptType = wallet.getScriptType();
|
||||
int purpose = scriptType.getDefaultDerivation().get(0).num();
|
||||
List<String> words = keystore.getSeed().getMnemonicCode();
|
||||
String passphrase = keystore.getSeed().getPassphrase() == null ? "" : keystore.getSeed().getPassphrase().asString();
|
||||
HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
|
||||
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
|
||||
return hdWalletFactory.getHD(purpose, seed, passphrase, params);
|
||||
} catch(Exception e) {
|
||||
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public WhirlpoolWallet getWhirlpoolWallet() throws WhirlpoolException {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
return whirlpoolWalletService.whirlpoolWallet();
|
||||
}
|
||||
|
||||
if(hdWallet == null) {
|
||||
throw new IllegalStateException("Whirlpool HD wallet not added");
|
||||
}
|
||||
|
||||
try {
|
||||
WhirlpoolWallet whirlpoolWallet = new WhirlpoolWallet(config, Utils.hexToBytes(hdWallet.getSeedHex()), hdWallet.getPassphrase(), walletId);
|
||||
return whirlpoolWalletService.openWallet(whirlpoolWallet, hdWallet.getPassphrase());
|
||||
} catch(Exception e) {
|
||||
throw new WhirlpoolException("Could not create whirlpool wallet ", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
whirlpoolWalletService.whirlpoolWallet().stop();
|
||||
}
|
||||
}
|
||||
|
||||
public void mix(BlockTransactionHashIndex utxo) throws WhirlpoolException {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
throw new WhirlpoolException("Whirlpool wallet not yet created");
|
||||
}
|
||||
|
||||
try {
|
||||
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
|
||||
whirlpoolWalletService.whirlpoolWallet().mix(whirlpoolUtxo);
|
||||
} catch(Exception e) {
|
||||
throw new WhirlpoolException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public void mixStop(BlockTransactionHashIndex utxo) throws WhirlpoolException {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
throw new WhirlpoolException("Whirlpool wallet not yet created");
|
||||
}
|
||||
|
||||
try {
|
||||
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
|
||||
whirlpoolWalletService.whirlpoolWallet().mixStop(whirlpoolUtxo);
|
||||
} catch(Exception e) {
|
||||
throw new WhirlpoolException(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress(BlockTransactionHashIndex utxo) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null || utxo.getStatus() == Status.FROZEN) {
|
||||
return null;
|
||||
}
|
||||
|
||||
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
|
||||
if(whirlpoolUtxo != null && whirlpoolUtxo.getUtxoState() != null) {
|
||||
MixProgress mixProgress = whirlpoolUtxo.getUtxoState().getMixProgress();
|
||||
if(mixProgress != null && !isMixing(utxo)) {
|
||||
log.debug("Utxo " + utxo + " mix state is " + whirlpoolUtxo.getUtxoState() + " but utxo is not mixing");
|
||||
return null;
|
||||
}
|
||||
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean isMixing(BlockTransactionHashIndex utxo) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null || !whirlpoolWalletService.whirlpoolWallet().isStarted()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whirlpoolWalletService.whirlpoolWallet().getMixingState().getUtxosMixing().stream().map(WhirlpoolUtxo::getUtxo).anyMatch(uo -> uo.tx_hash.equals(utxo.getHashAsString()) && uo.tx_output_n == (int)utxo.getIndex());
|
||||
}
|
||||
|
||||
public void refreshUtxos() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() != null) {
|
||||
whirlpoolWalletService.whirlpoolWallet().refreshUtxosAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform());
|
||||
}
|
||||
}
|
||||
|
||||
private void resyncMixesDone(Whirlpool whirlpool, Wallet postmixWallet) {
|
||||
Set<BlockTransactionHashIndex> receiveUtxos = postmixWallet.getWalletUtxos().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE).map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
for(BlockTransactionHashIndex utxo : receiveUtxos) {
|
||||
int mixesDone = recountMixesDone(postmixWallet, utxo);
|
||||
whirlpool.setMixesDone(utxo, mixesDone);
|
||||
}
|
||||
}
|
||||
|
||||
public int recountMixesDone(Wallet postmixWallet, BlockTransactionHashIndex postmixUtxo) {
|
||||
int mixesDone = 0;
|
||||
Set<BlockTransactionHashIndex> walletTxos = postmixWallet.getWalletTxos().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE).map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
BlockTransaction blkTx = postmixWallet.getTransactions().get(postmixUtxo.getHash());
|
||||
|
||||
while(blkTx != null) {
|
||||
mixesDone++;
|
||||
List<TransactionInput> inputs = blkTx.getTransaction().getInputs();
|
||||
blkTx = null;
|
||||
for(TransactionInput txInput : inputs) {
|
||||
BlockTransaction inputTx = postmixWallet.getTransactions().get(txInput.getOutpoint().getHash());
|
||||
if(inputTx != null && walletTxos.stream().anyMatch(txo -> txo.getHash().equals(inputTx.getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()) && inputTx.getTransaction() != null) {
|
||||
blkTx = inputTx;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mixesDone;
|
||||
}
|
||||
|
||||
public void setMixesDone(BlockTransactionHashIndex utxo, int mixesDone) {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
|
||||
if(whirlpoolUtxo != null) {
|
||||
whirlpoolUtxo.setMixsDone(mixesDone);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkIfMixing() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if(isMixing()) {
|
||||
if(!whirlpoolWalletService.whirlpoolWallet().isStarted()) {
|
||||
log.warn("Wallet is not started, but mixingProperty is true");
|
||||
WhirlpoolEventService.getInstance().post(new WalletStopEvent(whirlpoolWalletService.whirlpoolWallet()));
|
||||
} else if(whirlpoolWalletService.whirlpoolWallet().getMixingState().getUtxosMixing().isEmpty() &&
|
||||
!whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxos(SamouraiAccount.PREMIX, SamouraiAccount.POSTMIX).isEmpty()) {
|
||||
log.warn("No UTXOs mixing, but mixingProperty is true");
|
||||
//Will automatically restart
|
||||
AppServices.getWhirlpoolServices().stopWhirlpool(this, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void logDebug() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
log.warn("Whirlpool wallet for " + walletId + " not started");
|
||||
return;
|
||||
}
|
||||
|
||||
log.warn("Whirlpool debug for " + walletId + "\n" + whirlpoolWalletService.whirlpoolWallet().getDebug());
|
||||
}
|
||||
|
||||
public boolean hasWallet() {
|
||||
return hdWallet != null;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
if(whirlpoolWalletService.whirlpoolWallet() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return whirlpoolWalletService.whirlpoolWallet().isStarted();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
whirlpoolWalletService.closeWallet();
|
||||
}
|
||||
|
||||
public StartupService createStartupService() {
|
||||
if(startupService != null) {
|
||||
startupService.cancel();
|
||||
}
|
||||
|
||||
startupService = new StartupService(this);
|
||||
return startupService;
|
||||
}
|
||||
|
||||
public StartupService getStartupService() {
|
||||
return startupService;
|
||||
}
|
||||
|
||||
private WalletUtxo getUtxo(WhirlpoolUtxo whirlpoolUtxo) {
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet != null) {
|
||||
wallet = getStandardAccountWallet(whirlpoolUtxo.getAccount(), wallet);
|
||||
|
||||
if(wallet != null) {
|
||||
for(BlockTransactionHashIndex utxo : wallet.getWalletUtxos().keySet()) {
|
||||
if(utxo.getHashAsString().equals(whirlpoolUtxo.getUtxo().tx_hash) && utxo.getIndex() == whirlpoolUtxo.getUtxo().tx_output_n) {
|
||||
return new WalletUtxo(wallet, utxo);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static Wallet getWallet(String walletId) {
|
||||
return AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static Wallet getStandardAccountWallet(SamouraiAccount whirlpoolAccount, Wallet wallet) {
|
||||
StandardAccount standardAccount = getStandardAccount(whirlpoolAccount);
|
||||
if(StandardAccount.isWhirlpoolAccount(standardAccount) || wallet.getStandardAccountType() != standardAccount) {
|
||||
Wallet standardWallet = wallet.getChildWallet(standardAccount);
|
||||
if(standardWallet == null) {
|
||||
throw new IllegalStateException("Cannot find " + standardAccount + " wallet");
|
||||
}
|
||||
|
||||
return standardWallet;
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static StandardAccount getStandardAccount(SamouraiAccount whirlpoolAccount) {
|
||||
if(whirlpoolAccount == SamouraiAccount.PREMIX) {
|
||||
return StandardAccount.WHIRLPOOL_PREMIX;
|
||||
} else if(whirlpoolAccount == SamouraiAccount.POSTMIX) {
|
||||
return StandardAccount.WHIRLPOOL_POSTMIX;
|
||||
} else if(whirlpoolAccount == SamouraiAccount.BADBANK) {
|
||||
return StandardAccount.WHIRLPOOL_BADBANK;
|
||||
}
|
||||
|
||||
return StandardAccount.ACCOUNT_0;
|
||||
}
|
||||
|
||||
public static UnspentOutput getUnspentOutput(WalletNode node, BlockTransaction blockTransaction, int index) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get(index);
|
||||
|
||||
UnspentOutput out = new UnspentOutput();
|
||||
out.tx_hash = txOutput.getHash().toString();
|
||||
out.tx_output_n = txOutput.getIndex();
|
||||
out.value = txOutput.getValue();
|
||||
out.script = Utils.bytesToHex(txOutput.getScriptBytes());
|
||||
|
||||
try {
|
||||
out.addr = txOutput.getScript().getToAddresses()[0].toString();
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
|
||||
Transaction transaction = (Transaction)txOutput.getParent();
|
||||
out.tx_version = (int)transaction.getVersion();
|
||||
out.tx_locktime = transaction.getLocktime();
|
||||
if(AppServices.getCurrentBlockHeight() != null) {
|
||||
out.confirmations = blockTransaction.getConfirmations(AppServices.getCurrentBlockHeight());
|
||||
}
|
||||
|
||||
Wallet wallet = node.getWallet().isBip47() ? node.getWallet().getMasterWallet() : node.getWallet();
|
||||
if(wallet.getKeystores().size() != 1) {
|
||||
throw new IllegalStateException("Cannot mix outputs from a wallet with multiple keystores");
|
||||
}
|
||||
|
||||
SamouraiNetwork samouraiNetwork = AppServices.getWhirlpoolServices().getSamouraiNetwork();
|
||||
boolean testnet = FormatsUtilGeneric.getInstance().isTestNet(samouraiNetwork.getParams());
|
||||
|
||||
UnspentOutput.Xpub xpub = new UnspentOutput.Xpub();
|
||||
ExtendedKey.Header header = testnet ? ExtendedKey.Header.tpub : ExtendedKey.Header.xpub;
|
||||
xpub.m = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header);
|
||||
xpub.path = node.getWallet().isBip47() ? null : node.getDerivationPath().toUpperCase(Locale.ROOT);
|
||||
|
||||
out.xpub = xpub;
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
public void refreshTorCircuits() {
|
||||
AppServices.getHttpClientService().changeIdentity();
|
||||
}
|
||||
|
||||
public String getScode() {
|
||||
return config.getScode();
|
||||
}
|
||||
|
||||
public void setScode(String scode) {
|
||||
config.setScode(scode);
|
||||
}
|
||||
|
||||
public Tx0FeeTarget getTx0FeeTarget() {
|
||||
return tx0FeeTarget;
|
||||
}
|
||||
|
||||
public void setTx0FeeTarget(Tx0FeeTarget tx0FeeTarget) {
|
||||
this.tx0FeeTarget = tx0FeeTarget;
|
||||
}
|
||||
|
||||
public Tx0FeeTarget getMixFeeTarget() {
|
||||
return mixFeeTarget;
|
||||
}
|
||||
|
||||
public void setMixFeeTarget(Tx0FeeTarget mixFeeTarget) {
|
||||
this.mixFeeTarget = mixFeeTarget;
|
||||
}
|
||||
|
||||
public String getWalletId() {
|
||||
return walletId;
|
||||
}
|
||||
|
||||
public String getMixToWalletId() {
|
||||
return mixToWalletId;
|
||||
}
|
||||
|
||||
public void setResyncMixesDone(boolean resyncMixesDone) {
|
||||
this.resyncMixesDone = resyncMixesDone;
|
||||
}
|
||||
|
||||
public void setPostmixIndexRange(String indexRange) {
|
||||
if(indexRange != null) {
|
||||
try {
|
||||
config.setIndexRangePostmix(IndexRange.valueOf(indexRange));
|
||||
} catch(Exception e) {
|
||||
log.error("Invalid index range " + indexRange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setMixToWallet(String mixToWalletId, Integer minMixes) {
|
||||
if(mixToWalletId == null) {
|
||||
config.setExternalDestination(null);
|
||||
} else {
|
||||
Wallet mixToWallet = getWallet(mixToWalletId);
|
||||
if(mixToWallet == null) {
|
||||
throw new IllegalStateException("Cannot find mix to wallet with id " + mixToWalletId);
|
||||
}
|
||||
|
||||
int mixes = minMixes == null ? DEFAULT_MIXTO_MIN_MIXES : minMixes;
|
||||
|
||||
IPostmixHandler postmixHandler = new SparrowPostmixHandler(whirlpoolWalletService, mixToWallet, KeyPurpose.RECEIVE);
|
||||
ExternalDestination externalDestination = new ExternalDestination(postmixHandler, 0, mixes, DEFAULT_MIXTO_RANDOM_FACTOR);
|
||||
config.setExternalDestination(externalDestination);
|
||||
}
|
||||
|
||||
this.mixToWalletId = mixToWalletId;
|
||||
}
|
||||
|
||||
public boolean isMixing() {
|
||||
return mixingProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty mixingProperty() {
|
||||
return mixingProperty;
|
||||
}
|
||||
|
||||
public boolean isStarting() {
|
||||
return startingProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty startingProperty() {
|
||||
return startingProperty;
|
||||
}
|
||||
|
||||
public boolean isStopping() {
|
||||
return stoppingProperty.get();
|
||||
}
|
||||
|
||||
public BooleanProperty stoppingProperty() {
|
||||
return stoppingProperty;
|
||||
}
|
||||
|
||||
public Duration getStartupServiceDelay() {
|
||||
return startupServiceDelay;
|
||||
}
|
||||
|
||||
public void setStartupServiceDelay(Duration startupServiceDelay) {
|
||||
this.startupServiceDelay = startupServiceDelay;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixSuccess(MixSuccessEvent e) {
|
||||
WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo();
|
||||
WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo);
|
||||
if(walletUtxo != null) {
|
||||
log.debug("Mix success, new utxo " + e.getReceiveUtxo().getHash() + ":" + e.getReceiveUtxo().getIndex());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixSuccessEvent(walletUtxo.wallet, walletUtxo.utxo, e.getReceiveUtxo(), getReceiveNode(e, walletUtxo))));
|
||||
}
|
||||
}
|
||||
|
||||
private WalletNode getReceiveNode(MixSuccessEvent e, WalletUtxo walletUtxo) {
|
||||
for(WalletNode walletNode : walletUtxo.wallet.getNode(KeyPurpose.RECEIVE).getChildren()) {
|
||||
if(walletNode.getAddress().toString().equals(e.getReceiveDestination().getAddress())) {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixFail(MixFailEvent e) {
|
||||
WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo();
|
||||
WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo);
|
||||
if(walletUtxo != null) {
|
||||
log.debug("Mix failed for utxo " + whirlpoolUtxo.getUtxo().tx_hash + ":" + whirlpoolUtxo.getUtxo().tx_output_n + " " + e.getMixFailReason());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, e.getMixFailReason(), e.getError())));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onMixProgress(MixProgressEvent e) {
|
||||
WhirlpoolUtxo whirlpoolUtxo = e.getMixParams().getWhirlpoolUtxo();
|
||||
MixProgress mixProgress = whirlpoolUtxo.getUtxoState().getMixProgress();
|
||||
WalletUtxo walletUtxo = getUtxo(whirlpoolUtxo);
|
||||
if(walletUtxo != null && isMixing()) {
|
||||
log.debug("Mix progress for utxo " + whirlpoolUtxo.getUtxo().tx_hash + ":" + whirlpoolUtxo.getUtxo().tx_output_n + " " + whirlpoolUtxo.getMixsDone() + " " + mixProgress.getMixStep() + " " + whirlpoolUtxo.getUtxoState().getStatus());
|
||||
Platform.runLater(() -> EventManager.get().post(new WhirlpoolMixEvent(walletUtxo.wallet, walletUtxo.utxo, mixProgress)));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onWalletStart(WalletStartEvent e) {
|
||||
if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) {
|
||||
log.info("Mixing to " + e.getWhirlpoolWallet().getConfig().getExternalDestination());
|
||||
mixingProperty.set(true);
|
||||
|
||||
if(resyncMixesDone) {
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet != null) {
|
||||
Wallet postmixWallet = getStandardAccountWallet(SamouraiAccount.POSTMIX, wallet);
|
||||
resyncMixesDone(this, postmixWallet);
|
||||
resyncMixesDone = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void onWalletStop(WalletStopEvent e) {
|
||||
if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) {
|
||||
mixingProperty.set(false);
|
||||
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
if(wallet != null) {
|
||||
Platform.runLater(() -> {
|
||||
if(AppServices.isConnected()) {
|
||||
AppServices.getWhirlpoolServices().startWhirlpool(wallet, this, false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PoolsService extends Service<Collection<Pool>> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final Long totalUtxoValue;
|
||||
|
||||
public PoolsService(Whirlpool whirlpool, Long totalUtxoValue) {
|
||||
this.whirlpool = whirlpool;
|
||||
this.totalUtxoValue = totalUtxoValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Collection<Pool>> createTask() {
|
||||
return new Task<>() {
|
||||
protected Collection<Pool> call() throws Exception {
|
||||
return whirlpool.getPools(totalUtxoValue);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tx0PreviewsService extends Service<Tx0Previews> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final List<UtxoEntry> utxoEntries;
|
||||
|
||||
public Tx0PreviewsService(Whirlpool whirlpool, List<UtxoEntry> utxoEntries) {
|
||||
this.whirlpool = whirlpool;
|
||||
this.utxoEntries = utxoEntries;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Tx0Previews> createTask() {
|
||||
return new Task<>() {
|
||||
protected Tx0Previews call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Fetching premix preview...");
|
||||
|
||||
Collection<UnspentOutput> utxos = utxoEntries.stream().map(utxoEntry -> Whirlpool.getUnspentOutput(utxoEntry.getNode(), utxoEntry.getBlockTransaction(), (int)utxoEntry.getHashIndex().getIndex())).collect(Collectors.toList());
|
||||
return whirlpool.getTx0Previews(utxos);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class Tx0BroadcastService extends Service<Sha256Hash> {
|
||||
private final Whirlpool whirlpool;
|
||||
private final Pool pool;
|
||||
private final Collection<BlockTransactionHashIndex> utxos;
|
||||
|
||||
public Tx0BroadcastService(Whirlpool whirlpool, Pool pool, Collection<BlockTransactionHashIndex> utxos) {
|
||||
this.whirlpool = whirlpool;
|
||||
this.pool = pool;
|
||||
this.utxos = utxos;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Sha256Hash> createTask() {
|
||||
return new Task<>() {
|
||||
protected Sha256Hash call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Broadcasting premix transaction...");
|
||||
|
||||
Tx0 tx0 = whirlpool.broadcastTx0(pool, utxos);
|
||||
return Sha256Hash.wrap(tx0.getTx().getHashAsString());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class StartupService extends ScheduledService<WhirlpoolWallet> {
|
||||
private final Whirlpool whirlpool;
|
||||
|
||||
public StartupService(Whirlpool whirlpool) {
|
||||
this.whirlpool = whirlpool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<WhirlpoolWallet> createTask() {
|
||||
return new Task<>() {
|
||||
protected WhirlpoolWallet call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Starting Whirlpool...");
|
||||
|
||||
try {
|
||||
whirlpool.startingProperty.set(true);
|
||||
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
|
||||
if(AppServices.onlineProperty().get()) {
|
||||
whirlpoolWallet.startAsync().subscribeOn(Schedulers.io()).observeOn(JavaFxScheduler.platform()).subscribe();
|
||||
}
|
||||
|
||||
return whirlpoolWallet;
|
||||
} finally {
|
||||
whirlpool.startingProperty.set(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class ShutdownService extends Service<Boolean> {
|
||||
private final Whirlpool whirlpool;
|
||||
|
||||
public ShutdownService(Whirlpool whirlpool) {
|
||||
this.whirlpool = whirlpool;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Task<Boolean> createTask() {
|
||||
return new Task<>() {
|
||||
protected Boolean call() throws Exception {
|
||||
updateProgress(-1, 1);
|
||||
updateMessage("Disconnecting from Whirlpool...");
|
||||
|
||||
try {
|
||||
whirlpool.stoppingProperty.set(true);
|
||||
whirlpool.shutdown();
|
||||
return true;
|
||||
} finally {
|
||||
whirlpool.stoppingProperty.set(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static class WalletUtxo {
|
||||
public final Wallet wallet;
|
||||
public final BlockTransactionHashIndex utxo;
|
||||
|
||||
public WalletUtxo(Wallet wallet, BlockTransactionHashIndex utxo) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,361 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Previews;
|
||||
import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget;
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.BitcoinUnit;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.control.CopyableCoinLabel;
|
||||
import com.sparrowwallet.sparrow.control.CopyableLabel;
|
||||
import com.sparrowwallet.sparrow.event.WalletMasterMixConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.util.StringConverter;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class WhirlpoolController {
|
||||
private static final List<Tx0FeeTarget> FEE_TARGETS = List.of(Tx0FeeTarget.MIN, Tx0FeeTarget.BLOCKS_4, Tx0FeeTarget.BLOCKS_2);
|
||||
|
||||
@FXML
|
||||
private VBox whirlpoolBox;
|
||||
|
||||
@FXML
|
||||
private VBox step1;
|
||||
|
||||
@FXML
|
||||
private VBox step2;
|
||||
|
||||
@FXML
|
||||
private VBox step3;
|
||||
|
||||
@FXML
|
||||
private VBox step4;
|
||||
|
||||
@FXML
|
||||
private TextField scode;
|
||||
|
||||
@FXML
|
||||
private Slider premixPriority;
|
||||
|
||||
@FXML
|
||||
private CopyableLabel premixFeeRate;
|
||||
|
||||
@FXML
|
||||
private Label lowPremixFeeRate;
|
||||
|
||||
@FXML
|
||||
private ComboBox<Pool> pool;
|
||||
|
||||
@FXML
|
||||
private VBox selectedPool;
|
||||
|
||||
@FXML
|
||||
private CopyableCoinLabel poolFee;
|
||||
|
||||
@FXML
|
||||
private Label poolInsufficient;
|
||||
|
||||
@FXML
|
||||
private Label poolAnonset;
|
||||
|
||||
@FXML
|
||||
private HBox discountFeeBox;
|
||||
|
||||
@FXML
|
||||
private HBox nbOutputsBox;
|
||||
|
||||
@FXML
|
||||
private Label nbOutputsLoading;
|
||||
|
||||
@FXML
|
||||
private Label nbOutputs;
|
||||
|
||||
@FXML
|
||||
private CopyableCoinLabel discountFee;
|
||||
|
||||
private String walletId;
|
||||
private Wallet wallet;
|
||||
private MixConfig mixConfig;
|
||||
private List<UtxoEntry> utxoEntries;
|
||||
private Tx0Previews tx0Previews;
|
||||
private final ObjectProperty<Tx0Preview> tx0PreviewProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
public void initializeView(String walletId, Wallet wallet, List<UtxoEntry> utxoEntries) {
|
||||
this.walletId = walletId;
|
||||
this.wallet = wallet;
|
||||
this.utxoEntries = utxoEntries;
|
||||
this.mixConfig = wallet.getMasterMixConfig();
|
||||
|
||||
step1.managedProperty().bind(step1.visibleProperty());
|
||||
step2.managedProperty().bind(step2.visibleProperty());
|
||||
step3.managedProperty().bind(step3.visibleProperty());
|
||||
step4.managedProperty().bind(step4.visibleProperty());
|
||||
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(false);
|
||||
|
||||
scode.setText(mixConfig.getScode() == null ? "" : mixConfig.getScode());
|
||||
scode.setTextFormatter(new TextFormatter<>((change) -> {
|
||||
change.setText(change.getText().toUpperCase(Locale.ROOT));
|
||||
return change;
|
||||
}));
|
||||
scode.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pool.setItems(FXCollections.emptyObservableList());
|
||||
tx0PreviewProperty.set(null);
|
||||
mixConfig.setScode(newValue);
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(wallet));
|
||||
});
|
||||
|
||||
premixPriority.setMin(0);
|
||||
premixPriority.setMax(FEE_TARGETS.size() - 1);
|
||||
premixPriority.setMajorTickUnit(1);
|
||||
premixPriority.setMinorTickCount(0);
|
||||
premixPriority.setLabelFormatter(new StringConverter<>() {
|
||||
@Override
|
||||
public String toString(Double object) {
|
||||
return object.intValue() == 0 ? "Low" : (object.intValue() == 1 ? "Normal" : "High");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
premixPriority.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
pool.setItems(FXCollections.emptyObservableList());
|
||||
tx0Previews = null;
|
||||
tx0PreviewProperty.set(null);
|
||||
Tx0FeeTarget tx0FeeTarget = FEE_TARGETS.get(newValue.intValue());
|
||||
premixFeeRate.setText(getFeeRate(tx0FeeTarget) + " sats/vB");
|
||||
lowPremixFeeRate.setVisible(tx0FeeTarget == Tx0FeeTarget.MIN && getFeeRate(tx0FeeTarget) * 2 < getFeeRate(Tx0FeeTarget.BLOCKS_4));
|
||||
});
|
||||
premixPriority.setValue(1);
|
||||
lowPremixFeeRate.managedProperty().bind(lowPremixFeeRate.visibleProperty());
|
||||
lowPremixFeeRate.setVisible(false);
|
||||
|
||||
if(mixConfig.getScode() != null) {
|
||||
step1.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
}
|
||||
|
||||
pool.setConverter(new StringConverter<Pool>() {
|
||||
@Override
|
||||
public String toString(Pool selectedPool) {
|
||||
if(selectedPool == null) {
|
||||
pool.setTooltip(null);
|
||||
return "Fetching pools...";
|
||||
}
|
||||
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
BitcoinUnit bitcoinUnit = wallet.getAutoUnit();
|
||||
String satsValue = format.formatSatsValue(selectedPool.getDenomination()) + " sats";
|
||||
String btcValue = format.formatBtcValue(selectedPool.getDenomination()) + " BTC";
|
||||
|
||||
pool.setTooltip(bitcoinUnit == BitcoinUnit.BTC ? new Tooltip(satsValue) : new Tooltip(btcValue));
|
||||
return bitcoinUnit == BitcoinUnit.BTC ? btcValue : satsValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pool fromString(String string) {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
pool.valueProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue == null) {
|
||||
selectedPool.setVisible(false);
|
||||
} else {
|
||||
poolFee.setValue(newValue.getFeeValue());
|
||||
poolAnonset.setText(newValue.getAnonymitySet() + " UTXOs");
|
||||
selectedPool.setVisible(true);
|
||||
fetchTx0Preview(newValue);
|
||||
}
|
||||
});
|
||||
|
||||
step4.visibleProperty().addListener((observable, oldValue, newValue) -> {
|
||||
if(newValue && pool.getItems().isEmpty()) {
|
||||
fetchPools();
|
||||
}
|
||||
});
|
||||
|
||||
selectedPool.managedProperty().bind(selectedPool.visibleProperty());
|
||||
selectedPool.setVisible(false);
|
||||
pool.managedProperty().bind(pool.visibleProperty());
|
||||
poolInsufficient.managedProperty().bind(poolInsufficient.visibleProperty());
|
||||
poolInsufficient.visibleProperty().bind(pool.visibleProperty().not());
|
||||
discountFeeBox.managedProperty().bind(discountFeeBox.visibleProperty());
|
||||
discountFeeBox.setVisible(false);
|
||||
nbOutputsBox.managedProperty().bind(nbOutputsBox.visibleProperty());
|
||||
nbOutputsBox.setVisible(false);
|
||||
nbOutputsLoading.managedProperty().bind(nbOutputsLoading.visibleProperty());
|
||||
nbOutputs.managedProperty().bind(nbOutputs.visibleProperty());
|
||||
nbOutputsLoading.visibleProperty().bind(nbOutputs.visibleProperty().not());
|
||||
nbOutputs.setVisible(false);
|
||||
|
||||
tx0PreviewProperty.addListener((observable, oldValue, tx0Preview) -> {
|
||||
if(tx0Preview == null) {
|
||||
nbOutputsBox.setVisible(true);
|
||||
nbOutputsLoading.setText("Calculating...");
|
||||
nbOutputs.setVisible(false);
|
||||
discountFeeBox.setVisible(false);
|
||||
} else {
|
||||
discountFeeBox.setVisible(tx0Preview.getPool().getFeeValue() != tx0Preview.getTx0Data().getFeeValue());
|
||||
discountFee.setValue(tx0Preview.getTx0Data().getFeeValue());
|
||||
nbOutputsBox.setVisible(true);
|
||||
nbOutputs.setText(tx0Preview.getNbPremix() + " UTXOs");
|
||||
nbOutputs.setVisible(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int getFeeRate(Tx0FeeTarget tx0FeeTarget) {
|
||||
return SparrowMinerFeeSupplier.getFee(Integer.parseInt(tx0FeeTarget.getFeeTarget().getValue()));
|
||||
}
|
||||
|
||||
public boolean next() {
|
||||
if(step1.isVisible()) {
|
||||
step1.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step4.setVisible(true);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean back() {
|
||||
if(step2.isVisible()) {
|
||||
step2.setVisible(false);
|
||||
step1.setVisible(true);
|
||||
return false;
|
||||
}
|
||||
|
||||
if(step3.isVisible()) {
|
||||
step3.setVisible(false);
|
||||
step2.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
if(step4.isVisible()) {
|
||||
step4.setVisible(false);
|
||||
step3.setVisible(true);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void fetchPools() {
|
||||
long totalUtxoValue = utxoEntries.stream().mapToLong(Entry::getValue).sum();
|
||||
Whirlpool.PoolsService poolsService = new Whirlpool.PoolsService(AppServices.getWhirlpoolServices().getWhirlpool(walletId), totalUtxoValue);
|
||||
poolsService.setOnSucceeded(workerStateEvent -> {
|
||||
List<Pool> availablePools = poolsService.getValue().stream().toList();
|
||||
if(availablePools.isEmpty()) {
|
||||
pool.setVisible(false);
|
||||
|
||||
Whirlpool.PoolsService allPoolsService = new Whirlpool.PoolsService(AppServices.getWhirlpoolServices().getWhirlpool(walletId), null);
|
||||
allPoolsService.setOnSucceeded(poolsStateEvent -> {
|
||||
OptionalLong optMinValue = allPoolsService.getValue().stream().mapToLong(pool1 -> pool1.getPremixValueMin() + pool1.getFeeValue()).min();
|
||||
if(optMinValue.isPresent() && totalUtxoValue < optMinValue.getAsLong()) {
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
String satsValue = format.formatSatsValue(optMinValue.getAsLong()) + " sats";
|
||||
String btcValue = format.formatBtcValue(optMinValue.getAsLong()) + " BTC";
|
||||
poolInsufficient.setText("No available pools. Select a value over " + (Config.get().getBitcoinUnit() == BitcoinUnit.BTC ? btcValue : satsValue) + ".");
|
||||
}
|
||||
});
|
||||
allPoolsService.start();
|
||||
} else {
|
||||
pool.setDisable(false);
|
||||
pool.setItems(FXCollections.observableList(availablePools));
|
||||
pool.getSelectionModel().select(0);
|
||||
}
|
||||
});
|
||||
poolsService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
Optional<ButtonType> optButton = AppServices.showErrorDialog("Error fetching pools", exception.getMessage(), ButtonType.CANCEL, new ButtonType("Retry", ButtonBar.ButtonData.APPLY));
|
||||
if(optButton.isPresent()) {
|
||||
if(optButton.get().getButtonData().equals(ButtonBar.ButtonData.APPLY)) {
|
||||
fetchPools();
|
||||
} else {
|
||||
pool.setDisable(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
poolsService.start();
|
||||
}
|
||||
|
||||
private void fetchTx0Preview(Pool pool) {
|
||||
if(mixConfig.getScode() == null) {
|
||||
mixConfig.setScode("");
|
||||
EventManager.get().post(new WalletMasterMixConfigChangedEvent(wallet));
|
||||
}
|
||||
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
if(tx0Previews != null && mixConfig.getScode().equals(whirlpool.getScode())) {
|
||||
Tx0Preview tx0Preview = tx0Previews.getTx0Preview(pool.getPoolId());
|
||||
tx0PreviewProperty.set(tx0Preview);
|
||||
} else {
|
||||
tx0Previews = null;
|
||||
whirlpool.setScode(mixConfig.getScode());
|
||||
whirlpool.setTx0FeeTarget(FEE_TARGETS.get(premixPriority.valueProperty().intValue()));
|
||||
whirlpool.setMixFeeTarget(FEE_TARGETS.get(premixPriority.valueProperty().intValue()));
|
||||
|
||||
Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, utxoEntries);
|
||||
tx0PreviewsService.setOnRunning(workerStateEvent -> {
|
||||
nbOutputsBox.setVisible(true);
|
||||
nbOutputsLoading.setText("Calculating...");
|
||||
nbOutputs.setVisible(false);
|
||||
discountFeeBox.setVisible(false);
|
||||
tx0PreviewProperty.set(null);
|
||||
});
|
||||
tx0PreviewsService.setOnSucceeded(workerStateEvent -> {
|
||||
tx0Previews = tx0PreviewsService.getValue();
|
||||
Tx0Preview tx0Preview = tx0Previews.getTx0Preview(this.pool.getValue() == null ? pool.getPoolId() : this.pool.getValue().getPoolId());
|
||||
tx0PreviewProperty.set(tx0Preview);
|
||||
});
|
||||
tx0PreviewsService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
|
||||
nbOutputsLoading.setText("Error fetching Tx0: " + exception.getMessage());
|
||||
});
|
||||
tx0PreviewsService.start();
|
||||
}
|
||||
}
|
||||
|
||||
public ObjectProperty<Tx0Preview> getTx0PreviewProperty() {
|
||||
return tx0PreviewProperty;
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.samourai.whirlpool.client.tx0.Tx0Preview;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import javafx.beans.value.ChangeListener;
|
||||
import javafx.beans.value.ObservableValue;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.control.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
public class WhirlpoolDialog extends Dialog<Tx0Preview> {
|
||||
public WhirlpoolDialog(String walletId, Wallet wallet, List<UtxoEntry> utxoEntries) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
|
||||
try {
|
||||
FXMLLoader whirlpoolLoader = new FXMLLoader(AppServices.class.getResource("whirlpool/whirlpool.fxml"));
|
||||
dialogPane.setContent(whirlpoolLoader.load());
|
||||
WhirlpoolController whirlpoolController = whirlpoolLoader.getController();
|
||||
whirlpoolController.initializeView(walletId, wallet, utxoEntries);
|
||||
|
||||
dialogPane.setPrefWidth(600);
|
||||
dialogPane.setPrefHeight(570);
|
||||
dialogPane.setMinHeight(dialogPane.getPrefHeight());
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("whirlpool/whirlpool.css").toExternalForm());
|
||||
|
||||
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType previewButtonType = new javafx.scene.control.ButtonType("Preview Premix", ButtonBar.ButtonData.APPLY);
|
||||
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, cancelButtonType, previewButtonType);
|
||||
|
||||
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
|
||||
Button backButton = (Button)dialogPane.lookupButton(backButtonType);
|
||||
Button previewButton = (Button)dialogPane.lookupButton(previewButtonType);
|
||||
previewButton.setDisable(true);
|
||||
whirlpoolController.getTx0PreviewProperty().addListener(new ChangeListener<Tx0Preview>() {
|
||||
@Override
|
||||
public void changed(ObservableValue<? extends Tx0Preview> observable, Tx0Preview oldValue, Tx0Preview newValue) {
|
||||
previewButton.setDisable(newValue == null);
|
||||
}
|
||||
});
|
||||
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
previewButton.managedProperty().bind(previewButton.visibleProperty());
|
||||
|
||||
if(wallet.getMasterMixConfig().getScode() == null) {
|
||||
backButton.setDisable(true);
|
||||
}
|
||||
previewButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
nextButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
if(!whirlpoolController.next()) {
|
||||
nextButton.setVisible(false);
|
||||
previewButton.setDefaultButton(true);
|
||||
}
|
||||
backButton.setDisable(false);
|
||||
event.consume();
|
||||
});
|
||||
|
||||
backButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
nextButton.setVisible(true);
|
||||
if(!whirlpoolController.back()) {
|
||||
backButton.setDisable(true);
|
||||
}
|
||||
event.consume();
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> dialogButton.getButtonData().equals(ButtonBar.ButtonData.APPLY) ? whirlpoolController.getTx0PreviewProperty().get() : null);
|
||||
} catch(IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
public class WhirlpoolException extends Exception {
|
||||
public WhirlpoolException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public WhirlpoolException(String message, Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
@@ -1,330 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.soroban.client.SorobanConfig;
|
||||
import com.samourai.soroban.client.rpc.RpcClientService;
|
||||
import com.samourai.wallet.constants.SamouraiNetwork;
|
||||
import com.samourai.wallet.util.ExtLibJConfig;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
|
||||
import com.sparrowwallet.drongo.Drongo;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.MixConfig;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.WalletTabData;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.HttpClientService;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyCodeCombination;
|
||||
import javafx.scene.input.KeyCombination;
|
||||
import javafx.stage.Window;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.getHttpClientService;
|
||||
import static com.sparrowwallet.sparrow.AppServices.getTorProxy;
|
||||
import static org.bitcoinj.crypto.MnemonicCode.SPARROW_FIX_NFKD_MNEMONIC;
|
||||
|
||||
public class WhirlpoolServices {
|
||||
private static final Logger log = LoggerFactory.getLogger(WhirlpoolServices.class);
|
||||
|
||||
private final Map<String, Whirlpool> whirlpoolMap = new HashMap<>();
|
||||
|
||||
private final SorobanConfig sorobanConfig;
|
||||
|
||||
public WhirlpoolServices() {
|
||||
ExtLibJConfig extLibJConfig = computeExtLibJConfig();
|
||||
this.sorobanConfig = new SorobanConfig(extLibJConfig);
|
||||
System.setProperty(SPARROW_FIX_NFKD_MNEMONIC, "true");
|
||||
}
|
||||
|
||||
private ExtLibJConfig computeExtLibJConfig() {
|
||||
HttpClientService httpClientService = AppServices.getHttpClientService();
|
||||
boolean onion = (getTorProxy() != null);
|
||||
SamouraiNetwork samouraiNetwork = getSamouraiNetwork();
|
||||
return new ExtLibJConfig(samouraiNetwork, onion, Drongo.getProvider(), httpClientService);
|
||||
}
|
||||
|
||||
public SamouraiNetwork getSamouraiNetwork() {
|
||||
return SamouraiNetwork.valueOf(Network.get().getName().toUpperCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
public Whirlpool getWhirlpool(Wallet wallet) {
|
||||
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
|
||||
for(Map.Entry<Wallet, Storage> entry : AppServices.get().getOpenWallets().entrySet()) {
|
||||
if(entry.getKey() == masterWallet) {
|
||||
return whirlpoolMap.get(entry.getValue().getWalletId(entry.getKey()));
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Whirlpool getWhirlpool(String walletId) {
|
||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||
if(whirlpool == null) {
|
||||
Wallet wallet = AppServices.get().getWallet(walletId);
|
||||
whirlpool = new Whirlpool(wallet == null ? null : wallet.getStoredBlockHeight());
|
||||
whirlpoolMap.put(walletId, whirlpool);
|
||||
}
|
||||
|
||||
return whirlpool;
|
||||
}
|
||||
|
||||
private void bindDebugAccelerator() {
|
||||
List<Window> windows = whirlpoolMap.keySet().stream().map(walletId -> AppServices.get().getWindowForWallet(walletId)).filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
||||
for(Window window : windows) {
|
||||
KeyCombination keyCombination = new KeyCodeCombination(KeyCode.W, KeyCombination.CONTROL_DOWN, KeyCombination.SHIFT_DOWN, KeyCombination.ALT_DOWN);
|
||||
if(window.getScene() != null && !window.getScene().getAccelerators().containsKey(keyCombination)) {
|
||||
window.getScene().getAccelerators().put(keyCombination, () -> {
|
||||
for(Whirlpool whirlpool : whirlpoolMap.values()) {
|
||||
whirlpool.logDebug();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startAllWhirlpool() {
|
||||
for(Map.Entry<String, Whirlpool> entry : whirlpoolMap.entrySet().stream().filter(entry -> entry.getValue().hasWallet() && !entry.getValue().isStarted()).collect(Collectors.toList())) {
|
||||
Wallet wallet = AppServices.get().getWallet(entry.getKey());
|
||||
Whirlpool whirlpool = entry.getValue();
|
||||
startWhirlpool(wallet, whirlpool, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void startWhirlpool(Wallet wallet, Whirlpool whirlpool, boolean notifyIfMixToMissing) {
|
||||
if(wallet.getMasterMixConfig().getMixOnStartup() != Boolean.FALSE) {
|
||||
try {
|
||||
String mixToWalletId = getWhirlpoolMixToWalletId(wallet.getMasterMixConfig());
|
||||
whirlpool.setMixToWallet(mixToWalletId, wallet.getMasterMixConfig().getMinMixes());
|
||||
} catch(NoSuchElementException e) {
|
||||
if(notifyIfMixToMissing) {
|
||||
AppServices.showWarningDialog("Mix to wallet not open", wallet.getMasterName() + " is configured to mix to " + wallet.getMasterMixConfig().getMixToWalletName() + ", but this wallet is not open. Mix to wallets are required to be open to avoid address reuse.");
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.getMasterMixConfig() != null) {
|
||||
whirlpool.setPostmixIndexRange(wallet.getMasterMixConfig().getIndexRange());
|
||||
}
|
||||
|
||||
Whirlpool.StartupService startupService = whirlpool.createStartupService();
|
||||
if(whirlpool.getStartupServiceDelay() != null) {
|
||||
startupService.setDelay(whirlpool.getStartupServiceDelay());
|
||||
whirlpool.setStartupServiceDelay(null);
|
||||
}
|
||||
|
||||
startupService.setPeriod(Duration.minutes(2));
|
||||
startupService.setOnSucceeded(workerStateEvent -> {
|
||||
startupService.cancel();
|
||||
});
|
||||
startupService.setOnFailed(workerStateEvent -> {
|
||||
Throwable exception = workerStateEvent.getSource().getException();
|
||||
while(exception.getCause() != null) {
|
||||
exception = exception.getCause();
|
||||
}
|
||||
if(exception instanceof TimeoutException || exception instanceof SocketTimeoutException) {
|
||||
EventManager.get().post(new StatusEvent("Error connecting to Whirlpool server, will retry soon..."));
|
||||
HostAndPort torProxy = getTorProxy();
|
||||
if(torProxy != null) {
|
||||
whirlpool.refreshTorCircuits();
|
||||
}
|
||||
log.error("Error connecting to Whirlpool server: " + exception.getMessage());
|
||||
} else {
|
||||
log.error("Failed to start Whirlpool", workerStateEvent.getSource().getException());
|
||||
}
|
||||
});
|
||||
startupService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void stopAllWhirlpool() {
|
||||
for(Whirlpool whirlpool : whirlpoolMap.values().stream().filter(Whirlpool::isStarted).collect(Collectors.toList())) {
|
||||
stopWhirlpool(whirlpool, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void stopWhirlpool(Whirlpool whirlpool, boolean notifyOnFailure) {
|
||||
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
|
||||
shutdownService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to stop whirlpool", workerStateEvent.getSource().getException());
|
||||
if(notifyOnFailure) {
|
||||
AppServices.showErrorDialog("Failed to stop whirlpool", workerStateEvent.getSource().getException().getMessage());
|
||||
}
|
||||
});
|
||||
shutdownService.start();
|
||||
}
|
||||
|
||||
public String getWhirlpoolMixToWalletId(MixConfig mixConfig) {
|
||||
if(mixConfig == null || mixConfig.getMixToWalletFile() == null || mixConfig.getMixToWalletName() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AppServices.get().getOpenWallets().entrySet().stream()
|
||||
.filter(entry -> entry.getValue().getWalletFile().equals(mixConfig.getMixToWalletFile()) && entry.getKey().getName().equals(mixConfig.getMixToWalletName()))
|
||||
.map(entry -> entry.getValue().getWalletId(entry.getKey()))
|
||||
.findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
public Whirlpool getWhirlpoolForMixToWallet(String walletId) {
|
||||
return whirlpoolMap.values().stream().filter(whirlpool -> walletId.equals(whirlpool.getMixToWalletId())).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public static boolean canWalletMix(Wallet wallet) {
|
||||
return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getStandardAccountType() != null
|
||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
||||
}
|
||||
|
||||
public static boolean canWatchPostmix(Wallet wallet) {
|
||||
return Whirlpool.WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1;
|
||||
}
|
||||
|
||||
public static List<Wallet> prepareWhirlpoolWallet(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(walletId, decryptedWallet);
|
||||
whirlpool.setResyncMixesDone(true);
|
||||
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(decryptedWallet);
|
||||
|
||||
List<Wallet> childWallets = new ArrayList<>();
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
childWallets.add(childWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
ExtLibJConfig extLibJConfig = sorobanConfig.getExtLibJConfig();
|
||||
extLibJConfig.setOnion(getTorProxy() != null);
|
||||
getHttpClientService(); //Ensure proxy is updated
|
||||
|
||||
startAllWhirlpool();
|
||||
bindDebugAccelerator();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void disconnection(DisconnectionEvent event) {
|
||||
//Cancel any scheduled attempts to try reconnect
|
||||
whirlpoolMap.values().stream().filter(whirlpool -> whirlpool.getStartupService() != null).forEach(whirlpool -> whirlpool.getStartupService().cancel());
|
||||
stopAllWhirlpool();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
for(Whirlpool whirlpool : whirlpoolMap.values()) {
|
||||
whirlpool.checkIfMixing();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletOpened(WalletOpenedEvent event) {
|
||||
String walletId = event.getStorage().getWalletId(event.getWallet());
|
||||
Whirlpool whirlpool = whirlpoolMap.get(walletId);
|
||||
if(whirlpool != null && !whirlpool.isStarted() && AppServices.isConnected()) {
|
||||
startWhirlpool(event.getWallet(), whirlpool, true);
|
||||
}
|
||||
|
||||
Whirlpool mixFromWhirlpool = whirlpoolMap.entrySet().stream()
|
||||
.filter(entry -> {
|
||||
MixConfig mixConfig = AppServices.get().getWallet(entry.getKey()).getMasterMixConfig();
|
||||
return event.getStorage().getWalletFile().equals(mixConfig.getMixToWalletFile()) && event.getWallet().getName().equals(mixConfig.getMixToWalletName());
|
||||
})
|
||||
.map(Map.Entry::getValue).findFirst().orElse(null);
|
||||
|
||||
if(mixFromWhirlpool != null) {
|
||||
mixFromWhirlpool.setMixToWallet(walletId, AppServices.get().getWallet(mixFromWhirlpool.getWalletId()).getMasterMixConfig().getMinMixes());
|
||||
if(mixFromWhirlpool.isStarted()) {
|
||||
//Will automatically restart
|
||||
stopWhirlpool(mixFromWhirlpool, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData walletTabData : event.getClosedWalletTabData()) {
|
||||
String walletId = walletTabData.getStorage().getWalletId(walletTabData.getWallet());
|
||||
Whirlpool whirlpool = whirlpoolMap.remove(walletId);
|
||||
if(whirlpool != null) {
|
||||
if(whirlpool.isStarted()) {
|
||||
Whirlpool.ShutdownService shutdownService = new Whirlpool.ShutdownService(whirlpool);
|
||||
shutdownService.setOnSucceeded(workerStateEvent -> {
|
||||
WhirlpoolEventService.getInstance().unregister(whirlpool);
|
||||
});
|
||||
shutdownService.setOnFailed(workerStateEvent -> {
|
||||
log.error("Failed to shutdown whirlpool", workerStateEvent.getSource().getException());
|
||||
});
|
||||
shutdownService.start();
|
||||
} else {
|
||||
//Ensure http clients are shutdown
|
||||
whirlpool.shutdown();
|
||||
WhirlpoolEventService.getInstance().unregister(whirlpool);
|
||||
}
|
||||
}
|
||||
|
||||
Whirlpool mixToWhirlpool = getWhirlpoolForMixToWallet(walletId);
|
||||
if(mixToWhirlpool != null && event.getClosedWalletTabData().stream().noneMatch(walletTabData1 -> walletTabData1.getWalletForm().getWalletId().equals(mixToWhirlpool.getWalletId()))) {
|
||||
mixToWhirlpool.setMixToWallet(null, null);
|
||||
if(mixToWhirlpool.isStarted()) {
|
||||
//Will automatically restart
|
||||
stopWhirlpool(mixToWhirlpool, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
Whirlpool whirlpool = getWhirlpool(event.getWallet());
|
||||
if(whirlpool != null) {
|
||||
whirlpool.refreshUtxos();
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void whirlpoolIndexHighFrequency(WhirlpoolIndexHighFrequencyEvent event) {
|
||||
Whirlpool whirlpool = getWhirlpool(event.getWallet());
|
||||
if(whirlpool != null && whirlpool.isStarted() && !whirlpool.isStopping()) {
|
||||
log.warn("Rapidly increasing address index detected, temporarily disconnecting " + event.getWallet().getMasterName() + " from Whirlpool");
|
||||
Platform.runLater(() -> {
|
||||
EventManager.get().post(new StatusEvent("Error communicating with Whirlpool, will retry soon..."));
|
||||
whirlpool.setStartupServiceDelay(Duration.minutes(5));
|
||||
stopWhirlpool(whirlpool, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public SorobanConfig getSorobanConfig() {
|
||||
return sorobanConfig;
|
||||
}
|
||||
}
|
||||
-77
@@ -1,77 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool.dataPersister;
|
||||
|
||||
import com.samourai.wallet.util.AbstractOrchestrator;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersistableSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier;
|
||||
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowWalletStateSupplier;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class SparrowDataPersister implements DataPersister {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowDataPersister.class);
|
||||
|
||||
private final WalletStateSupplier walletStateSupplier;
|
||||
private final UtxoConfigSupplier utxoConfigSupplier;
|
||||
|
||||
private AbstractOrchestrator persistOrchestrator;
|
||||
private final int persistDelaySeconds;
|
||||
|
||||
public SparrowDataPersister(WhirlpoolWallet whirlpoolWallet, int persistDelaySeconds) throws Exception {
|
||||
WhirlpoolWalletConfig config = whirlpoolWallet.getConfig();
|
||||
String walletIdentifier = whirlpoolWallet.getWalletIdentifier();
|
||||
this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config);
|
||||
this.utxoConfigSupplier = new UtxoConfigPersistableSupplier(new SparrowUtxoConfigPersister(walletIdentifier));
|
||||
this.persistDelaySeconds = persistDelaySeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open() throws Exception {
|
||||
startPersistOrchestrator();
|
||||
}
|
||||
|
||||
protected void startPersistOrchestrator() {
|
||||
persistOrchestrator = new AbstractOrchestrator(persistDelaySeconds * 1000) {
|
||||
@Override
|
||||
protected void runOrchestrator() {
|
||||
try {
|
||||
persist(false);
|
||||
} catch (Exception e) {
|
||||
log.error("Error persisting Whirlpool data", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
persistOrchestrator.start(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
persistOrchestrator.stop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void load() throws Exception {
|
||||
utxoConfigSupplier.load();
|
||||
walletStateSupplier.load();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void persist(boolean force) throws Exception {
|
||||
utxoConfigSupplier.persist(force);
|
||||
walletStateSupplier.persist(force);
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletStateSupplier getWalletStateSupplier() {
|
||||
return walletStateSupplier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UtxoConfigSupplier getUtxoConfigSupplier() {
|
||||
return utxoConfigSupplier;
|
||||
}
|
||||
}
|
||||
-74
@@ -1,74 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool.dataPersister;
|
||||
|
||||
import com.google.common.collect.MapDifference;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigData;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersisted;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersisterFile;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.wallet.UtxoMixData;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletUtxoMixesChangedEvent;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class SparrowUtxoConfigPersister extends UtxoConfigPersisterFile {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowUtxoConfigPersister.class);
|
||||
|
||||
private final String walletId;
|
||||
|
||||
public SparrowUtxoConfigPersister(String walletId) {
|
||||
super(walletId);
|
||||
this.walletId = walletId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized UtxoConfigData read() throws Exception {
|
||||
Wallet wallet = getWallet();
|
||||
if(wallet == null) {
|
||||
throw new IllegalStateException("Can't find wallet with walletId " + walletId);
|
||||
}
|
||||
|
||||
Map<String, UtxoConfigPersisted> utxoConfigs = wallet.getUtxoMixes().entrySet().stream()
|
||||
.collect(Collectors.toMap(entry -> entry.getKey().toString(), entry -> new UtxoConfigPersisted(entry.getValue().getMixesDone(), entry.getValue().getExpired()),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
|
||||
ConcurrentHashMap::new));
|
||||
|
||||
return new UtxoConfigData(utxoConfigs);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doWrite(UtxoConfigData data) throws Exception {
|
||||
Wallet wallet = getWallet();
|
||||
if(wallet == null) {
|
||||
//Wallet is already closed
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, UtxoConfigPersisted> currentData = new HashMap<>(data.getUtxoConfigs());
|
||||
Map<Sha256Hash, UtxoMixData> changedUtxoMixes = currentData.entrySet().stream()
|
||||
.collect(Collectors.toMap(entry -> Sha256Hash.wrap(entry.getKey()), entry -> new UtxoMixData(entry.getValue().getMixsDone(), entry.getValue().getExpired()),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate utxo config hashes"); },
|
||||
HashMap::new));
|
||||
|
||||
MapDifference<Sha256Hash, UtxoMixData> mapDifference = Maps.difference(changedUtxoMixes, wallet.getUtxoMixes());
|
||||
Map<Sha256Hash, UtxoMixData> removedUtxoMixes = mapDifference.entriesOnlyOnRight();
|
||||
wallet.getUtxoMixes().putAll(changedUtxoMixes);
|
||||
wallet.getUtxoMixes().keySet().removeAll(removedUtxoMixes.keySet());
|
||||
|
||||
if(!changedUtxoMixes.isEmpty() || !removedUtxoMixes.isEmpty()) {
|
||||
EventManager.get().post(new WalletUtxoMixesChangedEvent(wallet, changedUtxoMixes, removedUtxoMixes));
|
||||
}
|
||||
}
|
||||
|
||||
private Wallet getWallet() {
|
||||
return AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.wallet.api.backend.beans.WalletResponse;
|
||||
import com.samourai.wallet.chain.ChainSupplier;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.NewBlockEvent;
|
||||
|
||||
public class SparrowChainSupplier implements ChainSupplier {
|
||||
private final int storedBlockHeight;
|
||||
private WalletResponse.InfoBlock latestBlock;
|
||||
|
||||
public SparrowChainSupplier(Integer storedBlockHeight) {
|
||||
this.storedBlockHeight = AppServices.getCurrentBlockHeight() == null ? (storedBlockHeight != null ? storedBlockHeight : 0) : AppServices.getCurrentBlockHeight();
|
||||
}
|
||||
|
||||
public void open() {
|
||||
this.latestBlock = computeLatestBlock();
|
||||
EventManager.get().register(this);
|
||||
}
|
||||
|
||||
public void close() {
|
||||
EventManager.get().unregister(this);
|
||||
}
|
||||
|
||||
private WalletResponse.InfoBlock computeLatestBlock() {
|
||||
WalletResponse.InfoBlock latestBlock = new WalletResponse.InfoBlock();
|
||||
latestBlock.height = AppServices.getCurrentBlockHeight() == null ? storedBlockHeight : AppServices.getCurrentBlockHeight();
|
||||
latestBlock.hash = AppServices.getLatestBlockHeader() == null ? Sha256Hash.ZERO_HASH.toString() :
|
||||
Utils.bytesToHex(Sha256Hash.twiceOf(AppServices.getLatestBlockHeader().bitcoinSerialize()).getReversedBytes());
|
||||
latestBlock.time = AppServices.getLatestBlockHeader() == null ? 1 : AppServices.getLatestBlockHeader().getTime();
|
||||
return latestBlock;
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletResponse.InfoBlock getLatestBlock() {
|
||||
return latestBlock;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newBlock(NewBlockEvent event) {
|
||||
this.latestBlock = computeLatestBlock();
|
||||
}
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.google.common.eventbus.Subscribe;
|
||||
import com.samourai.wallet.api.backend.IPushTx;
|
||||
import com.samourai.wallet.api.backend.ISweepBackend;
|
||||
import com.samourai.wallet.api.backend.seenBackend.ISeenBackend;
|
||||
import com.samourai.wallet.api.backend.seenBackend.SeenBackendWithFallback;
|
||||
import com.samourai.wallet.hd.HD_Wallet;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.samourai.wallet.httpClient.IHttpClient;
|
||||
import com.samourai.wallet.util.ExtLibJConfig;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
|
||||
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.coordinator.CoordinatorSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.AbstractDataSource;
|
||||
import com.samourai.whirlpool.client.wallet.data.dataSource.DataSourceConfig;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxo.UtxoSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier;
|
||||
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryChangedEvent;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.application.Platform;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public class SparrowDataSource extends AbstractDataSource {
|
||||
private static final Logger log = LoggerFactory.getLogger(SparrowDataSource.class);
|
||||
|
||||
private final ISeenBackend seenBackend;
|
||||
private final IPushTx pushTx;
|
||||
private final SparrowUtxoSupplier utxoSupplier;
|
||||
|
||||
public SparrowDataSource(
|
||||
WhirlpoolWallet whirlpoolWallet,
|
||||
HD_Wallet bip44w,
|
||||
WalletStateSupplier walletStateSupplier,
|
||||
UtxoConfigSupplier utxoConfigSupplier,
|
||||
DataSourceConfig dataSourceConfig)
|
||||
throws Exception {
|
||||
super(whirlpoolWallet, bip44w, walletStateSupplier, dataSourceConfig);
|
||||
this.seenBackend = computeSeenBackend(whirlpoolWallet.getConfig());
|
||||
this.pushTx = computePushTx();
|
||||
NetworkParameters params = whirlpoolWallet.getConfig().getSamouraiNetwork().getParams();
|
||||
this.utxoSupplier = new SparrowUtxoSupplier(walletSupplier, utxoConfigSupplier, dataSourceConfig, params);
|
||||
}
|
||||
|
||||
private ISeenBackend computeSeenBackend(WhirlpoolWalletConfig whirlpoolWalletConfig) {
|
||||
ExtLibJConfig extLibJConfig = whirlpoolWalletConfig.getSorobanConfig().getExtLibJConfig();
|
||||
IHttpClient httpClient = extLibJConfig.getHttpClientService().getHttpClient(HttpUsage.BACKEND);
|
||||
ISeenBackend sparrowSeenBackend = new SparrowSeenBackend(getWhirlpoolWallet().getWalletIdentifier(), httpClient);
|
||||
NetworkParameters params = whirlpoolWalletConfig.getSamouraiNetwork().getParams();
|
||||
return SeenBackendWithFallback.withOxt(sparrowSeenBackend, params);
|
||||
}
|
||||
|
||||
private IPushTx computePushTx() {
|
||||
return new IPushTx() {
|
||||
@Override
|
||||
public String pushTx(String hexTx) throws Exception {
|
||||
Transaction transaction = new Transaction(Utils.hexToBytes(hexTx));
|
||||
ElectrumServer electrumServer = new ElectrumServer();
|
||||
return electrumServer.broadcastTransactionPrivately(transaction).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String pushTx(String txHex, Collection<Integer> strictModeVouts) throws Exception {
|
||||
return pushTx(txHex);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void open(CoordinatorSupplier coordinatorSupplier) throws Exception {
|
||||
super.open(coordinatorSupplier);
|
||||
EventManager.get().register(this);
|
||||
((SparrowChainSupplier)getDataSourceConfig().getChainSupplier()).open();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void load(boolean initial) throws Exception {
|
||||
super.load(initial);
|
||||
utxoSupplier.refresh();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws Exception {
|
||||
EventManager.get().unregister(this);
|
||||
((SparrowChainSupplier)getDataSourceConfig().getChainSupplier()).close();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IPushTx getPushTx() {
|
||||
return pushTx;
|
||||
}
|
||||
|
||||
public static Wallet getWallet(String zpub) {
|
||||
return AppServices.get().getOpenWallets().keySet().stream()
|
||||
.filter(wallet -> {
|
||||
try {
|
||||
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
|
||||
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
|
||||
ExtendedKey extPubKey = wallet.getKeystores().get(0).getExtendedPublicKey();
|
||||
return extPubKey.toString(header).equals(zpub);
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
refreshWallet(event.getWalletId(), event.getWallet(), 0);
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletAddressesChanged(WalletAddressesChangedEvent event) {
|
||||
refreshWallet(event.getWalletId(), event.getWallet(), 0);
|
||||
}
|
||||
|
||||
private void refreshWallet(String walletId, Wallet wallet, int i) {
|
||||
try {
|
||||
// prefix matching <prefix>:master, :Premix, :Postmix
|
||||
String walletIdentifierPrefix = getWhirlpoolWallet().getWalletIdentifier().replace(":master", "");
|
||||
// match <prefix>:master, :Premix, :Postmix
|
||||
if(walletId.startsWith(walletIdentifierPrefix) && (wallet.isWhirlpoolMasterWallet() || wallet.isWhirlpoolChildWallet())) {
|
||||
//Workaround to avoid refreshing the wallet after it has been opened, but before it has been started
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(wallet);
|
||||
if(whirlpool != null && whirlpool.isStarting() && i < 1000) {
|
||||
Platform.runLater(() -> refreshWallet(walletId, wallet, i+1));
|
||||
} else {
|
||||
utxoSupplier.refresh();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error refreshing wallet", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISweepBackend getSweepBackend() {
|
||||
return null; // not necessary
|
||||
}
|
||||
|
||||
@Override
|
||||
public ISeenBackend getSeenBackend() {
|
||||
return seenBackend;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UtxoSupplier getUtxoSupplier() {
|
||||
return utxoSupplier;
|
||||
}
|
||||
}
|
||||
-109
@@ -1,109 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.whirlpool.dataSource;
|
||||
|
||||
import com.samourai.wallet.client.indexHandler.AbstractIndexHandler;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.WalletGapLimitChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletMixConfigChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WhirlpoolIndexHighFrequencyEvent;
|
||||
|
||||
public class SparrowIndexHandler extends AbstractIndexHandler {
|
||||
private final Wallet wallet;
|
||||
private final WalletNode walletNode;
|
||||
private final int defaultValue;
|
||||
|
||||
private static final long PERIOD = 1000 * 60 * 10L; //Period of 10 minutes
|
||||
private long periodStart;
|
||||
private int periodCount;
|
||||
|
||||
public SparrowIndexHandler(Wallet wallet, WalletNode walletNode) {
|
||||
this(wallet, walletNode, 0);
|
||||
}
|
||||
|
||||
public SparrowIndexHandler(Wallet wallet, WalletNode walletNode, int defaultValue) {
|
||||
this.wallet = wallet;
|
||||
this.walletNode = walletNode;
|
||||
this.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int get() {
|
||||
return Math.max(getCurrentIndex(), getStoredIndex());
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized int getAndIncrement() {
|
||||
int index = get();
|
||||
set(index + 1);
|
||||
return index;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void set(int value) {
|
||||
setStoredIndex(value);
|
||||
ensureSufficientGapLimit(value);
|
||||
}
|
||||
|
||||
private int getCurrentIndex() {
|
||||
Integer currentIndex = walletNode.getHighestUsedIndex();
|
||||
return currentIndex == null ? defaultValue : currentIndex + 1;
|
||||
}
|
||||
|
||||
private int getStoredIndex() {
|
||||
if(wallet.getMixConfig() != null) {
|
||||
if(walletNode.getKeyPurpose() == KeyPurpose.RECEIVE) {
|
||||
return wallet.getMixConfig().getReceiveIndex();
|
||||
} else if(walletNode.getKeyPurpose() == KeyPurpose.CHANGE) {
|
||||
return wallet.getMixConfig().getChangeIndex();
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
private void setStoredIndex(int index) {
|
||||
if(wallet.getMixConfig() != null) {
|
||||
if(walletNode.getKeyPurpose() == KeyPurpose.RECEIVE && wallet.getMixConfig().getReceiveIndex() != index) {
|
||||
wallet.getMixConfig().setReceiveIndex(index);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
|
||||
} else if(walletNode.getKeyPurpose() == KeyPurpose.CHANGE && wallet.getMixConfig().getChangeIndex() != index) {
|
||||
wallet.getMixConfig().setChangeIndex(index);
|
||||
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureSufficientGapLimit(int index) {
|
||||
int highestUsedIndex = getCurrentIndex() - 1;
|
||||
int existingGapLimit = wallet.getGapLimit();
|
||||
if(index > highestUsedIndex + existingGapLimit) {
|
||||
wallet.setGapLimit(Math.max(wallet.getGapLimit(), index - highestUsedIndex));
|
||||
EventManager.get().post(new WalletGapLimitChangedEvent(getWalletId(), wallet, existingGapLimit));
|
||||
checkFrequency();
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFrequency() {
|
||||
if(periodStart > 0 && System.currentTimeMillis() - periodStart < PERIOD) {
|
||||
periodCount++;
|
||||
} else {
|
||||
periodStart = System.currentTimeMillis();
|
||||
periodCount = 0;
|
||||
}
|
||||
|
||||
if(periodCount >= Wallet.DEFAULT_LOOKAHEAD) {
|
||||
EventManager.get().post(new WhirlpoolIndexHighFrequencyEvent(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
private String getWalletId() {
|
||||
try {
|
||||
return AppServices.get().getOpenWallets().get(wallet).getWalletId(wallet);
|
||||
} catch(Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user