diff --git a/build.gradle b/build.gradle index bf97cc59..181221c2 100644 --- a/build.gradle +++ b/build.gradle @@ -164,7 +164,7 @@ jlink { appVersion = "${sparrowVersion}" skipInstaller = os.macOsX imageOptions = [] - installerOptions = ['--file-associations', 'src/main/deploy/associations.properties', '--license-file', 'LICENSE'] + installerOptions = ['--file-associations', 'src/main/deploy/psbt.properties', '--file-associations', 'src/main/deploy/bitcoin.properties', '--file-associations', 'src/main/deploy/aopp.properties', '--license-file', 'LICENSE'] if(os.windows) { installerOptions += ['--win-per-user-install', '--win-dir-chooser', '--win-menu', '--win-shortcut'] imageOptions += ['--icon', 'src/main/deploy/package/windows/sparrow.ico'] diff --git a/drongo b/drongo index db9617ee..cc32285d 160000 --- a/drongo +++ b/drongo @@ -1 +1 @@ -Subproject commit db9617ee10383bb78e71ec2252d92bb7fe639440 +Subproject commit cc32285d58fcd1120b27797ff1d882ae4ae8a1ed diff --git a/src/main/deploy/aopp.properties b/src/main/deploy/aopp.properties new file mode 100644 index 00000000..0f7aefb1 --- /dev/null +++ b/src/main/deploy/aopp.properties @@ -0,0 +1,2 @@ +mime-type=x-scheme-handler/aopp +description=Verify Address Ownership URI \ No newline at end of file diff --git a/src/main/deploy/bitcoin.properties b/src/main/deploy/bitcoin.properties new file mode 100644 index 00000000..c5e54ebb --- /dev/null +++ b/src/main/deploy/bitcoin.properties @@ -0,0 +1,2 @@ +mime-type=x-scheme-handler/bitcoin +description=Bitcoin Scheme URI \ No newline at end of file diff --git a/src/main/deploy/package/osx/Info.plist b/src/main/deploy/package/osx/Info.plist index 590ebe2c..1ffca736 100644 --- a/src/main/deploy/package/osx/Info.plist +++ b/src/main/deploy/package/osx/Info.plist @@ -35,5 +35,24 @@ true NSCameraUsageDescription Sparrow requires access to the camera in order to scan QR codes + CFBundleURLTypes + + + CFBundleURLName + com.sparrowwallet.sparrow.bitcoin + CFBundleURLSchemes + + bitcoin + + + + CFBundleURLName + com.sparrowwallet.sparrow.aopp + CFBundleURLSchemes + + aopp + + + \ No newline at end of file diff --git a/src/main/deploy/associations.properties b/src/main/deploy/psbt.properties similarity index 51% rename from src/main/deploy/associations.properties rename to src/main/deploy/psbt.properties index 717f3943..c985fcde 100644 --- a/src/main/deploy/associations.properties +++ b/src/main/deploy/psbt.properties @@ -1,3 +1,3 @@ -extension=psbt mime-type=application/octet-stream +extension=psbt description=Partially Signed Bitcoin Transaction \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 87c3a40b..e5aa6029 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -1795,4 +1795,9 @@ public class AppController implements Initializable { openTransactionFromQR(null); } } + + @Subscribe + public void sendAction(SendActionEvent event) { + selectTab(event.getWallet()); + } } \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 3aefbd2d..cb441bce 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -29,7 +29,10 @@ import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; +import javafx.scene.control.Dialog; +import javafx.scene.control.Label; import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.text.Font; import javafx.stage.Screen; import javafx.stage.Stage; @@ -40,14 +43,17 @@ import org.controlsfx.control.HyperlinkLabel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.awt.*; import java.io.File; import java.io.IOException; import java.net.InetSocketAddress; import java.net.Proxy; +import java.net.URI; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; import java.util.*; +import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -135,6 +141,8 @@ public class AppServices { restartServices(); } } + + addURIHandlers(); } private void restartServices() { @@ -591,6 +599,50 @@ public class AppServices { } } + public static void addURIHandlers() { + Desktop.getDesktop().setOpenURIHandler(event -> { + URI uri = event.getURI(); + if("bitcoin".equals(uri.getScheme())) { + Platform.runLater(() -> openBitcoinUri(uri)); + } else if("aopp".equals(uri.getScheme())) { + log.info(uri.toString()); + } + }); + } + + private static void openBitcoinUri(URI uri) { + try { + BitcoinURI bitcoinURI = new BitcoinURI(uri.toString()); + + Wallet wallet = null; + Set wallets = get().getOpenWallets().keySet(); + if(wallets.isEmpty()) { + showErrorDialog("No wallet available", "Open a wallet to send to the provided bitcoin URI."); + } else if(wallets.size() == 1) { + wallet = wallets.iterator().next(); + } else { + ChoiceDialog walletChoiceDialog = new ChoiceDialog<>(wallets.iterator().next(), wallets); + walletChoiceDialog.setTitle("Choose Wallet"); + walletChoiceDialog.setHeaderText("Choose a wallet to pay from"); + Image image = new Image("/image/sparrow-small.png"); + walletChoiceDialog.getDialogPane().setGraphic(new ImageView(image)); + AppServices.setStageIcon(walletChoiceDialog.getDialogPane().getScene().getWindow()); + Optional optWallet = walletChoiceDialog.showAndWait(); + if(optWallet.isPresent()) { + wallet = optWallet.get(); + } + } + + if(wallet != null) { + final Wallet sendingWallet = wallet; + EventManager.get().post(new SendActionEvent(sendingWallet, new ArrayList<>(sendingWallet.getWalletUtxos().keySet()))); + Platform.runLater(() -> EventManager.get().post(new SendPaymentsEvent(sendingWallet, List.of(bitcoinURI.toPayment())))); + } + } catch(Exception e) { + showErrorDialog("Not a valid bitcoin URI", e.getMessage()); + } + } + public static Font getMonospaceFont() { return Font.font("Roboto Mono", 13); } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java new file mode 100644 index 00000000..7dda67d7 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/SendPaymentsEvent.java @@ -0,0 +1,24 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Payment; +import com.sparrowwallet.drongo.wallet.Wallet; + +import java.util.List; + +public class SendPaymentsEvent { + private final Wallet wallet; + private final List payments; + + public SendPaymentsEvent(Wallet wallet, List payments) { + this.wallet = wallet; + this.payments = payments; + } + + public Wallet getWallet() { + return wallet; + } + + public List getPayments() { + return payments; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java index 477ef264..45e00797 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/PaymentController.java @@ -282,7 +282,9 @@ public class PaymentController extends WalletFormController implements Initializ if(payment.getLabel() != null) { label.setText(payment.getLabel()); } - setRecipientValueSats(payment.getAmount()); + if(payment.getAmount() >= 0) { + setRecipientValueSats(payment.getAmount()); + } setFiatAmount(AppServices.getFiatCurrencyExchangeRate(), payment.getAmount()); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index f62919be..2f4439bd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -1074,6 +1074,17 @@ public class SendController extends WalletFormController implements Initializabl } } + @Subscribe + public void sendPayments(SendPaymentsEvent event) { + if(event.getWallet().equals(getWalletForm().getWallet())) { + if(event.getPayments() != null) { + clear(null); + setPayments(event.getPayments()); + updateTransaction(event.getPayments() == null || event.getPayments().stream().anyMatch(Payment::isSendMax)); + } + } + } + @Subscribe public void bitcoinUnitChanged(BitcoinUnitChangedEvent event) { BitcoinUnit unit = getBitcoinUnit(event.getBitcoinUnit());