mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-31 12:06:15 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcb83f8bfa | ||
|
|
f187603ec3 | ||
|
|
8d7308bc37 | ||
|
|
e44d1393f5 | ||
|
|
33ba472843 | ||
|
|
faa81f2273 | ||
|
|
0646c8aa28 | ||
|
|
deb47ca002 | ||
|
|
ec131bb8da | ||
|
|
31f287125f | ||
|
|
e131f645f6 | ||
|
|
eabc0da6d5 | ||
|
|
49573d1075 | ||
|
|
17093dbf72 | ||
|
|
c2b5b24702 | ||
|
|
65f1fa7cf8 | ||
|
|
cbee341544 | ||
|
|
95b1aa8e48 | ||
|
|
af89be96e5 | ||
|
|
fad960c192 | ||
|
|
1adeef04db | ||
|
|
47f925b677 | ||
|
|
5db3096386 | ||
|
|
62e98b0090 | ||
|
|
76490604ac | ||
|
|
783733b9d3 | ||
|
|
041b5aa34c | ||
|
|
33d23e9ea5 | ||
|
|
b3f6cc88f0 | ||
|
|
b912aa2eb9 | ||
|
|
d894343457 | ||
|
|
fb1e1cefda | ||
|
|
d960bdce96 | ||
|
|
fb679c0199 | ||
|
|
9ecfe0e94f | ||
|
|
1bc2f7d69f | ||
|
|
6e4d308c79 | ||
|
|
afb6037449 | ||
|
|
369983748d | ||
|
|
0d16c87b40 | ||
|
|
b59a65dcfe | ||
|
|
87cc28e0a4 | ||
|
|
1187925543 | ||
|
|
cd4edab4ae | ||
|
|
daf320f36b | ||
|
|
f6ff92865b | ||
|
|
d420c71673 | ||
|
|
07101b3ca0 | ||
|
|
00f7f3f5b3 | ||
|
|
5d2c133401 | ||
|
|
7b0dfd66a7 | ||
|
|
83719e7df2 | ||
|
|
f1b246f0b0 | ||
|
|
599880ea5c | ||
|
|
d625bab02e | ||
|
|
1676676e06 | ||
|
|
f7e603118f |
@@ -15,11 +15,11 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set up JDK 18.0.1
|
||||
- name: Set up JDK 22.0.2
|
||||
uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: 'temurin'
|
||||
java-version: '18.0.1'
|
||||
java-version: '22.0.2'
|
||||
- name: Show Build Versions
|
||||
run: ./gradlew -v
|
||||
- name: Build with Gradle
|
||||
|
||||
@@ -64,10 +64,12 @@ Usage: sparrow [options]
|
||||
Possible Values: [ERROR, WARN, INFO, DEBUG, TRACE]
|
||||
--network, -n
|
||||
Network to use
|
||||
Possible Values: [mainnet, testnet, regtest, signet]
|
||||
Possible Values: [mainnet, testnet, regtest, signet, testnet4]
|
||||
```
|
||||
|
||||
As a fallback, the network (mainnet, testnet, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
Note that testnet currently refers to testnet3.
|
||||
|
||||
As a fallback, the network (mainnet, testnet, testnet4, regtest or signet) can also be set using an environment variable `SPARROW_NETWORK`. For example:
|
||||
|
||||
`export SPARROW_NETWORK=testnet`
|
||||
|
||||
@@ -83,7 +85,7 @@ When not explicitly configured using the command line argument above, Sparrow st
|
||||
| Linux | ~/.sparrow |
|
||||
| Windows | %APPDATA%/Sparrow |
|
||||
|
||||
Testnet, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
Testnet3, testnet4, regtest and signet configurations (along with their wallets) are stored in subfolders to allow easy switching between networks.
|
||||
|
||||
## Reporting Issues
|
||||
|
||||
|
||||
+7
-7
@@ -7,7 +7,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '3.0.1'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.8.6'
|
||||
def sparrowVersion = '2.0.0'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -53,7 +53,7 @@ tasks.withType(AbstractArchiveTask) {
|
||||
}
|
||||
|
||||
javafx {
|
||||
version = "18"
|
||||
version = headless ? "18" : "22"
|
||||
modules = [ 'javafx.controls', 'javafx.fxml', 'javafx.swing', 'javafx.graphics' ]
|
||||
}
|
||||
|
||||
@@ -124,8 +124,7 @@ dependencies {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.bokmakierie:bokmakierie:1.0')
|
||||
implementation('io.samourai.code.whirlpool:whirlpool-client:1.0.6')
|
||||
implementation('io.samourai.code.wallet:java-http-client:2.0.2')
|
||||
implementation('org.eclipse.jetty:jetty-client:9.4.54.v20240208')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
@@ -215,7 +214,7 @@ jlink {
|
||||
uses 'org.eclipse.jetty.http.HttpFieldPreEncoder'
|
||||
}
|
||||
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', '2', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
options = ['--strip-native-commands', '--strip-java-debug-attributes', '--compress', 'zip-6', '--no-header-files', '--no-man-pages', '--ignore-signing-information', '--exclude-files', '**.png', '--exclude-resources', 'glob:/com.sparrowwallet.merged.module/META-INF/*']
|
||||
launcher {
|
||||
name = 'sparrow'
|
||||
jvmArgs = ["--add-opens=javafx.graphics/com.sun.javafx.css=org.controlsfx.controls",
|
||||
@@ -272,11 +271,11 @@ jlink {
|
||||
if(os.windows) {
|
||||
installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-menu-group', 'Sparrow', '--win-shortcut', '--resource-dir', 'src/main/deploy/package/windows/']
|
||||
imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico']
|
||||
installerType = "exe"
|
||||
installerType = "msi"
|
||||
}
|
||||
if(os.linux) {
|
||||
if(headless) {
|
||||
installerOptions = ['--license-file', 'LICENSE']
|
||||
installerOptions = ['--license-file', 'LICENSE', '--resource-dir', 'src/main/deploy/package/linux-headless/']
|
||||
} else {
|
||||
installerOptions += ['--resource-dir', 'src/main/deploy/package/linux/', '--linux-shortcut', '--linux-menu-group', 'Sparrow']
|
||||
}
|
||||
@@ -308,6 +307,7 @@ task removeGroupWritePermission(type: Exec) {
|
||||
task packageZipDistribution(type: Zip) {
|
||||
archiveFileName = "Sparrow-${sparrowVersion}.zip"
|
||||
destinationDirectory = file("$buildDir/jpackage")
|
||||
preserveFileTimestamps = os.macOsX
|
||||
from("$buildDir/jpackage/") {
|
||||
include "Sparrow/**"
|
||||
include "Sparrow.app/**"
|
||||
|
||||
@@ -82,7 +82,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="1.8.5"
|
||||
GIT_TAG="1.9.1"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
+1
-1
Submodule drongo updated: 3a2344f129...f8f50c0dd9
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
Package: sparrow
|
||||
Version: 2.0.0-1
|
||||
Section: utils
|
||||
Maintainer: Craig Raw <mail@sparrowwallet.com>
|
||||
Priority: optional
|
||||
Architecture: amd64
|
||||
Provides: sparrow
|
||||
Description: Sparrow
|
||||
Depends: libc6, zlib1g
|
||||
@@ -8,3 +8,4 @@ Type=Application
|
||||
Categories=Finance;Network;
|
||||
MimeType=application/psbt;application/bitcoin-transaction;application/pgp-signature;x-scheme-handler/bitcoin;x-scheme-handler/auth47;x-scheme-handler/lightning
|
||||
StartupWMClass=Sparrow
|
||||
SingleMainWindow=true
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.8.6</string>
|
||||
<string>2.0.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -7,9 +7,7 @@ import com.google.common.io.ByteSource;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTParseException;
|
||||
@@ -27,10 +25,7 @@ import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferenceGroup;
|
||||
import com.sparrowwallet.sparrow.preferences.PreferencesDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.CounterpartyDialog;
|
||||
import com.sparrowwallet.sparrow.paynym.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionData;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionView;
|
||||
@@ -187,9 +182,6 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem sweepPrivateKey;
|
||||
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@@ -397,7 +389,7 @@ public class AppController implements Initializable {
|
||||
MenuItem homeItem = new MenuItem("Home Folder...");
|
||||
homeItem.setOnAction(this::restartInHome);
|
||||
restart.getItems().add(homeItem);
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET));
|
||||
List<Network> networks = new ArrayList<>(List.of(Network.MAINNET, Network.TESTNET, Network.TESTNET4, Network.SIGNET));
|
||||
networks.remove(Network.get());
|
||||
for(Network network : networks) {
|
||||
MenuItem networkItem = new MenuItem(network.toDisplayString());
|
||||
@@ -423,10 +415,6 @@ public class AppController implements Initializable {
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
sweepPrivateKey.disableProperty().bind(Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not()));
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
configureSwitchServer();
|
||||
setServerType(Config.get().getServerType());
|
||||
@@ -1459,75 +1447,6 @@ public class AppController implements Initializable {
|
||||
optTransaction.ifPresent(transaction -> addTransactionTab(null, null, transaction));
|
||||
}
|
||||
|
||||
public void findMixingPartner(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(selectedWalletForm.getWalletId());
|
||||
if(soroban.getHdWallet() == null) {
|
||||
if(wallet.isEncrypted()) {
|
||||
Wallet copy = wallet.copy();
|
||||
WalletPasswordDialog dlg = new WalletPasswordDialog(copy.getMasterName(), WalletPasswordDialog.PasswordRequirement.LOAD);
|
||||
dlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<SecureString> password = dlg.showAndWait();
|
||||
if(password.isPresent()) {
|
||||
Storage storage = selectedWalletForm.getStorage();
|
||||
keyDerivationService = new Storage.KeyDerivationService(storage, password.get(), true);
|
||||
keyDerivationService.setOnSucceeded(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Done"));
|
||||
ECKey encryptionFullKey = keyDerivationService.getValue();
|
||||
Key key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2);
|
||||
copy.decrypt(key);
|
||||
|
||||
try {
|
||||
soroban.setHDWallet(copy);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
} finally {
|
||||
key.clear();
|
||||
encryptionFullKey.clear();
|
||||
keyDerivationService = null;
|
||||
}
|
||||
});
|
||||
keyDerivationService.setOnFailed(workerStateEvent -> {
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.END, "Failed"));
|
||||
if(keyDerivationService.getException() instanceof InvalidPasswordException) {
|
||||
Optional<ButtonType> optResponse = showErrorDialog("Invalid Password", "The wallet password was invalid. Try again?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optResponse.isPresent() && optResponse.get().equals(ButtonType.OK)) {
|
||||
Platform.runLater(() -> findMixingPartner(null));
|
||||
}
|
||||
} else {
|
||||
log.error("Error deriving wallet key", keyDerivationService.getException());
|
||||
}
|
||||
keyDerivationService = null;
|
||||
});
|
||||
EventManager.get().post(new StorageEvent(selectedWalletForm.getWalletId(), TimedEvent.Action.START, "Decrypting wallet..."));
|
||||
keyDerivationService.start();
|
||||
}
|
||||
} else {
|
||||
soroban.setHDWallet(wallet);
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
} else {
|
||||
CounterpartyDialog counterpartyDialog = new CounterpartyDialog(selectedWalletForm.getWalletId(), selectedWalletForm.getWallet());
|
||||
counterpartyDialog.initOwner(rootStack.getScene().getWindow());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
counterpartyDialog.initModality(Modality.NONE);
|
||||
}
|
||||
counterpartyDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
@@ -1727,14 +1646,6 @@ public class AppController implements Initializable {
|
||||
tabLabel.setGraphicTextGap(5.0);
|
||||
tab.setGraphic(tabLabel);
|
||||
tab.setClosable(true);
|
||||
tab.setOnCloseRequest(event -> {
|
||||
if(AppServices.getWhirlpoolServices().getWhirlpoolForMixToWallet(((WalletTabData)tab.getUserData()).getWalletForm().getWalletId()) != null) {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Close mix to wallet?", "This wallet has been configured as the final destination for mixes, and needs to be open for this to occur.\n\nAre you sure you want to close?", ButtonType.NO, ButtonType.YES);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
TabPane subTabs = new TabPane();
|
||||
subTabs.setSide(Side.LEFT);
|
||||
@@ -1961,6 +1872,25 @@ public class AppController implements Initializable {
|
||||
}
|
||||
|
||||
private void addTransactionTab(String name, File file, PSBT psbt) {
|
||||
//Add any missing previous outputs if available in open wallets
|
||||
for(PSBTInput psbtInput : psbt.getPsbtInputs()) {
|
||||
if(psbtInput.getUtxo() == null) {
|
||||
for(Wallet wallet : AppServices.get().getOpenWallets().keySet().stream().filter(Wallet::isValid).toList()) {
|
||||
TransactionOutPoint outpoint = psbtInput.getInput().getOutpoint();
|
||||
BlockTransaction blockTransaction = wallet.getWalletTransaction(outpoint.getHash());
|
||||
if(blockTransaction != null && blockTransaction.getTransaction().getOutputs().size() > outpoint.getIndex()) {
|
||||
psbtInput.setNonWitnessUtxo(blockTransaction.getTransaction());
|
||||
ScriptType type = psbtInput.getScriptType();
|
||||
if(type != null && Arrays.asList(ScriptType.WITNESS_TYPES).contains(type)) {
|
||||
psbtInput.setWitnessUtxo(blockTransaction.getTransaction().getOutputs().get((int)outpoint.getIndex()));
|
||||
psbtInput.setNonWitnessUtxo(null);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt);
|
||||
if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) {
|
||||
EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt));
|
||||
@@ -2554,7 +2484,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(true);
|
||||
showTxHex.setDisable(false);
|
||||
showPayNym.setDisable(true);
|
||||
findMixingPartner.setDisable(true);
|
||||
} else if(event instanceof WalletTabSelectedEvent) {
|
||||
WalletTabSelectedEvent walletTabEvent = (WalletTabSelectedEvent)event;
|
||||
WalletTabData walletTabData = walletTabEvent.getWalletTabData();
|
||||
@@ -2566,7 +2495,6 @@ public class AppController implements Initializable {
|
||||
showLoadingLog.setDisable(false);
|
||||
showTxHex.setDisable(true);
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !walletTabData.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(walletTabData.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2592,7 +2520,6 @@ public class AppController implements Initializable {
|
||||
if(selectedWalletForm.getWalletId().equals(event.getWalletId())) {
|
||||
exportWallet.setDisable(!event.getWallet().isValid() || selectedWalletForm.isLocked());
|
||||
showPayNym.setDisable(exportWallet.isDisable() || !event.getWallet().hasPaymentCode());
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(event.getWallet()) || !AppServices.onlineProperty().get());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3124,6 +3051,8 @@ public class AppController implements Initializable {
|
||||
public void requestWalletOpen(RequestWalletOpenEvent event) {
|
||||
if(tabs.getScene().getWindow().equals(event.getWindow())) {
|
||||
if(event.getFile() != null) {
|
||||
Optional<Tab> optExisting = tabs.getTabs().stream().filter(tab -> tab.getUserData() instanceof WalletTabData walletTabData && walletTabData.getStorage().getWalletFile().equals(event.getFile())).findFirst();
|
||||
optExisting.ifPresent(tab -> tabs.getTabs().remove(tab));
|
||||
openWalletFile(event.getFile(), true);
|
||||
} else {
|
||||
openWallet(true);
|
||||
|
||||
@@ -24,8 +24,6 @@ import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import com.sparrowwallet.sparrow.net.*;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.application.Application;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
@@ -95,11 +93,7 @@ public class AppServices {
|
||||
|
||||
private static AppServices INSTANCE;
|
||||
|
||||
private final WhirlpoolServices whirlpoolServices = new WhirlpoolServices();
|
||||
|
||||
private final SorobanServices sorobanServices = new SorobanServices();
|
||||
|
||||
private InteractionServices interactionServices;
|
||||
private final InteractionServices interactionServices;
|
||||
|
||||
private static HttpClientService httpClientService;
|
||||
|
||||
@@ -188,8 +182,6 @@ public class AppServices {
|
||||
this.application = application;
|
||||
this.interactionServices = interactionServices;
|
||||
EventManager.get().register(this);
|
||||
EventManager.get().register(whirlpoolServices);
|
||||
EventManager.get().register(sorobanServices);
|
||||
}
|
||||
|
||||
public void start() {
|
||||
@@ -534,14 +526,6 @@ public class AppServices {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
public static WhirlpoolServices getWhirlpoolServices() {
|
||||
return get().whirlpoolServices;
|
||||
}
|
||||
|
||||
public static SorobanServices getSorobanServices() {
|
||||
return get().sorobanServices;
|
||||
}
|
||||
|
||||
public static InteractionServices getInteractionServices() {
|
||||
return get().interactionServices;
|
||||
}
|
||||
@@ -589,6 +573,34 @@ public class AppServices {
|
||||
}
|
||||
}
|
||||
|
||||
public static void runAfterDelay(long delay, Runnable runnable) {
|
||||
if(delay <= 0) {
|
||||
if(Platform.isFxApplicationThread()) {
|
||||
runnable.run();
|
||||
} else {
|
||||
Platform.runLater(runnable);
|
||||
}
|
||||
} else {
|
||||
ScheduledService<Void> delayService = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
delayService.setOnSucceeded(_ -> {
|
||||
delayService.cancel();
|
||||
runnable.run();
|
||||
});
|
||||
delayService.setDelay(Duration.millis(delay));
|
||||
delayService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private static Image getWindowIcon() {
|
||||
if(windowIcon == null) {
|
||||
windowIcon = new Image(SparrowWallet.class.getResourceAsStream("/image/sparrow-icon.png"));
|
||||
@@ -1095,10 +1107,50 @@ public class AppServices {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public static final List<Network> WHIRLPOOL_NETWORKS = List.of(Network.MAINNET, Network.TESTNET);
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
&& wallet.getKeystores().get(0).getSeed().getType() == DeterministicSeed.Type.BIP39
|
||||
&& wallet.getStandardAccountType() != null
|
||||
&& StandardAccount.isMixableAccount(wallet.getStandardAccountType());
|
||||
}
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1;
|
||||
}
|
||||
|
||||
public static List<Wallet> addWhirlpoolWallets(Wallet decryptedWallet, String walletId, Storage storage) {
|
||||
List<Wallet> childWallets = new ArrayList<>();
|
||||
for(StandardAccount whirlpoolAccount : StandardAccount.WHIRLPOOL_ACCOUNTS) {
|
||||
if(decryptedWallet.getChildWallet(whirlpoolAccount) == null) {
|
||||
Wallet childWallet = decryptedWallet.addChildWallet(whirlpoolAccount);
|
||||
childWallets.add(childWallet);
|
||||
EventManager.get().post(new ChildWalletsAddedEvent(storage, decryptedWallet, childWallet));
|
||||
}
|
||||
}
|
||||
|
||||
return childWallets;
|
||||
}
|
||||
|
||||
public static Font getMonospaceFont() {
|
||||
return Font.font("Roboto Mono", 13);
|
||||
}
|
||||
|
||||
public static boolean isOnWayland() {
|
||||
if(org.controlsfx.tools.Platform.getCurrent() != org.controlsfx.tools.Platform.UNIX) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
|
||||
return waylandDisplay != null && !waylandDisplay.isEmpty();
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void newConnection(ConnectionEvent event) {
|
||||
currentBlockHeight = event.getBlockHeight();
|
||||
|
||||
@@ -89,28 +89,32 @@ public class SparrowDesktop extends Application {
|
||||
|
||||
AppController appController = AppServices.newAppWindow(stage);
|
||||
|
||||
if(createNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
final boolean showNewWallet = createNewWallet;
|
||||
//Delay opening new dialogs on Wayland
|
||||
AppServices.runAfterDelay(AppServices.isOnWayland() ? 1000 : 0, () -> {
|
||||
if(showNewWallet) {
|
||||
appController.newWallet(null);
|
||||
}
|
||||
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
List<File> recentWalletFiles = Config.get().getRecentWalletFiles();
|
||||
if(recentWalletFiles != null) {
|
||||
//Preserve wallet order as far as possible. Unencrypted wallets will still be opened first.
|
||||
List<File> encryptedWalletFiles = recentWalletFiles.stream().filter(Storage::isEncrypted).collect(Collectors.toList());
|
||||
List<File> sortedWalletFiles = new ArrayList<>(recentWalletFiles);
|
||||
sortedWalletFiles.removeAll(encryptedWalletFiles);
|
||||
sortedWalletFiles.addAll(encryptedWalletFiles);
|
||||
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
for(File walletFile : sortedWalletFiles) {
|
||||
if(walletFile.exists()) {
|
||||
appController.openWalletFile(walletFile, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
AppServices.openFileUriArgumentsAfterWalletLoading(stage);
|
||||
|
||||
AppServices.get().start();
|
||||
AppServices.get().start();
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.8.6";
|
||||
public static final String APP_VERSION = "2.0.0";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
@@ -66,6 +66,11 @@ public class SparrowWallet {
|
||||
Network.set(Network.TESTNET);
|
||||
}
|
||||
|
||||
File testnet4Flag = new File(Storage.getSparrowHome(), "network-" + Network.TESTNET4.getName());
|
||||
if(testnet4Flag.exists()) {
|
||||
Network.set(Network.TESTNET4);
|
||||
}
|
||||
|
||||
File signetFlag = new File(Storage.getSparrowHome(), "network-" + Network.SIGNET.getName());
|
||||
if(signetFlag.exists()) {
|
||||
Network.set(Network.SIGNET);
|
||||
|
||||
@@ -5,9 +5,6 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolServices;
|
||||
import javafx.collections.FXCollections;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.VBox;
|
||||
@@ -63,9 +60,9 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
}
|
||||
}
|
||||
|
||||
if(WhirlpoolServices.canWalletMix(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
if(AppServices.isWhirlpoolCompatible(masterWallet) && !masterWallet.isWhirlpoolMasterWallet()) {
|
||||
availableAccounts.add(WHIRLPOOL_PREMIX);
|
||||
} else if(WhirlpoolServices.canWatchPostmix(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
} else if(AppServices.isWhirlpoolPostmixCompatible(masterWallet) && !existingIndexes.contains(WHIRLPOOL_POSTMIX.getAccountNumber())) {
|
||||
availableAccounts.add(WHIRLPOOL_POSTMIX);
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ public class AddressCell extends TreeTableCell<Entry, UtxoEntry.AddressStatus> {
|
||||
tooltip.setShowDelay(Duration.millis(250));
|
||||
tooltip.setText(getTooltipText(utxoEntry, addressStatus.isDuplicate(), addressStatus.isDustAttack()));
|
||||
setTooltip(tooltip);
|
||||
getStyleClass().add("address-cell");
|
||||
|
||||
if(addressStatus.isDustAttack()) {
|
||||
setGraphic(getDustAttackHyperlink(utxoEntry));
|
||||
|
||||
@@ -77,7 +77,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setEqualPreferredColumnWidths();
|
||||
|
||||
addressCol.setSortType(TreeTableColumn.SortType.ASCENDING);
|
||||
getSortOrder().add(addressCol);
|
||||
|
||||
@@ -31,6 +31,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
private BitcoinUnit bitcoinUnit;
|
||||
private UnitFormat unitFormat;
|
||||
private CurrencyRate currencyRate;
|
||||
protected static final double STANDARD_WIDTH = 100.0;
|
||||
|
||||
public BitcoinUnit getBitcoinUnit() {
|
||||
return bitcoinUnit;
|
||||
@@ -143,4 +144,12 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
getSortOrder().add(column);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
protected void setEqualPreferredColumnWidths() {
|
||||
for(TreeTableColumn<?, ?> column : getColumns()) {
|
||||
column.setPrefWidth(STANDARD_WIDTH);
|
||||
}
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@ package com.sparrowwallet.sparrow.control;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.PdfUtils;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.control.Button;
|
||||
|
||||
public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur) {
|
||||
super(ur);
|
||||
public DescriptorQRDisplayDialog(String walletName, String outputDescriptor, UR ur, BBQR bbqr, boolean selectBbqrButton) {
|
||||
super(ur, bbqr, false, false, selectBbqrButton);
|
||||
|
||||
DialogPane dialogPane = getDialogPane();
|
||||
final ButtonType pdfButtonType = new javafx.scene.control.ButtonType("Save PDF...", ButtonBar.ButtonData.HELP_2);
|
||||
@@ -19,7 +20,7 @@ public class DescriptorQRDisplayDialog extends QRDisplayDialog {
|
||||
pdfButton.setGraphicTextGap(5);
|
||||
pdfButton.setGraphic(getGlyph(FontAwesome5.Glyph.FILE_PDF));
|
||||
pdfButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur);
|
||||
PdfUtils.saveOutputDescriptor(walletName, outputDescriptor, ur, isUseBbqrEncoding() ? bbqr : null);
|
||||
event.consume();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -841,7 +841,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
}
|
||||
} else if(entry instanceof WalletSummaryDialog.UnconfirmedEntry) {
|
||||
cell.getStyleClass().add("unconfirmed-row");
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry) {
|
||||
} else if(entry instanceof WalletSummaryDialog.SummaryEntry || entry instanceof WalletSummaryDialog.AllSummaryEntry) {
|
||||
cell.getStyleClass().add("summary-row");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,11 +171,18 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
} else if(exporter instanceof Bip129) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, true, false);
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, false);
|
||||
} else if(exporter instanceof Descriptor) {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
boolean selectBbqrOption = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr());
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR());
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, selectBbqrOption);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
qrDisplayDialog = new QRDisplayDialog(ur, bbqr, false, false, true);
|
||||
} else {
|
||||
qrDisplayDialog = new QRDisplayDialog(outputStream.toString(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
@@ -17,10 +17,13 @@ import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import javafx.application.Platform;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.Image;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
import org.controlsfx.control.SegmentedButton;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
@@ -32,17 +35,21 @@ import tornadofx.control.Field;
|
||||
import tornadofx.control.Fieldset;
|
||||
import tornadofx.control.Form;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.security.SignatureException;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.showErrorDialog;
|
||||
|
||||
public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private static final Logger log = LoggerFactory.getLogger(MessageSignDialog.class);
|
||||
|
||||
private static final Pattern signedMessagePattern = Pattern.compile("-----BEGIN BITCOIN SIGNED MESSAGE-----\\r?\\n(.*)\\r?\\n-----BEGIN BITCOIN SIGNATURE-----\\r?\\n(.*)\\r?\\n(.*)\\r?\\n-----END BITCOIN SIGNATURE-----\r?\n?");
|
||||
|
||||
private final TextField address;
|
||||
private final TextArea message;
|
||||
private final TextArea signature;
|
||||
@@ -104,7 +111,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
this.wallet = wallet;
|
||||
this.walletNode = walletNode;
|
||||
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
final DialogPane dialogPane = new MessageSignDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("dialog.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
@@ -199,13 +207,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(showQrButtonType, signButtonType, verifyButtonType, doneButtonType);
|
||||
|
||||
Button showQrButton = (Button) dialogPane.lookupButton(showQrButtonType);
|
||||
showQrButton.setDisable(wallet == null);
|
||||
showQrButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
showQrButton.setGraphicTextGap(5);
|
||||
showQrButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
Node showQrButton = dialogPane.lookupButton(showQrButtonType);
|
||||
|
||||
Button signButton = (Button) dialogPane.lookupButton(signButtonType);
|
||||
signButton.setDisable(!canSign);
|
||||
@@ -267,7 +269,7 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(ButtonBar.ButtonData.CANCEL_CLOSE));
|
||||
AppServices.moveToActiveWindowScreen(this);
|
||||
setResultConverter(dialogButton -> dialogButton == showQrButtonType || dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
setResultConverter(dialogButton -> dialogButton == signButtonType || dialogButton == verifyButtonType ? ButtonBar.ButtonData.APPLY : dialogButton.getButtonData());
|
||||
|
||||
Platform.runLater(() -> {
|
||||
if(address.getText().isEmpty()) {
|
||||
@@ -495,6 +497,82 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private void exportFile() {
|
||||
if(walletNode == null) {
|
||||
AppServices.showErrorDialog("Address not in wallet", "The provided address is not present in the currently selected wallet.");
|
||||
return;
|
||||
}
|
||||
|
||||
StringJoiner joiner = new StringJoiner("\n");
|
||||
joiner.add(message.getText().trim().replaceAll("\r*\n*", ""));
|
||||
//Note we can expect a single keystore due to the check in the constructor
|
||||
KeyDerivation firstDerivation = walletNode.getWallet().getKeystores().get(0).getKeyDerivation();
|
||||
joiner.add(KeyDerivation.writePath(firstDerivation.extend(walletNode.getDerivation()).getDerivation(), true));
|
||||
joiner.add(walletNode.getWallet().getScriptType().toString());
|
||||
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save Text File");
|
||||
fileChooser.setInitialFileName("signmessage.txt");
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showSaveDialog(window);
|
||||
if(file != null) {
|
||||
if(!file.getName().toLowerCase(Locale.ROOT).endsWith(".txt")) {
|
||||
file = new File(file.getAbsolutePath() + ".txt");
|
||||
}
|
||||
|
||||
try(BufferedWriter writer = new BufferedWriter(new FileWriter(file, StandardCharsets.UTF_8))) {
|
||||
writer.write(joiner.toString());
|
||||
} catch(IOException e) {
|
||||
log.error("Error saving signing message", e);
|
||||
AppServices.showErrorDialog("Error saving signing message", "Cannot write to " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void importFile() {
|
||||
Stage window = new Stage();
|
||||
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Open Signed Text File");
|
||||
fileChooser.getExtensionFilters().addAll(
|
||||
new FileChooser.ExtensionFilter("All Files", org.controlsfx.tools.Platform.getCurrent().equals(org.controlsfx.tools.Platform.UNIX) ? "*" : "*.*"),
|
||||
new FileChooser.ExtensionFilter("Text Files", "*.txt")
|
||||
);
|
||||
|
||||
AppServices.moveToActiveWindowScreen(window, 800, 450);
|
||||
File file = fileChooser.showOpenDialog(window);
|
||||
|
||||
if(file != null) {
|
||||
try {
|
||||
String content = Files.readString(file.toPath(), StandardCharsets.UTF_8);
|
||||
Matcher matcher = signedMessagePattern.matcher(content);
|
||||
if(matcher.matches()) {
|
||||
String signedMessage = matcher.group(1);
|
||||
String signedAddress = matcher.group(2);
|
||||
String signedSignature = matcher.group(3);
|
||||
|
||||
if(!message.getText().isEmpty() && !signedMessage.trim().equals(message.getText().trim().replaceAll("\r*\n*", ""))) {
|
||||
AppServices.showErrorDialog("Incorrect Message", "The file contained a different message of:\n\n" + signedMessage);
|
||||
return;
|
||||
} else if(!signedAddress.trim().equals(address.getText().trim())) {
|
||||
AppServices.showErrorDialog("Incorrect Address", "The file contained a different address of:\n\n" + signedAddress);
|
||||
return;
|
||||
}
|
||||
|
||||
message.setText(signedMessage);
|
||||
signature.setText(signedSignature);
|
||||
} else {
|
||||
signature.setText(content);
|
||||
}
|
||||
} catch(IOException e) {
|
||||
log.error("Error loading signed message", e);
|
||||
AppServices.showErrorDialog("Error loading signed message", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Glyph getSignGlyph() {
|
||||
if(wallet != null) {
|
||||
if(wallet.containsSource(KeystoreSource.HW_USB)) {
|
||||
@@ -539,4 +617,37 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
decryptWalletService.start();
|
||||
}
|
||||
}
|
||||
|
||||
private class MessageSignDialogPane extends DialogPane {
|
||||
@Override
|
||||
protected Node createButton(ButtonType buttonType) {
|
||||
if(buttonType.getButtonData() == ButtonBar.ButtonData.LEFT) {
|
||||
SplitMenuButton signByButton = new SplitMenuButton();
|
||||
signByButton.setText("Sign by QR");
|
||||
signByButton.setDisable(wallet == null);
|
||||
signByButton.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.QRCODE)));
|
||||
signByButton.setGraphicTextGap(5);
|
||||
signByButton.setOnAction(event -> {
|
||||
showQr();
|
||||
});
|
||||
MenuItem exportFile = new MenuItem("Sign by File...");
|
||||
exportFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_EXPORT)));
|
||||
exportFile.setOnAction(event -> {
|
||||
exportFile();
|
||||
});
|
||||
MenuItem importFile = new MenuItem("Load Signed File...");
|
||||
importFile.setGraphic(getGlyph(new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.FILE_IMPORT)));
|
||||
importFile.setOnAction(event -> {
|
||||
importFile();
|
||||
});
|
||||
signByButton.getItems().addAll(exportFile, importFile);
|
||||
final ButtonBar.ButtonData buttonData = buttonType.getButtonData();
|
||||
ButtonBar.setButtonData(signByButton, buttonData);
|
||||
|
||||
return signByButton;
|
||||
}
|
||||
|
||||
return super.createButton(buttonType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.mix.listener.MixStep;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import com.sparrowwallet.sparrow.wallet.UtxoEntry;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import com.sparrowwallet.sparrow.whirlpool.WhirlpoolException;
|
||||
import javafx.animation.Timeline;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.util.Duration;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.tools.Platform;
|
||||
|
||||
import java.util.Locale;
|
||||
|
||||
public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
private static final int ERROR_DISPLAY_MILLIS = 5 * 60 * 1000;
|
||||
|
||||
public MixStatusCell() {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
@@ -41,167 +25,9 @@ public class MixStatusCell extends TreeTableCell<Entry, UtxoEntry.MixStatus> {
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(Integer.toString(mixStatus.getMixesDone()));
|
||||
if(mixStatus.getNextMixUtxo() == null) {
|
||||
setContextMenu(new MixStatusContextMenu(mixStatus.getUtxoEntry(), mixStatus.getMixProgress() != null && mixStatus.getMixProgress().getMixStep() != MixStep.FAIL));
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
}
|
||||
|
||||
if(mixStatus.getNextMixUtxo() != null) {
|
||||
setMixSuccess(mixStatus.getNextMixUtxo());
|
||||
} else if(mixStatus.getMixFailReason() != null) {
|
||||
setMixFail(mixStatus.getMixFailReason(), mixStatus.getMixError(), mixStatus.getMixErrorTimestamp());
|
||||
} else if(mixStatus.getMixProgress() != null) {
|
||||
setMixProgress(mixStatus.getUtxoEntry(), mixStatus.getMixProgress());
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setMixSuccess(Utxo nextMixUtxo) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(-1);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText("Waiting for broadcast of " + nextMixUtxo.getHash().substring(0, 8) + "..." + ":" + nextMixUtxo.getIndex() );
|
||||
setTooltip(tt);
|
||||
}
|
||||
|
||||
private void setMixFail(MixFailReason mixFailReason, String mixError, Long mixErrorTimestamp) {
|
||||
if(mixFailReason.isError()) {
|
||||
long elapsed = mixErrorTimestamp == null ? 0L : System.currentTimeMillis() - mixErrorTimestamp;
|
||||
if(elapsed >= ERROR_DISPLAY_MILLIS) {
|
||||
//Old error, don't set again.
|
||||
return;
|
||||
}
|
||||
|
||||
Glyph failGlyph = getFailGlyph();
|
||||
setGraphic(failGlyph);
|
||||
Tooltip tt = new Tooltip();
|
||||
tt.setText(mixFailReason.getMessage() + (mixError == null ? "" : ": " + mixError) +
|
||||
"\nMix failures are generally caused by peers disconnecting during a mix." +
|
||||
"\nMake sure your internet connection is stable and the computer is configured to prevent sleeping." +
|
||||
"\nTo prevent sleeping, use the " + getPlatformSleepConfig() + " or enable the function in the Tools menu.");
|
||||
setTooltip(tt);
|
||||
|
||||
Duration fadeDuration = Duration.millis(ERROR_DISPLAY_MILLIS - elapsed);
|
||||
double fadeFromValue = 1.0 - ((double)elapsed / ERROR_DISPLAY_MILLIS);
|
||||
Timeline timeline = AnimationUtil.getSlowFadeOut(failGlyph, fadeDuration, fadeFromValue, 10);
|
||||
timeline.setOnFinished(event -> {
|
||||
setTooltip(null);
|
||||
});
|
||||
timeline.play();
|
||||
} else {
|
||||
setContextMenu(null);
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPlatformSleepConfig() {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.OSX) {
|
||||
return "OSX System Preferences";
|
||||
} else if(platform == Platform.WINDOWS) {
|
||||
return "Windows Control Panel";
|
||||
}
|
||||
|
||||
return "system power settings";
|
||||
}
|
||||
|
||||
private void setMixProgress(UtxoEntry utxoEntry, MixProgress mixProgress) {
|
||||
if(mixProgress.getMixStep() != MixStep.FAIL) {
|
||||
ProgressIndicator progressIndicator = getProgressIndicator();
|
||||
progressIndicator.setProgress(mixProgress.getMixStep().getProgressPercent() == 100 ? -1 : mixProgress.getMixStep().getProgressPercent() / 100.0);
|
||||
setGraphic(progressIndicator);
|
||||
Tooltip tt = new Tooltip();
|
||||
String status = mixProgress.getMixStep().getMessage().replaceAll("_", " ");
|
||||
status = status.substring(0, 1).toUpperCase(Locale.ROOT) + status.substring(1).toLowerCase(Locale.ROOT);
|
||||
if(mixProgress.getMixStep() == MixStep.REGISTER_INPUT) {
|
||||
status += "\n\nThis progress is normal for one mixing UTXO per pool while waiting to be randomly selected for a mix.\n" +
|
||||
"This may take hours or days, and time in the pool is generally more important than individual number of mixes.\n" +
|
||||
"Each UTXO's anonymity set is dependent not only on its own mix count, but that of its peers as well.";
|
||||
}
|
||||
tt.setText(status);
|
||||
setTooltip(tt);
|
||||
} else {
|
||||
setGraphic(null);
|
||||
setTooltip(null);
|
||||
}
|
||||
}
|
||||
|
||||
private ProgressIndicator getProgressIndicator() {
|
||||
ProgressIndicator progressIndicator;
|
||||
if(getGraphic() instanceof ProgressIndicator) {
|
||||
progressIndicator = (ProgressIndicator)getGraphic();
|
||||
} else {
|
||||
progressIndicator = new ProgressBar();
|
||||
}
|
||||
|
||||
return progressIndicator;
|
||||
}
|
||||
|
||||
private static Glyph getMixGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.RANDOM);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
private static Glyph getStopGlyph() {
|
||||
Glyph copyGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.STOP_CIRCLE);
|
||||
copyGlyph.setFontSize(12);
|
||||
return copyGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getFailGlyph() {
|
||||
Glyph failGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
failGlyph.getStyleClass().add("fail-warning");
|
||||
failGlyph.setFontSize(12);
|
||||
return failGlyph;
|
||||
}
|
||||
|
||||
private static class MixStatusContextMenu extends ContextMenu {
|
||||
public MixStatusContextMenu(UtxoEntry utxoEntry, boolean isMixing) {
|
||||
Whirlpool pool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(isMixing) {
|
||||
MenuItem mixStop = new MenuItem("Stop Mixing");
|
||||
if(pool != null) {
|
||||
mixStop.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
mixStop.setGraphic(getStopGlyph());
|
||||
mixStop.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mixStop(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error stopping mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixStop);
|
||||
} else {
|
||||
MenuItem mixNow = new MenuItem("Mix Now");
|
||||
if(pool != null) {
|
||||
mixNow.disableProperty().bind(pool.mixingProperty().not());
|
||||
}
|
||||
|
||||
mixNow.setGraphic(getMixGlyph());
|
||||
mixNow.setOnAction(event -> {
|
||||
hide();
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(utxoEntry.getWallet());
|
||||
if(whirlpool != null) {
|
||||
try {
|
||||
whirlpool.mix(utxoEntry.getHashIndex());
|
||||
} catch(WhirlpoolException e) {
|
||||
AppServices.showErrorDialog("Error mixing UTXO", e.getMessage());
|
||||
}
|
||||
}
|
||||
});
|
||||
getItems().add(mixNow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import javafx.beans.property.SimpleListProperty;
|
||||
@@ -15,10 +16,13 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
private final DeterministicSeed.Type type;
|
||||
|
||||
public MnemonicKeystoreDisplayPane(Keystore keystore) {
|
||||
super(keystore.getSeed().getType().getName(), keystore.getSeed().needsPassphrase() && (keystore.getSeed().getPassphrase() == null || keystore.getSeed().getPassphrase().length() > 0) ? "Passphrase entered" : "No passphrase", "", "image/" + WalletModel.SEED.getType() + ".png");
|
||||
showHideLink.setVisible(false);
|
||||
buttonBox.getChildren().clear();
|
||||
this.type = keystore.getSeed().getType();
|
||||
|
||||
showWordList(keystore.getSeed());
|
||||
}
|
||||
@@ -29,7 +33,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords / 3);
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
@@ -43,7 +47,7 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
@@ -57,4 +61,9 @@ public class MnemonicKeystoreDisplayPane extends MnemonicKeystorePane {
|
||||
stackPane.getChildren().add(vBox);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(type);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,10 @@ package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Bip39MnemonicCode;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Slip39MnemonicCode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.IntegerProperty;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
@@ -111,23 +113,9 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntry.getEditor().setText(words.get(i));
|
||||
wordEntry.getEditor().setEditable(false);
|
||||
} else {
|
||||
ScheduledService<Void> service = new ScheduledService<>() {
|
||||
@Override
|
||||
protected Task<Void> createTask() {
|
||||
return new Task<>() {
|
||||
@Override
|
||||
protected Void call() {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
service.setDelay(Duration.millis(500));
|
||||
service.setOnSucceeded(event1 -> {
|
||||
service.cancel();
|
||||
AppServices.runAfterDelay(500, () -> {
|
||||
Platform.runLater(() -> wordEntry.getEditor().requestFocus());
|
||||
});
|
||||
service.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -153,6 +141,10 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
protected void showWordList(DeterministicSeed seed) {
|
||||
List<String> words = seed.getMnemonicCode();
|
||||
showWordList(words);
|
||||
}
|
||||
|
||||
protected void showWordList(List<String> words) {
|
||||
setContent(getMnemonicWordsEntry(words.size(), true, true));
|
||||
setExpanded(true);
|
||||
|
||||
@@ -175,7 +167,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
vBox.setSpacing(10);
|
||||
|
||||
wordsPane = new TilePane();
|
||||
wordsPane.setPrefRows(numWords/3);
|
||||
wordsPane.setPrefRows(Math.ceilDiv(numWords, 3));
|
||||
wordsPane.setHgap(10);
|
||||
wordsPane.setVgap(10);
|
||||
wordsPane.setOrientation(Orientation.VERTICAL);
|
||||
@@ -189,7 +181,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
wordEntriesProperty = new SimpleListProperty<>(wordEntryList);
|
||||
List<WordEntry> wordEntries = new ArrayList<>(numWords);
|
||||
for(int i = 0; i < numWords; i++) {
|
||||
wordEntries.add(new WordEntry(i, wordEntryList));
|
||||
wordEntries.add(new WordEntry(i, wordEntryList, getWordlistProvider()));
|
||||
}
|
||||
for(int i = 0; i < numWords - 1; i++) {
|
||||
wordEntries.get(i).setNextEntry(wordEntries.get(i + 1));
|
||||
@@ -215,7 +207,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
buttonPane.getChildren().add(leftBox);
|
||||
AnchorPane.setLeftAnchor(leftBox, 0.0);
|
||||
|
||||
validLabel = new Label("Valid checksum", getValidGlyph());
|
||||
validLabel = new Label("Valid checksum", GlyphUtils.getSuccessGlyph());
|
||||
validLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
validLabel.setGraphicTextGap(5.0);
|
||||
validLabel.managedProperty().bind(validLabel.visibleProperty());
|
||||
@@ -224,7 +216,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
AnchorPane.setTopAnchor(validLabel, 5.0);
|
||||
AnchorPane.setLeftAnchor(validLabel, 0.0);
|
||||
|
||||
invalidLabel = new Label("Invalid checksum", getInvalidGlyph());
|
||||
invalidLabel = new Label("Invalid checksum", GlyphUtils.getInvalidGlyph());
|
||||
invalidLabel.setContentDisplay(ContentDisplay.LEFT);
|
||||
invalidLabel.setGraphicTextGap(5.0);
|
||||
invalidLabel.managedProperty().bind(invalidLabel.visibleProperty());
|
||||
@@ -242,7 +234,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
empty = false;
|
||||
}
|
||||
|
||||
if(!WordEntry.isValid(word)) {
|
||||
if(!getWordlistProvider().isValid(word)) {
|
||||
validWords = false;
|
||||
}
|
||||
}
|
||||
@@ -278,13 +270,20 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
//nothing by default
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.BIP39);
|
||||
}
|
||||
|
||||
protected WordlistProvider getWordListProvider(DeterministicSeed.Type type) {
|
||||
return type == DeterministicSeed.Type.SLIP39 ? new Slip39WordlistProvider() : new Bip39WordlistProvider();
|
||||
}
|
||||
|
||||
protected static class WordEntry extends HBox {
|
||||
private static List<String> wordList;
|
||||
private final TextField wordField;
|
||||
private WordEntry nextEntry;
|
||||
private TextField nextField;
|
||||
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList) {
|
||||
public WordEntry(int wordNumber, ObservableList<String> wordEntryList, WordlistProvider wordlistProvider) {
|
||||
super();
|
||||
setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -302,7 +301,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
for(int i = 0; i < words.length; i++) {
|
||||
String word = words[i];
|
||||
if(entry.nextField != null) {
|
||||
if(i == words.length - 2 && isValid(word)) {
|
||||
if(i == words.length - 2 && wordlistProvider.isValid(word)) {
|
||||
label.requestFocus();
|
||||
} else {
|
||||
entry.nextField.requestFocus();
|
||||
@@ -335,8 +334,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
});
|
||||
wordField.setTextFormatter(formatter);
|
||||
|
||||
wordList = Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordList, wordNumber, wordEntryList));
|
||||
AutoCompletionBinding<String> autoCompletionBinding = TextFields.bindAutoCompletion(wordField, new WordlistSuggestionProvider(wordlistProvider, wordNumber, wordEntryList));
|
||||
autoCompletionBinding.setDelay(50);
|
||||
autoCompletionBinding.setOnAutoCompleted(event -> {
|
||||
if(nextField != null) {
|
||||
@@ -357,7 +355,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(wordField, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordList.contains(newValue))
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid word", (newValue.length() > 0 || !lastWord) && !wordlistProvider.isValid(newValue))
|
||||
));
|
||||
|
||||
wordField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -378,28 +376,24 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
public void setNextField(TextField field) {
|
||||
this.nextField = field;
|
||||
}
|
||||
|
||||
public static boolean isValid(String word) {
|
||||
return wordList.contains(word);
|
||||
}
|
||||
}
|
||||
|
||||
protected static class WordlistSuggestionProvider implements Callback<AutoCompletionBinding.ISuggestionRequest, Collection<String>> {
|
||||
private final List<String> wordList;
|
||||
private final WordlistProvider wordlistProvider;
|
||||
private final int wordNumber;
|
||||
private final ObservableList<String> wordEntryList;
|
||||
|
||||
public WordlistSuggestionProvider(List<String> wordList, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordList = wordList;
|
||||
public WordlistSuggestionProvider(WordlistProvider wordlistProvider, int wordNumber, ObservableList<String> wordEntryList) {
|
||||
this.wordlistProvider = wordlistProvider;
|
||||
this.wordNumber = wordNumber;
|
||||
this.wordEntryList = wordEntryList;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> call(AutoCompletionBinding.ISuggestionRequest request) {
|
||||
if(wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
if(wordlistProvider.supportsPossibleLastWords() && wordNumber == wordEntryList.size() - 1 && allPreviousWordsValid()) {
|
||||
try {
|
||||
List<String> possibleLastWords = Bip39MnemonicCode.INSTANCE.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
List<String> possibleLastWords = wordlistProvider.getPossibleLastWords(wordEntryList.subList(0, wordEntryList.size() - 1));
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
possibleLastWords.removeIf(s -> !s.startsWith(request.getUserText()));
|
||||
}
|
||||
@@ -412,7 +406,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
List<String> suggestions = new ArrayList<>();
|
||||
if(!request.getUserText().isEmpty()) {
|
||||
for(String word : wordList) {
|
||||
for(String word : wordlistProvider.getWordlist()) {
|
||||
if(word.startsWith(request.getUserText())) {
|
||||
suggestions.add(word);
|
||||
}
|
||||
@@ -424,7 +418,7 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
|
||||
private boolean allPreviousWordsValid() {
|
||||
for(int i = 0; i < wordEntryList.size() - 1; i++) {
|
||||
if(!WordEntry.isValid(wordEntryList.get(i))) {
|
||||
if(!wordlistProvider.isValid(wordEntryList.get(i))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -485,17 +479,53 @@ public class MnemonicKeystorePane extends TitledDescriptionPane {
|
||||
}
|
||||
}
|
||||
|
||||
public static Glyph getValidGlyph() {
|
||||
Glyph validGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CHECK_CIRCLE);
|
||||
validGlyph.getStyleClass().add("success");
|
||||
validGlyph.setFontSize(12);
|
||||
return validGlyph;
|
||||
protected interface WordlistProvider {
|
||||
List<String> getWordlist();
|
||||
boolean isValid(String word);
|
||||
boolean supportsPossibleLastWords();
|
||||
List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
private static class Bip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Bip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException {
|
||||
return Bip39MnemonicCode.INSTANCE.getPossibleLastWords(previousWords);
|
||||
}
|
||||
}
|
||||
|
||||
private static class Slip39WordlistProvider implements WordlistProvider {
|
||||
@Override
|
||||
public List<String> getWordlist() {
|
||||
return Slip39MnemonicCode.INSTANCE.getWordList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isValid(String word) {
|
||||
return getWordlist().contains(word);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsPossibleLastWords() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getPossibleLastWords(List<String> previousWords) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,319 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.MnemonicException;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.event.KeystoreImportEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.sparrow.io.KeystoreMnemonicShareImport;
|
||||
import com.sparrowwallet.sparrow.io.Slip39;
|
||||
import javafx.beans.property.SimpleIntegerProperty;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.Priority;
|
||||
import org.controlsfx.glyphfont.Glyph;
|
||||
import org.controlsfx.validation.ValidationResult;
|
||||
import org.controlsfx.validation.ValidationSupport;
|
||||
import org.controlsfx.validation.Validator;
|
||||
import org.controlsfx.validation.decoration.StyleClassValidationDecoration;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class MnemonicShareKeystoreImportPane extends MnemonicKeystorePane {
|
||||
protected final Wallet wallet;
|
||||
private final KeystoreMnemonicShareImport importer;
|
||||
private final KeyDerivation defaultDerivation;
|
||||
private final List<List<String>> mnemonicShares = new ArrayList<>();
|
||||
|
||||
private SplitMenuButton importButton;
|
||||
|
||||
private Button calculateButton;
|
||||
private Button backButton;
|
||||
private Button nextButton;
|
||||
private int currentShare;
|
||||
|
||||
public MnemonicShareKeystoreImportPane(Wallet wallet, KeystoreMnemonicShareImport importer, KeyDerivation defaultDerivation) {
|
||||
super(importer.getName(), "Enter seed share", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png");
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.defaultDerivation = defaultDerivation;
|
||||
|
||||
createImportButton();
|
||||
buttonBox.getChildren().add(importButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
createEnterMnemonicButton();
|
||||
return enterMnemonicButton;
|
||||
}
|
||||
|
||||
private void createEnterMnemonicButton() {
|
||||
enterMnemonicButton = new SplitMenuButton();
|
||||
enterMnemonicButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
enterMnemonicButton.setText("Use 20 Words");
|
||||
defaultWordSizeProperty = new SimpleIntegerProperty(20);
|
||||
defaultWordSizeProperty.addListener((observable, oldValue, newValue) -> {
|
||||
enterMnemonicButton.setText("Use " + newValue + " Words");
|
||||
});
|
||||
enterMnemonicButton.setOnAction(event -> {
|
||||
resetShares();
|
||||
enterMnemonic(defaultWordSizeProperty.get());
|
||||
});
|
||||
int[] numberWords = new int[] {20, 33};
|
||||
for(int i = 0; i < numberWords.length; i++) {
|
||||
MenuItem item = new MenuItem("Use " + numberWords[i] + " Words");
|
||||
final int words = numberWords[i];
|
||||
item.setOnAction(event -> {
|
||||
resetShares();
|
||||
defaultWordSizeProperty.set(words);
|
||||
enterMnemonic(words);
|
||||
});
|
||||
enterMnemonicButton.getItems().add(item);
|
||||
}
|
||||
enterMnemonicButton.managedProperty().bind(enterMnemonicButton.visibleProperty());
|
||||
}
|
||||
|
||||
protected List<Node> createRightButtons() {
|
||||
calculateButton = new Button("Create Keystore");
|
||||
calculateButton.setDefaultButton(true);
|
||||
calculateButton.setOnAction(event -> {
|
||||
prepareImport();
|
||||
});
|
||||
calculateButton.managedProperty().bind(calculateButton.visibleProperty());
|
||||
calculateButton.setTooltip(new Tooltip("Create the keystore from the provided shares"));
|
||||
calculateButton.setVisible(false);
|
||||
|
||||
backButton = new Button("Back");
|
||||
backButton.setOnAction(event -> {
|
||||
lastShare();
|
||||
});
|
||||
backButton.managedProperty().bind(backButton.visibleProperty());
|
||||
backButton.setTooltip(new Tooltip("Display the last share added"));
|
||||
backButton.setVisible(currentShare > 0);
|
||||
|
||||
nextButton = new Button("Next");
|
||||
nextButton.setOnAction(event -> {
|
||||
nextShare();
|
||||
});
|
||||
nextButton.managedProperty().bind(nextButton.visibleProperty());
|
||||
nextButton.setTooltip(new Tooltip("Add the next share"));
|
||||
nextButton.visibleProperty().bind(calculateButton.visibleProperty().not());
|
||||
nextButton.setDefaultButton(true);
|
||||
nextButton.setDisable(true);
|
||||
|
||||
return List.of(backButton, nextButton, calculateButton);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void enterMnemonic(int numWords) {
|
||||
super.enterMnemonic(numWords);
|
||||
setDescription("Enter existing share");
|
||||
}
|
||||
|
||||
private void resetShares() {
|
||||
currentShare = 0;
|
||||
mnemonicShares.clear();
|
||||
}
|
||||
|
||||
private void lastShare() {
|
||||
currentShare--;
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
}
|
||||
|
||||
private void nextShare() {
|
||||
if(currentShare == mnemonicShares.size()) {
|
||||
mnemonicShares.add(wordEntriesProperty.get());
|
||||
} else {
|
||||
mnemonicShares.set(currentShare, wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
currentShare++;
|
||||
|
||||
if(currentShare < mnemonicShares.size()) {
|
||||
showWordList(mnemonicShares.get(currentShare));
|
||||
} else {
|
||||
setContent(getMnemonicWordsEntry(defaultWordSizeProperty.get(), true, true));
|
||||
}
|
||||
setExpanded(true);
|
||||
}
|
||||
|
||||
protected void onWordChange(boolean empty, boolean validWords, boolean validChecksum) {
|
||||
boolean validSet = false;
|
||||
boolean complete = false;
|
||||
if(!empty && validWords) {
|
||||
try {
|
||||
Share.fromMnemonic(String.join(" ", wordEntriesProperty.get()));
|
||||
validChecksum = true;
|
||||
|
||||
List<List<String>> existing = new ArrayList<>(mnemonicShares);
|
||||
if(currentShare >= mnemonicShares.size()) {
|
||||
existing.add(wordEntriesProperty.get());
|
||||
}
|
||||
|
||||
importer.getKeystore(wallet.getScriptType().getDefaultDerivation(), existing, passphraseProperty.get());
|
||||
validSet = true;
|
||||
complete = true;
|
||||
} catch(MnemonicException e) {
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(Slip39.Slip39ProgressException e) {
|
||||
validSet = true;
|
||||
invalidLabel.setText(e.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
} catch(ImportException e) {
|
||||
if(e.getCause() instanceof MnemonicException mnemonicException) {
|
||||
invalidLabel.setText(mnemonicException.getTitle());
|
||||
invalidLabel.setTooltip(new Tooltip(mnemonicException.getMessage()));
|
||||
} else {
|
||||
invalidLabel.setText("Import Error");
|
||||
invalidLabel.setTooltip(new Tooltip(e.getMessage()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calculateButton.setVisible(complete);
|
||||
backButton.setVisible(currentShare > 0 && !complete);
|
||||
nextButton.setDisable(!validChecksum || !validSet);
|
||||
validLabel.setVisible(complete);
|
||||
validLabel.setText(mnemonicShares.isEmpty() ? "Valid checksum" : "Completed share set");
|
||||
invalidLabel.setVisible(!complete && !empty);
|
||||
invalidLabel.setGraphic(validChecksum && validSet ? getIncompleteGlyph() : GlyphUtils.getFailureGlyph());
|
||||
}
|
||||
|
||||
private void createImportButton() {
|
||||
importButton = new SplitMenuButton();
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setText("Import Keystore");
|
||||
importButton.getStyleClass().add("default-button");
|
||||
importButton.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(getDefaultDerivation(), false);
|
||||
});
|
||||
String[] accounts = new String[] {"Import Default Account #0", "Import Account #1", "Import Account #2", "Import Account #3", "Import Account #4", "Import Account #5", "Import Account #6", "Import Account #7", "Import Account #8", "Import Account #9"};
|
||||
int scriptAccountsLength = ScriptType.P2SH.equals(wallet.getScriptType()) ? 1 : accounts.length;
|
||||
for(int i = 0; i < scriptAccountsLength; i++) {
|
||||
MenuItem item = new MenuItem(accounts[i]);
|
||||
final List<ChildNumber> derivation = wallet.getScriptType().getDefaultDerivation(i);
|
||||
item.setOnAction(event -> {
|
||||
importButton.setDisable(true);
|
||||
importKeystore(derivation, false);
|
||||
});
|
||||
importButton.getItems().add(item);
|
||||
}
|
||||
|
||||
importButton.managedProperty().bind(importButton.visibleProperty());
|
||||
importButton.setVisible(false);
|
||||
}
|
||||
|
||||
private List<ChildNumber> getDefaultDerivation() {
|
||||
return defaultDerivation == null || defaultDerivation.getDerivation().isEmpty() ? wallet.getScriptType().getDefaultDerivation() : defaultDerivation.getDerivation();
|
||||
}
|
||||
|
||||
private void prepareImport() {
|
||||
nextShare();
|
||||
backButton.setVisible(false);
|
||||
|
||||
if(importKeystore(wallet.getScriptType().getDefaultDerivation(), true)) {
|
||||
setExpanded(true);
|
||||
enterMnemonicButton.setVisible(false);
|
||||
importButton.setVisible(true);
|
||||
importButton.setDisable(false);
|
||||
setDescription("Ready to import");
|
||||
showHideLink.setText("Show Derivation...");
|
||||
showHideLink.setVisible(false);
|
||||
setContent(getDerivationEntry(getDefaultDerivation()));
|
||||
}
|
||||
}
|
||||
|
||||
private boolean importKeystore(List<ChildNumber> derivation, boolean dryrun) {
|
||||
importButton.setDisable(true);
|
||||
try {
|
||||
Keystore keystore = importer.getKeystore(derivation, mnemonicShares, passphraseProperty.get());
|
||||
if(!dryrun) {
|
||||
if(passphraseProperty.get() != null && !passphraseProperty.get().isEmpty()) {
|
||||
KeystorePassphraseDialog keystorePassphraseDialog = new KeystorePassphraseDialog(null, keystore, true);
|
||||
keystorePassphraseDialog.initOwner(this.getScene().getWindow());
|
||||
Optional<String> optPassphrase = keystorePassphraseDialog.showAndWait();
|
||||
if(optPassphrase.isEmpty() || !optPassphrase.get().equals(passphraseProperty.get())) {
|
||||
throw new ImportException("Re-entered passphrase did not match");
|
||||
}
|
||||
}
|
||||
|
||||
EventManager.get().post(new KeystoreImportEvent(keystore));
|
||||
}
|
||||
return true;
|
||||
} catch (ImportException e) {
|
||||
String errorMessage = e.getMessage();
|
||||
if(e.getCause() instanceof MnemonicException.MnemonicChecksumException) {
|
||||
errorMessage = "Invalid word list - checksum incorrect";
|
||||
} else if(e.getCause() != null && e.getCause().getMessage() != null && !e.getCause().getMessage().isEmpty()) {
|
||||
errorMessage = e.getCause().getMessage();
|
||||
}
|
||||
setError("Import Error", errorMessage + ".");
|
||||
importButton.setDisable(false);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Node getDerivationEntry(List<ChildNumber> derivation) {
|
||||
TextField derivationField = new TextField();
|
||||
derivationField.setPromptText("Derivation path");
|
||||
derivationField.setText(KeyDerivation.writePath(derivation));
|
||||
HBox.setHgrow(derivationField, Priority.ALWAYS);
|
||||
|
||||
ValidationSupport validationSupport = new ValidationSupport();
|
||||
validationSupport.setValidationDecorator(new StyleClassValidationDecoration());
|
||||
validationSupport.registerValidator(derivationField, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Invalid derivation", !KeyDerivation.isValid(newValue))
|
||||
));
|
||||
|
||||
Button importDerivationButton = new Button("Import Custom Derivation Keystore");
|
||||
importDerivationButton.setDisable(true);
|
||||
importDerivationButton.setOnAction(event -> {
|
||||
showHideLink.setVisible(true);
|
||||
setExpanded(false);
|
||||
List<ChildNumber> importDerivation = KeyDerivation.parsePath(derivationField.getText());
|
||||
importKeystore(importDerivation, false);
|
||||
});
|
||||
|
||||
derivationField.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
importButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || !KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
importDerivationButton.setDisable(newValue.isEmpty() || !KeyDerivation.isValid(newValue) || KeyDerivation.parsePath(newValue).equals(derivation));
|
||||
});
|
||||
|
||||
HBox contentBox = new HBox();
|
||||
contentBox.setAlignment(Pos.TOP_RIGHT);
|
||||
contentBox.setSpacing(20);
|
||||
contentBox.getChildren().add(derivationField);
|
||||
contentBox.getChildren().add(importDerivationButton);
|
||||
contentBox.setPadding(new Insets(10, 30, 10, 30));
|
||||
contentBox.setPrefHeight(60);
|
||||
|
||||
return contentBox;
|
||||
}
|
||||
|
||||
public static Glyph getIncompleteGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.PLUS_CIRCLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
warningGlyph.setFontSize(12);
|
||||
return warningGlyph;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected WordlistProvider getWordlistProvider() {
|
||||
return getWordListProvider(DeterministicSeed.Type.SLIP39);
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.URI;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -79,10 +79,6 @@ public class PayNymAvatar extends StackPane {
|
||||
this.paymentCodeProperty.set(paymentCode);
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
setPaymentCode(PaymentCode.fromString(paymentCode.toString()));
|
||||
}
|
||||
|
||||
public void clearPaymentCode() {
|
||||
this.paymentCodeProperty.set(null);
|
||||
}
|
||||
@@ -128,7 +124,7 @@ public class PayNymAvatar extends StackPane {
|
||||
log.debug("Requesting PayNym avatar from " + url);
|
||||
}
|
||||
|
||||
try(InputStream is = (proxy == null ? new URL(url).openStream() : new URL(url).openConnection(proxy).getInputStream())) {
|
||||
try(InputStream is = (proxy == null ? new URI(url).toURL().openStream() : new URI(url).toURL().openConnection(proxy).getInputStream())) {
|
||||
Image image = new Image(is, 150, 150, true, false);
|
||||
paymentCodeCache.put(cacheId, image);
|
||||
Platform.runLater(() -> EventManager.get().post(new PayNymImageLoadedEvent(paymentCode, image)));
|
||||
|
||||
@@ -81,10 +81,7 @@ public class PayNymCell extends ListCell<PayNym> {
|
||||
linkButton.setDisable(true);
|
||||
payNymController.linkPayNym(payNym);
|
||||
});
|
||||
|
||||
if(payNymController.isSelectLinkedOnly()) {
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
getStyleClass().add("unlinked");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,6 @@ public class PaymentCodeTextField extends CopyableTextField {
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
public void setPaymentCode(com.samourai.wallet.bip47.rpc.PaymentCode paymentCode) {
|
||||
this.paymentCodeStr = paymentCode.toString();
|
||||
setPaymentCodeString();
|
||||
}
|
||||
|
||||
private void setPaymentCodeString() {
|
||||
String abbrevPcode = paymentCodeStr.substring(0, 12) + "..." + paymentCodeStr.substring(paymentCodeStr.length() - 5);
|
||||
setText(abbrevPcode);
|
||||
|
||||
@@ -407,6 +407,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
if(total - fee <= dustThreshold) {
|
||||
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
|
||||
return;
|
||||
} else {
|
||||
Optional<ButtonType> optType = AppServices.showWarningDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds (" + total + " sats) for a transaction at this fee rate." +
|
||||
"\n\nContinue with a minimum fee rate transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(optType.isPresent() && optType.get() == ButtonType.NO) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.sparrowwallet.hummingbird.LegacyUREncoder;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.ImportException;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
@@ -169,6 +170,11 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
dialogPane.setContent(Borders.wrap(stackPane).lineBorder().buildAll());
|
||||
qrImageView.setImage(getQrCode(data));
|
||||
|
||||
if(qrImageView.getImage() == null) {
|
||||
Label warning = new Label("Message is too long for display as a QR code");
|
||||
stackPane.getChildren().add(warning);
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Close", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(cancelButtonType);
|
||||
|
||||
@@ -259,6 +265,10 @@ public class QRDisplayDialog extends Dialog<ButtonType> {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isUseBbqrEncoding() {
|
||||
return useBbqrEncoding;
|
||||
}
|
||||
|
||||
private void setUseBbqrEncoding(boolean useBbqrEncoding) {
|
||||
if(useBbqrEncoding) {
|
||||
this.useBbqrEncoding = true;
|
||||
|
||||
@@ -89,7 +89,7 @@ public class SearchWalletDialog extends Dialog<Entry> {
|
||||
results.setShowRoot(false);
|
||||
results.setPrefWidth(showWallet || showAccount ? 950 : 850);
|
||||
results.setUnitFormat(walletForms.iterator().next().getWallet());
|
||||
results.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
results.setEqualPreferredColumnWidths();
|
||||
results.setPlaceholder(new Label("No results"));
|
||||
results.setEditable(true);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.DeterministicSeed;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.SeedQR;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@@ -17,7 +18,7 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
|
||||
int lines = decryptedKeystore.getSeed().getMnemonicCode().size() / 3;
|
||||
int lines = Math.ceilDiv(decryptedKeystore.getSeed().getMnemonicCode().size(), 3);
|
||||
int height = lines * 40;
|
||||
|
||||
StackPane stackPane = new StackPane();
|
||||
@@ -43,15 +44,19 @@ public class SeedDisplayDialog extends Dialog<Void> {
|
||||
|
||||
stackPane.getChildren().addAll(anchorPane);
|
||||
|
||||
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().addAll(seedQRButtonType, cancelButtonType);
|
||||
if(decryptedKeystore.getSeed().getType() == DeterministicSeed.Type.BIP39) {
|
||||
final ButtonType seedQRButtonType = new javafx.scene.control.ButtonType("Show SeedQR", ButtonBar.ButtonData.LEFT);
|
||||
dialogPane.getButtonTypes().add(seedQRButtonType);
|
||||
|
||||
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
|
||||
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
showSeedQR(decryptedKeystore);
|
||||
});
|
||||
Button seedQRButton = (Button)dialogPane.lookupButton(seedQRButtonType);
|
||||
seedQRButton.addEventFilter(ActionEvent.ACTION, event -> {
|
||||
event.consume();
|
||||
showSeedQR(decryptedKeystore);
|
||||
});
|
||||
}
|
||||
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
dialogPane.getButtonTypes().add(cancelButtonType);
|
||||
|
||||
dialogPane.setPrefWidth(500);
|
||||
dialogPane.setPrefHeight(150 + height);
|
||||
|
||||
@@ -12,11 +12,9 @@ import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Theme;
|
||||
import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
|
||||
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
|
||||
import com.sparrowwallet.sparrow.event.SorobanInitiatedEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
@@ -246,19 +244,9 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
|
||||
private List<Map<BlockTransactionHashIndex, WalletNode>> getDisplayedUtxoSets() {
|
||||
boolean addUserSet = getOptimizationStrategy() == OptimizationStrategy.PRIVACY && SorobanServices.canWalletMix(walletTx.getWallet())
|
||||
&& walletTx.getPayments().size() == 1
|
||||
&& (walletTx.getPayments().get(0).getAddress().getScriptType() == walletTx.getWallet().getFreshNode(KeyPurpose.RECEIVE).getAddress().getScriptType());
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> displayedUtxoSets = new ArrayList<>();
|
||||
for(Map<BlockTransactionHashIndex, WalletNode> selectedUtxoSet : walletTx.getSelectedUtxoSets()) {
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, addUserSet ? 2 : walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
if(addUserSet && displayedUtxoSets.size() == 1) {
|
||||
Map<BlockTransactionHashIndex, WalletNode> addUserUtxoSet = new HashMap<>();
|
||||
addUserUtxoSet.put(new AddUserBlockTransactionHashIndex(!walletTx.isTwoPersonCoinjoin()), null);
|
||||
displayedUtxoSets.add(addUserUtxoSet);
|
||||
displayedUtxoSets.add(getDisplayedUtxos(selectedUtxoSet, walletTx.getSelectedUtxoSets().size()));
|
||||
}
|
||||
|
||||
List<Map<BlockTransactionHashIndex, WalletNode>> paddedUtxoSets = new ArrayList<>();
|
||||
@@ -339,11 +327,9 @@ public class TransactionDiagram extends GridPane {
|
||||
double setHeight = (height / numSets) - 5;
|
||||
for(int set = 0; set < numSets; set++) {
|
||||
boolean externalUserSet = displayedUtxoSets.get(set).values().stream().anyMatch(Objects::nonNull);
|
||||
boolean addUserSet = displayedUtxoSets.get(set).keySet().stream().anyMatch(ref -> ref instanceof AddUserBlockTransactionHashIndex);
|
||||
if(externalUserSet || addUserSet) {
|
||||
boolean replace = !isFinal() && set > 0 && SorobanServices.canWalletMix(walletTx.getWallet());
|
||||
Glyph bracketGlyph = !replace && walletTx.isCoinControlUsed() ? getLockGlyph() : (addUserSet ? getUserAddGlyph() : getCoinsGlyph(replace));
|
||||
String tooltipText = addUserSet ? "Click to add a mix partner" : (walletTx.getWallet().getFullDisplayName() + (replace ? "\nClick to replace with a mix partner" : ""));
|
||||
if(externalUserSet) {
|
||||
Glyph bracketGlyph = walletTx.isCoinControlUsed() ? getLockGlyph() : getCoinsGlyph();
|
||||
String tooltipText = walletTx.getWallet().getFullDisplayName();
|
||||
StackPane stackPane = getBracket(width, setHeight, bracketGlyph, tooltipText);
|
||||
allBrackets.getChildren().add(stackPane);
|
||||
} else {
|
||||
@@ -474,14 +460,6 @@ public class TransactionDiagram extends GridPane {
|
||||
tooltip.setText(joiner.toString());
|
||||
} else if(input instanceof InvisibleBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
} else if(input instanceof AddUserBlockTransactionHashIndex) {
|
||||
tooltip.setText("");
|
||||
label.setGraphic(walletTx.isTwoPersonCoinjoin() ? getQuestionGlyph() : getFeeWarningGlyph());
|
||||
label.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
if(walletTx.getInputTransactions() != null && walletTx.getInputTransactions().get(input.getHash()) != null) {
|
||||
BlockTransaction blockTransaction = walletTx.getInputTransactions().get(input.getHash());
|
||||
@@ -570,7 +548,7 @@ public class TransactionDiagram extends GridPane {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
curve.getStyleClass().add("input-line");
|
||||
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex || inputs.get(numUtxos-i) instanceof AddUserBlockTransactionHashIndex) {
|
||||
if(inputs.get(numUtxos-i) instanceof PayjoinBlockTransactionHashIndex) {
|
||||
curve.getStyleClass().add("input-dashed-line");
|
||||
} else if(inputs.get(numUtxos-i) instanceof InvisibleBlockTransactionHashIndex) {
|
||||
continue;
|
||||
@@ -952,46 +930,10 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private Glyph getUserAddGlyph() {
|
||||
Glyph userAddGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.USER_PLUS);
|
||||
userAddGlyph.getStyleClass().add("useradd-icon");
|
||||
userAddGlyph.setFontSize(12);
|
||||
userAddGlyph.setOnMouseEntered(event -> {
|
||||
userAddGlyph.setFontSize(18);
|
||||
});
|
||||
userAddGlyph.setOnMouseExited(event -> {
|
||||
userAddGlyph.setFontSize(12);
|
||||
});
|
||||
userAddGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
return userAddGlyph;
|
||||
}
|
||||
|
||||
private Glyph getCoinsGlyph(boolean allowReplacement) {
|
||||
private Glyph getCoinsGlyph() {
|
||||
Glyph coinsGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
if(allowReplacement) {
|
||||
coinsGlyph.getStyleClass().add("coins-replace-icon");
|
||||
coinsGlyph.setOnMouseEntered(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.USER_PLUS);
|
||||
coinsGlyph.setFontSize(18);
|
||||
});
|
||||
coinsGlyph.setOnMouseExited(event -> {
|
||||
coinsGlyph.setIcon(FontAwesome5.Glyph.COINS);
|
||||
coinsGlyph.setFontSize(12);
|
||||
});
|
||||
coinsGlyph.setOnMouseClicked(event -> {
|
||||
EventManager.get().post(new SorobanInitiatedEvent(walletTx.getWallet()));
|
||||
closeExpanded();
|
||||
event.consume();
|
||||
});
|
||||
} else {
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
}
|
||||
|
||||
coinsGlyph.getStyleClass().add("coins-icon");
|
||||
return coinsGlyph;
|
||||
}
|
||||
|
||||
@@ -1094,20 +1036,6 @@ public class TransactionDiagram extends GridPane {
|
||||
}
|
||||
}
|
||||
|
||||
private static class AddUserBlockTransactionHashIndex extends BlockTransactionHashIndex {
|
||||
private final boolean required;
|
||||
|
||||
public AddUserBlockTransactionHashIndex(boolean required) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
this.required = required;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getLabel() {
|
||||
return "Add Mix Partner" + (required ? "" : "?");
|
||||
}
|
||||
}
|
||||
|
||||
public static class AdditionalPayment extends Payment {
|
||||
private final List<Payment> additionalPayments;
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setEqualPreferredColumnWidths();
|
||||
setSortColumn(0, TreeTableColumn.SortType.DESCENDING);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,8 +27,8 @@ public class UsbStatusButton extends MenuButton {
|
||||
public void setDevices(List<Device> devices) {
|
||||
for(Device device : devices) {
|
||||
MenuItem deviceItem = new MenuItem(device.getModel().toDisplayString());
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_3 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
if(!device.isNeedsPinSent() && (device.getModel() == WalletModel.TREZOR_1 || device.getModel() == WalletModel.TREZOR_T || device.getModel() == WalletModel.TREZOR_SAFE_3 ||
|
||||
device.getModel() == WalletModel.TREZOR_SAFE_5 || device.getModel() == WalletModel.KEEPKEY || device.getModel() == WalletModel.BITBOX_02)) {
|
||||
deviceItem = new Menu(device.getModel().toDisplayString());
|
||||
MenuItem toggleItem = new MenuItem("Toggle Passphrase" + (!device.getModel().externalPassphraseEntry() ? "" : (device.isNeedsPassphraseSent() ? " Off" : " On")));
|
||||
toggleItem.setOnAction(event -> {
|
||||
|
||||
@@ -82,7 +82,7 @@ public class UtxosTreeTable extends CoinTreeTable {
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
setEqualPreferredColumnWidths();
|
||||
setSortColumn(getColumns().size() - 1, TreeTableColumn.SortType.DESCENDING);
|
||||
|
||||
getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
|
||||
|
||||
@@ -61,7 +61,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
}
|
||||
|
||||
List<WalletImport> walletImporters = new ArrayList<>(List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(),
|
||||
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig()));
|
||||
new KeystoneMultisig(), new Descriptor(), new SpecterDesktop(), new BlueWalletMultisig(), new Sparrow(), new JadeMultisig(), new PassportMultisig()));
|
||||
if(!selectedWalletForms.isEmpty()) {
|
||||
walletImporters.add(new WalletLabels(selectedWalletForms));
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ public class WalletSummaryDialog extends Dialog<Void> {
|
||||
table.setRoot(rootItem);
|
||||
rootItem.setExpanded(true);
|
||||
|
||||
table.setColumnResizePolicy(TreeTableView.CONSTRAINED_RESIZE_POLICY);
|
||||
table.setEqualPreferredColumnWidths();
|
||||
table.setPrefWidth(450);
|
||||
|
||||
VBox vBox = new VBox();
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class SorobanInitiatedEvent {
|
||||
private Wallet wallet;
|
||||
|
||||
public SorobanInitiatedEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.whirlpool.beans.Pool;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransaction;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
@@ -17,7 +16,6 @@ public class SpendUtxoEvent {
|
||||
private final Long fee;
|
||||
private final boolean requireAllUtxos;
|
||||
private final BlockTransaction replacedTransaction;
|
||||
private final Pool pool;
|
||||
private final PaymentCode paymentCode;
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos) {
|
||||
@@ -32,19 +30,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = requireAllUtxos;
|
||||
this.replacedTransaction = replacedTransaction;
|
||||
this.pool = null;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
public SpendUtxoEvent(Wallet wallet, List<BlockTransactionHashIndex> utxos, List<Payment> payments, List<byte[]> opReturns, Long fee, Pool pool) {
|
||||
this.wallet = wallet;
|
||||
this.utxos = utxos;
|
||||
this.payments = payments;
|
||||
this.opReturns = opReturns;
|
||||
this.fee = fee;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = pool;
|
||||
this.paymentCode = null;
|
||||
}
|
||||
|
||||
@@ -56,7 +41,6 @@ public class SpendUtxoEvent {
|
||||
this.fee = null;
|
||||
this.requireAllUtxos = false;
|
||||
this.replacedTransaction = null;
|
||||
this.pool = null;
|
||||
this.paymentCode = paymentCode;
|
||||
}
|
||||
|
||||
@@ -88,10 +72,6 @@ public class SpendUtxoEvent {
|
||||
return replacedTransaction;
|
||||
}
|
||||
|
||||
public Pool getPool() {
|
||||
return pool;
|
||||
}
|
||||
|
||||
public PaymentCode getPaymentCode() {
|
||||
return paymentCode;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolIndexHighFrequencyEvent {
|
||||
private final Wallet wallet;
|
||||
|
||||
public WhirlpoolIndexHighFrequencyEvent(Wallet wallet) {
|
||||
this.wallet = wallet;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.client.mix.listener.MixFailReason;
|
||||
import com.samourai.whirlpool.client.wallet.beans.MixProgress;
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WhirlpoolMixEvent {
|
||||
private final Wallet wallet;
|
||||
private final BlockTransactionHashIndex utxo;
|
||||
private final MixProgress mixProgress;
|
||||
private final Utxo nextUtxo;
|
||||
private final MixFailReason mixFailReason;
|
||||
private final String mixError;
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixProgress mixProgress) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = mixProgress;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = nextUtxo;
|
||||
this.mixFailReason = null;
|
||||
this.mixError = null;
|
||||
}
|
||||
|
||||
public WhirlpoolMixEvent(Wallet wallet, BlockTransactionHashIndex utxo, MixFailReason mixFailReason, String mixError) {
|
||||
this.wallet = wallet;
|
||||
this.utxo = utxo;
|
||||
this.mixProgress = null;
|
||||
this.nextUtxo = null;
|
||||
this.mixFailReason = mixFailReason;
|
||||
this.mixError = mixError;
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
return wallet;
|
||||
}
|
||||
|
||||
public BlockTransactionHashIndex getUtxo() {
|
||||
return utxo;
|
||||
}
|
||||
|
||||
public MixProgress getMixProgress() {
|
||||
return mixProgress;
|
||||
}
|
||||
|
||||
public Utxo getNextUtxo() {
|
||||
return nextUtxo;
|
||||
}
|
||||
|
||||
public MixFailReason getMixFailReason() {
|
||||
return mixFailReason;
|
||||
}
|
||||
|
||||
public String getMixError() {
|
||||
return mixError;
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.samourai.whirlpool.protocol.beans.Utxo;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
|
||||
public class WhirlpoolMixSuccessEvent extends WhirlpoolMixEvent {
|
||||
private final WalletNode walletNode;
|
||||
|
||||
public WhirlpoolMixSuccessEvent(Wallet wallet, BlockTransactionHashIndex utxo, Utxo nextUtxo, WalletNode walletNode) {
|
||||
super(wallet, utxo, nextUtxo);
|
||||
this.walletNode = walletNode;
|
||||
}
|
||||
|
||||
public WalletNode getWalletNode() {
|
||||
return walletNode;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,7 @@ public class FontAwesome5 extends GlyphFont {
|
||||
FEATHER_ALT('\uf56b'),
|
||||
FILE_CSV('\uf6dd'),
|
||||
FILE_IMPORT('\uf56f'),
|
||||
FILE_EXPORT('\uf56e'),
|
||||
FILE_PDF('\uf1c1'),
|
||||
HAND_HOLDING('\uf4bd'),
|
||||
HAND_HOLDING_MEDICAL('\ue05c'),
|
||||
|
||||
@@ -183,6 +183,13 @@ public class GlyphUtils {
|
||||
return successGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getInvalidGlyph() {
|
||||
Glyph invalidGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
|
||||
invalidGlyph.getStyleClass().add("failure");
|
||||
invalidGlyph.setFontSize(12);
|
||||
return invalidGlyph;
|
||||
}
|
||||
|
||||
public static Glyph getWarningGlyph() {
|
||||
Glyph warningGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.EXCLAMATION_TRIANGLE);
|
||||
warningGlyph.getStyleClass().add("warn-icon");
|
||||
|
||||
@@ -28,7 +28,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Unchained or Caravan Multisig";
|
||||
return "Unchained Caravan Multisig";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -56,11 +56,11 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
Keystore keystore = new Keystore(extKey.name.length() > Keystore.MAX_LABEL_LENGTH ? extKey.name.substring(0, Keystore.MAX_LABEL_LENGTH) : extKey.name);
|
||||
|
||||
if("Unknown".equals(extKey.bip32Path)) {
|
||||
extKey.bip32Path = "m/45'/0/0/0";
|
||||
extKey.bip32Path = "m/0/0/0/0";
|
||||
}
|
||||
|
||||
try {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path));
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, extKey.bip32Path, true));
|
||||
} catch(NumberFormatException e) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(extKey.xfp, scriptType.getDefaultDerivationPath()));
|
||||
}
|
||||
@@ -107,7 +107,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
CaravanFile cf = new CaravanFile();
|
||||
cf.name = wallet.getFullName();
|
||||
cf.addressType = wallet.getScriptType().toString().replace('-', '_');
|
||||
cf.network = Network.get().getName();
|
||||
cf.network = Network.getCanonical().getName();
|
||||
cf.client = new Client();
|
||||
|
||||
Quorum quorum = new Quorum();
|
||||
|
||||
@@ -48,7 +48,7 @@ public class CoboVaultSinglesig implements KeystoreFileImport, WalletImport {
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COBO_VAULT);
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(coboKeystore.MasterFingerprint.toLowerCase(Locale.ROOT), "m/" + coboKeystore.AccountKeyPath, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(coboKeystore.ExtPubKey));
|
||||
|
||||
ExtendedKey.Header header = ExtendedKey.Header.fromExtendedKey(coboKeystore.ExtPubKey);
|
||||
|
||||
@@ -7,10 +7,7 @@ import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -74,16 +71,16 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
if(header.getDefaultScriptType() != scriptType) {
|
||||
throw new ImportException("This wallet's script type (" + scriptType + ") does not match the " + getName() + " script type (" + header.getDefaultScriptType() + ")");
|
||||
}
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.path, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.xpub));
|
||||
} else if(scriptType.equals(ScriptType.P2SH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2sh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2sh));
|
||||
} else if(scriptType.equals(ScriptType.P2SH_P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_p2sh_deriv != null ? cck.p2wsh_p2sh_deriv : cck.p2sh_p2wsh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh_p2sh != null ? cck.p2wsh_p2sh : cck.p2sh_p2wsh));
|
||||
} else if(scriptType.equals(ScriptType.P2WSH)) {
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(cck.xfp, cck.p2wsh_deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(cck.p2wsh));
|
||||
} else {
|
||||
throw new ImportException("Correct derivation not found for script type: " + scriptType);
|
||||
@@ -111,7 +108,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Settings > Multisig Wallets > Export XPUB > " + account + " feature on your Coldcard.";
|
||||
return "Import file or QR created by using Advanced/Tools > Export Wallet > Sparrow Wallet" + (account > 0 ? " > 1 > " + account : "") + " on your Coldcard. For older firmware use Settings > Multisig Wallets > Export XPUB > " + account + ".";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,7 +157,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
Keystore keystore = new Keystore("Coldcard");
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(key, derivation));
|
||||
keystore.setKeyDerivation(new KeyDerivation(key, derivation, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(value));
|
||||
wallet.makeLabelsUnique(keystore);
|
||||
wallet.getKeystores().add(keystore);
|
||||
@@ -174,8 +171,10 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
wallet.setDefaultPolicy(policy);
|
||||
wallet.setScriptType(scriptType);
|
||||
|
||||
if(!wallet.isValid()) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet. " + getKeystoreImportDescription());
|
||||
try {
|
||||
wallet.checkWallet();
|
||||
} catch(InvalidWalletException e) {
|
||||
throw new IllegalStateException("This file does not describe a valid wallet: " + e.getMessage());
|
||||
}
|
||||
|
||||
return wallet;
|
||||
@@ -186,7 +185,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file created by using the Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export feature on your Coldcard.";
|
||||
return "Import file or QR created by using Settings > Multisig Wallets > [Wallet Detail] > Coldcard Export on your Coldcard.";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -239,7 +238,7 @@ public class ColdcardMultisig implements WalletImport, KeystoreFileImport, Walle
|
||||
|
||||
@Override
|
||||
public String getWalletExportDescription() {
|
||||
return "Export file that can be read by your Coldcard using the Settings > Multisig Wallets > Import from File feature.";
|
||||
return "Export file or QR that can be read by your Coldcard using Settings > Multisig Wallets > Import from File or QR.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -30,7 +30,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Import file created by using the Advanced > MicroSD > Export Wallet > Generic JSON > " + account + " feature on your Coldcard. Note this requires firmware version 3.1.3 or later.";
|
||||
return "Import file or QR created by using Advanced/Tools > Export Wallet > Sparrow Wallet" + (account > 0 ? " > 1 > " + account : "") + " on your Coldcard. For older firmware use Advanced > MicroSD > Export Wallet > Generic JSON > " + account + ".";
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -78,7 +78,7 @@ public class ColdcardSinglesig implements KeystoreFileImport, WalletImport {
|
||||
keystore.setLabel(getName());
|
||||
keystore.setSource(KeystoreSource.HW_AIRGAPPED);
|
||||
keystore.setWalletModel(WalletModel.COLDCARD);
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, ck.deriv, true));
|
||||
keystore.setExtendedPublicKey(ExtendedKey.fromDescriptor(ck.xpub));
|
||||
|
||||
return keystore;
|
||||
|
||||
@@ -77,7 +77,6 @@ public class Config {
|
||||
private int maxServerTimeout = DEFAULT_MAX_TIMEOUT;
|
||||
private int maxPageSize = DEFAULT_PAGE_SIZE;
|
||||
private boolean usePayNym;
|
||||
private boolean sameAppMixing;
|
||||
private boolean mempoolFullRbf;
|
||||
private Double appWidth;
|
||||
private Double appHeight;
|
||||
@@ -662,15 +661,6 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isSameAppMixing() {
|
||||
return sameAppMixing;
|
||||
}
|
||||
|
||||
public void setSameAppMixing(boolean sameAppMixing) {
|
||||
this.sameAppMixing = sameAppMixing;
|
||||
flush();
|
||||
}
|
||||
|
||||
public boolean isMempoolFullRbf() {
|
||||
return mempoolFullRbf;
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
keystore.setWalletModel(WalletModel.ELECTRUM);
|
||||
}
|
||||
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath));
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, derivationPath, true));
|
||||
keystore.setExtendedPublicKey(xPub);
|
||||
keystore.setLabel(ek.label != null ? ek.label : "Electrum");
|
||||
if(keystore.getLabel().length() > Keystore.MAX_LABEL_LENGTH) {
|
||||
@@ -328,8 +328,12 @@ public class Electrum implements KeystoreFileImport, WalletImport, WalletExport
|
||||
ek.derivation = keystore.getKeyDerivation().getDerivationPath();
|
||||
ek.root_fingerprint = keystore.getKeyDerivation().getMasterFingerprint();
|
||||
ek.xpub = keystore.getExtendedPublicKey().toString(xpubHeader);
|
||||
ek.type = "hardware";
|
||||
ek.hw_type = keystore.getWalletModel().getType();
|
||||
if(keystore.getWalletModel().hasUsb()) {
|
||||
ek.type = "hardware";
|
||||
ek.hw_type = keystore.getWalletModel().getType();
|
||||
} else {
|
||||
ek.type = "bip32";
|
||||
}
|
||||
ew.use_encryption = false;
|
||||
} else if(keystore.getSource() == KeystoreSource.SW_SEED) {
|
||||
ek.type = "bip32";
|
||||
|
||||
@@ -36,7 +36,7 @@ public class GordianSeedTool implements KeystoreFileImport {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription(int account) {
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.get().toDisplayString() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > " + Network.getCanonical().getCapitalizedName() + " > Master Key > Account Descriptor. Click the share icon at the bottom.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -38,7 +38,7 @@ public class Hwi {
|
||||
private static final Logger log = LoggerFactory.getLogger(Hwi.class);
|
||||
private static final String HWI_HOME_DIR = "hwi";
|
||||
private static final String HWI_VERSION_PREFIX = "hwi-";
|
||||
private static final String HWI_VERSION = "3.0.0";
|
||||
private static final String HWI_VERSION = "3.1.0";
|
||||
private static final String HWI_VERSION_DIR = HWI_VERSION_PREFIX + HWI_VERSION;
|
||||
|
||||
private static boolean isPromptActive = false;
|
||||
@@ -601,7 +601,7 @@ public class Hwi {
|
||||
private void addChainType(List<String> elements, boolean commandPresent) {
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), "--chain");
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.get()));
|
||||
elements.add(elements.size() - (commandPresent ? 1 : 0), getChainName(Network.getCanonical()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface KeystoreMnemonicShareImport extends KeystoreImport {
|
||||
Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.InputStream;
|
||||
@@ -45,4 +46,25 @@ public class PassportMultisig extends ColdcardMultisig {
|
||||
public boolean isWalletExportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isWalletImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getWalletImportDescription() {
|
||||
return "Import file or QR created by using Settings > Bitcoin > Multisig > [Wallet Detail] > Export via QR/microSD on your Passport.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet importWallet(InputStream inputStream, String password) throws ImportException {
|
||||
Wallet wallet = super.importWallet(inputStream, password);
|
||||
for(Keystore keystore : wallet.getKeystores()) {
|
||||
keystore.setLabel(keystore.getLabel().replace("Coldcard", "Passport"));
|
||||
keystore.setWalletModel(WalletModel.PASSPORT);
|
||||
}
|
||||
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.UREncoder;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoder;
|
||||
import com.sparrowwallet.sparrow.io.bbqr.BBQREncoding;
|
||||
import javafx.embed.swing.SwingFXUtils;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.stage.Stage;
|
||||
@@ -39,7 +42,7 @@ public class PdfUtils {
|
||||
private static final int QR_WIDTH = 480;
|
||||
private static final int QR_HEIGHT = 480;
|
||||
|
||||
public static void saveOutputDescriptor(String walletName, String outputDescriptor, UR ur) {
|
||||
public static void saveOutputDescriptor(String walletName, String outputDescriptor, UR ur, BBQR bbqr) {
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
fileChooser.setTitle("Save PDF");
|
||||
@@ -56,9 +59,22 @@ public class PdfUtils {
|
||||
Chunk title = new Chunk("Output descriptor for " + walletName, titleFont);
|
||||
document.add(title);
|
||||
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
String fragment = urEncoder.nextPart();
|
||||
if(urEncoder.isSinglePart()) {
|
||||
String fragment = null;
|
||||
if(bbqr != null) {
|
||||
BBQREncoder bbqrEncoder = new BBQREncoder(bbqr.type(), BBQREncoding.ZLIB, bbqr.data(), 2000, 0);
|
||||
if(bbqrEncoder.isSinglePart()) {
|
||||
fragment = bbqrEncoder.nextPart();
|
||||
}
|
||||
}
|
||||
|
||||
if(fragment == null) {
|
||||
UREncoder urEncoder = new UREncoder(ur, 2000, 10, 0);
|
||||
if(urEncoder.isSinglePart()) {
|
||||
fragment = urEncoder.nextPart();
|
||||
}
|
||||
}
|
||||
|
||||
if(fragment != null) {
|
||||
Image image = Image.getInstance(SwingFXUtils.fromFXImage(getQrCode(fragment), null), Color.WHITE);
|
||||
document.add(image);
|
||||
}
|
||||
|
||||
@@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.io;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonParseException;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.samourai.wallet.crypto.AESUtil;
|
||||
import com.samourai.wallet.util.CharSequenceX;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.SamouraiUtil;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
|
||||
@@ -46,9 +46,9 @@ public class Samourai implements KeystoreFileImport {
|
||||
|
||||
String decrypted;
|
||||
if(version == 1) {
|
||||
decrypted = AESUtil.decrypt(payload, new CharSequenceX(password), AESUtil.DefaultPBKDF2Iterations);
|
||||
decrypted = SamouraiUtil.decrypt(payload, password, SamouraiUtil.DefaultPBKDF2Iterations);
|
||||
} else if(version == 2) {
|
||||
decrypted = AESUtil.decryptSHA256(payload, new CharSequenceX(password));
|
||||
decrypted = SamouraiUtil.decryptSHA256(payload, password);
|
||||
} else {
|
||||
throw new ImportException("Unsupported backup version: " + version);
|
||||
}
|
||||
@@ -58,6 +58,8 @@ public class Samourai implements KeystoreFileImport {
|
||||
Keystore keystore = Keystore.fromSeed(seed, scriptType.getDefaultDerivation());
|
||||
keystore.setLabel(getWalletModel().toDisplayString());
|
||||
return keystore;
|
||||
} catch(JsonParseException e) {
|
||||
throw new ImportException("Failed to decrypt the wallet backup file, check if the password is correct.");
|
||||
} catch(ImportException e) {
|
||||
throw e;
|
||||
} catch(Exception e) {
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.RecoveryState;
|
||||
import com.sparrowwallet.drongo.wallet.slip39.Share;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
public class Slip39 implements KeystoreMnemonicShareImport {
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Mnemonic Shares (SLIP39)";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SEED;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import your 20 or 33 word mnemonic shares and optional passphrase.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(List<ChildNumber> derivation, List<List<String>> mnemonicShares, String passphrase) throws ImportException {
|
||||
try {
|
||||
RecoveryState recoveryState = new RecoveryState();
|
||||
for(List<String> mnemonicWords : mnemonicShares) {
|
||||
Share share = Share.fromMnemonic(String.join(" ", mnemonicWords));
|
||||
recoveryState.addShare(share);
|
||||
}
|
||||
|
||||
if(recoveryState.isComplete()) {
|
||||
byte[] secret = recoveryState.recover(passphrase.getBytes(StandardCharsets.UTF_8));
|
||||
DeterministicSeed seed = new DeterministicSeed(secret, passphrase, System.currentTimeMillis(), DeterministicSeed.Type.SLIP39);
|
||||
return Keystore.fromSeed(seed, derivation);
|
||||
} else {
|
||||
throw new Slip39ProgressException(recoveryState.getShortStatus(), recoveryState.getStatus());
|
||||
}
|
||||
} catch(MnemonicException e) {
|
||||
throw new ImportException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Slip39ProgressException extends ImportException {
|
||||
private final String title;
|
||||
|
||||
public Slip39ProgressException(String title, String message) {
|
||||
super(message);
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -108,9 +108,7 @@ public class SpecterDesktop implements WalletImport, WalletExport {
|
||||
WalletModel walletModel = device.getWalletModel();
|
||||
if(walletModel != null) {
|
||||
keystore.setWalletModel(walletModel);
|
||||
if(walletModel == WalletModel.TREZOR_1 || walletModel == WalletModel.TREZOR_T || walletModel == WalletModel.TREZOR_SAFE_3 || walletModel == WalletModel.KEEPKEY ||
|
||||
walletModel == WalletModel.LEDGER_NANO_S || walletModel == WalletModel.LEDGER_NANO_X || walletModel == WalletModel.LEDGER_NANO_S_PLUS ||
|
||||
walletModel == WalletModel.BITBOX_02 || walletModel == WalletModel.COLDCARD) {
|
||||
if(walletModel.hasUsb()) {
|
||||
keystore.setSource(KeystoreSource.HW_USB);
|
||||
} else if(walletModel == WalletModel.BITCOIN_CORE) {
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
|
||||
@@ -8,8 +8,6 @@ import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.SparrowWallet;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.whirlpool.Whirlpool;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -21,6 +19,7 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.PosixFilePermission;
|
||||
import java.nio.file.attribute.PosixFilePermissions;
|
||||
import java.security.cert.Certificate;
|
||||
@@ -174,15 +173,6 @@ public class Storage {
|
||||
}
|
||||
}
|
||||
|
||||
if(wallet.isWhirlpoolMasterWallet()) {
|
||||
String walletId = getWalletId(wallet);
|
||||
Whirlpool whirlpool = AppServices.getWhirlpoolServices().getWhirlpool(walletId);
|
||||
whirlpool.setScode(wallet.getMasterMixConfig().getScode());
|
||||
whirlpool.setHDWallet(getWalletId(wallet), copy);
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(walletId);
|
||||
soroban.setHDWallet(copy);
|
||||
}
|
||||
|
||||
StandardAccount standardAccount = wallet.getStandardAccountType();
|
||||
if(standardAccount != null && standardAccount.getMinimumGapLimit() != null && wallet.gapLimit() == null) {
|
||||
wallet.setGapLimit(standardAccount.getMinimumGapLimit());
|
||||
@@ -227,8 +217,8 @@ public class Storage {
|
||||
}
|
||||
|
||||
private void checkWalletNetwork(Wallet wallet) {
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.get()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.get() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
if(wallet.getNetwork() != null && wallet.getNetwork() != Network.getCanonical()) {
|
||||
throw new IllegalStateException("Provided " + wallet.getNetwork() + " wallet is invalid on a " + Network.getCanonical() + " network. Use a " + wallet.getNetwork() + " configuration to load this wallet.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -537,8 +527,21 @@ public class Storage {
|
||||
|
||||
public static File getSparrowDir() {
|
||||
File sparrowDir;
|
||||
if(Network.get() != Network.MAINNET) {
|
||||
sparrowDir = new File(getSparrowHome(), Network.get().getName());
|
||||
Network network = Network.get();
|
||||
if(network != Network.MAINNET) {
|
||||
sparrowDir = new File(getSparrowHome(), network.getHome());
|
||||
if(!network.getName().equals(network.getHome()) && !sparrowDir.exists()) {
|
||||
File networkNameDir = new File(getSparrowHome(), network.getName());
|
||||
if(networkNameDir.exists() && networkNameDir.isDirectory() && !Files.isSymbolicLink(networkNameDir.toPath())) {
|
||||
try {
|
||||
if(networkNameDir.renameTo(sparrowDir) && !isWindows()) {
|
||||
Files.createSymbolicLink(networkNameDir.toPath(), Path.of(sparrowDir.getName()));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error creating symlink from " + networkNameDir.getAbsolutePath() + " to " + sparrowDir.getName(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
sparrowDir = getSparrowHome();
|
||||
}
|
||||
@@ -547,6 +550,23 @@ public class Storage {
|
||||
createOwnerOnlyDirectory(sparrowDir);
|
||||
}
|
||||
|
||||
if(!network.getName().equals(network.getHome()) && !isWindows()) {
|
||||
try {
|
||||
Path networkNamePath = getSparrowHome().toPath().resolve(network.getName());
|
||||
if(Files.isSymbolicLink(networkNamePath)) {
|
||||
Path symlinkTarget = getSparrowHome().toPath().resolve(Files.readSymbolicLink(networkNamePath));
|
||||
if(!Files.isSameFile(sparrowDir.toPath(), symlinkTarget)) {
|
||||
Files.delete(networkNamePath);
|
||||
Files.createSymbolicLink(networkNamePath, Path.of(sparrowDir.getName()));
|
||||
}
|
||||
} else if(!Files.exists(networkNamePath)) {
|
||||
Files.createSymbolicLink(networkNamePath, Path.of(sparrowDir.getName()));
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.debug("Error updating symlink from " + network.getName() + " to " + sparrowDir.getName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
return sparrowDir;
|
||||
}
|
||||
|
||||
@@ -559,7 +579,7 @@ public class Storage {
|
||||
return new File(System.getProperty(SparrowWallet.APP_HOME_PROPERTY));
|
||||
}
|
||||
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
return new File(getHomeDir(), WINDOWS_SPARROW_DIR);
|
||||
}
|
||||
|
||||
@@ -567,7 +587,7 @@ public class Storage {
|
||||
}
|
||||
|
||||
static File getHomeDir() {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
return new File(System.getenv("APPDATA"));
|
||||
}
|
||||
|
||||
@@ -576,7 +596,7 @@ public class Storage {
|
||||
|
||||
public static boolean createOwnerOnlyDirectory(File directory) {
|
||||
try {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
Files.createDirectories(directory.toPath());
|
||||
return true;
|
||||
}
|
||||
@@ -594,7 +614,7 @@ public class Storage {
|
||||
|
||||
public static boolean createOwnerOnlyFile(File file) {
|
||||
try {
|
||||
if(Platform.getCurrent() == Platform.WINDOWS) {
|
||||
if(isWindows()) {
|
||||
Files.createFile(file.toPath());
|
||||
return true;
|
||||
}
|
||||
@@ -625,6 +645,10 @@ public class Storage {
|
||||
return ownerOnly;
|
||||
}
|
||||
|
||||
private static boolean isWindows() {
|
||||
return Platform.getCurrent() == Platform.WINDOWS;
|
||||
}
|
||||
|
||||
public static class LoadWalletService extends Service<WalletAndKey> {
|
||||
private final Storage storage;
|
||||
private final SecureString password;
|
||||
|
||||
@@ -37,6 +37,7 @@ public class BBQRDecoder {
|
||||
|
||||
if(receivedParts.size() == totalParts) {
|
||||
byte[] data = concatParts();
|
||||
data = header.inflate(data);
|
||||
|
||||
if(type == BBQRType.PSBT) {
|
||||
result = new Result(new PSBT(data));
|
||||
|
||||
@@ -35,7 +35,7 @@ public class BBQREncoder {
|
||||
BBQREncoding encoding = desiredEncoding;
|
||||
|
||||
try {
|
||||
encoded = encoding.encode(data);
|
||||
encoded = encoding.encode(encoding.deflate(data));
|
||||
if(encoding == BBQREncoding.ZLIB) {
|
||||
String uncompressed = BBQREncoding.BASE32.encode(data);
|
||||
if(encoded.length() > uncompressed.length()) {
|
||||
|
||||
@@ -42,6 +42,11 @@ public enum BBQREncoding {
|
||||
}, ZLIB("Z") {
|
||||
@Override
|
||||
public String encode(byte[] data) throws BBQREncodingException {
|
||||
return BASE32.encode(data);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] deflate(byte[] data) throws BBQREncodingException {
|
||||
try {
|
||||
Deflater deflater = new Deflater(JZlib.Z_BEST_COMPRESSION, 10, true);
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
@@ -49,7 +54,7 @@ public enum BBQREncoding {
|
||||
zOut.write(data);
|
||||
zOut.close();
|
||||
|
||||
return BASE32.encode(out.toByteArray());
|
||||
return out.toByteArray();
|
||||
} catch(Exception e) {
|
||||
throw new BBQREncodingException("Error deflating with zlib", e);
|
||||
}
|
||||
@@ -57,9 +62,14 @@ public enum BBQREncoding {
|
||||
|
||||
@Override
|
||||
public byte[] decode(String part) throws BBQREncodingException {
|
||||
return BASE32.decode(part);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte[] inflate(byte[] data) throws BBQREncodingException {
|
||||
try {
|
||||
Inflater inflater = new Inflater(10, true);
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(BASE32.decode(part));
|
||||
ByteArrayInputStream in = new ByteArrayInputStream(data);
|
||||
InflaterInputStream zIn = new InflaterInputStream(in, inflater);
|
||||
byte[] decoded = zIn.readAllBytes();
|
||||
zIn.close();
|
||||
@@ -96,6 +106,14 @@ public enum BBQREncoding {
|
||||
return code;
|
||||
}
|
||||
|
||||
public byte[] deflate(byte[] data) throws BBQREncodingException {
|
||||
return data;
|
||||
}
|
||||
|
||||
public byte[] inflate(byte[] data) throws BBQREncodingException {
|
||||
return data;
|
||||
}
|
||||
|
||||
public abstract String encode(byte[] data) throws BBQREncodingException;
|
||||
|
||||
public abstract byte[] decode(String part) throws BBQREncodingException;
|
||||
|
||||
@@ -13,6 +13,10 @@ public record BBQRHeader(BBQREncoding encoding, BBQRType type, int seqTotal, int
|
||||
return encoding.decode(part.substring(8));
|
||||
}
|
||||
|
||||
public byte[] inflate(byte[] data) {
|
||||
return encoding.inflate(data);
|
||||
}
|
||||
|
||||
public static BBQRHeader fromString(String part) {
|
||||
if(part.length() < 8) {
|
||||
throw new IllegalArgumentException("Part too short");
|
||||
|
||||
@@ -63,7 +63,7 @@ public interface KeystoreDao {
|
||||
long id = insertSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
seed.setId(id);
|
||||
} else {
|
||||
long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
long id = insertSeed(seed.getType().ordinal(), seed.getMnemonicString(true).asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds());
|
||||
seed.setId(id);
|
||||
}
|
||||
}
|
||||
@@ -96,7 +96,7 @@ public interface KeystoreDao {
|
||||
EncryptedData data = seed.getEncryptedData();
|
||||
updateSeed(seed.getType().ordinal(), null, data.getInitialisationVector(), data.getEncryptedBytes(), data.getKeySalt(), data.getEncryptionType().getDeriver().ordinal(), data.getEncryptionType().getCrypter().ordinal(), seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId());
|
||||
} else {
|
||||
updateSeed(seed.getType().ordinal(), seed.getMnemonicString().asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId());
|
||||
updateSeed(seed.getType().ordinal(), seed.getMnemonicString(true).asString(), null, null, null, null, null, seed.needsPassphrase(), seed.getCreationTimeSeconds(), seed.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ public class SatoCardApi extends CardApi {
|
||||
String masterXpub = this.cardProtocol.cardBip32GetXpub("m", xtype);
|
||||
ExtendedKey masterExtendedKey = ExtendedKey.fromDescriptor(masterXpub);
|
||||
String masterFingerprint = Utils.bytesToHex(masterExtendedKey.getKey().getFingerprint());
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString);
|
||||
KeyDerivation keyDerivation = new KeyDerivation(masterFingerprint, keyDerivationString, true);
|
||||
|
||||
Keystore keystore = new Keystore();
|
||||
keystore.setLabel(WalletModel.SATOCHIP.toDisplayString());
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.keystoreimport;
|
||||
|
||||
import com.sparrowwallet.sparrow.control.FileKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.MnemonicKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.TitledDescriptionPane;
|
||||
import com.sparrowwallet.sparrow.control.XprvKeystoreImportPane;
|
||||
import com.sparrowwallet.sparrow.control.*;
|
||||
import com.sparrowwallet.sparrow.io.*;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.scene.control.Accordion;
|
||||
@@ -15,7 +12,7 @@ public class SwController extends KeystoreImportDetailController {
|
||||
private Accordion importAccordion;
|
||||
|
||||
public void initializeView() {
|
||||
List<KeystoreImport> importers = List.of(new Bip39(), new Electrum(), new Bip32());
|
||||
List<KeystoreImport> importers = List.of(new Bip39(), new Bip32(), new Slip39());
|
||||
|
||||
for(KeystoreImport importer : importers) {
|
||||
if(importer.isDeprecated() && !Config.get().isShowDeprecatedImportExport()) {
|
||||
@@ -30,6 +27,8 @@ public class SwController extends KeystoreImportDetailController {
|
||||
importPane = new MnemonicKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicImport)importer, getMasterController().getDefaultDerivation());
|
||||
} else if(importer instanceof KeystoreXprvImport) {
|
||||
importPane = new XprvKeystoreImportPane(getMasterController().getWallet(), (KeystoreXprvImport)importer, getMasterController().getDefaultDerivation());
|
||||
} else if(importer instanceof KeystoreMnemonicShareImport) {
|
||||
importPane = new MnemonicShareKeystoreImportPane(getMasterController().getWallet(), (KeystoreMnemonicShareImport)importer, getMasterController().getDefaultDerivation());
|
||||
} else {
|
||||
throw new IllegalArgumentException("Could not create ImportPane for importer of type " + importer.getClass());
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ public class Auth47 {
|
||||
private String srbnName;
|
||||
private String resource;
|
||||
|
||||
public Auth47(URI uri) throws MalformedURLException {
|
||||
public Auth47(URI uri) throws MalformedURLException, URISyntaxException {
|
||||
this.nonce = uri.getHost();
|
||||
|
||||
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||
@@ -64,12 +64,12 @@ public class Auth47 {
|
||||
}
|
||||
if(strCallback.startsWith(SRBN_PROTOCOL)) {
|
||||
String srbnCallback = HTTPS_PROTOCOL + strCallback.substring(SRBN_PROTOCOL.length());
|
||||
URL srbnUrl = new URL(srbnCallback);
|
||||
URL srbnUrl = new URI(srbnCallback).toURL();
|
||||
this.srbn = true;
|
||||
this.srbnName = srbnUrl.getUserInfo();
|
||||
this.callback = new URL(HTTPS_PROTOCOL + srbnUrl.getHost());
|
||||
this.callback = new URI(HTTPS_PROTOCOL + srbnUrl.getHost()).toURL();
|
||||
} else {
|
||||
this.callback = new URL(strCallback);
|
||||
this.callback = new URI(strCallback).toURL();
|
||||
}
|
||||
|
||||
this.expiry = parameterMap.get("e");
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.wallet.httpClient.HttpResponseException;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.protocol.Sha256Hash;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.List;
|
||||
|
||||
public enum BroadcastSource {
|
||||
@@ -28,11 +29,11 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET, Network.TESTNET);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -46,16 +47,18 @@ public enum BroadcastSource {
|
||||
|
||||
@Override
|
||||
public List<Network> getSupportedNetworks() {
|
||||
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
|
||||
return List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET, Network.TESTNET4);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else if(Network.get() == Network.SIGNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/signet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/signet/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET4) {
|
||||
return new URI(getBaseUrl(proxy) + "/testnet4/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -72,11 +75,13 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else if(Network.get() == Network.SIGNET) {
|
||||
return new URI(getBaseUrl(proxy) + "/signet/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -93,11 +98,11 @@ public enum BroadcastSource {
|
||||
return List.of(Network.MAINNET);
|
||||
}
|
||||
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException {
|
||||
protected URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException {
|
||||
if(Network.get() == Network.MAINNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/api/tx").toURL();
|
||||
} else if(Network.get() == Network.TESTNET) {
|
||||
return new URL(getBaseUrl(proxy) + "/testnet/api/tx");
|
||||
return new URI(getBaseUrl(proxy) + "/testnet/api/tx").toURL();
|
||||
} else {
|
||||
throw new IllegalStateException("Cannot broadcast transaction to " + getName() + " on network " + Network.get());
|
||||
}
|
||||
@@ -109,7 +114,6 @@ public enum BroadcastSource {
|
||||
private final String onionUrl;
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(BroadcastSource.class);
|
||||
private static final SecureRandom secureRandom = new SecureRandom();
|
||||
|
||||
BroadcastSource(String name, String tlsUrl, String onionUrl) {
|
||||
this.name = name;
|
||||
@@ -137,7 +141,7 @@ public enum BroadcastSource {
|
||||
|
||||
public abstract List<Network> getSupportedNetworks();
|
||||
|
||||
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException;
|
||||
protected abstract URL getURL(HostAndPort proxy) throws MalformedURLException, URISyntaxException;
|
||||
|
||||
public Sha256Hash postTransactionData(String data) throws BroadcastException {
|
||||
//If a Tor proxy is configured, ensure we use a new circuit by configuring a random proxy password
|
||||
|
||||
@@ -119,7 +119,7 @@ public class Bwt {
|
||||
*/
|
||||
private void start(Collection<String> outputDescriptors, Collection<String> addresses, Integer rescanSince, Boolean forceRescan, Integer gapLimit, CallbackNotifier callback) {
|
||||
BwtConfig bwtConfig = new BwtConfig();
|
||||
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.get().getName();
|
||||
bwtConfig.network = Network.get() == Network.MAINNET ? "bitcoin" : Network.getCanonical().getName();
|
||||
|
||||
if(!outputDescriptors.isEmpty()) {
|
||||
bwtConfig.descriptors = outputDescriptors;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.samourai.wallet.httpClient.HttpResponseException;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.event.ExchangeRatesUpdatedEvent;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import javafx.concurrent.ScheduledService;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.JettyHttpClientService;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.samourai.wallet.httpClient.IHttpClient;
|
||||
import com.samourai.wallet.util.AsyncUtil;
|
||||
import com.samourai.wallet.util.ThreadUtil;
|
||||
import com.samourai.whirlpool.client.utils.ClientUtils;
|
||||
import com.sparrowwallet.sparrow.net.http.client.AsyncUtil;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.IHttpClient;
|
||||
import com.sparrowwallet.sparrow.net.http.client.JettyHttpClientService;
|
||||
import io.reactivex.Observable;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
@@ -22,15 +20,15 @@ public class HttpClientService extends JettyHttpClientService {
|
||||
}
|
||||
|
||||
public <T> T requestJson(String url, Class<T> responseType, Map<String, String> headers) throws Exception {
|
||||
return getHttpClient(HttpUsage.BACKEND).getJson(url, responseType, headers);
|
||||
return getHttpClient(HttpUsage.DEFAULT).getJson(url, responseType, headers);
|
||||
}
|
||||
|
||||
public <T> Observable<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body) {
|
||||
return getHttpClient(HttpUsage.BACKEND).postJson(url, responseType, headers, body).toObservable();
|
||||
return getHttpClient(HttpUsage.DEFAULT).postJson(url, responseType, headers, body).toObservable();
|
||||
}
|
||||
|
||||
public String postString(String url, Map<String, String> headers, String contentType, String content) throws Exception {
|
||||
IHttpClient httpClient = getHttpClient(HttpUsage.BACKEND);
|
||||
IHttpClient httpClient = getHttpClient(HttpUsage.DEFAULT);
|
||||
return AsyncUtil.getInstance().blockingGet(httpClient.postString(url, headers, contentType, content)).get();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.samourai.http.client.IHttpProxySupplier;
|
||||
import com.samourai.wallet.httpClient.HttpProxy;
|
||||
import com.samourai.wallet.httpClient.HttpProxyProtocol;
|
||||
import com.samourai.wallet.httpClient.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpProxy;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpProxyProtocol;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpUsage;
|
||||
import com.sparrowwallet.sparrow.net.http.client.IHttpProxySupplier;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
|
||||
@@ -38,12 +38,12 @@ public class LnurlAuth {
|
||||
private final byte[] k1;
|
||||
private final String action;
|
||||
|
||||
public LnurlAuth(URI uri) throws MalformedURLException {
|
||||
public LnurlAuth(URI uri) throws MalformedURLException, URISyntaxException {
|
||||
String lnurl = uri.getSchemeSpecificPart();
|
||||
Bech32.Bech32Data bech32 = Bech32.decode(lnurl, 2000);
|
||||
byte[] urlBytes = Bech32.convertBits(bech32.data, 0, bech32.data.length, 5, 8, false);
|
||||
String strUrl = new String(urlBytes, StandardCharsets.UTF_8);
|
||||
this.url = new URL(strUrl);
|
||||
this.url = new URI(strUrl).toURL();
|
||||
|
||||
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||
String query = url.getQuery();
|
||||
@@ -144,8 +144,8 @@ public class LnurlAuth {
|
||||
try {
|
||||
ECKey linkingKey = deriveLinkingKey(wallet);
|
||||
byte[] signature = getSignature(linkingKey);
|
||||
return new URL(url.toString() + "&sig=" + Utils.bytesToHex(signature) + "&key=" + Utils.bytesToHex(linkingKey.getPubKey()));
|
||||
} catch(MalformedURLException e) {
|
||||
return new URI(url.toString() + "&sig=" + Utils.bytesToHex(signature) + "&key=" + Utils.bytesToHex(linkingKey.getPubKey())).toURL();
|
||||
} catch(MalformedURLException | URISyntaxException e) {
|
||||
throw new IllegalStateException("Malformed return URL", e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,15 @@ public enum PublicElectrumServer {
|
||||
SETHFORPRIVACY_COM("fulcrum.sethforprivacy.com", "ssl://fulcrum.sethforprivacy.com:50002", Network.MAINNET),
|
||||
TESTNET_ARANGUREN_ORG("testnet.aranguren.org", "ssl://testnet.aranguren.org:51002", Network.TESTNET),
|
||||
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET),
|
||||
SIGNET_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:60602", Network.SIGNET);
|
||||
SIGNET_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:60602", Network.SIGNET),
|
||||
TESTNET4_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:40002", Network.TESTNET4);
|
||||
|
||||
PublicElectrumServer(String name, String url, Network network) {
|
||||
this.server = new Server(url, name);
|
||||
this.network = network;
|
||||
}
|
||||
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET);
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET, Network.TESTNET4);
|
||||
|
||||
private final Server server;
|
||||
private final Network network;
|
||||
|
||||
+5
-6
@@ -10,10 +10,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.*;
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.Proxy;
|
||||
import java.net.URL;
|
||||
import java.net.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
@@ -48,8 +45,8 @@ public class BitcoindTransport implements Transport {
|
||||
if(!bitcoindServer.getHostAndPort().hasPort()) {
|
||||
serverUrl += ":" + Network.get().getDefaultPort();
|
||||
}
|
||||
this.bitcoindUrl = new URL(serverUrl + "/wallet/" + bitcoindWallet);
|
||||
} catch(MalformedURLException e) {
|
||||
this.bitcoindUrl = new URI(serverUrl + "/wallet/" + bitcoindWallet).toURL();
|
||||
} catch(MalformedURLException | URISyntaxException e) {
|
||||
log.error("Malformed Bitcoin Core RPC URL", e);
|
||||
}
|
||||
}
|
||||
@@ -130,6 +127,8 @@ public class BitcoindTransport implements Transport {
|
||||
private static File getCookieDir(File bitcoindDir) {
|
||||
if(Network.get() == Network.TESTNET && Files.exists(Path.of(bitcoindDir.getAbsolutePath(), "testnet3", COOKIE_FILENAME))) {
|
||||
return new File(bitcoindDir, "testnet3");
|
||||
} else if(Network.get() == Network.TESTNET4 && Files.exists(Path.of(bitcoindDir.getAbsolutePath(), "testnet4", COOKIE_FILENAME))) {
|
||||
return new File(bitcoindDir, "testnet4");
|
||||
} else if(Network.get() == Network.REGTEST && Files.exists(Path.of(bitcoindDir.getAbsolutePath(), "regtest", COOKIE_FILENAME))) {
|
||||
return new File(bitcoindDir, "regtest");
|
||||
} else if(Network.get() == Network.SIGNET && Files.exists(Path.of(bitcoindDir.getAbsolutePath(), "signet", COOKIE_FILENAME))) {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import io.reactivex.Completable;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.functions.Action;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.slf4j.MDC;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class AsyncUtil {
|
||||
private static final ThreadUtil threadUtil = ThreadUtil.getInstance();
|
||||
private static AsyncUtil instance;
|
||||
|
||||
public static AsyncUtil getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new AsyncUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public <T> T unwrapException(Callable<T> c) throws Exception {
|
||||
try {
|
||||
return c.call();
|
||||
} catch(RuntimeException e) {
|
||||
// blockingXXX wraps errors with RuntimeException, unwrap it
|
||||
throw unwrapException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Exception unwrapException(Exception e) throws Exception {
|
||||
if(e.getCause() != null && e.getCause() instanceof Exception) {
|
||||
throw (Exception) e.getCause();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
|
||||
public <T> T blockingGet(Single<T> o) throws Exception {
|
||||
try {
|
||||
return unwrapException(o::blockingGet);
|
||||
} catch(ExecutionException e) {
|
||||
// blockingGet(threadUtil.runWithTimeoutAndRetry()) wraps InterruptedException("exit (done)")
|
||||
// with ExecutionException, unwrap it
|
||||
throw unwrapException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public <T> T blockingGet(Single<T> o, long timeoutMs) throws Exception {
|
||||
Callable<T> callable = () -> blockingGet(o);
|
||||
return blockingGet(runAsync(callable, timeoutMs));
|
||||
}
|
||||
|
||||
public <T> T blockingGet(Future<T> o, long timeoutMs) throws Exception {
|
||||
return o.get(timeoutMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
public <T> T blockingLast(Observable<T> o) throws Exception {
|
||||
return unwrapException(o::blockingLast);
|
||||
}
|
||||
|
||||
public void blockingAwait(Completable o) throws Exception {
|
||||
Callable<Optional> callable = () -> {
|
||||
o.blockingAwait();
|
||||
return Optional.empty();
|
||||
};
|
||||
unwrapException(callable);
|
||||
}
|
||||
|
||||
public void blockingAwait(Completable o, long timeoutMs) throws Exception {
|
||||
Callable<Optional> callable = () -> {
|
||||
o.blockingAwait();
|
||||
return Optional.empty();
|
||||
};
|
||||
blockingGet(runAsync(callable, timeoutMs));
|
||||
}
|
||||
|
||||
public <T> Single<T> timeout(Single<T> o, long timeoutMs) {
|
||||
try {
|
||||
return Single.just(blockingGet(o, timeoutMs));
|
||||
} catch(Exception e) {
|
||||
return Single.error(e);
|
||||
}
|
||||
}/*
|
||||
|
||||
public Completable timeout(Completable o, long timeoutMs) {
|
||||
try {
|
||||
return Completable.fromCallable(() -> {
|
||||
blockingAwait(o, timeoutMs);
|
||||
return Optional.empty();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return Completable.error(e);
|
||||
}
|
||||
}*/
|
||||
|
||||
public <T> Single<T> runIOAsync(final Callable<T> callable) {
|
||||
return Single.fromCallable(callable).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public Completable runIOAsyncCompletable(final Action action) {
|
||||
return Completable.fromAction(action).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
public <T> T runIO(final Callable<T> callable) throws Exception {
|
||||
return blockingGet(runIOAsync(callable));
|
||||
}
|
||||
|
||||
public void runIO(final Action action) throws Exception {
|
||||
blockingAwait(runIOAsyncCompletable(action));
|
||||
}
|
||||
|
||||
public Completable runAsync(Runnable runnable, long timeoutMs) {
|
||||
Future<?> future = runAsync(() -> {
|
||||
runnable.run();
|
||||
return Optional.empty(); // must return an object for using Completable.fromSingle()
|
||||
});
|
||||
return Completable.fromSingle(Single.fromFuture(future, timeoutMs, TimeUnit.MILLISECONDS));
|
||||
}
|
||||
|
||||
public <T> Future<T> runAsync(Callable<T> callable) {
|
||||
// preserve logging context
|
||||
String mdc = mdcAppend("runAsync=" + System.currentTimeMillis());
|
||||
return threadUtil.runAsync(() -> {
|
||||
MDC.put("mdc", mdc);
|
||||
return callable.call();
|
||||
});
|
||||
}
|
||||
|
||||
public <T> Single<T> runAsync(Callable<T> callable, long timeoutMs) {
|
||||
Future<T> future = runAsync(callable);
|
||||
return Single.fromFuture(future, timeoutMs, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private static String mdcAppend(String info) {
|
||||
String mdc = MDC.get("mdc");
|
||||
if(mdc == null) {
|
||||
mdc = "";
|
||||
} else {
|
||||
mdc += ",";
|
||||
}
|
||||
mdc += info;
|
||||
return mdc;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public abstract class HttpException extends Exception {
|
||||
|
||||
public HttpException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
public HttpException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public class HttpNetworkException extends HttpException {
|
||||
public HttpNetworkException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpNetworkException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class HttpProxy {
|
||||
private final HttpProxyProtocol protocol;
|
||||
private final String host;
|
||||
private final int port;
|
||||
|
||||
public HttpProxy(HttpProxyProtocol protocol, String host, int port) {
|
||||
this.protocol = protocol;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
public static boolean validate(String proxy) {
|
||||
// check protocol
|
||||
String[] protocols = Arrays.stream(HttpProxyProtocol.values()).map(p -> p.name()).toArray(String[]::new);
|
||||
String regex = "^(" + StringUtils.join(protocols, "|").toLowerCase() + ")://(.+?):([0-9]+)";
|
||||
return proxy.trim().toLowerCase().matches(regex);
|
||||
}
|
||||
|
||||
public HttpProxyProtocol getProtocol() {
|
||||
return protocol;
|
||||
}
|
||||
|
||||
public String getHost() {
|
||||
return host;
|
||||
}
|
||||
|
||||
public int getPort() {
|
||||
return port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return protocol + "://" + host + ":" + port;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public enum HttpProxyProtocol {
|
||||
HTTP,
|
||||
SOCKS,
|
||||
SOCKS5;
|
||||
|
||||
public static Optional<HttpProxyProtocol> find(String value) {
|
||||
try {
|
||||
return Optional.of(valueOf(value));
|
||||
} catch(Exception e) {
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public class HttpResponseException extends HttpException {
|
||||
private final String responseBody;
|
||||
private final int statusCode;
|
||||
|
||||
public HttpResponseException(Exception cause, String responseBody, int statusCode) {
|
||||
super(cause);
|
||||
this.responseBody = responseBody;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public HttpResponseException(String message, String responseBody, int statusCode) {
|
||||
super(message);
|
||||
this.responseBody = responseBody;
|
||||
this.statusCode = statusCode;
|
||||
}
|
||||
|
||||
public HttpResponseException(String responseBody, int statusCode) {
|
||||
this("response statusCode=" + statusCode, responseBody, statusCode);
|
||||
}
|
||||
|
||||
public String getResponseBody() {
|
||||
return responseBody;
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "HttpResponseException{" +
|
||||
"message=" + getMessage() + ", " +
|
||||
"responseBody='" + responseBody + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public class HttpSystemException extends HttpException {
|
||||
public HttpSystemException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public HttpSystemException(Exception cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class HttpUsage {
|
||||
public static final HttpUsage DEFAULT = new HttpUsage("Default");
|
||||
|
||||
private final String name;
|
||||
|
||||
public HttpUsage(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if(this == o) {
|
||||
return true;
|
||||
}
|
||||
if(o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
HttpUsage httpUsage = (HttpUsage) o;
|
||||
return Objects.equals(name, httpUsage.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public interface IBackendClient {
|
||||
<T> T getJson(String url, Class<T> responseType, Map<String, String> headers) throws HttpException;
|
||||
|
||||
<T> T getJson(String url, Class<T> responseType, Map<String, String> headers, boolean async) throws HttpException;
|
||||
|
||||
<T> T postUrlEncoded(String url, Class<T> responseType, Map<String, String> headers, Map<String, String> body) throws Exception;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import io.reactivex.Single;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IHttpClient extends IBackendClient {
|
||||
void connect() throws Exception;
|
||||
|
||||
<T> Single<Optional<T>> postJson(String url, Class<T> responseType, Map<String, String> headers, Object body);
|
||||
|
||||
Single<Optional<String>> postString(String urlStr, Map<String, String> headers, String contentType, String content);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
public interface IHttpClientService {
|
||||
IHttpClient getHttpClient(HttpUsage httpUsage);
|
||||
|
||||
void changeIdentity(); // change Tor identity if any
|
||||
|
||||
void stop();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface IHttpProxySupplier {
|
||||
Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage);
|
||||
|
||||
void changeIdentity();
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
|
||||
public class JSONUtils {
|
||||
private static JSONUtils instance;
|
||||
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
public JSONUtils() {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
|
||||
}
|
||||
|
||||
public static final JSONUtils getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new JSONUtils();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public ObjectMapper getObjectMapper() {
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.reactivex.Single;
|
||||
import io.reactivex.schedulers.Schedulers;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.Callable;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public abstract class JacksonHttpClient implements IHttpClient {
|
||||
private static final Logger log = LoggerFactory.getLogger(JacksonHttpClient.class);
|
||||
|
||||
private final Consumer<Exception> onNetworkError;
|
||||
|
||||
public JacksonHttpClient(Consumer<Exception> onNetworkError) {
|
||||
this.onNetworkError = onNetworkError;
|
||||
}
|
||||
|
||||
protected abstract String requestJsonGet(String urlStr, Map<String, String> headers, boolean async) throws HttpException;
|
||||
|
||||
protected abstract String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody) throws HttpException;
|
||||
|
||||
protected abstract String requestStringPost(String urlStr, Map<String, String> headers, String contentType, String content) throws HttpException;
|
||||
|
||||
protected abstract String requestJsonPostUrlEncoded(String urlStr, Map<String, String> headers, Map<String, String> body) throws HttpException;
|
||||
|
||||
@Override
|
||||
public <T> T getJson(String urlStr, Class<T> responseType, Map<String, String> headers) throws HttpException {
|
||||
return getJson(urlStr, responseType, headers, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getJson(String urlStr, Class<T> responseType, Map<String, String> headers, boolean async) throws HttpException {
|
||||
return httpObservableBlockingSingle(() -> { // run on ioThread
|
||||
try {
|
||||
String responseContent = handleNetworkError("getJson " + urlStr, () -> requestJsonGet(urlStr, headers, async));
|
||||
return parseJson(responseContent, responseType, 200);
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("getJson failed: " + urlStr + ": " + e.toString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Single<Optional<T>> postJson(final String urlStr, final Class<T> responseType, final Map<String, String> headers, final Object bodyObj) {
|
||||
return httpObservable(
|
||||
() -> {
|
||||
try {
|
||||
String jsonBody = getObjectMapper().writeValueAsString(bodyObj);
|
||||
String responseContent = handleNetworkError("postJson " + urlStr, () -> requestJsonPost(urlStr, headers, jsonBody));
|
||||
return parseJson(responseContent, responseType, 200);
|
||||
} catch(HttpException e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("postJson failed: " + urlStr + ": " + e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public Single<Optional<String>> postString(String urlStr, Map<String, String> headers, String contentType, String content) {
|
||||
return httpObservable(
|
||||
() -> {
|
||||
try {
|
||||
return handleNetworkError("postString " + urlStr, () -> requestStringPost(urlStr, headers, contentType, content));
|
||||
} catch(HttpException e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("postJson failed: " + urlStr + ": " + e.toString());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T postUrlEncoded(String urlStr, Class<T> responseType, Map<String, String> headers, Map<String, String> body) throws HttpException {
|
||||
return httpObservableBlockingSingle(() -> { // run on ioThread
|
||||
try {
|
||||
String responseContent = handleNetworkError("postUrlEncoded " + urlStr, () -> requestJsonPostUrlEncoded(urlStr, headers, body));
|
||||
return parseJson(responseContent, responseType, 200);
|
||||
} catch(Exception e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.error("postUrlEncoded failed: " + urlStr + ": " + e);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <T> T parseJson(String responseContent, Class<T> responseType, int statusCode) throws HttpException {
|
||||
T result;
|
||||
if(log.isTraceEnabled()) {
|
||||
String responseStr = (responseContent != null ? responseContent : "null");
|
||||
if(responseStr.length() > 500) {
|
||||
responseStr = responseStr.substring(0, 500) + "...";
|
||||
}
|
||||
log.trace("response[" + (responseType != null ? responseType.getCanonicalName() : "null") + "]: " + responseStr);
|
||||
}
|
||||
if(String.class.equals(responseType)) {
|
||||
result = (T) responseContent;
|
||||
} else {
|
||||
try {
|
||||
result = getObjectMapper().readValue(responseContent, responseType);
|
||||
} catch(Exception e) {
|
||||
throw new HttpResponseException(e, responseContent, statusCode);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected String handleNetworkError(String logInfo, Callable<String> doHttpRequest) throws HttpException {
|
||||
try {
|
||||
try {
|
||||
// first attempt
|
||||
return doHttpRequest.call();
|
||||
} catch(HttpNetworkException e) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.warn("HTTP_ERROR_NETWORK " + logInfo + ", retrying: " + e.getMessage());
|
||||
}
|
||||
// change tor proxy
|
||||
onNetworkError(e);
|
||||
|
||||
// retry second attempt
|
||||
return doHttpRequest.call();
|
||||
}
|
||||
} catch(HttpException e) { // forward
|
||||
throw e;
|
||||
} catch(Exception e) { // should never happen
|
||||
throw new HttpSystemException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected void onNetworkError(HttpNetworkException e) {
|
||||
if(onNetworkError != null) {
|
||||
synchronized(JacksonHttpClient.class) { // avoid overlapping Tor restarts between httpClients
|
||||
onNetworkError.accept(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> Single<Optional<T>> httpObservable(final Callable<T> supplier) {
|
||||
return Single.fromCallable(() -> Optional.ofNullable(supplier.call())).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
protected <T> T httpObservableBlockingSingle(final Callable<T> supplier) throws HttpException {
|
||||
try {
|
||||
Optional<T> opt = AsyncUtil.getInstance().blockingGet(httpObservable(supplier));
|
||||
return opt.orElse(null);
|
||||
} catch(HttpException e) { // forward
|
||||
throw e;
|
||||
} catch(Exception e) { // should never happen
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected ObjectMapper getObjectMapper() {
|
||||
return JSONUtils.getInstance().getObjectMapper();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.api.ContentResponse;
|
||||
import org.eclipse.jetty.client.api.Request;
|
||||
import org.eclipse.jetty.client.api.Response;
|
||||
import org.eclipse.jetty.client.util.FormContentProvider;
|
||||
import org.eclipse.jetty.client.util.InputStreamResponseListener;
|
||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||
import org.eclipse.jetty.http.HttpMethod;
|
||||
import org.eclipse.jetty.http.HttpStatus;
|
||||
import org.eclipse.jetty.util.Fields;
|
||||
import org.eclipse.jetty.util.component.LifeCycle;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.Scanner;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public class JettyHttpClient extends JacksonHttpClient {
|
||||
protected static Logger log = LoggerFactory.getLogger(JettyHttpClient.class);
|
||||
public static final String CONTENTTYPE_APPLICATION_JSON = "application/json";
|
||||
|
||||
private final HttpClient httpClient;
|
||||
private final long requestTimeout;
|
||||
private final HttpUsage httpUsage;
|
||||
|
||||
public JettyHttpClient(Consumer<Exception> onNetworkError, HttpClient httpClient, long requestTimeout, HttpUsage httpUsage) {
|
||||
super(onNetworkError);
|
||||
this.httpClient = httpClient;
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.httpUsage = httpUsage;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void connect() throws HttpException {
|
||||
try {
|
||||
if(!httpClient.isRunning()) {
|
||||
httpClient.start();
|
||||
}
|
||||
} catch(Exception e) {
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public void restart() {
|
||||
try {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("restart");
|
||||
}
|
||||
if(httpClient.isRunning()) {
|
||||
httpClient.stop();
|
||||
}
|
||||
httpClient.start();
|
||||
} catch(Exception e) {
|
||||
log.error("", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
if(httpClient.isRunning()) {
|
||||
httpClient.stop();
|
||||
Executor executor = httpClient.getExecutor();
|
||||
if(executor instanceof LifeCycle) {
|
||||
((LifeCycle) executor).stop();
|
||||
}
|
||||
}
|
||||
} catch(Exception e) {
|
||||
log.error("Error stopping client", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestJsonGet(String urlStr, Map<String, String> headers, boolean async) throws HttpException {
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.GET, headers);
|
||||
return makeRequest(req, async);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestJsonPost(String urlStr, Map<String, String> headers, String jsonBody) throws HttpException {
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
||||
req.content(new StringContentProvider(CONTENTTYPE_APPLICATION_JSON, jsonBody, StandardCharsets.UTF_8));
|
||||
return makeRequest(req, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestStringPost(String urlStr, Map<String, String> headers, String contentType, String content) throws HttpException {
|
||||
log.debug("POST " + urlStr);
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
||||
req.content(new StringContentProvider(content), contentType);
|
||||
return makeRequest(req, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String requestJsonPostUrlEncoded(String urlStr, Map<String, String> headers, Map<String, String> body) throws HttpException {
|
||||
Request req = computeHttpRequest(urlStr, HttpMethod.POST, headers);
|
||||
req.content(new FormContentProvider(computeBodyFields(body)));
|
||||
return makeRequest(req, false);
|
||||
}
|
||||
|
||||
private Fields computeBodyFields(Map<String, String> body) {
|
||||
Fields fields = new Fields();
|
||||
for(Map.Entry<String, String> entry : body.entrySet()) {
|
||||
fields.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
protected String makeRequest(Request req, boolean async) throws HttpException {
|
||||
String responseContent;
|
||||
if(async) {
|
||||
InputStreamResponseListener listener = new InputStreamResponseListener();
|
||||
req.send(listener);
|
||||
|
||||
// Call to the listener's get() blocks until the headers arrived
|
||||
Response response;
|
||||
try {
|
||||
response = listener.get(requestTimeout, TimeUnit.MILLISECONDS);
|
||||
} catch(Exception e) {
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
|
||||
// Read content
|
||||
InputStream is = listener.getInputStream();
|
||||
Scanner s = new Scanner(is).useDelimiter("\\A");
|
||||
responseContent = s.hasNext() ? s.next() : null;
|
||||
|
||||
// check status
|
||||
checkResponseStatus(response.getStatus(), responseContent);
|
||||
} else {
|
||||
ContentResponse response;
|
||||
try {
|
||||
response = req.send();
|
||||
} catch(Exception e) {
|
||||
throw new HttpNetworkException(e);
|
||||
}
|
||||
checkResponseStatus(response.getStatus(), response.getContentAsString());
|
||||
responseContent = response.getContentAsString();
|
||||
}
|
||||
return responseContent;
|
||||
}
|
||||
|
||||
private void checkResponseStatus(int status, String responseBody) throws HttpResponseException {
|
||||
if(!HttpStatus.isSuccess(status)) {
|
||||
log.error("Http query failed: status=" + status + ", responseBody=" + responseBody);
|
||||
throw new HttpResponseException(responseBody, status);
|
||||
}
|
||||
}
|
||||
|
||||
public HttpClient getJettyHttpClient() throws HttpException {
|
||||
connect();
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
private Request computeHttpRequest(String url, HttpMethod method, Map<String, String> headers) throws HttpException {
|
||||
if(url.endsWith("/rpc")) {
|
||||
// log RPC as TRACE
|
||||
if(log.isTraceEnabled()) {
|
||||
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
|
||||
log.trace("+" + method + ": " + url + headersStr);
|
||||
}
|
||||
} else {
|
||||
if(log.isDebugEnabled()) {
|
||||
String headersStr = headers != null ? " (" + headers.keySet() + ")" : "";
|
||||
log.debug("+" + method + ": " + url + headersStr);
|
||||
}
|
||||
}
|
||||
Request req = getJettyHttpClient().newRequest(url);
|
||||
req.method(method);
|
||||
if(headers != null) {
|
||||
for(Map.Entry<String, String> entry : headers.entrySet()) {
|
||||
req.header(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
req.timeout(requestTimeout, TimeUnit.MILLISECONDS);
|
||||
return req;
|
||||
}
|
||||
|
||||
public HttpUsage getHttpUsage() {
|
||||
return httpUsage;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import com.google.common.util.concurrent.RateLimiter;
|
||||
import com.sparrowwallet.sparrow.net.http.client.socks5.Socks5Proxy;
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.ProxyConfiguration;
|
||||
import org.eclipse.jetty.client.Socks4Proxy;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.eclipse.jetty.util.thread.QueuedThreadPool;
|
||||
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.lang.invoke.MethodHandles;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public class JettyHttpClientService implements IHttpClientService {
|
||||
private static final Logger log = LoggerFactory.getLogger(JettyHttpClientService.class);
|
||||
private static final String NAME = "HttpClient";
|
||||
public static final long DEFAULT_TIMEOUT = 30000;
|
||||
|
||||
// limit changing Tor identity on network error every 4 minutes
|
||||
private static final double RATE_CHANGE_IDENTITY_ON_NETWORK_ERROR = 1.0 / 240;
|
||||
|
||||
protected Map<HttpUsage, JettyHttpClient> httpClients; // used by Sparrow
|
||||
private final IHttpProxySupplier httpProxySupplier;
|
||||
private final long requestTimeout;
|
||||
|
||||
public JettyHttpClientService(long requestTimeout, IHttpProxySupplier httpProxySupplier) {
|
||||
this.httpProxySupplier = httpProxySupplier != null ? httpProxySupplier : computeHttpProxySupplierDefault();
|
||||
this.requestTimeout = requestTimeout;
|
||||
this.httpClients = new ConcurrentHashMap<>();
|
||||
}
|
||||
|
||||
public JettyHttpClientService(long requestTimeout) {
|
||||
this(requestTimeout, null);
|
||||
}
|
||||
|
||||
public JettyHttpClientService() {
|
||||
this(DEFAULT_TIMEOUT);
|
||||
}
|
||||
|
||||
protected static IHttpProxySupplier computeHttpProxySupplierDefault() {
|
||||
return new IHttpProxySupplier() {
|
||||
@Override
|
||||
public Optional<HttpProxy> getHttpProxy(HttpUsage httpUsage) {
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeIdentity() {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public JettyHttpClient getHttpClient(HttpUsage httpUsage) {
|
||||
JettyHttpClient httpClient = httpClients.get(httpUsage);
|
||||
if(httpClient == null) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("+httpClient[" + httpUsage + "]");
|
||||
}
|
||||
httpClient = computeHttpClient(httpUsage);
|
||||
httpClients.put(httpUsage, httpClient);
|
||||
}
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
protected JettyHttpClient computeHttpClient(HttpUsage httpUsage) {
|
||||
Consumer<Exception> onNetworkError = computeOnNetworkError();
|
||||
HttpClient httpClient = computeJettyClient(httpUsage);
|
||||
return new JettyHttpClient(onNetworkError, httpClient, requestTimeout, httpUsage);
|
||||
}
|
||||
|
||||
protected HttpClient computeJettyClient(HttpUsage httpUsage) {
|
||||
// we use jetty for proxy SOCKS support
|
||||
HttpClient jettyHttpClient = new HttpClient(new SslContextFactory());
|
||||
// jettyHttpClient.setSocketAddressResolver(new MySocketAddressResolver());
|
||||
|
||||
// prevent user-agent tracking
|
||||
jettyHttpClient.setUserAgentField(null);
|
||||
|
||||
// configure
|
||||
configureProxy(jettyHttpClient, httpUsage);
|
||||
configureThread(jettyHttpClient, httpUsage);
|
||||
|
||||
return jettyHttpClient;
|
||||
}
|
||||
|
||||
protected Consumer<Exception> computeOnNetworkError() {
|
||||
RateLimiter rateLimiter = RateLimiter.create(RATE_CHANGE_IDENTITY_ON_NETWORK_ERROR);
|
||||
return e -> {
|
||||
if(!rateLimiter.tryAcquire()) {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("onNetworkError: not changing Tor identity (too many recent attempts)");
|
||||
}
|
||||
return;
|
||||
}
|
||||
// change Tor identity on network error
|
||||
httpProxySupplier.changeIdentity();
|
||||
};
|
||||
}
|
||||
|
||||
protected void configureProxy(HttpClient jettyHttpClient, HttpUsage httpUsage) {
|
||||
Optional<HttpProxy> httpProxyOptional = httpProxySupplier.getHttpProxy(httpUsage);
|
||||
if(httpProxyOptional != null && httpProxyOptional.isPresent()) {
|
||||
HttpProxy httpProxy = httpProxyOptional.get();
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("+httpClient: proxy=" + httpProxy);
|
||||
}
|
||||
ProxyConfiguration.Proxy jettyProxy = computeJettyProxy(httpProxy);
|
||||
jettyHttpClient.getProxyConfiguration().getProxies().add(jettyProxy);
|
||||
} else {
|
||||
if(log.isDebugEnabled()) {
|
||||
log.debug("+httpClient: no proxy");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void configureThread(HttpClient jettyHttpClient, HttpUsage httpUsage) {
|
||||
String name = NAME + "-" + httpUsage.toString();
|
||||
|
||||
QueuedThreadPool threadPool = new QueuedThreadPool();
|
||||
threadPool.setName(name);
|
||||
threadPool.setDaemon(true);
|
||||
jettyHttpClient.setExecutor(threadPool);
|
||||
jettyHttpClient.setScheduler(new ScheduledExecutorScheduler(name + "-scheduler", true));
|
||||
}
|
||||
|
||||
protected ProxyConfiguration.Proxy computeJettyProxy(HttpProxy httpProxy) {
|
||||
ProxyConfiguration.Proxy jettyProxy = null;
|
||||
switch(httpProxy.getProtocol()) {
|
||||
case SOCKS:
|
||||
jettyProxy = new Socks4Proxy(httpProxy.getHost(), httpProxy.getPort());
|
||||
break;
|
||||
case SOCKS5:
|
||||
jettyProxy = new Socks5Proxy(httpProxy.getHost(), httpProxy.getPort());
|
||||
break;
|
||||
case HTTP:
|
||||
jettyProxy = new org.eclipse.jetty.client.HttpProxy(httpProxy.getHost(), httpProxy.getPort());
|
||||
break;
|
||||
}
|
||||
return jettyProxy;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized void stop() {
|
||||
for(JettyHttpClient httpClient : httpClients.values()) {
|
||||
httpClient.stop();
|
||||
}
|
||||
httpClients.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void changeIdentity() {
|
||||
stop();
|
||||
httpProxySupplier.changeIdentity();
|
||||
}
|
||||
|
||||
public IHttpProxySupplier getHttpProxySupplier() {
|
||||
return httpProxySupplier;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.sparrowwallet.sparrow.net.http.client;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class ThreadUtil {
|
||||
private static final Logger log = LoggerFactory.getLogger(ThreadUtil.class);
|
||||
|
||||
private static ThreadUtil instance;
|
||||
|
||||
private ExecutorService executorService;
|
||||
|
||||
protected ThreadUtil() {
|
||||
this.executorService = computeExecutorService();
|
||||
}
|
||||
|
||||
public static ThreadUtil getInstance() {
|
||||
if(instance == null) {
|
||||
instance = new ThreadUtil();
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
protected ExecutorService computeExecutorService() {
|
||||
return Executors.newFixedThreadPool(5,
|
||||
r -> {
|
||||
Thread t = Executors.defaultThreadFactory().newThread(r);
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
}
|
||||
|
||||
public void setExecutorService(ScheduledExecutorService executorService) {
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
public <T> Future<T> runAsync(Callable<T> callable) {
|
||||
return executorService.submit(callable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Socks5 backported from Jetty12 - we still use Jetty9 for JDK8 compatibility.
|
||||
*/
|
||||
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package com.sparrowwallet.sparrow.net.http.client.socks5;
|
||||
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Helper class for SOCKS5 proxying.
|
||||
*
|
||||
* @see Socks5Proxy
|
||||
*/
|
||||
public class Socks5 {
|
||||
/** The SOCKS protocol version: {@value}. */
|
||||
public static final byte VERSION = 0x05;
|
||||
|
||||
/** The SOCKS5 {@code CONNECT} command used in SOCKS5 connect requests. */
|
||||
public static final byte COMMAND_CONNECT = 0x01;
|
||||
|
||||
/** The reserved byte value: {@value}. */
|
||||
public static final byte RESERVED = 0x00;
|
||||
|
||||
/** The address type for IPv4 used in SOCKS5 connect requests and responses. */
|
||||
public static final byte ADDRESS_TYPE_IPV4 = 0x01;
|
||||
|
||||
/** The address type for domain names used in SOCKS5 connect requests and responses. */
|
||||
public static final byte ADDRESS_TYPE_DOMAIN = 0x03;
|
||||
|
||||
/** The address type for IPv6 used in SOCKS5 connect requests and responses. */
|
||||
public static final byte ADDRESS_TYPE_IPV6 = 0x04;
|
||||
|
||||
private Socks5() {
|
||||
}
|
||||
|
||||
/**
|
||||
* A SOCKS5 authentication method.
|
||||
*
|
||||
* <p>Implementations should send and receive the bytes that are specific to the particular
|
||||
* authentication method.
|
||||
*/
|
||||
public interface Authentication {
|
||||
/**
|
||||
* Performs the authentication send and receive bytes exchanges specific for this {@link
|
||||
* Authentication}.
|
||||
*
|
||||
* @param endPoint the {@link EndPoint} to send to and receive from the SOCKS5 server
|
||||
* @param callback the callback to complete when the authentication is complete
|
||||
*/
|
||||
void authenticate(EndPoint endPoint, Callback callback);
|
||||
|
||||
/** A factory for {@link Authentication}s. */
|
||||
interface Factory {
|
||||
/**
|
||||
* @return the authentication method defined by RFC 1928
|
||||
*/
|
||||
byte getMethod();
|
||||
|
||||
/**
|
||||
* @return a new {@link Authentication}
|
||||
*/
|
||||
Authentication newAuthentication();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation of the {@code NO AUTH} authentication method defined in <a
|
||||
* href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.
|
||||
*/
|
||||
public static class NoAuthenticationFactory implements Authentication.Factory {
|
||||
public static final byte METHOD = 0x00;
|
||||
|
||||
@Override
|
||||
public byte getMethod() {
|
||||
return METHOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication newAuthentication() {
|
||||
return (endPoint, callback) -> callback.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The implementation of the {@code USERNAME/PASSWORD} authentication method defined in <a
|
||||
* href="https://datatracker.ietf.org/doc/html/rfc1929">RFC 1929</a>.
|
||||
*/
|
||||
public static class UsernamePasswordAuthenticationFactory implements Authentication.Factory {
|
||||
public static final byte METHOD = 0x02;
|
||||
public static final byte VERSION = 0x01;
|
||||
private static final Logger LOG =
|
||||
LoggerFactory.getLogger(UsernamePasswordAuthenticationFactory.class);
|
||||
|
||||
private final String userName;
|
||||
private final String password;
|
||||
private final Charset charset;
|
||||
|
||||
public UsernamePasswordAuthenticationFactory(String userName, String password) {
|
||||
this(userName, password, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
public UsernamePasswordAuthenticationFactory(
|
||||
String userName, String password, Charset charset) {
|
||||
this.userName = Objects.requireNonNull(userName);
|
||||
this.password = Objects.requireNonNull(password);
|
||||
this.charset = Objects.requireNonNull(charset);
|
||||
}
|
||||
|
||||
@Override
|
||||
public byte getMethod() {
|
||||
return METHOD;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authentication newAuthentication() {
|
||||
return new UsernamePasswordAuthentication(this);
|
||||
}
|
||||
|
||||
private static class UsernamePasswordAuthentication implements Authentication, Callback {
|
||||
private final ByteBuffer byteBuffer = BufferUtil.allocate(2);
|
||||
private final UsernamePasswordAuthenticationFactory factory;
|
||||
private EndPoint endPoint;
|
||||
private Callback callback;
|
||||
|
||||
private UsernamePasswordAuthentication(UsernamePasswordAuthenticationFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(EndPoint endPoint, Callback callback) {
|
||||
this.endPoint = endPoint;
|
||||
this.callback = callback;
|
||||
|
||||
byte[] userNameBytes = factory.userName.getBytes(factory.charset);
|
||||
byte[] passwordBytes = factory.password.getBytes(factory.charset);
|
||||
ByteBuffer byteBuffer =
|
||||
(ByteBuffer)
|
||||
ByteBuffer.allocate(3 + userNameBytes.length + passwordBytes.length)
|
||||
.put(VERSION)
|
||||
.put((byte) userNameBytes.length)
|
||||
.put(userNameBytes)
|
||||
.put((byte) passwordBytes.length)
|
||||
.put(passwordBytes)
|
||||
.flip();
|
||||
endPoint.write(Callback.from(this::authenticationSent, this::failed), byteBuffer);
|
||||
}
|
||||
|
||||
private void authenticationSent() {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Written SOCKS5 username/password authentication request");
|
||||
}
|
||||
endPoint.fillInterested(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void succeeded() {
|
||||
try {
|
||||
int filled = endPoint.fill(byteBuffer);
|
||||
if(filled < 0) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
if(byteBuffer.remaining() < 2) {
|
||||
endPoint.fillInterested(this);
|
||||
return;
|
||||
}
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Received SOCKS5 username/password authentication response");
|
||||
}
|
||||
byte version = byteBuffer.get();
|
||||
if(version != VERSION) {
|
||||
throw new IOException(
|
||||
"Unsupported username/password authentication version: " + version);
|
||||
}
|
||||
byte status = byteBuffer.get();
|
||||
if(status != 0) {
|
||||
throw new IOException("SOCK5 username/password authentication failure");
|
||||
}
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("SOCKS5 username/password authentication succeeded");
|
||||
}
|
||||
callback.succeeded();
|
||||
} catch(Throwable x) {
|
||||
failed(x);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failed(Throwable x) {
|
||||
callback.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InvocationType getInvocationType() {
|
||||
return InvocationType.NON_BLOCKING;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,419 @@
|
||||
//
|
||||
// ========================================================================
|
||||
// Copyright (c) 1995 Mort Bay Consulting Pty Ltd and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
|
||||
// which is available at https://www.apache.org/licenses/LICENSE-2.0.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
|
||||
// ========================================================================
|
||||
//
|
||||
|
||||
package com.sparrowwallet.sparrow.net.http.client.socks5;
|
||||
|
||||
import org.eclipse.jetty.client.HttpClient;
|
||||
import org.eclipse.jetty.client.HttpClientTransport;
|
||||
import org.eclipse.jetty.client.HttpDestination;
|
||||
import org.eclipse.jetty.client.Origin;
|
||||
import org.eclipse.jetty.client.ProxyConfiguration.Proxy;
|
||||
import org.eclipse.jetty.io.AbstractConnection;
|
||||
import org.eclipse.jetty.io.ClientConnectionFactory;
|
||||
import org.eclipse.jetty.io.Connection;
|
||||
import org.eclipse.jetty.io.EndPoint;
|
||||
import org.eclipse.jetty.io.ssl.SslClientConnectionFactory;
|
||||
import org.eclipse.jetty.util.BufferUtil;
|
||||
import org.eclipse.jetty.util.Callback;
|
||||
import org.eclipse.jetty.util.Promise;
|
||||
import org.eclipse.jetty.util.ssl.SslContextFactory;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.InetAddress;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.ClosedChannelException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Executor;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Client-side proxy configuration for SOCKS5, defined by <a
|
||||
* href="https://datatracker.ietf.org/doc/html/rfc1928">RFC 1928</a>.
|
||||
*
|
||||
* <p>Multiple authentication methods are supported via {@link
|
||||
* #putAuthenticationFactory(Socks5.Authentication.Factory)}. By default only the {@link
|
||||
* Socks5.NoAuthenticationFactory NO AUTH} authentication method is configured. The {@link
|
||||
* Socks5.UsernamePasswordAuthenticationFactory USERNAME/PASSWORD} is available to applications but
|
||||
* must be explicitly configured and added.
|
||||
*/
|
||||
public class Socks5Proxy extends Proxy {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Socks5Proxy.class);
|
||||
|
||||
private final Map<Byte, Socks5.Authentication.Factory> authentications = new LinkedHashMap<>();
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given SOCKS5 proxy host and port.
|
||||
*
|
||||
* @param host the SOCKS5 proxy host name
|
||||
* @param port the SOCKS5 proxy port
|
||||
*/
|
||||
public Socks5Proxy(String host, int port) {
|
||||
this(new Origin.Address(host, port), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new instance with the given SOCKS5 proxy address.
|
||||
*
|
||||
* <p>When {@code secure=true} the communication between the client and the proxy will be
|
||||
* encrypted (using this proxy {@link #getSslContextFactory()} which typically defaults to that of
|
||||
* {@link HttpClient}.
|
||||
*
|
||||
* @param address the SOCKS5 proxy address (host and port)
|
||||
* @param secure whether the communication between the client and the SOCKS5 proxy should be
|
||||
* secure
|
||||
*/
|
||||
public Socks5Proxy(Origin.Address address, boolean secure) {
|
||||
super(address, secure);
|
||||
putAuthenticationFactory(new Socks5.NoAuthenticationFactory());
|
||||
}
|
||||
|
||||
protected static ClientConnectionFactory newSslClientConnectionFactory(HttpClient httpClient, SslContextFactory sslContextFactory, ClientConnectionFactory connectionFactory) {
|
||||
if(sslContextFactory == null) {
|
||||
sslContextFactory = httpClient.getSslContextFactory();
|
||||
}
|
||||
return new SslClientConnectionFactory(sslContextFactory, httpClient.getByteBufferPool(), httpClient.getExecutor(), connectionFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides this class with the given SOCKS5 authentication method.
|
||||
*
|
||||
* @param authenticationFactory the SOCKS5 authentication factory
|
||||
* @return the previous authentication method of the same type, or {@code null} if there was none
|
||||
* of that type already present
|
||||
*/
|
||||
public Socks5.Authentication.Factory putAuthenticationFactory(Socks5.Authentication.Factory authenticationFactory) {
|
||||
return authentications.put(authenticationFactory.getMethod(), authenticationFactory);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the authentication of the given {@code method}.
|
||||
*
|
||||
* @param method the authentication method to remove
|
||||
*/
|
||||
public Socks5.Authentication.Factory removeAuthenticationFactory(byte method) {
|
||||
return authentications.remove(method);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnectionFactory newClientConnectionFactory(ClientConnectionFactory connectionFactory) {
|
||||
return new Socks5ProxyClientConnectionFactory(connectionFactory);
|
||||
}
|
||||
|
||||
private static class Socks5ProxyConnection extends AbstractConnection implements Connection.UpgradeFrom {
|
||||
private static final Pattern IPv4_PATTERN = Pattern.compile("(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})");
|
||||
|
||||
// SOCKS5 response max length is 262 bytes.
|
||||
private final ByteBuffer byteBuffer = BufferUtil.allocate(512);
|
||||
private final ClientConnectionFactory connectionFactory;
|
||||
private final Map<String, Object> context;
|
||||
private final Map<Byte, Socks5.Authentication.Factory> authentications;
|
||||
private State state = State.HANDSHAKE;
|
||||
|
||||
private Socks5ProxyConnection(EndPoint endPoint, Executor executor, ClientConnectionFactory connectionFactory, Map<String, Object> context, Map<Byte, Socks5.Authentication.Factory> authentications) {
|
||||
super(endPoint, executor);
|
||||
this.connectionFactory = connectionFactory;
|
||||
this.context = context;
|
||||
this.authentications = new LinkedHashMap<>(authentications);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ByteBuffer onUpgradeFrom() {
|
||||
return BufferUtil.copy(byteBuffer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpen() {
|
||||
super.onOpen();
|
||||
sendHandshake();
|
||||
}
|
||||
|
||||
private void sendHandshake() {
|
||||
try {
|
||||
// +-------------+--------------------+------------------+
|
||||
// | version (1) | num of methods (1) | methods (1..255) |
|
||||
// +-------------+--------------------+------------------+
|
||||
int size = authentications.size();
|
||||
ByteBuffer byteBuffer =
|
||||
ByteBuffer.allocate(1 + 1 + size).put(Socks5.VERSION).put((byte) size);
|
||||
authentications.keySet().forEach(byteBuffer::put);
|
||||
byteBuffer.flip();
|
||||
getEndPoint().write(Callback.from(this::handshakeSent, this::fail), byteBuffer);
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void handshakeSent() {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Written SOCKS5 handshake request");
|
||||
}
|
||||
state = State.HANDSHAKE;
|
||||
fillInterested();
|
||||
}
|
||||
|
||||
private void fail(Throwable x) {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("SOCKS5 failure", x);
|
||||
}
|
||||
getEndPoint().close();
|
||||
@SuppressWarnings("unchecked")
|
||||
Promise<Connection> promise =
|
||||
(Promise<Connection>)
|
||||
this.context.get(HttpClientTransport.HTTP_CONNECTION_PROMISE_CONTEXT_KEY);
|
||||
promise.failed(x);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFillable() {
|
||||
try {
|
||||
switch(state) {
|
||||
case HANDSHAKE:
|
||||
receiveHandshake();
|
||||
break;
|
||||
case CONNECT:
|
||||
receiveConnect();
|
||||
break;
|
||||
default:
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void receiveHandshake() throws IOException {
|
||||
// +-------------+------------+
|
||||
// | version (1) | method (1) |
|
||||
// +-------------+------------+
|
||||
int filled = getEndPoint().fill(byteBuffer);
|
||||
if(filled < 0) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
if(byteBuffer.remaining() < 2) {
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Received SOCKS5 handshake response {}", BufferUtil.toDetailString(byteBuffer));
|
||||
}
|
||||
|
||||
byte version = byteBuffer.get();
|
||||
if(version != Socks5.VERSION) {
|
||||
throw new IOException("Unsupported SOCKS5 version: " + version);
|
||||
}
|
||||
|
||||
byte method = byteBuffer.get();
|
||||
if(method == -1) {
|
||||
throw new IOException("Unacceptable SOCKS5 authentication methods");
|
||||
}
|
||||
|
||||
Socks5.Authentication.Factory factory = authentications.get(method);
|
||||
if(factory == null) {
|
||||
throw new IOException("Unknown SOCKS5 authentication method: " + method);
|
||||
}
|
||||
|
||||
factory
|
||||
.newAuthentication()
|
||||
.authenticate(getEndPoint(), Callback.from(this::sendConnect, this::fail));
|
||||
}
|
||||
|
||||
private void sendConnect() {
|
||||
try {
|
||||
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
||||
// | version (1) | command (1) | reserved (1) | address type (1) | address bytes (4..255) |
|
||||
// port (2) |
|
||||
// +-------------+-------------+--------------+------------------+------------------------+----------+
|
||||
HttpDestination destination = (HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||
Origin.Address address = destination.getOrigin().getAddress();
|
||||
String host = address.getHost();
|
||||
short port = (short) address.getPort();
|
||||
|
||||
ByteBuffer byteBuffer;
|
||||
Matcher matcher = IPv4_PATTERN.matcher(host);
|
||||
if(matcher.matches()) {
|
||||
byteBuffer =
|
||||
ByteBuffer.allocate(10)
|
||||
.put(Socks5.VERSION)
|
||||
.put(Socks5.COMMAND_CONNECT)
|
||||
.put(Socks5.RESERVED)
|
||||
.put(Socks5.ADDRESS_TYPE_IPV4);
|
||||
for(int i = 1; i <= 4; ++i) {
|
||||
byteBuffer.put(Byte.parseByte(matcher.group(i)));
|
||||
}
|
||||
byteBuffer.putShort(port).flip();
|
||||
} else if(true /*URIUtil.isValidHostRegisteredName(host)*/) {
|
||||
byte[] bytes = host.getBytes(StandardCharsets.US_ASCII);
|
||||
if(bytes.length > 255) {
|
||||
throw new IOException("Invalid host name: " + host);
|
||||
}
|
||||
byteBuffer =
|
||||
(ByteBuffer)
|
||||
ByteBuffer.allocate(7 + bytes.length)
|
||||
.put(Socks5.VERSION)
|
||||
.put(Socks5.COMMAND_CONNECT)
|
||||
.put(Socks5.RESERVED)
|
||||
.put(Socks5.ADDRESS_TYPE_DOMAIN)
|
||||
.put((byte) bytes.length)
|
||||
.put(bytes)
|
||||
.putShort(port)
|
||||
.flip();
|
||||
} else {
|
||||
// Assume IPv6.
|
||||
byte[] bytes = InetAddress.getByName(host).getAddress();
|
||||
byteBuffer =
|
||||
(ByteBuffer)
|
||||
ByteBuffer.allocate(22)
|
||||
.put(Socks5.VERSION)
|
||||
.put(Socks5.COMMAND_CONNECT)
|
||||
.put(Socks5.RESERVED)
|
||||
.put(Socks5.ADDRESS_TYPE_IPV6)
|
||||
.put(bytes)
|
||||
.putShort(port)
|
||||
.flip();
|
||||
}
|
||||
|
||||
getEndPoint().write(Callback.from(this::connectSent, this::fail), byteBuffer);
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private void connectSent() {
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Written SOCKS5 connect request");
|
||||
}
|
||||
state = State.CONNECT;
|
||||
fillInterested();
|
||||
}
|
||||
|
||||
private void receiveConnect() throws IOException {
|
||||
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
||||
// | version (1) | reply (1) | reserved (1) | address type (1) | address bytes (4..255) | port
|
||||
// (2) |
|
||||
// +-------------+-----------+--------------+------------------+------------------------+----------+
|
||||
int filled = getEndPoint().fill(byteBuffer);
|
||||
if(filled < 0) {
|
||||
throw new ClosedChannelException();
|
||||
}
|
||||
if(byteBuffer.remaining() < 5) {
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
byte addressType = byteBuffer.get(3);
|
||||
int length = 6;
|
||||
if(addressType == Socks5.ADDRESS_TYPE_IPV4) {
|
||||
length += 4;
|
||||
} else if(addressType == Socks5.ADDRESS_TYPE_DOMAIN) {
|
||||
length += 1 + (byteBuffer.get(4) & 0xFF);
|
||||
} else if(addressType == Socks5.ADDRESS_TYPE_IPV6) {
|
||||
length += 16;
|
||||
} else {
|
||||
throw new IOException("Invalid SOCKS5 address type: " + addressType);
|
||||
}
|
||||
if(byteBuffer.remaining() < length) {
|
||||
fillInterested();
|
||||
return;
|
||||
}
|
||||
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("Received SOCKS5 connect response {}", BufferUtil.toDetailString(byteBuffer));
|
||||
}
|
||||
|
||||
// We have all the SOCKS5 bytes.
|
||||
byte version = byteBuffer.get();
|
||||
if(version != Socks5.VERSION) {
|
||||
throw new IOException("Unsupported SOCKS5 version: " + version);
|
||||
}
|
||||
|
||||
byte status = byteBuffer.get();
|
||||
switch(status) {
|
||||
case 0: {
|
||||
// Consume the buffer before upgrading to the tunnel.
|
||||
byteBuffer.position(length);
|
||||
tunnel();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
throw new IOException("SOCKS5 general failure");
|
||||
case 2:
|
||||
throw new IOException("SOCKS5 connection not allowed");
|
||||
case 3:
|
||||
throw new IOException("SOCKS5 network unreachable");
|
||||
case 4:
|
||||
throw new IOException("SOCKS5 host unreachable");
|
||||
case 5:
|
||||
throw new IOException("SOCKS5 connection refused");
|
||||
case 6:
|
||||
throw new IOException("SOCKS5 timeout expired");
|
||||
case 7:
|
||||
throw new IOException("SOCKS5 unsupported command");
|
||||
case 8:
|
||||
throw new IOException("SOCKS5 unsupported address");
|
||||
default:
|
||||
throw new IOException("SOCKS5 unknown status: " + status);
|
||||
}
|
||||
}
|
||||
|
||||
private void tunnel() {
|
||||
try {
|
||||
HttpDestination destination =
|
||||
(HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||
this.context.put("ssl.peer.host", destination.getHost());
|
||||
this.context.put("ssl.peer.port", destination.getPort());
|
||||
// Origin.Address address = destination.getOrigin().getAddress();
|
||||
// Don't want to do DNS resolution here.
|
||||
// InetSocketAddress inet = InetSocketAddress.createUnresolved(address.getHost(),
|
||||
// address.getPort());
|
||||
// context.put(ClientConnector.REMOTE_SOCKET_ADDRESS_CONTEXT_KEY, inet);
|
||||
ClientConnectionFactory connectionFactory = this.connectionFactory;
|
||||
if(destination.isSecure()) {
|
||||
connectionFactory =
|
||||
newSslClientConnectionFactory(destination.getHttpClient(), null, connectionFactory);
|
||||
}
|
||||
Connection newConnection = connectionFactory.newConnection(getEndPoint(), context);
|
||||
getEndPoint().upgrade(newConnection);
|
||||
if(LOG.isDebugEnabled()) {
|
||||
LOG.debug("SOCKS5 tunnel established: {} over {}", this, newConnection);
|
||||
}
|
||||
} catch(Throwable x) {
|
||||
fail(x);
|
||||
}
|
||||
}
|
||||
|
||||
private enum State {
|
||||
HANDSHAKE,
|
||||
CONNECT
|
||||
}
|
||||
}
|
||||
|
||||
private class Socks5ProxyClientConnectionFactory implements ClientConnectionFactory {
|
||||
private final ClientConnectionFactory connectionFactory;
|
||||
|
||||
private Socks5ProxyClientConnectionFactory(ClientConnectionFactory connectionFactory) {
|
||||
this.connectionFactory = connectionFactory;
|
||||
}
|
||||
|
||||
public Connection newConnection(EndPoint endPoint, Map<String, Object> context) {
|
||||
HttpDestination destination = (HttpDestination) context.get(HttpClientTransport.HTTP_DESTINATION_CONTEXT_KEY);
|
||||
Executor executor = destination.getHttpClient().getExecutor();
|
||||
Socks5ProxyConnection connection = new Socks5ProxyConnection(endPoint, executor, connectionFactory, context, authentications);
|
||||
return customize(connection, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.payjoin;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.gson.Gson;
|
||||
import com.samourai.wallet.httpClient.HttpResponseException;
|
||||
import com.sparrowwallet.drongo.protocol.Script;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionInput;
|
||||
@@ -17,6 +16,7 @@ import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.net.HttpClientService;
|
||||
import com.sparrowwallet.sparrow.net.Protocol;
|
||||
import com.sparrowwallet.sparrow.net.http.client.HttpResponseException;
|
||||
import javafx.concurrent.Service;
|
||||
import javafx.concurrent.Task;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
@@ -58,14 +58,6 @@ public class PayNym {
|
||||
return followers;
|
||||
}
|
||||
|
||||
public boolean isCollaborativeSend() {
|
||||
return collaborativeSend;
|
||||
}
|
||||
|
||||
public void setCollaborativeSend(boolean collaborativeSend) {
|
||||
this.collaborativeSend = collaborativeSend;
|
||||
}
|
||||
|
||||
public List<ScriptType> getScriptTypes() {
|
||||
return segwit ? getSegwitScriptTypes() : getV1ScriptTypes();
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.paynym;
|
||||
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
|
||||
public final class PayNymAddress extends P2WPKHAddress {
|
||||
private final PayNym payNym;
|
||||
|
||||
public PayNymAddress(PayNym payNym) {
|
||||
super(new byte[20]);
|
||||
this.payNym = payNym;
|
||||
}
|
||||
|
||||
public PayNym getPayNym() {
|
||||
return payNym;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return payNym.nymName();
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,6 @@ public class PayNymController {
|
||||
public static final String INVALID_PAYMENT_CODE_LABEL = "Invalid Payment Code";
|
||||
|
||||
private String walletId;
|
||||
private boolean selectLinkedOnly;
|
||||
private PayNym walletPayNym;
|
||||
private boolean requestingPassword;
|
||||
|
||||
@@ -88,9 +87,8 @@ public class PayNymController {
|
||||
|
||||
private final BooleanProperty closeProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
public void initializeView(String walletId, boolean selectLinkedOnly) {
|
||||
public void initializeView(String walletId) {
|
||||
this.walletId = walletId;
|
||||
this.selectLinkedOnly = selectLinkedOnly;
|
||||
|
||||
payNymName.managedProperty().bind(payNymName.visibleProperty());
|
||||
payNymRetrieve.managedProperty().bind(payNymRetrieve.visibleProperty());
|
||||
@@ -668,10 +666,6 @@ public class PayNymController {
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isSelectLinkedOnly() {
|
||||
return selectLinkedOnly;
|
||||
}
|
||||
|
||||
public PayNym getPayNym() {
|
||||
return payNymProperty.get();
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ import java.io.IOException;
|
||||
|
||||
public class PayNymDialog extends Dialog<PayNym> {
|
||||
public PayNymDialog(String walletId) {
|
||||
this(walletId, Operation.SHOW, false);
|
||||
this(walletId, Operation.SHOW);
|
||||
}
|
||||
|
||||
public PayNymDialog(String walletId, Operation operation, boolean selectLinkedOnly) {
|
||||
public PayNymDialog(String walletId, Operation operation) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
|
||||
@@ -21,7 +21,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
FXMLLoader payNymLoader = new FXMLLoader(AppServices.class.getResource("paynym/paynym.fxml"));
|
||||
dialogPane.setContent(payNymLoader.load());
|
||||
PayNymController payNymController = payNymLoader.getController();
|
||||
payNymController.initializeView(walletId, selectLinkedOnly);
|
||||
payNymController.initializeView(walletId);
|
||||
|
||||
EventManager.get().register(payNymController);
|
||||
|
||||
@@ -33,26 +33,13 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm());
|
||||
dialogPane.getStylesheets().add(AppServices.class.getResource("paynym/paynym.css").toExternalForm());
|
||||
|
||||
final ButtonType sendDirectlyButtonType = new javafx.scene.control.ButtonType("Send Directly", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType sendCollaborativelyButtonType = new javafx.scene.control.ButtonType("Send Collaboratively", ButtonBar.ButtonData.OK_DONE);
|
||||
final ButtonType sendDirectlyButtonType = new javafx.scene.control.ButtonType("Send To Contact", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType selectButtonType = new javafx.scene.control.ButtonType("Select Contact", ButtonBar.ButtonData.APPLY);
|
||||
final ButtonType cancelButtonType = new javafx.scene.control.ButtonType("Cancel", ButtonBar.ButtonData.CANCEL_CLOSE);
|
||||
final ButtonType doneButtonType = new javafx.scene.control.ButtonType("Done", ButtonBar.ButtonData.OK_DONE);
|
||||
|
||||
if(operation == Operation.SEND) {
|
||||
if(selectLinkedOnly) {
|
||||
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, cancelButtonType);
|
||||
} else {
|
||||
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, sendCollaborativelyButtonType, cancelButtonType);
|
||||
Button sendCollaborativelyButton = (Button)dialogPane.lookupButton(sendCollaborativelyButtonType);
|
||||
sendCollaborativelyButton.setDisable(true);
|
||||
sendCollaborativelyButton.setDefaultButton(false);
|
||||
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
||||
sendCollaborativelyButton.setDisable(payNym == null);
|
||||
sendCollaborativelyButton.setDefaultButton(payNym != null && !payNymController.isLinked(payNym));
|
||||
});
|
||||
}
|
||||
|
||||
dialogPane.getButtonTypes().addAll(sendDirectlyButtonType, cancelButtonType);
|
||||
Button sendDirectlyButton = (Button)dialogPane.lookupButton(sendDirectlyButtonType);
|
||||
sendDirectlyButton.setDisable(true);
|
||||
sendDirectlyButton.setDefaultButton(true);
|
||||
@@ -66,7 +53,7 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
selectButton.setDisable(true);
|
||||
selectButton.setDefaultButton(true);
|
||||
payNymController.payNymProperty().addListener((observable, oldValue, payNym) -> {
|
||||
selectButton.setDisable(payNym == null || (selectLinkedOnly && !payNymController.isLinked(payNym)));
|
||||
selectButton.setDisable(payNym == null || !payNymController.isLinked(payNym));
|
||||
});
|
||||
} else {
|
||||
dialogPane.getButtonTypes().add(doneButtonType);
|
||||
@@ -83,14 +70,8 @@ public class PayNymDialog extends Dialog<PayNym> {
|
||||
});
|
||||
|
||||
setResultConverter(dialogButton -> {
|
||||
if(dialogButton == sendCollaborativelyButtonType) {
|
||||
PayNym payNym = payNymController.getPayNym();
|
||||
payNym.setCollaborativeSend(true);
|
||||
return payNym;
|
||||
} else if(dialogButton == sendDirectlyButtonType || dialogButton == selectButtonType) {
|
||||
PayNym payNym = payNymController.getPayNym();
|
||||
payNym.setCollaborativeSend(false);
|
||||
return payNym;
|
||||
if(dialogButton == sendDirectlyButtonType || dialogButton == selectButtonType) {
|
||||
return payNymController.getPayNym();
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user