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