add initial support for sp wallets
This commit is contained in:
parent
dd50f6973b
commit
17a04510fd
2
drongo
2
drongo
@ -1 +1 @@
|
||||
Subproject commit 15e5aaa4b9846c7d410723a7785198c62ef66248
|
||||
Subproject commit bc526c90e1a4fa2988df1538d6925ea7fdceb5c3
|
||||
@ -1473,7 +1473,7 @@ public class AppController implements Initializable {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
if(wallet.getKeystores().size() == 1) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
|
||||
//Can sign and verify
|
||||
messageSignDialog = new MessageSignDialog(wallet);
|
||||
}
|
||||
|
||||
@ -1212,6 +1212,7 @@ public class AppServices {
|
||||
|
||||
public static boolean isWhirlpoolCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().get(0).hasSeed()
|
||||
@ -1222,6 +1223,7 @@ public class AppServices {
|
||||
|
||||
public static boolean isWhirlpoolPostmixCompatible(Wallet wallet) {
|
||||
return WHIRLPOOL_NETWORKS.contains(Network.get())
|
||||
&& wallet.getPolicyType() == PolicyType.SINGLE_HD
|
||||
&& wallet.getScriptType() != ScriptType.P2TR //Taproot not yet supported
|
||||
&& wallet.getKeystores().size() == 1
|
||||
&& wallet.getKeystores().getFirst().getWalletModel() != WalletModel.BITBOX_02; //BitBox02 does not support high account numbers
|
||||
|
||||
@ -55,8 +55,7 @@ public abstract class BaseController {
|
||||
descriptorArea.setMouseOverTextDelay(Duration.ofMillis(150));
|
||||
descriptorArea.addEventHandler(MouseOverTextEvent.MOUSE_OVER_TEXT_BEGIN, e -> {
|
||||
TwoDimensional.Position position = descriptorArea.getParagraph(0).getStyleSpans().offsetToPosition(e.getCharacterIndex(), Backward);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SILENT_PAYMENTS ?
|
||||
position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
int index = descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_HD || descriptorArea.getWallet().getPolicyType() == PolicyType.SINGLE_SP ? position.getMajor() - 1 : ((position.getMajor() - 1) / 2);
|
||||
if(position.getMajor() > 0 && index >= 0 && index < descriptorArea.getWallet().getKeystores().size()) {
|
||||
Keystore hoverKeystore = descriptorArea.getWallet().getKeystores().get(index);
|
||||
Point2D pos = e.getScreenPosition();
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.KeystoreSource;
|
||||
import com.sparrowwallet.drongo.wallet.StandardAccount;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
@ -68,7 +69,7 @@ public class AddAccountDialog extends Dialog<List<StandardAccount>> {
|
||||
|
||||
final ButtonType discoverButtonType = new javafx.scene.control.ButtonType("Discover", ButtonBar.ButtonData.LEFT);
|
||||
if(!availableAccounts.isEmpty() && (masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.SW_SEED)
|
||||
|| (masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
|| (masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)))) {
|
||||
dialogPane.getButtonTypes().add(discoverButtonType);
|
||||
Button discoverButton = (Button)dialogPane.lookupButton(discoverButtonType);
|
||||
discoverButton.disableProperty().bind(AppServices.onlineProperty().not());
|
||||
|
||||
@ -80,6 +80,7 @@ public class AddressTreeTable extends CoinTreeTable {
|
||||
contextMenu.getItems().add(showCountItem);
|
||||
getColumns().forEach(col -> col.setContextMenu(contextMenu));
|
||||
|
||||
setPlaceholder(getDefaultPlaceholder(rootEntry.getWallet()));
|
||||
setEditable(true);
|
||||
setupColumnWidths();
|
||||
|
||||
|
||||
@ -14,8 +14,7 @@ import org.fxmisc.richtext.CodeArea;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.MULTI;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.SINGLE;
|
||||
import static com.sparrowwallet.drongo.policy.PolicyType.*;
|
||||
import static com.sparrowwallet.drongo.protocol.ScriptType.MULTISIG;
|
||||
|
||||
public class DescriptorArea extends CodeArea {
|
||||
@ -33,13 +32,13 @@ public class DescriptorArea extends CodeArea {
|
||||
List<Keystore> keystores = wallet.getKeystores();
|
||||
int threshold = wallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
|
||||
if(SINGLE.equals(policyType)) {
|
||||
if(SINGLE_HD.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(MULTI.equals(policyType)) {
|
||||
if(MULTI_HD.equals(policyType)) {
|
||||
append(scriptType.getDescriptor(), "descriptor-text");
|
||||
append(MULTISIG.getDescriptor(), "descriptor-text");
|
||||
append(Integer.toString(threshold), "descriptor-text");
|
||||
@ -52,6 +51,12 @@ public class DescriptorArea extends CodeArea {
|
||||
append(MULTISIG.getCloseDescriptor(), "descriptor-text");
|
||||
append(scriptType.getCloseDescriptor(), "descriptor-text");
|
||||
}
|
||||
|
||||
if(SINGLE_SP.equals(policyType)) {
|
||||
append("sp(", "descriptor-text");
|
||||
replace(getLength(), getLength(), keystores.get(0).getScriptName(), List.of(keystores.get(0).isValid() ? "descriptor-text" : "descriptor-error", keystores.get(0).getScriptName()));
|
||||
append(")", "descriptor-text");
|
||||
}
|
||||
}
|
||||
|
||||
public Wallet getWallet() {
|
||||
|
||||
@ -4,6 +4,7 @@ import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
@ -408,7 +409,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
return wallet.getKeystores().size() == 1 && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
return wallet.getPolicyType() == PolicyType.SINGLE_HD && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
|
||||
@ -5,7 +5,7 @@ import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.SecureString;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.CryptoOutput;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryItem;
|
||||
import com.sparrowwallet.hummingbird.registry.RegistryType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@ -32,7 +32,7 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getCryptoOutput;
|
||||
import static com.sparrowwallet.sparrow.wallet.SettingsController.getUROutputDescriptor;
|
||||
|
||||
public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
private final Wallet wallet;
|
||||
@ -176,9 +176,9 @@ public class FileWalletExportPane extends TitledDescriptionPane {
|
||||
boolean addBbqrOption = exportWallet.getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
QREncoding encoding = exportWallet.getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(exportWallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(exportWallet);
|
||||
RegistryItem registryItem = getUROutputDescriptor(exportWallet);
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), cryptoOutput.toUR(), bbqr, encoding);
|
||||
qrDisplayDialog = new DescriptorQRDisplayDialog(exportWallet.getFullDisplayName(), outputDescriptor.toString(true), registryItem.toUR(), bbqr, encoding);
|
||||
} else if(exporter.getClass().equals(ColdcardMultisig.class)) {
|
||||
UR ur = UR.fromBytes(outputStream.toByteArray());
|
||||
BBQR bbqr = new BBQR(BBQRType.UNICODE, outputStream.toByteArray());
|
||||
|
||||
@ -14,6 +14,8 @@ import com.sparrowwallet.drongo.protocol.*;
|
||||
import com.sparrowwallet.drongo.psbt.PSBT;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTInput;
|
||||
import com.sparrowwallet.drongo.psbt.PSBTProofException;
|
||||
import com.sparrowwallet.drongo.silentpayments.*;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
@ -66,6 +68,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
private final ComboBox<Wallet> toWallet;
|
||||
private final FeeRangeSlider feeRange;
|
||||
private final CopyableLabel feeRate;
|
||||
private SilentPaymentAddress silentPaymentAddress;
|
||||
|
||||
public PrivateKeySweepDialog(Wallet wallet) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
@ -204,18 +207,31 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
});
|
||||
|
||||
toAddress.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
try {
|
||||
silentPaymentAddress = SilentPaymentAddress.from(newValue);
|
||||
} catch(Exception e) {
|
||||
silentPaymentAddress = null;
|
||||
}
|
||||
createButton.setDisable(!isValidKey() || !isValidToAddress());
|
||||
});
|
||||
|
||||
toWallet.valueProperty().addListener((observable, oldValue, selectedWallet) -> {
|
||||
if(selectedWallet != null) {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
if(selectedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
toAddress.setText(selectedWallet.getKeystores().getFirst().getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
toAddress.setText(selectedWallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
keyScriptType.setValue(ScriptType.P2PKH);
|
||||
if(wallet != null) {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
toAddress.setText(wallet.getKeystores().getFirst().getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
toAddress.setText(wallet.getFreshNode(KeyPurpose.RECEIVE).getAddress().toString());
|
||||
}
|
||||
}
|
||||
|
||||
AppServices.onEscapePressed(dialogPane.getScene(), () -> setResult(null));
|
||||
@ -272,10 +288,13 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
|
||||
private boolean isValidToAddress() {
|
||||
try {
|
||||
Address address = getToAddress();
|
||||
if(silentPaymentAddress != null) {
|
||||
return true;
|
||||
} catch (InvalidAddressException e) {
|
||||
}
|
||||
try {
|
||||
getToAddress();
|
||||
return true;
|
||||
} catch(InvalidAddressException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -347,7 +366,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
DumpedPrivateKey privateKey = getPrivateKey();
|
||||
ScriptType scriptType = keyScriptType.getValue();
|
||||
Address fromAddress = scriptType.getAddress(PolicyType.SINGLE_HD, privateKey.getKey());
|
||||
Address destAddress = getToAddress();
|
||||
Payment payment = silentPaymentAddress != null ? new SilentPayment(silentPaymentAddress, null, 0, true)
|
||||
: new Payment(getToAddress(), null, 0, true);
|
||||
|
||||
Date since = null;
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
@ -363,7 +383,7 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
|
||||
ElectrumServer.AddressUtxosService addressUtxosService = new ElectrumServer.AddressUtxosService(fromAddress, since);
|
||||
addressUtxosService.setOnSucceeded(successEvent -> {
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), destAddress);
|
||||
createTransaction(privateKey.getKey(), scriptType, addressUtxosService.getValue(), payment);
|
||||
});
|
||||
addressUtxosService.setOnFailed(failedEvent -> {
|
||||
Throwable rootCause = Throwables.getRootCause(failedEvent.getSource().getException());
|
||||
@ -383,7 +403,8 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Address destAddress) {
|
||||
private void createTransaction(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, Payment payment) {
|
||||
Address destAddress = payment instanceof SilentPayment silentPayment ? computeSilentPaymentAddress(privKey, scriptType, txOutputs, silentPayment) : payment.getAddress();
|
||||
ECKey pubKey = ECKey.fromPublicOnly(privKey);
|
||||
|
||||
Transaction noFeeTransaction = new Transaction();
|
||||
@ -468,6 +489,29 @@ public class PrivateKeySweepDialog extends Dialog<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
private Address computeSilentPaymentAddress(ECKey privKey, ScriptType scriptType, List<TransactionOutput> txOutputs, SilentPayment silentPayment) {
|
||||
ECKey summedPrivateKey = scriptType.getOutputKey(PolicyType.SINGLE_HD, privKey);
|
||||
if(scriptType == P2TR && summedPrivateKey.hasOddYCoord()) {
|
||||
summedPrivateKey = summedPrivateKey.negatePrivate();
|
||||
}
|
||||
|
||||
Set<HashIndex> outpoints = new LinkedHashSet<>();
|
||||
for(TransactionOutput txOutput : txOutputs) {
|
||||
outpoints.add(new HashIndex(txOutput.getHash(), txOutput.getIndex()));
|
||||
}
|
||||
|
||||
try {
|
||||
SilentPaymentUtils.computeOutputAddresses(List.of(silentPayment), summedPrivateKey, outpoints);
|
||||
if(!silentPayment.isAddressComputed()) {
|
||||
throw new IllegalStateException("Failed to compute silent payment address");
|
||||
}
|
||||
|
||||
return silentPayment.getAddress();
|
||||
} catch(InvalidSilentPaymentException e) {
|
||||
throw new IllegalStateException("Failed to compute silent payment address", e);
|
||||
}
|
||||
}
|
||||
|
||||
public Glyph getGlyph(FontAwesome5.Glyph glyphEnum) {
|
||||
Glyph glyph = new Glyph(FontAwesome5.FONT_NAME, glyphEnum);
|
||||
glyph.setFontSize(12);
|
||||
|
||||
@ -51,6 +51,7 @@ import org.openpnp.capture.CaptureDevice;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.CharBuffer;
|
||||
import java.nio.charset.CharsetDecoder;
|
||||
@ -582,6 +583,14 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
}
|
||||
}
|
||||
|
||||
private ECKey getKey(CryptoHDKey cryptoHDKey) {
|
||||
if(cryptoHDKey.isPrivateKey()) {
|
||||
return ECKey.fromPrivate(new BigInteger(1, cryptoHDKey.getKey()));
|
||||
} else {
|
||||
return ECKey.fromPublicOnly(cryptoHDKey.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
private OutputDescriptor getOutputDescriptor(CryptoOutput cryptoOutput) {
|
||||
ScriptType scriptType = getScriptType(cryptoOutput.getScriptExpressions());
|
||||
|
||||
@ -679,11 +688,16 @@ public class QRScanDialog extends Dialog<QRScanDialog.Result> {
|
||||
for(int i = 0; i < keys.size(); i++) {
|
||||
RegistryItem key = keys.get(i);
|
||||
if(key instanceof URHDKey urhdKey) {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
KeyDerivation keyDerivation = getKeyDerivation(urhdKey.getOrigin());
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
if(urhdKey.getChainCode() == null) {
|
||||
ECKey ecKey = getKey(urhdKey);
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(ecKey, keyDerivation, true));
|
||||
} else {
|
||||
ExtendedKey extendedKey = getExtendedKey(urhdKey);
|
||||
source = source.replaceAll("@" + i, OutputDescriptor.writeKey(extendedKey, keyDerivation, null, true, true));
|
||||
if(urhdKey.getName() != null) {
|
||||
mapExtendedPublicKeyLabels.put(extendedKey, urhdKey.getName());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Only extended HD keys are supported in output descriptors");
|
||||
|
||||
@ -44,13 +44,13 @@ public class WalletExportDialog extends Dialog<Wallet> {
|
||||
AnchorPane.setRightAnchor(scrollPane, 0.0);
|
||||
|
||||
List<WalletExport> exporters;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
|
||||
exporters = List.of(new Electrum(), new ElectrumPersonalServer(), new Descriptor(), new SpecterDesktop(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
|
||||
exporters = List.of(new Bip129(), new CaravanMultisig(), new ColdcardMultisig(), new CoboVaultMultisig(), new Electrum(), new ElectrumPersonalServer(), new KeystoneMultisig(),
|
||||
new Descriptor(), new JadeMultisig(), new PassportMultisig(), new SpecterDesktop(), new BlueWalletMultisig(), new SpecterDIY(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SILENT_PAYMENTS) {
|
||||
exporters = List.of(new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
exporters = List.of(new Descriptor(), new Sparrow(), new WalletLabels(allWalletForms), new WalletTransactions(selectedWalletForm));
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Cannot export wallet with policy type " + wallet.getPolicyType());
|
||||
}
|
||||
|
||||
@ -20,6 +20,6 @@ public class SettingsChangedEvent {
|
||||
}
|
||||
|
||||
public enum Type {
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
|
||||
POLICY, SCRIPT_TYPE, MUTLISIG_THRESHOLD, MULTISIG_TOTAL, KEYSTORE_LABEL, KEYSTORE_FINGERPRINT, KEYSTORE_DERIVATION, KEYSTORE_XPUB, KEYSTORE_SP_SCAN, GAP_LIMIT, BIRTH_DATE, WATCH_LAST;
|
||||
}
|
||||
}
|
||||
|
||||
@ -3,10 +3,10 @@ package com.sparrowwallet.sparrow.io;
|
||||
import com.sparrowwallet.drongo.KeyDerivation;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.OutputDescriptor;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Keystore;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletModel;
|
||||
import com.sparrowwallet.sparrow.wallet.KeystoreController;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -29,26 +29,32 @@ public class Descriptor implements WalletImport, WalletExport {
|
||||
public void exportWallet(Wallet wallet, OutputStream outputStream, String password) throws ExportException {
|
||||
try {
|
||||
BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
|
||||
bufferedWriter.write("# Receive and change descriptor (BIP389):");
|
||||
bufferedWriter.newLine();
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet);
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
} else {
|
||||
bufferedWriter.write("# Receive and change descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
bufferedWriter.write(outputDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
|
||||
bufferedWriter.write("# Receive descriptor (Bitcoin Core):");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
|
||||
bufferedWriter.write(receiveDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Change descriptor (Bitcoin Core):");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
|
||||
bufferedWriter.write(changeDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Receive descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor receiveDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.RECEIVE, null);
|
||||
bufferedWriter.write(receiveDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.newLine();
|
||||
bufferedWriter.write("# Change descriptor:");
|
||||
bufferedWriter.newLine();
|
||||
OutputDescriptor changeDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.CHANGE, null);
|
||||
bufferedWriter.write(changeDescriptor.toString(true));
|
||||
bufferedWriter.newLine();
|
||||
}
|
||||
|
||||
bufferedWriter.flush();
|
||||
} catch(Exception e) {
|
||||
|
||||
@ -188,7 +188,7 @@ public class Storage {
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.getSeed().setPassphrase(copyKeystore.getSeed().getPassphrase());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
keystore.setSilentPaymentScanAddress(wallet.getPolicyType() == PolicyType.SINGLE_SP ? derivedKeystore.getSilentPaymentScanAddress() : null);
|
||||
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
|
||||
copyKeystore.getSeed().clear();
|
||||
} else if(keystore.hasMasterPrivateExtendedKey()) {
|
||||
Keystore copyKeystore = copy.getKeystores().get(i);
|
||||
@ -196,7 +196,7 @@ public class Storage {
|
||||
keystore.setKeyDerivation(derivedKeystore.getKeyDerivation());
|
||||
keystore.setExtendedPublicKey(derivedKeystore.getExtendedPublicKey());
|
||||
keystore.setBip47ExtendedPrivateKey(derivedKeystore.getBip47ExtendedPrivateKey());
|
||||
keystore.setSilentPaymentScanAddress(wallet.getPolicyType() == PolicyType.SINGLE_SP ? derivedKeystore.getSilentPaymentScanAddress() : null);
|
||||
keystore.setSilentPaymentScanAddress(derivedKeystore.getSilentPaymentScanAddress());
|
||||
copyKeystore.getMasterPrivateKey().clear();
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ public interface KeystoreDao {
|
||||
long id = insert(truncate(keystore.getLabel()), keystore.getSource().ordinal(), keystore.getWalletModel().ordinal(),
|
||||
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getKeyDerivation().getMasterFingerprint(),
|
||||
keystore.getKeyDerivation().getDerivationPath(),
|
||||
keystore.hasMasterPrivateKey() || wallet.isBip47() ? null : keystore.getExtendedPublicKey().toString(),
|
||||
keystore.hasMasterPrivateKey() || wallet.isBip47() || keystore.getExtendedPublicKey() == null ? null : keystore.getExtendedPublicKey().toString(),
|
||||
keystore.getExternalPaymentCode() == null ? null : keystore.getExternalPaymentCode().toString(),
|
||||
keystore.getSilentPaymentScanAddress() == null ? null : keystore.getSilentPaymentScanAddress().toBytes(),
|
||||
keystore.getDeviceRegistration(),
|
||||
|
||||
@ -112,7 +112,7 @@ public interface WalletDao {
|
||||
wallet.getKeystores().addAll(createKeystoreDao().getForWalletId(wallet.getId()));
|
||||
|
||||
List<WalletNode> walletNodes = createWalletNodeDao().getForWalletId(wallet.getScriptType().ordinal(), wallet.getId());
|
||||
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(walletNode -> walletNode.getDerivation().size() == 1).collect(Collectors.toList()));
|
||||
wallet.getPurposeNodes().addAll(walletNodes.stream().filter(WalletNode::isPurposeNode).collect(Collectors.toList()));
|
||||
wallet.getPurposeNodes().forEach(walletNode -> walletNode.setWallet(wallet));
|
||||
|
||||
Map<Sha256Hash, BlockTransaction> blockTransactions = createBlockTransactionDao().getForWalletId(wallet.getId());
|
||||
|
||||
@ -16,7 +16,7 @@ public class WalletNodeMapper implements RowMapper<WalletNode> {
|
||||
walletNode.setLabel(rs.getString("walletNode.label"));
|
||||
byte[] addressData = rs.getBytes("walletNode.addressData");
|
||||
if(addressData != null) {
|
||||
ScriptType scriptType = ScriptType.values()[rs.getInt(6)];
|
||||
ScriptType scriptType = ScriptType.values()[rs.getInt(7)];
|
||||
walletNode.setAddress(scriptType.getAddress(addressData));
|
||||
}
|
||||
walletNode.setSilentPaymentTweak(rs.getBytes("walletNode.silentPaymentTweak"));
|
||||
|
||||
@ -75,7 +75,7 @@ public class SettingsDialog extends WalletDialog {
|
||||
Panel leftButtonPanel = new Panel();
|
||||
leftButtonPanel.setLayoutManager(new GridLayout(2).setHorizontalSpacing(1));
|
||||
leftButtonPanel.addComponent(new Button("Add Account", this::showAddAccount));
|
||||
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE || getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SILENT_PAYMENTS) {
|
||||
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_HD || getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
leftButtonPanel.addComponent(new Button("Show Seed", this::showSeed));
|
||||
} else {
|
||||
leftButtonPanel.addComponent(new EmptySpace(TerminalSize.ZERO));
|
||||
|
||||
@ -4,6 +4,7 @@ import com.google.common.eventbus.Subscribe;
|
||||
import com.sparrowwallet.drongo.*;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
@ -86,6 +87,12 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
@FXML
|
||||
private TextArea xpub;
|
||||
|
||||
@FXML
|
||||
private Field spScanField;
|
||||
|
||||
@FXML
|
||||
private TextArea spScan;
|
||||
|
||||
@FXML
|
||||
private TextField derivation;
|
||||
|
||||
@ -152,12 +159,20 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
|
||||
derivation.setPromptText(getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
|
||||
|
||||
if(keystore.getExtendedPublicKey() != null) {
|
||||
xpubField.managedProperty().bind(xpubField.visibleProperty());
|
||||
spScanField.managedProperty().bind(spScanField.visibleProperty());
|
||||
spScanField.visibleProperty().bind(xpubField.visibleProperty().not());
|
||||
|
||||
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && keystore.getSilentPaymentScanAddress() != null) {
|
||||
spScan.setText(keystore.getSilentPaymentScanAddress().toKeyString());
|
||||
setSpScanContext(keystore.getSilentPaymentScanAddress());
|
||||
} else if(keystore.getExtendedPublicKey() != null) {
|
||||
xpub.setText(keystore.getExtendedPublicKey().toString());
|
||||
setXpubContext(keystore.getExtendedPublicKey());
|
||||
} else {
|
||||
switchXpubHeader.setDisable(true);
|
||||
xpubField.setText(Network.get().getXpubHeader().getDisplayName() + ":");
|
||||
spScanField.setText(Network.get().getSilentPaymentsScanKeyHrp() + ":");
|
||||
}
|
||||
|
||||
if(keystore.getKeyDerivation() != null) {
|
||||
@ -210,6 +225,17 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
}
|
||||
scanXpubQR.setVisible(!valid);
|
||||
});
|
||||
spScan.textProperty().addListener((observable, oldValue, newValue) -> {
|
||||
boolean valid = SilentPaymentScanAddress.isValid(newValue);
|
||||
if(valid) {
|
||||
SilentPaymentScanAddress silentPaymentScanAddress = SilentPaymentScanAddress.fromKeyString(newValue);
|
||||
setSpScanContext(silentPaymentScanAddress);
|
||||
if(!silentPaymentScanAddress.equals(keystore.getSilentPaymentScanAddress())) {
|
||||
keystore.setSilentPaymentScanAddress(silentPaymentScanAddress);
|
||||
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.KEYSTORE_SP_SCAN));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if(keystore.getSource() != KeystoreSource.SW_WATCH && (!walletForm.getWallet().isMasterWallet() || !walletForm.getWallet().getChildWallets().isEmpty())) {
|
||||
setInputFieldsDisabled(true);
|
||||
@ -252,6 +278,21 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
scanXpubQR.setVisible(false);
|
||||
}
|
||||
|
||||
private void setSpScanContext(SilentPaymentScanAddress silentPaymentScanAddress) {
|
||||
ContextMenu contextMenu = new ContextMenu();
|
||||
MenuItem copySpScan = new MenuItem("Copy " + Network.get().getSilentPaymentsScanKeyHrp());
|
||||
copySpScan.setOnAction(AE -> {
|
||||
contextMenu.hide();
|
||||
ClipboardContent content = new ClipboardContent();
|
||||
content.putString(silentPaymentScanAddress.toKeyString());
|
||||
Clipboard.getSystemClipboard().setContent(content);
|
||||
});
|
||||
contextMenu.getItems().add(copySpScan);
|
||||
|
||||
spScanField.setText(Network.get().getSilentPaymentsScanKeyHrp() + ":");
|
||||
spScan.setContextMenu(contextMenu);
|
||||
}
|
||||
|
||||
public void selectSource(ActionEvent event) {
|
||||
keystoreSourceToggleGroup.selectToggle(null);
|
||||
ToggleButton sourceButton = (ToggleButton)event.getSource();
|
||||
@ -260,7 +301,8 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
launchImportDialog(keystoreSource);
|
||||
} else {
|
||||
fingerprint.setText(KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT);
|
||||
derivation.setText(getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
|
||||
derivation.setText(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP ? KeyDerivation.writePath(KeyDerivation.getBip352Derivation(0))
|
||||
: getWalletForm().getWallet().getScriptType().getDefaultDerivationPath());
|
||||
selectSourcePane.setVisible(false);
|
||||
}
|
||||
}
|
||||
@ -283,12 +325,19 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(xpub, Validator.combine(
|
||||
Validator.createEmptyValidator(Network.get().getXpubHeader().getDisplayName() + " is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is invalid", !ExtendedKey.isValid(newValue)),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Extended key is not unique", ExtendedKey.isValid(newValue) &&
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is required", getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP && newValue.trim().isEmpty()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getXpubHeader().getDisplayName() + " is invalid", getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP && !ExtendedKey.isValid(newValue)),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Extended key is not unique", ExtendedKey.isValid(newValue) && getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP &&
|
||||
walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore && k.getExtendedPublicKey() != null).map(Keystore::getExtendedPublicKey).collect(Collectors.toList()).contains(ExtendedKey.fromDescriptor(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(spScan, Validator.combine(
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getSilentPaymentsScanKeyHrp() + " is required", getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && newValue.trim().isEmpty()),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getSilentPaymentsScanKeyHrp() + " is invalid", getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && !SilentPaymentScanAddress.isValid(newValue)),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, Network.get().getSilentPaymentsScanKeyHrp() + " is not unique", SilentPaymentScanAddress.isValid(newValue) && getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP &&
|
||||
walletForm.getWallet().getKeystores().stream().filter(k -> k != keystore && k.getSilentPaymentScanAddress() != null).map(Keystore::getSilentPaymentScanAddress).collect(Collectors.toList()).contains(SilentPaymentScanAddress.fromKeyString(newValue)))
|
||||
));
|
||||
|
||||
validationSupport.registerValidator(derivation, Validator.combine(
|
||||
Validator.createEmptyValidator("Derivation is required"),
|
||||
(Control c, String newValue) -> ValidationResult.fromErrorIf( c, "Derivation is invalid", !KeyDerivation.isValid(newValue)),
|
||||
@ -305,7 +354,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
private void updateType(boolean showExport) {
|
||||
type.setText(getTypeLabel(keystore));
|
||||
type.setGraphic(getTypeIcon(keystore));
|
||||
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
|
||||
exportButton.setVisible(showExport && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD);
|
||||
viewSeedButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasSeed());
|
||||
viewKeyButton.setVisible(keystore.getSource() == KeystoreSource.SW_SEED && keystore.hasMasterPrivateExtendedKey());
|
||||
cardServiceButtons.setVisible(keystore.getWalletModel().isCard());
|
||||
@ -319,6 +368,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
setEditable(derivation, editable);
|
||||
setEditable(xpub, editable);
|
||||
scanXpubQR.setVisible(editable);
|
||||
setEditable(spScan, editable);
|
||||
|
||||
xpubField.setVisible(getWalletForm().getWallet().getPolicyType() != PolicyType.SINGLE_SP);
|
||||
}
|
||||
|
||||
private void setEditable(TextInputControl textInputControl, boolean editable) {
|
||||
@ -374,6 +426,9 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
private void launchImportDialog(KeystoreSource initialSource) {
|
||||
boolean restrictImport = keystore.getSource() != KeystoreSource.SW_WATCH && keystoreSourceToggleGroup.getToggles().stream().anyMatch(toggle -> ((ToggleButton)toggle).isDisabled());
|
||||
KeyDerivation currentDerivation = keystore.getKeyDerivation();
|
||||
if((currentDerivation == null || currentDerivation.getDerivation().isEmpty()) && getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
currentDerivation = new KeyDerivation(KeyDerivation.DEFAULT_WATCH_ONLY_FINGERPRINT, KeyDerivation.getBip352Derivation(0));
|
||||
}
|
||||
WalletModel currentModel = keystore.getWalletModel();
|
||||
String currentLabel = keystore.getLabel();
|
||||
KeystoreImportDialog dlg = new KeystoreImportDialog(getWalletForm().getWallet(), initialSource, currentDerivation, currentModel, currentLabel, restrictImport);
|
||||
@ -405,17 +460,22 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
keystore.setMasterPrivateExtendedKey(importedKeystore.getMasterPrivateExtendedKey());
|
||||
keystore.setSeed(importedKeystore.getSeed());
|
||||
keystore.setBip47ExtendedPrivateKey(importedKeystore.getBip47ExtendedPrivateKey());
|
||||
keystore.setSilentPaymentScanAddress(importedKeystore.getSilentPaymentScanAddress());
|
||||
|
||||
updateType(keystore.isValid());
|
||||
label.setText(keystore.getLabel());
|
||||
fingerprint.setText(keystore.getKeyDerivation().getMasterFingerprint());
|
||||
derivation.setText(keystore.getKeyDerivation().getDerivationPath());
|
||||
|
||||
if(keystore.getExtendedPublicKey() != null) {
|
||||
if(getWalletForm().getWallet().getPolicyType() == PolicyType.SINGLE_SP && keystore.getSilentPaymentScanAddress() != null) {
|
||||
spScan.setText(keystore.getSilentPaymentScanAddress().toKeyString());
|
||||
setSpScanContext(keystore.getSilentPaymentScanAddress());
|
||||
} else if(keystore.getExtendedPublicKey() != null) {
|
||||
xpub.setText(keystore.getExtendedPublicKey().toString());
|
||||
setXpubContext(keystore.getExtendedPublicKey());
|
||||
} else {
|
||||
xpub.setText("");
|
||||
spScan.setText("");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -651,6 +711,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
setEditable(fingerprint, !disabled);
|
||||
setEditable(derivation, !disabled);
|
||||
setEditable(xpub, !disabled);
|
||||
setEditable(spScan, !disabled);
|
||||
importButton.setDisable(disabled);
|
||||
}
|
||||
|
||||
@ -674,9 +735,12 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
derivation.setText(derivationPath + " ");
|
||||
derivation.setText(derivationPath);
|
||||
}
|
||||
if(keystore.getExtendedPublicKey() != null) {
|
||||
if(keystore.getExtendedPublicKey() != null && walletForm.getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
setXpubContext(keystore.getExtendedPublicKey());
|
||||
}
|
||||
if(keystore.getSilentPaymentScanAddress() != null && walletForm.getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
setSpScanContext(keystore.getSilentPaymentScanAddress());
|
||||
}
|
||||
} else if(event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_LABEL)) {
|
||||
if(!keystore.getLabel().equals(label.getText())) {
|
||||
label.setText(keystore.getLabel());
|
||||
@ -686,7 +750,7 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
if(event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_LABEL) || event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_FINGERPRINT) ||
|
||||
event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_DERIVATION) || event.getType().equals(SettingsChangedEvent.Type.KEYSTORE_XPUB)) {
|
||||
if(keystore.getSource() == KeystoreSource.SW_WATCH) {
|
||||
exportButton.setVisible(keystore.isValid() && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI);
|
||||
exportButton.setVisible(keystore.isValid() && getWalletForm().getWallet().getPolicyType() == PolicyType.MULTI_HD);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -696,7 +760,8 @@ public class KeystoreController extends WalletFormController implements Initiali
|
||||
public void keystoreLabelsChanged(KeystoreLabelsChangedEvent event) {
|
||||
if(event.getWalletId().equals(walletForm.getWalletId())) {
|
||||
for(Keystore changedKeystore : event.getChangedKeystores()) {
|
||||
if(xpub.getText().trim().equals(changedKeystore.getExtendedPublicKey().toString()) && !label.getText().equals(changedKeystore.getLabel())) {
|
||||
if(xpub.getText().trim().equals(changedKeystore.getExtendedPublicKey().toString()) && !label.getText().equals(changedKeystore.getLabel())
|
||||
|| spScan.getText().trim().equals(changedKeystore.getSilentPaymentScanAddress().toKeyString()) && !label.getText().equals(changedKeystore.getLabel())) {
|
||||
label.textProperty().removeListener(labelChangeListener);
|
||||
label.setText(changedKeystore.getLabel());
|
||||
keystore.setLabel(changedKeystore.getLabel());
|
||||
|
||||
@ -11,6 +11,7 @@ import com.sparrowwallet.drongo.bip47.InvalidPaymentCodeException;
|
||||
import com.sparrowwallet.drongo.bip47.PaymentCode;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.protocol.TransactionOutput;
|
||||
@ -314,14 +315,18 @@ public class PaymentController extends WalletFormController implements Initializ
|
||||
label.requestFocus();
|
||||
}
|
||||
} else if(newValue != null) {
|
||||
List<Address> existingAddresses = getOtherAddresses();
|
||||
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
|
||||
Address freshAddress = freshNode.getAddress();
|
||||
while(existingAddresses.contains(freshAddress) || (freshNode.getLabel() != null && !freshNode.getLabel().isEmpty())) {
|
||||
freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE, freshNode);
|
||||
freshAddress = freshNode.getAddress();
|
||||
if(newValue.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
address.setText(newValue.getKeystores().getFirst().getSilentPaymentScanAddress().getSilentPaymentAddress().getAddress());
|
||||
} else {
|
||||
List<Address> existingAddresses = getOtherAddresses();
|
||||
WalletNode freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE);
|
||||
Address freshAddress = freshNode.getAddress();
|
||||
while(existingAddresses.contains(freshAddress) || (freshNode.getLabel() != null && !freshNode.getLabel().isEmpty())) {
|
||||
freshNode = newValue.getFreshNode(KeyPurpose.RECEIVE, freshNode);
|
||||
freshAddress = freshNode.getAddress();
|
||||
}
|
||||
address.setText(freshAddress.toString());
|
||||
}
|
||||
address.setText(freshAddress.toString());
|
||||
label.requestFocus();
|
||||
}
|
||||
});
|
||||
|
||||
@ -6,6 +6,7 @@ import com.sparrowwallet.drongo.crypto.*;
|
||||
import com.sparrowwallet.drongo.policy.Policy;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentScanAddress;
|
||||
import com.sparrowwallet.drongo.wallet.*;
|
||||
import com.sparrowwallet.hummingbird.UR;
|
||||
import com.sparrowwallet.hummingbird.registry.*;
|
||||
@ -118,6 +119,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
keystoreTabs = new TabPane();
|
||||
keystoreTabsPane.getChildren().add(keystoreTabs);
|
||||
|
||||
policyType.setButtonCell(new PolicyTypeButtonCell());
|
||||
policyType.setCellFactory(_ -> new PolicyTypeListCell());
|
||||
policyType.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, policyType) -> {
|
||||
walletForm.getWallet().setPolicyType(policyType);
|
||||
|
||||
@ -131,8 +134,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
initialising = false;
|
||||
|
||||
multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI));
|
||||
if(policyType.equals(PolicyType.MULTI)) {
|
||||
multisigFieldset.setVisible(policyType.equals(PolicyType.MULTI_HD));
|
||||
if(policyType.equals(PolicyType.MULTI_HD)) {
|
||||
totalKeystores.bind(multisigControl.highValueProperty());
|
||||
} else {
|
||||
totalKeystores.set(1);
|
||||
@ -167,7 +170,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
return;
|
||||
} else if(optType.get() == ButtonType.YES) {
|
||||
clearKeystoreTabs();
|
||||
if(walletForm.getWallet().getPolicyType() == PolicyType.MULTI) {
|
||||
if(walletForm.getWallet().getPolicyType() == PolicyType.MULTI_HD) {
|
||||
totalKeystores.bind(multisigControl.highValueProperty());
|
||||
} else {
|
||||
totalKeystores.set(1);
|
||||
@ -225,7 +228,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
keystoreTabs.getTabs().remove(keystoreTabs.getTabs().size() - 1);
|
||||
}
|
||||
|
||||
if(walletForm.getWallet().getPolicyType().equals(PolicyType.MULTI)) {
|
||||
if(walletForm.getWallet().getPolicyType().equals(PolicyType.MULTI_HD)) {
|
||||
EventManager.get().post(new SettingsChangedEvent(walletForm.getWallet(), SettingsChangedEvent.Type.MULTISIG_TOTAL));
|
||||
}
|
||||
});
|
||||
@ -261,10 +264,10 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
saveWallet(false, false);
|
||||
|
||||
Wallet wallet = walletForm.getWallet();
|
||||
if(wallet.getPolicyType() == PolicyType.MULTI && wallet.getDefaultPolicy().getNumSignaturesRequired() < wallet.getKeystores().size() && addressChange) {
|
||||
if(wallet.getPolicyType() == PolicyType.MULTI_HD && wallet.getDefaultPolicy().getNumSignaturesRequired() < wallet.getKeystores().size() && addressChange) {
|
||||
String outputDescriptor = OutputDescriptor.getOutputDescriptor(wallet, KeyPurpose.DEFAULT_PURPOSES, null).toString(true);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(wallet);
|
||||
MultisigBackupDialog dialog = new MultisigBackupDialog(wallet, outputDescriptor, cryptoOutput.toUR());
|
||||
RegistryItem registryItem = getUROutputDescriptor(wallet);
|
||||
MultisigBackupDialog dialog = new MultisigBackupDialog(wallet, outputDescriptor, registryItem.toUR());
|
||||
dialog.initOwner(apply.getScene().getWindow());
|
||||
dialog.showAndWait();
|
||||
}
|
||||
@ -281,7 +284,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
|
||||
private void setFieldsFromWallet(Wallet wallet) {
|
||||
if(wallet.getPolicyType() == null) {
|
||||
wallet.setPolicyType(PolicyType.SINGLE);
|
||||
wallet.setPolicyType(PolicyType.SINGLE_HD);
|
||||
wallet.setScriptType(ScriptType.P2WPKH);
|
||||
Keystore keystore = new Keystore("Keystore 1");
|
||||
keystore.setSource(KeystoreSource.SW_WATCH);
|
||||
@ -290,9 +293,9 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
}
|
||||
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE)) {
|
||||
if(wallet.getPolicyType().equals(PolicyType.SINGLE_HD) || wallet.getPolicyType().equals(PolicyType.SINGLE_SP)) {
|
||||
totalKeystores.setValue(1);
|
||||
} else if(wallet.getPolicyType().equals(PolicyType.MULTI)) {
|
||||
} else if(wallet.getPolicyType().equals(PolicyType.MULTI_HD)) {
|
||||
multisigControl.setMax(Math.max(multisigControl.getMax(), wallet.getKeystores().size()));
|
||||
multisigControl.highValueProperty().set(wallet.getKeystores().size());
|
||||
multisigControl.lowValueProperty().set(wallet.getDefaultPolicy().getNumSignaturesRequired());
|
||||
@ -376,8 +379,8 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
|
||||
OutputDescriptor outputDescriptor = OutputDescriptor.getOutputDescriptor(walletForm.getWallet(), KeyPurpose.DEFAULT_PURPOSES, null);
|
||||
CryptoOutput cryptoOutput = getCryptoOutput(walletForm.getWallet());
|
||||
if(cryptoOutput == null) {
|
||||
RegistryItem registryItem = getUROutputDescriptor(walletForm.getWallet());
|
||||
if(registryItem == null) {
|
||||
AppServices.showErrorDialog("Unsupported Wallet Policy", "Cannot show a descriptor for this wallet.");
|
||||
return;
|
||||
}
|
||||
@ -385,32 +388,45 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
boolean addBbqrOption = walletForm.getWallet().getKeystores().stream().anyMatch(keystore -> keystore.getWalletModel().showBbqr());
|
||||
QREncoding encoding = walletForm.getWallet().getKeystores().stream().allMatch(keystore -> keystore.getWalletModel().selectBbqr()) ? QREncoding.BBQR : QREncoding.UR;
|
||||
|
||||
UR cryptoOutputUR = cryptoOutput.toUR();
|
||||
UR cryptoOutputUR = registryItem.toUR();
|
||||
BBQR bbqr = addBbqrOption ? new BBQR(BBQRType.UNICODE, outputDescriptor.toString(true).getBytes(StandardCharsets.UTF_8)) : null;
|
||||
QRDisplayDialog qrDisplayDialog = new DescriptorQRDisplayDialog(walletForm.getWallet().getFullDisplayName(), outputDescriptor.toString(true), cryptoOutputUR, bbqr, encoding);
|
||||
qrDisplayDialog.initOwner(showDescriptorQR.getScene().getWindow());
|
||||
qrDisplayDialog.showAndWait();
|
||||
}
|
||||
|
||||
public static CryptoOutput getCryptoOutput(Wallet wallet) {
|
||||
public static RegistryItem getUROutputDescriptor(Wallet wallet) {
|
||||
List<ScriptExpression> scriptExpressions = getScriptExpressions(wallet.getScriptType());
|
||||
|
||||
CryptoOutput cryptoOutput = null;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
cryptoOutput = new CryptoOutput(scriptExpressions, getCryptoHDKey(wallet.getKeystores().get(0)));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
RegistryItem registryItem = null;
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
KeyDerivation keyDerivation = keystore.getKeyDerivation();
|
||||
registryItem = new CryptoOutput(scriptExpressions, getCryptoHDKey(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivation(), keystore.getExtendedPublicKey(), keystore.getLabel()));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
|
||||
WalletNode firstReceive = new WalletNode(wallet, KeyPurpose.RECEIVE, 0);
|
||||
Utils.LexicographicByteArrayComparator lexicographicByteArrayComparator = new Utils.LexicographicByteArrayComparator();
|
||||
List<CryptoHDKey> cryptoHDKeys = wallet.getKeystores().stream().sorted((keystore1, keystore2) -> {
|
||||
return lexicographicByteArrayComparator.compare(keystore1.getPubKey(firstReceive).getPubKey(), keystore2.getPubKey(firstReceive).getPubKey());
|
||||
}).map(SettingsController::getCryptoHDKey).collect(Collectors.toList());
|
||||
}).map(keystore -> {
|
||||
KeyDerivation keyDerivation = keystore.getKeyDerivation();
|
||||
return getCryptoHDKey(keyDerivation.getMasterFingerprint(), keyDerivation.getDerivation(), keystore.getExtendedPublicKey(), keystore.getLabel());
|
||||
}).collect(Collectors.toList());
|
||||
MultiKey multiKey = new MultiKey(wallet.getDefaultPolicy().getNumSignaturesRequired(), null, cryptoHDKeys);
|
||||
List<ScriptExpression> multiScriptExpressions = new ArrayList<>(scriptExpressions);
|
||||
multiScriptExpressions.add(ScriptExpression.SORTED_MULTISIG);
|
||||
cryptoOutput = new CryptoOutput(multiScriptExpressions, multiKey);
|
||||
registryItem = new CryptoOutput(multiScriptExpressions, multiKey);
|
||||
} else if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
Keystore keystore = wallet.getKeystores().getFirst();
|
||||
KeyDerivation keyDerivation = keystore.getKeyDerivation();
|
||||
SilentPaymentScanAddress spScanAddress = keystore.getSilentPaymentScanAddress();
|
||||
URHDKey scanKey = getURHDKey(keyDerivation.getMasterFingerprint(), KeyDerivation.getBip352ScanDerivation(keyDerivation.getDerivation()), spScanAddress.getScanKey(), keystore.getLabel());
|
||||
URHDKey spendKey = getURHDKey(keyDerivation.getMasterFingerprint(), KeyDerivation.getBip352SpendDerivation(keyDerivation.getDerivation()), spScanAddress.getSpendKey(), keystore.getLabel());
|
||||
String annotations = wallet.getBirthHeight() != null ? "?" + OutputDescriptor.ANNOTATION_BLOCK_HEIGHT + "=" + wallet.getBirthHeight() : "";
|
||||
registryItem = new UROutputDescriptor("sp(@0,@1)" + annotations, List.of(scanKey, spendKey), wallet.getFullDisplayName(), null);
|
||||
}
|
||||
|
||||
return cryptoOutput;
|
||||
return registryItem;
|
||||
}
|
||||
|
||||
private static List<ScriptExpression> getScriptExpressions(ScriptType scriptType) {
|
||||
@ -435,12 +451,18 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
throw new IllegalArgumentException("Unknown script type of " + scriptType);
|
||||
}
|
||||
|
||||
private static CryptoHDKey getCryptoHDKey(Keystore keystore) {
|
||||
ExtendedKey extendedKey = keystore.getExtendedPublicKey();
|
||||
private static CryptoHDKey getCryptoHDKey(String masterFingerprint, List<ChildNumber> derivation, ExtendedKey extendedKey, String label) {
|
||||
CryptoCoinInfo cryptoCoinInfo = new CryptoCoinInfo(CryptoCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? CryptoCoinInfo.Network.MAINNET.ordinal() : CryptoCoinInfo.Network.TESTNET.ordinal());
|
||||
List<PathComponent> pathComponents = keystore.getKeyDerivation().getDerivation().stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
|
||||
CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(keystore.getKeyDerivation().getMasterFingerprint()), pathComponents.size());
|
||||
return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint(), keystore.getLabel(), null);
|
||||
List<PathComponent> pathComponents = derivation.stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
|
||||
CryptoKeypath cryptoKeypath = new CryptoKeypath(pathComponents, Utils.hexToBytes(masterFingerprint), pathComponents.size());
|
||||
return new CryptoHDKey(false, extendedKey.getKey().getPubKey(), extendedKey.getKey().getChainCode(), cryptoCoinInfo, cryptoKeypath, null, extendedKey.getParentFingerprint(), label, null);
|
||||
}
|
||||
|
||||
private static URHDKey getURHDKey(String masterFingerprint, List<ChildNumber> derivation, ECKey key, String label) {
|
||||
URCoinInfo cryptoCoinInfo = new URCoinInfo(URCoinInfo.Type.BITCOIN.ordinal(), Network.get() == Network.MAINNET ? URCoinInfo.Network.MAINNET.ordinal() : URCoinInfo.Network.TESTNET.ordinal());
|
||||
List<PathComponent> pathComponents = derivation.stream().map(cNum -> new IndexPathComponent(cNum.num(), cNum.isHardened())).collect(Collectors.toList());
|
||||
URKeypath cryptoKeypath = new URKeypath(pathComponents, Utils.hexToBytes(masterFingerprint), pathComponents.size());
|
||||
return new URHDKey(key.hasPrivKey(), key.hasPrivKey() ? key.getPrivKeyBytes() : key.getPubKey(), null, cryptoCoinInfo, cryptoKeypath, null, null, label, null);
|
||||
}
|
||||
|
||||
public void editDescriptor(ActionEvent event) {
|
||||
@ -451,7 +473,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
dialog.initOwner(editDescriptor.getScene().getWindow());
|
||||
dialog.setTitle("Edit wallet output descriptor");
|
||||
dialog.getDialogPane().setHeaderText("The wallet configuration is specified in the output descriptor.\nChanges to the output descriptor will modify the wallet configuration." +
|
||||
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI ? "\nKey expressions are shown in canonical order." : ""));
|
||||
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI_HD ? "\nKey expressions are shown in canonical order." : ""));
|
||||
Optional<String> text = dialog.showAndWait();
|
||||
if(text.isPresent() && !text.get().isEmpty() && !text.get().equals(outputDescriptorString)) {
|
||||
if(text.get().contains("(multi(")) {
|
||||
@ -514,6 +536,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
keystore.setWalletModel(existing.getWalletModel());
|
||||
if(existing.getKeyDerivation().getDerivation().equals(keystore.getKeyDerivation().getDerivation())) {
|
||||
keystore.setExtendedPublicKey(existing.getExtendedPublicKey());
|
||||
keystore.setSilentPaymentScanAddress(existing.getSilentPaymentScanAddress());
|
||||
} else {
|
||||
rederive = true;
|
||||
}
|
||||
@ -597,7 +620,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
dialog.initOwner(showDescriptor.getScene().getWindow());
|
||||
dialog.setTitle("Show wallet output descriptor");
|
||||
dialog.getDialogPane().setHeaderText("The wallet configuration is specified in the output descriptor.\nThis wallet is no longer editable - create a new wallet to change the descriptor." +
|
||||
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI ? "\nKey expressions are shown in canonical order." : ""));
|
||||
(walletForm.getWallet().getPolicyType() == PolicyType.MULTI_HD ? "\nKey expressions are shown in canonical order." : ""));
|
||||
dialog.showAndWait();
|
||||
}
|
||||
|
||||
@ -724,7 +747,7 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if(discoverAccounts && masterWallet.getKeystores().size() == 1 && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)) {
|
||||
if(discoverAccounts && masterWallet.getPolicyType() == PolicyType.SINGLE_HD && masterWallet.getKeystores().stream().allMatch(ks -> ks.getSource() == KeystoreSource.HW_USB)) {
|
||||
String fingerprint = masterWallet.getKeystores().get(0).getKeyDerivation().getMasterFingerprint();
|
||||
DeviceKeystoreDiscoverDialog deviceKeystoreDiscoverDialog = new DeviceKeystoreDiscoverDialog(List.of(fingerprint), masterWallet, standardAccounts);
|
||||
deviceKeystoreDiscoverDialog.initOwner(addAccount.getScene().getWindow());
|
||||
@ -827,9 +850,9 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
public void update(SettingsChangedEvent event) {
|
||||
Wallet wallet = event.getWallet();
|
||||
if(walletForm.getWallet().equals(wallet)) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI) {
|
||||
} else if(wallet.getPolicyType() == PolicyType.MULTI_HD) {
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(wallet.getPolicyType(), wallet.getScriptType(), wallet.getKeystores(), (int)multisigControl.getLowValue()));
|
||||
}
|
||||
|
||||
@ -1040,4 +1063,34 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
apply.setDisable(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static class PolicyTypeButtonCell extends ListCell<PolicyType> {
|
||||
@Override
|
||||
protected void updateItem(PolicyType policyType, boolean empty) {
|
||||
super.updateItem(policyType, empty);
|
||||
if(policyType == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(policyType.getName());
|
||||
setGraphic(null);
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PolicyTypeListCell extends ListCell<PolicyType> {
|
||||
@Override
|
||||
protected void updateItem(PolicyType policyType, boolean empty) {
|
||||
super.updateItem(policyType, empty);
|
||||
if(policyType == null || empty) {
|
||||
setText("");
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(policyType.getDescription());
|
||||
setGraphic(null);
|
||||
setGraphicTextGap(8.0d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
-fx-pref-width: 140px;
|
||||
}
|
||||
|
||||
#fingerprint, #derivation, #xpub {
|
||||
#fingerprint, #derivation, #xpub, #spScan {
|
||||
-fx-font-size: 13px;
|
||||
-fx-font-family: 'Fragment Mono Regular';
|
||||
}
|
||||
|
||||
@ -109,6 +109,9 @@
|
||||
</Button>
|
||||
</VBox>
|
||||
</Field>
|
||||
<Field fx:id="spScanField" text="spscan:">
|
||||
<TextArea fx:id="spScan" wrapText="true" prefRowCount="2" maxHeight="52" />
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
<StackPane fx:id="selectSourcePane">
|
||||
@ -141,9 +144,13 @@
|
||||
<KeystoreSource fx:constant="SW_SEED"/>
|
||||
</userData>
|
||||
</ToggleButton>
|
||||
<ToggleButton text="xPub / Watch Only Wallet" contentDisplay="TOP" wrapText="true" textAlignment="CENTER" toggleGroup="$keystoreSourceToggleGroup" onAction="#selectSource">
|
||||
<ToggleButton contentDisplay="CENTER" toggleGroup="$keystoreSourceToggleGroup" onAction="#selectSource">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="EYE" />
|
||||
<Label text="Watch Only Wallet" wrapText="true" textAlignment="CENTER" maxWidth="90" contentDisplay="TOP">
|
||||
<graphic>
|
||||
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="EYE" />
|
||||
</graphic>
|
||||
</Label>
|
||||
</graphic>
|
||||
<userData>
|
||||
<KeystoreSource fx:constant="SW_WATCH"/>
|
||||
|
||||
@ -32,12 +32,12 @@
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Settings" styleClass="header">
|
||||
<Field text="Policy Type:">
|
||||
<ComboBox fx:id="policyType">
|
||||
<ComboBox fx:id="policyType" prefWidth="160">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<PolicyType fx:constant="SINGLE" />
|
||||
<PolicyType fx:constant="MULTI" />
|
||||
<!-- <PolicyType fx:constant="CUSTOM" /> -->
|
||||
<PolicyType fx:constant="SINGLE_HD" />
|
||||
<PolicyType fx:constant="MULTI_HD" />
|
||||
<PolicyType fx:constant="SINGLE_SP" />
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user