Compare commits

...
58 Commits
Author SHA1 Message Date
Craig Raw db60afd13b v1.6.4 2022-05-19 13:32:33 +02:00
Craig Raw f176a2a04f add freeze utxo hyperlink to dust attack warning 2022-05-19 12:44:15 +02:00
Craig Raw 82be3a52dc show signature status on transaction tab for loaded transactions when offline 2022-05-19 11:23:40 +02:00
Craig Raw 4b2b8f653a ensure minimum relay fee rate is always equal or greater than 1 sat/vb 2022-05-19 08:50:34 +02:00
Craig Raw 555e5ecfb8 recalculate txid before copying 2022-05-18 08:44:34 +02:00
Craig Raw c0ca74ce6a add dust attack warning to utxos tab where small value txes are received on used addresses 2022-05-18 08:38:47 +02:00
Craig Raw 674498052f update gh build action and java versions 2022-05-17 12:30:22 +02:00
Craig Raw 1399b73dc2 upgrade to nightjar 0.2.33 for whirlpool startup fix 2022-05-17 11:20:17 +02:00
Craig Raw c51f3d9e66 improve validation and focus handling in integer spinners 2022-05-17 10:52:43 +02:00
Craig Raw 766a8c267f scan seed qr to bip39 and watch only keystores 2022-05-17 09:18:09 +02:00
Craig Raw 948d663fbf sign psbt from a transient scanned seed (seedqr, compactseedqr, ur:crypto-seed, ur:crypto-bip39 supported) 2022-05-17 08:04:57 +02:00
Craig Raw 66be5c43a6 fix whirlpool introduction text 2022-05-16 08:32:09 +02:00
Craig Raw e0b00513b9 make usb hw enumerate period configurable 2022-05-12 14:50:31 +02:00
Craig Raw 218761c594 disable privacy optimisation button for payjoins, improve rbf behaviour for wallet sweep txes 2022-05-12 10:57:31 +02:00
Craig Raw 5e4d6d5a78 only show mix selected button for p2wpkh wallets 2022-05-11 15:27:02 +02:00
Craig Raw b06df383dd enable max button when rbf replacement tx has only one output 2022-05-11 15:18:14 +02:00
Craig Raw 361e92c600 fix test build error 2022-05-11 13:23:44 +02:00
Craig Raw 69d0a2f96e configure aarch64 specific dependencies 2022-05-09 15:01:35 +02:00
Craig Raw bf078b2ea0 add native libraries for aarch64 2022-05-06 17:34:23 +02:00
Craig Raw b5fa8f0ee0 add delete wallet functionality, overwriting wallet file data first 2022-05-06 10:37:15 +02:00
Craig Raw 7fb230e56b upgrade to javafx 18 2022-05-05 10:54:48 +02:00
Craig Raw a53ecc4fdc Merge branch 'master' of github.com:sparrowwallet/sparrow 2022-05-05 10:19:58 +02:00
Craig Raw 3955eaaa3c upgrade to gradle 7.4.2 2022-05-05 10:19:31 +02:00
Craig Raw d1d090a12b followup 2022-05-04 15:10:15 +02:00
Craig Raw cd1509749a upgrade to hwi-2.1.0 with usb taproot signing and jade support 2022-05-04 14:33:48 +02:00
Craig Raw dd5278f442 update mix to button when wallet label is changed 2022-05-03 12:24:31 +02:00
Craig Raw 631f5d48df add note on installing wix to build windows installer 2022-05-03 11:43:22 +02:00
Craig Raw c981cf32b9 add restart in testnet/mainnet menu command 2022-05-03 09:20:30 +02:00
Craig Raw 984cabfc03 make connection toggle pulsing clearer by increasing opacity range 2022-04-29 15:54:37 +02:00
Craig Raw 8d28f8f0a9 reverse sort order of date column in utxos tab 2022-04-29 15:38:10 +02:00
Craig Raw 472fccc788 enable pasting a string into send to many spreadsheet using pre-editing cell context menu 2022-04-29 12:18:17 +02:00
Craig Raw b1e715b272 update wallet name in db on load if wallet filename is changed 2022-04-28 14:57:04 +02:00
Craig Raw 6931cf7a45 add select all button to utxos tab 2022-04-28 13:10:52 +02:00
Craig Raw 1ccfc3c042 disable lock all wallets menu item when all wallets are locked 2022-04-28 12:28:58 +02:00
Craig Raw dd1976f173 rename menu item where necessary to indicate wallet accounts are refreshed individually 2022-04-28 11:58:49 +02:00
Craig Raw eceaf40430 update confirmation status for opened unconfirmed wallet txes when mined 2022-04-28 11:22:24 +02:00
Craig Raw e565786bbc pass desired account number to keystore import description 2022-04-27 10:53:11 +02:00
Craig Raw 37a8a0a7f9 improve table sorting of utxos with the same hash 2022-04-26 16:21:13 +02:00
Craig Raw 6aa3bb2ff3 disable clear button when no utxos are selected 2022-04-26 16:03:49 +02:00
Craig Raw 516ee26ba0 remove ds store files 2022-04-26 14:58:23 +02:00
Craig Raw 1224abcd1d truncate keystore labels on caravan import 2022-04-26 14:34:45 +02:00
Craig Raw 81eda96690 v1.6.3 2022-03-31 15:11:00 +02:00
Craig Raw af9eb3cc64 add pdf export of wallet output descriptor from qr display dialog 2022-03-31 14:14:20 +02:00
Craig Raw d9bba16eb6 ensure order of unencrypted wallet tabs is retained across restarts 2022-03-31 11:42:46 +02:00
Craig Raw 58cd50f674 update bwt for bitcoin core v23 compatibility, ensure rescan when changing wallet birthday in transactions table 2022-03-31 10:38:38 +02:00
Craig Raw b2b9dbeb8d remove earn.com as fee rates source 2022-03-31 09:21:31 +02:00
Craig Raw 0469141fee use paynym.is onion address when proxy is set 2022-03-31 09:20:57 +02:00
Craig Raw b16c7345a8 allow collaborative sends for linked paynyms, support searching for custom paynyms when initiating collaborative sends 2022-03-30 18:26:41 +02:00
Craig Raw b1940e9293 fix issues updating utxo chart when mixing 2022-03-29 17:38:39 +02:00
Craig Raw 1f51f632c4 change show delay duration for help tooltips to 500ms 2022-03-29 12:10:47 +02:00
Craig Raw ba199ff11b avoid npe loading new wallets with watchlast set 2022-03-28 09:40:31 +02:00
Craig Raw 79c0f7769a indicate when a server failure occurs loading a transaction from file 2022-03-28 09:31:18 +02:00
Craig Raw 761ec0659f show error message if partial signatures do not match wallet on finalizing psbt 2022-03-27 11:00:25 +02:00
Craig Raw 5e31cdb7ac update utxo fiat balances as rates change 2022-03-26 14:15:00 +02:00
Craig Raw 468384d82a omit frozen utxos from soroban collaboration wallets 2022-03-26 11:19:30 +02:00
Craig Raw 230a4c5585 move subtabs to the left with clearer color hierarchy, always show once a multiple account wallet has been opened 2022-03-22 09:16:28 +02:00
Craig Raw 9048d341c6 disable bip47 support for taproot script type 2022-03-21 09:39:16 +02:00
Craig Raw b0f60bb671 warn if saved certificate file could not be deleted 2022-03-19 12:28:12 +02:00
105 changed files with 1232 additions and 229 deletions
+6 -6
View File
@@ -9,18 +9,18 @@ jobs:
matrix:
os: [windows-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v2
- uses: actions/checkout@v3
with:
submodules: true
- name: Set up JDK 16.0.1
uses: actions/setup-java@v2
- name: Set up JDK 17.0.2
uses: actions/setup-java@v3
with:
distribution: 'adopt'
java-version: '16.0.1'
distribution: 'temurin'
java-version: '17.0.2'
- name: Show Build Versions
run: ./gradlew -v
- name: Cache Gradle packages
uses: actions/cache@v2
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
+2
View File
@@ -28,6 +28,8 @@ The Sparrow binaries can be built from source using
`./gradlew jpackage`
Note that to build the Windows installer, you will need to install [WiX](https://github.com/wixtoolset/wix3/releases).
When updating to the latest HEAD
`git pull --recurse-submodules`
+23 -17
View File
@@ -1,16 +1,22 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.0.10'
id 'org.openjfx.javafxplugin' version '0.0.13'
id 'extra-java-module-info'
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.6.2'
def sparrowVersion = '1.6.4'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
osName = "osx"
}
def targetName = ""
def osArch = "x64"
if(System.getProperty("os.arch") == "aarch64") {
osArch = "aarch64"
targetName = "-" + osArch
}
group "com.sparrowwallet"
version "${sparrowVersion}"
@@ -29,7 +35,7 @@ tasks.withType(AbstractArchiveTask) {
}
javafx {
version = "17"
version = "18"
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
}
@@ -67,13 +73,13 @@ dependencies {
}
implementation('com.sparrowwallet:hummingbird:1.6.4')
implementation('co.nstant.in:cbor:0.9')
implementation('com.nativelibs4java:bridj:0.7-20140918-3') {
implementation("com.nativelibs4java:bridj${targetName}:0.7-20140918-3") {
exclude group: 'com.google.android.tools', module: 'dx'
}
implementation('com.github.sarxos:webcam-capture:0.3.13-SNAPSHOT') {
implementation("com.github.sarxos:webcam-capture${targetName}:0.3.13-SNAPSHOT") {
exclude group: 'com.nativelibs4java', module: 'bridj'
}
implementation("com.sparrowwallet:netlayer-jpms-${osName}:0.6.8") {
implementation("com.sparrowwallet:netlayer-jpms-${osName}${targetName}:0.6.8") {
exclude group: 'org.jetbrains.kotlin'
}
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.5.20')
@@ -92,11 +98,12 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.32')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.33')
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
implementation('org.apache.commons:commons-lang3:3.7')
implementation('net.sourceforge.streamsupport:streamsupport:1.7.0')
implementation('com.github.librepdf:openpdf:1.3.27')
testImplementation('junit:junit:4.12')
}
@@ -117,13 +124,13 @@ compileJava {
processResources {
doLast {
delete fileTree("$buildDir/resources/main/native").matching {
exclude "${osName}/**"
exclude "${osName}/${osArch}/**"
}
}
}
test {
jvmArgs '--add-opens=java.base/java.io=ALL-UNNAMED'
jvmArgs '--add-opens=java.base/java.io=com.google.gson'
}
run {
@@ -328,12 +335,12 @@ extraJavaModuleInfo {
requires('org.slf4j')
requires('com.fasterxml.jackson.databind')
}
module('bridj-0.7-20140918-3.jar', 'com.nativelibs4java.bridj', '0.7-20140918-3') {
module("bridj${targetName}-0.7-20140918-3.jar", 'com.nativelibs4java.bridj', '0.7-20140918-3') {
exports('org.bridj')
exports('org.bridj.cpp')
requires('java.logging')
}
module('webcam-capture-0.3.13-SNAPSHOT.jar', 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
module("webcam-capture${targetName}-0.3.13-SNAPSHOT.jar", 'com.github.sarxos.webcam.capture', '0.3.13-SNAPSHOT') {
exports('com.github.sarxos.webcam')
exports('com.github.sarxos.webcam.ds.buildin')
exports('com.github.sarxos.webcam.ds.buildin.natives')
@@ -452,6 +459,8 @@ extraJavaModuleInfo {
module('kotlin-stdlib-jdk7-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.jdk7', '1.5.20')
module('kotlin-stdlib-1.5.20.jar', 'kotlin.stdlib', '1.5.20') {
exports('kotlin')
exports('kotlin.jvm')
exports('kotlin.collections')
}
module('hummingbird-1.6.3.jar', 'com.sparrowwallet.hummingbird', '1.6.3') {
exports('com.sparrowwallet.hummingbird')
@@ -461,7 +470,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.32.jar', 'com.sparrowwallet.nightjar', '0.2.32') {
module('nightjar-0.2.33.jar', 'com.sparrowwallet.nightjar', '0.2.33') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
@@ -549,7 +558,7 @@ extraJavaModuleInfo {
module('jcip-annotations-1.0.jar', 'net.jcip.annotations', '1.0') {
exports('net.jcip.annotations')
}
module("netlayer-jpms-${osName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
module("netlayer-jpms-${osName}${targetName}-0.6.8.jar", 'netlayer.jpms', '0.6.8') {
exports('org.berndpruenster.netlayer.tor')
requires('com.github.ravn.jsocks')
requires('com.github.JesusMcCloud.jtorctl')
@@ -577,15 +586,12 @@ extraJavaModuleInfo {
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
requires('java.xml')
}
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
exports('kotlin.jvm')
exports('kotlin.collections')
}
module('jcommander-1.81.jar', 'com.beust.jcommander', '1.81') {
exports('com.beust.jcommander')
}
module('junit-4.12.jar', 'junit', '4.12') {
exports('org.junit')
requires('org.hamcrest.core')
}
module('hamcrest-core-1.3.jar', 'org.hamcrest.core', '1.3')
}
@@ -67,6 +67,8 @@ abstract public class ExtraModuleInfoTransform implements TransformAction<ExtraM
outputs.file(originalJar);
} else if (automaticModules.containsKey(originalJarName)) {
addAutomaticModuleName(originalJar, getModuleJar(outputs, originalJar), automaticModules.get(originalJarName));
} else if(originalJarName.startsWith("kotlin-stdlib-common")) {
//ignore
} else {
throw new RuntimeException("Not a module and no mapping defined: " + originalJarName);
}
+1 -1
Submodule drongo updated: fe57ad3e7a...d24243ce20
Binary file not shown.
+1 -1
View File
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+1 -1
View File
@@ -72,7 +72,7 @@ case "`uname`" in
Darwin* )
darwin=true
;;
MINGW* )
MSYS* | MINGW* )
msys=true
;;
NONSTOP* )
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.6.2</string>
<string>1.6.4</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow;
import com.beust.jcommander.JCommander;
import com.google.common.base.Charsets;
import com.google.common.eventbus.Subscribe;
import com.google.common.io.ByteSource;
@@ -90,6 +91,7 @@ public class AppController implements Initializable {
public static final String LOADING_TRANSACTIONS_MESSAGE = "Loading wallet, select Transactions tab to view...";
public static final String CONNECTION_FAILED_PREFIX = "Connection failed: ";
public static final String TRYING_ANOTHER_SERVER_MESSAGE = "trying another server...";
public static final String JPACKAGE_APP_PATH = "jpackage.app-path";
@FXML
private MenuItem saveTransaction;
@@ -109,6 +111,12 @@ public class AppController implements Initializable {
@FXML
private MenuItem exportWallet;
@FXML
private MenuItem deleteWallet;
@FXML
private MenuItem closeTab;
@FXML
private Menu fileMenu;
@@ -156,6 +164,9 @@ public class AppController implements Initializable {
@FXML
private MenuItem lockWallet;
@FXML
private MenuItem lockAllWallets;
@FXML
private MenuItem searchWallet;
@@ -178,6 +189,9 @@ public class AppController implements Initializable {
private CheckMenuItem preventSleep;
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
@FXML
private MenuItem restart;
@FXML
private StackPane rootStack;
@@ -196,6 +210,8 @@ public class AppController implements Initializable {
private Tab previouslySelectedTab;
private boolean subTabsVisible;
private final Set<Wallet> loadingWallets = new LinkedHashSet<>();
private final Set<Wallet> emptyLoadingWallets = new LinkedHashSet<>();
@@ -284,11 +300,13 @@ public class AppController implements Initializable {
EventManager.get().post(new TransactionTabsClosedEvent(closedTransactionTabs));
}
closeTab.setDisable(tabs.getTabs().isEmpty());
if(tabs.getTabs().isEmpty()) {
Stage tabStage = (Stage)tabs.getScene().getWindow();
tabStage.setTitle("Sparrow");
saveTransaction.setVisible(true);
saveTransaction.setDisable(true);
exportWallet.setDisable(true);
}
}
});
@@ -329,6 +347,8 @@ public class AppController implements Initializable {
showLoadingLog.selectedProperty().bindBidirectional(showLoadingLogProperty);
preventSleepProperty.set(Config.get().isPreventSleep());
preventSleep.selectedProperty().bindBidirectional(preventSleepProperty);
restart.setText("Restart in " + (Network.get() == Network.MAINNET ? Network.TESTNET.toDisplayString() : Network.MAINNET.toDisplayString()));
restart.setVisible(System.getProperty(JPACKAGE_APP_PATH) != null);
saveTransaction.setDisable(true);
showTransaction.visibleProperty().bind(Bindings.and(saveTransaction.visibleProperty(), saveTransaction.disableProperty().not()));
@@ -337,6 +357,8 @@ public class AppController implements Initializable {
savePSBTBinary.disableProperty().bind(saveTransaction.visibleProperty());
showPSBT.visibleProperty().bind(saveTransaction.visibleProperty().not());
exportWallet.setDisable(true);
deleteWallet.disableProperty().bind(exportWallet.disableProperty());
closeTab.setDisable(true);
lockWallet.setDisable(true);
searchWallet.disableProperty().bind(exportWallet.disableProperty());
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
@@ -780,6 +802,10 @@ public class AppController implements Initializable {
}
}
public void deleteWallet(ActionEvent event) {
deleteWallet(getSelectedWalletForm());
}
public void closeTab(ActionEvent event) {
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
}
@@ -834,6 +860,31 @@ public class AppController implements Initializable {
AppServices.get().setPreventSleep(item.isSelected());
}
public void restart(ActionEvent event) {
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
}
Args args = new Args();
ProcessHandle.current().info().arguments().ifPresent(argv -> {
JCommander jCommander = JCommander.newBuilder().addObject(args).acceptUnknownOptions(true).build();
jCommander.parse(argv);
});
args.network = (Network.get() == Network.MAINNET ? Network.TESTNET : Network.MAINNET);
try {
List<String> cmd = new ArrayList<>();
cmd.add(System.getProperty(JPACKAGE_APP_PATH));
cmd.addAll(args.toParams());
final ProcessBuilder builder = new ProcessBuilder(cmd);
builder.start();
quit(event);
} catch(Exception e) {
log.error("Error restarting application", e);
}
}
public void openFile(File file) {
if(isWalletFile(file)) {
openWalletFile(file, true);
@@ -891,6 +942,7 @@ public class AppController implements Initializable {
Storage storage = new Storage(file);
if(!storage.isEncrypted()) {
Storage.LoadWalletService loadWalletService = new Storage.LoadWalletService(storage);
loadWalletService.setExecutor(Storage.LoadWalletService.getSingleThreadedExecutor());
loadWalletService.setOnSucceeded(workerStateEvent -> {
WalletAndKey walletAndKey = loadWalletService.getValue();
openWallet(storage, walletAndKey, this, forceSameWindow);
@@ -1379,6 +1431,17 @@ public class AppController implements Initializable {
}
}
private boolean allWalletsLocked(Wallet lockingWallet) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData)tab.getUserData();
if(tabData instanceof WalletTabData walletTabData && walletTabData.getWallet() != lockingWallet && !walletTabData.getWalletForm().isLocked()) {
return false;
}
}
return true;
}
public void searchWallet(ActionEvent event) {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
if(selectedTab != null) {
@@ -1458,8 +1521,8 @@ public class AppController implements Initializable {
});
TabPane subTabs = new TabPane();
subTabs.setSide(Side.RIGHT);
setSubTabsVisible(subTabs, false);
subTabs.setSide(Side.LEFT);
setSubTabsVisible(subTabs, areSubTabsVisible());
subTabs.rotateGraphicProperty().set(true);
tab.setContent(subTabs);
@@ -1468,7 +1531,7 @@ public class AppController implements Initializable {
tab.setUserData(tabData);
tab.setContextMenu(getTabContextMenu(tab));
walletForm.lockedProperty().addListener((observable, oldValue, newValue) -> {
setSubTabsVisible(subTabs, !newValue && subTabs.getTabs().size() > 1);
setSubTabsVisible(subTabs, !newValue && areSubTabsVisible());
});
subTabs.getSelectionModel().selectedItemProperty().addListener((observable, old_val, selectedTab) -> {
@@ -1527,6 +1590,30 @@ public class AppController implements Initializable {
}
}
private void setSubTabsVisible(boolean visible) {
for(Tab tab : tabs.getTabs()) {
TabData tabData = (TabData) tab.getUserData();
if(tabData instanceof WalletTabData) {
setSubTabsVisible((TabPane)tab.getContent(), visible);
}
}
}
private boolean areSubTabsVisible() {
if(subTabsVisible) {
return true;
}
for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) {
if(wallet.getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) {
subTabsVisible = true;
return true;
}
}
return false;
}
public WalletForm addWalletSubTab(TabPane subTabs, Storage storage, Wallet wallet) {
try {
Tab subTab = new Tab();
@@ -1645,6 +1732,10 @@ public class AppController implements Initializable {
}
}
private void addTransactionTab(Transaction transaction, TransactionView initialView, Integer initialIndex) {
addTransactionTab(null, null, transaction, null, null, initialView, initialIndex);
}
private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex);
}
@@ -1778,10 +1869,65 @@ public class AppController implements Initializable {
tabs.getTabs().removeAll(tabs.getTabs());
});
contextMenu.getItems().addAll(close, closeOthers, closeAll);
MenuItem delete = new MenuItem("Delete...");
delete.setOnAction(event -> {
deleteWallet(getSelectedWalletForm());
});
contextMenu.getItems().addAll(close, closeOthers, closeAll, delete);
return contextMenu;
}
private void deleteWallet(WalletForm selectedWalletForm) {
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet?", "The wallet file and any backups will be deleted. Are you sure?", ButtonType.NO, ButtonType.YES);
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.YES) {
Storage storage = selectedWalletForm.getStorage();
if(selectedWalletForm.getMasterWallet().isEncrypted()) {
WalletPasswordDialog dlg = new WalletPasswordDialog(selectedWalletForm.getWallet().getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
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();
try {
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
deleteStorage(storage);
} finally {
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(() -> deleteWallet(getSelectedWalletForm()));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
tabs.getTabs().remove(tabs.getSelectionModel().getSelectedItem());
deleteStorage(storage);
}
}
}
private void deleteStorage(Storage storage) {
if(storage.isClosed()) {
Platform.runLater(storage::delete);
} else {
Platform.runLater(() -> deleteStorage(storage));
}
}
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");
@@ -1806,7 +1952,7 @@ public class AppController implements Initializable {
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
subTabs.getTabs().remove(subTab);
if(subTabs.getTabs().size() == 1) {
setSubTabsVisible(subTabs, false);
setSubTabsVisible(subTabs, areSubTabsVisible());
}
EventManager.get().post(new WalletDeletedEvent(wallet));
}
@@ -1845,7 +1991,7 @@ public class AppController implements Initializable {
private void serverToggleStartAnimation() {
Node thumbArea = serverToggle.lookup(".thumb-area");
if(thumbArea != null) {
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.4, 8);
Timeline timeline = AnimationUtil.getPulse(thumbArea, Duration.millis(600), 1.0, 0.25, 8);
timeline.play();
serverToggle.setUserData(new AnimationUtil.AnimatedNode(thumbArea, timeline));
}
@@ -1986,6 +2132,7 @@ public class AppController implements Initializable {
saveTransaction.setDisable(true);
lockWallet.setDisable(walletTabData.getWalletForm().lockedProperty().get());
exportWallet.setDisable(walletTabData.getWallet() == null || !walletTabData.getWallet().isValid() || walletTabData.getWalletForm().isLocked());
refreshWallet.setText(walletTabData.getWallet() == null || walletTabData.getWalletForm().getMasterWallet().getChildWallets().stream().allMatch(Wallet::isNested) ? "Refresh Wallet" : "Refresh Wallet Account");
showLoadingLog.setDisable(false);
showTxHex.setDisable(true);
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
@@ -2018,6 +2165,18 @@ public class AppController implements Initializable {
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
}
}
for(Tab walletTab : tabs.getTabs()) {
TabData tabData = (TabData) walletTab.getUserData();
if(tabData instanceof WalletTabData walletTabData) {
if(walletTabData.getWalletForm().getWalletId().equals(event.getWalletId()) && event.getWallet().isMasterWallet()) {
TabPane subTabs = (TabPane)walletTab.getContent();
Tab masterTab = subTabs.getTabs().stream().filter(tab -> ((WalletTabData)tab.getUserData()).getWallet().isMasterWallet()).findFirst().orElse(subTabs.getTabs().get(0));
Label masterLabel = (Label)masterTab.getGraphic();
masterLabel.setText(event.getWallet().getLabel() != null ? event.getWallet().getLabel() : event.getWallet().getAutomaticName());
}
}
}
}
@Subscribe
@@ -2252,6 +2411,14 @@ public class AppController implements Initializable {
serverToggleStopAnimation();
}
@Subscribe
public void walletOpened(WalletOpenedEvent walletOpenedEvent) {
if(!subTabsVisible && walletOpenedEvent.getWallet().getChildWallets().stream().anyMatch(childWallet -> !childWallet.isNested())) {
subTabsVisible = true;
setSubTabsVisible(true);
}
}
@Subscribe
public void walletTabsClosed(WalletTabsClosedEvent event) {
event.getClosedWalletTabData().stream().map(WalletTabData::getWallet).forEach(loadingWallets::remove);
@@ -2425,7 +2592,11 @@ public class AppController implements Initializable {
@Subscribe
public void viewTransaction(ViewTransactionEvent event) {
if(tabs.getScene().getWindow().equals(event.getWindow())) {
addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
if(event.getBlockTransaction() != null) {
addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex());
} else {
addTransactionTab(event.getTransaction(), event.getInitialView(), event.getInitialIndex());
}
}
}
@@ -2509,6 +2680,7 @@ public class AppController implements Initializable {
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(true);
exportWallet.setDisable(true);
lockAllWallets.setDisable(allWalletsLocked(event.getWallet()));
}
}
@@ -2518,6 +2690,7 @@ public class AppController implements Initializable {
if(selectedWalletForm != null && selectedWalletForm.getMasterWallet().equals(event.getWallet())) {
lockWallet.setDisable(false);
exportWallet.setDisable(!event.getWallet().isValid());
lockAllWallets.setDisable(false);
}
}
}
@@ -70,7 +70,7 @@ public class AppServices {
private static final int SERVER_PING_PERIOD_SECS = 60;
private static final int PUBLIC_SERVER_RETRY_PERIOD_SECS = 3;
private static final int ENUMERATE_HW_PERIOD_SECS = 30;
public static final int ENUMERATE_HW_PERIOD_SECS = 30;
private static final int RATES_PERIOD_SECS = 5 * 60;
private static final int VERSION_CHECK_PERIOD_HOURS = 24;
private static final ExchangeSource DEFAULT_EXCHANGE_SOURCE = ExchangeSource.COINGECKO;
@@ -292,9 +292,12 @@ public class AppServices {
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
crtFile.delete();
Platform.runLater(() -> restartService(connectionService));
return;
if(crtFile.delete()) {
Platform.runLater(() -> restartService(connectionService));
return;
} else {
AppServices.showErrorDialog("Could not delete certificate", "The certificate file at " + crtFile.getAbsolutePath() + " could not be deleted.\n\nPlease delete this file manually.");
}
}
}
}
@@ -355,7 +358,7 @@ public class AppServices {
private Hwi.ScheduledEnumerateService createDeviceEnumerateService() {
Hwi.ScheduledEnumerateService enumerateService = new Hwi.ScheduledEnumerateService(null);
enumerateService.setPeriod(Duration.seconds(ENUMERATE_HW_PERIOD_SECS));
enumerateService.setPeriod(Duration.seconds(Config.get().getEnumerateHwPeriod()));
enumerateService.setOnSucceeded(workerStateEvent -> {
List<Device> devices = enumerateService.getValue();
@@ -687,6 +690,10 @@ public class AppServices {
payjoinURIs.put(bitcoinURI.getAddress(), bitcoinURI);
}
public static void clearPayjoinURI(Address address) {
payjoinURIs.remove(address);
}
public static void clearTransactionHistoryCache(Wallet wallet) {
ElectrumServer.clearRetrievedScriptHashes(wallet);
@@ -920,7 +927,7 @@ public class AppServices {
System.setProperty(Network.BLOCK_HEIGHT_PROPERTY, Integer.toString(currentBlockHeight));
targetBlockFeeRates = event.getTargetBlockFeeRates();
addMempoolRateSizes(event.getMempoolRateSizes());
minimumRelayFeeRate = event.getMinimumRelayFeeRate();
minimumRelayFeeRate = Math.max(event.getMinimumRelayFeeRate(), Transaction.DEFAULT_MIN_RELAY_FEE);
latestBlockHeader = event.getBlockHeader();
Config.get().addRecentServer();
}
@@ -4,6 +4,9 @@ import com.beust.jcommander.Parameter;
import com.sparrowwallet.drongo.Network;
import org.slf4j.event.Level;
import java.util.ArrayList;
import java.util.List;
public class Args {
@Parameter(names = { "--dir", "-d" }, description = "Path to Sparrow home folder")
public String dir;
@@ -16,4 +19,23 @@ public class Args {
@Parameter(names = { "--help", "-h" }, description = "Show usage", help = true)
public boolean help;
public List<String> toParams() {
List<String> params = new ArrayList<>();
if(dir != null) {
params.add("-d");
params.add(dir);
}
if(network != null) {
params.add("-n");
params.add(network.toString());
}
if(level != null) {
params.add("-l");
params.add(level.toString());
}
return params;
}
}
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
public class MainApp extends Application {
public static final String APP_ID = "com.sparrowwallet.sparrow";
public static final String APP_NAME = "Sparrow";
public static final String APP_VERSION = "1.6.2";
public static final String APP_VERSION = "1.6.4";
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";
@@ -1,17 +1,23 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.Status;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletUtxoStatusChangedEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.NodeEntry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import javafx.geometry.Pos;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Tooltip;
import javafx.scene.control.TreeTableCell;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import java.util.Collections;
public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
public AddressCell() {
super();
@@ -37,10 +43,12 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
setContextMenu(new EntryCell.AddressContextMenu(address, utxoEntry.getOutputDescriptor(), new NodeEntry(utxoEntry.getWallet(), utxoEntry.getNode())));
Tooltip tooltip = new Tooltip();
tooltip.setShowDelay(Duration.millis(250));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate()));
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
setTooltip(tooltip);
if(addressStatus.isDuplicate()) {
if(addressStatus.isDustAttack()) {
setGraphic(getDustAttackHyperlink(utxoEntry));
} else if(addressStatus.isDuplicate()) {
setGraphic(getDuplicateGlyph());
} else {
setGraphic(null);
@@ -49,9 +57,9 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
}
}
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate, boolean dustAttack) {
return (utxoEntry.getNode().getWallet().isNested() ? utxoEntry.getNode().getWallet().getDisplayName() + " " : "" ) +
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : (dustAttack ? " (Possible dust attack)" : ""));
}
public static Glyph getDuplicateGlyph() {
@@ -60,4 +68,22 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
duplicateGlyph.setFontSize(12);
return duplicateGlyph;
}
public static Hyperlink getDustAttackHyperlink(UtxoEntry utxoEntry) {
Glyph dustAttackGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
dustAttackGlyph.getStyleClass().add("dust-attack-warning");
dustAttackGlyph.setFontSize(12);
Hyperlink hyperlink = new Hyperlink(utxoEntry.getHashIndex().getStatus() == Status.FROZEN ? "" : "Freeze?", dustAttackGlyph);
hyperlink.getStyleClass().add("freeze-dust-utxo");
hyperlink.setOnAction(event -> {
if(utxoEntry.getHashIndex().getStatus() != Status.FROZEN) {
hyperlink.setText("");
utxoEntry.getHashIndex().setStatus(Status.FROZEN);
EventManager.get().post(new WalletUtxoStatusChangedEvent(utxoEntry.getWallet(), Collections.singletonList(utxoEntry.getHashIndex())));
}
});
return hyperlink;
}
}
@@ -4,8 +4,8 @@ import com.sparrowwallet.drongo.BitcoinUnit;
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.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryClearedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
@@ -91,7 +91,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
EventManager.get().post(new WalletDataChangedEvent(wallet));
//Trigger full wallet rescan
wallet.clearHistory();
EventManager.get().post(new WalletHistoryClearedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
EventManager.get().post(new WalletAddressesChangedEvent(wallet, pastWallet, storage.getWalletId(wallet)));
}
});
if(wallet.getBirthDate() == null) {
@@ -0,0 +1,73 @@
package com.sparrowwallet.sparrow.control;
import com.lowagie.text.*;
import com.lowagie.text.Font;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.PdfWriter;
import com.sparrowwallet.hummingbird.UR;
import com.sparrowwallet.hummingbird.UREncoder;
import com.sparrowwallet.sparrow.AppServices;
import javafx.embed.swing.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.awt.*;
import java.io.File;
import java.io.FileOutputStream;
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
private static final Logger log = LoggerFactory.getLogger(DescriptorQRDisplayDialog.class);
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
super(ur);
DialogPane dialogPane = getDialogPane();
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
dialogPane.getButtonTypes().add(pdfButtonType);
Button pdfButton = (Button)dialogPane.lookupButton(pdfButtonType);
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
savePdf(walletName, outputDescriptor, ur);
event.consume();
});
}
private void savePdf(String walletName, String outputDescriptor, UR ur) {
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Save PDF");
fileChooser.setInitialFileName(walletName + ".pdf");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
if(file != null) {
try(Document document = new Document()) {
document.setMargins(36, 36, 48, 36);
PdfWriter.getInstance(document, new FileOutputStream(file));
document.open();
Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 16, Color.BLACK);
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
document.add(title);
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
String fragment = urEncoder.nextPart();
if(urEncoder.isSinglePart()) {
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
document.add(image);
}
Font descriptorFont = FontFactory.getFont(FontFactory.COURIER, 14, Color.BLACK);
Paragraph descriptor = new Paragraph(outputDescriptor, descriptorFont);
document.add(descriptor);
} catch(Exception e) {
log.error("Error creating descriptor PDF", e);
AppServices.showErrorDialog("Error creating PDF", e.getMessage());
}
}
}
}
@@ -206,14 +206,16 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
.map(e -> e.getBlockTransaction().getTransaction().getOutputs().get((int)e.getHashIndex().getIndex()))
.collect(Collectors.toList());
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
long changeTotal = ourOutputs.stream().mapToLong(TransactionOutput::getValue).sum() - consolidationOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
Transaction tx = blockTransaction.getTransaction();
double vSize = tx.getVirtualSize();
int inputSize = tx.getInputs().get(0).getLength() + (tx.getInputs().get(0).hasWitness() ? tx.getInputs().get(0).getWitness().getLength() / Transaction.WITNESS_SCALE_FACTOR : 0);
List<BlockTransactionHashIndex> walletUtxos = new ArrayList<>(transactionEntry.getWallet().getWalletUtxos().keySet());
//Remove any UTXOs created by the transaction that is to be replaced
walletUtxos.removeIf(utxo -> ourOutputs.stream().anyMatch(output -> output.getHash().equals(utxo.getHash()) && output.getIndex() == utxo.getIndex()));
Collections.shuffle(walletUtxos);
while((double)changeTotal / vSize < getMaxFeeRate() && !walletUtxos.isEmpty()) {
//If there is insufficent change output, include another random UTXO so the fee can be increased
//If there is insufficient change output, include another random UTXO so the fee can be increased
BlockTransactionHashIndex utxo = walletUtxos.remove(0);
utxos.add(utxo);
changeTotal += utxo.getValue();
@@ -223,6 +225,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
List<TransactionOutput> externalOutputs = new ArrayList<>(blockTransaction.getTransaction().getOutputs());
externalOutputs.removeAll(ourOutputs);
externalOutputs.addAll(consolidationOutputs);
final long rbfChange = changeTotal;
List<Payment> payments = externalOutputs.stream().map(txOutput -> {
try {
String label = transactionEntry.getLabel() == null ? "" : transactionEntry.getLabel();
@@ -240,7 +243,8 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
}
if(txOutput.getScript().getToAddress() != null) {
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), false);
//Disable change creation by enabling max payment when there is only one output and no additional UTXOs included
return new Payment(txOutput.getScript().getToAddress(), label, txOutput.getValue(), blockTransaction.getTransaction().getOutputs().size() == 1 && rbfChange == 0);
}
return null;
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
private final KeyDerivation requiredDerivation;
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(getAccount(wallet, requiredDerivation)), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
this.wallet = wallet;
this.importer = importer;
this.requiredDerivation = requiredDerivation;
@@ -34,4 +34,17 @@ public class FileKeystoreImportPane extends FileImportPane {
EventManager.get().post(new KeystoreImportEvent(keystore));
}
}
private static int getAccount(Wallet wallet, KeyDerivation requiredDerivation) {
if(wallet == null || requiredDerivation == null) {
return 0;
}
int account = wallet.getScriptType().getAccount(requiredDerivation.getDerivationPath());
if(account < 0) {
account = 0;
}
return account;
}
}
@@ -21,6 +21,7 @@ public class HelpLabel extends Label {
tooltip.textProperty().bind(helpTextProperty());
tooltip.graphicProperty().bind(helpGraphicProperty());
tooltip.setShowDuration(Duration.seconds(15));
tooltip.setShowDelay(Duration.millis(500));
getStyleClass().add("help-label");
Platform.runLater(() -> setTooltip(tooltip));
@@ -0,0 +1,83 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.NamedArg;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.util.converter.IntegerStringConverter;
public class IntegerSpinner extends Spinner<Integer> {
public IntegerSpinner() {
super();
setupEditor();
}
public IntegerSpinner(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue) {
super(min, max, initialValue);
setupEditor();
}
public IntegerSpinner(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue,
@NamedArg("amountToStepBy") int amountToStepBy) {
super(min, max, initialValue, amountToStepBy);
setupEditor();
}
private void setupEditor() {
getEditor().focusedProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null && !newValue) {
commitValue();
}
});
getEditor().textProperty().addListener((observable, oldValue, newValue) -> {
if(!newValue.matches("\\d*")) {
getEditor().setText(newValue.replaceAll("[^\\d]", ""));
}
});
}
public static class ValueFactory extends SpinnerValueFactory.IntegerSpinnerValueFactory {
public ValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max) {
super(min, max);
setupConverter(min);
}
public ValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue) {
super(min, max, initialValue);
setupConverter(initialValue);
}
public ValueFactory(@NamedArg("min") int min,
@NamedArg("max") int max,
@NamedArg("initialValue") int initialValue,
@NamedArg("amountToStepBy") int amountToStepBy) {
super(min, max, initialValue, amountToStepBy);
setupConverter(initialValue);
}
private void setupConverter(Integer defaultValue) {
setConverter(new IntegerStringConverter() {
@Override
public Integer fromString(String value) {
if(value == null) {
return null;
}
value = value.trim();
if(value.length() < 1) {
return defaultValue;
}
return Integer.valueOf(value);
}
});
}
}
}
@@ -24,18 +24,6 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
showWordList(keystore.getSeed());
}
private void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size()));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
}
}
protected Node getMnemonicWordsEntry(int numWords) {
VBox vBox = new VBox();
vBox.setSpacing(10);
@@ -1,6 +1,8 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.property.IntegerProperty;
@@ -26,13 +28,16 @@ import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.*;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class MnemonicKeystorePane extends TitledDescriptionPane {
private static final Logger log = LoggerFactory.getLogger(MnemonicKeystorePane.class);
protected SplitMenuButton enterMnemonicButton;
protected TilePane wordsPane;
protected Label validLabel;
@@ -73,9 +78,44 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
});
enterMnemonicButton.getItems().add(item);
}
enterMnemonicButton.getItems().add(new SeparatorMenuItem());
MenuItem scanItem = new MenuItem("Scan QR...");
scanItem.setOnAction(event -> {
scanQR();
});
enterMnemonicButton.getItems().add(scanItem);
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
}
protected void scanQR() {
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.seed != null) {
showWordList(result.seed);
Platform.runLater(() -> validLabel.requestFocus());
} else if(result.exception != null) {
log.error("Error scanning QR", result.exception);
showErrorDialog("Error scanning QR", result.exception.getMessage());
} else {
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a seed.");
}
}
}
protected void showWordList(DeterministicSeed seed) {
List<String> words = seed.getMnemonicCode();
setContent(getMnemonicWordsEntry(words.size()));
setExpanded(true);
for(int i = 0; i < wordsPane.getChildren().size(); i++) {
WordEntry wordEntry = (WordEntry)wordsPane.getChildren().get(i);
wordEntry.getEditor().setText(words.get(i));
wordEntry.getEditor().setEditable(false);
}
}
protected void enterMnemonic(int numWords) {
setDescription("Generate new or enter existing");
showHideLink.setVisible(false);
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.bip47.PaymentCode;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.paynym.PayNymService;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.concurrent.Service;
@@ -117,8 +118,8 @@ public class PayNymAvatar extends StackPane {
}
synchronized(lock) {
String url = "https://paynym.is/" + paymentCodeStr + "/avatar";
Proxy proxy = AppServices.getProxy();
String url = PayNymService.getHostUrl(proxy != null) + "/" + paymentCodeStr + "/avatar";
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
Image image = new Image(is, 150, 150, true, false);
@@ -137,7 +137,7 @@ public class QRDisplayDialog extends Dialog<UR> {
}
}
private Image getQrCode(String fragment) {
protected Image getQrCode(String fragment) {
try {
QRCodeWriter qrCodeWriter = new QRCodeWriter();
BitMatrix qrMatrix = qrCodeWriter.encode(fragment, BarcodeFormat.QR_CODE, QR_WIDTH, QR_HEIGHT);
@@ -10,15 +10,14 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
import com.sparrowwallet.drongo.address.P2SHAddress;
import com.sparrowwallet.drongo.address.P2WPKHAddress;
import com.sparrowwallet.drongo.crypto.*;
import com.sparrowwallet.drongo.policy.Policy;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.protocol.Base43;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
import com.sparrowwallet.drongo.wallet.SeedQR;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.hummingbird.LegacyURDecoder;
import com.sparrowwallet.hummingbird.registry.*;
@@ -257,6 +256,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
BitcoinURI bitcoinURI;
Address address;
ExtendedKey extendedKey;
DeterministicSeed seed;
try {
extendedKey = ExtendedKey.fromDescriptor(qrtext);
result = new Result(extendedKey);
@@ -334,6 +334,22 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
//Ignore, not parseable as base43 decoded bytes
}
try {
seed = SeedQR.getSeed(qrtext);
result = new Result(seed);
return;
} catch(Exception e) {
//Ignore, not parseable as a SeedQR
}
try {
seed = SeedQR.getSeed(qrResult.getRawBytes());
result = new Result(seed);
return;
} catch(Exception e) {
//Ignore, not parseable as a CompactSeedQR
}
result = new Result(qrtext);
}
}
@@ -401,6 +417,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
CryptoAccount cryptoAccount = (CryptoAccount)ur.decodeFromRegistry();
List<Wallet> wallets = getWallets(cryptoAccount);
return new Result(wallets);
} else if(urRegistryType.equals(RegistryType.CRYPTO_SEED)) {
CryptoSeed cryptoSeed = (CryptoSeed)ur.decodeFromRegistry();
DeterministicSeed seed = getSeed(cryptoSeed);
return new Result(seed);
} else if(urRegistryType.equals(RegistryType.CRYPTO_BIP39)) {
CryptoBip39 cryptoBip39 = (CryptoBip39)ur.decodeFromRegistry();
DeterministicSeed seed = getSeed(cryptoBip39);
return new Result(seed);
} else {
log.error("Unsupported UR type " + urRegistryType);
return new Result(new URException("UR type " + urRegistryType + " is not supported"));
@@ -523,6 +547,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
return null;
}
private DeterministicSeed getSeed(CryptoSeed cryptoSeed) {
return new DeterministicSeed(cryptoSeed.getSeed(), null, cryptoSeed.getBirthdate() == null ? System.currentTimeMillis() : cryptoSeed.getBirthdate().getTime());
}
private DeterministicSeed getSeed(CryptoBip39 cryptoBip39) {
return new DeterministicSeed(cryptoBip39.getWords(), null, System.currentTimeMillis(), DeterministicSeed.Type.BIP39);
}
}
private class QRScanListener implements WebcamListener {
@@ -626,6 +658,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
public final ExtendedKey extendedKey;
public final OutputDescriptor outputDescriptor;
public final List<Wallet> wallets;
public final DeterministicSeed seed;
public final String payload;
public final Throwable exception;
@@ -636,6 +669,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = null;
}
@@ -647,6 +681,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = null;
}
@@ -658,6 +693,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = null;
}
@@ -669,6 +705,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = null;
}
@@ -680,6 +717,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = extendedKey;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = null;
}
@@ -691,6 +729,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = outputDescriptor;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = null;
}
@@ -702,6 +741,19 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = wallets;
this.seed = null;
this.payload = null;
this.exception = null;
}
public Result(DeterministicSeed seed) {
this.transaction = null;
this.psbt = null;
this.uri = null;
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = seed;
this.payload = null;
this.exception = null;
}
@@ -713,6 +765,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = payload;
this.exception = null;
}
@@ -724,6 +777,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
this.extendedKey = null;
this.outputDescriptor = null;
this.wallets = null;
this.seed = null;
this.payload = null;
this.exception = exception;
}
@@ -14,6 +14,7 @@ import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.input.Clipboard;
import javafx.scene.layout.StackPane;
import javafx.stage.FileChooser;
import javafx.util.StringConverter;
@@ -47,7 +48,19 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
List<Payment> initialPayments = IntStream.range(0, 100).mapToObj(i -> new Payment(null, null, -1, false)).collect(Collectors.toList());
Grid grid = getGrid(initialPayments);
spreadsheetView = new SpreadsheetView(grid);
spreadsheetView = new SpreadsheetView(grid) {
@Override
public void pasteClipboard() {
final Clipboard clipboard = Clipboard.getSystemClipboard();
if(clipboard.hasString()) {
final TablePosition<?,?> tp = getSelectionModel().getFocusedCell();
SpreadsheetCell cell = getGrid().getRows().get(tp.getRow()).get(tp.getColumn());
getGrid().setCellValue(cell.getRow(), cell.getColumn(), cell.getCellType().convertValue(clipboard.getString()));
} else {
super.pasteClipboard();
}
}
};
spreadsheetView.getColumns().get(0).setPrefWidth(400);
spreadsheetView.getColumns().get(1).setPrefWidth(150);
spreadsheetView.getColumns().get(2).setPrefWidth(247);
@@ -57,6 +57,7 @@ public class UtxosChart extends BarChart<String, Number> {
for(int i = 0; i < utxoDataList.size(); i++) {
XYChart.Data<String, Number> newData = utxoDataList.get(i);
newData.setXValue((i+1) + ". " + newData.getXValue());
if(i < utxoSeries.getData().size()) {
XYChart.Data<String, Number> existingData = utxoSeries.getData().get(i);
if(!newData.getXValue().equals(existingData.getXValue()) || !newData.getYValue().equals(existingData.getYValue()) || (newData.getExtraValue() instanceof Entry && !newData.getExtraValue().equals(existingData.getExtraValue()))) {
@@ -70,7 +71,7 @@ public class UtxosChart extends BarChart<String, Number> {
}
if(utxoSeries.getData().size() > utxoDataList.size()) {
utxoSeries.getData().remove(Math.max(0, utxoDataList.size() - 1), utxoSeries.getData().size());
utxoSeries.getData().remove(utxoDataList.size(), utxoSeries.getData().size());
}
if(selectedEntries != null) {
@@ -24,6 +24,7 @@ public class UtxosTreeTable extends CoinTreeTable {
});
dateCol.setCellFactory(p -> new DateCell());
dateCol.setSortable(true);
dateCol.setComparator((o1, o2) -> (int) (o2.getValue() - o1.getValue()));
getColumns().add(dateCol);
TreeTableColumn<Entry, Entry> outputCol = new TreeTableColumn<>("Output");
@@ -35,7 +36,12 @@ public class UtxosTreeTable extends CoinTreeTable {
outputCol.setComparator((o1, o2) -> {
UtxoEntry entry1 = (UtxoEntry)o1;
UtxoEntry entry2 = (UtxoEntry)o2;
return entry1.getDescription().compareTo(entry2.getDescription());
int hashCompare = entry1.getHashIndex().getHash().toString().compareTo(entry2.getHashIndex().getHash().toString());
if(hashCompare != 0) {
return hashCompare;
}
return (int)(entry1.getHashIndex().getIndex() - entry2.getHashIndex().getIndex());
});
getColumns().add(outputCol);
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
public class TransactionFetchFailedEvent extends TransactionReferencesFailedEvent {
public TransactionFetchFailedEvent(Transaction transaction, Throwable exception) {
super(transaction, exception);
}
public TransactionFetchFailedEvent(Transaction transaction, Throwable exception, int pageStart, int pageEnd) {
super(transaction, exception, pageStart, pageEnd);
}
}
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.sparrow.transaction.TransactionView;
import com.sparrowwallet.sparrow.wallet.HashIndexEntry;
@@ -7,10 +8,19 @@ import javafx.stage.Window;
public class ViewTransactionEvent {
private final Window window;
private final Transaction transaction;
private final BlockTransaction blockTransaction;
private final TransactionView initialView;
private final Integer initialIndex;
public ViewTransactionEvent(Window window, Transaction transaction) {
this.window = window;
this.transaction = transaction;
this.blockTransaction = null;
this.initialView = TransactionView.HEADERS;
this.initialIndex = null;
}
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction) {
this(window, blockTransaction, TransactionView.HEADERS, null);
}
@@ -21,6 +31,7 @@ public class ViewTransactionEvent {
public ViewTransactionEvent(Window window, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) {
this.window = window;
this.transaction = blockTransaction.getTransaction();
this.blockTransaction = blockTransaction;
this.initialView = initialView;
this.initialIndex = initialIndex;
@@ -30,6 +41,10 @@ public class ViewTransactionEvent {
return window;
}
public Transaction getTransaction() {
return transaction;
}
public BlockTransaction getBlockTransaction() {
return blockTransaction;
}
@@ -53,7 +53,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
Keystore keystore = new Keystore(extKey.name);
Keystore keystore = new Keystore(extKey.name.length() > Keystore.MAX_LABEL_LENGTH ? extKey.name.substring(0, Keystore.MAX_LABEL_LENGTH) : extKey.name);
try {
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
} catch(NumberFormatException e) {
@@ -70,6 +70,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
keystore.setWalletModel(walletModel);
keystore.setSource(KeystoreSource.HW_USB);
}
wallet.makeLabelsUnique(keystore);
wallet.getKeystores().add(keystore);
}
@@ -28,7 +28,7 @@ public class CoboVaultMultisig extends ColdcardMultisig {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Cobo Vault.";
}
@@ -24,7 +24,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created by using the My Cobo Vault > ... > Export Wallet feature on your Cobo Vault.";
}
@@ -84,8 +84,8 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
}
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > 0 feature on your Coldcard.";
public String getKeystoreImportDescription(int account) {
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > " + account + " feature on your Coldcard.";
}
@Override
@@ -28,8 +28,8 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public String getKeystoreImportDescription() {
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > 0 feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
public String getKeystoreImportDescription(int account) {
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > " + account + " feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
}
@Override
@@ -15,8 +15,10 @@ import java.lang.reflect.Type;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS;
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS;
public class Config {
private static final Logger log = LoggerFactory.getLogger(Config.class);
@@ -44,7 +46,9 @@ public class Config {
private boolean preventSleep = false;
private List<File> recentWalletFiles;
private Integer keyDerivationPeriod;
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
private File hwi;
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
private Boolean hdCapture;
private String webcamDevice;
private ServerType serverType;
@@ -300,6 +304,10 @@ public class Config {
flush();
}
public long getDustAttackThreshold() {
return dustAttackThreshold;
}
public File getHwi() {
return hwi;
}
@@ -309,6 +317,10 @@ public class Config {
flush();
}
public int getEnumerateHwPeriod() {
return enumerateHwPeriod;
}
public Boolean getHdCapture() {
return hdCapture;
}
@@ -35,7 +35,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import a single keystore from an Electrum wallet (use File > Import > Electrum to import a multisig wallet).";
}
@@ -35,7 +35,7 @@ public class GordianSeedTool implements KeystoreFileImport {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.get().toDisplayString() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
}
@@ -33,7 +33,7 @@ public class Hwi {
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
private static final String HWI_HOME_DIR = "hwi";
private static final String HWI_VERSION_PREFIX = "hwi-";
private static final String HWI_VERSION = "2.0.2";
private static final String HWI_VERSION = "2.1.0";
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
private static boolean isPromptActive = false;
@@ -294,7 +294,14 @@ public class Hwi {
//The check will still happen on first invocation, but will not thereafter
//See https://github.com/bitcoin-core/HWI/issues/327 for details
if(platform == Platform.OSX) {
InputStream inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
String osArch = System.getProperty("os.arch");
InputStream inputStream;
if(osArch.equals("aarch64")) {
inputStream = Hwi.class.getResourceAsStream("/native/osx/aarch64/" + HWI_VERSION_DIR + "-mac-aarch64-signed.zip");
} else {
inputStream = Hwi.class.getResourceAsStream("/native/osx/x64/" + HWI_VERSION_DIR + "-mac-amd64-signed.zip");
}
if(inputStream == null) {
throw new IllegalStateException("Cannot load " + HWI_VERSION_DIR + " from classpath");
}
@@ -336,8 +343,9 @@ public class Hwi {
InputStream inputStream;
Path tempExecPath;
if(platform == Platform.WINDOWS) {
Files.createDirectories(getHwiHomeDir().toPath());
inputStream = Hwi.class.getResourceAsStream("/native/windows/x64/hwi.exe");
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null);
tempExecPath = Files.createTempFile(getHwiHomeDir().toPath(), HWI_VERSION_DIR, null);
} else {
inputStream = Hwi.class.getResourceAsStream("/native/linux/x64/hwi");
tempExecPath = Files.createTempFile(HWI_VERSION_DIR, null, PosixFilePermissions.asFileAttribute(ownerExecutableWritable));
@@ -368,7 +376,7 @@ public class Hwi {
}
private File getHwiHomeDir() {
if(Platform.getCurrent() == Platform.OSX) {
if(Platform.getCurrent() == Platform.OSX || Platform.getCurrent() == Platform.WINDOWS) {
return new File(Storage.getSparrowDir(), HWI_HOME_DIR);
}
@@ -1,5 +1,8 @@
package com.sparrowwallet.sparrow.io;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.URI;
import java.net.URISyntaxException;
@@ -9,11 +12,14 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
public class IOUtils {
private static final Logger log = LoggerFactory.getLogger(IOUtils.class);
public static FileType getFileType(File file) {
try {
String type = Files.probeContentType(file.toPath());
@@ -120,4 +126,28 @@ public class IOUtils {
return true;
}
public static boolean secureDelete(File file) {
if(file.exists()) {
long length = file.length();
SecureRandom random = new SecureRandom();
try(RandomAccessFile raf = new RandomAccessFile(file, "rws")) {
raf.seek(0);
raf.getFilePointer();
byte[] data = new byte[64];
int pos = 0;
while(pos < length) {
random.nextBytes(data);
raf.write(data);
pos += data.length;
}
} catch(IOException e) {
log.warn("Error overwriting file for deletion " + file.getName(), e);
}
return file.delete();
}
return false;
}
}
@@ -305,6 +305,11 @@ public class JsonPersistence implements Persistence {
com.google.common.io.Files.copy(walletFile, outputStream);
}
@Override
public boolean isClosed() {
return true;
}
@Override
public void close() {
//Nothing required
@@ -28,7 +28,7 @@ public class KeystoneMultisig extends ColdcardMultisig {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created by using the Multisig Wallet > ... > Show/Export XPUB feature on your Keystone.";
}
@@ -27,7 +27,7 @@ public class KeystoneSinglesig implements KeystoreFileImport, WalletImport {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created by using the My Keystone > ... > Export Wallet feature on your Keystone. Make sure to set the Watch-only Wallet to Sparrow in the Settings first.";
}
@@ -7,6 +7,10 @@ import java.io.InputStream;
public interface KeystoreFileImport extends KeystoreImport, FileImport {
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
String getKeystoreImportDescription(int account);
default String getKeystoreImportDescription() {
return getKeystoreImportDescription(0);
}
boolean isKeystoreImportScannable();
default boolean isFileFormatAvailable() {
return true;
@@ -1,8 +1,5 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.WalletModel;
public interface KeystoreImport extends Import {
String getKeystoreImportDescription();
}
@@ -27,7 +27,7 @@ public class PassportMultisig extends ColdcardMultisig {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created from New Account > Sparrow > Multisig > QR Code/microSD on your Passport. For existing accounts, use Manage Account > Export by QR/microSD.";
}
@@ -13,7 +13,7 @@ public class PassportSinglesig extends ColdcardSinglesig {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created from New Account > Sparrow > Standard > QR Code/microSD on your Passport. For existing accounts, use Manage Account > Export by QR/microSD.";
}
@@ -25,5 +25,6 @@ public interface Persistence {
String getWalletId(Storage storage, Wallet wallet);
String getWalletName(File walletFile, Wallet wallet);
void copyWallet(File walletFile, OutputStream outputStream) throws IOException;
boolean isClosed();
void close();
}
@@ -9,7 +9,7 @@ public class SeedSigner extends SpecterDIY {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
}
@@ -45,7 +45,7 @@ public class SpecterDIY implements KeystoreFileImport, WalletExport {
}
@Override
public String getKeystoreImportDescription() {
public String getKeystoreImportDescription(int account) {
return "Import file or QR created by using the Master Public Keys feature on your Specter DIY device. Note the default is P2WPKH for Single Signature, and P2WSH for Multi Signature.";
}
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.MainApp;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import org.controlsfx.tools.Platform;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -22,6 +23,8 @@ import java.security.cert.CertificateEncodingException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -123,6 +126,10 @@ public class Storage {
return persistence.isPersisted(this, wallet);
}
public boolean isClosed() {
return persistence.isClosed();
}
public void close() {
ClosePersistenceService closePersistenceService = new ClosePersistenceService();
closePersistenceService.start();
@@ -160,6 +167,11 @@ public class Storage {
persistence.copyWallet(walletFile, outputStream);
}
public void delete() {
deleteBackups();
IOUtils.secureDelete(walletFile);
}
public void deleteBackups() {
deleteBackups(null);
}
@@ -178,7 +190,7 @@ public class Storage {
private void deleteBackups(String prefix) {
File[] backups = getBackups(prefix);
for(File backup : backups) {
backup.delete();
IOUtils.secureDelete(backup);
}
}
@@ -509,6 +521,8 @@ public class Storage {
private final Storage storage;
private final SecureString password;
private static Executor singleThreadedExecutor;
public LoadWalletService(Storage storage) {
this.storage = storage;
this.password = null;
@@ -536,6 +550,15 @@ public class Storage {
}
};
}
public static Executor getSingleThreadedExecutor() {
if(singleThreadedExecutor == null) {
BasicThreadFactory factory = new BasicThreadFactory.Builder().namingPattern("LoadWalletService-single").daemon(true).priority(Thread.MIN_PRIORITY).build();
singleThreadedExecutor = Executors.newSingleThreadScheduledExecutor(factory);
}
return singleThreadedExecutor;
}
}
public static class KeyDerivationService extends Service<ECKey> {
@@ -84,7 +84,7 @@ public class DbPersistence implements Persistence {
Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey));
masterWallet = jdbi.withHandle(handle -> {
WalletDao walletDao = handle.attach(WalletDao.class);
return walletDao.getMainWallet(MASTER_SCHEMA);
return walletDao.getMainWallet(MASTER_SCHEMA, getWalletName(storage.getWalletFile(), null));
});
Map<WalletAndKey, Storage> childWallets = loadChildWallets(storage, masterWallet, encryptionKey);
@@ -109,7 +109,7 @@ public class DbPersistence implements Persistence {
Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey));
Wallet wallet = childJdbi.withHandle(handle -> {
WalletDao walletDao = handle.attach(WalletDao.class);
Wallet childWallet = walletDao.getMainWallet(schema);
Wallet childWallet = walletDao.getMainWallet(schema, null);
childWallet.setName(schema.substring(WALLET_SCHEMA_PREFIX.length()));
childWallet.setMasterWallet(masterWallet);
return childWallet;
@@ -568,6 +568,11 @@ public class DbPersistence implements Persistence {
com.google.common.io.Files.copy(walletFile, outputStream);
}
@Override
public boolean isClosed() {
return dataSource.isClosed();
}
@Override
public void close() {
EventManager.get().unregister(this);
@@ -55,6 +55,9 @@ public interface WalletDao {
@GetGeneratedKeys("id")
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Integer watchLast, Date birthDate, long defaultPolicy);
@SqlUpdate("update wallet set name = :name where id = :id")
void updateName(@Bind("id") long id, @Bind("name") String name);
@SqlUpdate("update wallet set label = :label where id = :id")
void updateLabel(@Bind("id") long id, @Bind("label") String label);
@@ -70,12 +73,17 @@ public interface WalletDao {
@SqlUpdate("set schema ?")
int setSchema(String schema);
default Wallet getMainWallet(String schema) {
default Wallet getMainWallet(String schema, String walletName) {
try {
setSchema(schema);
Wallet mainWallet = loadMainWallet();
if(mainWallet != null) {
loadWallet(mainWallet);
if(walletName != null && !walletName.equals(mainWallet.getName())) {
mainWallet.setName(walletName);
updateName(mainWallet.getId(), walletName);
}
}
return mainWallet;
@@ -41,7 +41,10 @@ public class Bwt {
if(!initialized) {
try {
org.controlsfx.tools.Platform platform = org.controlsfx.tools.Platform.getCurrent();
if(platform == org.controlsfx.tools.Platform.OSX) {
String osArch = System.getProperty("os.arch");
if(platform == org.controlsfx.tools.Platform.OSX && osArch.equals("aarch64")) {
NativeUtils.loadLibraryFromJar("/native/osx/aarch64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.OSX) {
NativeUtils.loadLibraryFromJar("/native/osx/x64/libbwt_jni.dylib");
} else if(platform == org.controlsfx.tools.Platform.WINDOWS) {
NativeUtils.loadLibraryFromJar("/native/windows/x64/bwt_jni.dll");
@@ -348,7 +348,7 @@ public class ElectrumServer {
private Set<WalletNode> getAddressNodes(Wallet wallet, WalletNode purposeNode) {
Integer watchLast = wallet.getWatchLast();
if(watchLast == null || watchLast < wallet.getGapLimit() || wallet.getStoredBlockHeight() == 0 || wallet.getTransactions().isEmpty()) {
if(watchLast == null || watchLast < wallet.getGapLimit() || wallet.getStoredBlockHeight() == null || wallet.getStoredBlockHeight() == 0 || wallet.getTransactions().isEmpty()) {
return purposeNode.getChildren();
}
@@ -20,6 +20,8 @@ public class PayNym {
private final List<PayNym> following;
private final List<PayNym> followers;
private boolean collaborativeSend;
public PayNym(PaymentCode paymentCode, String nymId, String nymName, boolean segwit, List<PayNym> following, List<PayNym> followers) {
this.paymentCode = paymentCode;
this.nymId = nymId;
@@ -53,6 +55,14 @@ 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();
}
@@ -453,7 +453,7 @@ public class PayNymController {
ButtonType previewType = new ButtonType("Preview", ButtonBar.ButtonData.LEFT);
ButtonType sendType = new ButtonType("Send", ButtonBar.ButtonData.YES);
Optional<ButtonType> optButtonType = AppServices.showAlertDialog("Link PayNym?",
"Linking to this contact will allow you to send to it non-collaboratively through unique private addresses you can generate independently.\n\n" +
"Linking to this contact will allow you to send to it directly (non-collaboratively) through unique private addresses you can generate independently.\n\n" +
"It will cost " + MINIMUM_P2PKH_OUTPUT_SATS + " sats to create the link through a notification transaction, plus the mining fee. Send transaction?", Alert.AlertType.CONFIRMATION, previewType, ButtonType.CANCEL, sendType);
if(optButtonType.isPresent() && optButtonType.get() == sendType) {
broadcastNotificationTransaction(payNym);
@@ -9,10 +9,10 @@ import java.io.IOException;
public class PayNymDialog extends Dialog<PayNym> {
public PayNymDialog(String walletId) {
this(walletId, false, false);
this(walletId, Operation.SHOW, false);
}
public PayNymDialog(String walletId, boolean selectPayNym, boolean selectLinkedOnly) {
public PayNymDialog(String walletId, Operation operation, boolean selectLinkedOnly) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
@@ -32,11 +32,34 @@ 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 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(selectPayNym) {
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));
});
}
Button sendDirectlyButton = (Button)dialogPane.lookupButton(sendDirectlyButtonType);
sendDirectlyButton.setDisable(true);
sendDirectlyButton.setDefaultButton(true);
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
sendDirectlyButton.setDisable(payNym == null || !payNymController.isLinked(payNym));
sendDirectlyButton.setDefaultButton(!sendDirectlyButton.isDisable());
});
} else if(operation == Operation.SELECT) {
dialogPane.getButtonTypes().addAll(selectButtonType, cancelButtonType);
Button selectButton = (Button)dialogPane.lookupButton(selectButtonType);
selectButton.setDisable(true);
@@ -58,9 +81,25 @@ public class PayNymDialog extends Dialog<PayNym> {
EventManager.get().unregister(payNymController);
});
setResultConverter(dialogButton -> dialogButton == selectButtonType ? payNymController.getPayNym() : null);
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;
}
return null;
});
} catch(IOException e) {
throw new RuntimeException(e);
}
}
public enum Operation {
SHOW, SELECT, SEND;
}
}
@@ -52,7 +52,7 @@ public class PayNymService {
body.put("code", paymentCode.toString());
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/create", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/create", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -70,7 +70,7 @@ public class PayNymService {
body.put("code", paymentCode.toString());
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/token", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/token", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -115,7 +115,7 @@ public class PayNymService {
body.put("signature", signature);
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/claim", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/claim", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -140,7 +140,7 @@ public class PayNymService {
body.put("signature", signature);
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/nym/add", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/nym/add", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -160,7 +160,7 @@ public class PayNymService {
body.put("target", paymentCode.toString());
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/follow", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/follow", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -174,7 +174,7 @@ public class PayNymService {
body.put("nym", nymIdentifier);
IHttpClient httpClient = httpClientService.getHttpClient(HttpUsage.COORDINATOR_REST);
return httpClient.postJson("https://paynym.is/api/v1/nym", Map.class, headers, body)
return httpClient.postJson(getHostUrl() + "/api/v1/nym", Map.class, headers, body)
.subscribeOn(Schedulers.io())
.observeOn(JavaFxScheduler.platform())
.map(Optional::get);
@@ -235,6 +235,14 @@ public class PayNymService {
httpClientService.setTorProxy(torProxy);
}
private String getHostUrl() {
return getHostUrl(getTorProxy() != null);
}
public static String getHostUrl(boolean tor) {
return tor ? "http://paynym7bwekdtb2hzgkpl6y2waqcrs2dii7lwincvxme7mdpcpxzfsad.onion" : "https://paynym.is";
}
public void shutdown() {
httpClientService.shutdown();
}
@@ -587,11 +587,12 @@ public class ServerPreferencesController extends PreferencesDetailController {
Optional<ButtonType> optButton = AppServices.showErrorDialog("SSL Handshake Failed", "The certificate provided by the server at " + tlsServerException.getServer().getHost() + " appears to have changed." +
"\n\nThis may indicate a man-in-the-middle attack!" +
"\n\nDo you still want to proceed?", ButtonType.NO, ButtonType.YES);
if(optButton.isPresent()) {
if(optButton.get() == ButtonType.YES) {
savedCrtFile.delete();
if(optButton.isPresent() && optButton.get() == ButtonType.YES) {
if(savedCrtFile.delete()) {
Platform.runLater(this::startElectrumConnection);
return;
} else {
AppServices.showErrorDialog("Could not delete certificate", "The certificate file at " + savedCrtFile.getAbsolutePath() + " could not be deleted.\n\nPlease delete this file manually.");
}
}
}
@@ -9,6 +9,7 @@ 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.Status;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.AppServices;
@@ -296,7 +297,9 @@ public class CounterpartyController extends SorobanController {
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
Map<BlockTransactionHashIndex, WalletNode> walletUtxos = wallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : walletUtxos.entrySet()) {
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
if(entry.getKey().getStatus() != Status.FROZEN) {
counterpartyCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
}
}
try {
@@ -39,10 +39,13 @@ 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.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;
@@ -148,6 +151,22 @@ public class InitiatorController extends SorobanController {
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(Config.get().isUsePayNym() && 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;
@@ -247,29 +266,11 @@ public class InitiatorController extends SorobanController {
return change;
};
counterparty.setTextFormatter(new TextFormatter<>(paymentCodeFilter));
counterparty.textProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
if(newValue.startsWith("P") && newValue.contains("...") && newValue.length() == 20 && counterpartyPaymentCode.get() != null) {
//Assumed valid payment code
} else if(Config.get().isUsePayNym() && PAYNYM_REGEX.matcher(newValue).matches()) {
if(!newValue.equals(counterpartyPayNymName.get())) {
payNymLoading.setVisible(true);
AppServices.getPayNymService().getPayNym(newValue).subscribe(payNym -> {
payNymLoading.setVisible(false);
counterpartyPayNymName.set(payNym.nymName());
counterpartyPaymentCode.set(new PaymentCode(payNym.paymentCode().toString()));
payNymAvatar.setPaymentCode(payNym.paymentCode());
}, error -> {
payNymLoading.setVisible(false);
//ignore, probably doesn't exist but will try again on meeting request
});
}
} else {
counterpartyPayNymName.set(null);
counterpartyPaymentCode.set(null);
payNymAvatar.clearPaymentCode();
}
counterparty.textProperty().addListener(counterpartyListener);
counterparty.addEventFilter(KeyEvent.ANY, event -> {
if(counterparty.isEditable() && event.getCode() == KeyCode.ENTER) {
searchPayNyms(counterparty.getText());
event.consume();
}
});
@@ -308,6 +309,25 @@ public class InitiatorController extends SorobanController {
});
}
private void searchPayNyms(String identifier) {
payNymLoading.setVisible(true);
AppServices.getPayNymService().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();
AppServices.getPayNymService().getPayNym(masterWallet.getPaymentCode().toString()).map(PayNym::following).subscribe(followerPayNyms -> {
@@ -433,7 +453,9 @@ public class InitiatorController extends SorobanController {
Payment payment = walletTransaction.getPayments().get(0);
Map<BlockTransactionHashIndex, WalletNode> firstSetUtxos = walletTransaction.isCoinControlUsed() ? walletTransaction.getSelectedUtxoSets().get(0) : wallet.getWalletUtxos();
for(Map.Entry<BlockTransactionHashIndex, WalletNode> entry : firstSetUtxos.entrySet()) {
initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
if(entry.getKey().getStatus() != Status.FROZEN) {
initiatorCahootsWallet.addUtxo(entry.getValue(), wallet.getWalletTransaction(entry.getKey().getHash()), (int)entry.getKey().getIndex());
}
}
SorobanCahootsService sorobanCahootsService = soroban.getSorobanCahootsService(initiatorCahootsWallet);
@@ -615,7 +637,7 @@ public class InitiatorController extends SorobanController {
}
public void findPayNym(ActionEvent event) {
PayNymDialog payNymDialog = new PayNymDialog(walletId, true, false);
PayNymDialog payNymDialog = new PayNymDialog(walletId, PayNymDialog.Operation.SELECT, false);
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(payNym -> {
counterpartyPayNymName.set(payNym.nymName());
@@ -14,6 +14,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.net.ElectrumServer;
@@ -59,6 +60,9 @@ import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
public class HeadersController extends TransactionFormController implements Initializable, DynamicUpdate {
private static final Logger log = LoggerFactory.getLogger(HeadersController.class);
@@ -78,7 +82,7 @@ public class HeadersController extends TransactionFormController implements Init
private TransactionDiagram transactionDiagram;
@FXML
private Spinner<Integer> version;
private IntegerSpinner version;
@FXML
private CopyableLabel segwit;
@@ -108,10 +112,10 @@ public class HeadersController extends TransactionFormController implements Init
private Field locktimeDateField;
@FXML
private Spinner<Integer> locktimeNone;
private IntegerSpinner locktimeNone;
@FXML
private Spinner<Integer> locktimeBlock;
private IntegerSpinner locktimeBlock;
@FXML
private Hyperlink locktimeCurrentHeight;
@@ -239,8 +243,12 @@ public class HeadersController extends TransactionFormController implements Init
updateTxId();
version.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 2, (int)tx.getVersion()));
version.setValueFactory(new IntegerSpinner.ValueFactory(1, 2, (int)tx.getVersion()));
version.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue == null || newValue < 1 || newValue > 2) {
return;
}
tx.setVersion(newValue);
if(oldValue != null) {
EventManager.get().post(new TransactionChangedEvent(tx));
@@ -301,9 +309,9 @@ public class HeadersController extends TransactionFormController implements Init
futureDateWarning.managedProperty().bind(futureDateWarning.visibleProperty());
futureDateWarning.setVisible(false);
locktimeNone.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
locktimeNone.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, 0));
if(tx.getLocktime() < Transaction.MAX_BLOCK_LOCKTIME) {
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
locktimeBlock.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1, (int)tx.getLocktime()));
if(tx.getLocktime() == 0) {
locktimeToggleGroup.selectToggle(locktimeNoneType);
} else {
@@ -312,13 +320,17 @@ public class HeadersController extends TransactionFormController implements Init
LocalDateTime date = Instant.now().atZone(ZoneId.systemDefault()).toLocalDateTime();
locktimeDate.setDateTimeValue(date);
} else {
locktimeBlock.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1));
locktimeBlock.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)Transaction.MAX_BLOCK_LOCKTIME-1));
LocalDateTime date = Instant.ofEpochSecond(tx.getLocktime()).atZone(ZoneId.systemDefault()).toLocalDateTime();
locktimeDate.setDateTimeValue(date);
locktimeToggleGroup.selectToggle(locktimeDateType);
}
locktimeBlock.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue == null || newValue < 0 || newValue >= Transaction.MAX_BLOCK_LOCKTIME) {
return;
}
tx.setLocktime(newValue);
locktimeCurrentHeight.setVisible(headersForm.isEditable() && AppServices.getCurrentBlockHeight() != null && newValue < AppServices.getCurrentBlockHeight());
futureBlockWarning.setVisible(AppServices.getCurrentBlockHeight() != null && newValue > AppServices.getCurrentBlockHeight());
@@ -793,7 +805,7 @@ public class HeadersController extends TransactionFormController implements Init
public void copyId(ActionEvent event) {
ClipboardContent content = new ClipboardContent();
content.putString(headersForm.getTransaction().getTxId().toString());
content.putString(headersForm.getTransaction().calculateTxId(false).toString());
Clipboard.getSystemClipboard().setContent(content);
}
@@ -828,7 +840,49 @@ public class HeadersController extends TransactionFormController implements Init
ToggleButton toggleButton = (ToggleButton)event.getSource();
toggleButton.setSelected(false);
EventManager.get().post(new RequestQRScanEvent(toggleButton.getScene().getWindow()));
QRScanDialog qrScanDialog = new QRScanDialog();
Optional<QRScanDialog.Result> optionalResult = qrScanDialog.showAndWait();
if(optionalResult.isPresent()) {
QRScanDialog.Result result = optionalResult.get();
if(result.transaction != null) {
EventManager.get().post(new ViewTransactionEvent(toggleButton.getScene().getWindow(), result.transaction));
} else if(result.psbt != null) {
EventManager.get().post(new ViewPSBTEvent(toggleButton.getScene().getWindow(), null, null, result.psbt));
} else if(result.seed != null) {
signFromSeed(result.seed);
} else if(result.exception != null) {
log.error("Error scanning QR", result.exception);
showErrorDialog("Error scanning QR", result.exception.getMessage());
} else {
AppServices.showErrorDialog("Invalid QR Code", "Cannot parse QR code into a transaction or seed.");
}
}
}
private void signFromSeed(DeterministicSeed seed) {
try {
String masterFingerprint = Keystore.fromSeed(seed, ScriptType.P2PKH.getDefaultDerivation()).getKeyDerivation().getMasterFingerprint();
Wallet walletCopy = headersForm.getSigningWallet().copy();
OptionalInt optIndex = IntStream.range(0, walletCopy.getKeystores().size())
.filter(i -> walletCopy.getKeystores().get(i).getKeyDerivation().getMasterFingerprint().equals(masterFingerprint)).findFirst();
if(optIndex.isPresent()) {
walletCopy.getKeystores().forEach(keystore -> {
keystore.setSeed(null);
keystore.setMasterPrivateExtendedKey(null);
});
Keystore original = walletCopy.getKeystores().get(optIndex.getAsInt());
Keystore replacement = Keystore.fromSeed(seed, original.getKeyDerivation().getDerivation());
walletCopy.getKeystores().set(optIndex.getAsInt(), replacement);
signUnencryptedKeystores(walletCopy);
} else {
AppServices.showErrorDialog("Invalid seed", "The QR code contains a seed that does not match any of the keystores in the signing wallet.");
}
} catch(MnemonicException e) {
log.error("Invalid seed", e);
AppServices.showErrorDialog("Invalid seed", e.getMessage());
} finally {
seed.clear();
}
}
public void savePSBT(ActionEvent event) {
@@ -951,8 +1005,13 @@ public class HeadersController extends TransactionFormController implements Init
private void finalizePSBT() {
if(headersForm.getPsbt() != null && headersForm.getPsbt().isSigned() && !headersForm.getPsbt().isFinalized()) {
headersForm.getSigningWallet().finalise(headersForm.getPsbt());
EventManager.get().post(new PSBTFinalizedEvent(headersForm.getPsbt()));
try {
headersForm.getSigningWallet().finalise(headersForm.getPsbt());
EventManager.get().post(new PSBTFinalizedEvent(headersForm.getPsbt()));
} catch(IllegalArgumentException e) {
AppServices.showErrorDialog("Cannot finalize PSBT", e.getMessage());
throw e;
}
}
}
@@ -1159,16 +1218,35 @@ public class HeadersController extends TransactionFormController implements Init
}
}
Long feeAmt = calculateFee(event.getInputTransactions());
if(feeAmt != null) {
updateFee(feeAmt);
}
if(!event.getInputTransactions().isEmpty()) {
Long feeAmt = calculateFee(event.getInputTransactions());
if(feeAmt != null) {
updateFee(feeAmt);
}
Map<Sha256Hash, BlockTransaction> allFetchedInputTransactions = new HashMap<>(event.getInputTransactions());
if(headersForm.getInputTransactions() != null) {
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
Map<Sha256Hash, BlockTransaction> allFetchedInputTransactions = new HashMap<>(event.getInputTransactions());
if(headersForm.getInputTransactions() != null) {
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
}
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
}
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
}
}
@Subscribe
public void transactionFetchFailed(TransactionFetchFailedEvent event) {
if(event.getTransaction().getTxId().equals(headersForm.getTransaction().getTxId())
&& !blockchainForm.isVisible() && !signingWalletForm.isVisible() && !signaturesForm.isVisible()) {
blockchainForm.setVisible(true);
blockStatus.setText("Unknown transaction status, server failed to respond");
Glyph errorGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
errorGlyph.setFontSize(12);
blockStatus.setGraphic(errorGlyph);
blockStatus.setContentDisplay(ContentDisplay.LEFT);
errorGlyph.getStyleClass().add("failure");
blockHeightField.setVisible(false);
blockTimestampField.setVisible(false);
blockHashField.setVisible(false);
}
}
@@ -1364,6 +1442,12 @@ public class HeadersController extends TransactionFormController implements Init
if(!changedLabelEntries.isEmpty()) {
Platform.runLater(() -> EventManager.get().post(new WalletEntryLabelsChangedEvent(event.getWallet(), changedLabelEntries)));
}
} else if(headersForm.getBlockTransaction() != null && headersForm.getBlockTransaction().getHeight() <= 0) {
BlockTransaction walletTransaction = event.getWallet().getWalletTransaction(headersForm.getBlockTransaction().getHash());
if(walletTransaction != null && walletTransaction.getHeight() > 0) {
headersForm.setBlockTransaction(walletTransaction);
updateBlockchainForm(headersForm.getBlockTransaction(), AppServices.getCurrentBlockHeight());
}
}
}
@@ -104,7 +104,7 @@ public class InputController extends TransactionFormController implements Initia
private CopyableLabel locktimeAbsolute;
@FXML
private Spinner<Integer> locktimeRelativeBlocks;
private IntegerSpinner locktimeRelativeBlocks;
@FXML
private RelativeTimelockSpinner locktimeRelativeSeconds;
@@ -406,7 +406,7 @@ public class InputController extends TransactionFormController implements Initia
}
});
locktimeRelativeBlocks.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0));
locktimeRelativeBlocks.setValueFactory(new IntegerSpinner.ValueFactory(0, (int)TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK, 0));
locktimeRelativeBlocks.managedProperty().bind(locktimeRelativeBlocks.visibleProperty());
locktimeRelativeSeconds.managedProperty().bind(locktimeRelativeSeconds.visibleProperty());
locktimeRelativeCombo.getSelectionModel().selectedItemProperty().addListener((ov, old_toggle, new_toggle) -> {
@@ -433,6 +433,10 @@ public class InputController extends TransactionFormController implements Initia
}
locktimeRelativeBlocks.valueProperty().addListener((obs, oldValue, newValue) -> {
if(newValue == null || newValue < 0 || newValue > TransactionInput.RELATIVE_TIMELOCK_VALUE_MASK) {
return;
}
setRelativeLocktime(txInput, transaction, oldValue != null);
});
locktimeRelativeSeconds.valueProperty().addListener((obs, oldValue, newValue) -> {
@@ -509,7 +513,7 @@ public class InputController extends TransactionFormController implements Initia
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(inputForm.getTransaction().getTxId()) && inputForm.getIndex() >= event.getPageStart() && inputForm.getIndex() < event.getPageEnd()) {
if(event.getTxId().equals(inputForm.getTransaction().getTxId()) && !event.getInputTransactions().isEmpty() && inputForm.getIndex() >= event.getPageStart() && inputForm.getIndex() < event.getPageEnd()) {
updateOutpoint(event.getInputTransactions());
if(inputForm.getPsbt() == null) {
updateSpends(event.getInputTransactions());
@@ -164,7 +164,7 @@ public class InputsController extends TransactionFormController implements Initi
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(inputsForm.getTransaction().getTxId()) && inputsForm.getPsbt() == null) {
if(event.getTxId().equals(inputsForm.getTransaction().getTxId()) && !event.getInputTransactions().isEmpty() && inputsForm.getPsbt() == null) {
updateBlockTransactionInputs(event.getInputTransactions());
}
}
@@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTInput;
import com.sparrowwallet.drongo.psbt.PSBTOutput;
import com.sparrowwallet.drongo.wallet.BlockTransaction;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.TransactionTabData;
@@ -30,6 +31,7 @@ import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.URL;
import java.util.*;
import java.util.stream.Collectors;
public class TransactionController implements Initializable {
private static final Logger log = LoggerFactory.getLogger(TransactionController.class);
@@ -57,6 +59,8 @@ public class TransactionController implements Initializable {
private int selectedInputIndex = -1;
private int selectedOutputIndex = -1;
private boolean transactionsFetched;
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@@ -68,6 +72,16 @@ public class TransactionController implements Initializable {
transactionMasterDetail.setShowDetailNode(Config.get().isShowTransactionHex());
txhex.setTransaction(getTransaction());
highlightTxHex();
fetchTransactions();
transactionMasterDetail.sceneProperty().addListener((observable, oldScene, newScene) -> {
if(oldScene == null && newScene != null) {
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.82);
}
});
}
private void fetchTransactions() {
fetchThisAndInputBlockTransactions(0, Math.min(getTransaction().getInputs().size(), PageForm.PAGE_SIZE));
fetchOutputBlockTransactions(0, Math.min(getTransaction().getOutputs().size(), PageForm.PAGE_SIZE));
@@ -76,12 +90,6 @@ public class TransactionController implements Initializable {
} else if(TransactionView.OUTPUT.equals(initialView) && initialIndex >= PageForm.PAGE_SIZE) {
fetchOutputBlockTransactions(initialIndex, initialIndex + 1);
}
transactionMasterDetail.sceneProperty().addListener((observable, oldScene, newScene) -> {
if(oldScene == null && newScene != null) {
transactionMasterDetail.setDividerPosition(AppServices.isReducedWindowHeight(transactionMasterDetail) ? 0.9 : 0.82);
}
});
}
private void initializeTxTree() {
@@ -353,6 +361,7 @@ public class TransactionController implements Initializable {
ElectrumServer.TransactionReferenceService transactionReferenceService = new ElectrumServer.TransactionReferenceService(references);
transactionReferenceService.setOnSucceeded(successEvent -> {
transactionsFetched = true;
Map<Sha256Hash, BlockTransaction> transactionMap = transactionReferenceService.getValue();
BlockTransaction thisBlockTx = null;
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
@@ -379,10 +388,36 @@ public class TransactionController implements Initializable {
});
transactionReferenceService.setOnFailed(failedEvent -> {
log.error("Error fetching transaction or input references", failedEvent.getSource().getException());
EventManager.get().post(new TransactionReferencesFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
if(references.contains(getTransaction().getTxId())) {
EventManager.get().post(new TransactionFetchFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
} else {
EventManager.get().post(new TransactionReferencesFailedEvent(getTransaction(), failedEvent.getSource().getException(), indexStart, maxIndex));
}
});
EventManager.get().post(new TransactionReferencesStartedEvent(getTransaction(), indexStart, maxIndex));
transactionReferenceService.start();
} else if(!AppServices.isConnected()) {
BlockTransaction blockTx = null;
Set<Sha256Hash> inputReferences = getTransaction().getInputs().stream().map(input -> input.getOutpoint().getHash()).collect(Collectors.toSet());
Map<Sha256Hash, BlockTransaction> inputTransactions = new HashMap<>();
for(Wallet wallet : AppServices.get().getOpenWallets().keySet()) {
Map<Sha256Hash, BlockTransaction> walletTransactions = wallet.getWalletTransactions();
if(blockTx == null && walletTransactions.get(getTransaction().getTxId()) != null) {
blockTx = walletTransactions.get(getTransaction().getTxId());
}
for(Sha256Hash inputReference : inputReferences) {
if(inputTransactions.get(inputReference) == null && walletTransactions.get(inputReference) != null) {
inputTransactions.put(inputReference, walletTransactions.get(inputReference));
}
}
}
if(inputTransactions.size() == inputReferences.size()) {
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, inputTransactions, 0, getTransaction().getInputs().size()));
} else {
EventManager.get().post(new BlockTransactionFetchedEvent(getTransaction(), blockTx, Collections.emptyMap(), 0, getTransaction().getInputs().size()));
}
}
}
@@ -428,7 +463,7 @@ public class TransactionController implements Initializable {
@Subscribe
public void viewTransaction(ViewTransactionEvent event) {
if(txdata.getTransaction().getTxId().equals(event.getBlockTransaction().getTransaction().getTxId())) {
if(txdata.getTransaction().getTxId().equals(event.getTransaction().getTxId())) {
TreeItem<TransactionForm> existingItem = getTreeItem(event.getInitialView(), event.getInitialIndex());
if(existingItem != null && !(existingItem.getValue() instanceof PageForm)) {
setTreeSelection(event.getInitialView(), event.getInitialIndex());
@@ -496,7 +531,7 @@ public class TransactionController implements Initializable {
@Subscribe
public void blockTransactionFetched(BlockTransactionFetchedEvent event) {
if(event.getTxId().equals(getTransaction().getTxId())) {
if(event.getTxId().equals(getTransaction().getTxId()) && !event.getInputTransactions().isEmpty()) {
if(event.getBlockTransaction() != null && (!Sha256Hash.ZERO_HASH.equals(event.getBlockTransaction().getBlockHash()) || txdata.getBlockTransaction() == null)) {
txdata.setBlockTransaction(event.getBlockTransaction());
}
@@ -543,4 +578,18 @@ public class TransactionController implements Initializable {
}
}
}
@Subscribe
public void newConnection(ConnectionEvent event) {
if(!transactionsFetched) {
fetchTransactions();
}
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
if(!transactionsFetched) {
fetchTransactions();
}
}
}
@@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.DateStringConverter;
import com.sparrowwallet.sparrow.control.IntegerSpinner;
import com.sparrowwallet.sparrow.event.SettingsChangedEvent;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@@ -10,8 +11,6 @@ import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.util.StringConverter;
import java.net.URL;
@@ -28,7 +27,7 @@ public class AdvancedController implements Initializable {
private DatePicker birthDate;
@FXML
private Spinner<Integer> gapLimit;
private IntegerSpinner gapLimit;
@FXML
private ComboBox<Integer> watchLast;
@@ -50,8 +49,12 @@ public class AdvancedController implements Initializable {
}
});
gapLimit.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(Wallet.DEFAULT_LOOKAHEAD, 9999, wallet.getGapLimit()));
gapLimit.setValueFactory(new IntegerSpinner.ValueFactory(Wallet.DEFAULT_LOOKAHEAD, 9999, wallet.getGapLimit()));
gapLimit.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == null || newValue < Wallet.DEFAULT_LOOKAHEAD || newValue > 9999) {
return;
}
wallet.setGapLimit(newValue);
if(!watchLast.getItems().equals(getWatchListItems(wallet))) {
Integer value = watchLast.getValue();
@@ -2,10 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.*;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
@@ -413,6 +410,18 @@ public class KeystoreController extends WalletFormController implements Initiali
}
AppServices.showErrorDialog("Missing Script Type", "QR Code did not contain any information for the " + getWalletForm().getWallet().getScriptType().getDescription() + " script type.");
} else if(result.seed != null) {
try {
Keystore keystore = Keystore.fromSeed(result.seed, getWalletForm().getWallet().getScriptType().getDefaultDerivation());
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
xpub.setText(keystore.getExtendedPublicKey().toString());
} catch(MnemonicException e) {
log.error("Error parsing seed", e);
AppServices.showErrorDialog("Error parsing seed", e.getMessage());
} finally {
result.seed.clear();
}
} else if(result.exception != null) {
log.error("Error scanning QR", result.exception);
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
@@ -7,6 +7,7 @@ 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;
@@ -31,7 +32,7 @@ public class MixToController implements Initializable {
private ComboBox<Wallet> mixToWallets;
@FXML
private Spinner<Integer> minMixes;
private IntegerSpinner minMixes;
@FXML
private ComboBox<IndexRange> indexRange;
@@ -94,8 +95,12 @@ public class MixToController implements Initializable {
});
int initialMinMixes = mixConfig.getMinMixes() == null ? Whirlpool.DEFAULT_MIXTO_MIN_MIXES : mixConfig.getMinMixes();
minMixes.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 10000, initialMinMixes));
minMixes.setValueFactory(new IntegerSpinner.ValueFactory(1, 10000, initialMinMixes));
minMixes.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue == null || newValue < 1 || newValue > 10000) {
return;
}
mixConfig.setMinMixes(newValue);
EventManager.get().post(new MixToConfigChangedEvent(wallet));
});
@@ -155,7 +155,7 @@ public class PaymentController extends WalletFormController implements Initializ
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(), true, selectLinkedOnly);
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly);
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(this::setPayNym);
} else if(newValue != null) {
@@ -279,10 +279,14 @@ public class PaymentController extends WalletFormController implements Initializ
}
public void setPayNym(PayNym payNym) {
PayNym existingPayNym = payNymProperty.get();
payNymProperty.set(payNym);
address.setText(payNym.nymName());
address.leftProperty().set(getPayNymGlyph());
label.requestFocus();
if(existingPayNym != null && payNym.nymName().equals(existingPayNym.nymName()) && payNym.isCollaborativeSend() != existingPayNym.isCollaborativeSend()) {
sendController.updateTransaction();
}
}
public void updateMixOnlyStatus() {
@@ -308,7 +312,9 @@ public class PaymentController extends WalletFormController implements Initializ
}
private void updateOpenWallets(Collection<Wallet> wallets) {
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet() && !wallet.isBip47()).collect(Collectors.toList());
List<Wallet> openWalletList = wallets.stream().filter(wallet -> wallet.isValid()
&& (wallet == sendController.getWalletForm().getWallet() || !wallet.isWhirlpoolChildWallet())
&& !wallet.isBip47()).collect(Collectors.toList());
if(sendController.getWalletForm().getWallet().hasPaymentCode()) {
openWalletList.add(payNymWallet);
@@ -350,25 +356,28 @@ public class PaymentController extends WalletFormController implements Initializ
}
private Address getRecipientAddress() throws InvalidAddressException {
if(payNymProperty.get() == null) {
PayNym payNym = payNymProperty.get();
if(payNym == null) {
return Address.fromString(address.getText());
}
try {
Wallet recipientBip47Wallet = getWalletForPayNym(payNymProperty.get());
if(recipientBip47Wallet != null) {
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
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;
if(!payNym.isCollaborativeSend()) {
try {
Wallet recipientBip47Wallet = getWalletForPayNym(payNym);
if(recipientBip47Wallet != null) {
WalletNode sendNode = recipientBip47Wallet.getFreshNode(KeyPurpose.SEND);
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;
}
}
} catch(InvalidPaymentCodeException e) {
log.error("Error creating payment code from PayNym", e);
}
} catch(InvalidPaymentCodeException e) {
log.error("Error creating payment code from PayNym", e);
}
return new PayNymAddress(payNymProperty.get());
return new PayNymAddress(payNym);
}
private Wallet getWalletForPayNym(PayNym payNym) throws InvalidPaymentCodeException {
@@ -490,6 +499,12 @@ public class PaymentController extends WalletFormController implements Initializ
}
public void clear() {
try {
AppServices.clearPayjoinURI(getRecipientAddress());
} catch(InvalidAddressException e) {
//ignore
}
address.setText("");
label.setText("");
@@ -400,6 +400,7 @@ public class SendController extends WalletFormController implements Initializabl
if(userFeeSet.get()) {
setTargetBlocks(getTargetBlocks(feeRate));
setFeeRangeRate(feeRate);
revalidate(fee, feeListener);
} else {
setFeeValueSats(walletTransaction.getFee());
}
@@ -985,7 +986,8 @@ public class SendController extends WalletFormController implements Initializabl
private boolean isMixPossible(List<Payment> payments) {
return (utxoSelectorProperty.get() == null || SorobanServices.canWalletMix(walletForm.getWallet()))
&& payments.size() == 1
&& (payments.get(0).getAddress().getScriptType() == getWalletForm().getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
&& (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) {
@@ -1633,6 +1635,7 @@ public class SendController extends WalletFormController implements Initializabl
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());
boolean addressReuse = userPayments.stream().anyMatch(payment -> walletAddresses.get(payment.getAddress()) != null && !walletAddresses.get(payment.getAddress()).getTransactionOutputs().isEmpty());
boolean payjoinPresent = userPayments.stream().anyMatch(payment -> AppServices.getPayjoinURI(payment.getAddress()) != null);
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
if(payNymPresent) {
@@ -1644,6 +1647,8 @@ public class SendController extends WalletFormController implements Initializabl
addLabel("Cannot coinjoin due to mixed address types", getInfoGlyph());
} else if(userPayments.size() > 1) {
addLabel("Cannot coinjoin due to multiple payments", getInfoGlyph());
} else if(payjoinPresent) {
addLabel("Cannot coinjoin due to payjoin", getInfoGlyph());
} else {
if(utxoSelectorProperty().get() != null) {
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
@@ -324,6 +324,7 @@ public class SettingsController extends WalletFormController implements Initiali
return;
}
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
List<ScriptExpression> scriptExpressions = getScriptExpressions(walletForm.getWallet().getScriptType());
CryptoOutput cryptoOutput;
@@ -341,7 +342,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
UR cryptoOutputUR = cryptoOutput.toUR();
QRDisplayDialog qrDisplayDialog = new QRDisplayDialog(cryptoOutputUR);
QRDisplayDialog qrDisplayDialog = new DescriptorQRDisplayDialog(walletForm.getWallet().getFullDisplayName(), outputDescriptor.toString(true), cryptoOutputUR);
qrDisplayDialog.showAndWait();
}
@@ -69,7 +69,13 @@ public class UtxoEntry extends HashIndexEntry {
private ObjectProperty<AddressStatus> addressStatusProperty;
public final void setDuplicateAddress(boolean value) {
addressStatusProperty().set(new AddressStatus(value));
AddressStatus addressStatus = addressStatusProperty().get();
addressStatusProperty().set(new AddressStatus(value, addressStatus.dustAttack));
}
public final void setDustAttack(boolean value) {
AddressStatus addressStatus = addressStatusProperty().get();
addressStatusProperty().set(new AddressStatus(addressStatus.duplicate, value));
}
public final boolean isDuplicateAddress() {
@@ -78,7 +84,7 @@ public class UtxoEntry extends HashIndexEntry {
public final ObjectProperty<AddressStatus> addressStatusProperty() {
if(addressStatusProperty == null) {
addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false));
addressStatusProperty = new SimpleObjectProperty<>(UtxoEntry.this, "addressStatus", new AddressStatus(false, false));
}
return addressStatusProperty;
@@ -86,9 +92,11 @@ public class UtxoEntry extends HashIndexEntry {
public class AddressStatus {
private final boolean duplicate;
private final boolean dustAttack;
public AddressStatus(boolean duplicate) {
public AddressStatus(boolean duplicate, boolean dustAttack) {
this.duplicate = duplicate;
this.dustAttack = dustAttack;
}
public UtxoEntry getUtxoEntry() {
@@ -102,6 +110,10 @@ public class UtxoEntry extends HashIndexEntry {
public boolean isDuplicate() {
return duplicate;
}
public boolean isDustAttack() {
return dustAttack;
}
}
/**
@@ -18,6 +18,7 @@ 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.net.ExchangeSource;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolDialog;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
@@ -82,6 +83,12 @@ public class UtxosController extends WalletFormController implements Initializab
@FXML
private Button mixTo;
@FXML
private Button selectAll;
@FXML
private Button clear;
@FXML
private Button sendSelected;
@@ -171,6 +178,7 @@ public class UtxosController extends WalletFormController implements Initializab
}
}
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());
@@ -186,9 +194,10 @@ public class UtxosController extends WalletFormController implements Initializab
}
private void updateFields(WalletUtxosEntry walletUtxosEntry) {
balance.setValue(walletUtxosEntry.getChildren().stream().mapToLong(Entry::getValue).sum());
mempoolBalance.setValue(walletUtxosEntry.getChildren().stream().filter(entry -> ((UtxoEntry)entry).getHashIndex().getHeight() <= 0).mapToLong(Entry::getValue).sum());
balance.setValue(walletUtxosEntry.getBalance());
mempoolBalance.setValue(walletUtxosEntry.getMempoolBalance());
utxoCount.setText(walletUtxosEntry.getChildren() != null ? Integer.toString(walletUtxosEntry.getChildren().size()) : "0");
selectAll.setDisable(walletUtxosEntry.getChildren() == null || walletUtxosEntry.getChildren().size() == 0);
}
private boolean canWalletMix() {
@@ -198,6 +207,8 @@ public class UtxosController extends WalletFormController implements Initializab
private void updateButtons(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());
@@ -368,6 +379,10 @@ public class UtxosController extends WalletFormController implements Initializab
.collect(Collectors.toList());
}
public void selectAll(ActionEvent event) {
utxosTable.getSelectionModel().selectAll();
}
public void clear(ActionEvent event) {
utxosTable.getSelectionModel().clearSelection();
}
@@ -586,6 +601,11 @@ public class UtxosController extends WalletFormController implements Initializab
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())) {
@@ -612,4 +632,20 @@ public class UtxosController extends WalletFormController implements Initializab
selectEntry(utxosTable, utxosTable.getRoot(), event.getEntry());
}
}
@Subscribe
public void fiatCurrencySelected(FiatCurrencySelectedEvent event) {
if(event.getExchangeSource() == ExchangeSource.NONE) {
fiatBalance.setCurrency(null);
fiatBalance.setBtcRate(0.0);
fiatMempoolBalance.setCurrency(null);
fiatMempoolBalance.setBtcRate(0.0);
}
}
@Subscribe
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
setFiatBalance(fiatBalance, event.getCurrencyRate(), getWalletForm().getWalletUtxosEntry().getBalance());
setFiatBalance(fiatMempoolBalance, event.getCurrencyRate(), getWalletForm().getWalletUtxosEntry().getMempoolBalance());
}
}
@@ -213,7 +213,7 @@ public class WalletForm {
private void updateWallets(Integer blockHeight, Wallet previousWallet) {
List<WalletNode> nestedHistoryChangedNodes = new ArrayList<>();
for(Wallet childWallet : wallet.getChildWallets()) {
for(Wallet childWallet : new ArrayList<>(wallet.getChildWallets())) {
if(childWallet.isNested()) {
Wallet previousChildWallet = previousWallet.getChildWallet(childWallet.getName());
if(previousChildWallet != null) {
@@ -2,16 +2,21 @@ 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 java.util.*;
import java.util.stream.Collectors;
public class WalletUtxosEntry extends Entry {
public static final int DUST_ATTACK_THRESHOLD_SATS = 1000;
public WalletUtxosEntry(Wallet wallet) {
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();
}
@@ -48,6 +53,18 @@ public class WalletUtxosEntry extends Entry {
}
}
protected void calculateDust() {
long dustAttackThreshold = Config.get().getDustAttackThreshold();
Set<WalletNode> duplicateNodes = getWallet().getWalletTxos().values().stream()
.collect(Collectors.groupingBy(e -> e, Collectors.counting()))
.entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toSet());
for(Entry entry : getChildren()) {
UtxoEntry utxoEntry = (UtxoEntry) entry;
utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && duplicateNodes.contains(utxoEntry.getNode()) && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
}
}
public void updateMixProgress() {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWallet());
if(whirlpool != null) {
@@ -74,6 +91,15 @@ public class WalletUtxosEntry extends Entry {
getChildren().removeAll(entriesRemoved);
calculateDuplicates();
calculateDust();
updateMixProgress();
}
public long getBalance() {
return getChildren().stream().mapToLong(Entry::getValue).sum();
}
public long getMempoolBalance() {
return getChildren().stream().filter(entry -> ((UtxoEntry)entry).getHashIndex().getHeight() <= 0).mapToLong(Entry::getValue).sum();
}
}
@@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
import com.google.common.net.HostAndPort;
import com.samourai.whirlpool.client.wallet.WhirlpoolEventService;
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;
@@ -161,6 +162,7 @@ public class WhirlpoolServices {
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
+1
View File
@@ -44,4 +44,5 @@ open module com.sparrowwallet.sparrow {
requires org.apache.commons.lang3;
requires net.sourceforge.streamsupport;
requires co.nstant.in.cbor;
requires com.github.librepdf.openpdf;
}
@@ -34,12 +34,19 @@
.master-only > .tab-header-area {
visibility: hidden;
-fx-pref-height: 0;
}
.wallet-subtabs > .tab-header-area .tab {
-fx-pref-height: 50;
-fx-pref-width: 90;
-fx-alignment: CENTER;
-fx-background-color: derive(#3da0e3, 32%);
-fx-background-insets: 0 1 0 1,0,0;
}
.wallet-subtabs > .tab-header-area .tab:selected {
-fx-background-color: #3da0e3;
}
.wallet-subtabs > .tab-header-area .tab-label {
@@ -49,6 +56,10 @@
-fx-translate-x: -6;
}
.wallet-subtabs > .tab-header-area .tab-label .label {
-fx-text-fill: #fff;
}
.status-bar .status-label {
-fx-alignment: center-left;
}
@@ -48,7 +48,8 @@
<SeparatorMenuItem styleClass="osxHide" />
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Preferences..." accelerator="Shortcut+P" onAction="#openPreferences"/>
<SeparatorMenuItem />
<MenuItem mnemonicParsing="false" text="Close Tab" accelerator="Shortcut+W" onAction="#closeTab"/>
<MenuItem fx:id="deleteWallet" mnemonicParsing="false" text="Delete Wallet..." onAction="#deleteWallet"/>
<MenuItem fx:id="closeTab" mnemonicParsing="false" text="Close Tab" accelerator="Shortcut+W" onAction="#closeTab"/>
<MenuItem styleClass="osxHide" mnemonicParsing="false" text="Quit" accelerator="Shortcut+Q" onAction="#quit"/>
</items>
</Menu>
@@ -102,7 +103,7 @@
<SeparatorMenuItem />
<MenuItem fx:id="minimizeToTray" mnemonicParsing="false" text="Minimize to System Tray" accelerator="Shortcut+Y" onAction="#minimizeToTray"/>
<MenuItem fx:id="lockWallet" mnemonicParsing="false" text="Lock Wallet" accelerator="Shortcut+L" onAction="#lockWallet"/>
<MenuItem mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/>
<MenuItem fx:id="lockAllWallets" mnemonicParsing="false" text="Lock All Wallets" accelerator="Shortcut+Shift+L" onAction="#lockWallets"/>
<SeparatorMenuItem />
<MenuItem fx:id="searchWallet" mnemonicParsing="false" text="Search Wallet" accelerator="Shortcut+Shift+S" onAction="#searchWallet"/>
<MenuItem fx:id="refreshWallet" mnemonicParsing="false" text="Refresh Wallet" accelerator="Shortcut+R" onAction="#refreshWallet"/>
@@ -118,6 +119,7 @@
<SeparatorMenuItem />
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
<MenuItem fx:id="restart" mnemonicParsing="false" text="Restart" onAction="#restart" />
</Menu>
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
<MenuItem mnemonicParsing="false" text="Show Introduction" onAction="#showIntroduction"/>
@@ -48,7 +48,6 @@
<FXCollections fx:factory="observableArrayList">
<FeeRatesSource fx:constant="ELECTRUM_SERVER" />
<FeeRatesSource fx:constant="MEMPOOL_SPACE" />
<FeeRatesSource fx:constant="BITCOINFEES_EARN_COM" />
<FeeRatesSource fx:constant="MINIMUM" />
</FXCollections>
</items>
@@ -28,6 +28,7 @@
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.HeadersController" stylesheets="@headers.css, @transaction.css, @../general.css">
<padding>
@@ -71,7 +72,7 @@
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset text="Headers" inputGrow="SOMETIMES">
<Field text="Version:">
<Spinner fx:id="version" prefWidth="60" editable="true" />
<IntegerSpinner fx:id="version" prefWidth="60" editable="true" />
</Field>
<Field text="Type:">
<CopyableLabel fx:id="segwit" />
@@ -94,10 +95,10 @@
</SegmentedButton>
</Field>
<Field fx:id="locktimeNoneField" text="Block:">
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
<IntegerSpinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
</Field>
<Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" max="499999999" prefWidth="120"/>
<IntegerSpinner fx:id="locktimeBlock" editable="true" max="499999999" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning">
<graphic>
@@ -17,6 +17,7 @@
<?import com.sparrowwallet.sparrow.control.AddressLabel?>
<?import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch?>
<?import com.sparrowwallet.sparrow.control.ScriptArea?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<GridPane hgap="10.0" vgap="10.0" styleClass="tx-pane" stylesheets="@input.css, @transaction.css, @../script.css, @../general.css" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1" fx:controller="com.sparrowwallet.sparrow.transaction.InputController">
<padding>
@@ -122,7 +123,7 @@
<CopyableLabel fx:id="locktimeAbsolute" />
</Field>
<Field fx:id="locktimeRelativeField" text="Value:">
<Spinner fx:id="locktimeRelativeBlocks" editable="true" prefWidth="110"/>
<IntegerSpinner fx:id="locktimeRelativeBlocks" editable="true" prefWidth="110"/>
<RelativeTimelockSpinner fx:id="locktimeRelativeSeconds" editable="true" prefWidth="110"/>
<ComboBox fx:id="locktimeRelativeCombo">
<items>
@@ -10,6 +10,7 @@
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.AdvancedController">
<center>
@@ -31,7 +32,7 @@
<HelpLabel helpText="The date of the earliest transaction (used to avoid scanning the entire blockchain)."/>
</Field>
<Field text="Gap limit:">
<Spinner fx:id="gapLimit" editable="true" prefWidth="90" />
<IntegerSpinner fx:id="gapLimit" editable="true" prefWidth="90" />
<HelpLabel helpText="Change how far ahead to look for additional transactions beyond the highest derivation with previous transaction outputs."/>
</Field>
<Field text="Watch addresses:">
@@ -11,8 +11,8 @@
<?import javafx.geometry.Insets?>
<?import javafx.collections.FXCollections?>
<?import javafx.scene.control.ComboBox?>
<?import javafx.scene.control.Spinner?>
<?import com.samourai.whirlpool.client.wallet.beans.IndexRange?>
<?import com.sparrowwallet.sparrow.control.IntegerSpinner?>
<BorderPane stylesheets="@../general.css" styleClass="line-border" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.MixToController">
<center>
@@ -34,7 +34,7 @@
<HelpLabel helpText="Select an open wallet to mix to."/>
</Field>
<Field text="Minimum mixes:">
<Spinner fx:id="minMixes" editable="true" prefWidth="90" />
<IntegerSpinner fx:id="minMixes" editable="true" prefWidth="90" />
<HelpLabel helpText="The minimum number of mixes required before mixing to the selected wallet.\nTo ensure privacy, each mix beyond this number will have a 75% probability to mix to the selected wallet."/>
</Field>
</Fieldset>
@@ -18,4 +18,12 @@
.chart-bar.selected {
-fx-bar-fill: rgba(30, 136, 207, 0.6);
}
.freeze-dust-utxo {
-fx-padding: 0 0 0 5px;
}
.tree-table-view:focused:row-selection .tree-table-row-cell:selected .freeze-dust-utxo {
-fx-text-fill: derive(white, -15%);
}
@@ -83,7 +83,8 @@
</HBox>
<Region HBox.hgrow="ALWAYS" />
<HBox styleClass="utxos-buttons-box" spacing="20" alignment="BOTTOM_RIGHT">
<Button text="Clear" onAction="#clear"/>
<Button fx:id="selectAll" text="Select All" onAction="#selectAll"/>
<Button fx:id="clear" text="Clear" onAction="#clear"/>
<Button fx:id="mixSelected" text="Mix Selected" graphicTextGap="5" onAction="#mixSelected">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="RANDOM" fontSize="12" />
@@ -13,11 +13,11 @@
}
.list-item:hover {
-fx-background-color: #4aa7e5;
-fx-background-color: linear-gradient(to right, #3da0e3, derive(#4aa7e5, 10%));
}
.list-item:selected {
-fx-background-color: #1e88cf;
-fx-background-color: linear-gradient(to right, #3da0e3, derive(#1e88cf, -10%));
}
.list-item:disabled {
@@ -123,6 +123,10 @@
-fx-text-fill: rgb(202, 18, 67);
}
.dust-attack-warning {
-fx-text-fill: rgb(238, 210, 2);
}
.unused-check {
-fx-text-fill: #50a14f;
}
@@ -41,12 +41,7 @@
</graphic>
</Label>
<HBox>
<VBox spacing="15">
<Label text="Initiating your first CoinJoin in Sparrow will add three new wallets to your existing wallet: Premix, Postmix and Badbank." wrapText="true" styleClass="content-text" />
<Label text="Premix contains UTXOs that have been split from your deposit UTXOs into equal amounts, waiting for their first mixing round. Postmix contains UTXOs that have been through at least one mixing round. Badbank contains any change from your premix transaction, and should be treated carefully." wrapText="true" styleClass="content-text" />
<Label text="Click on the tabs at the right of the wallet to use these wallets. Note that they will have reduced functionality (for example they will not display receiving addresses)." wrapText="true" styleClass="content-text" />
</VBox>
<TabPane side="RIGHT" rotateGraphic="true" styleClass="wallet-subtabs" minWidth="100" minHeight="280">
<TabPane side="LEFT" rotateGraphic="true" styleClass="wallet-subtabs" minWidth="100" minHeight="280">
<Tab text="" closable="false">
<graphic>
<Label text="Premix" contentDisplay="TOP">
@@ -78,6 +73,11 @@
<HBox/>
</Tab>
</TabPane>
<VBox spacing="15">
<Label text="Initiating your first CoinJoin in Sparrow will add three new wallets to your existing wallet: Premix, Postmix and Badbank." wrapText="true" styleClass="content-text" />
<Label text="Premix contains UTXOs that have been split from your deposit UTXOs into equal amounts, waiting for their first mixing round. Postmix contains UTXOs that have been through at least one mixing round. Badbank contains any change from your premix transaction, and should be treated carefully." wrapText="true" styleClass="content-text" />
<Label text="Click on the tabs on the left to use these wallets. Note that they will have reduced functionality (for example they will not display receiving addresses)." wrapText="true" styleClass="content-text" />
</VBox>
</HBox>
</VBox>
<VBox fx:id="step3" spacing="15">
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More