mirror of
https://github.com/sparrowwallet/sparrow.git
synced 2026-07-30 19:46:16 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad091217d6 | ||
|
|
40e06b96a9 | ||
|
|
3fd186e22c | ||
|
|
e8c7f57704 | ||
|
|
e6de33034b | ||
|
|
73b6b9219b | ||
|
|
d1d1b0806f | ||
|
|
5ee97966ee | ||
|
|
4819f329ae | ||
|
|
52aed8a3f5 | ||
|
|
86ef129c1b | ||
|
|
aff872eea0 | ||
|
|
5d0025b4a7 | ||
|
|
e625a4e542 | ||
|
|
72ff1df61e | ||
|
|
3776fbafd9 | ||
|
|
cd91aff3bb | ||
|
|
b530ced9ed | ||
|
|
22957e9d88 | ||
|
|
d76aecb34d | ||
|
|
4cbb402931 | ||
|
|
4da82b110c |
+5
-5
@@ -5,7 +5,7 @@ plugins {
|
||||
id 'org.beryx.jlink' version '2.24.0'
|
||||
}
|
||||
|
||||
def sparrowVersion = '1.5.4'
|
||||
def sparrowVersion = '1.5.5'
|
||||
def os = org.gradle.internal.os.OperatingSystem.current()
|
||||
def osName = os.getFamilyName()
|
||||
if(os.macOsX) {
|
||||
@@ -92,7 +92,7 @@ dependencies {
|
||||
implementation('org.slf4j:jul-to-slf4j:1.7.30') {
|
||||
exclude group: 'org.slf4j'
|
||||
}
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.24')
|
||||
implementation('com.sparrowwallet.nightjar:nightjar:0.2.25')
|
||||
implementation('io.reactivex.rxjava2:rxjava:2.2.15')
|
||||
implementation('io.reactivex.rxjava2:rxjavafx:2.2.2')
|
||||
implementation('org.apache.commons:commons-lang3:3.7')
|
||||
@@ -360,7 +360,7 @@ extraJavaModuleInfo {
|
||||
module('json-simple-1.1.1.jar', 'json.simple', '1.1.1') {
|
||||
exports('org.json.simple')
|
||||
}
|
||||
module('logback-classic-1.2.3.jar', 'logback.classic', '1.2.3') {
|
||||
module('logback-classic-1.2.8.jar', 'logback.classic', '1.2.8') {
|
||||
exports('ch.qos.logback.classic')
|
||||
requires('org.slf4j')
|
||||
requires('logback.core')
|
||||
@@ -460,7 +460,7 @@ extraJavaModuleInfo {
|
||||
module('cbor-0.9.jar', 'co.nstant.in.cbor', '0.9') {
|
||||
exports('co.nstant.in.cbor')
|
||||
}
|
||||
module('nightjar-0.2.24.jar', 'com.sparrowwallet.nightjar', '0.2.24') {
|
||||
module('nightjar-0.2.25.jar', 'com.sparrowwallet.nightjar', '0.2.25') {
|
||||
requires('com.google.common')
|
||||
requires('net.sourceforge.streamsupport')
|
||||
requires('org.slf4j')
|
||||
@@ -572,7 +572,7 @@ extraJavaModuleInfo {
|
||||
requires('org.slf4j')
|
||||
}
|
||||
module('jnacl-1.0.0.jar', 'eu.neilalexander.jnacl', '1.0.0')
|
||||
module('logback-core-1.2.3.jar', 'logback.core', '1.2.3') {
|
||||
module('logback-core-1.2.8.jar', 'logback.core', '1.2.8') {
|
||||
requires('java.xml')
|
||||
}
|
||||
module('kotlin-stdlib-common-1.5.20.jar', 'org.jetbrains.kotlin.kotlin.stdlib.common', '1.5.20') {
|
||||
|
||||
+1
-1
Submodule drongo updated: 20ec0f95fb...c1f6a1245e
@@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.5.4</string>
|
||||
<string>1.5.5</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@@ -28,6 +28,7 @@ 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.soroban.PayNymDialog;
|
||||
import com.sparrowwallet.sparrow.soroban.Soroban;
|
||||
import com.sparrowwallet.sparrow.soroban.SorobanServices;
|
||||
import com.sparrowwallet.sparrow.transaction.TransactionController;
|
||||
@@ -163,6 +164,9 @@ public class AppController implements Initializable {
|
||||
@FXML
|
||||
private MenuItem findMixingPartner;
|
||||
|
||||
@FXML
|
||||
private MenuItem showPayNym;
|
||||
|
||||
@FXML
|
||||
private CheckMenuItem preventSleep;
|
||||
private static final BooleanProperty preventSleepProperty = new SimpleBooleanProperty();
|
||||
@@ -329,9 +333,10 @@ public class AppController implements Initializable {
|
||||
lockWallet.setDisable(true);
|
||||
refreshWallet.disableProperty().bind(Bindings.or(exportWallet.disableProperty(), Bindings.or(serverToggle.disableProperty(), AppServices.onlineProperty().not())));
|
||||
sendToMany.disableProperty().bind(exportWallet.disableProperty());
|
||||
showPayNym.disableProperty().bind(findMixingPartner.disableProperty());
|
||||
findMixingPartner.setDisable(true);
|
||||
AppServices.onlineProperty().addListener((observable, oldValue, newValue) -> {
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
findMixingPartner.setDisable(exportWallet.isDisable() || getSelectedWalletForm() == null || !SorobanServices.canWalletMix(getSelectedWalletForm().getWallet()) || !newValue);
|
||||
});
|
||||
|
||||
setServerType(Config.get().getServerType());
|
||||
@@ -1289,6 +1294,14 @@ public class AppController implements Initializable {
|
||||
}
|
||||
}
|
||||
|
||||
public void showPayNym(ActionEvent event) {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
PayNymDialog payNymDialog = new PayNymDialog(selectedWalletForm.getWalletId(), false);
|
||||
payNymDialog.showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
public void minimizeToTray(ActionEvent event) {
|
||||
AppServices.get().minimizeStage((Stage)tabs.getScene().getWindow());
|
||||
}
|
||||
@@ -1459,7 +1472,7 @@ public class AppController implements Initializable {
|
||||
TabData tabData = new WalletTabData(TabData.TabType.WALLET, walletForm);
|
||||
subTab.setUserData(tabData);
|
||||
if(!wallet.isWhirlpoolChildWallet()) {
|
||||
subTab.setContextMenu(getSubTabContextMenu(subTab));
|
||||
subTab.setContextMenu(getSubTabContextMenu(wallet, subTabs, subTab));
|
||||
}
|
||||
|
||||
subTabs.getTabs().add(subTab);
|
||||
@@ -1495,12 +1508,14 @@ public class AppController implements Initializable {
|
||||
|
||||
public WalletForm getSelectedWalletForm() {
|
||||
Tab selectedTab = tabs.getSelectionModel().getSelectedItem();
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)selectedTab.getContent();
|
||||
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
|
||||
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
|
||||
return subWalletTabData.getWalletForm();
|
||||
if(selectedTab != null) {
|
||||
TabData tabData = (TabData)selectedTab.getUserData();
|
||||
if(tabData instanceof WalletTabData) {
|
||||
TabPane subTabs = (TabPane)selectedTab.getContent();
|
||||
Tab selectedSubTab = subTabs.getSelectionModel().getSelectedItem();
|
||||
WalletTabData subWalletTabData = (WalletTabData)selectedSubTab.getUserData();
|
||||
return subWalletTabData.getWalletForm();
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -1687,7 +1702,7 @@ public class AppController implements Initializable {
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
private ContextMenu getSubTabContextMenu(Tab subTab) {
|
||||
private ContextMenu getSubTabContextMenu(Wallet wallet, TabPane subTabs, Tab subTab) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem rename = new MenuItem("Rename Account");
|
||||
rename.setOnAction(event -> {
|
||||
@@ -1698,14 +1713,27 @@ public class AppController implements Initializable {
|
||||
String label = optLabel.get();
|
||||
subTabLabel.setText(label);
|
||||
|
||||
WalletTabData walletTabData = (WalletTabData)subTab.getUserData();
|
||||
Wallet wallet = walletTabData.getWallet();
|
||||
wallet.setLabel(label);
|
||||
EventManager.get().post(new WalletLabelChangedEvent(wallet));
|
||||
}
|
||||
});
|
||||
|
||||
contextMenu.getItems().add(rename);
|
||||
|
||||
if(!wallet.isMasterWallet() && !wallet.isWhirlpoolChildWallet()) {
|
||||
MenuItem delete = new MenuItem("Delete Account");
|
||||
delete.setOnAction(event -> {
|
||||
Optional<ButtonType> optButtonType = AppServices.showWarningDialog("Delete Wallet Account?", "Labels applied in this wallet account will be lost. Are you sure?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
|
||||
subTabs.getTabs().remove(subTab);
|
||||
if(subTabs.getTabs().size() == 1) {
|
||||
setSubTabsVisible(subTabs, false);
|
||||
}
|
||||
EventManager.get().post(new WalletDeletedEvent(wallet));
|
||||
}
|
||||
});
|
||||
contextMenu.getItems().add(delete);
|
||||
}
|
||||
|
||||
return contextMenu;
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,10 @@ import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURI;
|
||||
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletTransaction;
|
||||
import com.sparrowwallet.sparrow.control.TextUtils;
|
||||
import com.sparrowwallet.sparrow.control.TrayManager;
|
||||
import com.sparrowwallet.sparrow.event.*;
|
||||
@@ -543,6 +545,18 @@ public class AppServices {
|
||||
return getOpenWallets().entrySet().stream().filter(entry -> entry.getValue().getWalletId(entry.getKey()).equals(walletId)).map(Map.Entry::getKey).findFirst().orElse(null);
|
||||
}
|
||||
|
||||
public WalletTransaction getCreatedTransaction(Set<BlockTransactionHashIndex> utxos) {
|
||||
for(List<WalletTabData> walletTabDataList : walletWindows.values()) {
|
||||
for(WalletTabData walletTabData : walletTabDataList) {
|
||||
if(walletTabData.getWalletForm().getCreatedWalletTransaction() != null && utxos.equals(walletTabData.getWalletForm().getCreatedWalletTransaction().getSelectedUtxos().keySet())) {
|
||||
return walletTabData.getWalletForm().getCreatedWalletTransaction();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public Window getWindowForWallet(String walletId) {
|
||||
Optional<Window> optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().stream().anyMatch(walletTabData -> walletTabData.getWalletForm().getWalletId().equals(walletId))).map(Map.Entry::getKey).findFirst();
|
||||
return optWindow.orElse(null);
|
||||
|
||||
@@ -30,7 +30,7 @@ import java.util.stream.Collectors;
|
||||
public class MainApp extends Application {
|
||||
public static final String APP_ID = "com.sparrowwallet.sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "1.5.4";
|
||||
public static final String APP_VERSION = "1.5.5";
|
||||
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";
|
||||
|
||||
@@ -42,12 +42,14 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
protected ButtonBase importButton;
|
||||
private final SimpleStringProperty password = new SimpleStringProperty("");
|
||||
private final boolean scannable;
|
||||
private final boolean fileFormatAvailable;
|
||||
protected List<Wallet> wallets;
|
||||
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable) {
|
||||
public FileImportPane(FileImport importer, String title, String description, String content, String imageUrl, boolean scannable, boolean fileFormatAvailable) {
|
||||
super(title, description, content, imageUrl);
|
||||
this.importer = importer;
|
||||
this.scannable = scannable;
|
||||
this.fileFormatAvailable = fileFormatAvailable;
|
||||
|
||||
buttonBox.getChildren().clear();
|
||||
buttonBox.getChildren().add(createButton());
|
||||
@@ -55,7 +57,7 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
|
||||
@Override
|
||||
protected Control createButton() {
|
||||
if(scannable) {
|
||||
if(scannable && fileFormatAvailable) {
|
||||
ToggleButton scanButton = new ToggleButton("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
@@ -76,6 +78,16 @@ public abstract class FileImportPane extends TitledDescriptionPane {
|
||||
SegmentedButton segmentedButton = new SegmentedButton();
|
||||
segmentedButton.getButtons().addAll(scanButton, fileButton);
|
||||
return segmentedButton;
|
||||
} else if(scannable) {
|
||||
importButton = new Button("Scan...");
|
||||
Glyph cameraGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.CAMERA);
|
||||
cameraGlyph.setFontSize(12);
|
||||
importButton.setGraphic(cameraGlyph);
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
importButton.setOnAction(event -> {
|
||||
importQR();
|
||||
});
|
||||
return importButton;
|
||||
} else {
|
||||
importButton = new Button("Import File...");
|
||||
importButton.setAlignment(Pos.CENTER_RIGHT);
|
||||
|
||||
@@ -16,7 +16,7 @@ public class FileKeystoreImportPane extends FileImportPane {
|
||||
private final KeyDerivation requiredDerivation;
|
||||
|
||||
public FileKeystoreImportPane(Wallet wallet, KeystoreFileImport importer, KeyDerivation requiredDerivation) {
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
super(importer, importer.getName(), "Keystore import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.wallet = wallet;
|
||||
this.importer = importer;
|
||||
this.requiredDerivation = requiredDerivation;
|
||||
|
||||
@@ -12,7 +12,7 @@ public class FileWalletImportPane extends FileImportPane {
|
||||
private final WalletImport importer;
|
||||
|
||||
public FileWalletImportPane(WalletImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getWalletImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isWalletImportScannable(), true);
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@ public class FileWalletKeystoreImportPane extends FileImportPane {
|
||||
private byte[] fileBytes;
|
||||
|
||||
public FileWalletKeystoreImportPane(KeystoreFileImport importer) {
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable());
|
||||
super(importer, importer.getName(), "Wallet import", importer.getKeystoreImportDescription(), "image/" + importer.getWalletModel().getType() + ".png", importer.isKeystoreImportScannable(), importer.isFileFormatAvailable());
|
||||
this.importer = importer;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,7 +81,7 @@ public class MempoolSizeFeeRatesChart extends StackedAreaChart<String, Number> {
|
||||
long previousFeeRate = 0;
|
||||
for(Long feeRate : SendController.FEE_RATES_RANGE) {
|
||||
XYChart.Series<String, Number> series = new XYChart.Series<>();
|
||||
series.setName(feeRate + "+ vB");
|
||||
series.setName(feeRate + "+ sats/vB");
|
||||
long seriesTotalVSize = 0;
|
||||
|
||||
for(Date date : periodRateSizes.keySet()) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import com.sparrowwallet.drongo.address.P2PKHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2SHAddress;
|
||||
import com.sparrowwallet.drongo.address.P2WPKHAddress;
|
||||
import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.Base43;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
@@ -491,6 +493,7 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
keystore.setKeyDerivation(new KeyDerivation(masterFingerprint, KeyDerivation.writePath(outputDescriptor.getKeyDerivation(extendedKey).getDerivation())));
|
||||
keystore.setExtendedPublicKey(extendedKey);
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(outputDescriptor.isCosigner() ? PolicyType.MULTI : PolicyType.SINGLE, wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import javafx.scene.control.ContentDisplay;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.Tooltip;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.scene.shape.Circle;
|
||||
import javafx.scene.shape.CubicCurve;
|
||||
import javafx.scene.shape.Line;
|
||||
import javafx.util.Duration;
|
||||
@@ -44,6 +45,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private static final double DIAGRAM_HEIGHT = 210.0;
|
||||
private static final double REDUCED_DIAGRAM_HEIGHT = DIAGRAM_HEIGHT - 60;
|
||||
private static final int TOOLTIP_SHOW_DELAY = 50;
|
||||
private static final int RELATIVE_SIZE_MAX_RADIUS = 7;
|
||||
|
||||
private WalletTransaction walletTx;
|
||||
private final BooleanProperty finalProperty = new SimpleBooleanProperty(false);
|
||||
@@ -387,6 +389,7 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
double width = 140.0;
|
||||
List<BlockTransactionHashIndex> inputs = new ArrayList<>(displayedUtxos.keySet());
|
||||
long sum = walletTx.getTotal();
|
||||
int numUtxos = displayedUtxos.size();
|
||||
for(int i = 1; i <= numUtxos; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
@@ -398,7 +401,7 @@ public class TransactionDiagram extends GridPane {
|
||||
continue;
|
||||
}
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.setStartX(RELATIVE_SIZE_MAX_RADIUS);
|
||||
double scaleFactor = (double)i / (numUtxos + 1);
|
||||
int nodeHeight = 17;
|
||||
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
|
||||
@@ -406,12 +409,19 @@ public class TransactionDiagram extends GridPane {
|
||||
curve.setEndX(width);
|
||||
curve.setEndY(scale(getDiagramHeight(), 0.5, 0));
|
||||
|
||||
curve.setControlX1(scale(width, 0.2, 0));
|
||||
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
|
||||
curve.setControlY1(curve.getStartY());
|
||||
curve.setControlX2(scale(width, 0.8, 0));
|
||||
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
|
||||
curve.setControlY2(curve.getEndY());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
|
||||
if(sum > 0 && !curve.getStyleClass().contains("input-dashed-line")) {
|
||||
long radius = Math.round((double)inputs.get(numUtxos-i).getValue() * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1;
|
||||
Circle circle = new Circle(curve.getStartX(), curve.getStartY(), radius);
|
||||
circle.getStyleClass().add("size-indicator");
|
||||
group.getChildren().add(circle);
|
||||
}
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
@@ -458,6 +468,9 @@ public class TransactionDiagram extends GridPane {
|
||||
group.getChildren().add(yaxisLine);
|
||||
|
||||
double width = 140.0;
|
||||
long sum = walletTx.getTotal();
|
||||
List<Long> values = walletTx.getTransaction().getOutputs().stream().map(TransactionOutput::getValue).collect(Collectors.toList());
|
||||
values.add(walletTx.getFee());
|
||||
int numOutputs = displayedPayments.size() + walletTx.getChangeMap().size() + 1;
|
||||
for(int i = 1; i <= numOutputs; i++) {
|
||||
CubicCurve curve = new CubicCurve();
|
||||
@@ -465,18 +478,25 @@ public class TransactionDiagram extends GridPane {
|
||||
|
||||
curve.setStartX(0);
|
||||
curve.setStartY(scale(getDiagramHeight(), 0.5, 0));
|
||||
curve.setEndX(width);
|
||||
curve.setEndX(width - RELATIVE_SIZE_MAX_RADIUS);
|
||||
double scaleFactor = (double)i / (numOutputs + 1);
|
||||
int nodeHeight = 20;
|
||||
double additional = (0.5 - scaleFactor) * ((double)nodeHeight);
|
||||
curve.setEndY(scale(getDiagramHeight(), scaleFactor, additional));
|
||||
|
||||
curve.setControlX1(scale(width, 0.2, 0));
|
||||
curve.setControlX1(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.2, 0));
|
||||
curve.controlY1Property().bind(curve.startYProperty());
|
||||
curve.setControlX2(scale(width, 0.8, 0));
|
||||
curve.setControlX2(scale(width - RELATIVE_SIZE_MAX_RADIUS, 0.8, 0));
|
||||
curve.controlY2Property().bind(curve.endYProperty());
|
||||
|
||||
group.getChildren().add(curve);
|
||||
|
||||
if(sum > 0) {
|
||||
long radius = Math.min(RELATIVE_SIZE_MAX_RADIUS, Math.round((double)values.get(numOutputs-i) * (RELATIVE_SIZE_MAX_RADIUS - 1) / sum) + 1);
|
||||
Circle circle = new Circle(curve.getEndX(), curve.getEndY(), radius);
|
||||
circle.getStyleClass().add("size-indicator");
|
||||
group.getChildren().add(circle);
|
||||
}
|
||||
}
|
||||
|
||||
pane.getChildren().add(group);
|
||||
@@ -507,7 +527,7 @@ public class TransactionDiagram extends GridPane {
|
||||
recipientTooltip.setShowDelay(new Duration(TOOLTIP_SHOW_DELAY));
|
||||
recipientTooltip.setShowDuration(Duration.INDEFINITE);
|
||||
recipientLabel.setTooltip(recipientTooltip);
|
||||
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress()));
|
||||
outputNodes.add(new OutputNode(recipientLabel, payment.getAddress(), payment.getAmount()));
|
||||
}
|
||||
|
||||
for(Map.Entry<WalletNode, Long> changeEntry : walletTx.getChangeMap().entrySet()) {
|
||||
@@ -543,7 +563,7 @@ public class TransactionDiagram extends GridPane {
|
||||
actionBox.getChildren().add(replaceChangeLabel);
|
||||
}
|
||||
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress));
|
||||
outputNodes.add(new OutputNode(actionBox, changeAddress, changeEntry.getValue()));
|
||||
}
|
||||
|
||||
if(isFinal()) {
|
||||
@@ -634,7 +654,7 @@ public class TransactionDiagram extends GridPane {
|
||||
if(payment.getType() == Payment.Type.WHIRLPOOL_FEE) {
|
||||
return "Whirlpool Fee";
|
||||
} else if(walletTx.isPremixSend(payment)) {
|
||||
int premixIndex = getOutputIndex(payment.getAddress()) - 2;
|
||||
int premixIndex = getOutputIndex(payment.getAddress(), payment.getAmount()) - 2;
|
||||
return "Premix #" + premixIndex;
|
||||
} else if(walletTx.isBadbankSend(payment)) {
|
||||
return "Badbank Change";
|
||||
@@ -643,8 +663,8 @@ public class TransactionDiagram extends GridPane {
|
||||
return null;
|
||||
}
|
||||
|
||||
private int getOutputIndex(Address address) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress())).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
private int getOutputIndex(Address address, long amount) {
|
||||
return walletTx.getTransaction().getOutputs().stream().filter(txOutput -> address.equals(txOutput.getScript().getToAddress()) && txOutput.getValue() == amount).mapToInt(TransactionOutput::getIndex).findFirst().orElseThrow();
|
||||
}
|
||||
|
||||
private Wallet getToWallet(Payment payment) {
|
||||
@@ -880,7 +900,7 @@ public class TransactionDiagram extends GridPane {
|
||||
private final List<BlockTransactionHashIndex> additionalInputs;
|
||||
|
||||
public AdditionalBlockTransactionHashIndex(List<BlockTransactionHashIndex> additionalInputs) {
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, 0);
|
||||
super(Sha256Hash.ZERO_HASH, 0, new Date(), 0L, 0, additionalInputs.stream().mapToLong(BlockTransactionHashIndex::getValue).sum());
|
||||
this.additionalInputs = additionalInputs;
|
||||
}
|
||||
|
||||
@@ -949,16 +969,18 @@ public class TransactionDiagram extends GridPane {
|
||||
private class OutputNode implements Comparable<OutputNode> {
|
||||
public Node outputLabel;
|
||||
public Address address;
|
||||
public long amount;
|
||||
|
||||
public OutputNode(Node outputLabel, Address address) {
|
||||
public OutputNode(Node outputLabel, Address address, long amount) {
|
||||
this.outputLabel = outputLabel;
|
||||
this.address = address;
|
||||
this.amount = amount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(TransactionDiagram.OutputNode o) {
|
||||
try {
|
||||
return getOutputIndex(address) - getOutputIndex(o.address);
|
||||
return getOutputIndex(address, amount) - getOutputIndex(o.address, o.amount);
|
||||
} catch(Exception e) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ public class WalletImportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
importAccordion = new Accordion();
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SpecterDIY());
|
||||
List<KeystoreFileImport> keystoreImporters = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedTool(), new SpecterDIY());
|
||||
for(KeystoreFileImport importer : keystoreImporters) {
|
||||
FileWalletKeystoreImportPane importPane = new FileWalletKeystoreImportPane(importer);
|
||||
importAccordion.getPanes().add(importPane);
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletDeletedEvent extends WalletChangedEvent {
|
||||
public WalletDeletedEvent(Wallet wallet) {
|
||||
super(wallet);
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@ public class CaravanMultisig implements WalletImport, WalletExport {
|
||||
Wallet wallet = new Wallet();
|
||||
wallet.setName(cf.name);
|
||||
wallet.setPolicyType(PolicyType.MULTI);
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType);
|
||||
ScriptType scriptType = ScriptType.valueOf(cf.addressType.replace('-', '_'));
|
||||
|
||||
for(ExtPublicKey extKey : cf.extendedPublicKeys) {
|
||||
Keystore keystore = new Keystore(extKey.name);
|
||||
|
||||
@@ -43,7 +43,7 @@ public class Hwi {
|
||||
try {
|
||||
List<String> command;
|
||||
if(passphrase != null && !passphrase.isEmpty()) {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", passphrase, Command.ENUMERATE.toString());
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), "--password", escape(passphrase), Command.ENUMERATE.toString());
|
||||
} else {
|
||||
command = List.of(getHwiExecutable(Command.ENUMERATE).getAbsolutePath(), Command.ENUMERATE.toString());
|
||||
}
|
||||
@@ -414,7 +414,7 @@ public class Hwi {
|
||||
}
|
||||
|
||||
private List<String> getDeviceCommand(Device device, String passphrase, Command command, String... commandData) throws IOException {
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", passphrase, command.toString()));
|
||||
List<String> elements = new ArrayList<>(List.of(getHwiExecutable(command).getAbsolutePath(), "--device-path", device.getPath(), "--device-type", device.getType(), "--password", escape(passphrase), command.toString()));
|
||||
addChainType(elements);
|
||||
elements.addAll(Arrays.stream(commandData).filter(Objects::nonNull).collect(Collectors.toList()));
|
||||
return elements;
|
||||
@@ -437,6 +437,15 @@ public class Hwi {
|
||||
return network.toString();
|
||||
}
|
||||
|
||||
private String escape(String passphrase) {
|
||||
Platform platform = Platform.getCurrent();
|
||||
if(platform == Platform.WINDOWS) {
|
||||
return passphrase.replace("\"", "\\\"");
|
||||
}
|
||||
|
||||
return passphrase;
|
||||
}
|
||||
|
||||
public static class EnumerateService extends Service<List<Device>> {
|
||||
private final String passphrase;
|
||||
|
||||
|
||||
@@ -8,4 +8,7 @@ import java.io.InputStream;
|
||||
public interface KeystoreFileImport extends KeystoreImport, FileImport {
|
||||
Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException;
|
||||
boolean isKeystoreImportScannable();
|
||||
default boolean isFileFormatAvailable() {
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ public class SeedSigner extends SpecterDIY {
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Import QR created on your SeedSigner by selecting Generate XPUB in the Signing Tools menu. Note that SeedSigner currently only supports multisig wallets with a P2WSH script type.";
|
||||
return "Import QR created on your SeedSigner by selecting xPub from Seed in the Seed Tools menu once you have entered your seed.";
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.sparrowwallet.sparrow.io;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class SeedTool implements KeystoreFileImport {
|
||||
@Override
|
||||
public boolean isEncrypted(File file) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Seed Tool";
|
||||
}
|
||||
|
||||
@Override
|
||||
public WalletModel getWalletModel() {
|
||||
return WalletModel.SEED_TOOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Keystore getKeystore(ScriptType scriptType, InputStream inputStream, String password) throws ImportException {
|
||||
throw new ImportException("Only QR imports are supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isKeystoreImportScannable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getKeystoreImportDescription() {
|
||||
return "Select your seed and scan the QR code created by Authenticate > Derive Key > Other Key Derivations > Account Descriptor. Click the share icon at the bottom to show the QR.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFileFormatAvailable() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -196,6 +196,11 @@ public class DbPersistence implements Persistence {
|
||||
jdbi.useHandle(handle -> {
|
||||
WalletDao walletDao = handle.attach(WalletDao.class);
|
||||
try {
|
||||
if(dirtyPersistables.deleteAccount && !wallet.isMasterWallet()) {
|
||||
handle.execute("drop schema `" + getSchema(wallet) + "` cascade");
|
||||
return;
|
||||
}
|
||||
|
||||
walletDao.setSchema(getSchema(wallet));
|
||||
|
||||
if(dirtyPersistables.clearHistory) {
|
||||
@@ -631,6 +636,13 @@ public class DbPersistence implements Persistence {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletDeleted(WalletDeletedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).deleteAccount = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryCleared(WalletHistoryClearedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
@@ -710,6 +722,7 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
|
||||
private static class DirtyPersistables {
|
||||
public boolean deleteAccount;
|
||||
public boolean clearHistory;
|
||||
public final List<WalletNode> historyNodes = new ArrayList<>();
|
||||
public String label;
|
||||
@@ -725,6 +738,7 @@ public class DbPersistence implements Persistence {
|
||||
|
||||
public String toString() {
|
||||
return "Dirty Persistables" +
|
||||
"\nDelete account:" + deleteAccount +
|
||||
"\nClear history:" + clearHistory +
|
||||
"\nNodes:" + historyNodes +
|
||||
"\nLabel:" + label +
|
||||
|
||||
@@ -18,9 +18,9 @@ public class HwAirgappedController extends KeystoreImportDetailController {
|
||||
public void initializeView() {
|
||||
List<KeystoreFileImport> importers = Collections.emptyList();
|
||||
if(getMasterController().getWallet().getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SpecterDIY());
|
||||
importers = List.of(new ColdcardSinglesig(), new CoboVaultSinglesig(), new KeystoneSinglesig(), new PassportSinglesig(), new SeedSigner(), new SeedTool(), new SpecterDIY());
|
||||
} else if(getMasterController().getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SpecterDIY());
|
||||
importers = List.of(new ColdcardMultisig(), new CoboVaultMultisig(), new KeystoneMultisig(), new PassportMultisig(), new SeedSigner(), new SeedTool(), new SpecterDIY());
|
||||
}
|
||||
|
||||
for(KeystoreFileImport importer : importers) {
|
||||
|
||||
@@ -333,7 +333,7 @@ public class CounterpartyController extends SorobanController {
|
||||
}
|
||||
|
||||
private void followPaymentCode(Soroban soroban, PaymentCode paymentCodeInitiator) {
|
||||
if(Config.get().isUsePayNym()) {
|
||||
if(Config.get().isUsePayNym() && soroban.getHdWallet() != null) {
|
||||
soroban.getAuthToken(new HashMap<>()).subscribe(authToken -> {
|
||||
String signature = soroban.getSignature(authToken);
|
||||
soroban.followPaymentCode(paymentCodeInitiator, authToken, signature).subscribe(followMap -> {
|
||||
@@ -341,6 +341,8 @@ public class CounterpartyController extends SorobanController {
|
||||
}, error -> {
|
||||
log.warn("Could not follow payment code", error);
|
||||
});
|
||||
}, error -> {
|
||||
log.warn("Could not follow payment code", error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ public class InitiatorDialog extends Dialog<Transaction> {
|
||||
broadcastButton.visibleProperty().bind(nextButton.visibleProperty().not());
|
||||
|
||||
initiatorController.counterpartyPaymentCodeProperty().addListener((observable, oldValue, paymentCode) -> {
|
||||
nextButton.setDisable(paymentCode == null);
|
||||
nextButton.setDisable(paymentCode == null || !AppServices.isConnected());
|
||||
});
|
||||
|
||||
initiatorController.stepProperty().addListener((observable, oldValue, step) -> {
|
||||
|
||||
@@ -109,6 +109,7 @@ public class Soroban {
|
||||
Soroban soroban = AppServices.getSorobanServices().getSoroban(associatedWallet);
|
||||
if(soroban != null && soroban.getHdWallet() != null) {
|
||||
hdWallet = soroban.hdWallet;
|
||||
bip47Wallet = soroban.bip47Wallet;
|
||||
paymentCode = soroban.paymentCode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
|
||||
private ElectrumServer.TransactionMempoolService transactionMempoolService;
|
||||
|
||||
private final Map<Integer, String> outputIndexLabels = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public void initialize(URL location, ResourceBundle resources) {
|
||||
EventManager.get().register(this);
|
||||
@@ -401,6 +403,11 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
feeAmt = 0L;
|
||||
} else if(headersForm.getInputTransactions() != null) {
|
||||
feeAmt = calculateFee(headersForm.getInputTransactions());
|
||||
} else {
|
||||
Wallet wallet = getWalletFromTransactionInputs();
|
||||
if(wallet != null) {
|
||||
feeAmt = calculateFee(wallet.getTransactions());
|
||||
}
|
||||
}
|
||||
|
||||
if(feeAmt != null) {
|
||||
@@ -521,7 +528,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
}
|
||||
|
||||
BlockTransaction inputTx = inputTransactions.get(input.getOutpoint().getHash());
|
||||
if(inputTx == null) {
|
||||
if(inputTx == null && headersForm.getInputTransactions() != null) {
|
||||
inputTx = headersForm.getInputTransactions().get(input.getOutpoint().getHash());
|
||||
}
|
||||
|
||||
@@ -555,11 +562,18 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
Wallet wallet = getWalletFromTransactionInputs();
|
||||
|
||||
if(wallet != null) {
|
||||
Map<Sha256Hash, BlockTransaction> walletInputTransactions = inputTransactions;
|
||||
if(walletInputTransactions == null) {
|
||||
Set<Sha256Hash> refs = headersForm.getTransaction().getInputs().stream().map(txInput -> txInput.getOutpoint().getHash()).collect(Collectors.toSet());
|
||||
walletInputTransactions = new HashMap<>(wallet.getTransactions());
|
||||
walletInputTransactions.keySet().retainAll(refs);
|
||||
}
|
||||
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = new LinkedHashMap<>();
|
||||
Map<BlockTransactionHashIndex, WalletNode> walletTxos = wallet.getWalletTxos();
|
||||
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
|
||||
BlockTransactionHashIndex selectedTxo = walletTxos.keySet().stream().filter(txo -> txInput.getOutpoint().getHash().equals(txo.getHash()) && txInput.getOutpoint().getIndex() == txo.getIndex())
|
||||
.findFirst().orElse(new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0));
|
||||
.findFirst().orElse(getBlockTransactionInput(walletInputTransactions, txInput));
|
||||
selectedTxos.put(selectedTxo, walletTxos.get(selectedTxo));
|
||||
}
|
||||
|
||||
@@ -589,26 +603,26 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
BlockTransactionHashIndex receivedTxo = walletTxos.keySet().stream().filter(txo -> txo.getHash().equals(txOutput.getHash()) && txo.getIndex() == txOutput.getIndex()).findFirst().orElse(null);
|
||||
String label = headersForm.getName() == null || (headersForm.getName().startsWith("[") && headersForm.getName().endsWith("]") && headersForm.getName().length() == 8) ? null : headersForm.getName();
|
||||
try {
|
||||
payments.add(new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType));
|
||||
Payment payment = new Payment(txOutput.getScript().getToAddresses()[0], receivedTxo != null ? receivedTxo.getLabel() : label, txOutput.getValue(), false, paymentType);
|
||||
WalletTransaction createdTx = AppServices.get().getCreatedTransaction(selectedTxos.keySet());
|
||||
if(createdTx != null) {
|
||||
Optional<String> optLabel = createdTx.getPayments().stream().filter(pymt -> pymt.getAddress().equals(payment.getAddress()) && pymt.getAmount() == payment.getAmount()).map(Payment::getLabel).findFirst();
|
||||
if(optLabel.isPresent()) {
|
||||
payment.setLabel(optLabel.get());
|
||||
outputIndexLabels.put(txOutput.getIndex(), optLabel.get());
|
||||
}
|
||||
}
|
||||
payments.add(payment);
|
||||
} catch(Exception e) {
|
||||
//ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), inputTransactions);
|
||||
return new WalletTransaction(wallet, headersForm.getTransaction(), Collections.emptyList(), List.of(selectedTxos), payments, changeMap, fee.getValue(), walletInputTransactions);
|
||||
} else {
|
||||
Map<BlockTransactionHashIndex, WalletNode> selectedTxos = headersForm.getTransaction().getInputs().stream()
|
||||
.collect(Collectors.toMap(txInput -> {
|
||||
if(inputTransactions != null) {
|
||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
if(blockTransaction != null) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int)txInput.getOutpoint().getIndex());
|
||||
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
|
||||
}
|
||||
}
|
||||
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
|
||||
},
|
||||
.collect(Collectors.toMap(txInput -> getBlockTransactionInput(inputTransactions, txInput),
|
||||
txInput -> new WalletNode("m/0"),
|
||||
(u, v) -> { throw new IllegalStateException("Duplicate TXOs"); },
|
||||
LinkedHashMap::new));
|
||||
@@ -627,6 +641,18 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
}
|
||||
}
|
||||
|
||||
private BlockTransactionHashIndex getBlockTransactionInput(Map<Sha256Hash, BlockTransaction> inputTransactions, TransactionInput txInput) {
|
||||
if(inputTransactions != null) {
|
||||
BlockTransaction blockTransaction = inputTransactions.get(txInput.getOutpoint().getHash());
|
||||
if(blockTransaction != null) {
|
||||
TransactionOutput txOutput = blockTransaction.getTransaction().getOutputs().get((int) txInput.getOutpoint().getIndex());
|
||||
return new BlockTransactionHashIndex(blockTransaction.getHash(), blockTransaction.getHeight(), blockTransaction.getDate(), blockTransaction.getFee(), txInput.getOutpoint().getIndex(), txOutput.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockTransactionHashIndex(txInput.getOutpoint().getHash(), 0, null, null, txInput.getOutpoint().getIndex(), 0);
|
||||
}
|
||||
|
||||
private Wallet getWalletFromTransactionInputs() {
|
||||
for(TransactionInput txInput : headersForm.getTransaction().getInputs()) {
|
||||
for(Wallet openWallet : AppServices.get().getOpenWallets().keySet()) {
|
||||
@@ -1136,7 +1162,12 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
if(feeAmt != null) {
|
||||
updateFee(feeAmt);
|
||||
}
|
||||
transactionDiagram.update(getWalletTransaction(event.getInputTransactions()));
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> allFetchedInputTransactions = new HashMap<>(event.getInputTransactions());
|
||||
if(headersForm.getInputTransactions() != null) {
|
||||
allFetchedInputTransactions.putAll(headersForm.getInputTransactions());
|
||||
}
|
||||
transactionDiagram.update(getWalletTransaction(allFetchedInputTransactions));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1318,7 +1349,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
for(WalletNode walletNode : event.getHistoryChangedNodes()) {
|
||||
for(BlockTransactionHashIndex output : walletNode.getTransactionOutputs()) {
|
||||
if(output.getHash().equals(txid) && output.getLabel() == null) { //If we send to ourselves, usually change
|
||||
output.setLabel(headersForm.getName() + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
String label = outputIndexLabels.containsKey((int)output.getIndex()) ? outputIndexLabels.get((int)output.getIndex()) : headersForm.getName();
|
||||
output.setLabel(label + (walletNode.getKeyPurpose() == KeyPurpose.CHANGE ? " (change)" : " (received)"));
|
||||
changedLabelEntries.add(new HashIndexEntry(event.getWallet(), output, HashIndexEntry.Type.OUTPUT, walletNode.getKeyPurpose()));
|
||||
}
|
||||
if(output.getSpentBy() != null && output.getSpentBy().getHash().equals(txid) && output.getSpentBy().getLabel() == null) { //The norm - sending out
|
||||
|
||||
@@ -394,6 +394,24 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.extendedKey != null && result.extendedKey.getKey().isPubKeyOnly()) {
|
||||
xpub.setText(result.extendedKey.getExtendedKey());
|
||||
} else if(result.outputDescriptor != null && !result.outputDescriptor.getExtendedPublicKeys().isEmpty()) {
|
||||
ExtendedKey extendedKey = result.outputDescriptor.getExtendedPublicKeys().iterator().next();
|
||||
KeyDerivation keyDerivation = result.outputDescriptor.getKeyDerivation(extendedKey);
|
||||
fingerprint.setText(keyDerivation.getMasterFingerprint());
|
||||
derivation.setText(keyDerivation.getDerivationPath());
|
||||
xpub.setText(extendedKey.toString());
|
||||
} else if(result.wallets != null) {
|
||||
for(Wallet wallet : result.wallets) {
|
||||
if(getWalletForm().getWallet().getScriptType().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
Keystore keystore = wallet.getKeystores().get(0);
|
||||
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
||||
xpub.setText(keystore.getExtendedPublicKey().toString());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.showErrorDialog("Missing Script Type", "QR Code did not contain any information for the " + getWalletForm().getWallet().getScriptType().getDescription() + " script type.");
|
||||
} else if(result.exception != null) {
|
||||
log.error("Error scanning QR", result.exception);
|
||||
AppServices.showErrorDialog("Error scanning QR", result.exception.getMessage());
|
||||
|
||||
@@ -180,6 +180,7 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
if(newValue != null) {
|
||||
sendController.setPayNymPayment();
|
||||
}
|
||||
revalidateAmount();
|
||||
});
|
||||
|
||||
address.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
@@ -320,6 +321,10 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
}
|
||||
|
||||
private long getRecipientDustThreshold() {
|
||||
if(payNymProperty.get() != null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Address address;
|
||||
try {
|
||||
address = getRecipientAddress();
|
||||
|
||||
@@ -158,8 +158,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
private final ObjectProperty<WalletTransaction> walletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final ObjectProperty<WalletTransaction> createdWalletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private final BooleanProperty insufficientInputsProperty = new SimpleBooleanProperty(false);
|
||||
|
||||
private final StringProperty utxoLabelSelectionProperty = new SimpleStringProperty("");
|
||||
@@ -1044,7 +1042,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
opReturnsList.clear();
|
||||
excludedChangeNodes.clear();
|
||||
walletTransactionProperty.setValue(null);
|
||||
createdWalletTransactionProperty.set(null);
|
||||
walletForm.setCreatedWalletTransaction(null);
|
||||
insufficientInputsProperty.set(false);
|
||||
|
||||
validationSupport.setErrorDecorationEnabled(false);
|
||||
@@ -1124,7 +1122,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
addWalletTransactionNodes();
|
||||
createdWalletTransactionProperty.set(walletTransaction);
|
||||
walletForm.setCreatedWalletTransaction(walletTransaction);
|
||||
PSBT psbt = walletTransaction.createPSBT();
|
||||
EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransaction.getPayments().get(0).getLabel(), null, psbt));
|
||||
}
|
||||
@@ -1204,8 +1202,8 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
|
||||
@Subscribe
|
||||
public void walletHistoryChanged(WalletHistoryChangedEvent event) {
|
||||
if(event.getWallet().equals(walletForm.getWallet()) && createdWalletTransactionProperty.get() != null) {
|
||||
if(createdWalletTransactionProperty.get().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) {
|
||||
if(event.getWallet().equals(walletForm.getWallet()) && walletForm.getCreatedWalletTransaction() != null) {
|
||||
if(walletForm.getCreatedWalletTransaction().getSelectedUtxos() != null && allSelectedUtxosSpent(event.getHistoryChangedNodes())) {
|
||||
clear(null);
|
||||
} else {
|
||||
updateTransaction();
|
||||
@@ -1214,9 +1212,9 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
}
|
||||
|
||||
private boolean allSelectedUtxosSpent(List<WalletNode> historyChangedNodes) {
|
||||
Set<BlockTransactionHashIndex> unspentUtxos = new HashSet<>(createdWalletTransactionProperty.get().getSelectedUtxos().keySet());
|
||||
Set<BlockTransactionHashIndex> unspentUtxos = new HashSet<>(walletForm.getCreatedWalletTransaction().getSelectedUtxos().keySet());
|
||||
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxoEntry : createdWalletTransactionProperty.get().getSelectedUtxos().entrySet()) {
|
||||
for(Map.Entry<BlockTransactionHashIndex, WalletNode> selectedUtxoEntry : walletForm.getCreatedWalletTransaction().getSelectedUtxos().entrySet()) {
|
||||
BlockTransactionHashIndex utxo = selectedUtxoEntry.getKey();
|
||||
WalletNode utxoWalletNode = selectedUtxoEntry.getValue();
|
||||
|
||||
@@ -1288,7 +1286,7 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
whirlpoolProperty.set(event.getPool());
|
||||
updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax));
|
||||
|
||||
boolean isWhirlpoolPremix = (event.getPayments() != null && event.getPayments().stream().anyMatch(payment -> payment.getType().equals(Payment.Type.WHIRLPOOL_FEE)));
|
||||
boolean isWhirlpoolPremix = (event.getPool() != null);
|
||||
setInputFieldsDisabled(isWhirlpoolPremix);
|
||||
premixButton.setVisible(isWhirlpoolPremix);
|
||||
premixButton.setDefaultButton(isWhirlpoolPremix);
|
||||
@@ -1400,6 +1398,14 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
@Subscribe
|
||||
public void sorobanInitiated(SorobanInitiatedEvent event) {
|
||||
if(event.getWallet().equals(getWalletForm().getWallet())) {
|
||||
if(!AppServices.onlineProperty().get()) {
|
||||
Optional<ButtonType> optButtonType = AppServices.showErrorDialog("Cannot Mix Offline", "Sparrow needs to be connected to a server to perform collaborative mixes. Try to connect?", ButtonType.CANCEL, ButtonType.OK);
|
||||
if(optButtonType.isPresent() && optButtonType.get() == ButtonType.OK) {
|
||||
AppServices.onlineProperty().set(true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
InitiatorDialog initiatorDialog = new InitiatorDialog(getWalletForm().getWalletId(), getWalletForm().getWallet(), walletTransactionProperty.get());
|
||||
if(Config.get().isSameAppMixing()) {
|
||||
initiatorDialog.initModality(Modality.NONE);
|
||||
|
||||
@@ -304,7 +304,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
} else if(result.wallets != null) {
|
||||
for(Wallet wallet : result.wallets) {
|
||||
if(scriptType.getValue().equals(wallet.getScriptType()) && !wallet.getKeystores().isEmpty()) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet());
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||
setDescriptorText(outputDescriptor.toString());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -328,13 +328,15 @@ public class UtxosController extends WalletFormController implements Initializab
|
||||
Wallet badbankWallet = masterWallet.getChildWallet(StandardAccount.WHIRLPOOL_BADBANK);
|
||||
|
||||
List<Payment> payments = new ArrayList<>();
|
||||
try {
|
||||
Address whirlpoolFeeAddress = Address.fromString(tx0Preview.getTx0Data().getFeeAddress());
|
||||
Payment whirlpoolFeePayment = new Payment(whirlpoolFeeAddress, "Whirlpool Fee", tx0Preview.getFeeValue(), false);
|
||||
whirlpoolFeePayment.setType(Payment.Type.WHIRLPOOL_FEE);
|
||||
payments.add(whirlpoolFeePayment);
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalStateException("Cannot parse whirlpool fee address " + tx0Preview.getTx0Data().getFeeAddress(), e);
|
||||
if(tx0Preview.getTx0Data().getFeeAddress() != null) {
|
||||
try {
|
||||
Address whirlpoolFeeAddress = Address.fromString(tx0Preview.getTx0Data().getFeeAddress());
|
||||
Payment whirlpoolFeePayment = new Payment(whirlpoolFeeAddress, "Whirlpool Fee", tx0Preview.getFeeValue(), false);
|
||||
whirlpoolFeePayment.setType(Payment.Type.WHIRLPOOL_FEE);
|
||||
payments.add(whirlpoolFeePayment);
|
||||
} catch(InvalidAddressException e) {
|
||||
throw new IllegalStateException("Cannot parse whirlpool fee address " + tx0Preview.getTx0Data().getFeeAddress(), e);
|
||||
}
|
||||
}
|
||||
|
||||
WalletNode badbankNode = badbankWallet.getFreshNode(KeyPurpose.RECEIVE);
|
||||
|
||||
@@ -16,7 +16,9 @@ import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import javafx.application.Platform;
|
||||
import javafx.beans.property.BooleanProperty;
|
||||
import javafx.beans.property.ObjectProperty;
|
||||
import javafx.beans.property.SimpleBooleanProperty;
|
||||
import javafx.beans.property.SimpleObjectProperty;
|
||||
import javafx.util.Duration;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@@ -39,6 +41,7 @@ public class WalletForm {
|
||||
private WalletUtxosEntry walletUtxosEntry;
|
||||
private final List<NodeEntry> accountEntries = new ArrayList<>();
|
||||
private final List<Set<WalletNode>> walletTransactionNodes = new ArrayList<>();
|
||||
private final ObjectProperty<WalletTransaction> createdWalletTransactionProperty = new SimpleObjectProperty<>(null);
|
||||
|
||||
private ElectrumServer.TransactionMempoolService transactionMempoolService;
|
||||
|
||||
@@ -286,6 +289,14 @@ public class WalletForm {
|
||||
return allNodes.isEmpty() ? walletNodes : allNodes;
|
||||
}
|
||||
|
||||
public WalletTransaction getCreatedWalletTransaction() {
|
||||
return createdWalletTransactionProperty.get();
|
||||
}
|
||||
|
||||
public void setCreatedWalletTransaction(WalletTransaction createdWalletTransaction) {
|
||||
this.createdWalletTransactionProperty.set(createdWalletTransaction);
|
||||
}
|
||||
|
||||
public NodeEntry getNodeEntry(KeyPurpose keyPurpose) {
|
||||
NodeEntry purposeEntry;
|
||||
Optional<NodeEntry> optionalPurposeEntry = accountEntries.stream().filter(entry -> entry.getNode().getKeyPurpose().equals(keyPurpose)).findFirst();
|
||||
@@ -508,6 +519,14 @@ public class WalletForm {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletDeleted(WalletDeletedEvent event) {
|
||||
if(event.getWallet() == wallet && !wallet.isMasterWallet()) {
|
||||
wallet.getMasterWallet().getChildWallets().remove(wallet);
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletUtxoStatusChanged(WalletUtxoStatusChangedEvent event) {
|
||||
if(event.getWallet() == wallet) {
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
</ImageView>
|
||||
</HBox>
|
||||
<VBox spacing="10" styleClass="content-area">
|
||||
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact will a full understanding of your money." wrapText="true" />
|
||||
<Label text="Sparrow is a Bitcoin wallet with the goal of providing greater transparency and usability on the path to full financial self-sovereignty. It attempts to provide all of the detail about your wallet setup, transactions and UTXOs so that you can transact with a full understanding of your money." wrapText="true" />
|
||||
<Label text="Sparrow can operate in both an online and offline mode. In the online mode it connects to a Bitcoin Core node or Electrum server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator." wrapText="true" />
|
||||
<Label text="While it's possible to connect to a public Electrum server, connecting Sparrow to your Bitcoin Core node ensures your privacy, while connecting Sparrow to your private Electrum server ensures wallets load quicker, you have access to a full blockchain explorer, and your public keys are always encrypted on disk. " wrapText="true" />
|
||||
<Label text="While it's possible to connect to a public Electrum server, connecting Sparrow to your Bitcoin Core node ensures your privacy, while connecting Sparrow to your private Electrum server ensures wallets load quicker, you have access to a full blockchain explorer, and your public keys are always encrypted on disk." wrapText="true" />
|
||||
<HBox><Label text="If you find Sparrow useful, consider donating at "/><Hyperlink text="https://sparrowwallet.com/donate" onAction="#openDonate"/></HBox>
|
||||
</VBox>
|
||||
<HBox styleClass="button-area" alignment="BOTTOM_RIGHT" VBox.vgrow="SOMETIMES">
|
||||
|
||||
@@ -110,9 +110,11 @@
|
||||
<Menu fx:id="toolsMenu" mnemonicParsing="false" text="Tools">
|
||||
<MenuItem mnemonicParsing="false" text="Sign/Verify Message" accelerator="Shortcut+M" onAction="#signVerifyMessage"/>
|
||||
<MenuItem fx:id="sendToMany" mnemonicParsing="false" text="Send To Many" onAction="#sendToMany"/>
|
||||
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
|
||||
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
|
||||
<SeparatorMenuItem />
|
||||
<MenuItem fx:id="findMixingPartner" mnemonicParsing="false" text="Find Mix Partner" onAction="#findMixingPartner"/>
|
||||
<MenuItem fx:id="showPayNym" mnemonicParsing="false" text="Show PayNym" onAction="#showPayNym"/>
|
||||
<SeparatorMenuItem />
|
||||
<MenuItem styleClass="osxHide,windowsHide" mnemonicParsing="false" text="Install Udev Rules" onAction="#installUdevRules"/>
|
||||
<CheckMenuItem fx:id="preventSleep" mnemonicParsing="false" text="Prevent Computer Sleep" onAction="#preventSleep"/>
|
||||
</Menu>
|
||||
<Menu fx:id="helpMenu" mnemonicParsing="false" text="Help">
|
||||
|
||||
@@ -210,6 +210,10 @@
|
||||
-fx-text-fill: lightgray;
|
||||
}
|
||||
|
||||
.root #transactionDiagram .size-indicator {
|
||||
-fx-fill: lightgray;
|
||||
}
|
||||
|
||||
.root .progress-indicator.progress-timer.warn > .determinate-indicator > .indicator {
|
||||
-fx-background-color: -fx-box-border, radial-gradient(center 50% 50%, radius 50%, #e06c75 70%, derive(-fx-control-inner-background, -9%) 100%);
|
||||
}
|
||||
@@ -85,6 +85,10 @@
|
||||
-fx-stroke-width: 1px;
|
||||
}
|
||||
|
||||
#transactionDiagram .size-indicator {
|
||||
-fx-fill: -fx-text-base-color;
|
||||
}
|
||||
|
||||
#transactionDiagram .input-dashed-line {
|
||||
-fx-stroke-dash-array: 5px 5px;
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.9 KiB |
Reference in New Issue
Block a user