Compare commits

...
34 Commits
Author SHA1 Message Date
Craig Raw 97f312cb93 v1.5.2 2021-10-29 15:47:48 +02:00
Craig Raw 269fd7f0da fix issue when displaying tx with new wallet 2021-10-29 15:43:52 +02:00
Craig Raw f0a813d031 handle concurrent modifications when saving address nodes 2021-10-29 14:39:51 +02:00
Craig Raw ceb5d85648 various minor fixes 2021-10-29 14:33:47 +02:00
Craig Raw 6b6b23b51a pay to the next fresh address of any open wallet via dropdown on send tab address field 2021-10-29 13:23:27 +02:00
Craig Raw 180e76f0f8 support discovery of singlesig connected hardware wallet accounts 2021-10-29 11:22:34 +02:00
Craig Raw d3b1c51115 increase toggle connecting animation rate 2021-10-28 16:55:24 +02:00
Craig Raw 72cb696451 add bip44 account discovery 2021-10-28 16:44:36 +02:00
Craig Raw 784fa5e1e8 minor whirlpool related fixes 2021-10-28 13:52:47 +02:00
Craig Raw 37aa3c9712 support account renaming, configuration of mix index range 2021-10-27 16:14:35 +02:00
Craig Raw 9520f6d218 upgrade to nightjar 0.2.19 (whirlpool client 0.23.37), minor tx diagram improvements 2021-10-27 12:01:04 +02:00
Craig Raw f4810bb568 show transaction diagram on every transaction headers screen 2021-10-26 17:35:58 +02:00
craigrawandGitHub febd5c33a2 Merge pull request #281 from grettke/master
Add mailmap file for Craig Raw addresses.
2021-10-25 13:47:20 +02:00
Craig Raw 4b39316821 recount mixes if mix data unavailable, correct mix status cell to remove mix progress from non-mixing utxos, show registered inputs total in tooltip 2021-10-25 12:25:18 +02:00
Grant Rettke 69544a2dc8 Add mailmap file for Craig Raw addresses. 2021-10-24 20:03:13 -05:00
craigrawandGitHub 2a0412320a Merge pull request #205 from RequestPrivacy/build
Add note to bitcoinbinary.org in build info
2021-10-22 11:49:09 +02:00
craigrawandGitHub fffd489bcf Merge pull request #278 from alaznem/master
Add other requirements for building to main README
2021-10-22 11:47:56 +02:00
craigrawandGitHub 70a6b259ba Merge pull request #267 from sashafklein/patch-1
Clarify how to run sparrow from CL
2021-10-22 11:43:50 +02:00
Craig Raw 26c177bd00 follow up 2021-10-22 11:19:26 +02:00
Craig Raw 1497b3d3bb add fee rate selection for premix 2021-10-22 11:01:58 +02:00
Alazne MoralesandGitHub 230f3c3db7 Add other requirements for building to main README
The "other requirements" are listed in the [reproducible builds readme](https://github.com/sparrowwallet/sparrow/blob/master/docs/reproducible.md#other-requirements) already. However when trying to quickly build the binaries and follow the main README instructions, the last paragraph about "[m]ore detailed instructions on reproducing the binaries are provided" can be overlooked very easily. Because the paragraph is about the specific topic "reproducible builds" and not building the binaries from source in general.
2021-10-21 20:39:50 +00:00
Craig Raw 813e0f3ab1 retrieve stored index from mixconfig on whirlpool wallet load 2021-10-21 14:38:13 +02:00
Craig Raw 237f97852d add coin selection filter to exclude immature coinbase outputs 2021-10-16 11:10:24 +02:00
Sasha KleinandGitHub 97ef580602 Clarify how to run sparrow from CL
This tiny Readme edit would, I think, have preempted the confusion that prompted #263 . I thought that the general install installed a global command, and didn't think, for some time, to run from within the directory. Dumb, but figured clarifying the doc would be better than just figuring it out silently on my own.
2021-10-14 19:35:37 -05:00
Craig Raw 2debc07375 import and export a wallet as an output descriptor in a text file 2021-10-14 14:27:13 +02:00
Craig Raw 1b3a35fda7 improve ui with whirlpool startup errors 2021-10-14 13:35:17 +02:00
Craig Raw 23fd597ca5 constrain mix from and mix to options to match those supported in the whirlpool client 2021-10-14 11:12:31 +02:00
Craig Raw eb012f638e fix popup window placement on moving active window back to primary screen 2021-10-14 10:23:04 +02:00
Craig Raw 63259a2056 fix lcd text rendering issue on osx 2021-10-14 09:55:26 +02:00
Craig Raw bad209ea5b automatically increase gap limit if required by postmix handler 2021-10-13 15:21:14 +02:00
Craig Raw 776fcb3044 permit mixing from badbank 2021-10-12 12:46:42 +02:00
Craig Raw 63ec856e87 improve transaction file opening io 2021-10-11 11:13:56 +02:00
Craig Raw ada8ca28e8 add tor proxy prompt text 2021-10-11 09:51:25 +02:00
RequestPrivacy 90dea201b9 Add note to bitcoinbinary.org in build info
Actively encourage people to document their build process and provide their work back to the community. Further, push adoption of bitcoinbinary.org as a site for verification and establish sparrow as a well proven/verified wallet.
2021-09-22 15:38:49 +02:00
73 changed files with 1708 additions and 411 deletions
+2
View File
@@ -0,0 +1,2 @@
Craig Raw <craigraw@gmail.com> Craig Raw <craig@quirk.biz>
Craig Raw <craigraw@gmail.com> craigraw <craigraw@gmail.com>
+9 -2
View File
@@ -19,7 +19,12 @@ or for those without SSH credentials:
In order to build, Sparrow requires Java 16 to be installed.
The release binaries are built with [AdoptOpenJdk 16.0.1+9 Hotspot](https://adoptopenjdk.net/archive.html?variant=openjdk16&jvmVariant=hotspot).
These binaries can be built from source using
Other packages may also be necessary to build depending on the platform. On Debian/Ubuntu systems:
`sudo apt install -y rpm fakeroot binutils`
The Sparrow binaries can be built from source using
`./gradlew jpackage`
@@ -29,9 +34,11 @@ When updating to the latest HEAD
The release binaries are reproducible from v1.5.0 onwards (pre codesigning and installer packaging). More detailed [instructions on reproducing the binaries](docs/reproducible.md) are provided.
> A video documention of your built process uploaded to [bitcoinbinary.org](https://bitcoinbinary.org/) is appreciated. Alternatively check the site if you wish to see if someone else already verified the provided binaries.
## Running
If you prefer to run Sparrow directly from source, it can be launched with
If you prefer to run Sparrow directly from source, it can be launched from within the project directory with
`./sparrow`
+5 -5
View File
@@ -5,7 +5,7 @@ plugins {
id 'org.beryx.jlink' version '2.24.0'
}
def sparrowVersion = '1.5.1'
def sparrowVersion = '1.5.2'
def os = org.gradle.internal.os.OperatingSystem.current()
def osName = os.getFamilyName()
if(os.macOsX) {
@@ -91,7 +91,7 @@ dependencies {
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
exclude group: 'org.slf4j'
}
implementation('com.sparrowwallet.nightjar:nightjar:0.2.18-SNAPSHOT')
implementation('com.sparrowwallet.nightjar:nightjar:0.2.19')
testImplementation('junit:junit:4.12')
}
@@ -142,7 +142,7 @@ run {
"--add-opens=java.base/java.io=com.google.gson"]
if(os.macOsX) {
applicationDefaultJvmArgs += ["-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
applicationDefaultJvmArgs += ["-Dprism.lcdtext=false", "-Xdock:name=Sparrow", "-Xdock:icon=/Users/scy/git/sparrow/src/main/resources/sparrow-large.png",
"--add-opens=javafx.graphics/com.sun.glass.ui.mac=centerdevice.nsmenufx"]
}
}
@@ -194,7 +194,7 @@ jlink {
"--add-reads=com.sparrowwallet.merged.module=com.fasterxml.jackson.core"]
if(os.macOsX) {
jvmArgs += "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"
jvmArgs += ["-Dprism.lcdtext=false", "--add-opens=javafx.graphics/com.sun.glass.ui.mac=com.sparrowwallet.merged.module"]
}
}
addExtraDependencies("javafx")
@@ -449,7 +449,7 @@ extraJavaModuleInfo {
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
exports('co.nstant.in.cbor')
}
module('nightjar-0.2.18-SNAPSHOT.jar', 'com.sparrowwallet.nightjar', '0.2.18-SNAPSHOT') {
module('nightjar-0.2.19.jar', 'com.sparrowwallet.nightjar', '0.2.19') {
requires('com.google.common')
requires('net.sourceforge.streamsupport')
requires('org.slf4j')
+1 -1
View File
@@ -51,7 +51,7 @@ sudo apt install -y rpm fakeroot binutils
The project can cloned for a specific release tag as follows:
```shell
GIT_TAG="1.5.1"
GIT_TAG="1.5.2"
git clone --recursive --branch "${GIT_TAG}" git@github.com:sparrowwallet/sparrow.git
```
+1 -1
Submodule drongo updated: 025af05758...6c03c4106a
+1 -1
View File
@@ -21,7 +21,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.5.1</string>
<string>1.5.2</string>
<key>CFBundleSignature</key>
<string>????</string>
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
@@ -44,6 +44,7 @@ import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.fxml.Initializable;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.Side;
import javafx.scene.Node;
@@ -69,6 +70,7 @@ import org.slf4j.LoggerFactory;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.text.ParseException;
import java.util.*;
import java.util.stream.Collectors;
@@ -395,6 +397,9 @@ public class AppController implements Initializable {
controller.setStage(stage);
controller.initializeView();
setStageIcon(stage);
stage.setOnShowing(event -> {
AppServices.moveToActiveWindowScreen(stage, 600, 460);
});
return stage;
} catch(IOException e) {
@@ -454,10 +459,7 @@ public class AppController implements Initializable {
if(file.exists()) {
try {
byte[] bytes = new byte[(int)file.length()];
FileInputStream stream = new FileInputStream(file);
stream.read(bytes);
stream.close();
byte[] bytes = Files.readAllBytes(file.toPath());
String name = file.getName();
try {
@@ -910,7 +912,7 @@ public class AppController implements Initializable {
if(!wallet.isMasterWallet() && wallet.getMasterWallet().getKeystores().size() == copy.getKeystores().size() && wallet.getMasterWallet().getKeystores().get(i).hasSeed()) {
copyKeystore.getSeed().setPassphrase(wallet.getMasterWallet().getKeystores().get(i).getSeed().getPassphrase());
} else {
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullName(), copyKeystore);
KeystorePassphraseDialog passphraseDialog = new KeystorePassphraseDialog(wallet.getFullDisplayName(), copyKeystore);
Optional<String> optionalPassphrase = passphraseDialog.showAndWait();
if(optionalPassphrase.isPresent()) {
copyKeystore.getSeed().setPassphrase(optionalPassphrase.get());
@@ -977,6 +979,7 @@ public class AppController implements Initializable {
List<WalletImport> walletImporters = List.of(new ColdcardSinglesig(), new ColdcardMultisig(),
new Electrum(),
new SpecterDesktop(),
new Descriptor(),
new CoboVaultSinglesig(), new CoboVaultMultisig(),
new PassportSinglesig(),
new KeystoneSinglesig(), new KeystoneMultisig(),
@@ -1280,7 +1283,7 @@ public class AppController implements Initializable {
addWalletSubTab(subTabs, storage, wallet, backupWallet);
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(getAutomaticName(wallet.getMasterWallet()));
masterLabel.setText(wallet.getMasterWallet().getLabel() != null ? wallet.getMasterWallet().getLabel() : wallet.getMasterWallet().getAutomaticName());
Platform.runLater(() -> {
setSubTabsVisible(subTabs, true);
});
@@ -1310,7 +1313,9 @@ public class AppController implements Initializable {
try {
Tab subTab = new Tab();
subTab.setClosable(false);
Label subTabLabel = new Label(wallet.isMasterWallet() ? getAutomaticName(wallet) : wallet.getName());
String label = wallet.getLabel() != null ? wallet.getLabel() : (wallet.isMasterWallet() ? wallet.getAutomaticName() : wallet.getName());
Label subTabLabel = new Label(label);
subTabLabel.setPadding(new Insets(0, 3, 0, 3));
subTabLabel.setGraphic(getSubTabGlyph(wallet));
subTabLabel.setContentDisplay(ContentDisplay.TOP);
subTabLabel.setAlignment(Pos.TOP_CENTER);
@@ -1328,6 +1333,9 @@ public class AppController implements Initializable {
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
subTab.setUserData(tabData);
if(!wallet.isWhirlpoolChildWallet()) {
subTab.setContextMenu(getSubTabContextMenu(subTab));
}
subTabs.getTabs().add(subTab);
subTabs.getTabs().sort((o1, o2) -> {
@@ -1360,11 +1368,6 @@ public class AppController implements Initializable {
return tabGlyph;
}
private String getAutomaticName(Wallet wallet) {
int account = wallet.getAccountIndex();
return (account < 0 || account > 9) ? wallet.getName() : (!wallet.isWhirlpoolMasterWallet() || account > 1 ? "Account #" + account : "Deposit");
}
public WalletForm getSelectedWalletForm() {
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
TabData tabData = (TabData)selectedTab.getUserData();
@@ -1378,24 +1381,6 @@ public class AppController implements Initializable {
return null;
}
public void openExamples(ActionEvent event) {
try {
addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559");
addTransactionTab("p2sh", null, "0100000003a5ee1a0fd80dfbc3142df136ab56e082b799c13aa977c048bdf8f61bd158652c000000006b48304502203b0160de302cded63589a88214fe499a25aa1d86a2ea09129945cd632476a12c022100c77727daf0718307e184d55df620510cf96d4b5814ae3258519c0482c1ca82fa0121024f4102c1f1cf662bf99f2b034eb03edd4e6c96793cb9445ff519aab580649120ffffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0000000006a47304402204089adb8a1de1a9e22aa43b94d54f1e54dc9bea745d57df1a633e03dd9ede3c2022037d1e53e911ed7212186028f2e085f70524930e22eb6184af090ba4ab779a5b90121030644cb394bf381dbec91680bdf1be1986ad93cfb35603697353199fb285a119effffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0010000009300493046022100a07b2821f96658c938fa9c68950af0e69f3b2ce5f8258b3a6ad254d4bc73e11e022100e82fab8df3f7e7a28e91b3609f91e8ebf663af3a4dc2fd2abd954301a5da67e701475121022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e2103a767c7221e9f15f870f1ad9311f5ab937d79fcaeee15bb2c722bca515581b4c052aeffffffff02a3b81b00000000001976a914ea00917f128f569cbdf79da5efcd9001671ab52c88ac80969800000000001976a9143dec0ead289be1afa8da127a7dbdd425a05e25f688ac00000000");
addTransactionTab("p2sh-p2wpkh", null, "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000");
addTransactionTab("p2sh-p2wsh", null, "01000000000101708256c5896fb3f00ef37601f8e30c5b460dbcd1fca1cd7199f9b56fc4ecd5400000000023220020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4cffffffff01603edb0300000000160014bbef244bcad13cffb68b5cef3017c7423675552204004730440220010d2854b86b90b7c33661ca25f9d9f15c24b88c5c4992630f77ff004b998fb802204106fc3ec8481fa98e07b7e78809ac91b6ccaf60bf4d3f729c5a75899bb664a501473044022046d66321c6766abcb1366a793f9bfd0e11e0b080354f18188588961ea76c5ad002207262381a0661d66f5c39825202524c45f29d500c6476176cd910b1691176858701695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000");
addTransactionTab("p2wpkh", null, "01000000000101109d2e41430bfdec7e6dfb02bf78b5827eeb717ef25210ff3203b0db8c76c9260000000000ffffffff01a032eb0500000000160014bbef244bcad13cffb68b5cef3017c742367555220247304402202f7cac3494e521018ae0be4ca18517639ef7c00658d42a9f938b2b344c8454e2022039a54218832fad5d14b331329d9042c51ee6be287e95e49ee5b96fda1f5ce13f0121026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf300000000");
addTransactionTab("p2wsh", null, "0100000000010193a2db37b841b2a46f4e9bb63fe9c1012da3ab7fe30b9f9c974242778b5af8980000000000ffffffff01806fb307000000001976a914bbef244bcad13cffb68b5cef3017c7423675552288ac040047304402203cdcaf02a44e37e409646e8a506724e9e1394b890cb52429ea65bac4cc2403f1022024b934297bcd0c21f22cee0e48751c8b184cc3a0d704cae2684e14858550af7d01483045022100feb4e1530c13e72226dc912dcd257df90d81ae22dbddb5a3c2f6d86f81d47c8e022069889ddb76388fa7948aaa018b2480ac36132009bb9cfade82b651e88b4b137a01695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000");
//addTransactionTab("test1", null, "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd0481500400002305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900");
//addTransactionTab("3of3-1s.psbt", null, "70736274ff0100550200000001294c4871c059bb76be81e94b78059ee2e0c9b1b47f38edb6b4e75916062394930000000000feffffff01f82a0000000000001976a914e65b294f890792f2c2725d488567018d660f0cf488ac701c09004f0102aa7ed3044b1635bb800000021bf4bfc48934b7966b39bdebb689525d9b8bfed5c8b16e8c58f9afe4641d6d5f03800b5dbec0355c9f0b5e8227bc903e9d0ff1fe6ced0dcfb6d416541c7412c4331406b57041300000800000008000000080020000804f0102aa7ed3042cd31dee80000002d544b2364010378f8c6cec85f6b7ed83a8203dcdbedb97e2625f431f897b837e0363428de8fcfbfe373c0d9e1e0cc8163d886764bafe71c5822eaa232981356589145f63394f300000800000008000000080020000804f0102aa7ed3049ec7d9f580000002793e04aff18b4e40ebc48bcdc6232c54c69cf7265a38fbd85b35705e34d2d42f03368e79aa2b2b7f736d156905a7a45891df07baa2d0b7f127a537908cb82deed514130a48af300000800000008000000080020000800001012b983a000000000000220020f64748dad1cbad107761aaed5c59f25aba006498d260b440e0a091691350c9aa010569532102f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3262103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a221037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f753ae220602f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3261c130a48af300000800000008000000080020000800000000000000000220603171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a21c5f63394f300000800000008000000080020000800000000000000000220203171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a24830450221008d27cc4b03bc543726e73b69e7980e7364d6f33f979a5cd9b92fb3d050666bd002204fc81fc9c67baf7c3b77041ed316714a9c117a5bdbb020e8c771ea3bdc342434012206037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f71c06b570413000008000000080000000800200008000000000000000000000");
//addTransactionTab("signer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
//addTransactionTab("combiner.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
addTransactionTab("finalizer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000");
} catch(Exception e) {
log.error("Error opening examples", e);
}
}
private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException {
if(Utils.isBase64(string) && !Utils.isHex(string)) {
addTransactionTab(name, file, Base64.getDecoder().decode(string));
@@ -1577,6 +1562,28 @@ public class AppController implements Initializable {
return contextMenu;
}
private ContextMenu getSubTabContextMenu(Tab subTab) {
ContextMenu contextMenu = new ContextMenu();
MenuItem rename = new MenuItem("Rename Account");
rename.setOnAction(event -> {
Label subTabLabel = (Label)subTab.getGraphic();
WalletLabelDialog walletLabelDialog = new WalletLabelDialog(subTabLabel.getText());
Optional<String> optLabel = walletLabelDialog.showAndWait();
if(optLabel.isPresent()) {
String label = optLabel.get();
subTabLabel.setText(label);
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
Wallet wallet = walletTabData.getWallet();
wallet.setLabel(label);
EventManager.get().post(new WalletLabelChangedEvent(wallet));
}
});
contextMenu.getItems().add(rename);
return contextMenu;
}
public void setServerType(ServerType serverType) {
if(serverType == ServerType.PUBLIC_ELECTRUM_SERVER && !serverToggle.getStyleClass().contains("public-server")) {
serverToggle.getStyleClass().add("public-server");
@@ -1605,7 +1612,7 @@ public class AppController implements Initializable {
private void serverToggleStartAnimation() {
Node thumbArea = serverToggle.lookup(".thumb-area");
if(thumbArea != null) {
FadeTransition fadeTransition = new FadeTransition(Duration.millis(1000), thumbArea);
FadeTransition fadeTransition = new FadeTransition(Duration.millis(600), thumbArea);
fadeTransition.setFromValue(1.0);
fadeTransition.setToValue(0.4);
fadeTransition.setAutoReverse(true);
@@ -2031,7 +2038,7 @@ public class AppController implements Initializable {
@Subscribe
public void walletHistoryStarted(WalletHistoryStartedEvent event) {
if(AppServices.isConnected() && getOpenWallets().containsKey(event.getWallet())) {
if(event.getWalletNode() == null && event.getWallet().getTransactions().isEmpty()) {
if(event.getWalletNodes() == null && event.getWallet().getTransactions().isEmpty()) {
statusUpdated(new StatusEvent(LOADING_TRANSACTIONS_MESSAGE, 120));
if(statusTimeline == null || statusTimeline.getStatus() != Animation.Status.RUNNING) {
statusBar.setProgress(-1);
@@ -622,7 +622,7 @@ public class AppServices {
}
public static Optional<ButtonType> showErrorDialog(String title, String content, ButtonType... buttons) {
return showAlertDialog(title, content, Alert.AlertType.ERROR, buttons);
return showAlertDialog(title, content == null ? "See log file (Help menu)" : content, Alert.AlertType.ERROR, buttons);
}
public static Optional<ButtonType> showAlertDialog(String title, String content, Alert.AlertType alertType, ButtonType... buttons) {
@@ -695,7 +695,9 @@ public class AppServices {
public static void moveToWindowScreen(Window currentWindow, Window newWindow, double newWindowWidth, double newWindowHeight) {
Screen currentScreen = Screen.getScreens().stream().filter(screen -> screen.getVisualBounds().contains(currentWindow.getX(), currentWindow.getY())).findFirst().orElse(null);
if(currentScreen != null && !Screen.getPrimary().getVisualBounds().contains(currentWindow.getX(), currentWindow.getY()) && !currentScreen.getVisualBounds().contains(newWindow.getX(), newWindow.getY())) {
if(currentScreen != null
&& ((!Double.isNaN(newWindow.getX()) && !Double.isNaN(newWindow.getY())) || !Screen.getPrimary().getVisualBounds().contains(currentWindow.getX(), currentWindow.getY()))
&& !currentScreen.getVisualBounds().contains(newWindow.getX(), newWindow.getY())) {
double x = currentWindow.getX() + (currentWindow.getWidth() / 2) - (newWindowWidth / 2);
double y = currentWindow.getY() + (currentWindow.getHeight() / 2.2) - (newWindowHeight / 2);
newWindow.setX(x);
@@ -31,7 +31,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.5.1";
public static final String APP_VERSION = "1.5.2";
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,9 +1,12 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ServerType;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.collections.FXCollections;
import javafx.scene.control.*;
@@ -14,8 +17,9 @@ import org.controlsfx.glyphfont.Glyph;
import java.util.ArrayList;
import java.util.List;
public class AddAccountDialog extends Dialog<StandardAccount> {
public class AddAccountDialog extends Dialog<List<StandardAccount>> {
private final ComboBox<StandardAccount> standardAccountCombo;
private boolean discoverAccounts = false;
public AddAccountDialog(Wallet wallet) {
final DialogPane dialogPane = getDialogPane();
@@ -56,6 +60,18 @@ public class AddAccountDialog extends Dialog<StandardAccount> {
availableAccounts.add(StandardAccount.WHIRLPOOL_PREMIX);
}
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
if(!availableAccounts.isEmpty() && Config.get().getServerType() != ServerType.BITCOIN_CORE &&
(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
dialogPane.getButtonTypes().add(discoverButtonType);
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
discoverButton.setOnAction(event -> {
discoverAccounts = true;
});
}
standardAccountCombo.setItems(FXCollections.observableList(availableAccounts));
standardAccountCombo.setConverter(new StringConverter<>() {
@Override
@@ -86,6 +102,10 @@ public class AddAccountDialog extends Dialog<StandardAccount> {
content.getChildren().add(standardAccountCombo);
dialogPane.setContent(content);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? standardAccountCombo.getValue() : null);
setResultConverter(dialogButton -> dialogButton == ButtonType.OK ? List.of(standardAccountCombo.getValue()) : (dialogButton == discoverButtonType ? availableAccounts : null));
}
public boolean isDiscoverAccounts() {
return discoverAccounts;
}
}
@@ -48,7 +48,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
}
private String getTooltipText(UtxoEntry utxoEntry, boolean duplicate) {
return utxoEntry.getNode().getDerivationPath().replace("m", "..") + (duplicate ? " (Duplicate address)" : "");
return utxoEntry.getNode().toString() + (duplicate ? " (Duplicate address)" : "");
}
public static Glyph getDuplicateGlyph() {
@@ -0,0 +1,47 @@
package com.sparrowwallet.sparrow.control;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Cursor;
import javafx.scene.Node;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import org.controlsfx.control.textfield.CustomTextField;
public class ComboBoxTextField extends CustomTextField {
private final ObjectProperty<ComboBox<?>> comboProperty = new SimpleObjectProperty<>();
public ComboBoxTextField() {
super();
getStyleClass().add("combo-text-field");
setupCopyButtonField(super.rightProperty());
}
private void setupCopyButtonField(ObjectProperty<Node> rightProperty) {
Region showComboButton = new Region();
showComboButton.getStyleClass().addAll("graphic"); //$NON-NLS-1$
StackPane showComboButtonPane = new StackPane(showComboButton);
showComboButtonPane.getStyleClass().addAll("combo-button"); //$NON-NLS-1$
showComboButtonPane.setCursor(Cursor.DEFAULT);
showComboButtonPane.setOnMouseReleased(e -> {
if(comboProperty.isNotNull().get()) {
comboProperty.get().show();
}
});
rightProperty.set(showComboButtonPane);
}
public ComboBox<?> getComboProperty() {
return comboProperty.get();
}
public ObjectProperty<ComboBox<?>> walletComboProperty() {
return comboProperty;
}
public void setComboProperty(ComboBox<?> comboProperty) {
this.comboProperty.set(comboProperty);
}
}
@@ -0,0 +1,39 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.KeystoresDiscoveredEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.Collections;
import java.util.List;
import java.util.Map;
public class DeviceKeystoreDiscoverDialog extends DeviceDialog<Map<StandardAccount, Keystore>> {
private final Wallet masterWallet;
private final List<StandardAccount> availableAccounts;
public DeviceKeystoreDiscoverDialog(List<String> operationFingerprints, Wallet masterWallet, List<StandardAccount> availableAccounts) {
super(operationFingerprints);
this.masterWallet = masterWallet;
this.availableAccounts = availableAccounts;
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
setResultConverter(dialogButton -> dialogButton.getButtonData().isCancelButton() ? null : Collections.emptyMap());
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(masterWallet, availableAccounts, device, defaultDevice);
}
@Subscribe
public void keystoresDiscovered(KeystoresDiscoveredEvent event) {
setResult(event.getDiscoveredKeystores());
}
}
@@ -10,12 +10,14 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Device;
import com.sparrowwallet.sparrow.io.Hwi;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.geometry.Insets;
@@ -33,8 +35,7 @@ import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.List;
import java.util.*;
import java.util.stream.Collectors;
public class DevicePane extends TitledDescriptionPane {
@@ -46,6 +47,7 @@ public class DevicePane extends TitledDescriptionPane {
private final OutputDescriptor outputDescriptor;
private final KeyDerivation keyDerivation;
private final String message;
private final List<StandardAccount> availableAccounts;
private final Device device;
private CustomPasswordField pinField;
@@ -56,6 +58,7 @@ public class DevicePane extends TitledDescriptionPane {
private Button signButton;
private Button displayAddressButton;
private Button signMessageButton;
private Button discoverKeystoresButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
@@ -69,6 +72,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = null;
this.keyDerivation = requiredDerivation;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -91,6 +95,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -113,6 +118,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = outputDescriptor;
this.keyDerivation = null;
this.message = null;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -135,6 +141,7 @@ public class DevicePane extends TitledDescriptionPane {
this.outputDescriptor = null;
this.keyDerivation = keyDerivation;
this.message = message;
this.availableAccounts = null;
this.device = device;
this.defaultDevice = defaultDevice;
@@ -149,6 +156,29 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, signMessageButton);
}
public DevicePane(Wallet wallet, List<StandardAccount> availableAccounts, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.DISCOVER_KEYSTORES;
this.wallet = wallet;
this.psbt = null;
this.outputDescriptor = null;
this.keyDerivation = null;
this.message = null;
this.device = device;
this.defaultDevice = defaultDevice;
this.availableAccounts = availableAccounts;
setDefaultStatus();
showHideLink.setVisible(false);
createSetPassphraseButton();
createDiscoverKeystoresButton();
initialise(device);
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
}
private void initialise(Device device) {
if(device.isNeedsPinSent()) {
unlockButton.setDefaultButton(defaultDevice);
@@ -281,6 +311,17 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void createDiscoverKeystoresButton() {
discoverKeystoresButton = new Button("Discover");
discoverKeystoresButton.setAlignment(Pos.CENTER_RIGHT);
discoverKeystoresButton.setOnAction(event -> {
discoverKeystoresButton.setDisable(true);
discoverKeystores();
});
discoverKeystoresButton.managedProperty().bind(discoverKeystoresButton.visibleProperty());
discoverKeystoresButton.setVisible(false);
}
private void unlock(Device device) {
if(device.getModel().requiresPinPrompt()) {
promptPin();
@@ -620,6 +661,63 @@ public class DevicePane extends TitledDescriptionPane {
signMessageService.start();
}
private void discoverKeystores() {
if(wallet.getKeystores().size() != 1) {
setError("Could not discover keystores", "Only single signature wallets are supported for keystore discovery");
return;
}
String masterFingerprint = wallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
Wallet copyWallet = wallet.copy();
Map<StandardAccount, String> accountDerivationPaths = new LinkedHashMap<>();
for(StandardAccount availableAccount : availableAccounts) {
Wallet availableWallet = copyWallet.addChildWallet(availableAccount);
Keystore availableKeystore = availableWallet.getKeystores().get(0);
String derivationPath = availableKeystore.getKeyDerivation().getDerivationPath();
accountDerivationPaths.put(availableAccount, derivationPath);
}
Map<StandardAccount, Keystore> importedKeystores = new LinkedHashMap<>();
Hwi.GetXpubsService getXpubsService = new Hwi.GetXpubsService(device, passphrase.get(), accountDerivationPaths);
getXpubsService.setOnSucceeded(workerStateEvent -> {
Map<StandardAccount, String> accountXpubs = getXpubsService.getValue();
for(Map.Entry<StandardAccount, String> entry : accountXpubs.entrySet()) {
try {
Keystore keystore = new Keystore();
keystore.setLabel(device.getModel().toDisplayString());
keystore.setSource(KeystoreSource.HW_USB);
keystore.setWalletModel(device.getModel());
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, accountDerivationPaths.get(entry.getKey())));
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(entry.getValue()));
importedKeystores.put(entry.getKey(), keystore);
} catch(Exception e) {
setError("Could not retrieve xpub", e.getMessage());
}
}
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(wallet, importedKeystores);
walletDiscoveryService.setOnSucceeded(event -> {
importedKeystores.keySet().retainAll(walletDiscoveryService.getValue());
EventManager.get().post(new KeystoresDiscoveredEvent(importedKeystores));
});
walletDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
setError("Failed to discover accounts", event.getSource().getException().getMessage());
discoverKeystoresButton.setDisable(false);
});
walletDiscoveryService.start();
});
getXpubsService.setOnFailed(workerStateEvent -> {
setError("Could not retrieve xpub", getXpubsService.getException().getMessage());
discoverKeystoresButton.setDisable(false);
});
setDescription("Discovering...");
showHideLink.setVisible(false);
getXpubsService.start();
}
private void showOperationButton() {
if(deviceOperation.equals(DeviceOperation.IMPORT)) {
if(defaultDevice) {
@@ -642,6 +740,10 @@ public class DevicePane extends TitledDescriptionPane {
signMessageButton.setDefaultButton(defaultDevice);
signMessageButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.DISCOVER_KEYSTORES)) {
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false);
}
}
@@ -689,6 +791,6 @@ public class DevicePane extends TitledDescriptionPane {
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE;
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES;
}
}
@@ -106,7 +106,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> {
setText(address.toString());
setContextMenu(new AddressContextMenu(address, nodeEntry.getOutputDescriptor(), nodeEntry));
Tooltip tooltip = new Tooltip();
tooltip.setText(nodeEntry.getNode().getDerivationPath().replace("m", ".."));
tooltip.setText(nodeEntry.getNode().toString());
setTooltip(tooltip);
getStyleClass().add("address-cell");
@@ -28,7 +28,7 @@ public class KeystorePassphraseDialog extends Dialog<String> {
this.passphrase = (CustomPasswordField) TextFields.createClearablePasswordField();
final DialogPane dialogPane = getDialogPane();
setTitle("Keystore Passphrase" + (walletName != null ? " - " + walletName : ""));
setTitle("Keystore Passphrase" + (walletName != null ? " for " + walletName : ""));
dialogPane.setHeaderText((confirm ? "Re-enter" : "Enter") + " the BIP39 passphrase\n" + (confirm ? "to confirm:" : "for keystore: " + keystore.getLabel()));
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
AppServices.setStageIcon(dialogPane.getScene().getWindow());
@@ -10,12 +10,16 @@ import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
import javafx.animation.FadeTransition;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.util.Duration;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.tools.Platform;
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
public MixStatusCell() {
super();
setAlignment(Pos.CENTER_RIGHT);
@@ -44,9 +48,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
if(mixStatus.getNextMixUtxo() != null) {
setMixSuccess(mixStatus.getNextMixUtxo());
} else if(mixStatus.getMixFailReason() != null) {
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError());
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
} else if(mixStatus.getMixProgress() != null) {
setMixProgress(mixStatus.getMixProgress());
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
} else {
setGraphic(null);
setTooltip(null);
@@ -63,15 +67,30 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
setTooltip(tt);
}
private void setMixFail(MixFailReason mixFailReason, String mixError) {
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
if(mixFailReason != MixFailReason.CANCEL) {
setGraphic(getFailGlyph());
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
if(elapsed >= ERROR_DISPLAY_MILLIS) {
//Old error, don't set again.
return;
}
Glyph failGlyph = getFailGlyph();
setGraphic(failGlyph);
Tooltip tt = new Tooltip();
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
"\nMix failures are generally caused by peers disconnecting during a mix." +
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
setTooltip(tt);
FadeTransition ft = new FadeTransition(Duration.millis(ERROR_DISPLAY_MILLIS - elapsed), failGlyph);
ft.setFromValue(1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS));
ft.setToValue(0.0);
ft.setOnFinished(event -> {
setTooltip(null);
});
ft.play();
} else {
setGraphic(null);
setTooltip(null);
@@ -89,14 +108,28 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
return "system power settings";
}
private void setMixProgress(MixProgress mixProgress) {
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
if(mixProgress.getMixStep() != MixStep.FAIL) {
ProgressIndicator progressIndicator = getProgressIndicator();
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
setGraphic(progressIndicator);
Tooltip tt = new Tooltip();
tt.setText(mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1));
String status = mixProgress.getMixStep().getMessage().substring(0, 1).toUpperCase() + mixProgress.getMixStep().getMessage().substring(1);
tt.setText(status);
setTooltip(tt);
if(mixProgress.getMixStep() == MixStep.REGISTERED_INPUT) {
tt.setOnShowing(event -> {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
Whirlpool.RegisteredInputsService registeredInputsService = new Whirlpool.RegisteredInputsService(whirlpool, mixProgress.getPoolId());
registeredInputsService.setOnSucceeded(eventStateHandler -> {
if(registeredInputsService.getValue() != null) {
tt.setText(status + " (1 of " + registeredInputsService.getValue() + ")");
}
});
registeredInputsService.start();
});
}
} else {
setGraphic(null);
setTooltip(null);
@@ -3,16 +3,16 @@ package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.uri.BitcoinURI;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.Payment;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.drongo.wallet.WalletTransaction;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
@@ -38,6 +38,7 @@ public class TransactionDiagram extends GridPane {
private static final int TOOLTIP_SHOW_DELAY = 50;
private WalletTransaction walletTx;
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
public void update(WalletTransaction walletTx) {
setMinHeight(getDiagramHeight());
@@ -105,7 +106,7 @@ public class TransactionDiagram extends GridPane {
private Map<BlockTransactionHashIndex, WalletNode> getDisplayedUtxos() {
Map<BlockTransactionHashIndex, WalletNode> selectedUtxos = walletTx.getSelectedUtxos();
if(getPayjoinURI() != null) {
if(getPayjoinURI() != null && !selectedUtxos.containsValue(null)) {
selectedUtxos = new LinkedHashMap<>(selectedUtxos);
selectedUtxos.put(new PayjoinBlockTransactionHashIndex(), null);
}
@@ -221,25 +222,36 @@ public class TransactionDiagram extends GridPane {
Tooltip tooltip = new Tooltip();
if(walletNode != null) {
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + walletNode.getDerivationPath().replace("m", "..") + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
tooltip.setText("Spending " + getSatsValue(input.getValue()) + " sats from " + (isFinal() ? walletTx.getWallet().getFullDisplayName() : "") + " " + walletNode + "\n" + input.getHashAsString() + ":" + input.getIndex() + "\n" + walletTx.getWallet().getAddress(walletNode));
tooltip.getStyleClass().add("input-label");
if(input.getLabel() == null || input.getLabel().isEmpty()) {
label.getStyleClass().add("input-label");
}
label.setGraphic(excludeUtxoButton);
label.setContentDisplay(ContentDisplay.LEFT);
if(!isFinal()) {
label.setGraphic(excludeUtxoButton);
label.setContentDisplay(ContentDisplay.LEFT);
}
} else {
if(input instanceof PayjoinBlockTransactionHashIndex) {
tooltip.setText("Added once transaction is signed and sent to the payjoin server");
} else {
AdditionalBlockTransactionHashIndex additionalReference = (AdditionalBlockTransactionHashIndex) input;
} else if(input instanceof AdditionalBlockTransactionHashIndex additionalReference) {
StringJoiner joiner = new StringJoiner("\n");
for(BlockTransactionHashIndex additionalInput : additionalReference.getAdditionalInputs()) {
joiner.add(getInputDescription(additionalInput));
}
tooltip.setText(joiner.toString());
} else {
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)input.getIndex());
Address fromAddress = txOutput.getScript().getToAddress();
tooltip.setText("Input of " + getSatsValue(txOutput.getValue()) + " sats\n" + input.getHashAsString() + ":" + input.getIndex() + (fromAddress != null ? "\n" + fromAddress : ""));
} else {
tooltip.setText(input.getHashAsString() + ":" + input.getIndex());
}
label.getStyleClass().add("input-label");
}
tooltip.getStyleClass().add("input-label");
}
@@ -376,19 +388,23 @@ public class TransactionDiagram extends GridPane {
outputsBox.setAlignment(Pos.CENTER_LEFT);
outputsBox.getChildren().add(createSpacer());
List<OutputNode> outputNodes = new ArrayList<>();
for(Payment payment : displayedPayments) {
Glyph outputGlyph = getOutputGlyph(payment);
boolean labelledPayment = outputGlyph.getStyleClass().stream().anyMatch(style -> List.of("premix-icon", "badbank-icon", "whirlpoolfee-icon").contains(style)) || payment instanceof AdditionalPayment;
String recipientDesc = labelledPayment ? payment.getLabel() : payment.getAddress().toString().substring(0, 8) + "...";
Label recipientLabel = new Label(recipientDesc, outputGlyph);
payment.setLabel(getOutputLabel(payment));
Label recipientLabel = new Label(payment.getLabel() == null || payment.getType() == Payment.Type.FAKE_MIX ? payment.getAddress().toString().substring(0, 8) + "..." : payment.getLabel(), outputGlyph);
recipientLabel.getStyleClass().add("output-label");
recipientLabel.getStyleClass().add(labelledPayment ? "payment-label" : "recipient-label");
Tooltip recipientTooltip = new Tooltip((walletTx.isConsolidationSend(payment) ? "Consolidate " : "Pay ") + getSatsValue(payment.getAmount()) + " sats to " + (payment instanceof AdditionalPayment ? "\n" + payment : payment.getLabel() + "\n" + payment.getAddress().toString()));
Wallet toWallet = getToWallet(payment);
WalletNode toNode = walletTx.getWallet() != null ? walletTx.getWallet().getWalletAddresses().get(payment.getAddress()) : null;
Tooltip recipientTooltip = new Tooltip((toWallet == null ? (toNode != null ? "Consolidate " : "Pay ") : "Receive ")
+ getSatsValue(payment.getAmount()) + " sats to "
+ (payment instanceof AdditionalPayment ? "\n" + payment : (toWallet == null ? (payment.getLabel() == null ? (toNode != null ? toNode : "external address") : payment.getLabel()) : toWallet.getFullDisplayName()) + "\n" + payment.getAddress().toString()));
recipientTooltip.getStyleClass().add("recipient-label");
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
recipientLabel.setTooltip(recipientTooltip);
outputsBox.getChildren().add(recipientLabel);
outputsBox.getChildren().add(createSpacer());
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
}
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
@@ -397,29 +413,41 @@ public class TransactionDiagram extends GridPane {
boolean overGapLimit = (changeNode.getIndex() - defaultChangeNode.getIndex()) > walletTx.getWallet().getGapLimit();
HBox actionBox = new HBox();
String changeDesc = walletTx.getChangeAddress(changeNode).toString().substring(0, 8) + "...";
Address changeAddress = walletTx.getChangeAddress(changeNode);
String changeDesc = changeAddress.toString().substring(0, 8) + "...";
Label changeLabel = new Label(changeDesc, overGapLimit ? getChangeWarningGlyph() : getChangeGlyph());
changeLabel.getStyleClass().addAll("output-label", "change-label");
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode.getDerivationPath().replace("m", "..") + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
Tooltip changeTooltip = new Tooltip("Change of " + getSatsValue(changeEntry.getValue()) + " sats to " + changeNode + "\n" + walletTx.getChangeAddress(changeNode).toString() + (overGapLimit ? "\nAddress is beyond the gap limit!" : ""));
changeTooltip.getStyleClass().add("change-label");
changeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
changeLabel.setTooltip(changeTooltip);
actionBox.getChildren().add(changeLabel);
Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
nextChangeAddressButton.setOnAction(event -> {
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
});
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
replaceChangeLabel.getStyleClass().add("replace-change-label");
replaceChangeLabel.setVisible(false);
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
if(!isFinal()) {
Button nextChangeAddressButton = new Button("");
nextChangeAddressButton.setGraphic(getChangeReplaceGlyph());
nextChangeAddressButton.setOnAction(event -> {
EventManager.get().post(new ReplaceChangeAddressEvent(walletTx));
});
Tooltip replaceChangeTooltip = new Tooltip("Use next change address");
nextChangeAddressButton.setTooltip(replaceChangeTooltip);
Label replaceChangeLabel = new Label("", nextChangeAddressButton);
replaceChangeLabel.getStyleClass().add("replace-change-label");
replaceChangeLabel.setVisible(false);
actionBox.setOnMouseEntered(event -> replaceChangeLabel.setVisible(true));
actionBox.setOnMouseExited(event -> replaceChangeLabel.setVisible(false));
actionBox.getChildren().add(replaceChangeLabel);
}
actionBox.getChildren().addAll(changeLabel, replaceChangeLabel);
outputsBox.getChildren().add(actionBox);
outputNodes.add(new OutputNode(actionBox, changeAddress));
}
if(isFinal()) {
Collections.sort(outputNodes);
}
for(OutputNode outputNode : outputNodes) {
outputsBox.getChildren().add(outputNode.outputLabel);
outputsBox.getChildren().add(createSpacer());
}
@@ -427,7 +455,7 @@ public class TransactionDiagram extends GridPane {
Label feeLabel = highFee ? new Label("High Fee", getWarningGlyph()) : new Label("Fee", getFeeGlyph());
feeLabel.getStyleClass().addAll("output-label", "fee-label");
String percentage = String.format("%.2f", walletTx.getFeePercentage() * 100.0);
Tooltip feeTooltip = new Tooltip("Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
Tooltip feeTooltip = new Tooltip(walletTx.getFee() < 0 ? "Unknown fee" : "Fee of " + getSatsValue(walletTx.getFee()) + " sats (" + percentage + "%)");
feeTooltip.getStyleClass().add("fee-tooltip");
feeTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
feeLabel.setTooltip(feeTooltip);
@@ -445,7 +473,10 @@ public class TransactionDiagram extends GridPane {
String txDesc = "Transaction";
Label txLabel = new Label(txDesc);
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n" + String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes");
boolean isFinalized = walletTx.getTransaction().hasScriptSigs() || walletTx.getTransaction().hasWitnesses();
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n"
+ String.format("%.2f", walletTx.getTransaction().getVirtualSize()) + " vBytes"
+ (walletTx.getFee() < 0 ? "" : "\n" + String.format("%.2f", walletTx.getFee() / walletTx.getTransaction().getVirtualSize()) + " sats/vB" + (isFinalized ? "" : " (non-final)")));
tooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
tooltip.getStyleClass().add("transaction-tooltip");
txLabel.setTooltip(tooltip);
@@ -469,6 +500,37 @@ public class TransactionDiagram extends GridPane {
return spacer;
}
private String getOutputLabel(Payment payment) {
if(payment.getLabel() != null) {
return payment.getLabel();
}
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
return "Whirlpool Fee";
} else if(walletTx.isPremixSend(payment)) {
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
return "Premix #" + premixIndex;
} else if(walletTx.isBadbankSend(payment)) {
return "Badbank Change";
}
return null;
}
private int getOutputIndex(Address address) {
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
}
private Wallet getToWallet(Payment payment) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet != walletTx.getWallet() && openWallet.isValid() && openWallet.isWalletAddress(payment.getAddress())) {
return openWallet;
}
}
return null;
}
public Glyph getOutputGlyph(Payment payment) {
if(payment.getType().equals(Payment.Type.FAKE_MIX)) {
return getFakeMixGlyph();
@@ -482,6 +544,8 @@ public class TransactionDiagram extends GridPane {
return getWhirlpoolFeeGlyph();
} else if(payment instanceof AdditionalPayment) {
return ((AdditionalPayment)payment).getOutputGlyph(this);
} else if(getToWallet(payment) != null) {
return getDepositGlyph();
}
return getPaymentGlyph();
@@ -508,6 +572,13 @@ public class TransactionDiagram extends GridPane {
return consolidationGlyph;
}
public static Glyph getDepositGlyph() {
Glyph depositGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.ARROW_DOWN);
depositGlyph.getStyleClass().add("deposit-icon");
depositGlyph.setFontSize(12);
return depositGlyph;
}
public static Glyph getPremixGlyph() {
Glyph premixGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
premixGlyph.getStyleClass().add("premix-icon");
@@ -589,6 +660,18 @@ public class TransactionDiagram extends GridPane {
return lockGlyph;
}
public boolean isFinal() {
return finalProperty.get();
}
public BooleanProperty finalProperty() {
return finalProperty;
}
public void setFinal(boolean isFinal) {
this.finalProperty.set(isFinal);
}
private static class PayjoinBlockTransactionHashIndex extends BlockTransactionHashIndex {
public PayjoinBlockTransactionHashIndex() {
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
@@ -644,4 +727,23 @@ public class TransactionDiagram extends GridPane {
return additionalPayments.stream().map(payment -> payment.getAddress().toString()).collect(Collectors.joining("\n"));
}
}
private class OutputNode implements Comparable<OutputNode> {
public Node outputLabel;
public Address address;
public OutputNode(Node outputLabel, Address address) {
this.outputLabel = outputLabel;
this.address = address;
}
@Override
public int compareTo(TransactionDiagram.OutputNode o) {
try {
return getOutputIndex(address) - getOutputIndex(o.address);
} catch(Exception e) {
return 0;
}
}
}
}
@@ -42,9 +42,9 @@ public class WalletExportDialog extends Dialog<Wallet> {
List<WalletExport> exporters;
if(wallet.getPolicyType() == PolicyType.SINGLE) {
exporters = List.of(new Electrum(), new SpecterDesktop(), new Sparrow());
exporters = List.of(new Electrum(), new Descriptor(), new SpecterDesktop(), new Sparrow());
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
exporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow());
} else {
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
}
@@ -54,7 +54,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
importAccordion.getPanes().add(importPane);
}
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
List<WalletImport> walletImporters = List.of(new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow());
for(WalletImport importer : walletImporters) {
FileWalletImportPane importPane = new FileWalletImportPane(importer);
importAccordion.getPanes().add(importPane);
@@ -0,0 +1,65 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import javafx.application.Platform;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import org.controlsfx.control.textfield.CustomTextField;
import org.controlsfx.control.textfield.TextFields;
import org.controlsfx.glyphfont.Glyph;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
public class WalletLabelDialog extends Dialog<String> {
private final CustomTextField label;
public WalletLabelDialog(String initialName) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Account Name");
dialogPane.setHeaderText("Enter a name for this account:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(400);
dialogPane.setPrefHeight(200);
AppServices.moveToActiveWindowScreen(this);
Glyph wallet = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WALLET);
wallet.setFontSize(50);
dialogPane.setGraphic(wallet);
final VBox content = new VBox(20);
label = (CustomTextField) TextFields.createClearableTextField();
label.setText(initialName);
label.setTextFormatter(new TextFormatter<>((change) -> {
change.setText(change.getText().replaceAll("[\\\\/:*?\"<>|]", "_"));
return change;
}));
content.getChildren().add(label);
dialogPane.setContent(content);
ValidationSupport validationSupport = new ValidationSupport();
Platform.runLater(() -> {
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
validationSupport.registerValidator(label, Validator.combine(
Validator.createEmptyValidator("Account name is required")
));
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rename Account", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button)dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> label.getText().length() == 0, label.textProperty());
okButton.disableProperty().bind(isInvalid);
label.setPromptText("Account Name");
Platform.runLater(label::requestFocus);
setResultConverter(dialogButton -> dialogButton == okButtonType ? label.getText() : null);
}
}
@@ -0,0 +1,18 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Keystore;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import java.util.Map;
public class KeystoresDiscoveredEvent {
private final Map<StandardAccount, Keystore> discoveredKeystores;
public KeystoresDiscoveredEvent(Map<StandardAccount, Keystore> discoveredKeystores) {
this.discoveredKeystores = discoveredKeystores;
}
public Map<StandardAccount, Keystore> getDiscoveredKeystores() {
return discoveredKeystores;
}
}
@@ -11,6 +11,7 @@ public class SpendUtxoEvent {
private final Wallet wallet;
private final List<BlockTransactionHashIndex> utxos;
private final List<Payment> payments;
private final List<byte[]> opReturns;
private final Long fee;
private final boolean includeSpentMempoolOutputs;
private final Pool pool;
@@ -23,15 +24,17 @@ public class SpendUtxoEvent {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.opReturns = null;
this.fee = fee;
this.includeSpentMempoolOutputs = includeSpentMempoolOutputs;
this.pool = null;
}
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, Long fee, Pool pool) {
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
this.wallet = wallet;
this.utxos = utxos;
this.payments = payments;
this.opReturns = opReturns;
this.fee = fee;
this.includeSpentMempoolOutputs = false;
this.pool = pool;
@@ -49,6 +52,10 @@ public class SpendUtxoEvent {
return payments;
}
public List<byte[]> getOpReturns() {
return opReturns;
}
public Long getFee() {
return fee;
}
@@ -0,0 +1,17 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
/**
* This event is posted if the wallet's gap limit has changed, and triggers a history fetch for the new nodes.
*
*/
public class WalletGapLimitChangedEvent extends WalletChangedEvent {
public WalletGapLimitChangedEvent(Wallet wallet) {
super(wallet);
}
public int getGapLimit() {
return getWallet().getGapLimit();
}
}
@@ -3,15 +3,17 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
private final WalletNode walletNode;
import java.util.Set;
public WalletHistoryStartedEvent(Wallet wallet, WalletNode walletNode) {
public class WalletHistoryStartedEvent extends WalletHistoryStatusEvent {
private final Set<WalletNode> walletNodes;
public WalletHistoryStartedEvent(Wallet wallet, Set<WalletNode> walletNodes) {
super(wallet, true);
this.walletNode = walletNode;
this.walletNodes = walletNodes;
}
public WalletNode getWalletNode() {
return walletNode;
public Set<WalletNode> getWalletNodes() {
return walletNodes;
}
}
@@ -0,0 +1,13 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.wallet.Wallet;
public class WalletLabelChangedEvent extends WalletChangedEvent {
public WalletLabelChangedEvent(Wallet wallet) {
super(wallet);
}
public String getLabel() {
return getWallet().getLabel();
}
}
@@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.google.gson.*;
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
@@ -40,7 +41,7 @@ public class Config {
private boolean openWalletsInNewWindows = false;
private boolean hideEmptyUsedAddresses = false;
private boolean showTransactionHex = true;
private boolean showLoadingLog = false;
private boolean showLoadingLog = true;
private boolean showUtxosChart = true;
private boolean preventSleep = false;
private List<File> recentWalletFiles;
@@ -54,8 +55,6 @@ public class Config {
private CoreAuthType coreAuthType;
private File coreDataDir;
private String coreAuth;
private Boolean coreMultiWallet;
private String coreWallet;
private String electrumServer;
private File electrumServerCert;
private boolean useProxy;
@@ -0,0 +1,80 @@
package com.sparrowwallet.sparrow.io;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletModel;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
public class Descriptor implements WalletImport, WalletExport {
@Override
public String getName() {
return "Output Descriptor";
}
@Override
public WalletModel getWalletModel() {
return WalletModel.BITCOIN_CORE;
}
@Override
public void exportWallet(Wallet wallet, OutputStream outputStream) throws ExportException {
try {
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
String outputDescriptorString = outputDescriptor.toString(true);
outputStream.write(outputDescriptorString.getBytes(StandardCharsets.UTF_8));
outputStream.flush();
} catch(Exception e) {
throw new ExportException("Error writing output descriptor", e);
}
}
@Override
public String getWalletExportDescription() {
return "The output descriptor is a standardized description of the wallet compatible with Bitcoin Core, and can be used to create a watch-only copy using the Edit button on the Settings tab of a new Sparrow wallet.";
}
@Override
public String getExportFileExtension(Wallet wallet) {
return "txt";
}
@Override
public boolean isWalletExportScannable() {
return false;
}
@Override
public boolean walletExportRequiresDecryption() {
return false;
}
@Override
public boolean isEncrypted(File file) {
return false;
}
@Override
public String getWalletImportDescription() {
return "Import a file containing the output descriptor of a wallet. The output descriptor is a standardized description of the wallet compatible with Bitcoin Core.";
}
@Override
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
try {
String outputDescriptor = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)).lines().collect(Collectors.joining("\n"));
OutputDescriptor descriptor = OutputDescriptor.getOutputDescriptor(outputDescriptor.trim());
return descriptor.toWallet();
} catch(Exception e) {
throw new ImportException("Error importing output descriptor", e);
}
}
@Override
public boolean isWalletImportScannable() {
return false;
}
}
@@ -8,6 +8,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.psbt.PSBT;
import com.sparrowwallet.drongo.psbt.PSBTParseException;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.WalletModel;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
@@ -91,6 +92,15 @@ public class Hwi {
}
}
public Map<StandardAccount, String> getXpubs(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) throws ImportException {
Map<StandardAccount, String> accountXpubs = new LinkedHashMap<>();
for(Map.Entry<StandardAccount, String> entry : accountDerivationPaths.entrySet()) {
accountXpubs.put(entry.getKey(), getXpub(device, passphrase, entry.getValue()));
}
return accountXpubs;
}
public String getXpub(Device device, String passphrase, String derivationPath) throws ImportException {
try {
String output;
@@ -580,6 +590,28 @@ public class Hwi {
}
}
public static class GetXpubsService extends Service<Map<StandardAccount, String>> {
private final Device device;
private final String passphrase;
private final Map<StandardAccount, String> accountDerivationPaths;
public GetXpubsService(Device device, String passphrase, Map<StandardAccount, String> accountDerivationPaths) {
this.device = device;
this.passphrase = passphrase;
this.accountDerivationPaths = accountDerivationPaths;
}
@Override
protected Task<Map<StandardAccount, String>> createTask() {
return new Task<>() {
protected Map<StandardAccount, String> call() throws ImportException {
Hwi hwi = new Hwi();
return hwi.getXpubs(device, passphrase, accountDerivationPaths);
}
};
}
}
public static class SignPSBTService extends Service<PSBT> {
private final Device device;
private final String passphrase;
@@ -10,6 +10,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
public interface BlockTransactionDao {
@@ -35,7 +36,8 @@ public interface BlockTransactionDao {
void clear(long wallet);
default void addBlockTransactions(Wallet wallet) {
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : wallet.getTransactions().entrySet()) {
Map<Sha256Hash, BlockTransaction> walletTransactions = new HashMap<>(wallet.getTransactions());
for(Map.Entry<Sha256Hash, BlockTransaction> blkTxEntry : walletTransactions.entrySet()) {
blkTxEntry.getValue().setId(null);
addOrUpdate(wallet, blkTxEntry.getKey(), blkTxEntry.getValue());
}
@@ -236,10 +236,18 @@ public class DbPersistence implements Persistence {
}
}
if(dirtyPersistables.label != null) {
walletDao.updateLabel(wallet.getId(), dirtyPersistables.label.length() > 255 ? dirtyPersistables.label.substring(0, 255) : dirtyPersistables.label);
}
if(dirtyPersistables.blockHeight != null) {
walletDao.updateStoredBlockHeight(wallet.getId(), dirtyPersistables.blockHeight);
}
if(dirtyPersistables.gapLimit != null) {
walletDao.updateGapLimit(wallet.getId(), dirtyPersistables.gapLimit);
}
if(!dirtyPersistables.labelEntries.isEmpty()) {
BlockTransactionDao blockTransactionDao = handle.attach(BlockTransactionDao.class);
WalletNodeDao walletNodeDao = handle.attach(WalletNodeDao.class);
@@ -637,6 +645,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletLabelChanged(WalletLabelChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).label = event.getLabel();
}
}
@Subscribe
public void walletBlockHeightChanged(WalletBlockHeightChangedEvent event) {
if(persistsFor(event.getWallet())) {
@@ -644,6 +659,13 @@ public class DbPersistence implements Persistence {
}
}
@Subscribe
public void walletGapLimitChanged(WalletGapLimitChangedEvent event) {
if(persistsFor(event.getWallet())) {
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).gapLimit = event.getGapLimit();
}
}
@Subscribe
public void walletEntryLabelsChanged(WalletEntryLabelsChangedEvent event) {
if(persistsFor(event.getWallet())) {
@@ -690,7 +712,9 @@ public class DbPersistence implements Persistence {
private static class DirtyPersistables {
public boolean clearHistory;
public final List<WalletNode> historyNodes = new ArrayList<>();
public String label;
public Integer blockHeight = null;
public Integer gapLimit = null;
public final List<Entry> labelEntries = new ArrayList<>();
public final List<BlockTransactionHashIndex> utxoStatuses = new ArrayList<>();
public boolean mixConfig;
@@ -703,7 +727,9 @@ public class DbPersistence implements Persistence {
return "Dirty Persistables" +
"\nClear history:" + clearHistory +
"\nNodes:" + historyNodes +
"\nLabel:" + label +
"\nBlockHeight:" + blockHeight +
"\nGap limit:" + gapLimit +
"\nTx labels:" + labelEntries.stream().filter(entry -> entry instanceof TransactionEntry).map(entry -> ((TransactionEntry)entry).getBlockTransaction().getHash().toString()).collect(Collectors.toList()) +
"\nAddress labels:" + labelEntries.stream().filter(entry -> entry instanceof NodeEntry).map(entry -> ((NodeEntry)entry).getNode().toString() + " " + entry.getLabel()).collect(Collectors.toList()) +
"\nUTXO labels:" + labelEntries.stream().filter(entry -> entry instanceof HashIndexEntry).map(entry -> ((HashIndexEntry)entry).getHashIndex().toString()).collect(Collectors.toList()) +
@@ -8,16 +8,16 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
public interface MixConfigDao {
@SqlQuery("select id, scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
@SqlQuery("select id, scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex from mixConfig where wallet = ?")
@RegisterRowMapper(MixConfigMapper.class)
MixConfig getForWalletId(Long id);
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into mixConfig (scode, mixOnStartup, indexRange, mixToWalletFile, mixToWalletName, minMixes, receiveIndex, changeIndex, wallet) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insertMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
long insertMixConfig(String scode, Boolean mixOnStartup, String indexRange, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet);
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
void updateMixConfig(String scode, Boolean mixOnStartup, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
@SqlUpdate("update mixConfig set scode = ?, mixOnStartup = ?, indexRange = ?, mixToWalletFile = ?, mixToWalletName = ?, minMixes = ?, receiveIndex = ?, changeIndex = ?, wallet = ? where id = ?")
void updateMixConfig(String scode, Boolean mixOnStartup, String indexRange, String mixToWalletFile, String mixToWalletName, Integer minMixes, int receiveIndex, int changeIndex, long wallet, long id);
default void addMixConfig(Wallet wallet) {
if(wallet.getMixConfig() != null) {
@@ -32,10 +32,10 @@ public interface MixConfigDao {
}
if(mixConfig.getId() == null) {
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
long id = insertMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId());
mixConfig.setId(id);
} else {
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
updateMixConfig(mixConfig.getScode(), mixConfig.getMixOnStartup(), mixConfig.getIndexRange(), mixToWalletFile, mixConfig.getMixToWalletName(), mixConfig.getMinMixes(), mixConfig.getReceiveIndex(), mixConfig.getChangeIndex(), wallet.getId(), mixConfig.getId());
}
}
}
@@ -18,6 +18,7 @@ public class MixConfigMapper implements RowMapper<MixConfig> {
mixOnStartup = null;
}
String indexRange = rs.getString("indexRange");
String mixToWalletFile = rs.getString("mixToWalletFile");
String mixToWalletName = rs.getString("mixToWalletName");
@@ -26,7 +27,7 @@ public class MixConfigMapper implements RowMapper<MixConfig> {
minMixes = null;
}
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes, rs.getInt("receiveIndex"), rs.getInt("changeIndex"));
MixConfig mixConfig = new MixConfig(scode, mixOnStartup, indexRange, mixToWalletFile == null ? null : new File(mixToWalletFile), mixToWalletName, minMixes, rs.getInt("receiveIndex"), rs.getInt("changeIndex"));
mixConfig.setId(rs.getLong("id"));
return mixConfig;
}
@@ -36,25 +36,31 @@ public interface WalletDao {
@CreateSqlObject
UtxoMixDataDao createUtxoMixDataDao();
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id")
@RegisterRowMapper(WalletMapper.class)
List<Wallet> loadAllWallets();
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id = 1")
@RegisterRowMapper(WalletMapper.class)
Wallet loadMainWallet();
@SqlQuery("select wallet.id, wallet.name, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
@SqlQuery("select wallet.id, wallet.name, wallet.label, wallet.network, wallet.policyType, wallet.scriptType, wallet.storedBlockHeight, wallet.gapLimit, wallet.birthDate, policy.id, policy.name, policy.script from wallet left join policy on wallet.defaultPolicy = policy.id where wallet.id != 1")
@RegisterRowMapper(WalletMapper.class)
List<Wallet> loadChildWallets();
@SqlUpdate("insert into wallet (name, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?)")
@SqlUpdate("insert into wallet (name, label, network, policyType, scriptType, storedBlockHeight, gapLimit, birthDate, defaultPolicy) values (?, ?, ?, ?, ?, ?, ?, ?, ?)")
@GetGeneratedKeys("id")
long insert(String name, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
long insert(String name, String label, int network, int policyType, int scriptType, Integer storedBlockHeight, Integer gapLimit, Date birthDate, long defaultPolicy);
@SqlUpdate("update wallet set label = :label where id = :id")
void updateLabel(@Bind("id") long id, @Bind("label") String label);
@SqlUpdate("update wallet set storedBlockHeight = :blockHeight where id = :id")
void updateStoredBlockHeight(@Bind("id") long id, @Bind("blockHeight") Integer blockHeight);
@SqlUpdate("update wallet set gapLimit = :gapLimit where id = :id")
void updateGapLimit(@Bind("id") long id, @Bind("gapLimit") Integer gapLimit);
@SqlUpdate("set schema ?")
int setSchema(String schema);
@@ -105,7 +111,7 @@ public interface WalletDao {
setSchema(schema);
createPolicyDao().addPolicy(wallet.getDefaultPolicy());
long id = insert(truncate(wallet.getName()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
long id = insert(truncate(wallet.getName()), truncate(wallet.getLabel()), wallet.getNetwork().ordinal(), wallet.getPolicyType().ordinal(), wallet.getScriptType().ordinal(), wallet.getStoredBlockHeight(), wallet.gapLimit(), wallet.getBirthDate(), wallet.getDefaultPolicy().getId());
wallet.setId(id);
createKeystoreDao().addKeystores(wallet);
@@ -17,6 +17,7 @@ public class WalletMapper implements RowMapper<Wallet> {
public Wallet map(ResultSet rs, StatementContext ctx) throws SQLException {
Wallet wallet = new Wallet(rs.getString("wallet.name"));
wallet.setId(rs.getLong("wallet.id"));
wallet.setLabel(rs.getString("wallet.label"));
wallet.setNetwork(Network.values()[rs.getInt("wallet.network")]);
wallet.setPolicyType(PolicyType.values()[rs.getInt("wallet.policyType")]);
wallet.setScriptType(ScriptType.values()[rs.getInt("wallet.scriptType")]);
@@ -11,6 +11,7 @@ import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.jdbi.v3.sqlobject.statement.UseRowReducer;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@@ -60,7 +61,8 @@ public interface WalletNodeDao {
for(WalletNode purposeNode : wallet.getPurposeNodes()) {
long purposeNodeId = insertWalletNode(purposeNode.getDerivationPath(), truncate(purposeNode.getLabel()), wallet.getId(), null);
purposeNode.setId(purposeNodeId);
for(WalletNode addressNode : purposeNode.getChildren()) {
List<WalletNode> childNodes = new ArrayList<>(purposeNode.getChildren());
for(WalletNode addressNode : childNodes) {
long addressNodeId = insertWalletNode(addressNode.getDerivationPath(), truncate(addressNode.getLabel()), wallet.getId(), purposeNodeId);
addressNode.setId(addressNodeId);
addTransactionOutputs(addressNode);
@@ -5,8 +5,6 @@ import com.github.arteam.simplejsonrpc.client.Transport;
import com.github.arteam.simplejsonrpc.client.builder.BatchRequestBuilder;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcBatchException;
import com.github.arteam.simplejsonrpc.client.exception.JsonRpcException;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
@@ -19,6 +17,8 @@ import java.util.*;
import java.util.concurrent.atomic.AtomicLong;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
public class BatchedElectrumServerRpc implements ElectrumServerRpc {
private static final Logger log = LoggerFactory.getLogger(BatchedElectrumServerRpc.class);
private static final int MAX_RETRIES = 5;
@@ -75,7 +75,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, ScriptHashTx[]> batchRequest = client.createBatchRequest().keysType(String.class).returnType(ScriptHashTx[].class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
@@ -85,7 +85,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
} catch (JsonRpcBatchException e) {
if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
}
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
@@ -95,7 +95,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return result;
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
throw new ElectrumServerRpcException("Failed to retrieve transaction history for paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
}
}
@@ -113,7 +113,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return new RetryLogic<Map<String, ScriptHashTx[]>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
} catch(JsonRpcBatchException e) {
if(failOnError) {
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
}
Map<String, ScriptHashTx[]> result = (Map<String, ScriptHashTx[]>)e.getSuccesses();
@@ -123,7 +123,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return result;
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
throw new ElectrumServerRpcException("Failed to retrieve mempool transactions for paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
}
}
@@ -132,7 +132,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
JsonRpcClient client = new JsonRpcClient(transport);
BatchRequestBuilder<String, String> batchRequest = client.createBatchRequest().keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + getScriptHashesAbbreviation(pathScriptHashes.keySet())));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
@@ -142,9 +142,9 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
return new RetryLogic<Map<String, String>>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(batchRequest::execute);
} catch(JsonRpcBatchException e) {
//Even if we have some successes, failure to subscribe for all script hashes will result in outdated wallet view. Don't proceed.
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation((Collection<String>)e.getErrors().keySet()), e);
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString((Collection<String>)e.getErrors().keySet()), e);
} catch(Exception e) {
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + getScriptHashesAbbreviation(pathScriptHashes.keySet()), e);
throw new ElectrumServerRpcException("Failed to subscribe to paths: " + nodeRangesToString(pathScriptHashes.keySet()), e);
}
}
@@ -276,65 +276,4 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
throw new ElectrumServerRpcException("Error broadcasting transaction", e);
}
}
private static String getScriptHashesAbbreviation(Collection<String> scriptHashes) {
List<String> sortedHashes = new ArrayList<>(scriptHashes);
if(scriptHashes.isEmpty()) {
return "[]";
}
List<List<String>> contiguous = splitToContiguous(sortedHashes);
String abbrev = "[";
for(Iterator<List<String>> iter = contiguous.iterator(); iter.hasNext(); ) {
List<String> range = iter.next();
abbrev += range.get(0);
if(range.size() > 1) {
abbrev += "-" + range.get(range.size() - 1);
}
if(iter.hasNext()) {
abbrev += ", ";
}
}
abbrev += "]";
return abbrev;
}
static List<List<String>> splitToContiguous(List<String> input) {
List<List<String>> result = new ArrayList<>();
int prev = 0;
int keyPurpose = getKeyPurpose(input.get(0));
int index = getIndex(input.get(0));
for (int cur = 0; cur < input.size(); cur++) {
if(getKeyPurpose(input.get(cur)) != keyPurpose || getIndex(input.get(cur)) != index) {
result.add(input.subList(prev, cur));
prev = cur;
}
index = getIndex(input.get(cur)) + 1;
keyPurpose = getKeyPurpose(input.get(cur));
}
result.add(input.subList(prev, input.size()));
return result;
}
private static int getKeyPurpose(String path) {
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
if(childNumbers.isEmpty()) {
return -1;
}
return childNumbers.get(0).num();
}
private static int getIndex(String path) {
List<ChildNumber> childNumbers = KeyDerivation.parsePath(path);
if(childNumbers.isEmpty()) {
return -1;
}
return childNumbers.get(childNumbers.size() - 1).num();
}
}
@@ -1415,4 +1415,48 @@ public class ElectrumServer {
};
}
}
public static class WalletDiscoveryService extends Service<List<StandardAccount>> {
private final Wallet masterWalletCopy;
private final List<StandardAccount> standardAccounts;
private final Map<StandardAccount, Keystore> importedKeystores;
public WalletDiscoveryService(Wallet masterWallet, List<StandardAccount> standardAccounts) {
this.masterWalletCopy = masterWallet.copy();
this.standardAccounts = standardAccounts;
this.importedKeystores = new HashMap<>();
}
public WalletDiscoveryService(Wallet masterWallet, Map<StandardAccount, Keystore> importedKeystores) {
this.masterWalletCopy = masterWallet.copy();
this.standardAccounts = new ArrayList<>(importedKeystores.keySet());
this.importedKeystores = importedKeystores;
}
@Override
protected Task<List<StandardAccount>> createTask() {
return new Task<>() {
protected List<StandardAccount> call() throws ServerException {
ElectrumServer electrumServer = new ElectrumServer();
List<StandardAccount> discoveredAccounts = new ArrayList<>();
for(StandardAccount standardAccount : standardAccounts) {
Wallet wallet = masterWalletCopy.addChildWallet(standardAccount);
if(importedKeystores.containsKey(standardAccount)) {
wallet.getKeystores().clear();
wallet.getKeystores().add(importedKeystores.get(standardAccount));
}
Map<WalletNode, Set<BlockTransactionHash>> nodeTransactionMap = new TreeMap<>();
electrumServer.getReferences(wallet, wallet.getNode(KeyPurpose.RECEIVE).getChildren(), nodeTransactionMap, 0);
if(nodeTransactionMap.values().stream().anyMatch(blockTransactionHashes -> !blockTransactionHashes.isEmpty())) {
discoveredAccounts.add(standardAccount);
}
}
return discoveredAccounts;
}
};
}
}
}
@@ -204,6 +204,7 @@ public class ServerPreferencesController extends PreferencesDetailController {
corePort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
electrumPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
proxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
coreProxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
publicProxyPort.setTextFormatter(new TextFieldValidator(TextFieldValidator.ValidationModus.MAX_INTEGERS, 5).getFormatter());
coreHost.textProperty().addListener(getBitcoinCoreListener(config));
@@ -66,6 +66,9 @@ public class HeadersController extends TransactionFormController implements Init
@FXML
private IdLabel id;
@FXML
private TransactionDiagram transactionDiagram;
@FXML
private Spinner<Integer> version;
@@ -347,6 +350,8 @@ public class HeadersController extends TransactionFormController implements Init
updateFee(feeAmt);
}
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
blockchainForm.managedProperty().bind(blockchainForm.visibleProperty());
signingWalletForm.managedProperty().bind(signingWalletForm.visibleProperty());
@@ -489,6 +494,94 @@ public class HeadersController extends TransactionFormController implements Init
feeRate.setText(String.format("%.2f", feeRateAmt) + " sats/vB" + (headersForm.isTransactionFinalized() ? "" : " (non-final)"));
}
private WalletTransaction getWalletTransaction(Map<Sha256Hash, BlockTransaction> inputTransactions) {
Wallet wallet = getWalletFromTransactionInputs();
if(wallet != null) {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = new LinkedHashMap<>();
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
BlockTransactionHashIndex selectedTxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
.findFirst().orElse(new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0));
selectedTxos.put(selectedTxo, walletTxos.get(selectedTxo));
}
List<Payment> payments = new ArrayList<>();
Map<WalletNode, Long> changeMap = new LinkedHashMap<>();
Map<Script, WalletNode> changeOutputScripts = wallet.getWalletOutputScripts(KeyPurpose.CHANGE);
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
WalletNode changeNode = changeOutputScripts.get(txOutput.getScript());
if(changeNode != null) {
if(headersForm.getTransaction().getOutputs().size() == 4 && headersForm.getTransaction().getOutputs().stream().anyMatch(txo -> txo != txOutput && txo.getValue() == txOutput.getValue())) {
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], ".." + changeNode + " (Fake Mix)", txOutput.getValue(), false, Payment.Type.FAKE_MIX));
} catch(Exception e) {
//ignore
}
} else {
changeMap.put(changeNode, txOutput.getValue());
}
} else {
Payment.Type paymentType = Payment.Type.DEFAULT;
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
Wallet premixWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_PREMIX);
if(premixWallet != null && headersForm.getTransaction().getOutputs().stream().anyMatch(premixWallet::isWalletTxo) && txOutput.getIndex() == 1) {
paymentType = Payment.Type.WHIRLPOOL_FEE;
}
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType));
} catch(Exception e) {
//ignore
}
}
}
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, changeMap, fee.getValue(), inputTransactions);
} else {
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
.collect(Collectors.toMap(txInput -> {
if(inputTransactions != null) {
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
if(blockTransaction != null) {
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
}
}
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
},
txInput -> new WalletNode("m/0"),
(u, v) -> { throw new IllegalStateException("Duplicate TXOs"); },
LinkedHashMap::new));
selectedTxos.entrySet().forEach(entry -> entry.setValue(null));
List<Payment> payments = new ArrayList<>();
for(TransactionOutput txOutput : headersForm.getTransaction().getOutputs()) {
try {
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], null, txOutput.getValue(), false));
} catch(Exception e) {
//ignore
}
}
return new WalletTransaction(null, headersForm.getTransaction(), Collections.emptyList(), selectedTxos, payments, Collections.emptyMap(), fee.getValue(), inputTransactions);
}
}
private Wallet getWalletFromTransactionInputs() {
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
if(openWallet.isWalletTxo(txInput)) {
return openWallet;
}
}
}
return null;
}
private void updateBlockchainForm(BlockTransaction blockTransaction, Integer currentHeight) {
signaturesForm.setVisible(false);
blockchainForm.setVisible(true);
@@ -986,6 +1079,7 @@ public class HeadersController extends TransactionFormController implements Init
if(feeAmt != null) {
updateFee(feeAmt);
}
transactionDiagram.update(getWalletTransaction(event.getInputTransactions()));
}
}
@@ -1120,6 +1214,7 @@ public class HeadersController extends TransactionFormController implements Init
updateType();
updateSize();
updateFee(headersForm.getPsbt().getFee());
transactionDiagram.update(getWalletTransaction(headersForm.getInputTransactions()));
}
}
@@ -146,7 +146,7 @@ public class InputController extends TransactionFormController implements Initia
String baseText = getLegendText(txInput);
if(signingWallet != null) {
if(inputForm.isWalletTxo()) {
inputFieldset.setText(baseText + " - " + signingWallet.getFullName());
inputFieldset.setText(baseText + " from " + signingWallet.getFullDisplayName());
inputFieldset.setIcon(TransactionDiagram.getTxoGlyph());
} else {
inputFieldset.setText(baseText + " - Payjoin");
@@ -1,5 +1,7 @@
package com.sparrowwallet.sparrow.wallet;
import com.samourai.whirlpool.client.wallet.beans.IndexRange;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.wallet.MixConfig;
import com.sparrowwallet.drongo.wallet.StandardAccount;
import com.sparrowwallet.drongo.wallet.Wallet;
@@ -13,6 +15,7 @@ import javafx.fxml.Initializable;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.util.StringConverter;
import java.net.URL;
import java.util.ArrayList;
@@ -30,6 +33,9 @@ public class MixToController implements Initializable {
@FXML
private Spinner<Integer> minMixes;
@FXML
private ComboBox<IndexRange> indexRange;
private MixConfig mixConfig;
@Override
@@ -44,6 +50,7 @@ public class MixToController implements Initializable {
allWallets.add(NONE_WALLET);
List<Wallet> destinationWallets = AppServices.get().getOpenWallets().keySet().stream().filter(openWallet -> openWallet.isValid()
&& (openWallet.getScriptType() == ScriptType.P2WPKH || openWallet.getScriptType() == ScriptType.P2WSH || openWallet.getScriptType() == ScriptType.P2TR)
&& openWallet != wallet && openWallet != wallet.getMasterWallet()
&& (openWallet.getStandardAccountType() == null || !StandardAccount.WHIRLPOOL_ACCOUNTS.contains(openWallet.getStandardAccountType()))).collect(Collectors.toList());
allWallets.addAll(destinationWallets);
@@ -81,6 +88,35 @@ public class MixToController implements Initializable {
mixConfig.setMinMixes(newValue);
EventManager.get().post(new MixToConfigChangedEvent(wallet));
});
indexRange.setConverter(new StringConverter<>() {
@Override
public String toString(IndexRange indexRange) {
if(indexRange == null) {
return "";
}
return indexRange.toString().charAt(0) + indexRange.toString().substring(1).toLowerCase();
}
@Override
public IndexRange fromString(String string) {
return null;
}
});
indexRange.setValue(IndexRange.FULL);
if(mixConfig.getIndexRange() != null) {
try {
indexRange.setValue(IndexRange.valueOf(mixConfig.getIndexRange()));
} catch(Exception e) {
//ignore
}
}
indexRange.valueProperty().addListener((observable, oldValue, newValue) -> {
mixConfig.setIndexRange(newValue.toString());
EventManager.get().post(new MixToConfigChangedEvent(wallet));
});
}
public MixConfig getMixConfig() {
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.wallet;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.address.P2PKHAddress;
@@ -16,6 +17,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.BitcoinUnitChangedEvent;
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
import com.sparrowwallet.sparrow.event.FiatCurrencySelectedEvent;
import com.sparrowwallet.sparrow.event.OpenWalletsEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ExchangeSource;
import javafx.application.Platform;
@@ -23,11 +25,13 @@ import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.util.StringConverter;
import org.controlsfx.validation.ValidationResult;
import org.controlsfx.validation.ValidationSupport;
import org.controlsfx.validation.Validator;
@@ -38,6 +42,7 @@ import java.net.URL;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
@@ -49,7 +54,10 @@ public class PaymentController extends WalletFormController implements Initializ
private ValidationSupport validationSupport;
@FXML
private CopyableTextField address;
private ComboBox<Wallet> openWallets;
@FXML
private ComboBoxTextField address;
@FXML
private TextField label;
@@ -121,6 +129,27 @@ public class PaymentController extends WalletFormController implements Initializ
@Override
public void initializeView() {
openWallets.setConverter(new StringConverter<>() {
@Override
public String toString(Wallet wallet) {
return wallet == null ? "" : wallet.getFullDisplayName() + (wallet == sendController.getWalletForm().getWallet() ? " (Consolidation)" : "");
}
@Override
public Wallet fromString(String string) {
return null;
}
});
openWallets.setItems(FXCollections.observableList(AppServices.get().getOpenWallets().keySet().stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList())));
openWallets.prefWidthProperty().bind(address.widthProperty());
openWallets.valueProperty().addListener((observable, oldValue, newValue) -> {
if(newValue != null) {
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = newValue.getAddress(freshNode);
address.setText(freshAddress.toString());
}
});
address.textProperty().addListener((observable, oldValue, newValue) -> {
try {
BitcoinURI bitcoinURI = new BitcoinURI(newValue);
@@ -443,4 +472,9 @@ public class PaymentController extends WalletFormController implements Initializ
public void exchangeRatesUpdated(ExchangeRatesUpdatedEvent event) {
setFiatAmount(event.getCurrencyRate(), getRecipientValueSats());
}
@Subscribe
public void openWallets(OpenWalletsEvent event) {
openWallets.setItems(FXCollections.observableList(event.getWallets().stream().filter(wallet -> wallet.isValid() && !wallet.isWhirlpoolChildWallet()).collect(Collectors.toList())));
}
}
@@ -4,7 +4,6 @@ import com.google.common.eventbus.Subscribe;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
@@ -162,6 +161,8 @@ public class SendController extends WalletFormController implements Initializabl
private final BooleanProperty includeSpentMempoolOutputsProperty = new SimpleBooleanProperty(false);
private final List<byte[]> opReturnsList = new ArrayList<>();
private final Set<WalletNode> excludedChangeNodes = new HashSet<>();
private final ChangeListener<String> feeListener = new ChangeListener<>() {
@@ -556,7 +557,7 @@ public class SendController extends WalletFormController implements Initializabl
boolean includeMempoolOutputs = Config.get().isIncludeMempoolOutputs();
boolean includeSpentMempoolOutputs = includeSpentMempoolOutputsProperty.get();
walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(payments), getUtxoFilters(), payments, excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
walletTransactionService = new WalletTransactionService(wallet, getUtxoSelectors(payments), getUtxoFilters(), payments, opReturnsList, excludedChangeNodes, feeRate, getMinimumFeeRate(), userFee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
walletTransactionService.setOnSucceeded(event -> {
if(!walletTransactionService.isIgnoreResult()) {
walletTransactionProperty.setValue(walletTransactionService.getValue());
@@ -616,6 +617,7 @@ public class SendController extends WalletFormController implements Initializabl
private final List<UtxoSelector> utxoSelectors;
private final List<UtxoFilter> utxoFilters;
private final List<Payment> payments;
private final List<byte[]> opReturns;
private final Set<WalletNode> excludedChangeNodes;
private final double feeRate;
private final double longTermFeeRate;
@@ -626,11 +628,12 @@ public class SendController extends WalletFormController implements Initializabl
private final boolean includeSpentMempoolOutputs;
private boolean ignoreResult;
public WalletTransactionService(Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
public WalletTransactionService(Wallet wallet, List<UtxoSelector> utxoSelectors, List<UtxoFilter> utxoFilters, List<Payment> payments, List<byte[]> opReturns, Set<WalletNode> excludedChangeNodes, double feeRate, double longTermFeeRate, Long fee, Integer currentBlockHeight, boolean groupByAddress, boolean includeMempoolOutputs, boolean includeSpentMempoolOutputs) {
this.wallet = wallet;
this.utxoSelectors = utxoSelectors;
this.utxoFilters = utxoFilters;
this.payments = payments;
this.opReturns = opReturns;
this.excludedChangeNodes = excludedChangeNodes;
this.feeRate = feeRate;
this.longTermFeeRate = longTermFeeRate;
@@ -645,7 +648,7 @@ public class SendController extends WalletFormController implements Initializabl
protected Task<WalletTransaction> createTask() {
return new Task<>() {
protected WalletTransaction call() throws InsufficientFundsException {
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
return wallet.createWalletTransaction(utxoSelectors, utxoFilters, payments, opReturns, excludedChangeNodes, feeRate, longTermFeeRate, fee, currentBlockHeight, groupByAddress, includeMempoolOutputs, includeSpentMempoolOutputs);
}
};
}
@@ -662,10 +665,10 @@ public class SendController extends WalletFormController implements Initializabl
private List<UtxoFilter> getUtxoFilters() {
UtxoFilter utxoFilter = utxoFilterProperty.get();
if(utxoFilter != null) {
return List.of(utxoFilter, new FrozenUtxoFilter());
return List.of(utxoFilter, new FrozenUtxoFilter(), new CoinbaseUtxoFilter(getWalletForm().getWallet()));
}
return List.of(new FrozenUtxoFilter());
return List.of(new FrozenUtxoFilter(), new CoinbaseUtxoFilter(getWalletForm().getWallet()));
}
private void updateFeeRateSelection(FeeRatesSelection feeRatesSelection) {
@@ -1014,6 +1017,7 @@ public class SendController extends WalletFormController implements Initializabl
utxoSelectorProperty.setValue(null);
utxoFilterProperty.setValue(null);
includeSpentMempoolOutputsProperty.set(false);
opReturnsList.clear();
excludedChangeNodes.clear();
walletTransactionProperty.setValue(null);
createdWalletTransactionProperty.set(null);
@@ -1145,6 +1149,7 @@ public class SendController extends WalletFormController implements Initializabl
AppServices.showErrorDialog("Error broadcasting premix transaction", exception.getMessage());
});
ServiceProgressDialog progressDialog = new ServiceProgressDialog("Whirlpool", "Broadcast Premix Transaction", "/image/whirlpool.png", tx0BroadcastService);
AppServices.moveToActiveWindowScreen(progressDialog);
tx0BroadcastService.start();
}
@@ -1238,6 +1243,10 @@ public class SendController extends WalletFormController implements Initializabl
setPayments(List.of(payment));
}
if(event.getOpReturns() != null) {
opReturnsList.addAll(event.getOpReturns());
}
if(event.getFee() != null) {
setFeeValueSats(event.getFee());
userFeeSet.set(true);
@@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.io.StorageException;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
@@ -429,7 +430,8 @@ public class SettingsController extends WalletFormController implements Initiali
throw new IllegalStateException("Cannot export unsaved wallet");
}
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream().filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile())).map(Map.Entry::getKey).findFirst();
Optional<Wallet> optWallet = AppServices.get().getOpenWallets().entrySet().stream()
.filter(entry -> walletForm.getWalletFile().equals(entry.getValue().getWalletFile()) && entry.getKey().isMasterWallet()).map(Map.Entry::getKey).findFirst();
if(optWallet.isPresent()) {
Wallet wallet = optWallet.get();
if(!walletForm.getWallet().getName().equals(wallet.getName())) {
@@ -437,7 +439,7 @@ public class SettingsController extends WalletFormController implements Initiali
}
if(wallet == null) {
throw new IllegalStateException("Cannot find child wallet " + walletForm.getWallet().getFullName() + " to export");
throw new IllegalStateException("Cannot find child wallet " + walletForm.getWallet().getFullDisplayName() + " to export");
}
WalletExportDialog dlg = new WalletExportDialog(wallet);
@@ -452,58 +454,117 @@ public class SettingsController extends WalletFormController implements Initiali
Wallet masterWallet = openWallet.isMasterWallet() ? openWallet : openWallet.getMasterWallet();
AddAccountDialog addAccountDialog = new AddAccountDialog(masterWallet);
Optional<StandardAccount> optAccount = addAccountDialog.showAndWait();
if(optAccount.isPresent()) {
StandardAccount standardAccount = optAccount.get();
Optional<List<StandardAccount>> optAccounts = addAccountDialog.showAndWait();
if(optAccounts.isPresent()) {
List<StandardAccount> standardAccounts = optAccounts.get();
if(addAccountDialog.isDiscoverAccounts() && !AppServices.isConnected()) {
return;
}
if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) {
if(masterWallet.isEncrypted()) {
String walletId = walletForm.getWalletId();
WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
masterWallet.decrypt(key);
addAccounts(masterWallet, standardAccounts, addAccountDialog.isDiscoverAccounts());
}
}
try {
addAndSaveAccount(masterWallet, standardAccount);
} finally {
masterWallet.encrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isEncrypted()) {
childWallet.encrypt(key);
}
}
key.clear();
encryptionFullKey.clear();
password.get().clear();
private void addAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts, boolean discoverAccounts) {
if(masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)) {
if(masterWallet.isEncrypted()) {
String walletId = walletForm.getWalletId();
WalletPasswordDialog dlg = new WalletPasswordDialog(masterWallet.getName(), WalletPasswordDialog.PasswordRequirement.LOAD);
Optional<SecureString> password = dlg.showAndWait();
if(password.isPresent()) {
Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(walletForm.getStorage(), password.get(), true);
keyDerivationService.setOnSucceeded(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Done"));
ECKey encryptionFullKey = keyDerivationService.getValue();
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), walletForm.getStorage().getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
encryptionFullKey.clear();
masterWallet.decrypt(key);
if(discoverAccounts) {
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts);
walletDiscoveryService.setOnSucceeded(event -> {
addAndEncryptAccounts(masterWallet, walletDiscoveryService.getValue(), key);
});
walletDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
addAndEncryptAccounts(masterWallet, Collections.emptyList(), key);
AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage());
});
walletDiscoveryService.start();
} else {
addAndEncryptAccounts(masterWallet, standardAccounts, key);
}
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> addAccount(null));
}
});
keyDerivationService.setOnFailed(workerStateEvent -> {
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.END, "Failed"));
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
Platform.runLater(() -> addAccount(null));
}
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
addAndSaveAccount(masterWallet, standardAccount);
} else {
log.error("Error deriving wallet key", keyDerivationService.getException());
}
});
EventManager.get().post(new StorageEvent(walletId, TimedEvent.Action.START, "Decrypting wallet..."));
keyDerivationService.start();
}
} else {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
if(discoverAccounts) {
ElectrumServer.WalletDiscoveryService walletDiscoveryService = new ElectrumServer.WalletDiscoveryService(masterWallet, standardAccounts);
walletDiscoveryService.setOnSucceeded(event -> {
addAndSaveAccounts(masterWallet, walletDiscoveryService.getValue());
});
walletDiscoveryService.setOnFailed(event -> {
log.error("Failed to discover accounts", event.getSource().getException());
AppServices.showErrorDialog("Failed to discover accounts", event.getSource().getException().getMessage());
});
walletDiscoveryService.start();
} else {
addAndSaveAccounts(masterWallet, standardAccounts);
}
}
} else {
if(discoverAccounts && masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)) {
String fingerprint = masterWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
DeviceKeystoreDiscoverDialog deviceKeystoreDiscoverDialog = new DeviceKeystoreDiscoverDialog(List.of(fingerprint), masterWallet, standardAccounts);
Optional<Map<StandardAccount, Keystore>> optDiscoveredKeystores = deviceKeystoreDiscoverDialog.showAndWait();
if(optDiscoveredKeystores.isPresent()) {
Map<StandardAccount, Keystore> discoveredKeystores = optDiscoveredKeystores.get();
for(Map.Entry<StandardAccount, Keystore> entry : discoveredKeystores.entrySet()) {
Wallet childWallet = masterWallet.addChildWallet(entry.getKey());
childWallet.getKeystores().clear();
childWallet.getKeystores().add(entry.getValue());
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
}
saveChildWallets(masterWallet);
}
} else {
for(StandardAccount standardAccount : standardAccounts) {
Wallet childWallet = masterWallet.addChildWallet(standardAccount);
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
}
}
}
}
private void addAndEncryptAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts, Key key) {
try {
addAndSaveAccounts(masterWallet, standardAccounts);
} finally {
masterWallet.encrypt(key);
for(Wallet childWallet : masterWallet.getChildWallets()) {
if(!childWallet.isEncrypted()) {
childWallet.encrypt(key);
}
}
key.clear();
}
}
private void addAndSaveAccounts(Wallet masterWallet, List<StandardAccount> standardAccounts) {
for(StandardAccount standardAccount : standardAccounts) {
addAndSaveAccount(masterWallet, standardAccount);
}
}
@@ -515,12 +576,17 @@ public class SettingsController extends WalletFormController implements Initiali
EventManager.get().post(new ChildWalletAddedEvent(getWalletForm().getStorage(), masterWallet, childWallet));
}
saveChildWallets(masterWallet);
}
private void saveChildWallets(Wallet masterWallet) {
for(Wallet childWallet : masterWallet.getChildWallets()) {
Storage storage = AppServices.get().getOpenWallets().get(childWallet);
if(!storage.isPersisted(childWallet)) {
try {
storage.saveWallet(childWallet);
} catch(Exception e) {
log.error("Error saving wallet", e);
AppServices.showErrorDialog("Error saving wallet " + childWallet.getName(), e.getMessage());
}
}
@@ -102,7 +102,7 @@ public class TransactionsController extends WalletFormController implements Init
});
transactionsMasterDetail.setShowDetailNode(Config.get().isShowLoadingLog());
loadingLog.appendText("Wallet loading history for " + getWalletForm().getWallet().getFullName());
loadingLog.appendText("Wallet loading history for " + getWalletForm().getWallet().getFullDisplayName());
loadingLog.setEditable(false);
}
@@ -4,6 +4,7 @@ import com.samourai.whirlpool.client.mix.listener.MixFailReason;
import com.samourai.whirlpool.client.mix.listener.MixStep;
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
import com.samourai.whirlpool.protocol.beans.Utxo;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.AppServices;
@@ -127,6 +128,7 @@ public class UtxoEntry extends HashIndexEntry {
private Utxo nextMixUtxo;
private MixFailReason mixFailReason;
private String mixError;
private Long mixErrorTimestamp;
public MixStatus(MixProgress mixProgress) {
this.mixProgress = mixProgress;
@@ -139,6 +141,7 @@ public class UtxoEntry extends HashIndexEntry {
public MixStatus(MixFailReason mixFailReason, String mixError) {
this.mixFailReason = mixFailReason;
this.mixError = mixError;
this.mixErrorTimestamp = System.currentTimeMillis();
}
public UtxoEntry getUtxoEntry() {
@@ -151,12 +154,12 @@ public class UtxoEntry extends HashIndexEntry {
return wallet.getUtxoMixData(getHashIndex());
}
//Mix data not available - recount (and store if WhirlpoolWallet is running)
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(wallet);
if(whirlpool != null) {
UtxoMixData utxoMixData = whirlpool.getMixData(getHashIndex());
if(utxoMixData != null) {
return utxoMixData;
}
if(whirlpool != null && getUtxoEntry().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX && node.getKeyPurpose() == KeyPurpose.RECEIVE) {
int mixesDone = whirlpool.recountMixesDone(getUtxoEntry().getWallet(), getHashIndex());
whirlpool.setMixesDone(getHashIndex(), mixesDone);
return new UtxoMixData(mixesDone, null);
}
return new UtxoMixData(getUtxoEntry().getWallet().getStandardAccountType() == StandardAccount.WHIRLPOOL_POSTMIX ? 1 : 0, null);
@@ -181,5 +184,9 @@ public class UtxoEntry extends HashIndexEntry {
public String getMixError() {
return mixError;
}
public Long getMixErrorTimestamp() {
return mixErrorTimestamp;
}
}
}
@@ -5,7 +5,6 @@ import com.google.common.eventbus.Subscribe;
import com.samourai.whirlpool.client.tx0.Tx0Preview;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.SecureString;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
@@ -135,7 +134,13 @@ public class UtxosController extends WalletFormController implements Initializab
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
if(whirlpool != null) {
stopMix.visibleProperty().bind(whirlpool.mixingProperty());
if(whirlpool.startingProperty().getValue()) {
mixingStartingListener.changed(whirlpool.startingProperty(), null, whirlpool.startingProperty().getValue());
}
whirlpool.startingProperty().addListener(new WeakChangeListener<>(mixingStartingListener));
if(whirlpool.stoppingProperty().getValue()) {
mixingStoppingListener.changed(whirlpool.stoppingProperty(), null, whirlpool.stoppingProperty().getValue());
}
whirlpool.stoppingProperty().addListener(new WeakChangeListener<>(mixingStoppingListener));
whirlpool.mixingProperty().addListener(new WeakChangeListener<>(mixingListener));
updateMixToButton();
@@ -224,7 +229,7 @@ public class UtxosController extends WalletFormController implements Initializab
public void mixSelected(ActionEvent event) {
List<UtxoEntry> selectedEntries = getSelectedUtxos();
WhirlpoolDialog whirlpoolDialog = new WhirlpoolDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), selectedEntries);
WhirlpoolDialog whirlpoolDialog = new WhirlpoolDialog(getWalletForm().getMasterWalletId(), getWalletForm().getWallet(), selectedEntries);
Optional<Tx0Preview> optTx0Preview = whirlpoolDialog.showAndWait();
optTx0Preview.ifPresent(tx0Preview -> previewPremix(tx0Preview, selectedEntries));
}
@@ -314,10 +319,12 @@ public class UtxosController extends WalletFormController implements Initializab
payments.add(new Payment(premixAddress, "Premix #" + i, tx0Preview.getPremixValue(), false));
}
List<byte[]> opReturns = List.of(new byte[64]);
final List<BlockTransactionHashIndex> utxos = utxoEntries.stream().map(HashIndexEntry::getHashIndex).collect(Collectors.toList());
Platform.runLater(() -> {
EventManager.get().post(new SendActionEvent(getWalletForm().getWallet(), utxos));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), utxos, payments, tx0Preview.getTx0MinerFee(), tx0Preview.getPool())));
Platform.runLater(() -> EventManager.get().post(new SpendUtxoEvent(getWalletForm().getWallet(), utxos, payments, opReturns, tx0Preview.getTx0MinerFee(), tx0Preview.getPool())));
});
}
@@ -373,9 +380,11 @@ public class UtxosController extends WalletFormController implements Initializab
mixConfig.setMixToWalletName(changedMixConfig.getMixToWalletName());
mixConfig.setMixToWalletFile(changedMixConfig.getMixToWalletFile());
mixConfig.setMinMixes(changedMixConfig.getMinMixes());
mixConfig.setIndexRange(changedMixConfig.getIndexRange());
EventManager.get().post(new WalletMasterMixConfigChangedEvent(getWalletForm().getWallet()));
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWalletForm().getWallet());
whirlpool.setPostmixIndexRange(mixConfig.getIndexRange());
try {
String mixToWalletId = AppServices.getWhirlpoolServices().getWhirlpoolMixToWalletId(mixConfig);
whirlpool.setMixToWallet(mixToWalletId, mixConfig.getMinMixes());
@@ -494,6 +503,10 @@ public class UtxosController extends WalletFormController implements Initializab
utxosTable.updateHistoryStatus(event);
}
@Subscribe
public void newBlock(NewBlockEvent event) {
getWalletForm().getWalletUtxosEntry().updateMixProgress();
}
@Subscribe
public void bwtSyncStatus(BwtSyncStatusEvent event) {
@@ -25,6 +25,8 @@ import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;
import static com.sparrowwallet.drongo.wallet.WalletNode.nodeRangesToString;
public class WalletForm {
private static final Logger log = LoggerFactory.getLogger(WalletForm.class);
@@ -66,6 +68,10 @@ public class WalletForm {
return wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
}
public String getMasterWalletId() {
return storage.getWalletId(getMasterWallet());
}
public Storage getStorage() {
return storage;
}
@@ -119,11 +125,14 @@ public class WalletForm {
refreshHistory(blockHeight, pastWallet, null);
}
public void refreshHistory(Integer blockHeight, Wallet pastWallet, WalletNode node) {
public void refreshHistory(Integer blockHeight, Wallet pastWallet, Set<WalletNode> nodes) {
Wallet previousWallet = wallet.copy();
if(wallet.isValid() && AppServices.isConnected()) {
log.debug(node == null ? wallet.getFullName() + " refreshing full wallet history" : wallet.getFullName() + " requesting node wallet history for " + node.getDerivationPath());
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(node));
if(log.isDebugEnabled()) {
log.debug(nodes == null ? wallet.getFullName() + " refreshing full wallet history" : wallet.getFullName() + " requesting node wallet history for " + nodeRangesToString(nodes));
}
ElectrumServer.TransactionHistoryService historyService = new ElectrumServer.TransactionHistoryService(wallet, getWalletTransactionNodes(nodes));
historyService.setOnSucceeded(workerStateEvent -> {
if(historyService.getValue()) {
EventManager.get().post(new WalletHistoryFinishedEvent(wallet));
@@ -140,7 +149,7 @@ public class WalletForm {
EventManager.get().post(new WalletHistoryFailedEvent(wallet, workerStateEvent.getSource().getException()));
});
EventManager.get().post(new WalletHistoryStartedEvent(wallet, node));
EventManager.get().post(new WalletHistoryStartedEvent(wallet, nodes));
historyService.start();
}
}
@@ -245,19 +254,21 @@ public class WalletForm {
walletTransactionNodes.add(transactionNodes);
}
private Set<WalletNode> getWalletTransactionNodes(WalletNode walletNode) {
if(walletNode == null) {
private Set<WalletNode> getWalletTransactionNodes(Set<WalletNode> walletNodes) {
if(walletNodes == null) {
return null;
}
Set<WalletNode> allNodes = new LinkedHashSet<>();
for(Set<WalletNode> nodes : walletTransactionNodes) {
if(nodes.contains(walletNode)) {
allNodes.addAll(nodes);
for(WalletNode walletNode : walletNodes) {
for(Set<WalletNode> nodes : walletTransactionNodes) {
if(nodes.contains(walletNode)) {
allNodes.addAll(nodes);
}
}
}
return allNodes.isEmpty() ? Set.of(walletNode) : allNodes;
return allNodes.isEmpty() ? walletNodes : allNodes;
}
public NodeEntry getNodeEntry(KeyPurpose keyPurpose) {
@@ -388,7 +399,7 @@ public class WalletForm {
WalletNode walletNode = event.getWalletNode(wallet);
if(walletNode != null) {
log.debug(wallet.getFullName() + " history event for node " + walletNode + " (" + event.getScriptHash() + ")");
refreshHistory(AppServices.getCurrentBlockHeight(), null, walletNode);
refreshHistory(AppServices.getCurrentBlockHeight(), null, Set.of(walletNode));
}
}
}
@@ -499,6 +510,33 @@ public class WalletForm {
}
}
@Subscribe
public void walletLabelChanged(WalletLabelChangedEvent event) {
if(event.getWallet() == wallet) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
}
}
@Subscribe
public void walletGapLimitChanged(WalletGapLimitChangedEvent event) {
if(event.getWallet() == wallet) {
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
Set<WalletNode> newNodes = new LinkedHashSet<>();
for(KeyPurpose keyPurpose : KeyPurpose.DEFAULT_PURPOSES) {
Optional<WalletNode> optPurposeNode = wallet.getPurposeNodes().stream().filter(node -> node.getKeyPurpose() == keyPurpose).findFirst();
if(optPurposeNode.isPresent()) {
WalletNode purposeNode = optPurposeNode.get();
newNodes.addAll(purposeNode.fillToIndex(wallet.getLookAheadIndex(purposeNode)));
}
}
if(!newNodes.isEmpty()) {
Platform.runLater(() -> refreshHistory(AppServices.getCurrentBlockHeight(), null, newNodes));
}
}
}
@Subscribe
public void whirlpoolMixSuccess(WhirlpoolMixSuccessEvent event) {
if(event.getWallet() == wallet && event.getWalletNode() != null) {
@@ -12,7 +12,7 @@ public class WalletUtxosEntry extends Entry {
public WalletUtxosEntry(Wallet wallet) {
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(wallet, entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
calculateDuplicates();
retrieveMixProgress();
updateMixProgress();
}
@Override
@@ -38,13 +38,15 @@ public class WalletUtxosEntry extends Entry {
}
}
protected void retrieveMixProgress() {
public void updateMixProgress() {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(getWallet());
if(whirlpool != null) {
for(Entry entry : getChildren()) {
UtxoEntry utxoEntry = (UtxoEntry)entry;
MixProgress mixProgress = whirlpool.getMixProgress(utxoEntry.getHashIndex());
utxoEntry.setMixProgress(mixProgress);
if(mixProgress != null || utxoEntry.getMixStatus() == null || (utxoEntry.getMixStatus().getMixFailReason() == null && utxoEntry.getMixStatus().getNextMixUtxo() == null)) {
utxoEntry.setMixProgress(mixProgress);
}
}
}
}
@@ -62,6 +64,6 @@ public class WalletUtxosEntry extends Entry {
getChildren().removeAll(entriesRemoved);
calculateDuplicates();
retrieveMixProgress();
updateMixProgress();
}
}
@@ -25,18 +25,15 @@ import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.Network;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.protocol.Transaction;
import com.sparrowwallet.drongo.protocol.TransactionOutput;
import com.sparrowwallet.drongo.protocol.*;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.nightjar.http.JavaHttpClientService;
import com.sparrowwallet.nightjar.stomp.JavaStompClientService;
import com.sparrowwallet.nightjar.tor.WhirlpoolTorClientService;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WhirlpoolMixEvent;
import com.sparrowwallet.sparrow.event.WhirlpoolMixSuccessEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.dataPersister.SparrowDataPersister;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowDataSource;
@@ -49,13 +46,11 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.concurrent.ScheduledService;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import java.util.stream.Collectors;
public class Whirlpool {
@@ -71,12 +66,14 @@ public class Whirlpool {
private final TorClientService torClientService;
private final WhirlpoolWalletService whirlpoolWalletService;
private final WhirlpoolWalletConfig config;
private Tx0ParamService tx0ParamService;
private ExpirablePoolSupplier poolSupplier;
private Tx0Service tx0Service;
private final Tx0ParamService tx0ParamService;
private final ExpirablePoolSupplier poolSupplier;
private final Tx0Service tx0Service;
private Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
private HD_Wallet hdWallet;
private String walletId;
private String mixToWalletId;
private boolean resyncMixesDone;
private StartupService startupService;
@@ -100,7 +97,7 @@ public class Whirlpool {
}
private WhirlpoolWalletConfig computeWhirlpoolWalletConfig(HostAndPort torProxy) {
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet);
DataPersisterFactory dataPersisterFactory = (whirlpoolWallet, bip44w) -> new SparrowDataPersister(whirlpoolWallet, config.getPersistDelaySeconds());
DataSourceFactory dataSourceFactory = (whirlpoolWallet, bip44w, dataPersister) -> new SparrowDataSource(whirlpoolWallet, bip44w, dataPersister);
boolean onion = (torProxy != null);
@@ -110,9 +107,20 @@ public class Whirlpool {
WhirlpoolWalletConfig whirlpoolWalletConfig = new WhirlpoolWalletConfig(dataSourceFactory, httpClientService, stompClientService, torClientService, serverApi, whirlpoolServer.getParams(), false);
whirlpoolWalletConfig.setDataPersisterFactory(dataPersisterFactory);
whirlpoolWalletConfig.setPartner("SPARROW");
whirlpoolWalletConfig.setIndexRangePostmix(IndexRange.FULL);
return whirlpoolWalletConfig;
}
public Pool getPool(String poolId) {
try {
return getPools(null).stream().filter(pool -> pool.getPoolId().equals(poolId)).findFirst().orElse(null);
} catch(Exception e) {
log.error("Error retrieving pools", e);
}
return null;
}
public Collection<Pool> getPools(Long totalUtxoValue) throws Exception {
this.poolSupplier.load();
if(totalUtxoValue == null) {
@@ -143,7 +151,6 @@ public class Whirlpool {
}
private Tx0Config computeTx0Config() {
Tx0FeeTarget tx0FeeTarget = Tx0FeeTarget.BLOCKS_4;
Tx0FeeTarget mixFeeTarget = Tx0FeeTarget.BLOCKS_4;
return new Tx0Config(tx0ParamService, poolSupplier, tx0FeeTarget, mixFeeTarget, WhirlpoolAccount.BADBANK);
}
@@ -162,7 +169,7 @@ public class Whirlpool {
HD_WalletFactoryGeneric hdWalletFactory = HD_WalletFactoryGeneric.getInstance();
byte[] seed = hdWalletFactory.computeSeedFromWords(words);
this.walletId = walletId;
hdWallet = new HD_Wallet(purpose, words, config.getNetworkParameters(), seed, passphrase, 1);
hdWallet = new HD_Wallet(purpose, words, config.getNetworkParameters(), seed, passphrase);
} catch(Exception e) {
throw new IllegalStateException("Could not create Whirlpool HD wallet ", e);
}
@@ -236,18 +243,73 @@ public class Whirlpool {
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
if(whirlpoolUtxo != null && whirlpoolUtxo.getUtxoState() != null) {
return whirlpoolUtxo.getUtxoState().getMixProgress();
MixProgress mixProgress = whirlpoolUtxo.getUtxoState().getMixProgress();
if(mixProgress != null && !isMixing(utxo)) {
log.debug("Utxo " + utxo + " mix state is " + whirlpoolUtxo.getUtxoState() + " but utxo is not mixing");
return null;
}
return mixProgress;
}
return null;
}
private boolean isMixing(BlockTransactionHashIndex utxo) {
if(whirlpoolWalletService.whirlpoolWallet() == null || !whirlpoolWalletService.whirlpoolWallet().isStarted()) {
return false;
}
return whirlpoolWalletService.whirlpoolWallet().getMixingState().getUtxosMixing().stream().map(WhirlpoolUtxo::getUtxo).anyMatch(uo -> uo.tx_hash.equals(utxo.getHashAsString()) && uo.tx_output_n == (int)utxo.getIndex());
}
public void refreshUtxos() {
if(whirlpoolWalletService.whirlpoolWallet() != null) {
whirlpoolWalletService.whirlpoolWallet().refreshUtxos();
}
}
private void resyncMixesDone(Whirlpool whirlpool, Wallet postmixWallet) {
Set<BlockTransactionHashIndex> receiveUtxos = postmixWallet.getWalletUtxos().entrySet().stream()
.filter(entry -> entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE).map(Map.Entry::getKey).collect(Collectors.toSet());
for(BlockTransactionHashIndex utxo : receiveUtxos) {
int mixesDone = recountMixesDone(postmixWallet, utxo);
whirlpool.setMixesDone(utxo, mixesDone);
}
}
public int recountMixesDone(Wallet postmixWallet, BlockTransactionHashIndex postmixUtxo) {
int mixesDone = 0;
Set<BlockTransactionHashIndex> walletTxos = postmixWallet.getWalletTxos().entrySet().stream()
.filter(entry -> entry.getValue().getKeyPurpose() == KeyPurpose.RECEIVE).map(Map.Entry::getKey).collect(Collectors.toSet());
BlockTransaction blkTx = postmixWallet.getTransactions().get(postmixUtxo.getHash());
while(blkTx != null) {
mixesDone++;
List<TransactionInput> inputs = blkTx.getTransaction().getInputs();
blkTx = null;
for(TransactionInput txInput : inputs) {
BlockTransaction inputTx = postmixWallet.getTransactions().get(txInput.getOutpoint().getHash());
if(inputTx != null && walletTxos.stream().anyMatch(txo -> txo.getHash().equals(inputTx.getHash()) && txo.getIndex() == txInput.getOutpoint().getIndex()) && inputTx.getTransaction() != null) {
blkTx = inputTx;
}
}
}
return mixesDone;
}
public void setMixesDone(BlockTransactionHashIndex utxo, int mixesDone) {
if(whirlpoolWalletService.whirlpoolWallet() == null) {
return;
}
WhirlpoolUtxo whirlpoolUtxo = whirlpoolWalletService.whirlpoolWallet().getUtxoSupplier().findUtxo(utxo.getHashAsString(), (int)utxo.getIndex());
if(whirlpoolUtxo != null) {
whirlpoolUtxo.setMixsDone(mixesDone);
}
}
public boolean hasWallet() {
return hdWallet != null;
}
@@ -297,9 +359,15 @@ public class Whirlpool {
public static Wallet getStandardAccountWallet(WhirlpoolAccount whirlpoolAccount, Wallet wallet) {
StandardAccount standardAccount = getStandardAccount(whirlpoolAccount);
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount)) {
wallet = wallet.getChildWallet(standardAccount);
if(StandardAccount.WHIRLPOOL_ACCOUNTS.contains(standardAccount) || wallet.getStandardAccountType() != standardAccount) {
Wallet standardWallet = wallet.getChildWallet(standardAccount);
if(standardWallet == null) {
throw new IllegalStateException("Cannot find " + standardAccount + " wallet");
}
return standardWallet;
}
return wallet;
}
@@ -370,6 +438,10 @@ public class Whirlpool {
config.setServerApi(serverApi);
}
public void refreshTorCircuits() {
torClientService.changeIdentity();
}
public String getScode() {
return config.getScode();
}
@@ -378,6 +450,10 @@ public class Whirlpool {
config.setScode(scode);
}
public void setTx0FeeTarget(Tx0FeeTarget tx0FeeTarget) {
this.tx0FeeTarget = tx0FeeTarget;
}
public String getWalletId() {
return walletId;
}
@@ -386,6 +462,20 @@ public class Whirlpool {
return mixToWalletId;
}
public void setResyncMixesDone(boolean resyncMixesDone) {
this.resyncMixesDone = resyncMixesDone;
}
public void setPostmixIndexRange(String indexRange) {
if(indexRange != null) {
try {
config.setIndexRangePostmix(IndexRange.valueOf(indexRange));
} catch(Exception e) {
log.error("Invalid index range " + indexRange);
}
}
}
public void setMixToWallet(String mixToWalletId, Integer minMixes) {
if(mixToWalletId == null) {
config.setExternalDestination(null);
@@ -473,6 +563,15 @@ public class Whirlpool {
if(e.getWhirlpoolWallet() == whirlpoolWalletService.whirlpoolWallet()) {
log.info("Mixing to " + e.getWhirlpoolWallet().getConfig().getExternalDestination());
mixingProperty.set(true);
if(resyncMixesDone) {
Wallet wallet = AppServices.get().getWallet(walletId);
if(wallet != null) {
Wallet postmixWallet = getStandardAccountWallet(WhirlpoolAccount.POSTMIX, wallet);
resyncMixesDone(this, postmixWallet);
resyncMixesDone = false;
}
}
}
}
@@ -575,14 +674,17 @@ public class Whirlpool {
updateProgress(-1, 1);
updateMessage("Starting Whirlpool...");
whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) {
whirlpoolWallet.start();
}
whirlpool.startingProperty.set(false);
try {
whirlpool.startingProperty.set(true);
WhirlpoolWallet whirlpoolWallet = whirlpool.getWhirlpoolWallet();
if(AppServices.onlineProperty().get()) {
whirlpoolWallet.start();
}
return whirlpoolWallet;
return whirlpoolWallet;
} finally {
whirlpool.startingProperty.set(false);
}
}
};
}
@@ -601,10 +703,38 @@ public class Whirlpool {
protected Boolean call() throws Exception {
updateProgress(-1, 1);
updateMessage("Disconnecting from Whirlpool...");
whirlpool.stoppingProperty.set(true);
whirlpool.shutdown();
whirlpool.stoppingProperty.set(false);
return true;
try {
whirlpool.stoppingProperty.set(true);
whirlpool.shutdown();
return true;
} finally {
whirlpool.stoppingProperty.set(false);
}
}
};
}
}
public static class RegisteredInputsService extends Service<Integer> {
private final Whirlpool whirlpool;
private final String poolId;
public RegisteredInputsService(Whirlpool whirlpool, String poolId) {
this.whirlpool = whirlpool;
this.poolId = poolId;
}
@Override
protected Task<Integer> createTask() {
return new Task<>() {
protected Integer call() {
Pool pool = whirlpool.getPool(poolId);
if(pool != null) {
return pool.getNbRegistered();
}
return null;
}
};
}
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.whirlpool;
import com.samourai.whirlpool.client.tx0.Tx0Preview;
import com.samourai.whirlpool.client.tx0.Tx0Previews;
import com.samourai.whirlpool.client.wallet.beans.Tx0FeeTarget;
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
import com.sparrowwallet.drongo.BitcoinUnit;
import com.sparrowwallet.drongo.protocol.Transaction;
@@ -10,10 +11,12 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.CoinLabel;
import com.sparrowwallet.sparrow.control.CopyableLabel;
import com.sparrowwallet.sparrow.event.WalletMasterMixConfigChangedEvent;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowMinerFeeSupplier;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
@@ -26,6 +29,8 @@ import javafx.util.StringConverter;
import java.util.*;
public class WhirlpoolController {
private static final List<Tx0FeeTarget> FEE_TARGETS = List.of(Tx0FeeTarget.MIN, Tx0FeeTarget.BLOCKS_4, Tx0FeeTarget.BLOCKS_2);
@FXML
private VBox whirlpoolBox;
@@ -44,6 +49,12 @@ public class WhirlpoolController {
@FXML
private TextField scode;
@FXML
private Slider premixPriority;
@FXML
private CopyableLabel premixFeeRate;
@FXML
private ComboBox<Pool> pool;
@@ -108,6 +119,30 @@ public class WhirlpoolController {
EventManager.get().post(new WalletMasterMixConfigChangedEvent(wallet));
});
premixPriority.setMin(0);
premixPriority.setMax(FEE_TARGETS.size() - 1);
premixPriority.setMajorTickUnit(1);
premixPriority.setMinorTickCount(0);
premixPriority.setLabelFormatter(new StringConverter<>() {
@Override
public String toString(Double object) {
return object.intValue() == 0 ? "Low" : (object.intValue() == 1 ? "Normal" : "High");
}
@Override
public Double fromString(String string) {
return null;
}
});
premixPriority.valueProperty().addListener((observable, oldValue, newValue) -> {
pool.setItems(FXCollections.emptyObservableList());
tx0Previews = null;
tx0PreviewProperty.set(null);
Tx0FeeTarget tx0FeeTarget = FEE_TARGETS.get(newValue.intValue());
premixFeeRate.setText(SparrowMinerFeeSupplier.getFee(Integer.parseInt(tx0FeeTarget.getFeeTarget().getValue())) + " sats/vB");
});
premixPriority.setValue(1);
if(mixConfig.getScode() != null) {
step1.setVisible(false);
step3.setVisible(true);
@@ -280,6 +315,7 @@ public class WhirlpoolController {
} else {
tx0Previews = null;
whirlpool.setScode(mixConfig.getScode());
whirlpool.setTx0FeeTarget(FEE_TARGETS.get(premixPriority.valueProperty().intValue()));
Whirlpool.Tx0PreviewsService tx0PreviewsService = new Whirlpool.Tx0PreviewsService(whirlpool, wallet, utxoEntries);
tx0PreviewsService.setOnRunning(workerStateEvent -> {
@@ -19,10 +19,12 @@ import javafx.util.Duration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.SocketTimeoutException;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.TimeoutException;
import java.util.stream.Collectors;
public class WhirlpoolServices {
@@ -87,13 +89,29 @@ public class WhirlpoolServices {
}
}
if(wallet.getMasterMixConfig() != null) {
whirlpool.setPostmixIndexRange(wallet.getMasterMixConfig().getIndexRange());
}
Whirlpool.StartupService startupService = whirlpool.createStartupService();
startupService.setPeriod(Duration.minutes(2));
startupService.setOnSucceeded(workerStateEvent -> {
startupService.cancel();
});
startupService.setOnFailed(workerStateEvent -> {
log.error("Failed to start whirlpool", workerStateEvent.getSource().getException());
Throwable exception = workerStateEvent.getSource().getException();
while(exception.getCause() != null) {
exception = exception.getCause();
}
if(exception instanceof TimeoutException || exception instanceof SocketTimeoutException) {
EventManager.get().post(new StatusEvent("Error connecting to Whirlpool server, will retry soon..."));
if(torProxy != null) {
whirlpool.refreshTorCircuits();
}
log.error("Error connecting to Whirlpool server: " + exception.getMessage());
} else {
log.error("Failed to start Whirlpool", workerStateEvent.getSource().getException());
}
});
startupService.start();
}
@@ -136,13 +154,15 @@ public class WhirlpoolServices {
&& wallet.getKeystores().size() == 1
&& wallet.getKeystores().get(0).hasSeed()
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
&& !wallet.isWhirlpoolMixWallet();
&& wallet.getStandardAccountType() != null
&& StandardAccount.MIXABLE_ACCOUNTS.contains(wallet.getStandardAccountType());
}
public static void prepareWhirlpoolWallet(Wallet decryptedWallet, String walletId, Storage storage) {
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
whirlpool.setScode(decryptedWallet.getMasterMixConfig().getScode());
whirlpool.setHDWallet(walletId, decryptedWallet);
whirlpool.setResyncMixesDone(true);
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
@@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.whirlpool.dataPersister;
import com.samourai.wallet.util.AbstractOrchestrator;
import com.samourai.whirlpool.client.wallet.WhirlpoolWallet;
import com.samourai.whirlpool.client.wallet.WhirlpoolWalletConfig;
import com.samourai.whirlpool.client.wallet.data.dataPersister.DataPersister;
@@ -7,24 +8,49 @@ import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigPersistedS
import com.samourai.whirlpool.client.wallet.data.utxoConfig.UtxoConfigSupplier;
import com.samourai.whirlpool.client.wallet.data.walletState.WalletStateSupplier;
import com.sparrowwallet.sparrow.whirlpool.dataSource.SparrowWalletStateSupplier;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SparrowDataPersister implements DataPersister {
private static final Logger log = LoggerFactory.getLogger(SparrowDataPersister.class);
private final WalletStateSupplier walletStateSupplier;
private final UtxoConfigSupplier utxoConfigSupplier;
public SparrowDataPersister(WhirlpoolWallet whirlpoolWallet) throws Exception {
private AbstractOrchestrator persistOrchestrator;
private final int persistDelaySeconds;
public SparrowDataPersister(WhirlpoolWallet whirlpoolWallet, int persistDelaySeconds) throws Exception {
WhirlpoolWalletConfig config = whirlpoolWallet.getConfig();
String walletIdentifier = whirlpoolWallet.getWalletIdentifier();
this.walletStateSupplier = new SparrowWalletStateSupplier(walletIdentifier, config);
this.utxoConfigSupplier = new UtxoConfigPersistedSupplier(new SparrowUtxoConfigPersister(walletIdentifier));
this.persistDelaySeconds = persistDelaySeconds;
}
@Override
public void open() throws Exception {
startPersistOrchestrator();
}
protected void startPersistOrchestrator() {
persistOrchestrator = new AbstractOrchestrator(persistDelaySeconds * 1000) {
@Override
protected void runOrchestrator() {
try {
persist(false);
} catch (Exception e) {
log.error("Error persisting Whirlpool data", e);
}
}
};
persistOrchestrator.start(true);
}
@Override
public void close() throws Exception {
persistOrchestrator.stop();
}
@Override
@@ -62,7 +62,9 @@ public class SparrowUtxoConfigPersister extends UtxoConfigPersister {
wallet.getUtxoMixes().putAll(changedUtxoMixes);
wallet.getUtxoMixes().keySet().removeAll(removedUtxoMixes.keySet());
EventManager.get().post(new WalletUtxoMixesChangedEvent(wallet, changedUtxoMixes, removedUtxoMixes));
if(!changedUtxoMixes.isEmpty() || !removedUtxoMixes.isEmpty()) {
EventManager.get().post(new WalletUtxoMixesChangedEvent(wallet, changedUtxoMixes, removedUtxoMixes));
}
}
private Wallet getWallet() {
@@ -89,8 +89,10 @@ public class SparrowDataSource extends WalletResponseDataSource {
List<ExtendedKey.Header> headers = ExtendedKey.Header.getHeaders(Network.get());
ExtendedKey.Header header = headers.stream().filter(head -> head.getDefaultScriptType().equals(wallet.getScriptType()) && !head.isPrivateKey()).findFirst().orElse(ExtendedKey.Header.xpub);
address.address = wallet.getKeystores().get(0).getExtendedPublicKey().toString(header);
address.account_index = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1;
address.change_index = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1;
int receiveIndex = wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.RECEIVE).getHighestUsedIndex() + 1;
address.account_index = wallet.getMixConfig() != null ? Math.max(receiveIndex, wallet.getMixConfig().getReceiveIndex()) : receiveIndex;
int changeIndex = wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() == null ? 0 : wallet.getNode(KeyPurpose.CHANGE).getHighestUsedIndex() + 1;
address.change_index = wallet.getMixConfig() != null ? Math.max(changeIndex, wallet.getMixConfig().getChangeIndex()) : changeIndex;
address.n_tx = wallet.getTransactions().size();
addresses.add(address);
@@ -5,6 +5,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.drongo.wallet.WalletNode;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletGapLimitChangedEvent;
import com.sparrowwallet.sparrow.event.WalletMixConfigChangedEvent;
public class SparrowIndexHandler extends AbstractIndexHandler {
@@ -37,6 +38,7 @@ public class SparrowIndexHandler extends AbstractIndexHandler {
@Override
public synchronized void set(int value) {
setStoredIndex(value);
ensureSufficientGapLimit(value);
}
private int getCurrentIndex() {
@@ -58,13 +60,22 @@ public class SparrowIndexHandler extends AbstractIndexHandler {
private void setStoredIndex(int index) {
if(wallet.getMixConfig() != null) {
if(walletNode.getKeyPurpose() == KeyPurpose.RECEIVE) {
if(walletNode.getKeyPurpose() == KeyPurpose.RECEIVE && wallet.getMixConfig().getReceiveIndex() != index) {
wallet.getMixConfig().setReceiveIndex(index);
} else if(walletNode.getKeyPurpose() == KeyPurpose.CHANGE) {
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
} else if(walletNode.getKeyPurpose() == KeyPurpose.CHANGE && wallet.getMixConfig().getChangeIndex() != index) {
wallet.getMixConfig().setChangeIndex(index);
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
}
}
}
EventManager.get().post(new WalletMixConfigChangedEvent(wallet));
private void ensureSufficientGapLimit(int index) {
int highestUsedIndex = getCurrentIndex() - 1;
int existingGapLimit = wallet.getGapLimit();
if(index > highestUsedIndex + existingGapLimit) {
wallet.setGapLimit(Math.max(wallet.getGapLimit(), index - highestUsedIndex));
EventManager.get().post(new WalletGapLimitChangedEvent(wallet));
}
}
}
@@ -26,13 +26,18 @@ public class SparrowMinerFeeSupplier implements MinerFeeSupplier {
@Override
public int getFee(MinerFeeTarget feeTarget) {
if (AppServices.getTargetBlockFeeRates() == null) {
return FALLBACK_FEE_RATE;
}
return getMinimumFeeForTarget(Integer.parseInt(feeTarget.getValue()));
return getFee(Integer.parseInt(feeTarget.getValue()));
}
private Integer getMinimumFeeForTarget(int targetBlocks) {
public static int getFee(int targetBlocks) {
if(AppServices.getTargetBlockFeeRates() == null) {
return FALLBACK_FEE_RATE;
}
return getMinimumFeeForTarget(targetBlocks);
}
private static Integer getMinimumFeeForTarget(int targetBlocks) {
List<Map.Entry<Integer, Double>> feeRates = new ArrayList<>(AppServices.getTargetBlockFeeRates().entrySet());
Collections.reverse(feeRates);
for(Map.Entry<Integer, Double> feeRate : feeRates) {
@@ -40,6 +45,7 @@ public class SparrowMinerFeeSupplier implements MinerFeeSupplier {
return feeRate.getValue().intValue();
}
}
return feeRates.get(0).getValue().intValue();
}
@@ -132,6 +132,25 @@
-fx-background-color: #116a8d;
}
.combo-text-field .combo-button {
-fx-padding: 0 3 0 0;
}
.combo-text-field .combo-button > .graphic {
-fx-background-color: #949494;
-fx-scale-shape: false;
-fx-padding: 4.5 6 4.5 4.5;
-fx-shape: "M11.8513+192L19.004+192C19.4989+192+19.7463+192.554+19.396+192.878L15.821+196.191C15.6042+196.392+15.2511+196.392+15.0343+196.191L11.4594+192.878C11.1091+192.554+11.3565+192+11.8513+192Z ";
}
.combo-text-field .combo-button:hover > .graphic {
-fx-background-color: #0184bc;
}
.combo-text-field .combo-button:pressed > .graphic {
-fx-background-color: #116a8d;
}
.readonly.text-input {
-fx-text-fill: derive(-fx-text-inner-color, 40%);
}
@@ -7,7 +7,7 @@
<?import javafx.scene.layout.*?>
<AnchorPane stylesheets="@keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.HwAirgappedController">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true" hbarPolicy="NEVER" prefHeight="620">
<Accordion fx:id="importAccordion" />
</ScrollPane>
</AnchorPane>
@@ -7,7 +7,7 @@
<?import javafx.scene.layout.*?>
<AnchorPane stylesheets="@keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.HwUsbDevicesController">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true">
<Accordion fx:id="deviceAccordion" />
</ScrollPane>
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true" hbarPolicy="NEVER" prefHeight="620">
<Accordion fx:id="deviceAccordion" />
</ScrollPane>
</AnchorPane>
@@ -7,7 +7,7 @@
<?import javafx.scene.layout.*?>
<AnchorPane stylesheets="@keystoreimport.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.keystoreimport.SwController">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true">
<ScrollPane AnchorPane.leftAnchor="0" AnchorPane.rightAnchor="0" fitToWidth="true" hbarPolicy="NEVER" prefHeight="620">
<Accordion fx:id="importAccordion" />
</ScrollPane>
</AnchorPane>
@@ -101,8 +101,8 @@
<UnlabeledToggleSwitch fx:id="publicUseProxy"/>
</Field>
<Field text="Proxy URL:">
<TextField fx:id="publicProxyHost" />
<TextField fx:id="publicProxyPort" maxWidth="100" />
<TextField fx:id="publicProxyHost" promptText="e.g. 127.0.0.1" />
<TextField fx:id="publicProxyPort" maxWidth="120" promptText="e.g. 9050/9150" />
</Field>
</Fieldset>
</Form>
@@ -111,7 +111,7 @@
<Fieldset inputGrow="SOMETIMES" text="Bitcoin Core RPC">
<Field text="URL:">
<TextField fx:id="coreHost" promptText="e.g. 127.0.0.1"/>
<TextField fx:id="corePort" promptText="e.g. 8332" maxWidth="100" />
<TextField fx:id="corePort" promptText="e.g. 8332" maxWidth="120" />
</Field>
<Field text="Authentication:">
<SegmentedButton>
@@ -148,8 +148,8 @@
<UnlabeledToggleSwitch fx:id="coreUseProxy"/><HelpLabel helpText="Bitcoin Core RPC onion URLs, and all other non-RPC external addresses will be connected via this proxy if configured." />
</Field>
<Field text="Proxy URL:">
<TextField fx:id="coreProxyHost" />
<TextField fx:id="coreProxyPort" maxWidth="100" />
<TextField fx:id="coreProxyHost" promptText="e.g. 127.0.0.1" />
<TextField fx:id="coreProxyPort" maxWidth="120" promptText="e.g. 9050/9150" />
</Field>
</Fieldset>
</Form>
@@ -158,7 +158,7 @@
<Fieldset inputGrow="SOMETIMES" text="Private Electrum Server">
<Field text="URL:">
<TextField fx:id="electrumHost" promptText="e.g. 127.0.0.1 or Tor hostname (.onion)"/>
<TextField fx:id="electrumPort" promptText="e.g. 50002" maxWidth="100" />
<TextField fx:id="electrumPort" promptText="e.g. 50002" maxWidth="120" />
</Field>
<Field text="Use SSL:">
<UnlabeledToggleSwitch fx:id="electrumUseSsl"/>
@@ -175,8 +175,8 @@
<UnlabeledToggleSwitch fx:id="useProxy"/><HelpLabel helpText="All external addresses will be connected via this proxy if configured." />
</Field>
<Field text="Proxy URL:">
<TextField fx:id="proxyHost" />
<TextField fx:id="proxyPort" maxWidth="100" />
<TextField fx:id="proxyHost" promptText="e.g. 127.0.0.1" />
<TextField fx:id="proxyPort" maxWidth="120" promptText="e.g. 9050/9150" />
</Field>
</Fieldset>
</Form>
@@ -0,0 +1,2 @@
alter table wallet add column label varchar(255) after name;
alter table mixConfig add column indexRange varchar(10) after mixOnStartup;
@@ -24,6 +24,67 @@
-fx-padding: 0 0 0 12;
}
.headers-tabs > .tab-header-area .tab {
-fx-pref-height: 50;
-fx-pref-width: 90;
-fx-alignment: CENTER;
}
.headers-tabs > .tab-header-area .tab-label {
-fx-pref-height: 50;
-fx-pref-width: 90;
-fx-alignment: CENTER;
-fx-translate-x: -6;
}
#transactionDiagram .boundary {
-fx-stroke: transparent;
}
#transactionDiagram .input-label, #transactionDiagram .recipient-label, #transactionDiagram .change-label, #transactionDiagram .fee-tooltip, #transactionDiagram .transaction-tooltip {
-fx-font-size: 13px;
-fx-font-family: 'Roboto Mono';
}
#transactionDiagram .fee-warning-icon {
-fx-text-fill: rgb(202, 18, 67);
}
#transactionDiagram .inputs-type, #transactionDiagram .input-line, #transactionDiagram .output-line {
-fx-fill: transparent;
-fx-text-fill: #696c77;
-fx-stroke: #696c77;
-fx-stroke-width: 1px;
}
#transactionDiagram .input-dashed-line {
-fx-stroke-dash-array: 5px 5px;
}
#transactionDiagram .utxo-label .button, #transactionDiagram .replace-change-label .button {
-fx-padding: 0;
-fx-pref-height: 18;
-fx-pref-width: 18;
-fx-border-width: 0;
-fx-background-color: -fx-background;
}
#transactionDiagram .utxo-label .button .label .text {
-fx-fill: -fx-background;
}
#transactionDiagram .utxo-label:hover .button .label .text {
-fx-fill: -fx-text-base-color;
}
#transactionDiagram .change-warning-icon {
-fx-text-fill: rgb(238, 210, 2);
}
.details-lower .fieldset {
-fx-padding: 0 0 0 0;
}
.future-warning {
-fx-text-fill: rgb(238, 210, 2);
-fx-padding: 0 0 0 12;
@@ -25,6 +25,9 @@
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.control.Tooltip?>
<?import com.sparrowwallet.sparrow.control.DynamicForm?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.control.Tab?>
<?import com.sparrowwallet.sparrow.control.TransactionDiagram?>
<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>
@@ -52,83 +55,104 @@
<Separator GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2" styleClass="form-separator"/>
<Form GridPane.columnIndex="0" GridPane.rowIndex="2">
<Fieldset text="Headers" inputGrow="SOMETIMES">
<Field text="Version:">
<Spinner fx:id="version" prefWidth="60" editable="true" />
</Field>
<Field text="Type:">
<CopyableLabel fx:id="segwit" />
</Field>
</Fieldset>
</Form>
<TabPane side="RIGHT" GridPane.columnIndex="0" GridPane.rowIndex="2" GridPane.columnSpan="2" styleClass="headers-tabs">
<Tab text="Overview" closable="false">
<TransactionDiagram fx:id="transactionDiagram" maxWidth="700" final="true"/>
</Tab>
<Tab text="Detail" closable="false">
<GridPane hgap="10.0" vgap="10.0">
<columnConstraints>
<ColumnConstraints percentWidth="50" />
<ColumnConstraints percentWidth="50" />
</columnConstraints>
<rowConstraints>
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset text="Headers" inputGrow="SOMETIMES">
<Field text="Version:">
<Spinner fx:id="version" prefWidth="60" editable="true" />
</Field>
<Field text="Type:">
<CopyableLabel fx:id="segwit" />
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="2">
<Fieldset fx:id="locktimeFieldset" text="Absolute Locktime" inputGrow="SOMETIMES">
<Field text="Type:">
<SegmentedButton>
<toggleGroup>
<ToggleGroup fx:id="locktimeToggleGroup" />
</toggleGroup>
<buttons>
<ToggleButton fx:id="locktimeNoneType" text="Disabled" userData="none" toggleGroup="$locktimeToggleGroup" />
<ToggleButton fx:id="locktimeBlockType" text="Block" userData="block" toggleGroup="$locktimeToggleGroup" />
<ToggleButton fx:id="locktimeDateType" text="Date" userData="date" toggleGroup="$locktimeToggleGroup" />
</buttons>
</SegmentedButton>
</Field>
<Field fx:id="locktimeNoneField" text="Block:">
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
</Field>
<Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<tooltip>
<Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/>
</tooltip>
</Label>
</Field>
<Field fx:id="locktimeDateField" text="Date:">
<DateTimePicker fx:id="locktimeDate" prefWidth="180"/>
<Label fx:id="futureDateWarning">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<tooltip>
<Tooltip text="Future date specified - transaction cannot be broadcast until this date"/>
</tooltip>
</Label>
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="0">
<Fieldset fx:id="locktimeFieldset" text="Absolute Locktime" inputGrow="SOMETIMES">
<Field text="Type:">
<SegmentedButton>
<toggleGroup>
<ToggleGroup fx:id="locktimeToggleGroup" />
</toggleGroup>
<buttons>
<ToggleButton fx:id="locktimeNoneType" text="Disabled" userData="none" toggleGroup="$locktimeToggleGroup" />
<ToggleButton fx:id="locktimeBlockType" text="Block" userData="block" toggleGroup="$locktimeToggleGroup" />
<ToggleButton fx:id="locktimeDateType" text="Date" userData="date" toggleGroup="$locktimeToggleGroup" />
</buttons>
</SegmentedButton>
</Field>
<Field fx:id="locktimeNoneField" text="Block:">
<Spinner fx:id="locktimeNone" disable="true" prefWidth="120"/>
</Field>
<Field fx:id="locktimeBlockField" text="Block:">
<Spinner fx:id="locktimeBlock" editable="true" prefWidth="120"/>
<Hyperlink fx:id="locktimeCurrentHeight" text="Set current height" onAction="#setLocktimeToCurrentHeight" />
<Label fx:id="futureBlockWarning">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<tooltip>
<Tooltip text="Future block specified - transaction cannot be broadcast until this block height"/>
</tooltip>
</Label>
</Field>
<Field fx:id="locktimeDateField" text="Date:">
<DateTimePicker fx:id="locktimeDate" prefWidth="180"/>
<Label fx:id="futureDateWarning">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_TRIANGLE" styleClass="future-warning" />
</graphic>
<tooltip>
<Tooltip text="Future date specified - transaction cannot be broadcast until this date"/>
</tooltip>
</Label>
</Field>
</Fieldset>
</Form>
<Separator GridPane.columnIndex="0" GridPane.rowIndex="3" GridPane.columnSpan="2" styleClass="form-separator"/>
<HBox GridPane.columnIndex="0" GridPane.rowIndex="1" GridPane.columnSpan="2">
<padding>
<Insets right="40"/>
</padding>
<Separator styleClass="form-separator" HBox.hgrow="ALWAYS"/>
</HBox>
<Form GridPane.columnIndex="0" GridPane.rowIndex="4">
<Fieldset text="Size" inputGrow="SOMETIMES">
<Field text="Bytes:">
<CopyableLabel fx:id="size" />
</Field>
<Field text="vBytes:">
<CopyableLabel fx:id="virtualSize" />
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="0" GridPane.rowIndex="2" styleClass="details-lower">
<Fieldset text="Size" inputGrow="SOMETIMES">
<Field text="Bytes:">
<CopyableLabel fx:id="size" />
</Field>
<Field text="vBytes:">
<CopyableLabel fx:id="virtualSize" />
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="4">
<Fieldset text="Fee" inputGrow="SOMETIMES">
<Field text="Amount:">
<CoinLabel fx:id="fee" />
</Field>
<Field text="Rate:">
<CopyableLabel fx:id="feeRate" />
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="1" GridPane.rowIndex="2" styleClass="details-lower">
<Fieldset text="Fee" inputGrow="SOMETIMES">
<Field text="Amount:">
<CoinLabel fx:id="fee" />
</Field>
<Field text="Rate:">
<CopyableLabel fx:id="feeRate" />
</Field>
</Fieldset>
</Form>
</GridPane>
</Tab>
</TabPane>
<Separator GridPane.columnIndex="0" GridPane.rowIndex="5" GridPane.columnSpan="2" styleClass="form-separator"/>
@@ -3,13 +3,16 @@
<?import java.lang.*?>
<?import java.util.*?>
<?import javafx.scene.*?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.*?>
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.HelpLabel?>
<?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?>
<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>
@@ -36,6 +39,23 @@
</Field>
</Fieldset>
</Form>
<Form GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="SOMETIMES" text="Postmix Wallet Settings">
<Field text="Index range:">
<ComboBox fx:id="indexRange" prefWidth="140">
<items>
<FXCollections fx:factory="observableArrayList">
<IndexRange fx:constant="FULL" />
<IndexRange fx:constant="EVEN" />
<IndexRange fx:constant="ODD" />
</FXCollections>
</items>
</ComboBox>
<HelpLabel helpText="Using different index ranges allows the same wallet to be mixed simultaneously on multiple clients.\nSelect Full if simultaneous mixing is not required to keep wallet indexing compact for quicker load times.\nSelect Even to mix on the Samourai mobile app simultaneously.\nSelect Odd to mix on Samourai CLI/GUI simultaneously."/>
</Field>
</Fieldset>
</Form>
</GridPane>
</center>
</BorderPane>
@@ -9,13 +9,13 @@
<?import tornadofx.control.Form?>
<?import tornadofx.control.Fieldset?>
<?import tornadofx.control.Field?>
<?import com.sparrowwallet.sparrow.control.CopyableTextField?>
<?import javafx.collections.FXCollections?>
<?import com.sparrowwallet.sparrow.control.FiatLabel?>
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import org.controlsfx.glyphfont.Glyph?>
<?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.control.ComboBoxTextField?>
<GridPane styleClass="send-form" hgap="10.0" vgap="10.0" stylesheets="@payment.css, @send.css, @wallet.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.wallet.PaymentController">
<padding>
<Insets top="10.0" bottom="10.0" />
@@ -31,11 +31,14 @@
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
<Fieldset inputGrow="ALWAYS">
<Field text="Pay to:">
<CopyableTextField fx:id="address" styleClass="address-text-field">
<tooltip>
<Tooltip text="Address or bitcoin: URI"/>
</tooltip>
</CopyableTextField>
<StackPane>
<ComboBox fx:id="openWallets" />
<ComboBoxTextField fx:id="address" styleClass="address-text-field" comboProperty="$openWallets">
<tooltip>
<Tooltip text="Address or bitcoin: URI"/>
</tooltip>
</ComboBoxTextField>
</StackPane>
</Field>
<Field text="Label:">
<TextField fx:id="label">
@@ -38,5 +38,9 @@
}
.field-label {
-fx-pref-width: 120px;
-fx-pref-width: 130px;
}
.field-control {
-fx-pref-width: 180px;
}
@@ -11,6 +11,7 @@
<?import javafx.scene.image.Image?>
<?import javafx.geometry.Insets?>
<?import com.sparrowwallet.sparrow.control.CoinLabel?>
<?import com.sparrowwallet.sparrow.control.CopyableLabel?>
<StackPane prefHeight="460.0" prefWidth="600.0" stylesheets="@whirlpool.css, @../general.css" styleClass="whirlpool-pane" fx:controller="com.sparrowwallet.sparrow.whirlpool.WhirlpoolController" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml">
<VBox spacing="20">
<HBox styleClass="title-area">
@@ -91,7 +92,21 @@
<Insets top="20" />
</padding>
<Label text="SCODE:" styleClass="field-label" />
<TextField fx:id="scode" />
<TextField fx:id="scode" styleClass="field-control"/>
</HBox>
<HBox styleClass="field-box">
<padding>
<Insets top="10" />
</padding>
<Label text="Premix Priority:" styleClass="field-label" />
<Slider fx:id="premixPriority" snapToTicks="true" showTickLabels="true" showTickMarks="true" styleClass="field-control" />
</HBox>
<HBox styleClass="field-box">
<padding>
<Insets top="10" />
</padding>
<Label text="Premix Fee Rate:" styleClass="field-label" />
<CopyableLabel fx:id="premixFeeRate" />
</HBox>
</VBox>
<VBox fx:id="step4" spacing="15">
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB