tapsigner and satscard initialization fixes, satscard address and private key retrieval, core address scanning support

This commit is contained in:
Craig Raw 2023-02-01 09:39:49 +02:00
parent 176e440195
commit 4e3491ec64
27 changed files with 561 additions and 157 deletions

View file

@ -55,30 +55,19 @@ public class CardImportPane extends TitledDescriptionPane {
importButton.setGraphic(tapGlyph);
importButton.setAlignment(Pos.CENTER_RIGHT);
importButton.setOnAction(event -> {
importButton.setDisable(true);
importCard();
});
return importButton;
}
private void importCard() {
try {
if(!importer.isInitialized()) {
setDescription("Card not initialized");
setContent(getInitializationPanel());
showHideLink.setVisible(false);
setExpanded(true);
return;
}
} catch(CardException e) {
setError("Card Error", e.getMessage());
return;
}
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getPinEntry());
showHideLink.setVisible(false);
setExpanded(true);
importButton.setDisable(false);
return;
}
@ -86,6 +75,21 @@ public class CardImportPane extends TitledDescriptionPane {
messageProperty.addListener((observable, oldValue, newValue) -> {
Platform.runLater(() -> setDescription(newValue));
});
try {
if(!importer.isInitialized()) {
setDescription("Card not initialized");
setContent(getInitializationPanel(messageProperty));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
} catch(CardException e) {
setError("Card Error", e.getMessage());
importButton.setDisable(false);
return;
}
CardImportService cardImportService = new CardImportService(importer, pin.get(), derivation, messageProperty);
cardImportService.setOnSucceeded(event -> {
EventManager.get().post(new KeystoreImportEvent(cardImportService.getValue()));
@ -99,16 +103,17 @@ public class CardImportPane extends TitledDescriptionPane {
log.error("Error importing keystore from card", event.getSource().getException());
setError("Import Error", rootCause.getMessage());
}
importButton.setDisable(false);
});
cardImportService.start();
}
private Node getInitializationPanel() {
private Node getInitializationPanel(StringProperty messageProperty) {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for chain code");
entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup();
@ -124,18 +129,26 @@ public class CardImportPane extends TitledDescriptionPane {
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
CardInitializationService cardInitializationService = new CardInitializationService(importer, chainCode);
cardInitializationService.setOnSucceeded(event1 -> {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou will now need to enter the PIN code found on the back. You can change the PIN code once it has been imported.");
setDescription("Enter PIN code");
setContent(getPinEntry());
setExpanded(true);
CardInitializationService cardInitializationService = new CardInitializationService(importer, pin.get(), chainCode, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
setDescription("Leave card on reader");
setExpanded(false);
importButton.setDisable(false);
});
cardInitializationService.setOnFailed(event1 -> {
Throwable e = event1.getSource().getException();
log.error("Error initializing card", e);
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + e.getMessage());
cardInitializationService.setOnFailed(failEvent -> {
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getPinEntry());
importButton.setDisable(false);
} else {
log.error("Error initializing card", failEvent.getSource().getException());
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
initializeButton.setDisable(false);
}
});
cardInitializationService.start();
});
@ -172,11 +185,15 @@ public class CardImportPane extends TitledDescriptionPane {
public static class CardInitializationService extends Service<Void> {
private final KeystoreCardImport cardImport;
private final String pin;
private final byte[] chainCode;
private final StringProperty messageProperty;
public CardInitializationService(KeystoreCardImport cardImport, byte[] chainCode) {
public CardInitializationService(KeystoreCardImport cardImport, String pin, byte[] chainCode, StringProperty messageProperty) {
this.cardImport = cardImport;
this.pin = pin;
this.chainCode = chainCode;
this.messageProperty = messageProperty;
}
@Override
@ -184,7 +201,7 @@ public class CardImportPane extends TitledDescriptionPane {
return new Task<>() {
@Override
protected Void call() throws Exception {
cardImport.initialize(chainCode);
cardImport.initialize(pin, chainCode, messageProperty);
return null;
}
};

View file

@ -93,7 +93,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
Hyperlink hyperlink = new Hyperlink();
hyperlink.setTranslateY(30);
hyperlink.setOnAction(event -> {
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate());
WalletBirthDateDialog dlg = new WalletBirthDateDialog(wallet.getBirthDate(), false);
Optional<Date> optDate = dlg.showAndWait();
if(optDate.isPresent()) {
Storage storage = AppServices.get().getOpenWallets().get(wallet);

View file

@ -9,11 +9,11 @@ import com.sparrowwallet.sparrow.io.Device;
import java.util.stream.Collectors;
public class DeviceAddressDialog extends DeviceDialog<String> {
public class DeviceDisplayAddressDialog extends DeviceDialog<String> {
private final Wallet wallet;
private final OutputDescriptor outputDescriptor;
public DeviceAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
public DeviceDisplayAddressDialog(Wallet wallet, OutputDescriptor outputDescriptor) {
super(outputDescriptor.getExtendedPublicKeys().stream().map(extKey -> outputDescriptor.getKeyDerivation(extKey).getMasterFingerprint()).collect(Collectors.toList()));
this.wallet = wallet;
this.outputDescriptor = outputDescriptor;

View file

@ -0,0 +1,29 @@
package com.sparrowwallet.sparrow.control;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.DeviceAddressEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceGetAddressDialog extends DeviceDialog<Address> {
public DeviceGetAddressDialog(List<String> operationFingerprints) {
super(operationFingerprints);
EventManager.get().register(this);
setOnCloseRequest(event -> {
EventManager.get().unregister(this);
});
}
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(DevicePane.DeviceOperation.GET_ADDRESS, device, defaultDevice);
}
@Subscribe
public void deviceAddress(DeviceAddressEvent event) {
setResult(event.getAddress());
}
}

View file

@ -4,6 +4,7 @@ import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.policy.Policy;
@ -66,7 +67,8 @@ public class DevicePane extends TitledDescriptionPane {
private Button displayAddressButton;
private Button signMessageButton;
private Button discoverKeystoresButton;
private Button unsealButton;
private Button getPrivateKeyButton;
private Button getAddressButton;
private final SimpleStringProperty passphrase = new SimpleStringProperty("");
private final SimpleStringProperty pin = new SimpleStringProperty("");
@ -201,9 +203,9 @@ public class DevicePane extends TitledDescriptionPane {
buttonBox.getChildren().addAll(setPassphraseButton, discoverKeystoresButton);
}
public DevicePane(Device device, boolean defaultDevice) {
public DevicePane(DeviceOperation deviceOperation, Device device, boolean defaultDevice) {
super(device.getModel().toDisplayString(), "", "", "image/" + device.getType() + ".png");
this.deviceOperation = DeviceOperation.UNSEAL;
this.deviceOperation = deviceOperation;
this.wallet = null;
this.psbt = null;
this.outputDescriptor = null;
@ -216,7 +218,16 @@ public class DevicePane extends TitledDescriptionPane {
setDefaultStatus();
showHideLink.setVisible(false);
createUnsealButton();
Button button;
if(deviceOperation == DeviceOperation.GET_PRIVATE_KEY) {
createGetPrivateKeyButton();
button = getPrivateKeyButton;
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
createGetAddressButton();
button = getAddressButton;
} else {
throw new UnsupportedOperationException("Cannot construct device pane for operation " + deviceOperation);
}
initialise(device);
@ -224,7 +235,7 @@ public class DevicePane extends TitledDescriptionPane {
Platform.runLater(() -> setDescription(newValue));
});
buttonBox.getChildren().add(unsealButton);
buttonBox.getChildren().add(button);
}
private void initialise(Device device) {
@ -250,7 +261,7 @@ public class DevicePane extends TitledDescriptionPane {
}
private void setDefaultStatus() {
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Place card on reader" : "Unlocked");
setDescription(device.isNeedsPinSent() ? "Locked" : device.isNeedsPassphraseSent() ? "Passphrase Required" : device.isCard() ? "Leave card on reader" : "Unlocked");
}
private void createUnlockButton() {
@ -370,15 +381,26 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setVisible(false);
}
private void createUnsealButton() {
unsealButton = new Button("Unseal");
unsealButton.setAlignment(Pos.CENTER_RIGHT);
unsealButton.setOnAction(event -> {
unsealButton.setDisable(true);
unseal();
private void createGetPrivateKeyButton() {
getPrivateKeyButton = new Button("Get Private Key");
getPrivateKeyButton.setAlignment(Pos.CENTER_RIGHT);
getPrivateKeyButton.setOnAction(event -> {
getPrivateKeyButton.setDisable(true);
getPrivateKey();
});
unsealButton.managedProperty().bind(unsealButton.visibleProperty());
unsealButton.setVisible(false);
getPrivateKeyButton.managedProperty().bind(getPrivateKeyButton.visibleProperty());
getPrivateKeyButton.setVisible(false);
}
private void createGetAddressButton() {
getAddressButton = new Button("Get Address");
getAddressButton.setAlignment(Pos.CENTER_RIGHT);
getAddressButton.setOnAction(event -> {
getAddressButton.setDisable(true);
getAddress();
});
getAddressButton.managedProperty().bind(getAddressButton.visibleProperty());
getAddressButton.setVisible(false);
}
private void unlock(Device device) {
@ -618,15 +640,24 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
if(!cardApi.isInitialized()) {
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(importButton));
showHideLink.setVisible(false);
setExpanded(true);
importButton.setDisable(false);
return;
}
setDescription("Card not initialized");
setContent(getCardInitializationPanel(cardApi));
setContent(getCardInitializationPanel(cardApi, importButton, DeviceOperation.IMPORT));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
Service<Keystore> importService = cardApi.getImportService(derivation, messageProperty);
handleCardOperation(importService, importButton, "Import", event -> {
handleCardOperation(importService, importButton, "Import", true, event -> {
importKeystore(derivation, importService.getValue());
});
} catch(Exception e) {
@ -705,7 +736,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<PSBT> signService = cardApi.getSignService(wallet, psbt, messageProperty);
handleCardOperation(signService, signButton, "Signing", event -> {
handleCardOperation(signService, signButton, "Signing", true, event -> {
EventManager.get().post(new PSBTSignedEvent(psbt, signService.getValue()));
});
} catch(Exception e) {
@ -730,8 +761,8 @@ public class DevicePane extends TitledDescriptionPane {
}
}
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, EventHandler<WorkerStateEvent> successHandler) {
if(pin.get().length() < 6) {
private void handleCardOperation(Service<?> service, ButtonBase operationButton, String operationDescription, boolean pinRequired, EventHandler<WorkerStateEvent> successHandler) {
if(pinRequired && pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(operationButton));
showHideLink.setVisible(false);
@ -774,7 +805,7 @@ public class DevicePane extends TitledDescriptionPane {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<String> signMessageService = cardApi.getSignMessageService(message, wallet.getScriptType(), keyDerivation.getDerivation(), messageProperty);
handleCardOperation(signMessageService, signMessageButton, "Signing", event -> {
handleCardOperation(signMessageService, signMessageButton, "Signing", true, event -> {
String signature = signMessageService.getValue();
EventManager.get().post(new MessageSignedEvent(wallet, signature));
});
@ -855,18 +886,51 @@ public class DevicePane extends TitledDescriptionPane {
getXpubsService.start();
}
private void unseal() {
private void getPrivateKey() {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
Service<ECKey> unsealService = cardApi.getUnsealService(messageProperty);
handleCardOperation(unsealService, unsealButton, "Unseal", event -> {
EventManager.get().post(new DeviceUnsealedEvent(unsealService.getValue(), cardApi.getDefaultScriptType()));
Service<ECKey> privateKeyService = cardApi.getPrivateKeyService(messageProperty);
handleCardOperation(privateKeyService, getPrivateKeyButton, "Private Key", true, event -> {
EventManager.get().post(new DeviceGetPrivateKeyEvent(privateKeyService.getValue(), cardApi.getDefaultScriptType()));
});
} catch(Exception e) {
log.error("Unseal Error: " + e.getMessage(), e);
setError("Unseal Error", e.getMessage());
unsealButton.setDisable(false);
log.error("Private Key Error: " + e.getMessage(), e);
setError("Private Key Error", e.getMessage());
getPrivateKeyButton.setDisable(false);
}
}
}
private void getAddress() {
if(device.isCard()) {
try {
CardApi cardApi = CardApi.getCardApi(device.getModel(), pin.get());
if(!cardApi.isInitialized()) {
if(pin.get().length() < 6) {
setDescription(pin.get().isEmpty() ? "Enter PIN code" : "PIN code too short");
setContent(getCardPinEntry(getAddressButton));
showHideLink.setVisible(false);
setExpanded(true);
getAddressButton.setDisable(false);
return;
}
setDescription("Card not initialized");
setContent(getCardInitializationPanel(cardApi, getAddressButton, DeviceOperation.GET_ADDRESS));
showHideLink.setVisible(false);
setExpanded(true);
return;
}
Service<Address> addressService = cardApi.getAddressService(messageProperty);
handleCardOperation(addressService, getAddressButton, "Address", false, event -> {
EventManager.get().post(new DeviceAddressEvent(addressService.getValue()));
});
} catch(Exception e) {
log.error("Address Error: " + e.getMessage(), e);
setError("Address Error", e.getMessage());
getAddressButton.setDisable(false);
}
}
}
@ -897,9 +961,13 @@ public class DevicePane extends TitledDescriptionPane {
discoverKeystoresButton.setDefaultButton(defaultDevice);
discoverKeystoresButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.UNSEAL)) {
unsealButton.setDefaultButton(defaultDevice);
unsealButton.setVisible(true);
} else if(deviceOperation.equals(DeviceOperation.GET_PRIVATE_KEY)) {
getPrivateKeyButton.setDefaultButton(defaultDevice);
getPrivateKeyButton.setVisible(true);
showHideLink.setVisible(false);
} else if(deviceOperation.equals(DeviceOperation.GET_ADDRESS)) {
getAddressButton.setDefaultButton(defaultDevice);
getAddressButton.setVisible(true);
showHideLink.setVisible(false);
}
}
@ -943,12 +1011,12 @@ public class DevicePane extends TitledDescriptionPane {
return contentBox;
}
private Node getCardInitializationPanel(CardApi cardApi) {
private Node getCardInitializationPanel(CardApi cardApi, ButtonBase operationButton, DeviceOperation deviceOperation) {
VBox initTypeBox = new VBox(5);
RadioButton automatic = new RadioButton("Automatic (Recommended)");
RadioButton advanced = new RadioButton("Advanced");
TextField entropy = new TextField();
entropy.setPromptText("Enter input for chain code");
entropy.setPromptText("Enter input for user entropy");
entropy.setDisable(true);
ToggleGroup toggleGroup = new ToggleGroup();
@ -964,19 +1032,30 @@ public class DevicePane extends TitledDescriptionPane {
Button initializeButton = new Button("Initialize");
initializeButton.setDefaultButton(true);
initializeButton.setOnAction(event -> {
initializeButton.setDisable(true);
byte[] chainCode = toggleGroup.getSelectedToggle() == automatic ? null : Sha256Hash.hashTwice(entropy.getText().getBytes(StandardCharsets.UTF_8));
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode);
cardInitializationService.setOnSucceeded(event1 -> {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou will now need to enter the PIN code found on the back. You can change the PIN code once it has been imported.");
setDescription("Enter PIN code");
setContent(getCardPinEntry(importButton));
importButton.setDisable(false);
setExpanded(true);
Service<Void> cardInitializationService = cardApi.getInitializationService(chainCode, messageProperty);
cardInitializationService.setOnSucceeded(successEvent -> {
if(deviceOperation == DeviceOperation.IMPORT) {
AppServices.showSuccessDialog("Card Initialized", "The card was successfully initialized.\n\nYou can now import the keystore.");
} else if(deviceOperation == DeviceOperation.GET_ADDRESS) {
AppServices.showSuccessDialog("Card Reinitialized", "The card was successfully reinitialized.\n\nYou can now retrieve the new deposit address.");
}
operationButton.setDisable(false);
setDefaultStatus();
setExpanded(false);
});
cardInitializationService.setOnFailed(event1 -> {
Throwable e = event1.getSource().getException();
log.error("Error initializing card", e);
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + e.getMessage());
cardInitializationService.setOnFailed(failEvent -> {
Throwable rootCause = Throwables.getRootCause(failEvent.getSource().getException());
if(rootCause instanceof CardAuthorizationException) {
setError(rootCause.getMessage(), null);
setContent(getCardPinEntry(operationButton));
operationButton.setDisable(false);
} else {
log.error("Error initializing card", failEvent.getSource().getException());
AppServices.showErrorDialog("Card Initialization Failed", "The card was not initialized.\n\n" + failEvent.getSource().getException().getMessage());
initializeButton.setDisable(false);
}
});
cardInitializationService.start();
});
@ -1018,6 +1097,6 @@ public class DevicePane extends TitledDescriptionPane {
}
public enum DeviceOperation {
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, UNSEAL;
IMPORT, SIGN, DISPLAY_ADDRESS, SIGN_MESSAGE, DISCOVER_KEYSTORES, GET_PRIVATE_KEY, GET_ADDRESS;
}
}

View file

@ -4,12 +4,12 @@ import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.DeviceUnsealedEvent;
import com.sparrowwallet.sparrow.event.DeviceGetPrivateKeyEvent;
import com.sparrowwallet.sparrow.io.Device;
import java.util.List;
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.UnsealedKey> {
public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.DevicePrivateKey> {
public DeviceUnsealDialog(List<String> operationFingerprints) {
super(operationFingerprints);
EventManager.get().register(this);
@ -20,13 +20,13 @@ public class DeviceUnsealDialog extends DeviceDialog<DeviceUnsealDialog.Unsealed
@Override
protected DevicePane getDevicePane(Device device, boolean defaultDevice) {
return new DevicePane(device, defaultDevice);
return new DevicePane(DevicePane.DeviceOperation.GET_PRIVATE_KEY, device, defaultDevice);
}
@Subscribe
public void deviceUnsealed(DeviceUnsealedEvent event) {
setResult(new UnsealedKey(event.getPrivateKey(), event.getScriptType()));
public void deviceGetPrivateKey(DeviceGetPrivateKeyEvent event) {
setResult(new DevicePrivateKey(event.getPrivateKey(), event.getScriptType()));
}
public record UnsealedKey(ECKey privateKey, ScriptType scriptType) {}
public record DevicePrivateKey(ECKey privateKey, ScriptType scriptType) {}
}

View file

@ -15,7 +15,9 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.net.ElectrumServer;
import com.sparrowwallet.sparrow.net.ServerType;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.event.ActionEvent;
@ -44,6 +46,7 @@ import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@ -290,11 +293,11 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
private void unsealPrivateKey() {
DeviceUnsealDialog deviceUnsealDialog = new DeviceUnsealDialog(Collections.emptyList());
Optional<DeviceUnsealDialog.UnsealedKey> optPrivateKey = deviceUnsealDialog.showAndWait();
Optional<DeviceUnsealDialog.DevicePrivateKey> optPrivateKey = deviceUnsealDialog.showAndWait();
if(optPrivateKey.isPresent()) {
DeviceUnsealDialog.UnsealedKey unsealedKey = optPrivateKey.get();
key.setText(unsealedKey.privateKey().getPrivateKeyEncoded().toBase58());
keyScriptType.setValue(unsealedKey.scriptType());
DeviceUnsealDialog.DevicePrivateKey devicePrivateKey = optPrivateKey.get();
key.setText(devicePrivateKey.privateKey().getPrivateKeyEncoded().toBase58());
keyScriptType.setValue(devicePrivateKey.scriptType());
}
}
@ -305,7 +308,16 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
Address fromAddress = scriptType.getAddress(privateKey.getKey());
Address destAddress = getToAddress();
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress);
Date since = null;
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
WalletBirthDateDialog addressScanDateDialog = new WalletBirthDateDialog(null, true);
Optional<Date> optSince = addressScanDateDialog.showAndWait();
if(optSince.isPresent()) {
since = optSince.get();
}
}
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
addressUtxosService.setOnSucceeded(successEvent -> {
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
});
@ -313,6 +325,12 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
log.error("Error retrieving outputs for address " + fromAddress, failedEvent.getSource().getException());
AppServices.showErrorDialog("Error retrieving outputs for address", failedEvent.getSource().getException().getMessage());
});
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Address Scan", "Scanning address for transactions...", "/image/sparrow.png", addressUtxosService);
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
}
addressUtxosService.start();
} catch(Exception e) {
log.error("Error creating sweep transaction", e);
@ -340,8 +358,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
long dustThreshold = destAddress.getScriptType().getDustThreshold(sweepOutput, Transaction.DUST_RELAY_TX_FEE);
if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
return;
feeRate = Transaction.DEFAULT_MIN_RELAY_FEE;
fee = (long)Math.ceil(noFeeTransaction.getVirtualSize() * feeRate) + 1;
if(total - fee <= dustThreshold) {
AppServices.showErrorDialog("Insufficient funds", "The unspent outputs for this private key contain insufficient funds to spend (" + total + " sats).");
return;
}
}
Transaction transaction = new Transaction();

View file

@ -21,12 +21,12 @@ import java.util.Date;
public class WalletBirthDateDialog extends Dialog<Date> {
private final DatePicker birthDatePicker;
public WalletBirthDateDialog(Date birthDate) {
public WalletBirthDateDialog(Date birthDate, boolean singleAddress) {
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
setTitle("Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first wallet transaction:");
setTitle(singleAddress ? "Address Scan Start Date" : "Wallet Birth Date");
dialogPane.setHeaderText("Select an approximate date earlier than the first " + (singleAddress ? "" : "wallet") + " transaction:");
dialogPane.getStylesheets().add(AppServices.class.getResource("general.css").toExternalForm());
dialogPane.getButtonTypes().addAll(ButtonType.CANCEL);
dialogPane.setPrefWidth(420);
@ -58,7 +58,7 @@ public class WalletBirthDateDialog extends Dialog<Date> {
));
});
final ButtonType okButtonType = new javafx.scene.control.ButtonType("Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
final ButtonType okButtonType = new javafx.scene.control.ButtonType(singleAddress ? "Scan Address" : "Rescan Wallet", ButtonBar.ButtonData.OK_DONE);
dialogPane.getButtonTypes().addAll(okButtonType);
Button okButton = (Button) dialogPane.lookupButton(okButtonType);
BooleanBinding isInvalid = Bindings.createBooleanBinding(() -> birthDatePicker.getValue() == null, birthDatePicker.valueProperty());

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.address.Address;
public class DeviceAddressEvent {
private final Address address;
public DeviceAddressEvent(Address address) {
this.address = address;
}
public Address getAddress() {
return address;
}
}

View file

@ -3,11 +3,11 @@ package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
public class DeviceUnsealedEvent {
public class DeviceGetPrivateKeyEvent {
private final ECKey privateKey;
private final ScriptType scriptType;
public DeviceUnsealedEvent(ECKey privateKey, ScriptType scriptType) {
public DeviceGetPrivateKeyEvent(ECKey privateKey, ScriptType scriptType) {
this.privateKey = privateKey;
this.scriptType = scriptType;
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.io;
import com.google.common.base.Throwables;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -46,7 +47,7 @@ public abstract class CardApi {
public abstract boolean isInitialized() throws CardException;
public abstract void initialize(byte[] entropy) throws CardException;
public abstract void initialize(int slot, byte[] entropy) throws CardException;
public abstract WalletModel getCardType() throws CardException;
@ -62,7 +63,7 @@ public abstract class CardApi {
public abstract Keystore getKeystore() throws CardException;
public abstract Service<Void> getInitializationService(byte[] entropy);
public abstract Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty);
public abstract Service<Keystore> getImportService(List<ChildNumber> derivation, StringProperty messageProperty);
@ -70,7 +71,9 @@ public abstract class CardApi {
public abstract Service<String> getSignMessageService(String message, ScriptType scriptType, List<ChildNumber> derivation, StringProperty messageProperty);
public abstract Service<ECKey> getUnsealService(StringProperty messageProperty);
public abstract Service<ECKey> getPrivateKeyService(StringProperty messageProperty);
public abstract Service<Address> getAddressService(StringProperty messageProperty);
public abstract void disconnect();

View file

@ -1,8 +1,10 @@
package com.sparrowwallet.sparrow.io;
import javafx.beans.property.StringProperty;
import javax.smartcardio.CardException;
public interface CardImport extends ImportExport {
boolean isInitialized() throws CardException;
void initialize(byte[] chainCode) throws CardException;
void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException;
}

View file

@ -0,0 +1,5 @@
package com.sparrowwallet.sparrow.io.ckcard;
public class CardAuthDump extends CardUnseal {
boolean tampered;
}

View file

@ -0,0 +1,26 @@
package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.address.InvalidAddressException;
import javax.smartcardio.CardException;
public class CardDump extends CardResponse {
int slot;
boolean used = true;
boolean sealed;
String address;
byte[] pubkey;
public Address getAddress() throws CardException {
try {
if(address != null) {
return Address.fromString(address);
}
} catch(InvalidAddressException e) {
throw new CardException("Invalid address provided", e);
}
return null;
}
}

View file

@ -88,15 +88,32 @@ public class CardProtocol {
}
}
public CardRead read(String cvc) throws CardException {
Map<String, Object> args = new HashMap<>();
args.put("nonce", getNonce());
public CardRead read(String cvc, int currentSlot) throws CardException {
byte[] userNonce = getNonce();
byte[] cardNonce = lastCardNonce;
JsonObject read = sendAuth("read", args, cvc);
return gson.fromJson(read, CardRead.class);
Map<String, Object> args = new HashMap<>();
args.put("nonce", userNonce);
JsonObject read;
if(cvc == null) {
read = send("read", args);
} else {
read = sendAuth("read", args, cvc);
}
CardRead cardRead = gson.fromJson(read, CardRead.class);
ECDSASignature ecdsaSignature = cardRead.getSignature();
Sha256Hash verificationData = getVerificationData(cardNonce, userNonce, new byte[] { (byte)currentSlot });
if(!ecdsaSignature.verify(verificationData.getBytes(), cardRead.pubkey)) {
throw new CardException("Card authentication failure: Provided signature did not match public key");
}
return cardRead;
}
public CardSetup setup(String cvc, byte[] chainCode) throws CardException {
public CardSetup setup(String cvc, int slot, byte[] chainCode) throws CardException {
if(chainCode == null) {
chainCode = Sha256Hash.hashTwice(secureRandom.generateSeed(128));
}
@ -106,6 +123,7 @@ public class CardProtocol {
}
Map<String, Object> args = new HashMap<>();
args.put("slot", slot);
args.put("chain_code", chainCode);
JsonObject setup = sendAuth("new", args, cvc);
return gson.fromJson(setup, CardSetup.class);
@ -188,16 +206,30 @@ public class CardProtocol {
return gson.fromJson(backup, CardBackup.class);
}
public CardUnseal unseal(String cvc) throws CardException {
CardStatus status = getStatus();
public CardUnseal unseal(String cvc, int slot) throws CardException {
Map<String, Object> args = new HashMap<>();
args.put("slot", status.getSlot());
args.put("slot", slot);
JsonObject unseal = sendAuth("unseal", args, cvc);
return gson.fromJson(unseal, CardUnseal.class);
}
public CardDump dump(int slot) throws CardException {
Map<String, Object> args = new HashMap<>();
args.put("slot", slot);
JsonObject dump = send("dump", args);
return gson.fromJson(dump, CardDump.class);
}
public CardAuthDump dump(String cvc, int slot) throws CardException {
Map<String, Object> args = new HashMap<>();
args.put("slot", slot);
JsonObject dump = sendAuth("dump", args, cvc);
return gson.fromJson(dump, CardAuthDump.class);
}
public void disconnect() throws CardException {
cardTransport.disconnect();
}

View file

@ -1,7 +1,26 @@
package com.sparrowwallet.sparrow.io.ckcard;
public class CardRead {
import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.ECKey;
import java.math.BigInteger;
import java.util.Arrays;
public class CardRead extends CardResponse {
byte[] sig;
byte[] pubkey;
byte[] card_nonce;
public ECDSASignature getSignature() {
if(sig != null) {
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64));
return new ECDSASignature(r, s);
}
return null;
}
public ECKey getPubKey() {
return ECKey.fromPublicOnly(pubkey);
}
}

View file

@ -1,27 +1,5 @@
package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.crypto.ECDSASignature;
import com.sparrowwallet.drongo.crypto.ECKey;
import java.math.BigInteger;
import java.util.Arrays;
public class CardSign extends CardResponse {
public class CardSign extends CardRead {
int slot;
byte[] sig;
byte[] pubkey;
public ECDSASignature getSignature() {
if(sig != null) {
BigInteger r = new BigInteger(1, Arrays.copyOfRange(sig, 0, 32));
BigInteger s = new BigInteger(1, Arrays.copyOfRange(sig, 32, 64));
return new ECDSASignature(r, s);
}
return null;
}
public ECKey getPubKey() {
return ECKey.fromPublicOnly(pubkey);
}
}

View file

@ -24,7 +24,7 @@ public class CardStatus extends CardResponse {
boolean testnet;
public boolean isInitialized() {
return getCardType() != WalletModel.TAPSIGNER || path != null;
return (getCardType() == WalletModel.TAPSIGNER && path != null) || (getCardType() == WalletModel.SATSCARD && addr != null);
}
public String getIdentifier() {
@ -49,7 +49,7 @@ public class CardStatus extends CardResponse {
return tapsigner ? WalletModel.TAPSIGNER : WalletModel.SATSCARD;
}
public int getSlot() {
public int getCurrentSlot() {
if(slots == null || slots.isEmpty()) {
return 0;
}
@ -57,6 +57,14 @@ public class CardStatus extends CardResponse {
return slots.get(0).intValue();
}
public int getLastSlot() {
if(slots == null || slots.size() < 2) {
return 0;
}
return slots.get(1).intValue();
}
@Override
public String toString() {
return "CardStatus{" +

View file

@ -66,6 +66,8 @@ public class CardTransport {
cborBuilder.put(entry.getKey(), byteValue);
} else if(entry.getValue() instanceof Long longValue) {
cborBuilder.put(entry.getKey(), longValue);
} else if(entry.getValue() instanceof Integer integerValue) {
cborBuilder.put(entry.getKey(), integerValue);
} else if(entry.getValue() instanceof Boolean booleanValue) {
cborBuilder.put(entry.getKey(), booleanValue);
} else if(entry.getValue() instanceof List<?> listValue) {

View file

@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow.io.ckcard;
import com.sparrowwallet.drongo.ExtendedKey;
import com.sparrowwallet.drongo.KeyDerivation;
import com.sparrowwallet.drongo.Utils;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.crypto.ChildNumber;
import com.sparrowwallet.drongo.crypto.ECKey;
import com.sparrowwallet.drongo.protocol.*;
@ -50,9 +51,9 @@ public class CkCardApi extends CardApi {
}
@Override
public void initialize(byte[] chainCode) throws CardException {
public void initialize(int slot, byte[] chainCode) throws CardException {
cardProtocol.verify();
cardProtocol.setup(cvc, chainCode);
cardProtocol.setup(cvc, slot, chainCode);
}
@Override
@ -129,8 +130,12 @@ public class CkCardApi extends CardApi {
}
@Override
public Service<Void> getInitializationService(byte[] entropy) {
return new CardImportPane.CardInitializationService(new Tapsigner(), entropy);
public Service<Void> getInitializationService(byte[] entropy, StringProperty messageProperty) {
if(cardType == WalletModel.TAPSIGNER) {
return new CardImportPane.CardInitializationService(new Tapsigner(), cvc, entropy, messageProperty);
}
return new CardInitializationService(entropy, messageProperty);
}
@Override
@ -245,15 +250,46 @@ public class CkCardApi extends CardApi {
}
@Override
public Service<ECKey> getUnsealService(StringProperty messageProperty) {
return new UnsealService(messageProperty);
public Service<ECKey> getPrivateKeyService(StringProperty messageProperty) {
return new PrivateKeyService(messageProperty);
}
ECKey unseal() throws CardException {
CardUnseal cardUnseal = cardProtocol.unseal(cvc);
ECKey getPrivateKey(int slot, boolean unsealed) throws CardException {
if(unsealed) {
CardAuthDump cardAuthDump = cardProtocol.dump(cvc, slot);
return cardAuthDump.getPrivateKey();
}
CardUnseal cardUnseal = cardProtocol.unseal(cvc, slot);
return cardUnseal.getPrivateKey();
}
@Override
public Service<Address> getAddressService(StringProperty messageProperty) {
return new AddressService(messageProperty);
}
Address getAddress(int currentSlot, int lastSlot, String addr) throws CardException {
if(currentSlot == lastSlot) {
CardDump cardDump = cardProtocol.dump(currentSlot);
if(!cardDump.sealed) {
return cardDump.getAddress();
}
}
CardRead cardRead = cardProtocol.read(null, currentSlot);
Address address = getDefaultScriptType().getAddress(cardRead.getPubKey());
String left = addr.substring(0, addr.indexOf('_'));
String right = addr.substring(addr.lastIndexOf('_') + 1);
if(!address.toString().startsWith(left) || !address.toString().endsWith(right)) {
throw new CardException("Card authentication failed: Provided pubkey does not match given address");
}
return address;
}
@Override
public void disconnect() {
try {
@ -263,6 +299,37 @@ public class CkCardApi extends CardApi {
}
}
public class CardInitializationService extends Service<Void> {
private final byte[] chainCode;
private final StringProperty messageProperty;
public CardInitializationService(byte[] chainCode, StringProperty messageProperty) {
this.chainCode = chainCode;
this.messageProperty = messageProperty;
}
@Override
protected Task<Void> createTask() {
return new Task<>() {
@Override
protected Void call() throws Exception {
CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + ".");
}
if(cardStatus.isInitialized()) {
throw new IllegalStateException("Card already initialized.");
}
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
initialize(cardStatus.getCurrentSlot(), chainCode);
return null;
}
};
}
}
public class AuthDelayService extends Service<Void> {
private final CardStatus cardStatus;
private final IntegerProperty delayProperty;
@ -390,10 +457,10 @@ public class CkCardApi extends CardApi {
}
}
public class UnsealService extends Service<ECKey> {
public class PrivateKeyService extends Service<ECKey> {
private final StringProperty messageProperty;
public UnsealService(StringProperty messageProperty) {
public PrivateKeyService(StringProperty messageProperty) {
this.messageProperty = messageProperty;
}
@ -404,12 +471,48 @@ public class CkCardApi extends CardApi {
protected ECKey call() throws Exception {
CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to unseal private keys.");
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to retrieve a private key.");
}
int slot = cardStatus.getCurrentSlot();
boolean unsealed = false;
if(!cardStatus.isInitialized()) {
//If card has been unsealed, but a new slot is not initialized, retrieve private key for previous slot
slot = slot - 1;
unsealed = true;
}
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
return unseal();
return getPrivateKey(slot, unsealed);
}
};
}
}
public class AddressService extends Service<Address> {
private final StringProperty messageProperty;
public AddressService(StringProperty messageProperty) {
this.messageProperty = messageProperty;
}
@Override
protected Task<Address> createTask() {
return new Task<>() {
@Override
protected Address call() throws Exception {
CardStatus cardStatus = getStatus();
if(cardStatus.getCardType() != WalletModel.SATSCARD) {
throw new IllegalStateException("Please use a " + WalletModel.SATSCARD.toDisplayString() + " to retrieve an address.");
}
if(!cardStatus.isInitialized()) {
throw new IllegalStateException("Please re-initialize card before attempting to get the address.");
}
checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
return getAddress(cardStatus.getCurrentSlot(), cardStatus.getLastSlot(), cardStatus.addr);
}
};
}

View file

@ -26,11 +26,24 @@ public class Tapsigner implements KeystoreCardImport {
}
@Override
public void initialize(byte[] chainCode) throws CardException {
public void initialize(String pin, byte[] chainCode, StringProperty messageProperty) throws CardException {
if(pin.length() < 6) {
throw new CardException("PIN too short.");
}
if(pin.length() > 32) {
throw new CardException("PIN too long.");
}
CkCardApi cardApi = null;
try {
cardApi = new CkCardApi(null);
cardApi.initialize(chainCode);
cardApi = new CkCardApi(pin);
CardStatus cardStatus = cardApi.getStatus();
if(cardStatus.isInitialized()) {
throw new IllegalStateException("Card is already initialized.");
}
cardApi.checkWait(cardStatus, new SimpleIntegerProperty(), messageProperty);
cardApi.initialize(0, chainCode);
} finally {
if(cardApi != null) {
cardApi.disconnect();

View file

@ -1767,15 +1767,22 @@ public class ElectrumServer {
public static class AddressUtxosService extends Service<List<TransactionOutput>> {
private final Address address;
private final Date since;
public AddressUtxosService(Address address) {
public AddressUtxosService(Address address, Date since) {
this.address = address;
this.since = since;
}
@Override
protected Task<List<TransactionOutput>> createTask() {
return new Task<>() {
protected List<TransactionOutput> call() throws ServerException {
if(ElectrumServer.cormorant != null) {
updateProgress(-1, 0);
ElectrumServer.cormorant.checkAddressImport(address, since);
}
ElectrumServer electrumServer = new ElectrumServer();
return electrumServer.getUtxos(address);
}

View file

@ -1,6 +1,7 @@
package com.sparrowwallet.sparrow.net.cormorant;
import com.google.common.eventbus.EventBus;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.io.Server;
@ -12,6 +13,8 @@ import com.sparrowwallet.sparrow.net.cormorant.electrum.ElectrumServerRunnable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Date;
public class Cormorant {
private static final Logger log = LoggerFactory.getLogger(Cormorant.class);
@ -62,6 +65,11 @@ public class Cormorant {
}
}
public void checkAddressImport(Address address, Date since) {
//Will block until address descriptor has been added
bitcoindClient.importAddress(address, since);
}
public boolean isRunning() {
return running;
}

View file

@ -153,6 +153,13 @@ public class BitcoindClient {
importWallets(wallet.isMasterWallet() ? wallet.getAllWallets() : wallet.getMasterWallet().getAllWallets());
}
public void importAddress(Address address, Date since) {
Map<String, ScanDate> outputDescriptors = new HashMap<>();
String addressOutputDescriptor = OutputDescriptor.toDescriptorString(address);
outputDescriptors.put(OutputDescriptor.normalize(addressOutputDescriptor), new ScanDate(since, null, false));
importDescriptors(outputDescriptors);
}
private Map<String, ScanDate> getWalletDescriptors(Collection<Wallet> wallets) throws ImportFailedException {
List<Wallet> validWallets = wallets.stream().filter(Wallet::isValid).collect(Collectors.toList());
@ -297,7 +304,7 @@ public class BitcoindClient {
}
if(!importingDescriptors.isEmpty()) {
log.warn("Importing descriptors " + importingDescriptors);
log.debug("Importing descriptors " + importingDescriptors);
List<ImportDescriptor> importDescriptors = importingDescriptors.entrySet().stream()
.map(entry -> {

View file

@ -443,7 +443,7 @@ public class KeystoreController extends WalletFormController implements Initiali
log.error("Error communicating with card", e);
AppServices.showErrorDialog("Error communicating with card", e.getMessage());
});
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Authentication Delay", "Waiting for authenication delay to clear...", "/image/tapsigner.png", authDelayService);
ServiceProgressDialog serviceProgressDialog = new ServiceProgressDialog("Authentication Delay", "Waiting for authentication delay to clear...", "/image/tapsigner.png", authDelayService);
AppServices.moveToActiveWindowScreen(serviceProgressDialog);
authDelayService.start();
} else {

View file

@ -21,6 +21,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.io.CardApi;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ExchangeSource;
@ -142,6 +143,13 @@ public class PaymentController extends WalletFormController implements Initializ
}
};
private static final Wallet nfcCardWallet = new Wallet() {
@Override
public String getFullDisplayName() {
return "NFC Card...";
}
};
@Override
public void initialize(URL location, ResourceBundle resources) {
EventManager.get().register(this);
@ -162,6 +170,13 @@ public class PaymentController extends WalletFormController implements Initializ
PayNymDialog payNymDialog = new PayNymDialog(sendController.getWalletForm().getWalletId(), PayNymDialog.Operation.SEND, selectLinkedOnly);
Optional<PayNym> optPayNym = payNymDialog.showAndWait();
optPayNym.ifPresent(this::setPayNym);
} else if(newValue == nfcCardWallet) {
DeviceGetAddressDialog deviceGetAddressDialog = new DeviceGetAddressDialog(Collections.emptyList());
Optional<Address> optAddress = deviceGetAddressDialog.showAndWait();
if(optAddress.isPresent()) {
address.setText(optAddress.get().toString());
label.requestFocus();
}
} else if(newValue != null) {
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
Address freshAddress = freshNode.getAddress();
@ -324,6 +339,10 @@ public class PaymentController extends WalletFormController implements Initializ
openWalletList.add(payNymWallet);
}
if(CardApi.isReaderAvailable()) {
openWalletList.add(nfcCardWallet);
}
openWallets.setItems(FXCollections.observableList(openWalletList));
}
@ -332,6 +351,10 @@ public class PaymentController extends WalletFormController implements Initializ
return getPayNymGlyph();
}
if(wallet == nfcCardWallet) {
return getNfcCardGlyph();
}
Wallet masterWallet = wallet.isMasterWallet() ? wallet : wallet.getMasterWallet();
Storage storage = AppServices.get().getOpenWallets().get(masterWallet);
if(storage != null) {
@ -639,6 +662,13 @@ public class PaymentController extends WalletFormController implements Initializ
return payNymGlyph;
}
public static Glyph getNfcCardGlyph() {
Glyph nfcCardGlyph = new Glyph(FontAwesome5.FONT_NAME, FontAwesome5.Glyph.WIFI);
nfcCardGlyph.getStyleClass().add("nfccard-icon");
nfcCardGlyph.setFontSize(12);
return nfcCardGlyph;
}
@Subscribe
public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) {
BitcoinUnit unit = sendController.getBitcoinUnit(event.getBitcoinUnit());

View file

@ -9,7 +9,6 @@ import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.QRCodeWriter;
import com.sparrowwallet.drongo.KeyPurpose;
import com.sparrowwallet.drongo.OutputDescriptor;
import com.sparrowwallet.drongo.address.Address;
import com.sparrowwallet.drongo.wallet.BlockTransactionHashIndex;
import com.sparrowwallet.drongo.wallet.KeystoreSource;
import com.sparrowwallet.drongo.wallet.Wallet;
@ -27,7 +26,6 @@ import javafx.fxml.Initializable;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
import org.controlsfx.glyphfont.Glyph;
import org.fxmisc.richtext.CodeArea;
import org.slf4j.Logger;
@ -207,21 +205,21 @@ public class ReceiveController extends WalletFormController implements Initializ
List<Device> possibleDevices = (List<Device>)displayAddress.getUserData();
if(possibleDevices != null && !possibleDevices.isEmpty()) {
if(possibleDevices.size() > 1 || possibleDevices.get(0).isNeedsPinSent() || possibleDevices.get(0).isNeedsPassphraseSent()) {
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
} else {
Device actualDevice = possibleDevices.get(0);
Hwi.DisplayAddressService displayAddressService = new Hwi.DisplayAddressService(actualDevice, "", wallet.getScriptType(), addressDescriptor);
displayAddressService.setOnFailed(failedEvent -> {
Platform.runLater(() -> {
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
});
});
displayAddressService.start();
}
} else {
DeviceAddressDialog dlg = new DeviceAddressDialog(wallet, addressDescriptor);
DeviceDisplayAddressDialog dlg = new DeviceDisplayAddressDialog(wallet, addressDescriptor);
dlg.showAndWait();
}
}