From 6a1c3fa3da1907c0a460843e7ba2ac7bee2d2a2d Mon Sep 17 00:00:00 2001 From: Craig Raw Date: Tue, 8 Dec 2020 12:32:59 +0200 Subject: [PATCH] support opening wallets in new windows --- .../sparrowwallet/sparrow/AppController.java | 214 +++++++++++------- .../sparrowwallet/sparrow/AppServices.java | 71 +++++- .../com/sparrowwallet/sparrow/MainApp.java | 24 +- .../com/sparrowwallet/sparrow/TabData.java | 22 +- .../sparrow/TransactionTabData.java | 10 +- .../OpenWalletsNewWindowsStatusEvent.java | 13 ++ .../sparrow/event/ViewPSBTEvent.java | 14 +- .../sparrow/event/ViewWalletEvent.java | 29 +++ .../com/sparrowwallet/sparrow/io/Config.java | 10 + .../transaction/HeadersController.java | 6 +- .../sparrow/wallet/SendController.java | 2 +- .../com/sparrowwallet/sparrow/app.fxml | 1 + 12 files changed, 275 insertions(+), 141 deletions(-) create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsNewWindowsStatusEvent.java create mode 100644 src/main/java/com/sparrowwallet/sparrow/event/ViewWalletEvent.java diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index 3040d1dd..dc009033 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -44,13 +44,9 @@ import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.image.Image; import javafx.scene.image.ImageView; -import javafx.scene.input.Dragboard; -import javafx.scene.input.TransferMode; +import javafx.scene.input.*; import javafx.scene.layout.StackPane; -import javafx.stage.FileChooser; -import javafx.stage.Stage; -import javafx.stage.StageStyle; -import javafx.stage.Window; +import javafx.stage.*; import javafx.util.Duration; import org.controlsfx.control.Notifications; import org.controlsfx.control.StatusBar; @@ -93,6 +89,9 @@ public class AppController implements Initializable { @FXML private ToggleGroup theme; + @FXML + private CheckMenuItem openWalletsInNewWindows; + @FXML private CheckMenuItem showTxHex; @@ -131,7 +130,7 @@ public class AppController implements Initializable { if(db.hasFiles()) { for(File file : db.getFiles()) { if(isWalletFile(file)) { - openWalletFile(file); + openWalletFile(file, true); } else { openTransactionFile(file); } @@ -190,6 +189,11 @@ public class AppController implements Initializable { } }); + tabs.getScene().getWindow().setOnCloseRequest(event -> { + EventManager.get().unregister(this); + EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), Collections.emptyMap())); + }); + BitcoinUnit unit = Config.get().getBitcoinUnit(); if(unit == null) { unit = BitcoinUnit.AUTO; @@ -209,9 +213,11 @@ public class AppController implements Initializable { selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle)); setTheme(null); + openWalletsInNewWindows.setSelected(Config.get().isOpenWalletsInNewWindows()); showTxHex.setSelected(Config.get().isShowTransactionHex()); exportWallet.setDisable(true); + serverToggle.setSelected(isOnline()); onlineProperty().bindBidirectional(serverToggle.selectedProperty()); onlineProperty().addListener((observable, oldValue, newValue) -> { Platform.runLater(() -> setServerToggleTooltip(getCurrentBlockHeight())); @@ -284,9 +290,12 @@ public class AppController implements Initializable { private void openTransactionFile(File file) { for(Tab tab : tabs.getTabs()) { TabData tabData = (TabData)tab.getUserData(); - if(file.equals(tabData.getFile())) { - tabs.getSelectionModel().select(tab); - return; + if(tabData instanceof TransactionTabData) { + TransactionTabData transactionTabData = (TransactionTabData)tabData; + if(file.equals(transactionTabData.getFile())) { + tabs.getSelectionModel().select(tab); + return; + } } } @@ -299,10 +308,7 @@ public class AppController implements Initializable { String name = file.getName(); try { - Tab tab = addTransactionTab(name, bytes); - TabData tabData = (TabData)tab.getUserData(); - tabData.setFile(file); - tabs.getSelectionModel().select(tab); + addTransactionTab(name, file, bytes); } catch(ParseException e) { ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes); ByteSource byteSource = new ByteSource() { @@ -313,8 +319,7 @@ public class AppController implements Initializable { }; String text = byteSource.asCharSource(Charsets.UTF_8).read().trim(); - Tab tab = addTransactionTab(name, text); - tabs.getSelectionModel().select(tab); + addTransactionTab(name, file, text); } } catch(IOException e) { showErrorDialog("Error opening file", e.getMessage()); @@ -335,10 +340,7 @@ public class AppController implements Initializable { Optional text = dialog.showAndWait(); if(text.isPresent() && !text.get().isEmpty()) { try { - Tab tab = addTransactionTab(null, text.get().trim()); - TabData tabData = (TabData)tab.getUserData(); - tabData.setText(text.get()); - tabs.getSelectionModel().select(tab); + addTransactionTab(null, null, text.get().trim()); } catch(PSBTParseException e) { showErrorDialog("Invalid PSBT", e.getMessage()); } catch(TransactionParseException e) { @@ -382,11 +384,9 @@ public class AppController implements Initializable { if(optionalResult.isPresent()) { QRScanDialog.Result result = optionalResult.get(); if(result.transaction != null) { - Tab tab = addTransactionTab(null, result.transaction); - tabs.getSelectionModel().select(tab); + addTransactionTab(null, null, result.transaction); } else if(result.psbt != null) { - Tab tab = addTransactionTab(null, result.psbt); - tabs.getSelectionModel().select(tab); + addTransactionTab(null, null, result.psbt); } else if(result.exception != null) { log.error("Error scanning QR", result.exception); showErrorDialog("Error scanning QR", result.exception.getMessage()); @@ -485,6 +485,12 @@ public class AppController implements Initializable { } } + public void openWalletsInNewWindows(ActionEvent event) { + CheckMenuItem item = (CheckMenuItem)event.getSource(); + Config.get().setOpenWalletsInNewWindows(item.isSelected()); + EventManager.get().post(new OpenWalletsNewWindowsStatusEvent(item.isSelected())); + } + public void showTxHex(ActionEvent event) { CheckMenuItem item = (CheckMenuItem)event.getSource(); Config.get().setShowTransactionHex(item.isSelected()); @@ -514,12 +520,15 @@ public class AppController implements Initializable { File walletFile = Storage.getWalletFile(walletName.get()); Storage storage = new Storage(walletFile); Wallet wallet = new Wallet(walletName.get(), PolicyType.SINGLE, ScriptType.P2WPKH); - Tab tab = addWalletTab(storage, wallet); - tabs.getSelectionModel().select(tab); + addWalletTabOrWindow(storage, wallet, false); } } public void openWallet(ActionEvent event) { + openWallet(false); + } + + public void openWallet(boolean forceSameWindow) { Stage window = new Stage(); FileChooser fileChooser = new FileChooser(); fileChooser.setTitle("Open Wallet"); @@ -527,11 +536,11 @@ public class AppController implements Initializable { File file = fileChooser.showOpenDialog(window); if(file != null) { - openWalletFile(file); + openWalletFile(file, forceSameWindow); } } - public void openWalletFile(File file) { + public void openWalletFile(File file, boolean forceSameWindow) { try { Storage storage = new Storage(file); FileType fileType = IOUtils.getFileType(file); @@ -542,8 +551,7 @@ public class AppController implements Initializable { if(!wallet.isValid()) { throw new IllegalStateException("Wallet file is not valid."); } - Tab tab = addWalletTab(storage, wallet); - tabs.getSelectionModel().select(tab); + addWalletTabOrWindow(storage, wallet, forceSameWindow); } else if(FileType.BINARY.equals(fileType)) { WalletPasswordDialog dlg = new WalletPasswordDialog(file.getName(), WalletPasswordDialog.PasswordRequirement.LOAD); Optional optionalPassword = dlg.showAndWait(); @@ -559,8 +567,7 @@ public class AppController implements Initializable { try { checkWalletNetwork(walletAndKey.wallet); restorePublicKeysFromSeed(walletAndKey.wallet, walletAndKey.key); - Tab tab = addWalletTab(storage, walletAndKey.wallet); - tabs.getSelectionModel().select(tab); + addWalletTabOrWindow(storage, walletAndKey.wallet, forceSameWindow); } catch(Exception e) { showErrorDialog("Error Opening Wallet", e.getMessage()); } finally { @@ -710,8 +717,7 @@ public class AppController implements Initializable { if(password.isPresent()) { if(password.get().length() == 0) { storage.setEncryptionPubKey(Storage.NO_PASSWORD_KEY); - Tab tab = addWalletTab(storage, wallet); - tabs.getSelectionModel().select(tab); + addWalletTabOrWindow(storage, wallet, false); } else { Storage.KeyDerivationService keyDerivationService = new Storage.KeyDerivationService(storage, password.get()); keyDerivationService.setOnSucceeded(workerStateEvent -> { @@ -724,8 +730,7 @@ public class AppController implements Initializable { key = new Key(encryptionFullKey.getPrivKeyBytes(), storage.getKeyDeriver().getSalt(), EncryptionType.Deriver.ARGON2); wallet.encrypt(key); storage.setEncryptionPubKey(encryptionPubKey); - Tab tab = addWalletTab(storage, wallet); - tabs.getSelectionModel().select(tab); + addWalletTabOrWindow(storage, wallet, false); } finally { encryptionFullKey.clear(); if(key != null) { @@ -782,17 +787,28 @@ public class AppController implements Initializable { messageSignDialog.showAndWait(); } - public Tab addWalletTab(Storage storage, Wallet wallet) { - for(Tab tab : tabs.getTabs()) { - TabData tabData = (TabData)tab.getUserData(); - if(tabData.getType() == TabData.TabType.WALLET) { - WalletTabData walletTabData = (WalletTabData) tabData; - if(storage.getWalletFile().equals(walletTabData.getStorage().getWalletFile())) { - return tab; - } - } + public void addWalletTabOrWindow(Storage storage, Wallet wallet, boolean forceSameWindow) { + Window existingWalletWindow = AppServices.get().getWindowForWallet(storage); + if(existingWalletWindow instanceof Stage) { + Stage existingWalletStage = (Stage)existingWalletWindow; + existingWalletStage.toFront(); + + EventManager.get().post(new ViewWalletEvent(existingWalletWindow, wallet, storage)); + return; } + if(!forceSameWindow && Config.get().isOpenWalletsInNewWindows() && !getOpenWallets().isEmpty()) { + Stage stage = new Stage(); + AppController appController = AppServices.newAppWindow(stage); + stage.toFront(); + stage.setX(AppServices.get().getWalletWindowMaxX() + 30); + appController.addWalletTab(storage, wallet); + } else { + addWalletTab(storage, wallet); + } + } + + public void addWalletTab(Storage storage, Wallet wallet) { try { String name = storage.getWalletFile().getName(); if(name.endsWith(".json")) { @@ -817,7 +833,7 @@ public class AppController implements Initializable { tab.setUserData(tabData); tabs.getTabs().add(tab); - return tab; + tabs.getSelectionModel().select(tab); } catch(IOException e) { throw new RuntimeException(e); } @@ -825,63 +841,70 @@ public class AppController implements Initializable { public void openExamples(ActionEvent event) { try { - addTransactionTab("p2pkh", "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559"); - addTransactionTab("p2sh", "0100000003a5ee1a0fd80dfbc3142df136ab56e082b799c13aa977c048bdf8f61bd158652c000000006b48304502203b0160de302cded63589a88214fe499a25aa1d86a2ea09129945cd632476a12c022100c77727daf0718307e184d55df620510cf96d4b5814ae3258519c0482c1ca82fa0121024f4102c1f1cf662bf99f2b034eb03edd4e6c96793cb9445ff519aab580649120ffffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0000000006a47304402204089adb8a1de1a9e22aa43b94d54f1e54dc9bea745d57df1a633e03dd9ede3c2022037d1e53e911ed7212186028f2e085f70524930e22eb6184af090ba4ab779a5b90121030644cb394bf381dbec91680bdf1be1986ad93cfb35603697353199fb285a119effffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0010000009300493046022100a07b2821f96658c938fa9c68950af0e69f3b2ce5f8258b3a6ad254d4bc73e11e022100e82fab8df3f7e7a28e91b3609f91e8ebf663af3a4dc2fd2abd954301a5da67e701475121022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e2103a767c7221e9f15f870f1ad9311f5ab937d79fcaeee15bb2c722bca515581b4c052aeffffffff02a3b81b00000000001976a914ea00917f128f569cbdf79da5efcd9001671ab52c88ac80969800000000001976a9143dec0ead289be1afa8da127a7dbdd425a05e25f688ac00000000"); - addTransactionTab("p2sh-p2wpkh", "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000"); - addTransactionTab("p2sh-p2wsh", "01000000000101708256c5896fb3f00ef37601f8e30c5b460dbcd1fca1cd7199f9b56fc4ecd5400000000023220020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4cffffffff01603edb0300000000160014bbef244bcad13cffb68b5cef3017c7423675552204004730440220010d2854b86b90b7c33661ca25f9d9f15c24b88c5c4992630f77ff004b998fb802204106fc3ec8481fa98e07b7e78809ac91b6ccaf60bf4d3f729c5a75899bb664a501473044022046d66321c6766abcb1366a793f9bfd0e11e0b080354f18188588961ea76c5ad002207262381a0661d66f5c39825202524c45f29d500c6476176cd910b1691176858701695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000"); - addTransactionTab("p2wpkh", "01000000000101109d2e41430bfdec7e6dfb02bf78b5827eeb717ef25210ff3203b0db8c76c9260000000000ffffffff01a032eb0500000000160014bbef244bcad13cffb68b5cef3017c742367555220247304402202f7cac3494e521018ae0be4ca18517639ef7c00658d42a9f938b2b344c8454e2022039a54218832fad5d14b331329d9042c51ee6be287e95e49ee5b96fda1f5ce13f0121026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf300000000"); - addTransactionTab("p2wsh", "0100000000010193a2db37b841b2a46f4e9bb63fe9c1012da3ab7fe30b9f9c974242778b5af8980000000000ffffffff01806fb307000000001976a914bbef244bcad13cffb68b5cef3017c7423675552288ac040047304402203cdcaf02a44e37e409646e8a506724e9e1394b890cb52429ea65bac4cc2403f1022024b934297bcd0c21f22cee0e48751c8b184cc3a0d704cae2684e14858550af7d01483045022100feb4e1530c13e72226dc912dcd257df90d81ae22dbddb5a3c2f6d86f81d47c8e022069889ddb76388fa7948aaa018b2480ac36132009bb9cfade82b651e88b4b137a01695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000"); - //addTransactionTab("test1", "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd0481500400002305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900"); - //addTransactionTab("3of3-1s.psbt", "70736274ff0100550200000001294c4871c059bb76be81e94b78059ee2e0c9b1b47f38edb6b4e75916062394930000000000feffffff01f82a0000000000001976a914e65b294f890792f2c2725d488567018d660f0cf488ac701c09004f0102aa7ed3044b1635bb800000021bf4bfc48934b7966b39bdebb689525d9b8bfed5c8b16e8c58f9afe4641d6d5f03800b5dbec0355c9f0b5e8227bc903e9d0ff1fe6ced0dcfb6d416541c7412c4331406b57041300000800000008000000080020000804f0102aa7ed3042cd31dee80000002d544b2364010378f8c6cec85f6b7ed83a8203dcdbedb97e2625f431f897b837e0363428de8fcfbfe373c0d9e1e0cc8163d886764bafe71c5822eaa232981356589145f63394f300000800000008000000080020000804f0102aa7ed3049ec7d9f580000002793e04aff18b4e40ebc48bcdc6232c54c69cf7265a38fbd85b35705e34d2d42f03368e79aa2b2b7f736d156905a7a45891df07baa2d0b7f127a537908cb82deed514130a48af300000800000008000000080020000800001012b983a000000000000220020f64748dad1cbad107761aaed5c59f25aba006498d260b440e0a091691350c9aa010569532102f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3262103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a221037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f753ae220602f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3261c130a48af300000800000008000000080020000800000000000000000220603171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a21c5f63394f300000800000008000000080020000800000000000000000220203171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a24830450221008d27cc4b03bc543726e73b69e7980e7364d6f33f979a5cd9b92fb3d050666bd002204fc81fc9c67baf7c3b77041ed316714a9c117a5bdbb020e8c771ea3bdc342434012206037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f71c06b570413000008000000080000000800200008000000000000000000000"); - //addTransactionTab("signer.psbt", "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); - //addTransactionTab("combiner.psbt", "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); - addTransactionTab("finalizer.psbt", "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); + addTransactionTab("p2pkh", null, "01000000019c2e0f24a03e72002a96acedb12a632e72b6b74c05dc3ceab1fe78237f886c48010000006a47304402203da9d487be5302a6d69e02a861acff1da472885e43d7528ed9b1b537a8e2cac9022002d1bca03a1e9715a99971bafe3b1852b7a4f0168281cbd27a220380a01b3307012102c9950c622494c2e9ff5a003e33b690fe4832477d32c2d256c67eab8bf613b34effffffff02b6f50500000000001976a914bdf63990d6dc33d705b756e13dd135466c06b3b588ac845e0201000000001976a9145fb0e9755a3424efd2ba0587d20b1e98ee29814a88ac06241559"); + addTransactionTab("p2sh", null, "0100000003a5ee1a0fd80dfbc3142df136ab56e082b799c13aa977c048bdf8f61bd158652c000000006b48304502203b0160de302cded63589a88214fe499a25aa1d86a2ea09129945cd632476a12c022100c77727daf0718307e184d55df620510cf96d4b5814ae3258519c0482c1ca82fa0121024f4102c1f1cf662bf99f2b034eb03edd4e6c96793cb9445ff519aab580649120ffffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0000000006a47304402204089adb8a1de1a9e22aa43b94d54f1e54dc9bea745d57df1a633e03dd9ede3c2022037d1e53e911ed7212186028f2e085f70524930e22eb6184af090ba4ab779a5b90121030644cb394bf381dbec91680bdf1be1986ad93cfb35603697353199fb285a119effffffff0fce901eb7b7551ba5f414735ff93b83a2a57403df11059ec88245fba2aaf1a0010000009300493046022100a07b2821f96658c938fa9c68950af0e69f3b2ce5f8258b3a6ad254d4bc73e11e022100e82fab8df3f7e7a28e91b3609f91e8ebf663af3a4dc2fd2abd954301a5da67e701475121022afc20bf379bc96a2f4e9e63ffceb8652b2b6a097f63fbee6ecec2a49a48010e2103a767c7221e9f15f870f1ad9311f5ab937d79fcaeee15bb2c722bca515581b4c052aeffffffff02a3b81b00000000001976a914ea00917f128f569cbdf79da5efcd9001671ab52c88ac80969800000000001976a9143dec0ead289be1afa8da127a7dbdd425a05e25f688ac00000000"); + addTransactionTab("p2sh-p2wpkh", null, "01000000000101db6b1b20aa0fd7b23880be2ecbd4a98130974cf4748fb66092ac4d3ceb1a5477010000001716001479091972186c449eb1ded22b78e40d009bdf0089feffffff02b8b4eb0b000000001976a914a457b684d7f0d539a46a45bbc043f35b59d0d96388ac0008af2f000000001976a914fd270b1ee6abcaea97fea7ad0402e8bd8ad6d77c88ac02473044022047ac8e878352d3ebbde1c94ce3a10d057c24175747116f8288e5d794d12d482f0220217f36a485cae903c713331d877c1f64677e3622ad4010726870540656fe9dcb012103ad1d8e89212f0b92c74d23bb710c00662ad1470198ac48c43f7d6f93a2a2687392040000"); + addTransactionTab("p2sh-p2wsh", null, "01000000000101708256c5896fb3f00ef37601f8e30c5b460dbcd1fca1cd7199f9b56fc4ecd5400000000023220020615ae01ed1bc1ffaad54da31d7805d0bb55b52dfd3941114330368c1bbf69b4cffffffff01603edb0300000000160014bbef244bcad13cffb68b5cef3017c7423675552204004730440220010d2854b86b90b7c33661ca25f9d9f15c24b88c5c4992630f77ff004b998fb802204106fc3ec8481fa98e07b7e78809ac91b6ccaf60bf4d3f729c5a75899bb664a501473044022046d66321c6766abcb1366a793f9bfd0e11e0b080354f18188588961ea76c5ad002207262381a0661d66f5c39825202524c45f29d500c6476176cd910b1691176858701695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000"); + addTransactionTab("p2wpkh", null, "01000000000101109d2e41430bfdec7e6dfb02bf78b5827eeb717ef25210ff3203b0db8c76c9260000000000ffffffff01a032eb0500000000160014bbef244bcad13cffb68b5cef3017c742367555220247304402202f7cac3494e521018ae0be4ca18517639ef7c00658d42a9f938b2b344c8454e2022039a54218832fad5d14b331329d9042c51ee6be287e95e49ee5b96fda1f5ce13f0121026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf300000000"); + addTransactionTab("p2wsh", null, "0100000000010193a2db37b841b2a46f4e9bb63fe9c1012da3ab7fe30b9f9c974242778b5af8980000000000ffffffff01806fb307000000001976a914bbef244bcad13cffb68b5cef3017c7423675552288ac040047304402203cdcaf02a44e37e409646e8a506724e9e1394b890cb52429ea65bac4cc2403f1022024b934297bcd0c21f22cee0e48751c8b184cc3a0d704cae2684e14858550af7d01483045022100feb4e1530c13e72226dc912dcd257df90d81ae22dbddb5a3c2f6d86f81d47c8e022069889ddb76388fa7948aaa018b2480ac36132009bb9cfade82b651e88b4b137a01695221026ccfb8061f235cc110697c0bfb3afb99d82c886672f6b9b5393b25a434c0cbf32103befa190c0c22e2f53720b1be9476dcf11917da4665c44c9c71c3a2d28a933c352102be46dc245f58085743b1cc37c82f0d63a960efa43b5336534275fc469b49f4ac53ae00000000"); + //addTransactionTab("test1", null, "02000000000102ba4dc5a4a14bfaa941b7d115b379b5e15f960635cf694c178b9116763cbd63b11600000017160014fc164cbcac023f5eacfcead2d17d8768c41949affeffffff074d44d2856beb68ba52e8832da60a1682768c2421c2d9a8109ef4e66babd1fd1e000000171600148c3098be6b430859115f5ee99c84c368afecd0481500400002305310000000000017a914ffaf369c2212b178c7a2c21c9ccdd5d126e74c4187327f0300000000001976a914a7cda2e06b102a143ab606937a01d152e300cd3e88ac02473044022006da0ca227f765179219e08a33026b94e7cacff77f87b8cd8eb1b46d6dda11d6022064faa7912924fd23406b6ed3328f1bbbc3760dc51109a49c1b38bf57029d304f012103c6a2fcd030270427d4abe1041c8af929a9e2dbab07b243673453847ab842ee1f024730440220786316a16095105a0af28dccac5cf80f449dea2ea810a9559a89ecb989c2cb3d02205cbd9913d1217ffec144ae4f2bd895f16d778c2ec49ae9c929fdc8bcc2a2b1db0121024d4985241609d072a59be6418d700e87688f6c4d99a51ad68e66078211f076ee38820900"); + //addTransactionTab("3of3-1s.psbt", null, "70736274ff0100550200000001294c4871c059bb76be81e94b78059ee2e0c9b1b47f38edb6b4e75916062394930000000000feffffff01f82a0000000000001976a914e65b294f890792f2c2725d488567018d660f0cf488ac701c09004f0102aa7ed3044b1635bb800000021bf4bfc48934b7966b39bdebb689525d9b8bfed5c8b16e8c58f9afe4641d6d5f03800b5dbec0355c9f0b5e8227bc903e9d0ff1fe6ced0dcfb6d416541c7412c4331406b57041300000800000008000000080020000804f0102aa7ed3042cd31dee80000002d544b2364010378f8c6cec85f6b7ed83a8203dcdbedb97e2625f431f897b837e0363428de8fcfbfe373c0d9e1e0cc8163d886764bafe71c5822eaa232981356589145f63394f300000800000008000000080020000804f0102aa7ed3049ec7d9f580000002793e04aff18b4e40ebc48bcdc6232c54c69cf7265a38fbd85b35705e34d2d42f03368e79aa2b2b7f736d156905a7a45891df07baa2d0b7f127a537908cb82deed514130a48af300000800000008000000080020000800001012b983a000000000000220020f64748dad1cbad107761aaed5c59f25aba006498d260b440e0a091691350c9aa010569532102f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3262103171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a221037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f753ae220602f26969eb8d1da34d17d33ff99e2f020cc33b3d11d9798ec14f46b82bc455d3261c130a48af300000800000008000000080020000800000000000000000220603171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a21c5f63394f300000800000008000000080020000800000000000000000220203171d9b824205cd5db6e9353676a292ca954b24d8310a36fc983469ba3fb507a24830450221008d27cc4b03bc543726e73b69e7980e7364d6f33f979a5cd9b92fb3d050666bd002204fc81fc9c67baf7c3b77041ed316714a9c117a5bdbb020e8c771ea3bdc342434012206037f3794f3be4c4acc086ac84d6902c025713eabf8890f20f44acf0b34e3c0f0f71c06b570413000008000000080000000800200008000000000000000000000"); + //addTransactionTab("signer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f618765000000220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8872202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); + //addTransactionTab("combiner.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); + addTransactionTab("finalizer.psbt", null, "70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000000107da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae0001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e8870107232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b20289030108da0400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000"); } catch(Exception e) { log.error("Error opening examples", e); } } - private Tab addTransactionTab(String name, String string) throws ParseException, PSBTParseException, TransactionParseException { + private void addTransactionTab(String name, File file, String string) throws ParseException, PSBTParseException, TransactionParseException { if(Utils.isBase64(string) && !Utils.isHex(string)) { - return addTransactionTab(name, Base64.getDecoder().decode(string)); + addTransactionTab(name, file, Base64.getDecoder().decode(string)); } else if(Utils.isHex(string)) { - return addTransactionTab(name, Utils.hexToBytes(string)); + addTransactionTab(name, file, Utils.hexToBytes(string)); + } else { + throw new ParseException("Input is not base64 or hex", 0); } - - throw new ParseException("Input is not base64 or hex", 0); } - private Tab addTransactionTab(String name, byte[] bytes) throws PSBTParseException, ParseException, TransactionParseException { + private void addTransactionTab(String name, File file, byte[] bytes) throws PSBTParseException, ParseException, TransactionParseException { if(PSBT.isPSBT(bytes)) { PSBT psbt = new PSBT(bytes); - return addTransactionTab(name, psbt); - } - - if(Transaction.isTransaction(bytes)) { + addTransactionTab(name, file, psbt); + } else if(Transaction.isTransaction(bytes)) { try { Transaction transaction = new Transaction(bytes); - return addTransactionTab(name, transaction); + addTransactionTab(name, file, transaction); } catch(Exception e) { throw new TransactionParseException(e.getMessage()); } + } else { + throw new ParseException("Not a valid PSBT or transaction", 0); } - - throw new ParseException("Not a valid PSBT or transaction", 0); } - private Tab addTransactionTab(String name, Transaction transaction) { - return addTransactionTab(name, transaction, null, null, null, null); + private void addTransactionTab(String name, File file, Transaction transaction) { + addTransactionTab(name, file, transaction, null, null, null, null); } - private Tab addTransactionTab(String name, PSBT psbt) { - return addTransactionTab(name, psbt.getTransaction(), psbt, null, null, null); + private void addTransactionTab(String name, File file, PSBT psbt) { + Window psbtWalletWindow = AppServices.get().getWindowForPSBT(psbt); + if(psbtWalletWindow != null && !tabs.getScene().getWindow().equals(psbtWalletWindow)) { + EventManager.get().post(new ViewPSBTEvent(psbtWalletWindow, name, file, psbt)); + if(psbtWalletWindow instanceof Stage) { + Stage stage = (Stage)psbtWalletWindow; + stage.toFront(); + } + } else { + addTransactionTab(name, file, psbt.getTransaction(), psbt, null, null, null); + } } - private Tab addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { - return addTransactionTab(null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex); + private void addTransactionTab(BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { + addTransactionTab(null, null, blockTransaction.getTransaction(), null, blockTransaction, initialView, initialIndex); } - private Tab addTransactionTab(String name, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { + private void addTransactionTab(String name, File file, Transaction transaction, PSBT psbt, BlockTransaction blockTransaction, TransactionView initialView, Integer initialIndex) { for(Tab tab : tabs.getTabs()) { TabData tabData = (TabData)tab.getUserData(); if(tabData instanceof TransactionTabData) { @@ -916,7 +939,8 @@ public class AppController implements Initializable { } } - return tab; + tabs.getSelectionModel().select(tab); + return; } } } @@ -950,12 +974,11 @@ public class AppController implements Initializable { } controller.initializeView(); - TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, transactionData); + TabData tabData = new TransactionTabData(TabData.TabType.TRANSACTION, file, transactionData); tab.setUserData(tabData); tabs.getTabs().add(tab); - - return tab; + tabs.getSelectionModel().select(tab); } catch(IOException e) { throw new RuntimeException(e); } @@ -1188,19 +1211,33 @@ public class AppController implements Initializable { setServerToggleTooltip(event.getHeight()); } + @Subscribe + public void viewWallet(ViewWalletEvent event) { + if(tabs.getScene().getWindow().equals(event.getWindow())) { + for(Tab tab : tabs.getTabs()) { + TabData tabData = (TabData) tab.getUserData(); + if(tabData.getType() == TabData.TabType.WALLET) { + WalletTabData walletTabData = (WalletTabData) tabData; + if(event.getStorage().getWalletFile().equals(walletTabData.getStorage().getWalletFile())) { + tabs.getSelectionModel().select(tab); + return; + } + } + } + } + } + @Subscribe public void viewTransaction(ViewTransactionEvent event) { if(tabs.getScene().getWindow().equals(event.getWindow())) { - Tab tab = addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex()); - tabs.getSelectionModel().select(tab); + addTransactionTab(event.getBlockTransaction(), event.getInitialView(), event.getInitialIndex()); } } @Subscribe public void viewPSBT(ViewPSBTEvent event) { if(tabs.getScene().getWindow().equals(event.getWindow())) { - Tab tab = addTransactionTab(event.getLabel(), event.getPsbt()); - tabs.getSelectionModel().select(tab); + addTransactionTab(event.getLabel(), event.getFile(), event.getPsbt()); } } @@ -1210,6 +1247,11 @@ public class AppController implements Initializable { selectedToggle.ifPresent(toggle -> bitcoinUnit.selectToggle(toggle)); } + @Subscribe + public void openWalletsInNewWindowsStatusChanged(OpenWalletsNewWindowsStatusEvent event) { + openWalletsInNewWindows.setSelected(event.isOpenWalletsInNewWindows()); + } + @Subscribe public void requestOpenWallets(RequestOpenWalletsEvent event) { EventManager.get().post(new OpenWalletsEvent(tabs.getScene().getWindow(), getOpenWallets())); @@ -1218,7 +1260,7 @@ public class AppController implements Initializable { @Subscribe public void requestWalletOpen(RequestWalletOpenEvent event) { if(tabs.getScene().getWindow().equals(event.getWindow())) { - openWallet(null); + openWallet(true); } } diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index 41cd2621..8a317331 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -3,6 +3,7 @@ package com.sparrowwallet.sparrow; import com.google.common.eventbus.Subscribe; import com.sparrowwallet.drongo.address.Address; import com.sparrowwallet.drongo.protocol.Transaction; +import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.drongo.uri.BitcoinURI; import com.sparrowwallet.drongo.wallet.KeystoreSource; import com.sparrowwallet.drongo.wallet.Wallet; @@ -22,6 +23,9 @@ import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.concurrent.ScheduledService; import javafx.concurrent.Worker; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; import javafx.scene.control.Alert; import javafx.scene.image.Image; import javafx.scene.text.Font; @@ -32,6 +36,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.File; +import java.io.IOException; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.temporal.ChronoUnit; @@ -52,13 +57,13 @@ public class AppServices { private final MainApp application; - private final Map> windows = new LinkedHashMap<>(); + private final Map> walletWindows = new LinkedHashMap<>(); private static final BooleanProperty onlineProperty = new SimpleBooleanProperty(false); private ExchangeSource.RatesService ratesService; - private final ElectrumServer.ConnectionService connectionService; + private ElectrumServer.ConnectionService connectionService; private Hwi.ScheduledEnumerateService deviceEnumerateService; @@ -116,9 +121,10 @@ public class AppServices { public AppServices(MainApp application) { this.application = application; - EventManager.get().register(this); + } + public void start() { Config config = Config.get(); connectionService = createConnectionService(); if(config.getMode() == Mode.ONLINE && config.getElectrumServer() != null && !config.getElectrumServer().isEmpty()) { @@ -231,10 +237,52 @@ public class AppServices { return INSTANCE; } + public static AppController newAppWindow(Stage stage) { + try { + FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml")); + Parent root = appLoader.load(); + AppController appController = appLoader.getController(); + + Scene scene = new Scene(root); + scene.getStylesheets().add(AppServices.class.getResource("app.css").toExternalForm()); + + stage.setTitle("Sparrow"); + stage.setMinWidth(650); + stage.setMinHeight(800); + stage.setScene(scene); + stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png"))); + + appController.initializeView(); + stage.show(); + return appController; + } catch(IOException e) { + log.error("Could not load app FXML", e); + throw new IllegalStateException(e); + } + } + public MainApp getApplication() { return application; } + public Map getOpenWallets(Window window) { + return walletWindows.get(window); + } + + public Window getWindowForWallet(Storage storage) { + Optional optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().values().stream().anyMatch(storage1 -> storage1.getWalletFile().equals(storage.getWalletFile()))).map(Map.Entry::getKey).findFirst(); + return optWindow.orElse(null); + } + + public Window getWindowForPSBT(PSBT psbt) { + Optional optWindow = walletWindows.entrySet().stream().filter(entry -> entry.getValue().keySet().stream().anyMatch(wallet -> wallet.canSign(psbt))).map(Map.Entry::getKey).findFirst(); + return optWindow.orElse(null); + } + + public double getWalletWindowMaxX() { + return walletWindows.keySet().stream().mapToDouble(Window::getX).max().orElse(0d); + } + public static boolean isOnline() { return onlineProperty.get(); } @@ -383,11 +431,20 @@ public class AppServices { @Subscribe public void openWallets(OpenWalletsEvent event) { - windows.put(event.getWindow(), event.getWalletsMap()); - List> allWallets = windows.values().stream().flatMap(map -> map.entrySet().stream()).collect(Collectors.toList()); + if(event.getWalletsMap().isEmpty()) { + walletWindows.remove(event.getWindow()); + } else { + walletWindows.put(event.getWindow(), event.getWalletsMap()); + } - List walletFiles = allWallets.stream().map(entry -> entry.getValue().getWalletFile()).collect(Collectors.toList()); - Config.get().setRecentWalletFiles(walletFiles); + List> allWallets = walletWindows.values().stream().flatMap(map -> map.entrySet().stream()).collect(Collectors.toList()); + + Platform.runLater(() -> { + if(!Window.getWindows().isEmpty()) { + List walletFiles = allWallets.stream().map(entry -> entry.getValue().getWalletFile()).collect(Collectors.toList()); + Config.get().setRecentWalletFiles(walletFiles); + } + }); boolean usbWallet = false; for(Map.Entry entry : allWallets) { diff --git a/src/main/java/com/sparrowwallet/sparrow/MainApp.java b/src/main/java/com/sparrowwallet/sparrow/MainApp.java index 5b7ec960..a7f3c990 100644 --- a/src/main/java/com/sparrowwallet/sparrow/MainApp.java +++ b/src/main/java/com/sparrowwallet/sparrow/MainApp.java @@ -69,23 +69,7 @@ public class MainApp extends Application { } AppServices.initialize(this); - - FXMLLoader transactionLoader = new FXMLLoader(getClass().getResource("app.fxml")); - Parent root = transactionLoader.load(); - AppController appController = transactionLoader.getController(); - - Scene scene = new Scene(root); - scene.getStylesheets().add(getClass().getResource("app.css").toExternalForm()); - - stage.setTitle("Sparrow"); - stage.setMinWidth(650); - stage.setMinHeight(800); - stage.setScene(scene); - stage.getIcons().add(new Image(MainApp.class.getResourceAsStream("/image/sparrow.png"))); - - appController.initializeView(); - - stage.show(); + AppController appController = AppServices.newAppWindow(stage); if(createNewWallet) { appController.newWallet(null); @@ -93,7 +77,7 @@ public class MainApp extends Application { List recentWalletFiles = Config.get().getRecentWalletFiles(); if(recentWalletFiles != null) { - //Resort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first. + //Re-sort to preserve wallet order as far as possible. Unencrypted wallets will still be opened first. List encryptedWalletFiles = recentWalletFiles.stream().filter(file -> FileType.BINARY.equals(IOUtils.getFileType(file))).collect(Collectors.toList()); Collections.reverse(encryptedWalletFiles); List sortedWalletFiles = new ArrayList<>(recentWalletFiles); @@ -102,10 +86,12 @@ public class MainApp extends Application { for(File walletFile : sortedWalletFiles) { if(walletFile.exists()) { - Platform.runLater(() -> appController.openWalletFile(walletFile)); + appController.openWalletFile(walletFile, false); } } } + + AppServices.get().start(); } @Override diff --git a/src/main/java/com/sparrowwallet/sparrow/TabData.java b/src/main/java/com/sparrowwallet/sparrow/TabData.java index 3844d5e1..c8c58c9f 100644 --- a/src/main/java/com/sparrowwallet/sparrow/TabData.java +++ b/src/main/java/com/sparrowwallet/sparrow/TabData.java @@ -1,11 +1,7 @@ package com.sparrowwallet.sparrow; -import java.io.File; - public class TabData { - private TabType type; - private File file; - private String text; + private final TabType type; public TabData(TabType type) { this.type = type; @@ -15,22 +11,6 @@ public class TabData { return type; } - public File getFile() { - return file; - } - - public void setFile(File file) { - this.file = file; - } - - public String getText() { - return text; - } - - public void setText(String text) { - this.text = text; - } - public enum TabType { WALLET, TRANSACTION } diff --git a/src/main/java/com/sparrowwallet/sparrow/TransactionTabData.java b/src/main/java/com/sparrowwallet/sparrow/TransactionTabData.java index 7ea3c581..285050ae 100644 --- a/src/main/java/com/sparrowwallet/sparrow/TransactionTabData.java +++ b/src/main/java/com/sparrowwallet/sparrow/TransactionTabData.java @@ -4,14 +4,22 @@ import com.sparrowwallet.drongo.protocol.Transaction; import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.sparrow.transaction.TransactionData; +import java.io.File; + public class TransactionTabData extends TabData { + private final File file; private final TransactionData transactionData; - public TransactionTabData(TabType type, TransactionData transactionData) { + public TransactionTabData(TabType type, File file, TransactionData transactionData) { super(type); + this.file = file; this.transactionData = transactionData; } + public File getFile() { + return file; + } + public TransactionData getTransactionData() { return transactionData; } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsNewWindowsStatusEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsNewWindowsStatusEvent.java new file mode 100644 index 00000000..87a8c81b --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/OpenWalletsNewWindowsStatusEvent.java @@ -0,0 +1,13 @@ +package com.sparrowwallet.sparrow.event; + +public class OpenWalletsNewWindowsStatusEvent { + private final boolean openWalletsInNewWindows; + + public OpenWalletsNewWindowsStatusEvent(boolean openWalletsInNewWindows) { + this.openWalletsInNewWindows = openWalletsInNewWindows; + } + + public boolean isOpenWalletsInNewWindows() { + return openWalletsInNewWindows; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ViewPSBTEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ViewPSBTEvent.java index fe42f8d9..87d9ca42 100644 --- a/src/main/java/com/sparrowwallet/sparrow/event/ViewPSBTEvent.java +++ b/src/main/java/com/sparrowwallet/sparrow/event/ViewPSBTEvent.java @@ -4,20 +4,24 @@ import com.sparrowwallet.drongo.psbt.PSBT; import com.sparrowwallet.sparrow.transaction.TransactionView; import javafx.stage.Window; +import java.io.File; + public class ViewPSBTEvent { private final Window window; private final String label; + private final File file; private final PSBT psbt; private final TransactionView initialView; private final Integer initialIndex; - public ViewPSBTEvent(Window window, String label, PSBT psbt) { - this(window, label, psbt, TransactionView.HEADERS, null); + public ViewPSBTEvent(Window window, String label, File file, PSBT psbt) { + this(window, label, file, psbt, TransactionView.HEADERS, null); } - public ViewPSBTEvent(Window window, String label, PSBT psbt, TransactionView initialView, Integer initialIndex) { + public ViewPSBTEvent(Window window, String label, File file, PSBT psbt, TransactionView initialView, Integer initialIndex) { this.window = window; this.label = label; + this.file = file; this.psbt = psbt; this.initialView = initialView; this.initialIndex = initialIndex; @@ -31,6 +35,10 @@ public class ViewPSBTEvent { return label; } + public File getFile() { + return file; + } + public PSBT getPsbt() { return psbt; } diff --git a/src/main/java/com/sparrowwallet/sparrow/event/ViewWalletEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/ViewWalletEvent.java new file mode 100644 index 00000000..694becf4 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/ViewWalletEvent.java @@ -0,0 +1,29 @@ +package com.sparrowwallet.sparrow.event; + +import com.sparrowwallet.drongo.wallet.Wallet; +import com.sparrowwallet.sparrow.io.Storage; +import javafx.stage.Window; + +public class ViewWalletEvent { + private final Window window; + private final Wallet wallet; + private final Storage storage; + + public ViewWalletEvent(Window window, Wallet wallet, Storage storage) { + this.window = window; + this.wallet = wallet; + this.storage = storage; + } + + public Window getWindow() { + return window; + } + + public Wallet getWallet() { + return wallet; + } + + public Storage getStorage() { + return storage; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index e5f5e601..316e09cb 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -31,6 +31,7 @@ public class Config { private boolean notifyNewTransactions = true; private boolean checkNewVersions = true; private Theme theme; + private boolean openWalletsInNewWindows = false; private boolean showTransactionHex = true; private List recentWalletFiles; private Integer keyDerivationPeriod; @@ -185,6 +186,15 @@ public class Config { flush(); } + public boolean isOpenWalletsInNewWindows() { + return openWalletsInNewWindows; + } + + public void setOpenWalletsInNewWindows(boolean openWalletsInNewWindows) { + this.openWalletsInNewWindows = openWalletsInNewWindows; + flush(); + } + public boolean isShowTransactionHex() { return showTransactionHex; } diff --git a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java index 6af5819b..477995fd 100644 --- a/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java +++ b/src/main/java/com/sparrowwallet/sparrow/transaction/HeadersController.java @@ -420,7 +420,7 @@ public class HeadersController extends TransactionFormController implements Init signaturesProgressBar.initialize(headersForm.getSignatureKeystoreMap(), threshold); }); - EventManager.get().post(new RequestOpenWalletsEvent()); + Platform.runLater(() -> EventManager.get().post(new RequestOpenWalletsEvent())); } blockchainForm.setDynamicUpdate(this); @@ -830,7 +830,7 @@ public class HeadersController extends TransactionFormController implements Init try { Payjoin payjoin = new Payjoin(payjoinURI, headersForm.getSigningWallet(), headersForm.getPsbt()); PSBT proposalPsbt = payjoin.requestPayjoinPSBT(true); - EventManager.get().post(new ViewPSBTEvent(payjoinButton.getScene().getWindow(), headersForm.getName() + " Payjoin", proposalPsbt)); + EventManager.get().post(new ViewPSBTEvent(payjoinButton.getScene().getWindow(), headersForm.getName() + " Payjoin", null, proposalPsbt)); } catch(PayjoinReceiverException e) { AppServices.showErrorDialog("Invalid Payjoin Transaction", e.getMessage()); } @@ -884,7 +884,7 @@ public class HeadersController extends TransactionFormController implements Init @Subscribe public void openWallets(OpenWalletsEvent event) { - if((id.getScene() == null || id.getScene().getWindow().equals(event.getWindow())) && headersForm.getPsbt() != null && headersForm.getBlockTransaction() == null) { + if(id.getScene().getWindow().equals(event.getWindow()) && headersForm.getPsbt() != null && headersForm.getBlockTransaction() == null) { List availableWallets = event.getWallets().stream().filter(wallet -> wallet.canSign(headersForm.getPsbt())).sorted(new WalletSignComparator()).collect(Collectors.toList()); Map availableWalletsMap = new LinkedHashMap<>(event.getWalletsMap()); availableWalletsMap.keySet().retainAll(availableWallets); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java index c6ca9683..2e0bbd18 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/SendController.java @@ -728,7 +728,7 @@ public class SendController extends WalletFormController implements Initializabl addWalletTransactionNodes(); createdWalletTransactionProperty.set(walletTransactionProperty.get()); PSBT psbt = walletTransactionProperty.get().createPSBT(); - EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransactionProperty.get().getPayments().get(0).getLabel(), psbt)); + EventManager.get().post(new ViewPSBTEvent(createButton.getScene().getWindow(), walletTransactionProperty.get().getPayments().get(0).getLabel(), null, psbt)); } private void addWalletTransactionNodes() { diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 1d6872b7..3c5e5c23 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -78,6 +78,7 @@ +