Compare commits

..
41 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
83 changed files with 845 additions and 150 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`
+22 -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.3'
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,7 +98,7 @@ 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')
@@ -118,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 {
@@ -329,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')
@@ -453,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')
@@ -462,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')
@@ -550,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')
@@ -578,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: 20f4ac9657...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.3</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;
@@ -286,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);
}
}
});
@@ -331,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()));
@@ -339,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())));
@@ -782,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());
}
@@ -836,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);
@@ -1382,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) {
@@ -1672,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);
}
@@ -1805,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");
@@ -1833,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));
}
@@ -1872,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));
}
@@ -2013,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());
@@ -2472,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());
}
}
}
@@ -2556,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()));
}
}
@@ -2565,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;
@@ -358,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();
@@ -690,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);
@@ -923,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.3";
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;
}
}
@@ -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;
}
}
@@ -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);
@@ -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);
@@ -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);
@@ -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.";
}
@@ -126,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();
@@ -163,6 +167,11 @@ public class Storage {
persistence.copyWallet(walletFile, outputStream);
}
public void delete() {
deleteBackups();
IOUtils.secureDelete(walletFile);
}
public void deleteBackups() {
deleteBackups(null);
}
@@ -181,7 +190,7 @@ public class Storage {
private void deleteBackups(String prefix) {
File[] backups = getBackups(prefix);
for(File backup : backups) {
backup.delete();
IOUtils.secureDelete(backup);
}
}
@@ -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");
@@ -60,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);
@@ -79,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;
@@ -109,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;
@@ -240,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));
@@ -302,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 {
@@ -313,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());
@@ -794,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);
}
@@ -829,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) {
@@ -1165,16 +1218,18 @@ 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));
}
}
@@ -1387,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<>();
@@ -387,6 +396,28 @@ public class TransactionController implements Initializable {
});
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()));
}
}
}
@@ -432,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());
@@ -500,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());
}
@@ -547,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));
});
@@ -312,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);
@@ -497,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());
@@ -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;
}
}
/**
@@ -83,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;
@@ -172,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());
@@ -190,6 +197,7 @@ public class UtxosController extends WalletFormController implements Initializab
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() {
@@ -199,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());
@@ -369,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();
}
@@ -587,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())) {
@@ -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,7 @@ public class WalletUtxosEntry extends Entry {
getChildren().removeAll(entriesRemoved);
calculateDuplicates();
calculateDust();
updateMixProgress();
}
@@ -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
@@ -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"/>
@@ -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" />
@@ -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;
}
@@ -76,7 +76,7 @@
<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" />
<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>
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.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,12 +3,12 @@ Name: el-CC-3-of-3-sb-2
Policy: 3 of 3
Format: P2WSH
# derivation: m/48'/0'/0'/2'
Derivation: m/48'/0'/0'/2'
06B57041: xpub6EfEGa5isJbQFSswM5Uptw5BSq2Td1ZDJr3QUNUcMySpC7itZ3ccypVHtLPnvMzKQ2qxrAgH49vhVxRcaQLFbixAVRR8RACrYTp88Uv9h8Z
# derivation: m/48'/0'/0'/2'
Derivation: m/48'/0'/0'/2'
4B569672: xpub6FFEQVG6QR28giDuML74Y7EMPwqEiKftNjScLzg5WKM41bf6LMP2XspjBgNp28tvkNUZdokmTY4TcRbuGZBSMvNoUECrKW1y3TBPeQJVmAg
# derivation: m/47'/0'/0'/1'
Derivation: m/47'/0'/0'/1'
CA9A2B19: xpub6Eb6Z1xtmWRiWKgRpHf6dHiEagGd6FLiBXrnma1nFK4PGRYqSVqVyJaxna5Mb8etSP4ATKVAvKnXG1a9HZauoAawuSDJT5RgH2HqEVHZVHY