├── LICENSE ├── README.md ├── mac-package.sh ├── mac.icns ├── pom.xml └── src └── main ├── java └── timestamper │ ├── Main.java │ ├── MainController.java │ ├── SendMoneyController.java │ ├── WalletPasswordController.java │ ├── WalletSetPasswordController.java │ ├── WalletSettingsController.java │ ├── controls │ ├── BitcoinAddressValidator.java │ ├── ClickableBitcoinAddress.java │ └── NotificationBarPane.java │ └── utils │ ├── AlertWindowController.java │ ├── BitcoinUIModel.java │ ├── GuiUtils.java │ ├── KeyDerivationTasks.java │ ├── TextFieldValidator.java │ ├── ThrottledRunLater.java │ ├── WTUtils.java │ └── easing │ ├── EasingInterpolator.java │ ├── EasingMode.java │ └── ElasticInterpolator.java └── resources └── timestamper ├── 200px-Padlock.svg.png ├── bitcoin_logo_plain.png ├── checkpoints ├── checkpoints.testnet ├── controls └── bitcoin_address.fxml ├── main.fxml ├── send_money.fxml ├── utils ├── alert.fxml └── text-validation.css ├── wallet.css ├── wallet_password.fxml ├── wallet_set_password.fxml └── wallet_settings.fxml /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timestamper 2 | 3 | The simple tutorial app from Mike Hearn's DevCore 2015 talk: how to build a program that timestamps documents in 30 4 | minutes. -------------------------------------------------------------------------------- /mac-package.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | jh=`/usr/libexec/java_home` 4 | 5 | $jh/bin/javapackager -deploy \ 6 | -BappVersion=1 \ 7 | -Bmac.CFBundleIdentifier=net.plan99.timestamper \ 8 | -Bmac.CFBundleName=Timestamper \ 9 | -Bicon=mac.icns \ 10 | -Bruntime="$jh/../../" \ 11 | -native dmg \ 12 | -name Timestamper \ 13 | -title Timestamper \ 14 | -vendor Vinumeris \ 15 | -outdir deploy \ 16 | -appclass timestamper.Main \ 17 | -srcfiles target/timestamper-shaded.jar \ 18 | -outfile Timestamper -------------------------------------------------------------------------------- /mac.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/devcoretalk/a4c07279f9e67b282b149495d2f64e8f26046302/mac.icns -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | timestamper 8 | net.plan99 9 | 0.1 10 | 11 | Timestamper 12 | 13 | 14 | 15 | 16 | org.apache.maven.plugins 17 | maven-compiler-plugin 18 | 19 | 1.8 20 | 1.8 21 | true 22 | true 23 | 24 | 25 | 26 | 27 | org.apache.maven.plugins 28 | maven-shade-plugin 29 | 30 | 31 | 32 | package 33 | 34 | shade 35 | 36 | 37 | false 38 | 39 | 40 | 41 | *:* 42 | 43 | META-INF/*.SF 44 | META-INF/*.DSA 45 | META-INF/*.RSA 46 | 47 | 48 | 49 | true 50 | bundled 51 | 52 | 53 | ${project.artifactId}.Main 54 | 55 | 56 | target/${project.artifactId}-shaded.jar 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | org.bitcoinj 68 | bitcoinj-core 69 | 0.13-SNAPSHOT 70 | 71 | 72 | org.slf4j 73 | slf4j-jdk14 74 | 1.7.10 75 | 76 | 77 | 78 | com.google.guava 79 | guava 80 | 16.0.1 81 | 82 | 89 | 90 | org.fxmisc.easybind 91 | easybind 92 | 1.0.2 93 | 94 | 95 | de.jensd 96 | fontawesomefx 97 | 8.0.0 98 | 99 | 100 | net.glxn 101 | qrgen 102 | 1.3 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /src/main/java/timestamper/Main.java: -------------------------------------------------------------------------------- 1 | package timestamper; 2 | 3 | import com.google.common.util.concurrent.*; 4 | import javafx.scene.input.*; 5 | import org.bitcoinj.core.NetworkParameters; 6 | import org.bitcoinj.kits.WalletAppKit; 7 | import org.bitcoinj.params.*; 8 | import org.bitcoinj.utils.BriefLogFormatter; 9 | import org.bitcoinj.utils.Threading; 10 | import org.bitcoinj.wallet.DeterministicSeed; 11 | import javafx.application.Application; 12 | import javafx.application.Platform; 13 | import javafx.fxml.FXMLLoader; 14 | import javafx.scene.Node; 15 | import javafx.scene.Scene; 16 | import javafx.scene.layout.Pane; 17 | import javafx.scene.layout.StackPane; 18 | import javafx.stage.Stage; 19 | import timestamper.controls.NotificationBarPane; 20 | import timestamper.utils.GuiUtils; 21 | import timestamper.utils.TextFieldValidator; 22 | 23 | import javax.annotation.Nullable; 24 | import java.io.File; 25 | import java.io.IOException; 26 | import java.net.URL; 27 | 28 | import static timestamper.utils.GuiUtils.*; 29 | 30 | public class Main extends Application { 31 | public static String APP_NAME = "Timestamper"; 32 | 33 | public static NetworkParameters params = RegTestParams.get(); 34 | public static WalletAppKit bitcoin; 35 | public static Main instance; 36 | 37 | private StackPane uiStack; 38 | private Pane mainUI; 39 | public MainController controller; 40 | public NotificationBarPane notificationBar; 41 | public Stage mainWindow; 42 | 43 | @Override 44 | public void start(Stage mainWindow) throws Exception { 45 | try { 46 | realStart(mainWindow); 47 | } catch (Throwable e) { 48 | GuiUtils.crashAlert(e); 49 | throw e; 50 | } 51 | } 52 | 53 | private void realStart(Stage mainWindow) throws IOException { 54 | instance = this; 55 | // Show the crash dialog for any exceptions that we don't handle and that hit the main loop. 56 | GuiUtils.handleCrashesOnThisThread(); 57 | 58 | this.mainWindow = mainWindow; 59 | 60 | if (System.getProperty("os.name").toLowerCase().contains("mac")) { 61 | // We could match the Mac Aqua style here, except that (a) Modena doesn't look that bad, and (b) 62 | // the date picker widget is kinda broken in AquaFx and I can't be bothered fixing it. 63 | // AquaFx.style(); 64 | } 65 | 66 | // Load the GUI. The MainController class will be automagically created and wired up. 67 | URL location = getClass().getResource("main.fxml"); 68 | FXMLLoader loader = new FXMLLoader(location); 69 | mainUI = loader.load(); 70 | controller = loader.getController(); 71 | // Configure the window with a StackPane so we can overlay things on top of the main UI, and a 72 | // NotificationBarPane so we can slide messages and progress bars in from the bottom. Note that 73 | // ordering of the construction and connection matters here, otherwise we get (harmless) CSS error 74 | // spew to the logs. 75 | notificationBar = new NotificationBarPane(mainUI); 76 | mainWindow.setTitle(APP_NAME); 77 | uiStack = new StackPane(); 78 | Scene scene = new Scene(uiStack); 79 | TextFieldValidator.configureScene(scene); // Add CSS that we need. 80 | scene.getStylesheets().add(getClass().getResource("wallet.css").toString()); 81 | uiStack.getChildren().add(notificationBar); 82 | mainWindow.setScene(scene); 83 | 84 | // Make log output concise. 85 | BriefLogFormatter.init(); 86 | // Tell bitcoinj to run event handlers on the JavaFX UI thread. This keeps things simple and means 87 | // we cannot forget to switch threads when adding event handlers. Unfortunately, the DownloadListener 88 | // we give to the app kit is currently an exception and runs on a library thread. It'll get fixed in 89 | // a future version. 90 | Threading.USER_THREAD = Platform::runLater; 91 | // Create the app kit. It won't do any heavyweight initialization until after we start it. 92 | setupWalletKit(null); 93 | 94 | if (bitcoin.isChainFileLocked()) { 95 | informationalAlert("Already running", "This application is already running and cannot be started twice."); 96 | Platform.exit(); 97 | return; 98 | } 99 | 100 | mainWindow.show(); 101 | 102 | WalletSetPasswordController.estimateKeyDerivationTimeMsec(); 103 | 104 | bitcoin.addListener(new Service.Listener() { 105 | @Override 106 | public void failed(Service.State from, Throwable failure) { 107 | GuiUtils.crashAlert(failure); 108 | } 109 | }, Platform::runLater); 110 | bitcoin.startAsync(); 111 | 112 | scene.getAccelerators().put(KeyCombination.valueOf("Shortcut+F"), () -> bitcoin.peerGroup().getDownloadPeer().close()); 113 | } 114 | 115 | public void setupWalletKit(@Nullable DeterministicSeed seed) { 116 | // If seed is non-null it means we are restoring from backup. 117 | bitcoin = new WalletAppKit(params, new File("."), APP_NAME + "-" + params.getPaymentProtocolId()) { 118 | @Override 119 | protected void onSetupCompleted() { 120 | // Don't make the user wait for confirmations for now, as the intention is they're sending it 121 | // their own money! 122 | bitcoin.wallet().allowSpendingUnconfirmedTransactions(); 123 | Platform.runLater(controller::onBitcoinSetup); 124 | } 125 | }; 126 | // Now configure and start the appkit. This will take a second or two - we could show a temporary splash screen 127 | // or progress widget to keep the user engaged whilst we initialise, but we don't. 128 | if (params == RegTestParams.get()) { 129 | bitcoin.connectToLocalHost(); // You should run a regtest mode bitcoind locally. 130 | } else if (params == TestNet3Params.get()) { 131 | // As an example! 132 | bitcoin.useTor(); 133 | // bitcoin.setDiscovery(new HttpDiscovery(params, URI.create("http://localhost:8080/peers"), ECKey.fromPublicOnly(BaseEncoding.base16().decode("02cba68cfd0679d10b186288b75a59f9132b1b3e222f6332717cb8c4eb2040f940".toUpperCase())))); 134 | } else { 135 | bitcoin.useTor(); 136 | } 137 | bitcoin.setDownloadListener(controller.progressBarUpdater()) 138 | .setBlockingStartup(false) 139 | .setUserAgent(APP_NAME, "1.0"); 140 | if (seed != null) 141 | bitcoin.restoreWalletFromSeed(seed); 142 | } 143 | 144 | private Node stopClickPane = new Pane(); 145 | 146 | public class OverlayUI { 147 | public Node ui; 148 | public T controller; 149 | 150 | public OverlayUI(Node ui, T controller) { 151 | this.ui = ui; 152 | this.controller = controller; 153 | } 154 | 155 | public void show() { 156 | checkGuiThread(); 157 | if (currentOverlay == null) { 158 | uiStack.getChildren().add(stopClickPane); 159 | uiStack.getChildren().add(ui); 160 | blurOut(mainUI); 161 | //darken(mainUI); 162 | fadeIn(ui); 163 | zoomIn(ui); 164 | } else { 165 | // Do a quick transition between the current overlay and the next. 166 | // Bug here: we don't pay attention to changes in outsideClickDismisses. 167 | explodeOut(currentOverlay.ui); 168 | fadeOutAndRemove(uiStack, currentOverlay.ui); 169 | uiStack.getChildren().add(ui); 170 | ui.setOpacity(0.0); 171 | fadeIn(ui, 100); 172 | zoomIn(ui, 100); 173 | } 174 | currentOverlay = this; 175 | } 176 | 177 | public void outsideClickDismisses() { 178 | stopClickPane.setOnMouseClicked((ev) -> done()); 179 | } 180 | 181 | public void done() { 182 | checkGuiThread(); 183 | if (ui == null) return; // In the middle of being dismissed and got an extra click. 184 | explodeOut(ui); 185 | fadeOutAndRemove(uiStack, ui, stopClickPane); 186 | blurIn(mainUI); 187 | //undark(mainUI); 188 | this.ui = null; 189 | this.controller = null; 190 | currentOverlay = null; 191 | } 192 | } 193 | 194 | @Nullable 195 | private OverlayUI currentOverlay; 196 | 197 | public OverlayUI overlayUI(Node node, T controller) { 198 | checkGuiThread(); 199 | OverlayUI pair = new OverlayUI(node, controller); 200 | // Auto-magically set the overlayUI member, if it's there. 201 | try { 202 | controller.getClass().getField("overlayUI").set(controller, pair); 203 | } catch (IllegalAccessException | NoSuchFieldException ignored) { 204 | } 205 | pair.show(); 206 | return pair; 207 | } 208 | 209 | /** Loads the FXML file with the given name, blurs out the main UI and puts this one on top. */ 210 | public OverlayUI overlayUI(String name) { 211 | try { 212 | checkGuiThread(); 213 | // Load the UI from disk. 214 | URL location = GuiUtils.getResource(name); 215 | FXMLLoader loader = new FXMLLoader(location); 216 | Pane ui = loader.load(); 217 | T controller = loader.getController(); 218 | OverlayUI pair = new OverlayUI(ui, controller); 219 | // Auto-magically set the overlayUI member, if it's there. 220 | try { 221 | if (controller != null) 222 | controller.getClass().getField("overlayUI").set(controller, pair); 223 | } catch (IllegalAccessException | NoSuchFieldException ignored) { 224 | ignored.printStackTrace(); 225 | } 226 | pair.show(); 227 | return pair; 228 | } catch (IOException e) { 229 | throw new RuntimeException(e); // Can't happen. 230 | } 231 | } 232 | 233 | @Override 234 | public void stop() throws Exception { 235 | bitcoin.stopAsync(); 236 | bitcoin.awaitTerminated(); 237 | // Forcibly terminate the JVM because Orchid likes to spew non-daemon threads everywhere. 238 | Runtime.getRuntime().exit(0); 239 | } 240 | 241 | public static void main(String[] args) { 242 | launch(args); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/main/java/timestamper/MainController.java: -------------------------------------------------------------------------------- 1 | package timestamper; 2 | 3 | import com.subgraph.orchid.*; 4 | import javafx.animation.*; 5 | import javafx.application.*; 6 | import javafx.beans.property.*; 7 | import javafx.event.*; 8 | import javafx.scene.control.*; 9 | import javafx.scene.layout.*; 10 | import javafx.stage.*; 11 | import javafx.util.*; 12 | import org.bitcoinj.core.*; 13 | import org.bitcoinj.script.*; 14 | import org.bitcoinj.store.*; 15 | import org.bitcoinj.utils.*; 16 | import org.fxmisc.easybind.*; 17 | import timestamper.controls.*; 18 | import timestamper.utils.*; 19 | import timestamper.utils.easing.*; 20 | 21 | import java.io.*; 22 | import java.nio.file.*; 23 | import java.util.*; 24 | 25 | import static timestamper.Main.*; 26 | import static timestamper.utils.GuiUtils.*; 27 | 28 | /** 29 | * Gets created auto-magically by FXMLLoader via reflection. The widget fields are set to the GUI controls they're named 30 | * after. This class handles all the updates and event handling for the main UI. 31 | */ 32 | public class MainController { 33 | public HBox controlsBox; 34 | public Label balance; 35 | public Button sendMoneyOutBtn; 36 | public ClickableBitcoinAddress addressControl; 37 | public ListView pendingProofsList; 38 | 39 | private static class Proof implements Serializable { 40 | byte[] tx, partialMerkleTree; 41 | Sha256Hash blockHash; 42 | 43 | transient SimpleIntegerProperty depth = new SimpleIntegerProperty(); 44 | transient String filename; 45 | 46 | public void saveTo(String filename) throws IOException { 47 | try (ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get(filename)))) { 48 | oos.writeObject(this); 49 | } 50 | } 51 | 52 | public static Proof readFrom(String filename) throws IOException { 53 | try (ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(filename)))) { 54 | return (Proof) ois.readObject(); 55 | } catch (ClassNotFoundException e) { 56 | throw new RuntimeException(e); 57 | } 58 | } 59 | } 60 | 61 | private BitcoinUIModel model = new BitcoinUIModel(); 62 | private NotificationBarPane.Item syncItem; 63 | 64 | // Called by FXMLLoader. 65 | public void initialize() { 66 | addressControl.setOpacity(0.0); 67 | 68 | pendingProofsList.setCellFactory(new Callback, ListCell>() { 69 | @Override 70 | public ListCell call(ListView param) { 71 | return new ListCell() { 72 | @Override 73 | protected void updateItem(Proof item, boolean empty) { 74 | super.updateItem(item, empty); 75 | if (empty) { 76 | setText(""); 77 | setGraphic(null); 78 | } else { 79 | setText("Proof for " + item.filename); 80 | ProgressBar bar = new ProgressBar(); 81 | bar.progressProperty().bind(item.depth.divide(3.0)); 82 | setGraphic(bar); 83 | } 84 | } 85 | }; 86 | } 87 | }); 88 | } 89 | 90 | public void onBitcoinSetup() { 91 | model.setWallet(bitcoin.wallet()); 92 | addressControl.addressProperty().bind(model.addressProperty()); 93 | balance.textProperty().bind(EasyBind.map(model.balanceProperty(), coin -> MonetaryFormat.BTC.noCode().format(coin).toString())); 94 | // Don't let the user click send money when the wallet is empty. 95 | sendMoneyOutBtn.disableProperty().bind(model.balanceProperty().isEqualTo(Coin.ZERO)); 96 | 97 | TorClient torClient = Main.bitcoin.peerGroup().getTorClient(); 98 | if (torClient != null) { 99 | SimpleDoubleProperty torProgress = new SimpleDoubleProperty(-1); 100 | String torMsg = "Initialising Tor"; 101 | syncItem = Main.instance.notificationBar.pushItem(torMsg, torProgress); 102 | torClient.addInitializationListener(new TorInitializationListener() { 103 | @Override 104 | public void initializationProgress(String message, int percent) { 105 | Platform.runLater(() -> { 106 | syncItem.label.set(torMsg + ": " + message); 107 | torProgress.set(percent / 100.0); 108 | }); 109 | } 110 | 111 | @Override 112 | public void initializationCompleted() { 113 | Platform.runLater(() -> { 114 | syncItem.cancel(); 115 | showBitcoinSyncMessage(); 116 | }); 117 | } 118 | }); 119 | } else { 120 | showBitcoinSyncMessage(); 121 | } 122 | model.syncProgressProperty().addListener(x -> { 123 | if (model.syncProgressProperty().get() >= 1.0) { 124 | readyToGoAnimation(); 125 | if (syncItem != null) { 126 | syncItem.cancel(); 127 | syncItem = null; 128 | } 129 | } else if (syncItem == null) { 130 | showBitcoinSyncMessage(); 131 | } 132 | }); 133 | } 134 | 135 | private void showBitcoinSyncMessage() { 136 | syncItem = Main.instance.notificationBar.pushItem("Synchronising with the Bitcoin network", model.syncProgressProperty()); 137 | } 138 | 139 | public void sendMoneyOut(ActionEvent event) { 140 | // Hide this UI and show the send money UI. This UI won't be clickable until the user dismisses send_money. 141 | Main.instance.overlayUI("send_money.fxml"); 142 | } 143 | 144 | public void settingsClicked(ActionEvent event) { 145 | Main.OverlayUI screen = Main.instance.overlayUI("wallet_settings.fxml"); 146 | screen.controller.initialize(null); 147 | } 148 | 149 | public void restoreFromSeedAnimation() { 150 | // Buttons slide out ... 151 | TranslateTransition leave = new TranslateTransition(Duration.millis(1200), controlsBox); 152 | leave.setByY(80.0); 153 | leave.play(); 154 | } 155 | 156 | public void readyToGoAnimation() { 157 | // Buttons slide in and clickable address appears simultaneously. 158 | TranslateTransition arrive = new TranslateTransition(Duration.millis(1200), controlsBox); 159 | arrive.setInterpolator(new ElasticInterpolator(EasingMode.EASE_OUT, 1, 2)); 160 | arrive.setToY(0.0); 161 | FadeTransition reveal = new FadeTransition(Duration.millis(1200), addressControl); 162 | reveal.setToValue(1.0); 163 | ParallelTransition group = new ParallelTransition(arrive, reveal); 164 | group.setDelay(NotificationBarPane.ANIM_OUT_DURATION); 165 | group.setCycleCount(1); 166 | group.play(); 167 | } 168 | 169 | public DownloadProgressTracker progressBarUpdater() { 170 | return model.getDownloadProgressTracker(); 171 | } 172 | 173 | public void onTimestampClicked(ActionEvent event) { 174 | // Ask the user for the document to timestamp 175 | File doc = new FileChooser().showOpenDialog(Main.instance.mainWindow); 176 | if (doc == null) return; // User cancelled 177 | try { 178 | timestamp(doc); 179 | } catch (IOException e) { 180 | crashAlert(e); 181 | } catch (InsufficientMoneyException e) { 182 | informationalAlert("Insufficient funds", 183 | "You need bitcoins in this wallet in order to pay network fees."); 184 | } 185 | } 186 | 187 | private void timestamp(File doc) throws IOException, InsufficientMoneyException { 188 | // Hash it 189 | Sha256Hash hash = Sha256Hash.hashFileContents(doc); 190 | 191 | // Create a tx with an OP_RETURN output 192 | Transaction tx = new Transaction(Main.params); 193 | tx.addOutput(Coin.ZERO, ScriptBuilder.createOpReturnScript(hash.getBytes())); 194 | 195 | // Send it to the Bitcoin network 196 | Main.bitcoin.wallet().sendCoins(Wallet.SendRequest.forTx(tx)); 197 | 198 | // Add it to the UI list 199 | Proof proof = new Proof(); 200 | proof.tx = tx.bitcoinSerialize(); 201 | proof.filename = doc.toString(); 202 | pendingProofsList.getItems().add(proof); 203 | 204 | // Grab the merkle branch when it appears in the block chain 205 | Main.bitcoin.peerGroup().addEventListener(new AbstractPeerEventListener() { 206 | @Override 207 | public void onBlocksDownloaded(Peer peer, Block block, FilteredBlock filteredBlock, int blocksLeft) { 208 | List hashes = new ArrayList<>(); 209 | PartialMerkleTree tree = filteredBlock.getPartialMerkleTree(); 210 | tree.getTxnHashAndMerkleRoot(hashes); 211 | if (hashes.contains(tx.getHash())) { 212 | proof.partialMerkleTree = tree.bitcoinSerialize(); 213 | proof.blockHash = filteredBlock.getHash(); 214 | } 215 | } 216 | }); 217 | 218 | // Wait for confirmations (3) 219 | tx.getConfidence().addEventListener((confidence, reason) -> { 220 | if (confidence.getConfidenceType() != TransactionConfidence.ConfidenceType.BUILDING) 221 | return; 222 | proof.depth.set(confidence.getDepthInBlocks()); 223 | if (proof.depth.get() == 3) { 224 | // Save the proof to disk 225 | String filename = doc.toString() + ".timestamp"; 226 | try { 227 | proof.saveTo(filename); 228 | // Remove it from the UI list 229 | pendingProofsList.getItems().remove(proof); 230 | // Notify the user that it's done 231 | informationalAlert("Proof complete", "Saved to " + filename); 232 | } catch (IOException e) { 233 | crashAlert(e); 234 | } 235 | } 236 | }); 237 | } 238 | 239 | public void onVerifyClicked(ActionEvent event) { 240 | // Ask the user for the document to verify 241 | FileChooser chooser = new FileChooser(); 242 | chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Timestamp proofs", "*.timestamp")); 243 | File proofFile = chooser.showOpenDialog(Main.instance.mainWindow); 244 | if (proofFile == null) return; // User cancelled. 245 | 246 | try { 247 | StoredBlock cursor = verify(proofFile); 248 | // Notify the user that the proof is valid and what the timestamp is 249 | informationalAlert("Proof valid!", "Document existed at " + cursor.getHeader().getTime()); 250 | } catch (IOException | BlockStoreException e) { 251 | crashAlert(e); 252 | } catch (ProofException e) { 253 | informationalAlert("Proof was invalid", e.getMessage()); 254 | } 255 | } 256 | 257 | private StoredBlock verify(File proofFile) throws IOException, ProofException, BlockStoreException { 258 | // Load the proof file 259 | Proof proof = Proof.readFrom(proofFile.getAbsolutePath()); 260 | 261 | // Hash the document 262 | String docFile = proofFile.getAbsoluteFile().toString().replace(".timestamp", ""); 263 | Sha256Hash hash = Sha256Hash.hashFileContents(new File(docFile)); 264 | 265 | // Verify the hash is in the OP_RETURN output of the tx 266 | Transaction tx = new Transaction(Main.params, proof.tx); 267 | boolean found = false; 268 | for (TransactionOutput output : tx.getOutputs()) { 269 | if (!output.getScriptPubKey().isOpReturn()) continue; 270 | //noinspection ConstantConditions 271 | if (!Arrays.equals(output.getScriptPubKey().getChunks().get(1).data, 272 | hash.getBytes())) 273 | throw new ProofException("Hash does not match OP_RETURN output"); 274 | found = true; 275 | break; 276 | } 277 | if (!found) throw new ProofException("No OP_RETURN output in transaction"); 278 | // Verify the transaction is in the Merkle proof 279 | PartialMerkleTree tree = new PartialMerkleTree(Main.params, proof.partialMerkleTree, 0); 280 | List hashes = new ArrayList<>(); 281 | Sha256Hash merkleRoot = tree.getTxnHashAndMerkleRoot(hashes); 282 | if (!hashes.contains(tx.getHash())) 283 | throw new ProofException("Transaction not found in Merkle proof"); 284 | 285 | // Find the block given the hash 286 | StoredBlock cursor = Main.bitcoin.chain().getChainHead(); 287 | while (cursor != null && !cursor.getHeader().getHash().equals(proof.blockHash)) { 288 | cursor = cursor.getPrev(Main.bitcoin.store()); 289 | } 290 | if (cursor == null) 291 | throw new ProofException("Could not find given block hash: " + proof.blockHash); 292 | 293 | // Verify the Merkle proof is linked to the block header 294 | if (!cursor.getHeader().getMerkleRoot().equals(merkleRoot)) 295 | throw new ProofException("Merkle root does not match block header"); 296 | return cursor; 297 | } 298 | 299 | private class ProofException extends Exception { 300 | public ProofException(String s) { 301 | super(s); 302 | } 303 | } 304 | } 305 | -------------------------------------------------------------------------------- /src/main/java/timestamper/SendMoneyController.java: -------------------------------------------------------------------------------- 1 | package timestamper; 2 | 3 | import javafx.scene.layout.HBox; 4 | import org.bitcoinj.core.*; 5 | import com.google.common.util.concurrent.FutureCallback; 6 | import com.google.common.util.concurrent.Futures; 7 | import javafx.event.ActionEvent; 8 | import javafx.scene.control.Button; 9 | import javafx.scene.control.Label; 10 | import javafx.scene.control.TextField; 11 | import org.spongycastle.crypto.params.KeyParameter; 12 | import timestamper.controls.BitcoinAddressValidator; 13 | import timestamper.utils.TextFieldValidator; 14 | import timestamper.utils.WTUtils; 15 | 16 | import static com.google.common.base.Preconditions.checkState; 17 | import static timestamper.utils.GuiUtils.*; 18 | 19 | public class SendMoneyController { 20 | public Button sendBtn; 21 | public Button cancelBtn; 22 | public TextField address; 23 | public Label titleLabel; 24 | public TextField amountEdit; 25 | public Label btcLabel; 26 | 27 | public Main.OverlayUI overlayUI; 28 | 29 | private Wallet.SendResult sendResult; 30 | private KeyParameter aesKey; 31 | 32 | // Called by FXMLLoader 33 | public void initialize() { 34 | Coin balance = Main.bitcoin.wallet().getBalance(); 35 | checkState(!balance.isZero()); 36 | new BitcoinAddressValidator(Main.params, address, sendBtn); 37 | new TextFieldValidator(amountEdit, text -> 38 | !WTUtils.didThrow(() -> checkState(Coin.parseCoin(text).compareTo(balance) <= 0))); 39 | amountEdit.setText(balance.toPlainString()); 40 | } 41 | 42 | public void cancel(ActionEvent event) { 43 | overlayUI.done(); 44 | } 45 | 46 | public void send(ActionEvent event) { 47 | // Address exception cannot happen as we validated it beforehand. 48 | try { 49 | Coin amount = Coin.parseCoin(amountEdit.getText()); 50 | Address destination = new Address(Main.params, address.getText()); 51 | Wallet.SendRequest req; 52 | if (amount.equals(Main.bitcoin.wallet().getBalance())) 53 | req = Wallet.SendRequest.emptyWallet(destination); 54 | else 55 | req = Wallet.SendRequest.to(destination, amount); 56 | req.aesKey = aesKey; 57 | sendResult = Main.bitcoin.wallet().sendCoins(req); 58 | Futures.addCallback(sendResult.broadcastComplete, new FutureCallback() { 59 | @Override 60 | public void onSuccess(Transaction result) { 61 | checkGuiThread(); 62 | overlayUI.done(); 63 | } 64 | 65 | @Override 66 | public void onFailure(Throwable t) { 67 | // We died trying to empty the wallet. 68 | crashAlert(t); 69 | } 70 | }); 71 | sendResult.tx.getConfidence().addEventListener((tx, reason) -> { 72 | if (reason == TransactionConfidence.Listener.ChangeReason.SEEN_PEERS) 73 | updateTitleForBroadcast(); 74 | }); 75 | sendBtn.setDisable(true); 76 | address.setDisable(true); 77 | ((HBox)amountEdit.getParent()).getChildren().remove(amountEdit); 78 | ((HBox)btcLabel.getParent()).getChildren().remove(btcLabel); 79 | updateTitleForBroadcast(); 80 | } catch (InsufficientMoneyException e) { 81 | informationalAlert("Could not empty the wallet", 82 | "You may have too little money left in the wallet to make a transaction."); 83 | overlayUI.done(); 84 | } catch (ECKey.KeyIsEncryptedException e) { 85 | askForPasswordAndRetry(); 86 | } catch (AddressFormatException e) { 87 | // Cannot happen because we already validated it when the text field changed. 88 | throw new RuntimeException(e); 89 | } 90 | } 91 | 92 | private void askForPasswordAndRetry() { 93 | Main.OverlayUI pwd = Main.instance.overlayUI("wallet_password.fxml"); 94 | final String addressStr = address.getText(); 95 | final String amountStr = amountEdit.getText(); 96 | pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> { 97 | // We only get here if the user found the right password. If they don't or they cancel, we end up back on 98 | // the main UI screen. By now the send money screen is history so we must recreate it. 99 | checkGuiThread(); 100 | Main.OverlayUI screen = Main.instance.overlayUI("send_money.fxml"); 101 | screen.controller.aesKey = cur; 102 | screen.controller.address.setText(addressStr); 103 | screen.controller.amountEdit.setText(amountStr); 104 | screen.controller.send(null); 105 | }); 106 | } 107 | 108 | private void updateTitleForBroadcast() { 109 | final int peers = sendResult.tx.getConfidence().numBroadcastPeers(); 110 | titleLabel.setText(String.format("Broadcasting ... seen by %d peers", peers)); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/timestamper/WalletPasswordController.java: -------------------------------------------------------------------------------- 1 | package timestamper; 2 | 3 | import javafx.application.Platform; 4 | import org.bitcoinj.crypto.KeyCrypterScrypt; 5 | import com.google.common.primitives.Longs; 6 | import com.google.protobuf.ByteString; 7 | import javafx.beans.property.ReadOnlyObjectProperty; 8 | import javafx.beans.property.SimpleObjectProperty; 9 | import javafx.event.ActionEvent; 10 | import javafx.fxml.FXML; 11 | import javafx.scene.control.Label; 12 | import javafx.scene.control.PasswordField; 13 | import javafx.scene.control.ProgressIndicator; 14 | import javafx.scene.image.ImageView; 15 | import javafx.scene.layout.GridPane; 16 | import javafx.scene.layout.HBox; 17 | import org.slf4j.Logger; 18 | import org.slf4j.LoggerFactory; 19 | import org.spongycastle.crypto.params.KeyParameter; 20 | import timestamper.utils.KeyDerivationTasks; 21 | 22 | import java.time.Duration; 23 | 24 | import static com.google.common.base.Preconditions.checkNotNull; 25 | import static timestamper.utils.GuiUtils.*; 26 | 27 | /** 28 | * User interface for entering a password on demand, e.g. to send money. Also used when encrypting a wallet. Shows a 29 | * progress meter as we scrypt the password. 30 | */ 31 | public class WalletPasswordController { 32 | private static final Logger log = LoggerFactory.getLogger(WalletPasswordController.class); 33 | 34 | @FXML HBox buttonsBox; 35 | @FXML PasswordField pass1; 36 | @FXML ImageView padlockImage; 37 | @FXML ProgressIndicator progressMeter; 38 | @FXML GridPane widgetGrid; 39 | @FXML Label explanationLabel; 40 | 41 | public Main.OverlayUI overlayUI; 42 | 43 | private SimpleObjectProperty aesKey = new SimpleObjectProperty<>(); 44 | 45 | public void initialize() { 46 | progressMeter.setOpacity(0); 47 | Platform.runLater(pass1::requestFocus); 48 | } 49 | 50 | @FXML void confirmClicked(ActionEvent event) { 51 | String password = pass1.getText(); 52 | if (password.isEmpty() || password.length() < 4) { 53 | informationalAlert("Bad password", "The password you entered is empty or too short."); 54 | return; 55 | } 56 | 57 | final KeyCrypterScrypt keyCrypter = (KeyCrypterScrypt) Main.bitcoin.wallet().getKeyCrypter(); 58 | checkNotNull(keyCrypter); // We should never arrive at this GUI if the wallet isn't actually encrypted. 59 | KeyDerivationTasks tasks = new KeyDerivationTasks(keyCrypter, password, getTargetTime()) { 60 | @Override 61 | protected void onFinish(KeyParameter aesKey, int timeTakenMsec) { 62 | checkGuiThread(); 63 | if (Main.bitcoin.wallet().checkAESKey(aesKey)) { 64 | WalletPasswordController.this.aesKey.set(aesKey); 65 | } else { 66 | log.warn("User entered incorrect password"); 67 | fadeOut(progressMeter); 68 | fadeIn(widgetGrid); 69 | fadeIn(explanationLabel); 70 | fadeIn(buttonsBox); 71 | informationalAlert("Wrong password", 72 | "Please try entering your password again, carefully checking for typos or spelling errors."); 73 | } 74 | } 75 | }; 76 | progressMeter.progressProperty().bind(tasks.progress); 77 | tasks.start(); 78 | 79 | fadeIn(progressMeter); 80 | fadeOut(widgetGrid); 81 | fadeOut(explanationLabel); 82 | fadeOut(buttonsBox); 83 | } 84 | 85 | public void cancelClicked(ActionEvent event) { 86 | overlayUI.done(); 87 | } 88 | 89 | public ReadOnlyObjectProperty aesKeyProperty() { 90 | return aesKey; 91 | } 92 | 93 | /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 94 | 95 | public static final String TAG = WalletPasswordController.class.getName() + ".target-time"; 96 | 97 | // Writes the given time to the wallet as a tag so we can find it again in this class. 98 | public static void setTargetTime(Duration targetTime) { 99 | ByteString bytes = ByteString.copyFrom(Longs.toByteArray(targetTime.toMillis())); 100 | Main.bitcoin.wallet().setTag(TAG, bytes); 101 | } 102 | 103 | // Reads target time or throws if not set yet (should never happen). 104 | public static Duration getTargetTime() throws IllegalArgumentException { 105 | return Duration.ofMillis(Longs.fromByteArray(Main.bitcoin.wallet().getTag(TAG).toByteArray())); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/timestamper/WalletSetPasswordController.java: -------------------------------------------------------------------------------- 1 | package timestamper; 2 | 3 | import com.google.protobuf.*; 4 | import javafx.application.*; 5 | import javafx.event.*; 6 | import javafx.fxml.*; 7 | import javafx.scene.control.*; 8 | import javafx.scene.layout.*; 9 | import org.bitcoinj.crypto.*; 10 | import org.bitcoinj.wallet.*; 11 | import org.slf4j.*; 12 | import org.spongycastle.crypto.params.*; 13 | import timestamper.utils.*; 14 | 15 | import java.time.*; 16 | import java.util.concurrent.*; 17 | 18 | import static timestamper.utils.GuiUtils.*; 19 | 20 | public class WalletSetPasswordController { 21 | private static final Logger log = LoggerFactory.getLogger(WalletSetPasswordController.class); 22 | public PasswordField pass1, pass2; 23 | 24 | public ProgressIndicator progressMeter; 25 | public GridPane widgetGrid; 26 | public Button closeButton; 27 | public Label explanationLabel; 28 | 29 | public Main.OverlayUI overlayUI; 30 | // These params were determined empirically on a top-range (as of 2014) MacBook Pro with native scrypt support, 31 | // using the scryptenc command line tool from the original scrypt distribution, given a memory limit of 40mb. 32 | public static final Protos.ScryptParameters SCRYPT_PARAMETERS = Protos.ScryptParameters.newBuilder() 33 | .setP(6) 34 | .setR(8) 35 | .setN(32768) 36 | .setSalt(ByteString.copyFrom(KeyCrypterScrypt.randomSalt())) 37 | .build(); 38 | 39 | public void initialize() { 40 | progressMeter.setOpacity(0); 41 | } 42 | 43 | public static Duration estimatedKeyDerivationTime = null; 44 | 45 | public static CompletableFuture estimateKeyDerivationTimeMsec() { 46 | // This is run in the background after startup. If we haven't recorded it before, do a key derivation to see 47 | // how long it takes. This helps us produce better progress feedback, as on Windows we don't currently have a 48 | // native Scrypt impl and the Java version is ~3 times slower, plus it depends a lot on CPU speed. 49 | CompletableFuture future = new CompletableFuture<>(); 50 | new Thread(() -> { 51 | log.info("Doing background test key derivation"); 52 | KeyCrypterScrypt scrypt = new KeyCrypterScrypt(SCRYPT_PARAMETERS); 53 | long start = System.currentTimeMillis(); 54 | scrypt.deriveKey("test password"); 55 | long msec = System.currentTimeMillis() - start; 56 | log.info("Background test key derivation took {}msec", msec); 57 | Platform.runLater(() -> { 58 | estimatedKeyDerivationTime = Duration.ofMillis(msec); 59 | future.complete(estimatedKeyDerivationTime); 60 | }); 61 | }).start(); 62 | return future; 63 | } 64 | 65 | @FXML 66 | public void setPasswordClicked(ActionEvent event) { 67 | if (!pass1.getText().equals(pass2.getText())) { 68 | informationalAlert("Passwords do not match", "Try re-typing your chosen passwords."); 69 | return; 70 | } 71 | String password = pass1.getText(); 72 | // This is kind of arbitrary and we could do much more to help people pick strong passwords. 73 | if (password.length() < 4) { 74 | informationalAlert("Password too short", "You need to pick a password at least five characters or longer."); 75 | return; 76 | } 77 | 78 | fadeIn(progressMeter); 79 | fadeOut(widgetGrid); 80 | fadeOut(explanationLabel); 81 | fadeOut(closeButton); 82 | 83 | 84 | KeyCrypterScrypt scrypt = new KeyCrypterScrypt(SCRYPT_PARAMETERS); 85 | 86 | // Deriving the actual key runs on a background thread. 500msec is empirical on my laptop (actual val is more like 333 but we give padding time). 87 | KeyDerivationTasks tasks = new KeyDerivationTasks(scrypt, password, estimatedKeyDerivationTime) { 88 | @Override 89 | protected void onFinish(KeyParameter aesKey, int timeTakenMsec) { 90 | // Write the target time to the wallet so we can make the progress bar work when entering the password. 91 | WalletPasswordController.setTargetTime(Duration.ofMillis(timeTakenMsec)); 92 | // The actual encryption part doesn't take very long as most private keys are derived on demand. 93 | log.info("Key derived, now encrypting"); 94 | Main.bitcoin.wallet().encrypt(scrypt, aesKey); 95 | log.info("Encryption done"); 96 | informationalAlert("Wallet encrypted", 97 | "You can remove the password at any time from the settings screen."); 98 | overlayUI.done(); 99 | } 100 | }; 101 | progressMeter.progressProperty().bind(tasks.progress); 102 | tasks.start(); 103 | } 104 | 105 | public void closeClicked(ActionEvent event) { 106 | overlayUI.done(); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/timestamper/WalletSettingsController.java: -------------------------------------------------------------------------------- 1 | package timestamper; 2 | 3 | import org.bitcoinj.crypto.MnemonicCode; 4 | import org.bitcoinj.wallet.DeterministicSeed; 5 | import com.google.common.base.Joiner; 6 | import com.google.common.base.Splitter; 7 | import com.google.common.util.concurrent.Service; 8 | import javafx.application.Platform; 9 | import javafx.beans.binding.BooleanBinding; 10 | import javafx.event.ActionEvent; 11 | import javafx.fxml.FXML; 12 | import javafx.scene.control.Button; 13 | import javafx.scene.control.DatePicker; 14 | import javafx.scene.control.TextArea; 15 | import org.slf4j.Logger; 16 | import org.slf4j.LoggerFactory; 17 | import org.spongycastle.crypto.params.KeyParameter; 18 | import timestamper.utils.TextFieldValidator; 19 | 20 | import javax.annotation.Nullable; 21 | import java.time.Instant; 22 | import java.time.LocalDate; 23 | import java.time.ZoneId; 24 | import java.time.ZoneOffset; 25 | import java.util.List; 26 | 27 | import static com.google.common.base.Preconditions.checkNotNull; 28 | import static javafx.beans.binding.Bindings.*; 29 | import static timestamper.utils.GuiUtils.checkGuiThread; 30 | import static timestamper.utils.GuiUtils.informationalAlert; 31 | import static timestamper.utils.WTUtils.didThrow; 32 | import static timestamper.utils.WTUtils.unchecked; 33 | 34 | public class WalletSettingsController { 35 | private static final Logger log = LoggerFactory.getLogger(WalletSettingsController.class); 36 | 37 | @FXML Button passwordButton; 38 | @FXML DatePicker datePicker; 39 | @FXML TextArea wordsArea; 40 | @FXML Button restoreButton; 41 | 42 | public Main.OverlayUI overlayUI; 43 | 44 | private KeyParameter aesKey; 45 | 46 | // Note: NOT called by FXMLLoader! 47 | public void initialize(@Nullable KeyParameter aesKey) { 48 | DeterministicSeed seed = Main.bitcoin.wallet().getKeyChainSeed(); 49 | if (aesKey == null) { 50 | if (seed.isEncrypted()) { 51 | log.info("Wallet is encrypted, requesting password first."); 52 | // Delay execution of this until after we've finished initialising this screen. 53 | Platform.runLater(() -> askForPasswordAndRetry()); 54 | return; 55 | } 56 | } else { 57 | this.aesKey = aesKey; 58 | seed = seed.decrypt(checkNotNull(Main.bitcoin.wallet().getKeyCrypter()), "", aesKey); 59 | // Now we can display the wallet seed as appropriate. 60 | passwordButton.setText("Remove password"); 61 | } 62 | 63 | // Set the date picker to show the birthday of this wallet. 64 | Instant creationTime = Instant.ofEpochSecond(seed.getCreationTimeSeconds()); 65 | LocalDate origDate = creationTime.atZone(ZoneId.systemDefault()).toLocalDate(); 66 | datePicker.setValue(origDate); 67 | 68 | // Set the mnemonic seed words. 69 | final List mnemonicCode = seed.getMnemonicCode(); 70 | checkNotNull(mnemonicCode); // Already checked for encryption. 71 | String origWords = Joiner.on(" ").join(mnemonicCode); 72 | wordsArea.setText(origWords); 73 | 74 | // Validate words as they are being typed. 75 | MnemonicCode codec = unchecked(MnemonicCode::new); 76 | TextFieldValidator validator = new TextFieldValidator(wordsArea, text -> 77 | !didThrow(() -> codec.check(Splitter.on(' ').splitToList(text))) 78 | ); 79 | 80 | // Clear the date picker if the user starts editing the words, if it contained the current wallets date. 81 | // This forces them to set the birthday field when restoring. 82 | wordsArea.textProperty().addListener(o -> { 83 | if (origDate.equals(datePicker.getValue())) 84 | datePicker.setValue(null); 85 | }); 86 | 87 | BooleanBinding datePickerIsInvalid = or( 88 | datePicker.valueProperty().isNull(), 89 | 90 | createBooleanBinding(() -> 91 | datePicker.getValue().isAfter(LocalDate.now()) 92 | , /* depends on */ datePicker.valueProperty()) 93 | ); 94 | 95 | // Don't let the user click restore if the words area contains the current wallet words, or are an invalid set, 96 | // or if the date field isn't set, or if it's in the future. 97 | restoreButton.disableProperty().bind( 98 | or( 99 | or( 100 | not(validator.valid), 101 | equal(origWords, wordsArea.textProperty()) 102 | ), 103 | 104 | datePickerIsInvalid 105 | ) 106 | ); 107 | 108 | // Highlight the date picker in red if it's empty or in the future, so the user knows why restore is disabled. 109 | datePickerIsInvalid.addListener((dp, old, cur) -> { 110 | if (cur) { 111 | datePicker.getStyleClass().add("validation_error"); 112 | } else { 113 | datePicker.getStyleClass().remove("validation_error"); 114 | } 115 | }); 116 | } 117 | 118 | private void askForPasswordAndRetry() { 119 | Main.OverlayUI pwd = Main.instance.overlayUI("wallet_password.fxml"); 120 | pwd.controller.aesKeyProperty().addListener((observable, old, cur) -> { 121 | // We only get here if the user found the right password. If they don't or they cancel, we end up back on 122 | // the main UI screen. 123 | checkGuiThread(); 124 | Main.OverlayUI screen = Main.instance.overlayUI("wallet_settings.fxml"); 125 | screen.controller.initialize(cur); 126 | }); 127 | } 128 | 129 | public void closeClicked(ActionEvent event) { 130 | overlayUI.done(); 131 | } 132 | 133 | public void restoreClicked(ActionEvent event) { 134 | // Don't allow a restore unless this wallet is presently empty. We don't want to end up with two wallets, too 135 | // much complexity, even though WalletAppKit will keep the current one as a backup file in case of disaster. 136 | if (Main.bitcoin.wallet().getBalance().value > 0) { 137 | informationalAlert("Wallet is not empty", 138 | "You must empty this wallet out before attempting to restore an older one, as mixing wallets " + 139 | "together can lead to invalidated backups."); 140 | return; 141 | } 142 | 143 | if (aesKey != null) { 144 | // This is weak. We should encrypt the new seed here. 145 | informationalAlert("Wallet is encrypted", 146 | "After restore, the wallet will no longer be encrypted and you must set a new password."); 147 | } 148 | 149 | log.info("Attempting wallet restore using seed '{}' from date {}", wordsArea.getText(), datePicker.getValue()); 150 | informationalAlert("Wallet restore in progress", 151 | "Your wallet will now be resynced from the Bitcoin network. This can take a long time for old wallets."); 152 | overlayUI.done(); 153 | Main.instance.controller.restoreFromSeedAnimation(); 154 | 155 | long birthday = datePicker.getValue().atStartOfDay().toEpochSecond(ZoneOffset.UTC); 156 | DeterministicSeed seed = new DeterministicSeed(Splitter.on(' ').splitToList(wordsArea.getText()), null, "", birthday); 157 | // Shut down bitcoinj and restart it with the new seed. 158 | Main.bitcoin.addListener(new Service.Listener() { 159 | @Override 160 | public void terminated(Service.State from) { 161 | Main.instance.setupWalletKit(seed); 162 | Main.bitcoin.startAsync(); 163 | } 164 | }, Platform::runLater); 165 | Main.bitcoin.stopAsync(); 166 | } 167 | 168 | 169 | public void passwordButtonClicked(ActionEvent event) { 170 | if (aesKey == null) { 171 | Main.instance.overlayUI("wallet_set_password.fxml"); 172 | } else { 173 | Main.bitcoin.wallet().decrypt(aesKey); 174 | informationalAlert("Wallet decrypted", "A password will no longer be required to send money or edit settings."); 175 | passwordButton.setText("Set password"); 176 | aesKey = null; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/main/java/timestamper/controls/BitcoinAddressValidator.java: -------------------------------------------------------------------------------- 1 | package timestamper.controls; 2 | 3 | import org.bitcoinj.core.Address; 4 | import org.bitcoinj.core.AddressFormatException; 5 | import org.bitcoinj.core.NetworkParameters; 6 | import javafx.scene.Node; 7 | import javafx.scene.control.TextField; 8 | import timestamper.utils.TextFieldValidator; 9 | 10 | /** 11 | * Given a text field, some network params and optionally some nodes, will make the text field an angry red colour 12 | * if the address is invalid for those params, and enable/disable the nodes. 13 | */ 14 | public class BitcoinAddressValidator { 15 | private NetworkParameters params; 16 | private Node[] nodes; 17 | 18 | public BitcoinAddressValidator(NetworkParameters params, TextField field, Node... nodes) { 19 | this.params = params; 20 | this.nodes = nodes; 21 | 22 | // Handle the red highlighting, but don't highlight in red just when the field is empty because that makes 23 | // the example/prompt address hard to read. 24 | new TextFieldValidator(field, text -> text.isEmpty() || testAddr(text)); 25 | // However we do want the buttons to be disabled when empty so we apply a different test there. 26 | field.textProperty().addListener((observableValue, prev, current) -> { 27 | toggleButtons(current); 28 | }); 29 | toggleButtons(field.getText()); 30 | } 31 | 32 | private void toggleButtons(String current) { 33 | boolean valid = testAddr(current); 34 | for (Node n : nodes) n.setDisable(!valid); 35 | } 36 | 37 | private boolean testAddr(String text) { 38 | try { 39 | new Address(params, text); 40 | return true; 41 | } catch (AddressFormatException e) { 42 | return false; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/timestamper/controls/ClickableBitcoinAddress.java: -------------------------------------------------------------------------------- 1 | package timestamper.controls; 2 | 3 | import org.bitcoinj.core.Address; 4 | import org.bitcoinj.uri.BitcoinURI; 5 | import de.jensd.fx.fontawesome.AwesomeDude; 6 | import de.jensd.fx.fontawesome.AwesomeIcon; 7 | import javafx.beans.binding.StringExpression; 8 | import javafx.beans.property.ObjectProperty; 9 | import javafx.beans.property.SimpleObjectProperty; 10 | import javafx.event.ActionEvent; 11 | import javafx.fxml.FXML; 12 | import javafx.fxml.FXMLLoader; 13 | import javafx.scene.control.ContextMenu; 14 | import javafx.scene.control.Label; 15 | import javafx.scene.control.Tooltip; 16 | import javafx.scene.effect.DropShadow; 17 | import javafx.scene.image.Image; 18 | import javafx.scene.image.ImageView; 19 | import javafx.scene.input.Clipboard; 20 | import javafx.scene.input.ClipboardContent; 21 | import javafx.scene.input.MouseButton; 22 | import javafx.scene.input.MouseEvent; 23 | import javafx.scene.layout.AnchorPane; 24 | import javafx.scene.layout.Pane; 25 | import net.glxn.qrgen.QRCode; 26 | import net.glxn.qrgen.image.ImageType; 27 | import timestamper.Main; 28 | import timestamper.utils.GuiUtils; 29 | 30 | import java.awt.*; 31 | import java.io.ByteArrayInputStream; 32 | import java.io.IOException; 33 | import java.net.URI; 34 | 35 | import static javafx.beans.binding.Bindings.convert; 36 | 37 | /** 38 | * A custom control that implements a clickable, copyable Bitcoin address. Clicking it opens a local wallet app. The 39 | * address looks like a blue hyperlink. Next to it there are two icons, one that copies to the clipboard and another 40 | * that shows a QRcode. 41 | */ 42 | public class ClickableBitcoinAddress extends AnchorPane { 43 | @FXML protected Label addressLabel; 44 | @FXML protected ContextMenu addressMenu; 45 | @FXML protected Label copyWidget; 46 | @FXML protected Label qrCode; 47 | 48 | protected SimpleObjectProperty
address = new SimpleObjectProperty<>(); 49 | private final StringExpression addressStr; 50 | 51 | public ClickableBitcoinAddress() { 52 | try { 53 | FXMLLoader loader = new FXMLLoader(getClass().getResource("bitcoin_address.fxml")); 54 | loader.setRoot(this); 55 | loader.setController(this); 56 | // The following line is supposed to help Scene Builder, although it doesn't seem to be needed for me. 57 | loader.setClassLoader(getClass().getClassLoader()); 58 | loader.load(); 59 | 60 | AwesomeDude.setIcon(copyWidget, AwesomeIcon.COPY); 61 | Tooltip.install(copyWidget, new Tooltip("Copy address to clipboard")); 62 | 63 | AwesomeDude.setIcon(qrCode, AwesomeIcon.QRCODE); 64 | Tooltip.install(qrCode, new Tooltip("Show a barcode scannable with a mobile phone for this address")); 65 | 66 | addressStr = convert(address); 67 | addressLabel.textProperty().bind(addressStr); 68 | } catch (IOException e) { 69 | throw new RuntimeException(e); 70 | } 71 | } 72 | 73 | public String uri() { 74 | return BitcoinURI.convertToBitcoinURI(address.get(), null, Main.APP_NAME, null); 75 | } 76 | 77 | public Address getAddress() { 78 | return address.get(); 79 | } 80 | 81 | public void setAddress(Address address) { 82 | this.address.set(address); 83 | } 84 | 85 | public ObjectProperty
addressProperty() { 86 | return address; 87 | } 88 | 89 | @FXML 90 | protected void copyAddress(ActionEvent event) { 91 | // User clicked icon or menu item. 92 | Clipboard clipboard = Clipboard.getSystemClipboard(); 93 | ClipboardContent content = new ClipboardContent(); 94 | content.putString(addressStr.get()); 95 | content.putHtml(String.format("%s", uri(), addressStr.get())); 96 | clipboard.setContent(content); 97 | } 98 | 99 | @FXML 100 | protected void requestMoney(MouseEvent event) { 101 | if (event.getButton() == MouseButton.SECONDARY || (event.getButton() == MouseButton.PRIMARY && event.isMetaDown())) { 102 | // User right clicked or the Mac equivalent. Show the context menu. 103 | addressMenu.show(addressLabel, event.getScreenX(), event.getScreenY()); 104 | } else { 105 | // User left clicked. 106 | try { 107 | Desktop.getDesktop().browse(URI.create(uri())); 108 | } catch (IOException e) { 109 | GuiUtils.informationalAlert("Opening wallet app failed", "Perhaps you don't have one installed?"); 110 | } 111 | } 112 | } 113 | 114 | @FXML 115 | protected void copyWidgetClicked(MouseEvent event) { 116 | copyAddress(null); 117 | } 118 | 119 | @FXML 120 | protected void showQRCode(MouseEvent event) { 121 | // Serialize to PNG and back into an image. Pretty lame but it's the shortest code to write and I'm feeling 122 | // lazy tonight. 123 | final byte[] imageBytes = QRCode 124 | .from(uri()) 125 | .withSize(320, 240) 126 | .to(ImageType.PNG) 127 | .stream() 128 | .toByteArray(); 129 | Image qrImage = new Image(new ByteArrayInputStream(imageBytes)); 130 | ImageView view = new ImageView(qrImage); 131 | view.setEffect(new DropShadow()); 132 | // Embed the image in a pane to ensure the drop-shadow interacts with the fade nicely, otherwise it looks weird. 133 | // Then fix the width/height to stop it expanding to fill the parent, which would result in the image being 134 | // non-centered on the screen. Finally fade/blur it in. 135 | Pane pane = new Pane(view); 136 | pane.setMaxSize(qrImage.getWidth(), qrImage.getHeight()); 137 | final Main.OverlayUI overlay = Main.instance.overlayUI(pane, this); 138 | view.setOnMouseClicked(event1 -> overlay.done()); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/timestamper/controls/NotificationBarPane.java: -------------------------------------------------------------------------------- 1 | package timestamper.controls; 2 | 3 | 4 | import javafx.animation.Interpolator; 5 | import javafx.animation.KeyFrame; 6 | import javafx.animation.KeyValue; 7 | import javafx.animation.Timeline; 8 | import javafx.beans.property.SimpleStringProperty; 9 | import javafx.beans.value.ObservableDoubleValue; 10 | import javafx.collections.FXCollections; 11 | import javafx.collections.ListChangeListener; 12 | import javafx.collections.ObservableList; 13 | import javafx.scene.Node; 14 | import javafx.scene.control.Label; 15 | import javafx.scene.control.ProgressBar; 16 | import javafx.scene.layout.BorderPane; 17 | import javafx.scene.layout.HBox; 18 | import javafx.scene.layout.Priority; 19 | import javafx.util.Duration; 20 | import timestamper.utils.GuiUtils; 21 | import timestamper.utils.easing.EasingMode; 22 | import timestamper.utils.easing.ElasticInterpolator; 23 | 24 | import javax.annotation.Nullable; 25 | 26 | /** 27 | * Wraps the given Node in a BorderPane and allows a thin bar to slide in from the bottom or top, squeezing the content 28 | * node. The API allows different "items" to be added/removed and they will be displayed one at a time, fading between 29 | * them when the topmost is removed. Each item is meant to be used for e.g. a background task and can contain a button 30 | * and/or a progress bar. 31 | */ 32 | public class NotificationBarPane extends BorderPane { 33 | public static final Duration ANIM_IN_DURATION = GuiUtils.UI_ANIMATION_TIME.multiply(2); 34 | public static final Duration ANIM_OUT_DURATION = GuiUtils.UI_ANIMATION_TIME; 35 | 36 | private HBox bar; 37 | private Label label; 38 | private double barHeight; 39 | private ProgressBar progressBar; 40 | 41 | public class Item { 42 | public final SimpleStringProperty label; 43 | @Nullable public final ObservableDoubleValue progress; 44 | 45 | public Item(String label, @Nullable ObservableDoubleValue progress) { 46 | this.label = new SimpleStringProperty(label); 47 | this.progress = progress; 48 | } 49 | 50 | public void cancel() { 51 | items.remove(this); 52 | } 53 | } 54 | 55 | public final ObservableList items; 56 | 57 | public NotificationBarPane(Node content) { 58 | super(content); 59 | progressBar = new ProgressBar(); 60 | label = new Label("infobar!"); 61 | bar = new HBox(label); 62 | bar.setMinHeight(0.0); 63 | bar.getStyleClass().add("info-bar"); 64 | bar.setFillHeight(true); 65 | setBottom(bar); 66 | // Figure out the height of the bar based on the CSS. Must wait until after we've been added to the parent node. 67 | sceneProperty().addListener(o -> { 68 | if (getParent() == null) return; 69 | getParent().applyCss(); 70 | getParent().layout(); 71 | barHeight = bar.getHeight(); 72 | bar.setPrefHeight(0.0); 73 | }); 74 | items = FXCollections.observableArrayList(); 75 | items.addListener((ListChangeListener) change -> { 76 | config(); 77 | showOrHide(); 78 | }); 79 | } 80 | 81 | private void config() { 82 | if (items.isEmpty()) return; 83 | Item item = items.get(0); 84 | 85 | bar.getChildren().clear(); 86 | label.textProperty().bind(item.label); 87 | label.setMaxWidth(Double.MAX_VALUE); 88 | HBox.setHgrow(label, Priority.ALWAYS); 89 | bar.getChildren().add(label); 90 | if (item.progress != null) { 91 | progressBar.setMinWidth(200); 92 | progressBar.progressProperty().bind(item.progress); 93 | bar.getChildren().add(progressBar); 94 | } 95 | } 96 | 97 | private void showOrHide() { 98 | if (items.isEmpty()) 99 | animateOut(); 100 | else 101 | animateIn(); 102 | } 103 | 104 | public boolean isShowing() { 105 | return bar.getPrefHeight() > 0; 106 | } 107 | 108 | private void animateIn() { 109 | animate(barHeight); 110 | } 111 | 112 | private void animateOut() { 113 | animate(0.0); 114 | } 115 | 116 | private Timeline timeline; 117 | protected void animate(Number target) { 118 | if (timeline != null) { 119 | timeline.stop(); 120 | timeline = null; 121 | } 122 | Duration duration; 123 | Interpolator interpolator; 124 | if (target.intValue() > 0) { 125 | interpolator = new ElasticInterpolator(EasingMode.EASE_OUT, 1, 2); 126 | duration = ANIM_IN_DURATION; 127 | } else { 128 | interpolator = Interpolator.EASE_OUT; 129 | duration = ANIM_OUT_DURATION; 130 | } 131 | KeyFrame kf = new KeyFrame(duration, new KeyValue(bar.prefHeightProperty(), target, interpolator)); 132 | timeline = new Timeline(kf); 133 | timeline.setOnFinished(x -> timeline = null); 134 | timeline.play(); 135 | } 136 | 137 | public Item pushItem(String string, @Nullable ObservableDoubleValue progress) { 138 | Item i = new Item(string, progress); 139 | items.add(i); 140 | return i; 141 | } 142 | } -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/AlertWindowController.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils; 2 | 3 | import javafx.scene.control.Button; 4 | import javafx.scene.control.Label; 5 | import javafx.stage.Stage; 6 | 7 | public class AlertWindowController { 8 | public Label messageLabel; 9 | public Label detailsLabel; 10 | public Button okButton; 11 | public Button cancelButton; 12 | public Button actionButton; 13 | 14 | /** Initialize this alert dialog for information about a crash. */ 15 | public void crashAlert(Stage stage, String crashMessage) { 16 | messageLabel.setText("Unfortunately, we screwed up and the app crashed. Sorry about that!"); 17 | detailsLabel.setText(crashMessage); 18 | 19 | cancelButton.setVisible(false); 20 | actionButton.setVisible(false); 21 | okButton.setOnAction(actionEvent -> stage.close()); 22 | } 23 | 24 | /** Initialize this alert for general information: OK button only, nothing happens on dismissal. */ 25 | public void informational(Stage stage, String message, String details) { 26 | messageLabel.setText(message); 27 | detailsLabel.setText(details); 28 | cancelButton.setVisible(false); 29 | actionButton.setVisible(false); 30 | okButton.setOnAction(actionEvent -> stage.close()); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/BitcoinUIModel.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils; 2 | 3 | import org.bitcoinj.core.*; 4 | import javafx.application.Platform; 5 | import javafx.beans.property.ReadOnlyDoubleProperty; 6 | import javafx.beans.property.ReadOnlyObjectProperty; 7 | import javafx.beans.property.SimpleDoubleProperty; 8 | import javafx.beans.property.SimpleObjectProperty; 9 | 10 | import java.util.Date; 11 | 12 | /** 13 | * A class that exposes relevant bitcoin stuff as JavaFX bindable properties. 14 | */ 15 | public class BitcoinUIModel { 16 | private SimpleObjectProperty
address = new SimpleObjectProperty<>(); 17 | private SimpleObjectProperty balance = new SimpleObjectProperty<>(Coin.ZERO); 18 | private SimpleDoubleProperty syncProgress = new SimpleDoubleProperty(-1); 19 | private ProgressBarUpdater syncProgressUpdater = new ProgressBarUpdater(); 20 | 21 | public BitcoinUIModel() { 22 | } 23 | 24 | public BitcoinUIModel(Wallet wallet) { 25 | setWallet(wallet); 26 | } 27 | 28 | public void setWallet(Wallet wallet) { 29 | wallet.addEventListener(new AbstractWalletEventListener() { 30 | @Override 31 | public void onWalletChanged(Wallet wallet) { 32 | super.onWalletChanged(wallet); 33 | update(wallet); 34 | } 35 | }, Platform::runLater); 36 | update(wallet); 37 | } 38 | 39 | private void update(Wallet wallet) { 40 | balance.set(wallet.getBalance()); 41 | address.set(wallet.currentReceiveAddress()); 42 | } 43 | 44 | private class ProgressBarUpdater extends DownloadProgressTracker { 45 | @Override 46 | protected void progress(double pct, int blocksLeft, Date date) { 47 | super.progress(pct, blocksLeft, date); 48 | Platform.runLater(() -> syncProgress.set(pct / 100.0)); 49 | } 50 | 51 | @Override 52 | protected void doneDownload() { 53 | super.doneDownload(); 54 | Platform.runLater(() -> syncProgress.set(1.0)); 55 | } 56 | } 57 | 58 | public DownloadProgressTracker getDownloadProgressTracker() { return syncProgressUpdater; } 59 | 60 | public ReadOnlyDoubleProperty syncProgressProperty() { return syncProgress; } 61 | 62 | public ReadOnlyObjectProperty
addressProperty() { 63 | return address; 64 | } 65 | 66 | public ReadOnlyObjectProperty balanceProperty() { 67 | return balance; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/GuiUtils.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils; 2 | 3 | import com.google.common.base.Throwables; 4 | import javafx.animation.*; 5 | import javafx.application.Platform; 6 | import javafx.fxml.FXMLLoader; 7 | import javafx.scene.Node; 8 | import javafx.scene.Scene; 9 | import javafx.scene.effect.GaussianBlur; 10 | import javafx.scene.layout.Pane; 11 | import javafx.stage.Modality; 12 | import javafx.stage.Stage; 13 | import javafx.util.Duration; 14 | import timestamper.MainController; 15 | 16 | import java.io.IOException; 17 | import java.net.URL; 18 | import java.util.function.BiConsumer; 19 | 20 | import static com.google.common.base.Preconditions.checkState; 21 | import static timestamper.utils.WTUtils.unchecked; 22 | 23 | public class GuiUtils { 24 | public static void runAlert(BiConsumer setup) { 25 | try { 26 | // JavaFX2 doesn't actually have a standard alert template. Instead the Scene Builder app will create FXML 27 | // files for an alert window for you, and then you customise it as you see fit. I guess it makes sense in 28 | // an odd sort of way. 29 | Stage dialogStage = new Stage(); 30 | dialogStage.initModality(Modality.APPLICATION_MODAL); 31 | FXMLLoader loader = new FXMLLoader(GuiUtils.class.getResource("alert.fxml")); 32 | Pane pane = loader.load(); 33 | AlertWindowController controller = loader.getController(); 34 | setup.accept(dialogStage, controller); 35 | dialogStage.setScene(new Scene(pane)); 36 | dialogStage.showAndWait(); 37 | } catch (IOException e) { 38 | // We crashed whilst trying to show the alert dialog (this should never happen). Give up! 39 | throw new RuntimeException(e); 40 | } 41 | } 42 | 43 | public static void crashAlert(Throwable t) { 44 | t.printStackTrace(); 45 | Throwable rootCause = Throwables.getRootCause(t); 46 | Runnable r = () -> { 47 | runAlert((stage, controller) -> controller.crashAlert(stage, rootCause.toString())); 48 | Platform.exit(); 49 | }; 50 | if (Platform.isFxApplicationThread()) 51 | r.run(); 52 | else 53 | Platform.runLater(r); 54 | } 55 | 56 | /** Show a GUI alert box for any unhandled exceptions that propagate out of this thread. */ 57 | public static void handleCrashesOnThisThread() { 58 | Thread.currentThread().setUncaughtExceptionHandler((thread, exception) -> { 59 | GuiUtils.crashAlert(Throwables.getRootCause(exception)); 60 | }); 61 | } 62 | 63 | public static void informationalAlert(String message, String details, Object... args) { 64 | String formattedDetails = String.format(details, args); 65 | Runnable r = () -> runAlert((stage, controller) -> controller.informational(stage, message, formattedDetails)); 66 | if (Platform.isFxApplicationThread()) 67 | r.run(); 68 | else 69 | Platform.runLater(r); 70 | } 71 | 72 | public static final int UI_ANIMATION_TIME_MSEC = 600; 73 | public static final Duration UI_ANIMATION_TIME = Duration.millis(UI_ANIMATION_TIME_MSEC); 74 | 75 | public static Animation fadeIn(Node ui) { 76 | return fadeIn(ui, 0); 77 | } 78 | 79 | public static Animation fadeIn(Node ui, int delayMillis) { 80 | ui.setCache(true); 81 | FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui); 82 | ft.setFromValue(0.0); 83 | ft.setToValue(1.0); 84 | ft.setOnFinished(ev -> ui.setCache(false)); 85 | ft.setDelay(Duration.millis(delayMillis)); 86 | ft.play(); 87 | return ft; 88 | } 89 | 90 | public static Animation fadeOut(Node ui) { 91 | FadeTransition ft = new FadeTransition(Duration.millis(UI_ANIMATION_TIME_MSEC), ui); 92 | ft.setFromValue(ui.getOpacity()); 93 | ft.setToValue(0.0); 94 | ft.play(); 95 | return ft; 96 | } 97 | 98 | public static Animation fadeOutAndRemove(Pane parentPane, Node... nodes) { 99 | Animation animation = fadeOut(nodes[0]); 100 | animation.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes)); 101 | return animation; 102 | } 103 | 104 | public static Animation fadeOutAndRemove(Duration duration, Pane parentPane, Node... nodes) { 105 | nodes[0].setCache(true); 106 | FadeTransition ft = new FadeTransition(duration, nodes[0]); 107 | ft.setFromValue(nodes[0].getOpacity()); 108 | ft.setToValue(0.0); 109 | ft.setOnFinished(actionEvent -> parentPane.getChildren().removeAll(nodes)); 110 | ft.play(); 111 | return ft; 112 | } 113 | 114 | public static void blurOut(Node node) { 115 | GaussianBlur blur = new GaussianBlur(0.0); 116 | node.setEffect(blur); 117 | Timeline timeline = new Timeline(); 118 | KeyValue kv = new KeyValue(blur.radiusProperty(), 10.0); 119 | KeyFrame kf = new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC), kv); 120 | timeline.getKeyFrames().add(kf); 121 | timeline.play(); 122 | } 123 | 124 | public static void blurIn(Node node) { 125 | GaussianBlur blur = (GaussianBlur) node.getEffect(); 126 | Timeline timeline = new Timeline(); 127 | KeyValue kv = new KeyValue(blur.radiusProperty(), 0.0); 128 | KeyFrame kf = new KeyFrame(Duration.millis(UI_ANIMATION_TIME_MSEC), kv); 129 | timeline.getKeyFrames().add(kf); 130 | timeline.setOnFinished(actionEvent -> node.setEffect(null)); 131 | timeline.play(); 132 | } 133 | 134 | public static ScaleTransition zoomIn(Node node) { 135 | return zoomIn(node, 0); 136 | } 137 | 138 | public static ScaleTransition zoomIn(Node node, int delayMillis) { 139 | return scaleFromTo(node, 0.95, 1.0, delayMillis); 140 | } 141 | 142 | public static ScaleTransition explodeOut(Node node) { 143 | return scaleFromTo(node, 1.0, 1.05, 0); 144 | } 145 | 146 | private static ScaleTransition scaleFromTo(Node node, double from, double to, int delayMillis) { 147 | ScaleTransition scale = new ScaleTransition(Duration.millis(UI_ANIMATION_TIME_MSEC / 2), node); 148 | scale.setFromX(from); 149 | scale.setFromY(from); 150 | scale.setToX(to); 151 | scale.setToY(to); 152 | scale.setDelay(Duration.millis(delayMillis)); 153 | scale.play(); 154 | return scale; 155 | } 156 | 157 | /** 158 | * A useful helper for development purposes. Used as a switch for loading files from local disk, allowing live 159 | * editing whilst the app runs without rebuilds. 160 | */ 161 | public static URL getResource(String name) { 162 | if (false) 163 | return unchecked(() -> new URL("file:///your/path/here/src/main/timestamper/" + name)); 164 | else 165 | return MainController.class.getResource(name); 166 | } 167 | 168 | public static void checkGuiThread() { 169 | checkState(Platform.isFxApplicationThread()); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/KeyDerivationTasks.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils; 2 | 3 | import org.bitcoinj.crypto.KeyCrypterScrypt; 4 | import com.google.common.util.concurrent.Uninterruptibles; 5 | import javafx.beans.property.ReadOnlyDoubleProperty; 6 | import javafx.concurrent.Task; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.spongycastle.crypto.params.KeyParameter; 10 | 11 | import javax.annotation.*; 12 | import java.time.Duration; 13 | import java.util.concurrent.TimeUnit; 14 | 15 | import static timestamper.utils.GuiUtils.checkGuiThread; 16 | 17 | /** 18 | * Background tasks for pumping a progress meter and deriving an AES key using scrypt. 19 | */ 20 | public class KeyDerivationTasks { 21 | private static final Logger log = LoggerFactory.getLogger(KeyDerivationTasks.class); 22 | 23 | public final Task keyDerivationTask; 24 | public final ReadOnlyDoubleProperty progress; 25 | 26 | private final Task progressTask; 27 | 28 | private volatile int timeTakenMsec = -1; 29 | 30 | public KeyDerivationTasks(KeyCrypterScrypt scrypt, String password, @Nullable Duration targetTime) { 31 | keyDerivationTask = new Task() { 32 | @Override 33 | protected KeyParameter call() throws Exception { 34 | long start = System.currentTimeMillis(); 35 | try { 36 | log.info("Started key derivation"); 37 | KeyParameter result = scrypt.deriveKey(password); 38 | timeTakenMsec = (int) (System.currentTimeMillis() - start); 39 | log.info("Key derivation done in {}ms", timeTakenMsec); 40 | return result; 41 | } catch (Throwable e) { 42 | log.error("Exception during key derivation", e); 43 | throw e; 44 | } 45 | } 46 | }; 47 | 48 | // And the fake progress meter ... if the vals were calculated correctly progress bar should reach 100% 49 | // a brief moment after the keys were derived successfully. 50 | progressTask = new Task() { 51 | private KeyParameter aesKey; 52 | 53 | @Override 54 | protected Void call() throws Exception { 55 | if (targetTime != null) { 56 | long startTime = System.currentTimeMillis(); 57 | long curTime; 58 | long targetTimeMillis = targetTime.toMillis(); 59 | while ((curTime = System.currentTimeMillis()) < startTime + targetTimeMillis) { 60 | double progress = (curTime - startTime) / (double) targetTimeMillis; 61 | updateProgress(progress, 1.0); 62 | 63 | // 60fps would require 16msec sleep here. 64 | Uninterruptibles.sleepUninterruptibly(20, TimeUnit.MILLISECONDS); 65 | } 66 | // Wait for the encryption thread before switching back to main UI. 67 | updateProgress(1.0, 1.0); 68 | } else { 69 | updateProgress(-1, -1); 70 | } 71 | aesKey = keyDerivationTask.get(); 72 | return null; 73 | } 74 | 75 | @Override 76 | protected void succeeded() { 77 | checkGuiThread(); 78 | onFinish(aesKey, timeTakenMsec); 79 | } 80 | }; 81 | progress = progressTask.progressProperty(); 82 | } 83 | 84 | public void start() { 85 | new Thread(keyDerivationTask, "Key derivation").start(); 86 | new Thread(progressTask, "Progress ticker").start(); 87 | } 88 | 89 | protected void onFinish(KeyParameter aesKey, int timeTakenMsec) { 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/TextFieldValidator.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils; 2 | 3 | import javafx.beans.property.BooleanProperty; 4 | import javafx.beans.property.SimpleBooleanProperty; 5 | import javafx.scene.Scene; 6 | import javafx.scene.control.TextInputControl; 7 | 8 | import java.util.function.Predicate; 9 | 10 | public class TextFieldValidator { 11 | public final BooleanProperty valid = new SimpleBooleanProperty(false); 12 | 13 | public TextFieldValidator(TextInputControl control, Predicate validator) { 14 | this.valid.set(validator.test(control.getText())); 15 | apply(control, valid.get()); 16 | control.textProperty().addListener((observableValue, prev, current) -> { 17 | boolean nowValid = validator.test(current); 18 | if (nowValid == valid.get()) return; 19 | valid.set(nowValid); 20 | }); 21 | valid.addListener(o -> apply(control, valid.get())); 22 | } 23 | 24 | private static void apply(TextInputControl textField, boolean nowValid) { 25 | if (nowValid) { 26 | textField.getStyleClass().remove("validation_error"); 27 | } else { 28 | textField.getStyleClass().add("validation_error"); 29 | } 30 | } 31 | 32 | public static void configureScene(Scene scene) { 33 | final String file = TextFieldValidator.class.getResource("text-validation.css").toString(); 34 | scene.getStylesheets().add(file); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/ThrottledRunLater.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Licensed under the Apache License, Version 2.0 (the "License"); 3 | * you may not use this file except in compliance with the License. 4 | * You may obtain a copy of the License at 5 | * 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Unless required by applicable law or agreed to in writing, software 9 | * distributed under the License is distributed on an "AS IS" BASIS, 10 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | * See the License for the specific language governing permissions and 12 | * limitations under the License. 13 | */ 14 | 15 | package timestamper.utils; 16 | 17 | import javafx.application.Platform; 18 | 19 | import java.util.concurrent.atomic.AtomicBoolean; 20 | 21 | /** 22 | * A simple wrapper around {@link javafx.application.Platform#runLater(Runnable)} which will do nothing if the previous 23 | * invocation of runLater didn't execute on the JavaFX UI thread yet. In this way you can avoid flooding 24 | * the event loop if you have a background thread that for whatever reason wants to update the UI very 25 | * frequently. Without this class you could end up bloating up memory usage and causing the UI to stutter 26 | * if the UI thread couldn't keep up with your background worker. 27 | */ 28 | public class ThrottledRunLater implements Runnable { 29 | private final Runnable runnable; 30 | private final AtomicBoolean pending = new AtomicBoolean(); 31 | 32 | /** Created this way, the no-args runLater will execute this classes run method. */ 33 | public ThrottledRunLater() { 34 | this.runnable = null; 35 | } 36 | 37 | /** Created this way, the no-args runLater will execute the given runnable. */ 38 | public ThrottledRunLater(Runnable runnable) { 39 | this.runnable = runnable; 40 | } 41 | 42 | public void runLater(Runnable runnable) { 43 | if (!pending.getAndSet(true)) { 44 | Platform.runLater(() -> { 45 | pending.set(false); 46 | runnable.run(); 47 | }); 48 | } 49 | } 50 | 51 | public void runLater() { 52 | runLater(runnable != null ? runnable : this); 53 | } 54 | 55 | @Override 56 | public void run() { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/WTUtils.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils; 2 | 3 | import org.slf4j.Logger; 4 | import org.slf4j.LoggerFactory; 5 | 6 | /** 7 | * Some generic utilities to make Java a bit less annoying. 8 | */ 9 | public class WTUtils { 10 | private static final Logger log = LoggerFactory.getLogger(WTUtils.class); 11 | 12 | public interface UncheckedRun { 13 | public T run() throws Throwable; 14 | } 15 | 16 | public interface UncheckedRunnable { 17 | public void run() throws Throwable; 18 | } 19 | 20 | public static T unchecked(UncheckedRun run) { 21 | try { 22 | return run.run(); 23 | } catch (Throwable throwable) { 24 | throw new RuntimeException(throwable); 25 | } 26 | } 27 | 28 | public static void uncheck(UncheckedRunnable run) { 29 | try { 30 | run.run(); 31 | } catch (Throwable throwable) { 32 | throw new RuntimeException(throwable); 33 | } 34 | } 35 | 36 | public static void ignoreAndLog(UncheckedRunnable runnable) { 37 | try { 38 | runnable.run(); 39 | } catch (Throwable t) { 40 | log.error("Ignoring error", t); 41 | } 42 | } 43 | 44 | public static T ignoredAndLogged(UncheckedRun runnable) { 45 | try { 46 | return runnable.run(); 47 | } catch (Throwable t) { 48 | log.error("Ignoring error", t); 49 | return null; 50 | } 51 | } 52 | 53 | public static boolean didThrow(UncheckedRun run) { 54 | try { 55 | run.run(); 56 | return false; 57 | } catch (Throwable throwable) { 58 | return true; 59 | } 60 | } 61 | 62 | public static boolean didThrow(UncheckedRunnable run) { 63 | try { 64 | run.run(); 65 | return false; 66 | } catch (Throwable throwable) { 67 | return true; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/easing/EasingInterpolator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2013, Christian Schudt 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package timestamper.utils.easing; 26 | 27 | import javafx.animation.Interpolator; 28 | import javafx.beans.property.ObjectProperty; 29 | import javafx.beans.property.SimpleObjectProperty; 30 | 31 | /** 32 | * The abstract base class for all easing interpolators. 33 | * 34 | * @author Christian Schudt 35 | */ 36 | public abstract class EasingInterpolator extends Interpolator { 37 | 38 | /** 39 | * The easing mode. 40 | */ 41 | private ObjectProperty easingMode = new SimpleObjectProperty<>(EasingMode.EASE_OUT); 42 | 43 | /** 44 | * Constructs the interpolator with a specific easing mode. 45 | * 46 | * @param easingMode The easing mode. 47 | */ 48 | public EasingInterpolator(EasingMode easingMode) { 49 | this.easingMode.set(easingMode); 50 | } 51 | 52 | /** 53 | * The easing mode property. 54 | * 55 | * @return The property. 56 | * @see #getEasingMode() 57 | * @see #setEasingMode(EasingMode) 58 | */ 59 | public ObjectProperty easingModeProperty() { 60 | return easingMode; 61 | } 62 | 63 | /** 64 | * Gets the easing mode. 65 | * 66 | * @return The easing mode. 67 | * @see #easingModeProperty() 68 | */ 69 | public EasingMode getEasingMode() { 70 | return easingMode.get(); 71 | } 72 | 73 | /** 74 | * Sets the easing mode. 75 | * 76 | * @param easingMode The easing mode. 77 | * @see #easingModeProperty() 78 | */ 79 | public void setEasingMode(EasingMode easingMode) { 80 | this.easingMode.set(easingMode); 81 | } 82 | 83 | /** 84 | * Defines the base curve for the interpolator. 85 | * The base curve is then transformed into an easing-in, easing-out easing-both curve. 86 | * 87 | * @param v The normalized value/time/progress of the interpolation (between 0 and 1). 88 | * @return The resulting value of the function, should return a value between 0 and 1. 89 | * @see Interpolator#curve(double) 90 | */ 91 | protected abstract double baseCurve(final double v); 92 | 93 | /** 94 | * Curves the function depending on the easing mode. 95 | * 96 | * @param v The normalized value (between 0 and 1). 97 | * @return The resulting value of the function. 98 | */ 99 | @Override 100 | protected final double curve(final double v) { 101 | switch (easingMode.get()) { 102 | case EASE_IN: 103 | return baseCurve(v); 104 | case EASE_OUT: 105 | return 1 - baseCurve(1 - v); 106 | case EASE_BOTH: 107 | if (v <= 0.5) { 108 | return baseCurve(2 * v) / 2; 109 | } else { 110 | return (2 - baseCurve(2 * (1 - v))) / 2; 111 | } 112 | 113 | } 114 | return baseCurve(v); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/easing/EasingMode.java: -------------------------------------------------------------------------------- 1 | package timestamper.utils.easing; 2 | 3 | /** 4 | * Defines the three easing modes, ease-in, ease-out and ease-both. 5 | * 6 | * @author Christian Schudt 7 | */ 8 | public enum EasingMode { 9 | EASE_IN, 10 | EASE_OUT, 11 | EASE_BOTH 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/timestamper/utils/easing/ElasticInterpolator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * The MIT License (MIT) 3 | * 4 | * Copyright (c) 2013, Christian Schudt 5 | * 6 | * Permission is hereby granted, free of charge, to any person obtaining a copy 7 | * of this software and associated documentation files (the "Software"), to deal 8 | * in the Software without restriction, including without limitation the rights 9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | * copies of the Software, and to permit persons to whom the Software is 11 | * furnished to do so, subject to the following conditions: 12 | * 13 | * The above copyright notice and this permission notice shall be included in 14 | * all copies or substantial portions of the Software. 15 | * 16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | * THE SOFTWARE. 23 | */ 24 | 25 | package timestamper.utils.easing; 26 | 27 | import javafx.beans.property.DoubleProperty; 28 | import javafx.beans.property.SimpleDoubleProperty; 29 | 30 | /** 31 | * This interpolator simulates an elastic behavior. 32 | *

33 | * The following curve illustrates the interpolation. 34 | *

35 | * 36 | * 37 | * x 38 | * 39 | * t 40 | * 42 | * 43 | *

44 | * The math in this class is taken from 45 | * http://www.robertpenner.com/easing/. 46 | * 47 | * @author Christian Schudt 48 | */ 49 | public class ElasticInterpolator extends EasingInterpolator { 50 | 51 | /** 52 | * The amplitude. 53 | */ 54 | private DoubleProperty amplitude = new SimpleDoubleProperty(this, "amplitude", 1); 55 | 56 | /** 57 | * The number of oscillations. 58 | */ 59 | private DoubleProperty oscillations = new SimpleDoubleProperty(this, "oscillations", 3); 60 | 61 | /** 62 | * Default constructor. Initializes the interpolator with ease out mode. 63 | */ 64 | public ElasticInterpolator() { 65 | this(EasingMode.EASE_OUT); 66 | } 67 | 68 | /** 69 | * Constructs the interpolator with a specific easing mode. 70 | * 71 | * @param easingMode The easing mode. 72 | */ 73 | public ElasticInterpolator(EasingMode easingMode) { 74 | super(easingMode); 75 | } 76 | 77 | /** 78 | * Sets the easing mode. 79 | * 80 | * @param easingMode The easing mode. 81 | * @see #easingModeProperty() 82 | */ 83 | public ElasticInterpolator(EasingMode easingMode, double amplitude, double oscillations) { 84 | super(easingMode); 85 | this.amplitude.set(amplitude); 86 | this.oscillations.set(oscillations); 87 | } 88 | 89 | /** 90 | * The oscillations property. Defines number of oscillations. 91 | * 92 | * @return The property. 93 | * @see #getOscillations() 94 | * @see #setOscillations(double) 95 | */ 96 | public DoubleProperty oscillationsProperty() { 97 | return oscillations; 98 | } 99 | 100 | /** 101 | * The amplitude. The minimum value is 1. If this value is < 1 it will be set to 1 during animation. 102 | * 103 | * @return The property. 104 | * @see #getAmplitude() 105 | * @see #setAmplitude(double) 106 | */ 107 | public DoubleProperty amplitudeProperty() { 108 | return amplitude; 109 | } 110 | 111 | /** 112 | * Gets the amplitude. 113 | * 114 | * @return The amplitude. 115 | * @see #amplitudeProperty() 116 | */ 117 | public double getAmplitude() { 118 | return amplitude.get(); 119 | } 120 | 121 | /** 122 | * Sets the amplitude. 123 | * 124 | * @param amplitude The amplitude. 125 | * @see #amplitudeProperty() 126 | */ 127 | public void setAmplitude(final double amplitude) { 128 | this.amplitude.set(amplitude); 129 | } 130 | 131 | /** 132 | * Gets the number of oscillations. 133 | * 134 | * @return The oscillations. 135 | * @see #oscillationsProperty() 136 | */ 137 | public double getOscillations() { 138 | return oscillations.get(); 139 | } 140 | 141 | /** 142 | * Sets the number of oscillations. 143 | * 144 | * @param oscillations The oscillations. 145 | * @see #oscillationsProperty() 146 | */ 147 | public void setOscillations(final double oscillations) { 148 | this.oscillations.set(oscillations); 149 | } 150 | 151 | @Override 152 | protected double baseCurve(double v) { 153 | if (v == 0) { 154 | return 0; 155 | } 156 | if (v == 1) { 157 | return 1; 158 | } 159 | double p = 1.0 / oscillations.get(); 160 | double a = amplitude.get(); 161 | double s; 162 | if (a < Math.abs(1)) { 163 | a = 1; 164 | s = p / 4; 165 | } else { 166 | s = p / (2 * Math.PI) * Math.asin(1 / a); 167 | } 168 | return -(a * Math.pow(2, 10 * (v -= 1)) * Math.sin((v - s) * (2 * Math.PI) / p)); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/resources/timestamper/200px-Padlock.svg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/devcoretalk/a4c07279f9e67b282b149495d2f64e8f26046302/src/main/resources/timestamper/200px-Padlock.svg.png -------------------------------------------------------------------------------- /src/main/resources/timestamper/bitcoin_logo_plain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/devcoretalk/a4c07279f9e67b282b149495d2f64e8f26046302/src/main/resources/timestamper/bitcoin_logo_plain.png -------------------------------------------------------------------------------- /src/main/resources/timestamper/checkpoints: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/devcoretalk/a4c07279f9e67b282b149495d2f64e8f26046302/src/main/resources/timestamper/checkpoints -------------------------------------------------------------------------------- /src/main/resources/timestamper/checkpoints.testnet: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikehearn/devcoretalk/a4c07279f9e67b282b149495d2f64e8f26046302/src/main/resources/timestamper/checkpoints.testnet -------------------------------------------------------------------------------- /src/main/resources/timestamper/controls/bitcoin_address.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/resources/timestamper/main.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 24 | 25 | 26 | 34 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 54 | 55 | 56 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /src/main/resources/timestamper/utils/text-validation.css: -------------------------------------------------------------------------------- 1 | .validation_error { 2 | -fx-background-color: red, 3 | linear-gradient( 4 | to bottom, 5 | derive(red,70%) 5%, 6 | derive(red,90%) 40% 7 | ); 8 | } 9 | 10 | .text-area.validation_error .content { 11 | -fx-background-color: red, 12 | linear-gradient( 13 | to bottom, 14 | derive(red,70%) 5%, 15 | derive(red,90%) 40% 16 | ); 17 | } 18 | 19 | .date-picker.validation_error > .text-field { 20 | -fx-background-color: red, 21 | linear-gradient( 22 | to bottom, 23 | derive(red,70%) 5%, 24 | derive(red,90%) 40% 25 | ); 26 | } 27 | 28 | .validation_warning { 29 | -fx-background-color: orange, 30 | linear-gradient( 31 | to bottom, 32 | derive(orange,70%) 5%, 33 | derive(orange,90%) 40% 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/main/resources/timestamper/wallet.css: -------------------------------------------------------------------------------- 1 | .mnemonic-area *.text { 2 | -fx-text-alignment: center; 3 | } 4 | 5 | .title-banner { 6 | -fx-background-color: #b3fbc4; 7 | } 8 | 9 | .title-label { 10 | -fx-background-color: darkseagreen; 11 | -fx-text-fill: white; 12 | } 13 | 14 | .root-pane { 15 | -fx-background-color: white; 16 | } 17 | 18 | .info-bar { 19 | -fx-background-color: linear-gradient(to bottom, black, darkslategray); 20 | -fx-padding: 10; 21 | } 22 | .info-bar > Label { 23 | -fx-text-fill: white; 24 | -fx-font-weight: bold; 25 | } 26 | .info-bar > .progress-bar > .bar { 27 | -fx-padding: 8; 28 | } 29 | .info-bar > .progress-bar > .track { 30 | -fx-opacity: 0.0; 31 | } 32 | 33 | 34 | .fat-button { 35 | -fx-padding: 10 15 10 15; 36 | -fx-min-width: 100; 37 | -fx-base: whitesmoke; 38 | } 39 | 40 | .fat-button:default { 41 | -fx-base: orange; 42 | -fx-text-fill: white; 43 | } 44 | 45 | .fat-button:cancel { 46 | -fx-background-color: lightgray, white; 47 | -fx-background-insets: 0, 1; 48 | -fx-background-radius: 3; 49 | -fx-text-fill: black; 50 | } 51 | 52 | .fat-button:cancel:hover { 53 | -fx-base: white; 54 | -fx-background-insets: -1, 0, 1, 2; 55 | -fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color; 56 | -fx-background-radius: 3; 57 | -fx-text-fill: black; 58 | } 59 | -------------------------------------------------------------------------------- /src/main/resources/timestamper/wallet_password.fxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 36 | 37 | 38 | 39 | 43 | 44 | 45 |