Compare commits
No commits in common. "master" and "sp" have entirely different histories.
@ -20,7 +20,7 @@ if(System.getProperty("os.arch") == "aarch64") {
|
||||
def headless = "true".equals(System.getProperty("java.awt.headless"))
|
||||
|
||||
group = 'com.sparrowwallet'
|
||||
version = '2.5.3'
|
||||
version = '2.4.3'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
@ -104,12 +104,12 @@ dependencies {
|
||||
implementation('org.apache.commons:commons-lang3:3.20.0')
|
||||
implementation('org.apache.commons:commons-compress:1.28.0')
|
||||
implementation('com.github.librepdf:openpdf:1.3.43')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.5')
|
||||
implementation('com.googlecode.lanterna:lanterna:3.1.3')
|
||||
implementation('net.coobird:thumbnailator:0.4.21')
|
||||
implementation('com.github.hervegirod:fxsvgimage:1.1')
|
||||
implementation('com.sparrowwallet:toucan:0.9.0')
|
||||
implementation('com.jcraft:jzlib:1.1.3')
|
||||
implementation('io.github.doblon8:jzbar:0.4.0')
|
||||
implementation('io.github.doblon8:jzbar:0.3.1')
|
||||
testImplementation('org.junit.jupiter:junit-jupiter-api:5.14.1')
|
||||
testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine:5.14.1')
|
||||
testRuntimeOnly('org.junit.platform:junit-platform-launcher')
|
||||
|
||||
@ -56,7 +56,7 @@ sudo apt install -y rpm fakeroot binutils
|
||||
First, assign a temporary variable in your shell for the specific release you want to build. For the current one specify:
|
||||
|
||||
```shell
|
||||
GIT_TAG="2.5.2"
|
||||
GIT_TAG="2.4.2"
|
||||
```
|
||||
|
||||
The project can then be initially cloned as follows:
|
||||
|
||||
2
drongo
2
drongo
@ -1 +1 @@
|
||||
Subproject commit 077d2142cc3aad84f6f58868cf8f17fc61027fdc
|
||||
Subproject commit 29cd4b7909a2d891107af55b1d4d6c2690cfa2ab
|
||||
2
lark
2
lark
@ -1 +1 @@
|
||||
Subproject commit e9c6f35fe66aee105ef3c532fcefeb7130dab169
|
||||
Subproject commit 45e7a2f97eaf15fc31ef151194703f0f368e264c
|
||||
@ -21,7 +21,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>2.5.3</string>
|
||||
<string>2.4.3</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<!-- See https://developer.apple.com/app-store/categories/ for list of AppStore categories -->
|
||||
|
||||
@ -1328,16 +1328,13 @@ public class AppController implements Initializable {
|
||||
return;
|
||||
}
|
||||
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getPolicyType(), wallet.getBirthDate(), false);
|
||||
WalletNameDialog nameDlg = new WalletNameDialog(wallet.getName(), true, wallet.getBirthDate());
|
||||
nameDlg.initOwner(rootStack.getScene().getWindow());
|
||||
Optional<WalletNameDialog.NameAndBirthDate> optNameAndBirthDate = nameDlg.showAndWait();
|
||||
if(optNameAndBirthDate.isPresent()) {
|
||||
WalletNameDialog.NameAndBirthDate nameAndBirthDate = optNameAndBirthDate.get();
|
||||
wallet.setName(nameAndBirthDate.getName());
|
||||
wallet.setBirthDate(nameAndBirthDate.getBirthDate());
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP && wallet.getBirthDate() == null) {
|
||||
wallet.setBirthDate(new Date());
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
@ -1476,7 +1473,7 @@ public class AppController implements Initializable {
|
||||
WalletForm selectedWalletForm = getSelectedWalletForm();
|
||||
if(selectedWalletForm != null) {
|
||||
Wallet wallet = selectedWalletForm.getWallet();
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD || wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_HD) {
|
||||
//Can sign and verify
|
||||
messageSignDialog = new MessageSignDialog(wallet);
|
||||
}
|
||||
@ -1511,7 +1508,7 @@ public class AppController implements Initializable {
|
||||
bitcoinUnit = wallet.getAutoUnit();
|
||||
}
|
||||
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, Config.get().getUnitFormat(), initialPayments);
|
||||
sendToManyDialog = new SendToManyDialog(bitcoinUnit, initialPayments);
|
||||
sendToManyDialog.initModality(Modality.NONE);
|
||||
Optional<List<Payment>> optPayments = sendToManyDialog.showAndWait();
|
||||
sendToManyDialog = null;
|
||||
@ -2058,34 +2055,6 @@ public class AppController implements Initializable {
|
||||
AppServices.showErrorDialog("Invalid PSBT", e.getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
psbt.verifySigHashes();
|
||||
} catch(PSBTSignatureException e) {
|
||||
Optional<ButtonType> result = AppServices.showWarningDialog("Non-Default Sighash",
|
||||
e.getMessage() + "\n\nReview this PSBT carefully before signing.\n\nOpen the transaction?", ButtonType.YES, ButtonType.NO);
|
||||
if(result.isEmpty() || result.get() != ButtonType.YES) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Skip the warning for already-confirmed transactions loaded for inspection
|
||||
if(blockTransaction == null) {
|
||||
List<TransactionOutput> unknownScriptOutputs = new ArrayList<>();
|
||||
for(int i = 0; i < transaction.getOutputs().size(); i++) {
|
||||
TransactionOutput txOutput = transaction.getOutputs().get(i);
|
||||
if(txOutput.getValue() > 0 && txOutput.getScript().getToAddress() == null) {
|
||||
//Silent payment outputs have an empty script and non-zero value until the recipient script is computed
|
||||
if(psbt != null && i < psbt.getPsbtOutputs().size() && psbt.getPsbtOutputs().get(i).getSilentPaymentAddress() != null) {
|
||||
continue;
|
||||
}
|
||||
unknownScriptOutputs.add(txOutput);
|
||||
}
|
||||
}
|
||||
if(!unknownScriptOutputs.isEmpty() && !confirmUnknownScriptOutputs(unknownScriptOutputs)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -2192,23 +2161,6 @@ public class AppController implements Initializable {
|
||||
return result.isPresent() && result.get() == ButtonType.YES;
|
||||
}
|
||||
|
||||
private boolean confirmUnknownScriptOutputs(List<TransactionOutput> unknownScriptOutputs) {
|
||||
long totalAmount = unknownScriptOutputs.stream().mapToLong(TransactionOutput::getValue).sum();
|
||||
UnitFormat format = Config.get().getUnitFormat() == null ? UnitFormat.DOT : Config.get().getUnitFormat();
|
||||
BitcoinUnit unit = Config.get().getBitcoinUnit();
|
||||
if(unit == null || unit.equals(BitcoinUnit.AUTO)) {
|
||||
unit = totalAmount >= BitcoinUnit.getAutoThreshold() ? BitcoinUnit.BTC : BitcoinUnit.SATOSHIS;
|
||||
}
|
||||
String amount = unit.equals(BitcoinUnit.BTC) ? format.formatBtcValue(totalAmount) + " BTC" : format.formatSatsValue(totalAmount) + " sats";
|
||||
String outputDesc = unknownScriptOutputs.size() == 1 ? "an output" : unknownScriptOutputs.size() + " outputs";
|
||||
Optional<ButtonType> result = AppServices.showWarningDialog("Unknown Script Type",
|
||||
"This transaction contains " + outputDesc + " of a non-standard or unrecognised script type, totalling " + amount + ".\n\n" +
|
||||
"Sparrow cannot resolve these outputs to addresses, so they will not appear in the transaction diagram. " +
|
||||
"Review the individual output(s) in the transaction tree carefully before signing or broadcasting.\n\n" +
|
||||
"Open the transaction?", ButtonType.YES, ButtonType.NO);
|
||||
return result.isPresent() && result.get() == ButtonType.YES;
|
||||
}
|
||||
|
||||
private String getTabName(Tab tab) {
|
||||
return ((Label)tab.getGraphic()).getText();
|
||||
}
|
||||
|
||||
@ -69,11 +69,9 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppController.CONNECTION_FAILED_PREFIX;
|
||||
import static com.sparrowwallet.sparrow.control.DownloadVerifierDialog.*;
|
||||
|
||||
public class AppServices {
|
||||
@ -369,18 +367,15 @@ public class AppServices {
|
||||
onlineProperty.setValue(false);
|
||||
onlineProperty.addListener(onlineServicesListener);
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER) {
|
||||
boolean changed = changePublicServer();
|
||||
connectionService.setPeriod(changed ? Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS) : Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
if(!changed) {
|
||||
Platform.runLater(() -> EventManager.get().post(new StatusEvent(CONNECTION_FAILED_PREFIX + "No public servers available that can serve the open wallets, retrying later...")));
|
||||
}
|
||||
Config.get().changePublicServer();
|
||||
connectionService.setPeriod(Duration.seconds(PUBLIC_SERVER_RETRY_PERIOD_SECS));
|
||||
} else {
|
||||
connectionService.setPeriod(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
}
|
||||
|
||||
log.debug("Connection failed", failEvent.getSource().getException());
|
||||
EventManager.get().post(new ConnectionFailedEvent(failEvent.getSource().getException()));
|
||||
});
|
||||
|
||||
return connectionService;
|
||||
@ -871,22 +866,6 @@ public class AppServices {
|
||||
return Storage.isWalletFile(file);
|
||||
}
|
||||
|
||||
public boolean changePublicServer() {
|
||||
List<PolicyType> policyTypes = getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
return changePublicServer(policyTypes.isEmpty() ? List.of(PolicyType.SINGLE_HD) : policyTypes);
|
||||
}
|
||||
|
||||
private boolean changePublicServer(List<PolicyType> policyTypes) {
|
||||
Config config = Config.get();
|
||||
List<Server> otherServers = PublicElectrumServer.getServers().stream().filter(pes -> pes.supportsAllPolicyTypes(policyTypes))
|
||||
.map(PublicElectrumServer::getServer).filter(server -> !server.equals(config.getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
config.setPublicElectrumServer(otherServers.get(ThreadLocalRandom.current().nextInt(otherServers.size())));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Optional<ButtonType> showWarningDialog(String title, String content, ButtonType... buttons) {
|
||||
return showAlertDialog(title, content, Alert.AlertType.WARNING, buttons);
|
||||
}
|
||||
@ -1484,16 +1463,9 @@ public class AppServices {
|
||||
@Subscribe
|
||||
public void walletHistoryFailed(WalletHistoryFailedEvent event) {
|
||||
if(Config.get().getServerType() == ServerType.PUBLIC_ELECTRUM_SERVER && isConnected()) {
|
||||
String currentName = Config.get().getServerDisplayName();
|
||||
onlineProperty.set(false);
|
||||
boolean changed = changePublicServer();
|
||||
if(changed) {
|
||||
log.warn("Failed to fetch wallet history from " + currentName + ", reconnecting to another server...");
|
||||
} else {
|
||||
log.warn("Failed to fetch wallet history from " + currentName + ", retrying later");
|
||||
connectionService.setDelay(Duration.seconds(PRIVATE_SERVER_RETRY_PERIOD_SECS));
|
||||
EventManager.get().post(new StatusEvent("Wallet load failed: No other public servers available that can serve the open wallets, retrying later..."));
|
||||
}
|
||||
log.warn("Failed to fetch wallet history from " + Config.get().getServerDisplayName() + ", reconnecting to another server...");
|
||||
Config.get().changePublicServer();
|
||||
onlineProperty.set(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@ import java.util.*;
|
||||
public class SparrowWallet {
|
||||
public static final String APP_ID = "sparrow";
|
||||
public static final String APP_NAME = "Sparrow";
|
||||
public static final String APP_VERSION = "2.5.3";
|
||||
public static final String APP_VERSION = "2.4.3";
|
||||
public static final String APP_VERSION_SUFFIX = "";
|
||||
public static final String APP_HOME_PROPERTY = "sparrow.home";
|
||||
public static final String NETWORK_ENV_PROPERTY = "SPARROW_NETWORK";
|
||||
|
||||
@ -16,10 +16,7 @@ import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
|
||||
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.io.Storage;
|
||||
import com.sparrowwallet.sparrow.net.ElectrumServer;
|
||||
import com.sparrowwallet.sparrow.net.ServerType;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.Cormorant;
|
||||
import com.sparrowwallet.sparrow.net.cormorant.bitcoind.BitcoindClient;
|
||||
import com.sparrowwallet.sparrow.wallet.Entry;
|
||||
import io.reactivex.Observable;
|
||||
import io.reactivex.subjects.PublishSubject;
|
||||
@ -110,7 +107,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
|
||||
} else if(event.isLoading()) {
|
||||
if(event.getStatusMessage() != null) {
|
||||
setPlaceholder(new Label(event.getStatusMessage() + (event.getStatusMessage().contains("...") ? "" : "...")));
|
||||
setPlaceholder(new Label(event.getStatusMessage() + "..."));
|
||||
} else {
|
||||
setPlaceholder(new Label("Loading transactions..."));
|
||||
}
|
||||
@ -126,7 +123,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
StackPane stackPane = new StackPane();
|
||||
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
|
||||
|
||||
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting() && !isFullyScanned(wallet)) {
|
||||
if((Config.get().getServerType() == ServerType.BITCOIN_CORE || wallet.getPolicyType() == PolicyType.SINGLE_SP) && !AppServices.isConnecting()) {
|
||||
Hyperlink hyperlink = new Hyperlink();
|
||||
hyperlink.setTranslateY(30);
|
||||
hyperlink.setOnAction(event -> {
|
||||
@ -153,47 +150,12 @@ public class CoinTreeTable extends TreeTableView<Entry> {
|
||||
}
|
||||
|
||||
stackPane.getChildren().add(hyperlink);
|
||||
} else if(!AppServices.isConnecting() && Config.get().getServerType() == ServerType.BITCOIN_CORE && isFullyScanned(wallet)) {
|
||||
Date prunedDate = getPrunedDate();
|
||||
if(prunedDate != null) {
|
||||
DateFormat dateFormat = new SimpleDateFormat(DateStringConverter.FORMAT_PATTERN);
|
||||
Label prunedLabel = new Label("Scanned to pruned start date of " + dateFormat.format(prunedDate));
|
||||
prunedLabel.setTranslateY(30);
|
||||
stackPane.getChildren().add(prunedLabel);
|
||||
}
|
||||
}
|
||||
|
||||
stackPane.setAlignment(Pos.CENTER);
|
||||
return stackPane;
|
||||
}
|
||||
|
||||
private boolean isFullyScanned(Wallet wallet) {
|
||||
if(wallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return wallet.isValid() && ElectrumServer.isSilentPaymentsFullyCovered(wallet.getSilentPaymentScanAddress());
|
||||
}
|
||||
|
||||
if(Config.get().getServerType() == ServerType.BITCOIN_CORE) {
|
||||
Date prunedDate = getPrunedDate();
|
||||
return prunedDate != null && wallet.getBirthDate() != null && !wallet.getBirthDate().after(prunedDate);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Date getPrunedDate() {
|
||||
Cormorant cormorant = ElectrumServer.getCormorant();
|
||||
if(cormorant == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
BitcoindClient bitcoindClient = cormorant.getBitcoindClient();
|
||||
if(bitcoindClient == null || !bitcoindClient.isPruned()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return bitcoindClient.getCachedPrunedDate();
|
||||
}
|
||||
|
||||
protected void setupColumnSort(int defaultColumnIndex, TreeTableColumn.SortType defaultSortType) {
|
||||
WalletTable.Sort columnSort = getSavedColumnSort();
|
||||
if(columnSort == null) {
|
||||
|
||||
@ -11,7 +11,7 @@ import java.util.Date;
|
||||
public class DateAxisFormatter extends StringConverter<Number> {
|
||||
private static final DateFormat HOUR_FORMAT = new SimpleDateFormat("HH:mm");
|
||||
private static final DateFormat DAY_FORMAT = new SimpleDateFormat("d MMM");
|
||||
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yyyy");
|
||||
private static final DateFormat MONTH_FORMAT = new SimpleDateFormat("MMM yy");
|
||||
|
||||
private final DateFormat dateFormat;
|
||||
private int oddCounter;
|
||||
|
||||
@ -136,7 +136,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
HBox actionBox = new HBox();
|
||||
actionBox.getStyleClass().add("cell-actions");
|
||||
|
||||
if(!nodeEntry.getNode().getWallet().isBip47() && nodeEntry.getNode().getWallet().getPolicyType() != PolicyType.SINGLE_SP) {
|
||||
if(!nodeEntry.getNode().getWallet().isBip47()) {
|
||||
Button receiveButton = new Button("");
|
||||
receiveButton.setGraphic(getReceiveGlyph());
|
||||
receiveButton.setOnAction(event -> {
|
||||
@ -414,8 +414,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
private static boolean canSignMessage(WalletNode walletNode) {
|
||||
Wallet wallet = walletNode.getWallet();
|
||||
PolicyType policyType = wallet.getPolicyType();
|
||||
return (policyType == PolicyType.SINGLE_HD || policyType == PolicyType.SINGLE_SP) && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
return wallet.getPolicyType() == PolicyType.SINGLE_HD && (!wallet.isBip47() || walletNode.getKeyPurpose() == KeyPurpose.RECEIVE);
|
||||
}
|
||||
|
||||
private static boolean containsWalletOutputs(TransactionEntry transactionEntry) {
|
||||
@ -670,7 +669,7 @@ public class EntryCell extends TreeTableCell<Entry, Entry> implements Confirmati
|
||||
|
||||
public static class AddressContextMenu extends ContextMenu {
|
||||
public AddressContextMenu(Address address, String outputDescriptor, NodeEntry nodeEntry, boolean addUtxoItems, TreeTableView<Entry> treetable) {
|
||||
if(nodeEntry == null || (!nodeEntry.getWallet().isBip47() && nodeEntry.getWallet().getPolicyType() != PolicyType.SINGLE_SP)) {
|
||||
if(nodeEntry == null || !nodeEntry.getWallet().isBip47()) {
|
||||
MenuItem receiveToAddress = new MenuItem("Receive To");
|
||||
receiveToAddress.setGraphic(getReceiveGlyph());
|
||||
receiveToAddress.setOnAction(event -> {
|
||||
|
||||
@ -160,17 +160,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
signature.setStyle("-fx-pref-height: 80px");
|
||||
signature.setWrapText(true);
|
||||
signature.setOnMouseClicked(event -> signature.selectAll());
|
||||
|
||||
ContextMenu signatureMenu = new ContextMenu();
|
||||
MenuItem copyItem = new MenuItem("Copy");
|
||||
copyItem.setOnAction(e -> signature.copy());
|
||||
MenuItem pasteItem = new MenuItem("Paste");
|
||||
pasteItem.setOnAction(e -> signature.paste());
|
||||
MenuItem clearItem = new MenuItem("Clear");
|
||||
clearItem.setOnAction(e -> signature.clear());
|
||||
signatureMenu.getItems().addAll(copyItem, pasteItem, clearItem);
|
||||
signature.setContextMenu(signatureMenu);
|
||||
|
||||
signatureField.getInputs().add(signature);
|
||||
|
||||
Field formatField = new Field();
|
||||
@ -306,8 +295,8 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
|
||||
private void checkWalletSigning(Wallet wallet) {
|
||||
if(wallet.getKeystores().size() != 1 || (wallet.getPolicyType() != PolicyType.SINGLE_HD && wallet.getPolicyType() != PolicyType.SINGLE_SP)) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using this wallet type");
|
||||
if(wallet.getKeystores().size() != 1 || wallet.getPolicyType() != PolicyType.SINGLE_HD) {
|
||||
throw new IllegalArgumentException("Cannot sign messages using a non-HD wallet or a wallet with multiple keystores");
|
||||
}
|
||||
}
|
||||
|
||||
@ -388,24 +377,18 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
private void signUnencryptedKeystore(Wallet decryptedWallet) {
|
||||
try {
|
||||
Keystore keystore = decryptedWallet.getKeystores().getFirst();
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
String signatureText;
|
||||
if(decryptedWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
ECKey spendPrivKey = keystore.getSpendPrivateKey(Collections.emptyMap());
|
||||
signatureText = Bip322.signMessageBip322Sp(walletNode.getAddress(), message.getText().trim(), spendPrivKey, walletNode.getSilentPaymentTweak());
|
||||
spendPrivKey.clear();
|
||||
if(isBip322()) {
|
||||
ScriptType scriptType = decryptedWallet.getScriptType();
|
||||
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
|
||||
} else {
|
||||
ECKey privKey = keystore.getKey(walletNode);
|
||||
if(isBip322()) {
|
||||
ScriptType scriptType = decryptedWallet.getScriptType();
|
||||
signatureText = Bip322.signMessageBip322(scriptType, message.getText().trim(), privKey);
|
||||
} else {
|
||||
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
}
|
||||
privKey.clear();
|
||||
ScriptType scriptType = isElectrumSignatureFormat() ? ScriptType.P2PKH : decryptedWallet.getScriptType();
|
||||
signatureText = privKey.signMessage(message.getText().trim(), scriptType);
|
||||
}
|
||||
signature.clear();
|
||||
signature.appendText(signatureText);
|
||||
privKey.clear();
|
||||
} catch(Exception e) {
|
||||
log.error("Could not sign message", e);
|
||||
AppServices.showErrorDialog("Could not sign message", e.getMessage());
|
||||
@ -515,7 +498,10 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private void showBip322Qr() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
byte[] psbtBytes = psbt.getForExport().serialize();
|
||||
CryptoPSBT cryptoPSBT = new CryptoPSBT(psbtBytes);
|
||||
@ -528,40 +514,6 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
}
|
||||
}
|
||||
|
||||
private PSBT buildBip322Psbt(Wallet signingWallet) {
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
Keystore keystore = signingWallet.getKeystores().getFirst();
|
||||
ECKey spendPubKey = keystore.getSilentPaymentScanAddress().getSpendKey();
|
||||
KeyDerivation spendDerivation = new KeyDerivation(keystore.getKeyDerivation().getMasterFingerprint(), KeyDerivation.writePath(KeyDerivation.getBip352SpendDerivation(keystore.getKeyDerivation().getDerivation())));
|
||||
return Bip322.getBip322PsbtSp(walletNode.getAddress(), message.getText().trim(), walletNode.getSilentPaymentTweak(), Map.of(spendPubKey, spendDerivation));
|
||||
}
|
||||
|
||||
PSBT psbt = Bip322.getBip322Psbt(signingWallet.getScriptType(), walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
return psbt;
|
||||
}
|
||||
|
||||
private String extractBip322Signature(PSBT signedPsbt) {
|
||||
String psbtMessage = signedPsbt.getGenericSignedMessage();
|
||||
if(psbtMessage != null && !psbtMessage.equals(message.getText().trim())) {
|
||||
Optional<ButtonType> response = AppServices.showWarningDialog("Message mismatch",
|
||||
"The message in the signed PSBT does not match the message in this dialog.\n\nPSBT message: " + psbtMessage +
|
||||
"\n\nContinue extracting the signature?", ButtonType.NO, ButtonType.YES);
|
||||
if(response.isEmpty() || response.get() != ButtonType.YES) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
if(signingWallet.getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
return Bip322.getBip322SignatureFromPsbtSp(signedPsbt);
|
||||
}
|
||||
|
||||
ECKey pubKey = signingWallet.getKeystores().getFirst().getPubKey(walletNode);
|
||||
return Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), signedPsbt, pubKey);
|
||||
}
|
||||
|
||||
private void addBip322DerivationInfo(PSBT psbt, Wallet signingWallet) {
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBTInput psbtInput = psbt.getPsbtInputs().get(0);
|
||||
@ -585,11 +537,11 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
QRScanDialog.Result result = optionalResult.get();
|
||||
if(result.psbt != null) {
|
||||
try {
|
||||
String sig = extractBip322Signature(result.psbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
ECKey pubKey = signingWallet.getKeystores().get(0).getPubKey(walletNode);
|
||||
String sig = Bip322.getBip322SignatureFromPsbt(signingWallet.getScriptType(), result.psbt, pubKey);
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
} catch(Exception e) {
|
||||
log.error("Error extracting BIP-322 signature from PSBT", e);
|
||||
AppServices.showErrorDialog("Error extracting signature", e.getMessage());
|
||||
@ -647,7 +599,9 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
|
||||
private void exportBip322File() {
|
||||
Wallet signingWallet = walletNode.getWallet();
|
||||
PSBT psbt = buildBip322Psbt(signingWallet);
|
||||
ScriptType scriptType = signingWallet.getScriptType();
|
||||
PSBT psbt = Bip322.getBip322Psbt(scriptType, walletNode.getAddress(), message.getText().trim());
|
||||
addBip322DerivationInfo(psbt, signingWallet);
|
||||
|
||||
Stage window = new Stage();
|
||||
FileChooser fileChooser = new FileChooser();
|
||||
@ -688,11 +642,10 @@ public class MessageSignDialog extends Dialog<ButtonBar.ButtonData> {
|
||||
try {
|
||||
byte[] psbtBytes = Files.readAllBytes(file.toPath());
|
||||
PSBT signedPsbt = new PSBT(psbtBytes, false);
|
||||
String sig = extractBip322Signature(signedPsbt);
|
||||
if(sig != null) {
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
}
|
||||
ECKey pubKey = walletNode.getWallet().getKeystores().get(0).getPubKey(walletNode);
|
||||
String sig = Bip322.getBip322SignatureFromPsbt(walletNode.getWallet().getScriptType(), signedPsbt, pubKey);
|
||||
signature.clear();
|
||||
signature.appendText(sig);
|
||||
return;
|
||||
} catch(Exception e) {
|
||||
if(file.getName().toLowerCase(Locale.ROOT).endsWith(".psbt")) {
|
||||
|
||||
@ -9,13 +9,13 @@ import com.sparrowwallet.drongo.dns.DnsPayment;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentCache;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentResolver;
|
||||
import com.sparrowwallet.drongo.dns.DnsPaymentValidationException;
|
||||
import com.sparrowwallet.drongo.protocol.Transaction;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPayment;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.uri.BitcoinURIParseException;
|
||||
import com.sparrowwallet.drongo.wallet.Payment;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.UnitFormat;
|
||||
import com.sparrowwallet.sparrow.event.RequestConnectEvent;
|
||||
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@ -28,8 +28,6 @@ import javafx.event.ActionEvent;
|
||||
import javafx.scene.Node;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.input.Clipboard;
|
||||
import javafx.scene.input.KeyCode;
|
||||
import javafx.scene.input.KeyEvent;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.FileChooser;
|
||||
import javafx.util.StringConverter;
|
||||
@ -37,26 +35,20 @@ import org.controlsfx.control.spreadsheet.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
private final BitcoinUnit bitcoinUnit;
|
||||
private final UnitFormat unitFormat;
|
||||
private final UnitFormatDoubleCellType amountCellType;
|
||||
private final SpreadsheetView spreadsheetView;
|
||||
public static final SendToAddressCellType SEND_TO_ADDRESS = new SendToAddressCellType();
|
||||
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, UnitFormat unitFormat, List<Payment> payments) {
|
||||
public SendToManyDialog(BitcoinUnit bitcoinUnit, List<Payment> payments) {
|
||||
this.bitcoinUnit = bitcoinUnit;
|
||||
this.unitFormat = unitFormat == null ? UnitFormat.DOT : unitFormat;
|
||||
this.amountCellType = new UnitFormatDoubleCellType(this.unitFormat);
|
||||
|
||||
final DialogPane dialogPane = new SendToManyDialogPane();
|
||||
setDialogPane(dialogPane);
|
||||
@ -127,9 +119,11 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
addressCell.getStyleClass().add("fixed-width");
|
||||
list.add(addressCell);
|
||||
|
||||
long rawAmount = sendToPayment.payment().getAmount();
|
||||
Double amount = rawAmount < 0 ? null : bitcoinUnit.getValue(rawAmount);
|
||||
SpreadsheetCell amountCell = amountCellType.createCell(row, 1, 1, 1, amount);
|
||||
double amount = (double)sendToPayment.payment().getAmount();
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
amount = amount / Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
SpreadsheetCell amountCell = SpreadsheetCellType.DOUBLE.createCell(row, 1, 1, 1, amount < 0 ? null : amount);
|
||||
amountCell.setFormat(bitcoinUnit == BitcoinUnit.BTC ? "0.00000000" : "###,###");
|
||||
amountCell.getStyleClass().add("number-value");
|
||||
if(OsType.getCurrent() == OsType.MACOS) {
|
||||
@ -183,7 +177,7 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
for(int row = 0; row < spreadsheetView.getGrid().getRowCount(); row++) {
|
||||
ObservableList<SpreadsheetCell> rowCells = spreadsheetView.getItems().get(row);
|
||||
SendToAddress sendToAddress = (SendToAddress)rowCells.getFirst().getItem();
|
||||
if(sendToAddress != null && sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||
if(sendToAddress.hrn != null && DnsPaymentCache.getDnsPayment(sendToAddress.hrn) == null) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@ -222,15 +216,12 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
try {
|
||||
String rawAmount = csvReader.get(1).trim();
|
||||
String groupingStripped = rawAmount.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "");
|
||||
long amount;
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
String normalised = groupingStripped.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
|
||||
double doubleAmount = Double.parseDouble(normalised);
|
||||
amount = bitcoinUnit.getSatsValue(doubleAmount);
|
||||
double doubleAmount = Double.parseDouble(csvReader.get(1).replace(",", ""));
|
||||
amount = (long)(doubleAmount * Transaction.SATOSHIS_PER_BITCOIN);
|
||||
} else {
|
||||
amount = Long.parseLong(groupingStripped);
|
||||
amount = Long.parseLong(csvReader.get(1).replace(",", ""));
|
||||
}
|
||||
String label = csvReader.get(2);
|
||||
Optional<String> optDnsPaymentHrn = DnsPayment.getHrn(csvReader.get(0));
|
||||
@ -368,160 +359,6 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
};
|
||||
|
||||
private static class UnitFormatDoubleCellType extends SpreadsheetCellType<Double> {
|
||||
private final UnitFormat unitFormat;
|
||||
|
||||
UnitFormatDoubleCellType(UnitFormat unitFormat) {
|
||||
super(new UnitFormatDoubleConverter(unitFormat));
|
||||
this.unitFormat = unitFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "double";
|
||||
}
|
||||
|
||||
public SpreadsheetCell createCell(int row, int column, int rowSpan, int columnSpan, Double value) {
|
||||
SpreadsheetCell cell = new SpreadsheetCellBase(row, column, rowSpan, columnSpan, this);
|
||||
cell.setItem(value);
|
||||
return cell;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SpreadsheetCellEditor createEditor(SpreadsheetView view) {
|
||||
return new UnitFormatDoubleEditor(view, unitFormat);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(Object value, Object... options) {
|
||||
if(value == null || value instanceof Number) {
|
||||
return true;
|
||||
}
|
||||
try {
|
||||
String s = value.toString();
|
||||
return s == null || s.isEmpty() || converter.fromString(s) != null;
|
||||
} catch(Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double convertValue(Object value) {
|
||||
if(value instanceof Double d) {
|
||||
return d;
|
||||
}
|
||||
if(value instanceof Number n) {
|
||||
return n.doubleValue();
|
||||
}
|
||||
return converter.fromString(value == null ? null : value.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Double item) {
|
||||
return converter.toString(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Double item, String format) {
|
||||
return ((StringConverterWithFormat<Double>)converter).toStringFormat(item, format);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitFormatDoubleConverter extends StringConverterWithFormat<Double> {
|
||||
private final UnitFormat unitFormat;
|
||||
|
||||
UnitFormatDoubleConverter(UnitFormat unitFormat) {
|
||||
this.unitFormat = unitFormat;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double fromString(String str) {
|
||||
if(str == null || str.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
String normalised = str.trim()
|
||||
.replaceAll(Pattern.quote(unitFormat.getGroupingSeparator()), "")
|
||||
.replaceAll(Pattern.quote(unitFormat.getDecimalSeparator()), ".");
|
||||
try {
|
||||
return Double.valueOf(normalised);
|
||||
} catch(NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString(Double item) {
|
||||
return toStringFormat(item, "");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toStringFormat(Double item, String format) {
|
||||
if(item == null || item.isNaN()) {
|
||||
return "";
|
||||
}
|
||||
if(format == null || format.isEmpty()) {
|
||||
return Double.toString(item);
|
||||
}
|
||||
return new DecimalFormat(format, unitFormat.getDecimalFormatSymbols()).format(item);
|
||||
}
|
||||
}
|
||||
|
||||
private static class UnitFormatDoubleEditor extends SpreadsheetCellEditor {
|
||||
private final UnitFormat unitFormat;
|
||||
private final TextField textField;
|
||||
|
||||
UnitFormatDoubleEditor(SpreadsheetView view, UnitFormat unitFormat) {
|
||||
super(view);
|
||||
this.unitFormat = unitFormat;
|
||||
this.textField = new TextField();
|
||||
this.textField.setTextFormatter(new CoinTextFormatter(unitFormat));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void startEdit(Object item, String format, Object... options) {
|
||||
if(item instanceof Double d && !d.isNaN()) {
|
||||
String text = (format == null || format.isEmpty())
|
||||
? Double.toString(d)
|
||||
: new DecimalFormat(format, unitFormat.getDecimalFormatSymbols()).format(d);
|
||||
textField.setText(text);
|
||||
} else {
|
||||
textField.setText("");
|
||||
}
|
||||
textField.getStyleClass().removeAll("error");
|
||||
textField.setOnKeyPressed(this::onKeyPressed);
|
||||
textField.requestFocus();
|
||||
textField.selectAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
textField.setOnKeyPressed(null);
|
||||
textField.setOnKeyReleased(null);
|
||||
textField.getStyleClass().removeAll("error");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TextField getEditor() {
|
||||
return textField;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getControlValue() {
|
||||
String raw = textField.getText();
|
||||
return raw == null ? "" : raw.trim();
|
||||
}
|
||||
|
||||
private void onKeyPressed(KeyEvent event) {
|
||||
if(event.getCode() == KeyCode.ENTER) {
|
||||
endEdit(true);
|
||||
event.consume();
|
||||
} else if(event.getCode() == KeyCode.ESCAPE) {
|
||||
endEdit(false);
|
||||
event.consume();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class SendToAddress {
|
||||
private final String hrn;
|
||||
private final Address address;
|
||||
@ -650,7 +487,11 @@ public class SendToManyDialog extends Dialog<List<Payment>> {
|
||||
}
|
||||
|
||||
if(sendToAddress != null && value != null) {
|
||||
payments.add(sendToAddress.toPayment(label, bitcoinUnit.getSatsValue(value), false));
|
||||
if(bitcoinUnit == BitcoinUnit.BTC) {
|
||||
value = value * Transaction.SATOSHIS_PER_BITCOIN;
|
||||
}
|
||||
|
||||
payments.add(sendToAddress.toPayment(label, value.longValue(), false));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@ -49,13 +48,9 @@ public class WalletNameDialog extends Dialog<WalletNameDialog.NameAndBirthDate>
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName, boolean hasExistingTransactions, Date startDate, boolean rename) {
|
||||
this(initialName, hasExistingTransactions, null, startDate, rename);
|
||||
}
|
||||
|
||||
public WalletNameDialog(String initialName, boolean hasExistingTransactions, PolicyType walletPolicyType, Date startDate, boolean rename) {
|
||||
final DialogPane dialogPane = getDialogPane();
|
||||
AppServices.setStageIcon(dialogPane.getScene().getWindow());
|
||||
boolean requestBirthDate = !rename && (walletPolicyType == PolicyType.SINGLE_SP || Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
|
||||
boolean requestBirthDate = !rename && (Config.get().getServerType() == null || Config.get().getServerType() == ServerType.BITCOIN_CORE);
|
||||
|
||||
setTitle("Wallet Name");
|
||||
dialogPane.setHeaderText("Enter a name for this wallet:");
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.event;
|
||||
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
|
||||
public class WalletSilentPaymentAddressesChangedEvent extends WalletChangedEvent {
|
||||
public WalletSilentPaymentAddressesChangedEvent(Wallet wallet) {
|
||||
super(wallet.resolveMasterWallet());
|
||||
}
|
||||
}
|
||||
@ -18,12 +18,12 @@ import org.slf4j.LoggerFactory;
|
||||
import java.io.*;
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.sparrowwallet.sparrow.AppServices.ENUMERATE_HW_PERIOD_SECS;
|
||||
import static com.sparrowwallet.sparrow.net.PagedBatchRequestBuilder.DEFAULT_PAGE_SIZE;
|
||||
import static com.sparrowwallet.sparrow.net.TcpTransport.DEFAULT_MAX_TIMEOUT;
|
||||
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SATS;
|
||||
import static com.sparrowwallet.sparrow.wallet.WalletUtxosEntry.DUST_ATTACK_THRESHOLD_SP_SATS;
|
||||
|
||||
public class Config {
|
||||
private static final Logger log = LoggerFactory.getLogger(Config.class);
|
||||
@ -64,7 +64,6 @@ public class Config {
|
||||
private List<File> recentWalletFiles;
|
||||
private Integer keyDerivationPeriod;
|
||||
private long dustAttackThreshold = DUST_ATTACK_THRESHOLD_SATS;
|
||||
private long dustAttackThresholdSp = DUST_ATTACK_THRESHOLD_SP_SATS;
|
||||
private int enumerateHwPeriod = ENUMERATE_HW_PERIOD_SECS;
|
||||
private QRDensity qrDensity;
|
||||
private QREncoding qrEncoding;
|
||||
@ -449,10 +448,6 @@ public class Config {
|
||||
return dustAttackThreshold;
|
||||
}
|
||||
|
||||
public long getDustAttackThresholdSp() {
|
||||
return dustAttackThresholdSp;
|
||||
}
|
||||
|
||||
public int getEnumerateHwPeriod() {
|
||||
return enumerateHwPeriod;
|
||||
}
|
||||
@ -561,6 +556,13 @@ public class Config {
|
||||
flush();
|
||||
}
|
||||
|
||||
public void changePublicServer() {
|
||||
List<Server> otherServers = PublicElectrumServer.getServers().stream().map(PublicElectrumServer::getServer).filter(server -> !server.equals(getPublicElectrumServer())).collect(Collectors.toList());
|
||||
if(!otherServers.isEmpty()) {
|
||||
setPublicElectrumServer(otherServers.get(new Random().nextInt(otherServers.size())));
|
||||
}
|
||||
}
|
||||
|
||||
public Server getCoreServer() {
|
||||
return coreServer;
|
||||
}
|
||||
|
||||
@ -337,11 +337,6 @@ public class DbPersistence implements Persistence {
|
||||
walletConfigDao.addOrUpdate(wallet, wallet.getWalletConfig());
|
||||
}
|
||||
|
||||
if(dirtyPersistables.silentPaymentAddresses) {
|
||||
SilentPaymentAddressDao silentPaymentAddressDao = handle.attach(SilentPaymentAddressDao.class);
|
||||
silentPaymentAddressDao.clearAndAddAll(wallet);
|
||||
}
|
||||
|
||||
if(dirtyPersistables.walletTable != null) {
|
||||
WalletTableDao walletTableDao = handle.attach(WalletTableDao.class);
|
||||
walletTableDao.addOrUpdate(wallet, dirtyPersistables.walletTable.getTableType(), dirtyPersistables.walletTable);
|
||||
@ -893,13 +888,6 @@ public class DbPersistence implements Persistence {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) {
|
||||
if(persistsFor(event.getWallet())) {
|
||||
updateExecutor.execute(() -> dirtyPersistablesMap.computeIfAbsent(event.getWallet(), key -> new DirtyPersistables()).silentPaymentAddresses = true);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DirtyPersistables {
|
||||
public boolean deleteAccount;
|
||||
public boolean clearHistory;
|
||||
@ -918,7 +906,6 @@ public class DbPersistence implements Persistence {
|
||||
public final List<Keystore> labelKeystores = new ArrayList<>();
|
||||
public final List<Keystore> encryptionKeystores = new ArrayList<>();
|
||||
public final List<Keystore> registrationKeystores = new ArrayList<>();
|
||||
public boolean silentPaymentAddresses;
|
||||
|
||||
public String toString() {
|
||||
return "Dirty Persistables" +
|
||||
@ -940,8 +927,7 @@ public class DbPersistence implements Persistence {
|
||||
"\nUTXO mixes removed:" + removedUtxoMixes +
|
||||
"\nKeystore labels:" + labelKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
|
||||
"\nKeystore encryptions:" + encryptionKeystores.stream().map(Keystore::getLabel).collect(Collectors.toList()) +
|
||||
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList()) +
|
||||
"\nSilent payment addresses:" + silentPaymentAddresses;
|
||||
"\nKeystore registrations:" + registrationKeystores.stream().map(Keystore::getDeviceRegistration).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,40 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import org.jdbi.v3.sqlobject.config.RegisterRowMapper;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlBatch;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlQuery;
|
||||
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface SilentPaymentAddressDao {
|
||||
@SqlQuery("select address, silentPaymentAddress from silentPaymentAddress")
|
||||
@RegisterRowMapper(SilentPaymentAddressMapper.class)
|
||||
Map<Address, SilentPaymentAddress> getAll();
|
||||
|
||||
@SqlBatch("insert into silentPaymentAddress (address, silentPaymentAddress) values (?, ?)")
|
||||
void insertSilentPaymentAddresses(List<byte[]> addresses, List<byte[]> silentPaymentAddresses);
|
||||
|
||||
@SqlUpdate("delete from silentPaymentAddress")
|
||||
void clear();
|
||||
|
||||
default void clearAndAddAll(Wallet wallet) {
|
||||
clear();
|
||||
|
||||
List<byte[]> addresses = new ArrayList<>();
|
||||
List<byte[]> silentPaymentAddresses = new ArrayList<>();
|
||||
for(Map.Entry<Address, SilentPaymentAddress> entry : wallet.getSilentPaymentAddresses().entrySet()) {
|
||||
addresses.add(entry.getKey().getData());
|
||||
silentPaymentAddresses.add(entry.getValue().serialize());
|
||||
}
|
||||
|
||||
if(!addresses.isEmpty()) {
|
||||
insertSilentPaymentAddresses(addresses, silentPaymentAddresses);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.io.db;
|
||||
|
||||
import com.sparrowwallet.drongo.address.Address;
|
||||
import com.sparrowwallet.drongo.address.P2TRAddress;
|
||||
import com.sparrowwallet.drongo.silentpayments.SilentPaymentAddress;
|
||||
import org.jdbi.v3.core.mapper.RowMapper;
|
||||
import org.jdbi.v3.core.statement.StatementContext;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Map;
|
||||
|
||||
public class SilentPaymentAddressMapper implements RowMapper<Map.Entry<Address, SilentPaymentAddress>> {
|
||||
@Override
|
||||
public Map.Entry<Address, SilentPaymentAddress> map(ResultSet rs, StatementContext ctx) throws SQLException {
|
||||
Address address = new P2TRAddress(rs.getBytes("address"));
|
||||
SilentPaymentAddress silentPaymentAddress = SilentPaymentAddress.fromBytes(rs.getBytes("silentPaymentAddress"));
|
||||
|
||||
return new Map.Entry<>() {
|
||||
@Override
|
||||
public Address getKey() {
|
||||
return address;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SilentPaymentAddress getValue() {
|
||||
return silentPaymentAddress;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SilentPaymentAddress setValue(SilentPaymentAddress value) {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -30,9 +30,6 @@ public interface WalletDao {
|
||||
@CreateSqlObject
|
||||
DetachedLabelDao createDetachedLabelDao();
|
||||
|
||||
@CreateSqlObject
|
||||
SilentPaymentAddressDao createSilentPaymentAddressDao();
|
||||
|
||||
@CreateSqlObject
|
||||
WalletConfigDao createWalletConfigDao();
|
||||
|
||||
@ -124,8 +121,6 @@ public interface WalletDao {
|
||||
Map<String, String> detachedLabels = createDetachedLabelDao().getAll();
|
||||
wallet.getDetachedLabels().putAll(detachedLabels);
|
||||
|
||||
wallet.getSilentPaymentAddresses().putAll(createSilentPaymentAddressDao().getAll());
|
||||
|
||||
wallet.setWalletConfig(createWalletConfigDao().getForWalletId(wallet.getId()));
|
||||
|
||||
Map<TableType, WalletTable> walletTables = createWalletTableDao().getForWalletId(wallet.getId());
|
||||
@ -149,7 +144,6 @@ public interface WalletDao {
|
||||
createWalletNodeDao().addWalletNodes(wallet);
|
||||
createBlockTransactionDao().addBlockTransactions(wallet);
|
||||
createDetachedLabelDao().clearAndAddAll(wallet);
|
||||
createSilentPaymentAddressDao().clearAndAddAll(wallet);
|
||||
createWalletConfigDao().addWalletConfig(wallet);
|
||||
createWalletTableDao().addWalletTables(wallet);
|
||||
createMixConfigDao().addMixConfig(wallet);
|
||||
|
||||
@ -4,7 +4,6 @@ import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.sparrowwallet.drongo.ExtendedKey;
|
||||
import com.sparrowwallet.drongo.KeyPurpose;
|
||||
import com.sparrowwallet.drongo.Utils;
|
||||
import com.sparrowwallet.drongo.crypto.ChildNumber;
|
||||
import com.sparrowwallet.drongo.crypto.ECKey;
|
||||
import com.sparrowwallet.drongo.protocol.ScriptType;
|
||||
@ -70,17 +69,19 @@ public class Auth47 {
|
||||
this.srbnName = srbnUrl.getUserInfo();
|
||||
this.callback = new URI(HTTPS_PROTOCOL + srbnUrl.getHost()).toURL();
|
||||
} else {
|
||||
URI callbackUri = new URI(strCallback);
|
||||
if(!Utils.isSecureUrl(callbackUri)) {
|
||||
throw new IllegalArgumentException("Invalid callback parameter (not https, http .onion or srbn): " + strCallback);
|
||||
}
|
||||
this.callback = callbackUri.toURL();
|
||||
this.callback = new URI(strCallback).toURL();
|
||||
}
|
||||
|
||||
this.expiry = parameterMap.get("e");
|
||||
this.resource = parameterMap.get("r");
|
||||
if(resource == null) {
|
||||
this.resource = srbn ? "srbn" : strCallback;
|
||||
if(srbn) {
|
||||
this.resource = "srbn";
|
||||
} else if(strCallback.startsWith("http")) {
|
||||
this.resource = strCallback;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid callback parameter (not http/s or srbn): " + strCallback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -89,8 +89,6 @@ public class ElectrumServer {
|
||||
|
||||
private static final Map<String, SilentPaymentsScanCache> spScanCaches = new ConcurrentHashMap<>();
|
||||
|
||||
private static final int TAPROOT_ACTIVATION_HEIGHT = 709632;
|
||||
|
||||
private final static Map<String, Integer> subscribedRecent = new ConcurrentHashMap<>();
|
||||
|
||||
private final static Map<String, String> broadcastRecent = new ConcurrentHashMap<>();
|
||||
@ -877,11 +875,6 @@ public class ElectrumServer {
|
||||
|
||||
for(BlockTransactionHash reference : references.keySet()) {
|
||||
Transaction transaction = references.get(reference);
|
||||
if(transaction == null) {
|
||||
transactionMap.put(reference.getHash(), UNFETCHABLE_BLOCK_TRANSACTION);
|
||||
checkReferences.removeIf(ref -> ref.getHash().equals(reference.getHash()));
|
||||
continue;
|
||||
}
|
||||
|
||||
Date blockDate = null;
|
||||
if(reference.getHeight() > 0) {
|
||||
@ -895,7 +888,7 @@ public class ElectrumServer {
|
||||
}
|
||||
|
||||
Long fee = reference.getFee();
|
||||
if(fee == null && wallet != null) {
|
||||
if(fee == null) {
|
||||
BlockTransaction cached = wallet.getWalletTransaction(reference.getHash());
|
||||
if(cached != null && cached.getFee() != null) {
|
||||
fee = cached.getFee();
|
||||
@ -1384,33 +1377,6 @@ public class ElectrumServer {
|
||||
return spScanCaches.containsKey(scanAddress.getAddress());
|
||||
}
|
||||
|
||||
public static boolean isSilentPaymentsFullyCovered(SilentPaymentScanAddress scanAddress) {
|
||||
if(scanAddress == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SilentPaymentsScanCache cache = spScanCaches.get(scanAddress.getAddress());
|
||||
if(cache == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
cache.lock();
|
||||
try {
|
||||
Integer serverStart = cache.getServerStart();
|
||||
if(!cache.isCompleted() || serverStart == null) {
|
||||
return false;
|
||||
}
|
||||
int earliestPossibleStart = Network.get() == Network.MAINNET ? TAPROOT_ACTIVATION_HEIGHT : 0;
|
||||
return serverStart <= earliestPossibleStart;
|
||||
} finally {
|
||||
cache.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static Cormorant getCormorant() {
|
||||
return cormorant;
|
||||
}
|
||||
|
||||
private static void cancelSilentPaymentScans() {
|
||||
for(SilentPaymentsScanCache cache : spScanCaches.values()) {
|
||||
cache.lock();
|
||||
@ -1534,7 +1500,7 @@ public class ElectrumServer {
|
||||
}
|
||||
}
|
||||
if(cache.isCancelled()) {
|
||||
throw new ServerException("Silent payments scan was cancelled for " + spAddress.substring(0, 10) + "...");
|
||||
throw new ServerException("Silent payments scan was cancelled for " + spAddress);
|
||||
}
|
||||
return cache.snapshotEntries();
|
||||
} finally {
|
||||
|
||||
@ -41,17 +41,9 @@ public class LnurlAuth {
|
||||
public LnurlAuth(URI uri) throws MalformedURLException, URISyntaxException {
|
||||
String lnurl = uri.getSchemeSpecificPart();
|
||||
Bech32.Bech32Data bech32 = Bech32.decode(lnurl, 2000);
|
||||
if(!"lnurl".equals(bech32.hrp)) {
|
||||
throw new IllegalArgumentException("LNURL-auth bech32 prefix must be lnurl");
|
||||
}
|
||||
|
||||
byte[] urlBytes = Bech32.convertBits(bech32.data, 0, bech32.data.length, 5, 8, false);
|
||||
String strUrl = new String(urlBytes, StandardCharsets.UTF_8);
|
||||
URI decodedUri = new URI(strUrl);
|
||||
if(!Utils.isSecureUrl(decodedUri)) {
|
||||
throw new IllegalArgumentException("LNURL-auth URL must be https or http .onion");
|
||||
}
|
||||
this.url = decodedUri.toURL();
|
||||
this.url = new URI(strUrl).toURL();
|
||||
|
||||
Map<String, String> parameterMap = new LinkedHashMap<>();
|
||||
String query = url.getQuery();
|
||||
|
||||
@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.sparrow.io.Server;
|
||||
|
||||
import java.util.Arrays;
|
||||
@ -21,24 +20,17 @@ public enum PublicElectrumServer {
|
||||
TESTNET_QTORNADO_COM("testnet.qtornado.com", "ssl://testnet.qtornado.com:51002", Network.TESTNET),
|
||||
SIGNET_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:60602", Network.SIGNET),
|
||||
TESTNET4_MEMPOOL_SPACE("mempool.space", "ssl://mempool.space:40002", Network.TESTNET4),
|
||||
TESTNET4_C3_SOFT("blackie.c3-soft.com", "ssl://blackie.c3-soft.com:57010", Network.TESTNET4),
|
||||
FRIGATE_2140_DEV("frigate.2140.dev", "ssl://frigate.2140.dev:50002", Network.MAINNET, List.of(PolicyType.SINGLE_HD, PolicyType.MULTI_HD, PolicyType.SINGLE_SP));
|
||||
TESTNET4_C3_SOFT("blackie.c3-soft.com", "ssl://blackie.c3-soft.com:57010", Network.TESTNET4);
|
||||
|
||||
PublicElectrumServer(String name, String url, Network network) {
|
||||
this(name, url, network, List.of(PolicyType.SINGLE_HD, PolicyType.MULTI_HD));
|
||||
}
|
||||
|
||||
PublicElectrumServer(String name, String url, Network network, List<PolicyType> supportedPolicyTypes) {
|
||||
this.server = new Server(url, name);
|
||||
this.network = network;
|
||||
this.supportedPolicyTypes = supportedPolicyTypes;
|
||||
}
|
||||
|
||||
public static final List<Network> SUPPORTED_NETWORKS = List.of(Network.MAINNET, Network.TESTNET, Network.SIGNET, Network.TESTNET4);
|
||||
|
||||
private final Server server;
|
||||
private final Network network;
|
||||
private final List<PolicyType> supportedPolicyTypes;
|
||||
|
||||
public Server getServer() {
|
||||
return server;
|
||||
@ -52,14 +44,6 @@ public enum PublicElectrumServer {
|
||||
return network;
|
||||
}
|
||||
|
||||
public boolean isSupportedPolicyType(PolicyType policyType) {
|
||||
return supportedPolicyTypes.contains(policyType);
|
||||
}
|
||||
|
||||
public boolean supportsAllPolicyTypes(List<PolicyType> policyTypes) {
|
||||
return policyTypes.stream().allMatch(this::isSupportedPolicyType);
|
||||
}
|
||||
|
||||
public static List<PublicElectrumServer> getServers() {
|
||||
return Arrays.stream(values()).filter(server -> server.network == Network.get()).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@ -43,10 +43,6 @@ class SilentPaymentsScanCache {
|
||||
return state == State.CANCELLED;
|
||||
}
|
||||
|
||||
boolean isCompleted() {
|
||||
return state == State.COMPLETED;
|
||||
}
|
||||
|
||||
void cancel() {
|
||||
assert lock.isHeldByCurrentThread();
|
||||
if(state == State.SCANNING) {
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonFactory;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonToken;
|
||||
import com.github.arteam.simplejsonrpc.server.JsonRpcServer;
|
||||
import com.google.common.base.Splitter;
|
||||
import com.google.common.net.HostAndPort;
|
||||
@ -40,7 +37,6 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
public static final long PER_REQUEST_READ_TIMEOUT_MILLIS = 50;
|
||||
public static final int SOCKET_READ_TIMEOUT_MILLIS = 5000;
|
||||
private static final Pattern ID_PATTERN = Pattern.compile("\"id\"\\s*:\\s*(\\d+)");
|
||||
private static final JsonFactory JSON_FACTORY = new JsonFactory();
|
||||
|
||||
protected final HostAndPort server;
|
||||
protected final SocketFactory socketFactory;
|
||||
@ -56,9 +52,9 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
private final Condition readingCondition = readLock.newCondition();
|
||||
|
||||
private final ReentrantLock clientRequestLock = new ReentrantLock();
|
||||
private volatile boolean running = false;
|
||||
private boolean running = false;
|
||||
private volatile boolean reading = true;
|
||||
private volatile boolean closed = false;
|
||||
private boolean closed = false;
|
||||
private boolean firstRead = true;
|
||||
private int readTimeoutIndex;
|
||||
private int requestIdCount = 1;
|
||||
@ -164,7 +160,7 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
firstRead = false;
|
||||
}
|
||||
|
||||
while(reading && running) {
|
||||
while(reading) {
|
||||
try {
|
||||
readingCondition.await();
|
||||
} catch(InterruptedException e) {
|
||||
@ -178,10 +174,6 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
throw new IOException("Error reading response: " + lastException.getMessage(), lastException);
|
||||
}
|
||||
|
||||
if(!running) {
|
||||
throw new IOException("Transport closed");
|
||||
}
|
||||
|
||||
reading = true;
|
||||
|
||||
readingCondition.signal();
|
||||
@ -192,80 +184,58 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
}
|
||||
|
||||
public void readInputLoop() throws ServerException {
|
||||
BufferedReader in;
|
||||
readLock.lock();
|
||||
readReadySignal.countDown();
|
||||
|
||||
try {
|
||||
in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
try {
|
||||
//Don't start reading until first RPC request is sent
|
||||
readingCondition.await();
|
||||
} catch(InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
|
||||
|
||||
while(running) {
|
||||
try {
|
||||
String received = readInputStream(in);
|
||||
wireLog.info("< " + received);
|
||||
if(received.contains("method") && !received.contains("error")) {
|
||||
//Handle subscription notification
|
||||
jsonRpcServer.handle(received, subscriptionService);
|
||||
} else {
|
||||
//Handle client's response
|
||||
response = received;
|
||||
reading = false;
|
||||
readingCondition.signal();
|
||||
readingCondition.await();
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
//Restore interrupt status and continue
|
||||
Thread.currentThread().interrupt();
|
||||
} catch(Exception e) {
|
||||
log.trace("Connection error while reading", e);
|
||||
if(running) {
|
||||
lastException = e;
|
||||
reading = false;
|
||||
readingCondition.signal();
|
||||
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch(IOException e) {
|
||||
if(!closed) {
|
||||
log.error("Error opening socket inputstream", e);
|
||||
}
|
||||
if(running) {
|
||||
signalException(e);
|
||||
lastException = e;
|
||||
reading = false;
|
||||
readingCondition.signal();
|
||||
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
|
||||
running = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
//Wait for first RPC request before starting to read. The lock must be acquired before
|
||||
//signaling readiness so readResponse() blocks until we reach the atomic await/unlock.
|
||||
readLock.lock();
|
||||
try {
|
||||
readReadySignal.countDown();
|
||||
if(running) {
|
||||
readingCondition.await();
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
|
||||
while(running) {
|
||||
try {
|
||||
String received = readInputStream(in);
|
||||
wireLog.info("< " + received);
|
||||
if(isNotification(received)) {
|
||||
jsonRpcServer.handle(received, subscriptionService);
|
||||
} else {
|
||||
deliverResponse(received);
|
||||
}
|
||||
} catch(InterruptedException e) {
|
||||
//Restore interrupt status and continue
|
||||
Thread.currentThread().interrupt();
|
||||
} catch(Exception e) {
|
||||
if(!closed) {
|
||||
log.trace("Connection error while reading", e);
|
||||
}
|
||||
if(running) {
|
||||
signalException(e);
|
||||
//Allow this thread to terminate as we will need to reconnect with a new transport anyway
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverResponse(String received) throws InterruptedException {
|
||||
readLock.lock();
|
||||
try {
|
||||
response = received;
|
||||
reading = false;
|
||||
readingCondition.signal();
|
||||
while(!reading && running) {
|
||||
readingCondition.await();
|
||||
}
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private void signalException(Exception e) {
|
||||
readLock.lock();
|
||||
try {
|
||||
lastException = e;
|
||||
reading = false;
|
||||
readingCondition.signal();
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
@ -331,19 +301,10 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
running = false;
|
||||
closed = true;
|
||||
|
||||
readLock.lock();
|
||||
try {
|
||||
readingCondition.signalAll();
|
||||
} finally {
|
||||
readLock.unlock();
|
||||
}
|
||||
|
||||
if(socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
closed = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -351,26 +312,6 @@ public class TcpTransport implements CloseableTransport, TimeoutCounter {
|
||||
return readTimeoutIndex;
|
||||
}
|
||||
|
||||
private static boolean isNotification(String json) {
|
||||
try(JsonParser parser = JSON_FACTORY.createParser(json)) {
|
||||
if(parser.nextToken() != JsonToken.START_OBJECT) {
|
||||
return false;
|
||||
}
|
||||
while(parser.nextToken() == JsonToken.FIELD_NAME) {
|
||||
String field = parser.currentName();
|
||||
JsonToken value = parser.nextToken();
|
||||
if("method".equals(field)) {
|
||||
return value == JsonToken.VALUE_STRING;
|
||||
}
|
||||
parser.skipChildren();
|
||||
}
|
||||
return false;
|
||||
} catch(Exception e) {
|
||||
log.warn("Could not parse JSON-RPC message from server: " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Set<String> extractIdSet(String json) {
|
||||
if(json == null || json.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
|
||||
@ -115,8 +115,4 @@ public class Cormorant {
|
||||
public static EventBus getEventBus() {
|
||||
return EVENT_BUS;
|
||||
}
|
||||
|
||||
public BitcoindClient getBitcoindClient() {
|
||||
return bitcoindClient;
|
||||
}
|
||||
}
|
||||
|
||||
@ -77,7 +77,6 @@ public class BitcoindClient {
|
||||
private final boolean useWallets;
|
||||
private boolean pruned;
|
||||
private Integer pruneHeight;
|
||||
private volatile Date cachedPrunedDate;
|
||||
private boolean legacyWalletExists;
|
||||
|
||||
private final Lock syncingLock = new ReentrantLock();
|
||||
@ -286,18 +285,12 @@ public class BitcoindClient {
|
||||
if(blockchainInfo.pruned()) {
|
||||
String pruneBlockHash = getBitcoindService().getBlockHash(blockchainInfo.pruneheight());
|
||||
VerboseBlockHeader pruneBlockHeader = getBitcoindService().getBlockHeader(pruneBlockHash);
|
||||
Date prunedDate = new Date(pruneBlockHeader.time() * 1000);
|
||||
cachedPrunedDate = prunedDate;
|
||||
return Optional.of(prunedDate);
|
||||
return Optional.of(new Date(pruneBlockHeader.time() * 1000));
|
||||
}
|
||||
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
public Date getCachedPrunedDate() {
|
||||
return cachedPrunedDate;
|
||||
}
|
||||
|
||||
private ScanDate getScanDate(String normalizedDescriptor, Wallet wallet, KeyPurpose keyPurpose, Date earliestBirthDate) {
|
||||
Integer range = (keyPurpose == null ? null : Math.max(getWalletRange(normalizedDescriptor, wallet, keyPurpose), getDefaultRange(wallet, keyPurpose)));
|
||||
|
||||
|
||||
@ -6,8 +6,6 @@ import com.google.common.eventbus.Subscribe;
|
||||
import com.google.common.net.HostAndPort;
|
||||
import com.sparrowwallet.drongo.Network;
|
||||
import com.sparrowwallet.drongo.OsType;
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.EventManager;
|
||||
import com.sparrowwallet.sparrow.Mode;
|
||||
@ -49,9 +47,10 @@ import java.io.FileInputStream;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
|
||||
public class ServerSettingsController extends SettingsDetailController {
|
||||
private static final Logger log = LoggerFactory.getLogger(ServerSettingsController.class);
|
||||
@ -216,9 +215,6 @@ public class ServerSettingsController extends SettingsDetailController {
|
||||
}
|
||||
serverTypeToggleGroup.selectToggle(serverTypeToggleGroup.getToggles().stream().filter(toggle -> toggle.getUserData() == serverType).findFirst().orElse(null));
|
||||
|
||||
List<PolicyType> policyTypes = AppServices.get().getOpenWallets().keySet().stream().map(Wallet::getPolicyType).filter(Objects::nonNull).collect(Collectors.toList());
|
||||
publicElectrumServer.setButtonCell(new PublicElectrumServerButtonCell());
|
||||
publicElectrumServer.setCellFactory(_ -> new PublicElectrumServerListCell(policyTypes));
|
||||
publicElectrumServer.setItems(FXCollections.observableList(PublicElectrumServer.getServers()));
|
||||
publicElectrumServer.getSelectionModel().selectedItemProperty().addListener(getPublicElectrumServerListener(config));
|
||||
|
||||
@ -446,7 +442,7 @@ public class ServerSettingsController extends SettingsDetailController {
|
||||
if(configPublicElectrumServer == null && PublicElectrumServer.supportedNetwork()) {
|
||||
List<PublicElectrumServer> servers = PublicElectrumServer.getServers();
|
||||
if(!servers.isEmpty()) {
|
||||
publicElectrumServer.setValue(servers.get(ThreadLocalRandom.current().nextInt(servers.size())));
|
||||
publicElectrumServer.setValue(servers.get(new Random().nextInt(servers.size())));
|
||||
}
|
||||
} else {
|
||||
publicElectrumServer.setValue(configPublicElectrumServer);
|
||||
@ -1042,38 +1038,4 @@ public class ServerSettingsController extends SettingsDetailController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PublicElectrumServerButtonCell extends ListCell<PublicElectrumServer> {
|
||||
@Override
|
||||
protected void updateItem(PublicElectrumServer server, boolean empty) {
|
||||
super.updateItem(server, empty);
|
||||
if(server == null || empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(server.toString());
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class PublicElectrumServerListCell extends ListCell<PublicElectrumServer> {
|
||||
private final List<PolicyType> openPolicyTypes;
|
||||
|
||||
public PublicElectrumServerListCell(List<PolicyType> openPolicyTypes) {
|
||||
this.openPolicyTypes = openPolicyTypes;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateItem(PublicElectrumServer server, boolean empty) {
|
||||
super.updateItem(server, empty);
|
||||
if(server == null || empty) {
|
||||
setText(null);
|
||||
setGraphic(null);
|
||||
} else {
|
||||
setText(server + (openPolicyTypes.contains(PolicyType.SINGLE_SP) && server.isSupportedPolicyType(PolicyType.SINGLE_SP) ? " (supports Silent Payments)" : ""));
|
||||
setGraphic(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,6 @@ package com.sparrowwallet.sparrow.terminal.settings;
|
||||
|
||||
import com.googlecode.lanterna.TerminalSize;
|
||||
import com.googlecode.lanterna.gui2.*;
|
||||
import com.sparrowwallet.sparrow.AppServices;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
|
||||
|
||||
@ -33,7 +32,7 @@ public class PublicElectrumDialog extends ServerProxyDialog {
|
||||
url.addItem(server);
|
||||
}
|
||||
if(Config.get().getPublicElectrumServer() == null) {
|
||||
AppServices.get().changePublicServer();
|
||||
Config.get().changePublicServer();
|
||||
}
|
||||
url.setSelectedItem(PublicElectrumServer.fromServer(Config.get().getPublicElectrumServer()));
|
||||
url.addListener((selectedIndex, previousSelection, changedByUserInteraction) -> {
|
||||
|
||||
@ -20,7 +20,6 @@ import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
public class Bip39Dialog extends NewWalletDialog {
|
||||
@ -161,9 +160,6 @@ public class Bip39Dialog extends NewWalletDialog {
|
||||
Keystore keystore = importer.getKeystore(type.policyType(), wallet.getScriptType().getDefaultDerivation(), getWords(), passphrase.getText());
|
||||
wallet.getKeystores().add(keystore);
|
||||
wallet.setDefaultPolicy(Policy.getPolicy(type.policyType(), wallet.getScriptType(), wallet.getKeystores(), 1));
|
||||
if(type.policyType() == PolicyType.SINGLE_SP) {
|
||||
wallet.setBirthDate(new Date());
|
||||
}
|
||||
return List.of(wallet);
|
||||
}
|
||||
|
||||
|
||||
@ -532,10 +532,8 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
noWalletsWarningLink.visibleProperty().bind(noWalletsWarning.visibleProperty());
|
||||
|
||||
boolean taprootInput = psbt.getPsbtInputs().stream().anyMatch(PSBTInput::isTaproot);
|
||||
boolean silentPaymentOutput = psbt.getPsbtOutputs().stream().anyMatch(o -> o.getSilentPaymentAddress() != null);
|
||||
SigHash requiredSigHash = taprootInput ? SigHash.DEFAULT : SigHash.ALL;
|
||||
SigHash psbtSigHash = silentPaymentOutput ? requiredSigHash : psbt.getPsbtInputs().stream().map(PSBTInput::getSigHash).filter(Objects::nonNull).findFirst().orElse(requiredSigHash);
|
||||
sigHash.setItems(FXCollections.observableList(silentPaymentOutput ? List.of(requiredSigHash) : (taprootInput ? SigHash.TAPROOT_SIGNING_TYPES : SigHash.LEGACY_SIGNING_TYPES)));
|
||||
SigHash psbtSigHash = psbt.getPsbtInputs().stream().map(PSBTInput::getSigHash).filter(Objects::nonNull).findFirst().orElse(taprootInput ? SigHash.DEFAULT : SigHash.ALL);
|
||||
sigHash.setItems(FXCollections.observableList(taprootInput ? SigHash.TAPROOT_SIGNING_TYPES : SigHash.LEGACY_SIGNING_TYPES));
|
||||
sigHash.setValue(psbtSigHash);
|
||||
sigHash.setConverter(new StringConverter<>() {
|
||||
@Override
|
||||
@ -544,8 +542,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
return "";
|
||||
}
|
||||
|
||||
boolean recommended = (taprootInput && sigHash == SigHash.DEFAULT) || (!taprootInput && sigHash == SigHash.ALL);
|
||||
return sigHash.getName() + (recommended ? (silentPaymentOutput ? " (Required)" : " (Recommended)") : "");
|
||||
return sigHash.getName() + ((taprootInput && sigHash == SigHash.DEFAULT) || (!taprootInput && sigHash == SigHash.ALL) ? " (Recommended)" : "");
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -578,8 +575,6 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
|
||||
int threshold = signingWallet.getDefaultPolicy().getNumSignaturesRequired();
|
||||
signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold);
|
||||
|
||||
learnSilentPaymentAddresses(signingWallet, headersForm.getPsbt());
|
||||
});
|
||||
|
||||
blockchainForm.setDynamicUpdate(this);
|
||||
@ -1158,11 +1153,6 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
Map<PSBTInput, WalletNode> signingNodes = unencryptedWallet.getSigningNodes(headersForm.getPsbt());
|
||||
List<SilentPayment> silentPayments = unencryptedWallet.computeSilentPaymentOutputs(headersForm.getPsbt(), signingNodes);
|
||||
if(!silentPayments.isEmpty()) {
|
||||
Wallet signingWallet = headersForm.getSigningWallet();
|
||||
for(SilentPayment silentPayment : silentPayments) {
|
||||
signingWallet.addSilentPaymentAddress(silentPayment.getAddress(), silentPayment.getSilentPaymentAddress());
|
||||
}
|
||||
EventManager.get().post(new WalletSilentPaymentAddressesChangedEvent(signingWallet));
|
||||
EventManager.get().post(new TransactionOutputsChangedEvent(headersForm.getTransaction()));
|
||||
}
|
||||
unencryptedWallet.sign(signingNodes);
|
||||
@ -1293,7 +1283,7 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletNodeHistoryChangedEvent(scriptHashes.iterator().next())));
|
||||
}
|
||||
|
||||
if(transactionMempoolService.getIterationCount() > 12 && !transactionMempoolService.isCancelled()) {
|
||||
if(transactionMempoolService.getIterationCount() > 3 && !transactionMempoolService.isCancelled()) {
|
||||
transactionMempoolService.cancel();
|
||||
broadcastProgressBar.setProgress(0);
|
||||
log.error("Timeout searching for broadcasted transaction");
|
||||
@ -1450,26 +1440,6 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
requestPayjoinPSBTService.start();
|
||||
}
|
||||
|
||||
private void learnSilentPaymentAddresses(Wallet wallet, PSBT psbt) {
|
||||
if(wallet == null || psbt == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<Address, SilentPaymentAddress> verified = wallet.verifySilentPaymentOutputs(psbt);
|
||||
|
||||
boolean changed = false;
|
||||
for(Map.Entry<Address, SilentPaymentAddress> entry : verified.entrySet()) {
|
||||
if(!entry.getValue().equals(wallet.getSilentPaymentAddress(entry.getKey()))) {
|
||||
wallet.addSilentPaymentAddress(entry.getKey(), entry.getValue());
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if(changed) {
|
||||
EventManager.get().post(new WalletSilentPaymentAddressesChangedEvent(wallet));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void update() {
|
||||
BlockTransaction blockTransaction = headersForm.getBlockTransaction();
|
||||
@ -1691,7 +1661,6 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
@Subscribe
|
||||
public void psbtCombined(PSBTCombinedEvent event) {
|
||||
if(event.getPsbt().equals(headersForm.getPsbt())) {
|
||||
learnSilentPaymentAddresses(headersForm.getSigningWallet(), headersForm.getPsbt());
|
||||
if(headersForm.getSigningWallet() != null) {
|
||||
updateSignedKeystores(headersForm.getSigningWallet());
|
||||
} else if(headersForm.getPsbt().isSigned()) {
|
||||
@ -1706,7 +1675,6 @@ public class HeadersController extends TransactionFormController implements Init
|
||||
@Subscribe
|
||||
public void psbtFinalized(PSBTFinalizedEvent event) {
|
||||
if(event.getPsbt().equals(headersForm.getPsbt())) {
|
||||
learnSilentPaymentAddresses(headersForm.getSigningWallet(), headersForm.getPsbt());
|
||||
if(headersForm.getSigningWallet() != null) {
|
||||
updateSignedKeystores(headersForm.getSigningWallet());
|
||||
}
|
||||
|
||||
@ -1559,11 +1559,6 @@ public class SendController extends WalletFormController implements Initializabl
|
||||
@Subscribe
|
||||
public void excludeUtxo(ExcludeUtxoEvent event) {
|
||||
if(event.getWalletTransaction() == walletTransactionProperty.get()) {
|
||||
BlockTransaction replacedTransaction = replacedTransactionProperty.get();
|
||||
if(replacedTransaction != null && !getWalletForm().getWallet().isSafeToAddInputsOrOutputs(replacedTransaction)) {
|
||||
AppServices.showErrorDialog("Cannot Exclude Input", "Removing an input from this replacement transaction could break silent payment outputs as the original output script depends on the input set.");
|
||||
return;
|
||||
}
|
||||
UtxoSelector utxoSelector = utxoSelectorProperty.get();
|
||||
if(utxoSelector instanceof MaxUtxoSelector) {
|
||||
Collection<BlockTransactionHashIndex> utxos = event.getWalletTransaction().getSelectedUtxos().keySet();
|
||||
|
||||
@ -125,8 +125,9 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
walletForm.getWallet().setPolicyType(policyType);
|
||||
|
||||
scriptType.setItems(FXCollections.observableArrayList(ScriptType.getAddressableScriptTypes(policyType)));
|
||||
scriptType.getSelectionModel().select(policyType.getDefaultScriptType());
|
||||
|
||||
if(!initialising) {
|
||||
scriptType.getSelectionModel().select(policyType.getDefaultScriptType());
|
||||
clearKeystoreTabs();
|
||||
}
|
||||
initialising = false;
|
||||
@ -258,9 +259,6 @@ public class SettingsController extends WalletFormController implements Initiali
|
||||
revert.setDisable(true);
|
||||
apply.setDisable(true);
|
||||
boolean addressChange = ((SettingsWalletForm)walletForm).isAddressChange();
|
||||
if(walletForm.getWallet().getPolicyType() == PolicyType.SINGLE_SP && walletForm.getWallet().getBirthDate() == null && walletForm.getStorage().getEncryptionPubKey() == null) {
|
||||
walletForm.getWallet().setBirthDate(new Date());
|
||||
}
|
||||
saveWallet(false, false);
|
||||
|
||||
Wallet wallet = walletForm.getWallet();
|
||||
|
||||
@ -245,11 +245,8 @@ public class TransactionEntry extends Entry implements Comparable<TransactionEnt
|
||||
|
||||
public Long getVSizeFromTip() {
|
||||
if(!AppServices.getMempoolHistogram().isEmpty()) {
|
||||
Double feeRate = blockTransaction.getFeeRate();
|
||||
if(feeRate == null) {
|
||||
return null;
|
||||
}
|
||||
Set<MempoolRateSize> rateSizes = AppServices.getMempoolHistogram().get(AppServices.getMempoolHistogram().lastKey());
|
||||
double feeRate = blockTransaction.getFeeRate();
|
||||
return rateSizes.stream().filter(rateSize -> rateSize.getFee() > feeRate).mapToLong(MempoolRateSize::getVSize).sum();
|
||||
}
|
||||
|
||||
|
||||
@ -228,7 +228,7 @@ public class WalletForm {
|
||||
|
||||
boolean shouldHold = !spSubscriptionHeld;
|
||||
|
||||
ElectrumServer.SilentPaymentScanService scanService = new ElectrumServer.SilentPaymentScanService(wallet, shouldHold, wallet.getNeededScanStart());
|
||||
ElectrumServer.SilentPaymentScanService scanService = new ElectrumServer.SilentPaymentScanService(wallet, shouldHold, computeNeededStart(wallet));
|
||||
scanService.setOnSucceeded(workerStateEvent -> {
|
||||
spScanInProgress = false;
|
||||
spSubscriptionHeld = true;
|
||||
@ -296,6 +296,20 @@ public class WalletForm {
|
||||
}
|
||||
}
|
||||
|
||||
private static int computeNeededStart(Wallet wallet) {
|
||||
Integer stored = wallet.getStoredBlockHeight();
|
||||
if(stored != null && stored > 0) {
|
||||
return Math.max(0, stored - BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM);
|
||||
}
|
||||
if(wallet.getBirthHeight() != null) {
|
||||
return Math.max(0, wallet.getBirthHeight() - BlockTransactionHash.BLOCKS_TO_FULLY_CONFIRM);
|
||||
}
|
||||
if(wallet.getBirthDate() != null) {
|
||||
return (int)(wallet.getBirthDate().getTime() / 1000L);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private void updateWallets(Integer blockHeight, Wallet previousWallet) {
|
||||
List<WalletNode> nestedHistoryChangedNodes = new ArrayList<>();
|
||||
for(Wallet childWallet : new ArrayList<>(wallet.getChildWallets())) {
|
||||
@ -749,13 +763,6 @@ public class WalletForm {
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletSilentPaymentAddressesChanged(WalletSilentPaymentAddressesChangedEvent event) {
|
||||
if(event.getWallet() == wallet) {
|
||||
Platform.runLater(() -> EventManager.get().post(new WalletDataChangedEvent(wallet)));
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe
|
||||
public void walletTabsClosed(WalletTabsClosedEvent event) {
|
||||
for(WalletTabData tabData : event.getClosedWalletTabData()) {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package com.sparrowwallet.sparrow.wallet;
|
||||
|
||||
import com.sparrowwallet.drongo.policy.PolicyType;
|
||||
import com.sparrowwallet.drongo.wallet.Wallet;
|
||||
import com.sparrowwallet.drongo.wallet.WalletNode;
|
||||
import com.sparrowwallet.sparrow.io.Config;
|
||||
@ -10,7 +9,6 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class WalletUtxosEntry extends Entry {
|
||||
public static final int DUST_ATTACK_THRESHOLD_SATS = 1000;
|
||||
public static final int DUST_ATTACK_THRESHOLD_SP_SATS = 5000;
|
||||
|
||||
public WalletUtxosEntry(Wallet wallet) {
|
||||
super(wallet, wallet.getName(), wallet.getWalletUtxos().entrySet().stream().map(entry -> new UtxoEntry(entry.getValue().getWallet(), entry.getKey(), HashIndexEntry.Type.OUTPUT, entry.getValue())).collect(Collectors.toList()));
|
||||
@ -52,22 +50,14 @@ public class WalletUtxosEntry extends Entry {
|
||||
}
|
||||
|
||||
protected void calculateDust() {
|
||||
if(getWallet().getPolicyType() == PolicyType.SINGLE_SP) {
|
||||
long dustAttackThreshold = Config.get().getDustAttackThresholdSp();
|
||||
for(Entry entry : getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry) entry;
|
||||
utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
|
||||
}
|
||||
} else {
|
||||
long dustAttackThreshold = Config.get().getDustAttackThreshold();
|
||||
Set<WalletNode> duplicateNodes = getWallet().getWalletTxos().values().stream()
|
||||
.collect(Collectors.groupingBy(e -> e, Collectors.counting()))
|
||||
.entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
long dustAttackThreshold = Config.get().getDustAttackThreshold();
|
||||
Set<WalletNode> duplicateNodes = getWallet().getWalletTxos().values().stream()
|
||||
.collect(Collectors.groupingBy(e -> e, Collectors.counting()))
|
||||
.entrySet().stream().filter(e -> e.getValue() > 1).map(Map.Entry::getKey).collect(Collectors.toSet());
|
||||
|
||||
for(Entry entry : getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry) entry;
|
||||
utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && duplicateNodes.contains(utxoEntry.getNode()) && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
|
||||
}
|
||||
for(Entry entry : getChildren()) {
|
||||
UtxoEntry utxoEntry = (UtxoEntry) entry;
|
||||
utxoEntry.setDustAttack(utxoEntry.getValue() <= dustAttackThreshold && duplicateNodes.contains(utxoEntry.getNode()) && !utxoEntry.getWallet().allInputsFromWallet(utxoEntry.getHashIndex().getHash()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
alter table wallet add column birthHeight integer after birthDate;
|
||||
alter table walletNode add column silentPaymentTweak varbinary(32) after addressData;
|
||||
alter table keystore add column silentPaymentScanAddress varbinary(65) after externalPaymentCode;
|
||||
create table silentPaymentAddress (address varbinary(32) primary key not null, silentPaymentAddress varbinary(67) not null);
|
||||
alter table keystore add column silentPaymentScanAddress varbinary(65) after externalPaymentCode;
|
||||
@ -32,7 +32,7 @@
|
||||
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
|
||||
<Fieldset inputGrow="SOMETIMES" text="Settings" styleClass="header">
|
||||
<Field text="Policy Type:">
|
||||
<ComboBox fx:id="policyType" prefWidth="180">
|
||||
<ComboBox fx:id="policyType" prefWidth="160">
|
||||
<items>
|
||||
<FXCollections fx:factory="observableArrayList">
|
||||
<PolicyType fx:constant="SINGLE_HD" />
|
||||
@ -41,7 +41,7 @@
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<HelpLabel helpText="Single signature wallets use a single keystore (hardware wallet or seed).\nMultisignature wallets require multiple keystores, of which a certain number need to sign.\nSilent Payments wallets also use a single keystore and can receive privately to a reusable address." />
|
||||
<HelpLabel helpText="Single signature wallets use a single keystore (hardware wallet or seed).\nMultisignature wallets require multiple keystores (N), of which a certain number (M) need to sign." />
|
||||
</Field>
|
||||
<Field text="Script Type:">
|
||||
<ComboBox fx:id="scriptType">
|
||||
@ -57,7 +57,7 @@
|
||||
</FXCollections>
|
||||
</items>
|
||||
</ComboBox>
|
||||
<HelpLabel helpText="Native Segwit the type chosen for most new wallets.\nTaproot is a newer type useful for specific needs.\nNested Segwit and Legacy are useful for recovering older wallets.\nFor existing wallets, be sure to choose the type that matches the wallet you are restoring." />
|
||||
<HelpLabel helpText="Native Segwit types are the default and are usually the best choice.\nNested Segwit types are a good choice for the widest compatibility with older wallets.\nLegacy types should be avoided, unless configuring an old wallet." />
|
||||
</Field>
|
||||
</Fieldset>
|
||||
</Form>
|
||||
|
||||
@ -1,64 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.control;
|
||||
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.GregorianCalendar;
|
||||
import java.util.TimeZone;
|
||||
|
||||
public class DateAxisFormatterTest {
|
||||
private static final long HOUR = 60 * 60 * 1000L;
|
||||
private static final long DAY = 24 * HOUR;
|
||||
private static final long YEAR = 365 * DAY;
|
||||
|
||||
private static String thirdLabel(DateAxisFormatter formatter, long timestamp) {
|
||||
formatter.toString(timestamp);
|
||||
formatter.toString(timestamp);
|
||||
return formatter.toString(timestamp);
|
||||
}
|
||||
|
||||
private static long timestamp(int year, int month, int day) {
|
||||
Calendar cal = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
|
||||
cal.clear();
|
||||
cal.set(year, month, day);
|
||||
return cal.getTimeInMillis();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void multiYearDurationIncludesFourDigitYear() {
|
||||
DateAxisFormatter formatter = new DateAxisFormatter(2 * YEAR);
|
||||
long ts = timestamp(2024, Calendar.AUGUST, 15);
|
||||
String actual = thirdLabel(formatter, ts);
|
||||
String fourDigitYear = new SimpleDateFormat("yyyy").format(new Date(ts));
|
||||
Assertions.assertTrue(actual.contains(fourDigitYear),
|
||||
"Expected 4-digit year " + fourDigitYear + " in label, got: " + actual);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subYearDurationUsesDayMonth() {
|
||||
DateAxisFormatter formatter = new DateAxisFormatter(30 * DAY);
|
||||
long ts = timestamp(2024, Calendar.AUGUST, 15);
|
||||
Assertions.assertEquals(new SimpleDateFormat("d MMM").format(new Date(ts)),
|
||||
thirdLabel(formatter, ts));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void subDayDurationUsesHourMinute() {
|
||||
DateAxisFormatter formatter = new DateAxisFormatter(2 * HOUR);
|
||||
long ts = timestamp(2024, Calendar.AUGUST, 15);
|
||||
Assertions.assertEquals(new SimpleDateFormat("HH:mm").format(new Date(ts)),
|
||||
thirdLabel(formatter, ts));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void everyThirdLabelIsRendered() {
|
||||
DateAxisFormatter formatter = new DateAxisFormatter(2 * YEAR);
|
||||
long ts = timestamp(2024, Calendar.AUGUST, 15);
|
||||
Assertions.assertEquals("", formatter.toString(ts));
|
||||
Assertions.assertEquals("", formatter.toString(ts));
|
||||
Assertions.assertNotEquals("", formatter.toString(ts));
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class Auth47Test {
|
||||
@Test
|
||||
public void acceptsHttpsCallbacksWithResource() throws Exception {
|
||||
Auth47 auth47 = new Auth47(new URI("auth47://nonce?c=https://example.com/auth&r=example"));
|
||||
|
||||
assertEquals("https", auth47.getCallback().getProtocol());
|
||||
assertEquals("example.com", auth47.getCallback().getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsSrbnCallbacks() throws Exception {
|
||||
Auth47 auth47 = new Auth47(new URI("auth47://nonce?c=srbn://alice@relay.example.com"));
|
||||
|
||||
assertEquals("https", auth47.getCallback().getProtocol());
|
||||
assertEquals("relay.example.com", auth47.getCallback().getHost());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsHttpClearnetCallbacks() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
new Auth47(new URI("auth47://nonce?c=http://example.com/auth&r=example")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNonHttpCallbacksWithResource() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
new Auth47(new URI("auth47://nonce?c=file:///tmp/auth47&r=example")));
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package com.sparrowwallet.sparrow.net;
|
||||
|
||||
import com.sparrowwallet.drongo.protocol.Bech32;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
public class LnurlAuthTest {
|
||||
private static final String K1 = "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f";
|
||||
|
||||
@Test
|
||||
public void acceptsHttpsCallbacks() throws Exception {
|
||||
LnurlAuth lnurlAuth = new LnurlAuth(lightningUri("https://example.com/lnurl-auth?tag=login&k1=" + K1));
|
||||
|
||||
assertEquals("example.com", lnurlAuth.getDomain());
|
||||
assertEquals("login to example.com", lnurlAuth.getLoginMessage());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void acceptsHttpOnionCallbacks() throws Exception {
|
||||
LnurlAuth lnurlAuth = new LnurlAuth(lightningUri("http://abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion/lnurl-auth?tag=login&k1=" + K1));
|
||||
|
||||
assertEquals("abcdefghijklmnopqrstuvwxyzabcdefghijklmnop.onion", lnurlAuth.getDomain());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsHttpClearnetCallbacks() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
new LnurlAuth(lightningUri("http://example.com/lnurl-auth?tag=login&k1=" + K1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNonHttpCallbacks() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
new LnurlAuth(lightningUri("ftp://example.com/lnurl-auth?tag=login&k1=" + K1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void rejectsNonLnurlBech32Prefix() {
|
||||
assertThrows(IllegalArgumentException.class, () ->
|
||||
new LnurlAuth(lightningUri("lnurlx", "https://example.com/lnurl-auth?tag=login&k1=" + K1)));
|
||||
}
|
||||
|
||||
private static URI lightningUri(String url) throws Exception {
|
||||
return lightningUri("lnurl", url);
|
||||
}
|
||||
|
||||
private static URI lightningUri(String prefix, String url) throws Exception {
|
||||
byte[] urlBytes = url.getBytes(StandardCharsets.UTF_8);
|
||||
byte[] lnurlData = Bech32.convertBits(urlBytes, 0, urlBytes.length, 8, 5, true);
|
||||
return new URI("lightning:" + Bech32.encode(prefix, Bech32.Encoding.BECH32, lnurlData));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user