Initial commit
This commit is contained in:
		
						commit
						76c94b76cc
					
				
							
								
								
									
										132
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,132 @@ | |||
| # Created by https://www.gitignore.io/api/java,gradle,intellij | ||||
| # Edit at https://www.gitignore.io/?templates=java,gradle,intellij | ||||
| 
 | ||||
| ### Intellij ### | ||||
| # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm | ||||
| # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 | ||||
| 
 | ||||
| .idea/ | ||||
| 
 | ||||
| # User-specific stuff | ||||
| .idea/**/workspace.xml | ||||
| .idea/**/tasks.xml | ||||
| .idea/**/usage.statistics.xml | ||||
| .idea/**/dictionaries | ||||
| .idea/**/shelf | ||||
| 
 | ||||
| # Generated files | ||||
| .idea/**/contentModel.xml | ||||
| 
 | ||||
| # Sensitive or high-churn files | ||||
| .idea/**/dataSources/ | ||||
| .idea/**/dataSources.ids | ||||
| .idea/**/dataSources.local.xml | ||||
| .idea/**/sqlDataSources.xml | ||||
| .idea/**/dynamic.xml | ||||
| .idea/**/uiDesigner.xml | ||||
| .idea/**/dbnavigator.xml | ||||
| 
 | ||||
| # Gradle | ||||
| .idea/**/gradle.xml | ||||
| .idea/**/libraries | ||||
| 
 | ||||
| # Gradle and Maven with auto-import | ||||
| # When using Gradle or Maven with auto-import, you should exclude module files, | ||||
| # since they will be recreated, and may cause churn.  Uncomment if using | ||||
| # auto-import. | ||||
| .idea/modules.xml | ||||
| .idea/*.iml | ||||
| .idea/modules | ||||
| 
 | ||||
| # CMake | ||||
| cmake-build-*/ | ||||
| 
 | ||||
| # Mongo Explorer plugin | ||||
| .idea/**/mongoSettings.xml | ||||
| 
 | ||||
| # File-based project format | ||||
| *.iws | ||||
| 
 | ||||
| # IntelliJ | ||||
| out/ | ||||
| 
 | ||||
| # mpeltonen/sbt-idea plugin | ||||
| .idea_modules/ | ||||
| 
 | ||||
| # JIRA plugin | ||||
| atlassian-ide-plugin.xml | ||||
| 
 | ||||
| # Cursive Clojure plugin | ||||
| .idea/replstate.xml | ||||
| 
 | ||||
| # Crashlytics plugin (for Android Studio and IntelliJ) | ||||
| com_crashlytics_export_strings.xml | ||||
| crashlytics.properties | ||||
| crashlytics-build.properties | ||||
| fabric.properties | ||||
| 
 | ||||
| # Editor-based Rest Client | ||||
| .idea/httpRequests | ||||
| 
 | ||||
| # Android studio 3.1+ serialized cache file | ||||
| .idea/caches/build_file_checksums.ser | ||||
| 
 | ||||
| # JetBrains templates | ||||
| **___jb_tmp___ | ||||
| 
 | ||||
| ### Intellij Patch ### | ||||
| # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 | ||||
| 
 | ||||
| # *.iml | ||||
| # modules.xml | ||||
| # .idea/misc.xml | ||||
| # *.ipr | ||||
| 
 | ||||
| # Sonarlint plugin | ||||
| .idea/sonarlint | ||||
| 
 | ||||
| ### Java ### | ||||
| # Compiled class file | ||||
| *.class | ||||
| 
 | ||||
| # Log file | ||||
| *.log | ||||
| 
 | ||||
| # BlueJ files | ||||
| *.ctxt | ||||
| 
 | ||||
| # Mobile Tools for Java (J2ME) | ||||
| .mtj.tmp/ | ||||
| 
 | ||||
| # Package Files # | ||||
| *.jar | ||||
| *.war | ||||
| *.nar | ||||
| *.ear | ||||
| *.zip | ||||
| *.tar.gz | ||||
| *.rar | ||||
| 
 | ||||
| # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml | ||||
| hs_err_pid* | ||||
| 
 | ||||
| ### Gradle ### | ||||
| .gradle | ||||
| build/ | ||||
| 
 | ||||
| # Ignore Gradle GUI config | ||||
| gradle-app.setting | ||||
| 
 | ||||
| # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) | ||||
| !gradle-wrapper.jar | ||||
| 
 | ||||
| # Cache of project | ||||
| .gradletasknamecache | ||||
| 
 | ||||
| # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 | ||||
| # gradle/wrapper/gradle-wrapper.properties | ||||
| 
 | ||||
| ### Gradle Patch ### | ||||
| **/build/ | ||||
| 
 | ||||
| # End of https://www.gitignore.io/api/java,gradle,intellij | ||||
							
								
								
									
										77
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,77 @@ | |||
| # Rudefox Cold | ||||
| 
 | ||||
| Rudefox is a pair of command line tools for the creation and management of Bitcoin wallets. | ||||
| 
 | ||||
| + Use `rudefox-cold` on an air-gapped single-board computer, like a Raspberry Pi, to generate new wallets, obtain addresses and extended public keys from existing wallets and sign transactions offline. | ||||
| + Use `rudefox-hot` on a connected device to view your balance and utxo's and to create new, unsigned transactions. | ||||
| 
 | ||||
| ## Mnemonic Generation | ||||
| 
 | ||||
| ```bash | ||||
| alice@cold.machine:~$ rudefox-cold mnemonic --interactive --dice-entropy --bits 128 | ||||
| Input 50 dice rolls [1-6]: 234322343242422344161254151 | ||||
| Input 23 more dice rolls [1-6]: 33116265515343114314456 | ||||
| Mnemonic Sentence: mountain tilt wing silk rude fox almost volume wine media verify card | ||||
| ``` | ||||
| 
 | ||||
| ### Mnemonic Options | ||||
| 
 | ||||
| ```bash | ||||
| usage: rudefox-cold mnemonic | ||||
|  -b,--bits <128|160|192|224|256>   bits of entropy (default: 256) | ||||
|  -d,--dice-entropy                 use dice entropy source | ||||
|  -e,--events <EVENT_STRING>        string representing events from entropy source | ||||
|  -h,--help                         display help message | ||||
|  -i,--interactive                  use interactive command line mode | ||||
| ``` | ||||
| 
 | ||||
| ## Xpub Export | ||||
| 
 | ||||
| ```bash | ||||
| alice@cold.machine:~$ rudefox-cold wallet --sentence "stove prefer lunch collect small orphan wasp size beyond auction  | ||||
| guilt great" --passphrase apple | ||||
| xpub6CKy5SECeJipZid8dF3bopoMGdRzd7hMJuPzMGesZCobrMSssZyASexzXuzRTPVLcqqdyAEZJKPMGvDthgZW2Z3mPHLohxEAVbkvGKAXjqx | ||||
| ``` | ||||
| ### QR Code Generation | ||||
| 
 | ||||
| ```bash-qrcode | ||||
| alice@cold.machine:~$ rudefox-cold wallet --sentence "stove prefer lunch collect small orphan wasp size beyond auction  | ||||
| guilt great" --passphrase apple -q | ||||
| 
 | ||||
| █▀▀▀▀▀█ ▄█   █▄█ ▀▄▀ ▀▄▄▄██▄  █▀▄ █▀▀▀▀▀█ | ||||
| █ ███ █ ▀  ▀▄█▄▄▄▄██▄▄▄█  █  █▄   █ ███ █ | ||||
| █ ▀▀▀ █ █▄▀ █▄█▄▄▄▀█▀▄  ▄▀▀█▀ ▄▄▄ █ ▀▀▀ █ | ||||
| ▀▀▀▀▀▀▀ █▄▀▄█ ▀▄█▄▀ ▀▄█ █ █▄▀▄▀ ▀ ▀▀▀▀▀▀▀ | ||||
| ██ █  ▀█    ███ ██  ▄█▄▀▄▀▀██ █▀▄▄▀▀▀ ▀▀▄ | ||||
| ██ ▀ █▀█▄▀█▄██▀ ▀ █▀ ▄▄▄▀██ █▀▀▀█ █▀  █▀ | ||||
| █▀█▄  ▀█▀▀ ▄█▄█▄▀ █▀█  ██▀▄█▀ ▄██▀█ ▀   ▀ | ||||
| ▀██   ▀▄▄▀▄ ██ ▀██▄▀▄█▀▀█▄▀▄▀█▀▄▄▀▀█▄▀ █▀ | ||||
| ▀█▄▄ ▀▀▄ ▀▀▀▄▀ ██▄▄ ▀ ▀ █▀ ▀▄▀ █ █ █▀█▄ ▄ | ||||
| ███ █ ▀██ ▀▄▀█ ▄▄▀ ▄ ▄█▄▄ ▀▄██▄ ▀▄█▀▀  ▀▀ | ||||
| ▀█▄█▄ ▀ ▀▄▄█  ▀▄ █ ▄▀▀  ▄▄▀▄█▄█ ██▀▀▀▀▀ █ | ||||
| ▀ ▀▀▄ ▀▄█ ▄  ██▄▀▀██▄▄█▄ ▄█▄   █▀ ▄ ▄▀██ | ||||
| █ ▀▄▄▄▀█▀██▄▄▀▄▄▀█▄ █ ▄██▀▄  ▄ ▄▄█▀█ █ ▄ | ||||
| ▀ ██ ▀▀▀▄ ▀██▄▄▄ ██  █▀█▀▄█ ▄▄▀▄▀▀ ▄█ ▀█▀ | ||||
| ▀▄▄█ █▀ █▄▀ ██▀█▄ █▄▀████▀▄██ ▄▄▀█ █▀█▀▀█ | ||||
| ▀  ▄▄ ▀▄▄▄▄▀█ ▄▀▀█████  ▀▀▀▄ █ ▄█▀█▀▀█▀▀▀ | ||||
| ▀ ▀▀  ▀▀▄▀▀▄▄▀█▀▄ ▄███ ▀██▀▀█ █▄█▀▀▀█▄ █ | ||||
| █▀▀▀▀▀█ ▀▄█▄▀ ▄  █ ▀▄▄▄▄▄█▀  ▀▀ █ ▀ █ ▄█▄ | ||||
| █ ███ █ ▄ █▄ ▄ █▄▄  ▀█  █ ▄ ▀██ █▀█▀█ ▀█▀ | ||||
| █ ▀▀▀ █ ▄▄▀█ █ ▄ ▄█▄ ▄▄▀█▄▀  ▄█ █ ▀ █▀ █▀ | ||||
| ▀▀▀▀▀▀▀ ▀▀   ▀  ▀    ▀▀   ▀  ▀  ▀▀▀  ▀▀▀ | ||||
| alice@cold.machine:~$  | ||||
| ``` | ||||
| 
 | ||||
| ### Wallet Options | ||||
| 
 | ||||
| ```bash | ||||
| usage: rudefox-cold wallet | ||||
|  -h,--help                      display help message | ||||
|  -p,--passphrase <PASSPHRASE>   optional seed passphrase | ||||
|  -s,--sentence <SENTENCE>       mnemonic sentence | ||||
|  -q,--qrcode                    optional seed passphrase | ||||
| ``` | ||||
| 
 | ||||
| # Pending Use Cases | ||||
| 
 | ||||
| - verify seed in interactive mode | ||||
							
								
								
									
										88
									
								
								build.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								build.gradle
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,88 @@ | |||
| plugins { | ||||
|     id 'java' | ||||
|     id 'application' | ||||
|     id 'maven-publish' | ||||
|     id 'com.palantir.git-version' version '0.12.3' | ||||
| } | ||||
| 
 | ||||
| repositories { | ||||
| 
 | ||||
|     maven { | ||||
|         url "https://repo.rudefox.io/maven-public" | ||||
|     } | ||||
| 	 | ||||
|     mavenCentral() | ||||
| } | ||||
| 
 | ||||
| sourceCompatibility = 1.8 | ||||
| targetCompatibility = 1.8 | ||||
| 
 | ||||
| def isRelease = versionDetails().commitDistance == 0 | ||||
| 
 | ||||
| group 'io.rudefox' | ||||
| version isRelease ? gitVersion.call() : gitVersion.call() + "-SNAPSHOT" | ||||
| 
 | ||||
| application { | ||||
|     applicationName 'rudefox-cold' | ||||
|     mainClassName = 'io.rudefox.cold.RudefoxCold' | ||||
| } | ||||
| 
 | ||||
| run { | ||||
|     standardInput = System.in | ||||
| } | ||||
| 
 | ||||
| def junitVersion = "5.6.2" | ||||
| 
 | ||||
| tasks.withType(Test) { | ||||
|     useJUnitPlatform() | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
|     compile 'io.rudefox:vixen:0.0.2' | ||||
|     compile 'info.picocli:picocli:4.0.4' | ||||
|     compile 'com.google.zxing:core:3.4.0' | ||||
|     testCompile 'com.bjdweck.test:commons-test:0.0.1' | ||||
|     testCompile "org.junit.jupiter:junit-jupiter-params:$junitVersion" | ||||
|     testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion" | ||||
|     testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion" | ||||
| } | ||||
| 
 | ||||
| publishing { | ||||
| 
 | ||||
|     publications { | ||||
|         jar(MavenPublication) { | ||||
|             from components.java | ||||
|             pom { | ||||
|                 name = 'Rudefox Burrow' | ||||
|                 description = 'Bitcoin offline seed generation and tools' | ||||
|                 url = 'https://rudefox.io' | ||||
|                 developers { | ||||
|                     developer { | ||||
|                         name = 'B.J. Dweck' | ||||
|                     } | ||||
|                 } | ||||
|                 scm { | ||||
|                     developerConnection = 'scm:git:https://git.rudefox.io/rudefox/burrow.git' | ||||
|                     url = 'https://git.rudefox.io/rudefox/burrow/' | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     repositories { | ||||
|         maven { | ||||
|             name = "rudefox" | ||||
| 
 | ||||
|             def repoBaseUrl = 'https://repo.rudefox.io/repository' | ||||
|             url = isRelease ? "${repoBaseUrl}/maven-releases/" : "${repoBaseUrl}/maven-snapshots/" | ||||
| 
 | ||||
|             credentials { | ||||
|                 username System.getenv("RUDEFOX_MAVEN_USR") ?: | ||||
|                         (project.hasProperty('rudefox_maven_username') ? project.rudefox_maven_username : "defualt") | ||||
| 
 | ||||
|                 password System.getenv("RUDEFOX_MAVEN_PSW") ?: | ||||
|                         (project.hasProperty('rudefox_maven_password') ? project.rudefox_maven_password : "defualt") | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										15
									
								
								gradle.properties.sample
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								gradle.properties.sample
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,15 @@ | |||
| # bitcoind connection info for integration testing | ||||
| bitcoind_host=backend.sample.com | ||||
| bitcoind_port=8332 | ||||
| bitcoind_username=admin | ||||
| bitcoind_password=l6RYSFfGHosjvHDfK_f7GSBkdGHk26fjHchZrJ87dSH= | ||||
| 
 | ||||
| # public esplora URL for integration testing | ||||
| esplora_url=http://backend.sample.com:8080 | ||||
| esplora_testnet_url=http://backend.sample.com:8081/testnet/ | ||||
| 
 | ||||
| # web server credentials for website deployment (use in the ':deployWebsite' task) | ||||
| webserver_host=web.sample.com | ||||
| webserver_user=http_admin | ||||
| webserver_password=mycrazypassword | ||||
| webserver_target_dir=/path/to/webroot/without-ending-slash | ||||
							
								
								
									
										1
									
								
								settings.gradle
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								settings.gradle
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| rootProject.name = 'burrow' | ||||
							
								
								
									
										115
									
								
								src/main/java/io/rudefox/cold/Dice8EntropyGenerator.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								src/main/java/io/rudefox/cold/Dice8EntropyGenerator.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,115 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.bitcoin.mnemonic.Entropy; | ||||
| 
 | ||||
| import java.util.Scanner; | ||||
| import java.util.regex.Pattern; | ||||
| 
 | ||||
| public class Dice8EntropyGenerator { | ||||
| 
 | ||||
|     public static final int DICE_PER_ROLL = 11; | ||||
| 
 | ||||
|     private final int targetBitsOfEntropy; | ||||
|     private final Scanner inputScanner; | ||||
| 
 | ||||
|     private Entropy entropy = new Entropy(); | ||||
| 
 | ||||
|     public Dice8EntropyGenerator(int targetBitsOfEntropy, Scanner inputScanner) { | ||||
| 
 | ||||
|         this.targetBitsOfEntropy = targetBitsOfEntropy; | ||||
|         this.inputScanner = inputScanner; | ||||
|     } | ||||
| 
 | ||||
|     Entropy generate() { | ||||
| 
 | ||||
|         entropy = new Entropy(); | ||||
| 
 | ||||
|         for (int rollSetCount = 0; entropy.getBitLength() < targetBitsOfEntropy; rollSetCount++) | ||||
|             doDiceRoll(rollSetCount); | ||||
| 
 | ||||
|         return entropy.truncate(targetBitsOfEntropy).appendChecksum(); | ||||
|     } | ||||
| 
 | ||||
|     private void doDiceRoll(int currentRollSet) { | ||||
| 
 | ||||
|         String eventString = scanNextRollSetString(); | ||||
| 
 | ||||
|         StringBuilder rollValuesLine = new StringBuilder("|"); | ||||
| 
 | ||||
|         for (int rollNumber = 0; rollNumber < DICE_PER_ROLL; rollNumber++) { | ||||
| 
 | ||||
|             int rollValue = Integer.parseInt(eventString.charAt(rollNumber) + ""); | ||||
| 
 | ||||
|             String formatString = | ||||
|                     rollNumber == 3 || rollNumber == 7 ? "  %d   |" : " %d |"; | ||||
| 
 | ||||
|             rollValuesLine.append(String.format(formatString, rollValue)); | ||||
| 
 | ||||
|             entropy = entropy.appendBits(rollValue - 1, 3); | ||||
|         } | ||||
| 
 | ||||
|         System.out.printf("%n%s%n%s%n%s%n%n", | ||||
|                 rollValuesLine, | ||||
|                 getBitsLine(), | ||||
|                 getSeedWordsLine(currentRollSet) | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     private String scanNextRollSetString() { | ||||
| 
 | ||||
|         String rollSetString = ""; | ||||
|         Pattern rollSetPattern = Pattern.compile(String.format("[1-8]{%d}", DICE_PER_ROLL)); | ||||
| 
 | ||||
|         while (!rollSetPattern.matcher(rollSetString).matches()) { | ||||
|             System.out.print("Input 11 x 8-sided dice rolls [1-8]: "); | ||||
|             rollSetString = inputScanner.next(); | ||||
|         } | ||||
| 
 | ||||
|         return rollSetString; | ||||
|     } | ||||
| 
 | ||||
|     private StringBuilder getBitsLine() { | ||||
| 
 | ||||
|         String entropyBitString = entropy.toString(); | ||||
| 
 | ||||
|         String currentRollSetBitString = | ||||
|                 entropyBitString.substring(entropyBitString.length() - (DICE_PER_ROLL * 3)); | ||||
| 
 | ||||
|         StringBuilder rollSetBitsLine = new StringBuilder("|"); | ||||
|         for (int rollSetBitIndex = 0; rollSetBitIndex < currentRollSetBitString.length(); rollSetBitIndex++) { | ||||
| 
 | ||||
|             int entropyBitIndex = entropyBitString.length() - currentRollSetBitString.length() + rollSetBitIndex; | ||||
| 
 | ||||
|             if (entropyBitIndex < targetBitsOfEntropy) | ||||
|                 rollSetBitsLine.append(currentRollSetBitString.charAt(rollSetBitIndex)); | ||||
|             else | ||||
|                 rollSetBitsLine.append("-"); | ||||
| 
 | ||||
|             if (rollSetBitIndex == 32) rollSetBitsLine.append("|"); | ||||
|             else if (rollSetBitIndex % 3 == 2) rollSetBitsLine.append(" "); | ||||
|             else if (rollSetBitIndex % 11 == 10) rollSetBitsLine.append(" | "); | ||||
|         } | ||||
| 
 | ||||
|         return rollSetBitsLine; | ||||
|     } | ||||
| 
 | ||||
|     private String getSeedWordsLine(int rollSet) { | ||||
| 
 | ||||
|         int baseIndex = rollSet * 3; | ||||
| 
 | ||||
|         return String.format( | ||||
|                 "|%-15s|%-17s|%-15s|", | ||||
|                 getFormattedSeedWord(baseIndex), | ||||
|                 getFormattedSeedWord(baseIndex + 1), | ||||
|                 getFormattedSeedWord(baseIndex + 2)); | ||||
|     } | ||||
| 
 | ||||
|     private String getFormattedSeedWord(int index) { | ||||
| 
 | ||||
|         int lastWordIndex = (3 * targetBitsOfEntropy / 32) - 1; | ||||
| 
 | ||||
|         String seedWord = index != lastWordIndex ? entropy.getWord(index) : "CHECKWORD"; | ||||
| 
 | ||||
|         return String.format(" %2d. %s", index + 1, seedWord); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										47
									
								
								src/main/java/io/rudefox/cold/DiceEventBuffer.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/main/java/io/rudefox/cold/DiceEventBuffer.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,47 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.bitcoin.mnemonic.Entropy; | ||||
| import com.bjdweck.math.UnsignedInt; | ||||
| 
 | ||||
| class DiceEventBuffer { | ||||
| 
 | ||||
|     private final int diceBase; | ||||
|     private final StringBuilder buffer; | ||||
|     private final int targetBitsOfEntropy; | ||||
| 
 | ||||
|     DiceEventBuffer(int targetBitsOfEntropy, int diceBase) { | ||||
| 
 | ||||
|         this.diceBase = diceBase; | ||||
|         this.targetBitsOfEntropy = targetBitsOfEntropy; | ||||
|         this.buffer = new StringBuilder(getRequiredEvents()); | ||||
|     } | ||||
| 
 | ||||
|     int getRequiredEvents() { | ||||
|         return (int) Math.ceil(this.targetBitsOfEntropy * Math.log(2) / Math.log(diceBase)); | ||||
|     } | ||||
| 
 | ||||
|     void appendEvents(String eventString) { | ||||
|         for (char inChar : eventString.toCharArray()) | ||||
|             if (inChar >= '1' && inChar <= '6' && events() < getRequiredEvents()) | ||||
|                 append((char) (inChar - 1)); | ||||
|     } | ||||
| 
 | ||||
|     Entropy toEntropy() { | ||||
|         UnsignedInt entropy = new UnsignedInt(toString(), diceBase); | ||||
|         byte[] entropyBytes = entropy.getLowestOrderBits(this.targetBitsOfEntropy).toBigEndianByteArray(); | ||||
|         return Entropy.fromRawEntropy(entropyBytes); | ||||
|     } | ||||
| 
 | ||||
|     int events() { | ||||
|         return buffer.length(); | ||||
|     } | ||||
| 
 | ||||
|     private void append(char c) { | ||||
|         buffer.append(c); | ||||
|     } | ||||
| 
 | ||||
|     @Override | ||||
|     public String toString() { | ||||
|         return buffer.toString(); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										125
									
								
								src/main/java/io/rudefox/cold/MnemonicCommand.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/main/java/io/rudefox/cold/MnemonicCommand.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,125 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.bitcoin.mnemonic.Entropy; | ||||
| import picocli.CommandLine; | ||||
| 
 | ||||
| import java.security.SecureRandom; | ||||
| import java.util.Scanner; | ||||
| 
 | ||||
| @CommandLine.Command(name = "mnemonic", description = "generate mnemonic sentence") | ||||
| public class MnemonicCommand implements Runnable { | ||||
| 
 | ||||
|     private static final int DEFAULT_BITS_OF_ENTROPY = 256; | ||||
| 
 | ||||
|     private boolean isDiceEntropy() { | ||||
|         return entropyOptions != null && (entropyOptions.isDice6Entropy || entropyOptions.isDice8Entropy); | ||||
|     } | ||||
| 
 | ||||
|     private boolean isInteractiveMode() { | ||||
|         return entropyOptions != null && entropyOptions.eventMethod.isInteractiveMode; | ||||
|     } | ||||
| 
 | ||||
|     private boolean isDice6InteractiveMode() { | ||||
|         return isInteractiveMode() && entropyOptions.isDice6Entropy; | ||||
|     } | ||||
| 
 | ||||
|     private boolean isDice8InteractiveMode() { | ||||
|         return isInteractiveMode() && entropyOptions.isDice8Entropy; | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.ArgGroup(exclusive = false) | ||||
|     EntropyOptions entropyOptions; | ||||
| 
 | ||||
|     static class EntropyOptions { | ||||
| 
 | ||||
|         @CommandLine.Option(names = {"-6", "--dice6-entropy"}, | ||||
|                 description = "use 6-sided dice entropy source") | ||||
|         boolean isDice6Entropy; | ||||
| 
 | ||||
|         @CommandLine.Option(names = {"-8", "--dice8-entropy"}, | ||||
|                 description = "use 8-sided dice entropy source") | ||||
|         boolean isDice8Entropy; | ||||
| 
 | ||||
|         @CommandLine.ArgGroup(multiplicity = "1") | ||||
|         EventMethod eventMethod; | ||||
| 
 | ||||
|         static class EventMethod { | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-e", "--events"}, paramLabel = "[1-6]{100}|[1-8]{86}", | ||||
|                     description = "string representing events from entropy source", | ||||
|                     required = true) | ||||
|             String getEventString; | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-i", "--interactive"}, | ||||
|                     description = "use interactive command line mode", | ||||
|                     required = true) | ||||
|             boolean isInteractiveMode; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.Option(names = {"-b", "--bits"}, defaultValue = DEFAULT_BITS_OF_ENTROPY+"", | ||||
|             description = "bits of entropy (default: 256)", | ||||
|             paramLabel = "128|160|192|224|256") | ||||
|     int targetBitsOfEntropy; | ||||
| 
 | ||||
|     public void run() { | ||||
| 
 | ||||
|         Entropy entropyBytes = getEntropy(); | ||||
| 
 | ||||
|         System.out.println(entropyBytes.toMnemonic().getSentence()); | ||||
|     } | ||||
| 
 | ||||
|     Entropy getEntropy() { | ||||
| 
 | ||||
|         if (!isDiceEntropy()) | ||||
|             return getGeneratedEntropy(); | ||||
| 
 | ||||
|         DiceEventBuffer diceEventBuffer; | ||||
| 
 | ||||
|         if (entropyOptions.isDice6Entropy) | ||||
|             diceEventBuffer = new DiceEventBuffer(this.targetBitsOfEntropy, 6); | ||||
|         else | ||||
|             diceEventBuffer = new DiceEventBuffer(this.targetBitsOfEntropy, 8); | ||||
| 
 | ||||
|         if (isDice6InteractiveMode()) | ||||
|             return getDice6EntropyInteractive(diceEventBuffer); | ||||
| 
 | ||||
|         if (isDice8InteractiveMode()) | ||||
|             return new Dice8EntropyGenerator(targetBitsOfEntropy, new Scanner(System.in)).generate(); | ||||
| 
 | ||||
|         diceEventBuffer.appendEvents(entropyOptions.eventMethod.getEventString); | ||||
|         return diceEventBuffer.toEntropy(); | ||||
|     } | ||||
| 
 | ||||
|     private Entropy getDice6EntropyInteractive(DiceEventBuffer diceEventBuffer) { | ||||
| 
 | ||||
|         int requiredEvents = diceEventBuffer.getRequiredEvents(); | ||||
| 
 | ||||
|         Scanner scanner = new Scanner(System.in); | ||||
| 
 | ||||
|         boolean firstIteration = true; | ||||
| 
 | ||||
|         while (diceEventBuffer.events() < requiredEvents) { | ||||
| 
 | ||||
|             int remainingEvents = requiredEvents - diceEventBuffer.events(); | ||||
|             System.out.print(String.format("Input %d %sdice rolls [1-6]: ", remainingEvents, (firstIteration ? "" : "more "))); | ||||
| 
 | ||||
|             String inputString = scanner.next(); | ||||
| 
 | ||||
|             diceEventBuffer.appendEvents(inputString); | ||||
| 
 | ||||
|             firstIteration = false; | ||||
|         } | ||||
| 
 | ||||
|         return diceEventBuffer.toEntropy(); | ||||
|     } | ||||
| 
 | ||||
|     private Entropy getGeneratedEntropy() { | ||||
| 
 | ||||
|         SecureRandom random = new SecureRandom(); | ||||
|         int byteLength = targetBitsOfEntropy / 8; | ||||
|         byte[] randomBytes = new byte[byteLength]; | ||||
|         random.nextBytes(randomBytes); | ||||
|         return Entropy.fromRawEntropy(randomBytes); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										111
									
								
								src/main/java/io/rudefox/cold/QRCode.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								src/main/java/io/rudefox/cold/QRCode.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,111 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.google.zxing.BarcodeFormat; | ||||
| import com.google.zxing.EncodeHintType; | ||||
| import com.google.zxing.WriterException; | ||||
| import com.google.zxing.common.BitMatrix; | ||||
| import com.google.zxing.qrcode.QRCodeWriter; | ||||
| import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel; | ||||
| 
 | ||||
| import java.nio.charset.Charset; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| import java.util.HashMap; | ||||
| import java.util.Map; | ||||
| 
 | ||||
| class QRCode { | ||||
| 
 | ||||
|     private static final String FALLBACK_SET_STRING = "##"; | ||||
| 
 | ||||
|     static String toQRCode(String data) { | ||||
| 
 | ||||
|         QRCodeWriter qrCodeWriter = new QRCodeWriter(); | ||||
|         Map<EncodeHintType, Object> hints = new HashMap<>(); | ||||
|         hints.put(EncodeHintType.MARGIN, 0); | ||||
|         hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L); | ||||
| 
 | ||||
|         BitMatrix bitMatrix = null; | ||||
|         try { | ||||
|             bitMatrix = qrCodeWriter.encode(data, BarcodeFormat.QR_CODE, 0, 0, hints); | ||||
|         } catch (WriterException e) { | ||||
|             e.printStackTrace(); | ||||
|         } | ||||
| 
 | ||||
|         if (bitMatrix == null) | ||||
|             return "[Error Generating QR Code]"; | ||||
| 
 | ||||
|         if (charsetDoesNotSupportFullBlockChar()) | ||||
|             return bitMatrix.toString(FALLBACK_SET_STRING, "  "); | ||||
| 
 | ||||
|         return toUTF8(bitMatrix); | ||||
|     } | ||||
| 
 | ||||
|     private static String toUTF8(BitMatrix bitMatrix) { | ||||
| 
 | ||||
|         char SPACE = '\u0020'; | ||||
|         char LOWER_BLOCK = '\u2584'; | ||||
|         char UPPER_BLOCK = '\u2580'; | ||||
|         char FULL_BLOCK = '\u2588'; | ||||
| 
 | ||||
|         int[] blockCodepoints = new int[] {SPACE, LOWER_BLOCK, UPPER_BLOCK, FULL_BLOCK}; | ||||
| 
 | ||||
|         StringBuilder qrCode = new StringBuilder(); | ||||
| 
 | ||||
|         for (int y = 0; y < bitMatrix.getHeight(); y += 2) { | ||||
| 
 | ||||
|             for (int x = 0; x < bitMatrix.getWidth(); x++) { | ||||
| 
 | ||||
|                 int upper_bit = bitMatrix.get(x, y) ? 1 : 0; | ||||
|                 int lower_bit = y+1 < bitMatrix.getHeight() && bitMatrix.get(x, y+1) ? 1 : 0; | ||||
| 
 | ||||
|                 int index = upper_bit << 1 | lower_bit; | ||||
|                 int blockCodepoint = blockCodepoints[index]; | ||||
| 
 | ||||
|                 qrCode.appendCodePoint(blockCodepoint); | ||||
|             } | ||||
| 
 | ||||
|             if (y < bitMatrix.getHeight() - 1) | ||||
|                 qrCode.append("\n"); | ||||
|         } | ||||
| 
 | ||||
|         return qrCode.toString(); | ||||
|     } | ||||
| 
 | ||||
|     private static boolean charsetDoesNotSupportFullBlockChar() { | ||||
| 
 | ||||
|         Charset charset = ServiceLocator.DEFAULT_CHARSET; | ||||
|         String charsetString = charset.toString(); | ||||
| 
 | ||||
|         return charset.equals(StandardCharsets.ISO_8859_1) || | ||||
|                 charset.equals(StandardCharsets.US_ASCII) || | ||||
|                 charsetString.equalsIgnoreCase("euc-jp") || | ||||
|                 charsetString.equalsIgnoreCase("euc-kr") || | ||||
|                 charsetString.equalsIgnoreCase("iso-2022-jp") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-10") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-13") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-14") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-15") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-16") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-2") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-3") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-4") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-5") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-6") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-7") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-8") || | ||||
|                 charsetString.equalsIgnoreCase("iso-8859-8-i") || | ||||
|                 charsetString.equalsIgnoreCase("macintosh") || | ||||
|                 charsetString.equalsIgnoreCase("shift_jis") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1250") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1251") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1252") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1253") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1254") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1255") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1256") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1257") || | ||||
|                 charsetString.equalsIgnoreCase("windows-1258") || | ||||
|                 charsetString.equalsIgnoreCase("windows-874") || | ||||
|                 charsetString.equalsIgnoreCase("x-mac-cyrillic") || | ||||
|                 charsetString.equalsIgnoreCase("x-user-defined"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										32
									
								
								src/main/java/io/rudefox/cold/RudefoxCold.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/main/java/io/rudefox/cold/RudefoxCold.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,32 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.bitcoin.params.INetworkParameters; | ||||
| import com.bjdweck.bitcoin.params.NetworkParameters; | ||||
| import picocli.CommandLine; | ||||
| 
 | ||||
| @CommandLine.Command( | ||||
|         name = "rudefox-cold", | ||||
|         synopsisSubcommandLabel = "COMMAND", | ||||
|         description = "Offline wallet tool", | ||||
|         subcommands = {MnemonicCommand.class, WalletCommand.class} | ||||
| ) | ||||
| public class RudefoxCold implements Runnable { | ||||
| 
 | ||||
|     @CommandLine.Option(names = "--testnet", description = "run on Bitcoin Testnet (default: Mainnet)") | ||||
|     boolean testnet = false; | ||||
| 
 | ||||
|     public INetworkParameters getNetworkParameters() { | ||||
|         return testnet ? NetworkParameters.Testnet() : NetworkParameters.Mainnet(); | ||||
|     } | ||||
| 
 | ||||
|     public static void main(String[] args) { | ||||
|         new CommandLine(new RudefoxCold()).execute(args); | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.Spec | ||||
|     CommandLine.Model.CommandSpec commandSpec; | ||||
| 
 | ||||
|     public void run() { | ||||
|         throw new CommandLine.ParameterException(commandSpec.commandLine(), "Missing required subcommand"); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										8
									
								
								src/main/java/io/rudefox/cold/ServiceLocator.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/main/java/io/rudefox/cold/ServiceLocator.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,8 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import java.nio.charset.Charset; | ||||
| 
 | ||||
| public class ServiceLocator { | ||||
| 
 | ||||
|     public static Charset DEFAULT_CHARSET = Charset.defaultCharset(); | ||||
| } | ||||
							
								
								
									
										137
									
								
								src/main/java/io/rudefox/cold/WalletCommand.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										137
									
								
								src/main/java/io/rudefox/cold/WalletCommand.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,137 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.bitcoin.address.BitcoinAddress; | ||||
| import com.bjdweck.bitcoin.extendedkey.AddressChain; | ||||
| import com.bjdweck.bitcoin.extendedkey.DerivationPath; | ||||
| import com.bjdweck.bitcoin.extendedkey.KeyOrigin; | ||||
| import com.bjdweck.bitcoin.extendedkey.PrivateExtendedKey; | ||||
| import com.bjdweck.bitcoin.hdwallet.Bip44Purpose; | ||||
| import com.bjdweck.bitcoin.hdwallet.Bip44SigningAccount; | ||||
| import com.bjdweck.bitcoin.hdwallet.Bip44Wallet; | ||||
| import com.bjdweck.bitcoin.mnemonic.Mnemonic; | ||||
| import com.bjdweck.bitcoin.mnemonic.Seed; | ||||
| import com.bjdweck.bitcoin.psbt.Psbt; | ||||
| import com.bjdweck.bitcoin.transaction.scriptpubkey.ScriptPubKey; | ||||
| import picocli.CommandLine; | ||||
| 
 | ||||
| @SuppressWarnings("ALL") | ||||
| @CommandLine.Command(name = "wallet", description = "BIP44 wallet operations") | ||||
| public class WalletCommand implements Runnable { | ||||
| 
 | ||||
|     @CommandLine.ParentCommand | ||||
|     private RudefoxCold globalOptions; | ||||
| 
 | ||||
|     @CommandLine.Option(names = {"-s", "--sentence"}, description = "mnemonic sentence", required = true) | ||||
|     String sentence; | ||||
| 
 | ||||
|     @CommandLine.Option(names = {"-p", "--passphrase"}, description = "optional mnemonic passphrase") | ||||
|     String passphrase; | ||||
| 
 | ||||
|     Bip44Wallet getWallet() { | ||||
|         Seed seed = Mnemonic.fromSentence(sentence).generateSeed(passphrase); | ||||
|         return new Bip44Wallet(PrivateExtendedKey.fromSeed(seed), globalOptions.getNetworkParameters()); | ||||
|     } | ||||
| 
 | ||||
|     Bip44SigningAccount getAccount(Bip44Purpose purpose, int accountSeq) { | ||||
|         return getWallet().deriveAccount(purpose, accountSeq); | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.Command(name = "listxpub", description = "list xpub (default: SLIP-0132)") | ||||
|     public int listXpub( | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-q", "--qrcode"}, description = "output QR code") | ||||
|             boolean isQRCode, | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-p", "--purpose"}, description = "purpose", | ||||
|                     paramLabel = "[P2PKH, P2SH_P2WPKH, P2WPKH, P2SH_MultiSigP2WSH, MultiSigP2WSH]", | ||||
|                     defaultValue = "P2PKH") | ||||
|             Bip44Purpose purpose, | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-a", "--account"}, description = "account sequence number", | ||||
|                     paramLabel = "n", defaultValue = "0") | ||||
|             int accountSeq) { | ||||
| 
 | ||||
|         Bip44SigningAccount bip44SigningAccount = getAccount(purpose, accountSeq); | ||||
|         String slip0132 = bip44SigningAccount.toXpubSlip0132Base58(); | ||||
|         String keyOrigin = bip44SigningAccount.getKeyOrigin().toString(); | ||||
| 
 | ||||
|         if (isQRCode) { | ||||
|             System.out.println(QRCode.toQRCode(slip0132)); | ||||
|             return 0; | ||||
|         } | ||||
| 
 | ||||
|         System.out.printf("[%s]%s%n", keyOrigin, slip0132); | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.Command(name = "listaddress", description = "list account addresses") | ||||
|     public int listAddress( | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-p", "--purpose"}, description = "purpose", | ||||
|                     paramLabel = "[P2PKH, P2SH_P2WPKH, P2WPKH, P2SH_MultiSigP2WSH, MultiSigP2WSH]", | ||||
|                     defaultValue = "P2PKH") | ||||
|             Bip44Purpose purpose, | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-a", "--account"}, description = "account sequence number", | ||||
|                     paramLabel = "<n>", defaultValue = "0") | ||||
|             int accountSeq, | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-c", "--address-chain"}, description = "address chain", | ||||
|                     paramLabel = "[INTERNAL|EXTERNAL]", defaultValue = "EXTERNAL") | ||||
|             AddressChain addressChain, | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-s", "--address-seq"}, description = "starting address sequence number", | ||||
|                     paramLabel = "<n>", defaultValue = "0") | ||||
|             int addressSeq, | ||||
| 
 | ||||
|             @CommandLine.Option(names = {"-n", "--count"}, description = "number of addresses to list", | ||||
|                     paramLabel = "<n>", defaultValue = "10") | ||||
|             int count) { | ||||
| 
 | ||||
|         Bip44SigningAccount account = getAccount(purpose, accountSeq); | ||||
| 
 | ||||
|         for (int i = 0; i < count; i++) { | ||||
| 
 | ||||
|             int seq = addressSeq + i; | ||||
| 
 | ||||
|             DerivationPath addressPath = DerivationPath.m().slash(addressChain.getSequence()).slash(seq); | ||||
|             KeyOrigin keyOrigin = account.getKeyOrigin().combineRelativePath(addressPath); | ||||
| 
 | ||||
|             ScriptPubKey scriptPubKey = account.deriveScriptPubKey(addressChain, seq); | ||||
|             BitcoinAddress address = scriptPubKey.getAddress(globalOptions.getNetworkParameters()); | ||||
| 
 | ||||
|             System.out.printf("%s: %s%n", keyOrigin, address); | ||||
|         } | ||||
| 
 | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.Command(name = "signpsbt", description = "sign a PSBT") | ||||
|     public int signPsbt( | ||||
|             @CommandLine.Parameters() | ||||
|             String psbtBase64) { | ||||
| 
 | ||||
|         Psbt psbt = Psbt.fromBase64(psbtBase64); | ||||
| 
 | ||||
|         psbt.getValidationViolationMap(); | ||||
|     /* | ||||
|         Satoshi fee = psbt.getFee(); | ||||
|         Satoshi spendAmount = psbt.getSpendAmount(getWallet()); | ||||
|         BitcoinAddress spendAddress = psbt.getSpendAddress(getWallet()); | ||||
| 
 | ||||
|         System.out.printf("Confirm spend of %s to %s with a fee of %s", spendAmount.toBitcoinString(), spendAddress.getAddressString(), fee.toBitcoinString()); | ||||
| */ | ||||
|         psbt.signPsbt(getWallet()); | ||||
| 
 | ||||
|         System.out.println(psbt.toBase64()); | ||||
| 
 | ||||
|         return 0; | ||||
|     } | ||||
| 
 | ||||
|     @CommandLine.Spec | ||||
|     CommandLine.Model.CommandSpec commandSpec; | ||||
| 
 | ||||
|     public void run() { | ||||
|         throw new CommandLine.ParameterException(commandSpec.commandLine(), "Missing required wallet subcommand"); | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,64 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.test.CliTestFixture; | ||||
| import org.junit.jupiter.api.Test; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| 
 | ||||
| class mnemonic_8_sided_dice_tests extends CliTestFixture { | ||||
| 
 | ||||
|     @Test | ||||
|     void with_arguments_interactive_8_sided_dice_should_generate_mnemonic_sentence() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs("mnemonic -i8 --bits 128"); | ||||
| 
 | ||||
|         expectedOutput("Input 11 x 8-sided dice rolls [1-8]: "); | ||||
|         provideInput("12345678123" + EOL); | ||||
| 
 | ||||
|         expectedOutput(EOL); | ||||
|         expectedOutput("| 1 | 2 | 3 |  4   | 5 | 6 | 7 |  8   | 1 | 2 | 3 |" + EOL); | ||||
|         expectedOutput("|000 001 010 01 | 1 100 101 110 1 | 11 000 001 010|" + EOL); | ||||
|         expectedOutput("|  1. ahead     |  2. slight      |  3. scout     |" + EOL); | ||||
|         expectedOutput(EOL); | ||||
| 
 | ||||
|         expectedOutput("Input 11 x 8-sided dice rolls [1-8]: "); | ||||
|         provideInput("12345678123" + EOL); | ||||
| 
 | ||||
|         expectedOutput(EOL); | ||||
|         expectedOutput("| 1 | 2 | 3 |  4   | 5 | 6 | 7 |  8   | 1 | 2 | 3 |" + EOL); | ||||
|         expectedOutput("|000 001 010 01 | 1 100 101 110 1 | 11 000 001 010|" + EOL); | ||||
|         expectedOutput("|  4. ahead     |  5. slight      |  6. scout     |" + EOL); | ||||
|         expectedOutput(EOL); | ||||
| 
 | ||||
|         expectedOutput("Input 11 x 8-sided dice rolls [1-8]: "); | ||||
|         provideInput("12345678123" + EOL); | ||||
| 
 | ||||
|         expectedOutput(EOL); | ||||
|         expectedOutput("| 1 | 2 | 3 |  4   | 5 | 6 | 7 |  8   | 1 | 2 | 3 |" + EOL); | ||||
|         expectedOutput("|000 001 010 01 | 1 100 101 110 1 | 11 000 001 010|" + EOL); | ||||
|         expectedOutput("|  7. ahead     |  8. slight      |  9. scout     |" + EOL); | ||||
|         expectedOutput(EOL); | ||||
| 
 | ||||
|         expectedOutput("Input 11 x 8-sided dice rolls [1-8]: "); | ||||
|         provideInput("12345678123" + EOL); | ||||
| 
 | ||||
|         expectedOutput(EOL); | ||||
|         expectedOutput("| 1 | 2 | 3 |  4   | 5 | 6 | 7 |  8   | 1 | 2 | 3 |" + EOL); | ||||
|         expectedOutput("|000 001 010 01 | 1 100 101 110 1 | 11 000 00- ---|" + EOL); | ||||
|         expectedOutput("| 10. ahead     | 11. slight      | 12. CHECKWORD |" + EOL); | ||||
|         expectedOutput(EOL); | ||||
| 
 | ||||
|         expectedOutput("ahead slight scout ahead slight scout ahead slight scout ahead slight scan" + EOL); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         verifyOutput(); | ||||
|     } | ||||
| 
 | ||||
|     private void doMain() { | ||||
| 
 | ||||
|         setInput(); | ||||
| 
 | ||||
|         RudefoxCold.main(getArgs()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										71
									
								
								src/test/java/io/rudefox/cold/mnemonic_command_tests.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/test/java/io/rudefox/cold/mnemonic_command_tests.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,71 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.test.CliTestFixture; | ||||
| import org.junit.jupiter.api.Test; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| 
 | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| 
 | ||||
| class mnemonic_command_tests extends CliTestFixture { | ||||
| 
 | ||||
|     @Test | ||||
|     void with_arguments_interactive_dice_should_generate_mnemonic_sentence() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs("mnemonic --interactive --dice6-entropy --bits 128"); | ||||
| 
 | ||||
|         expectedOutput("Input 50 dice rolls [1-6]: "); | ||||
|         provideInput("234322343242422344161254151\r\n"); | ||||
| 
 | ||||
|         expectedOutput("Input 23 more dice rolls [1-6]: "); | ||||
|         provideInput("33116265515343114314456\r\n"); | ||||
| 
 | ||||
|         expectedOutput("mountain tilt wing silk rude fox almost volume wine media verify card" + EOL); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         verifyOutput(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_no_arguments_should_output_mnemonic() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs("mnemonic"); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals(24, getOutput().split(" ").length); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_arguments_non_interactive_dice_bits_should_generate_mnemonic_sentence() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs("mnemonic --dice6-entropy --events 23432234324242234416125415133116265515343114314456 --bits 128"); | ||||
| 
 | ||||
|         expectedOutput("mountain tilt wing silk rude fox almost volume wine media verify card" + EOL); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         verifyOutput(); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_arguments_without_interactive_dice_should_generate_mnemonic_sentence() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs("mnemonic --dice6-entropy --events 2343223432424223441612541513311626551534311431445623432234324242234416125415133116265515343114314456"); | ||||
| 
 | ||||
|         expectedOutput("first welcome social broccoli nasty rather weird uncle spirit horn update pencil help rescue " + | ||||
|                 "grape enough fork wave eight fuel ribbon pony clean couple" + EOL); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         verifyOutput(); | ||||
|     } | ||||
| 
 | ||||
|     private void doMain() { | ||||
| 
 | ||||
|         setInput(); | ||||
| 
 | ||||
|         RudefoxCold.main(getArgs()); | ||||
|     } | ||||
| } | ||||
							
								
								
									
										145
									
								
								src/test/java/io/rudefox/cold/wallet_command_tests.java
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/test/java/io/rudefox/cold/wallet_command_tests.java
									
									
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,145 @@ | |||
| package io.rudefox.cold; | ||||
| 
 | ||||
| import com.bjdweck.test.CliTestFixture; | ||||
| import org.junit.jupiter.api.Test; | ||||
| 
 | ||||
| import java.io.UnsupportedEncodingException; | ||||
| import java.nio.charset.StandardCharsets; | ||||
| 
 | ||||
| import static org.junit.jupiter.api.Assertions.assertEquals; | ||||
| 
 | ||||
| class wallet_command_tests extends CliTestFixture { | ||||
| 
 | ||||
|     static { | ||||
|         OUTPUT_CHARSET = "UTF8"; | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_sentence_and_passphrase_should_output_xpub() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs(new String[] {"wallet", | ||||
|                                "--sentence","stove prefer lunch collect small orphan wasp size beyond auction guilt great", | ||||
|                                "--passphrase","apple", | ||||
|                                "listxpub"}); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals("[39F8B071:m/44'/0'/0']xpub6CKy5SECeJipZid8dF3bopoMGdRzd7hMJuPzMGesZCobrMSssZyASexzXuzRTPVLcqqdyAEZJKPMGvDthgZW2Z3mPHLohxEAVbkvGKAXjqx" + EOL, getOutput()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_testnet_sentence_and_passphrase_and_witness_should_output_vpub() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs(new String[] {"--testnet", | ||||
|                                "wallet", | ||||
|                                "--sentence","icon issue absorb apology price atom bread toward worry final dune dial swing armor donkey", | ||||
|                                "--passphrase","apple", | ||||
|                                "listxpub", | ||||
|                                "--purpose","P2WPKH", | ||||
|                                "--account","6"}); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals("[CBACA81C:m/84'/1'/6']vpub5Z1KP7PJ8f85AqbvPMKeFHxmDfrgimiAar3KqwCzfsw65tmFLevm4MJX9cRcoiERYkjeG5Z3VwhSf1E3fVBFF86J33CHSED45Hk6kLZumqV" + EOL, getOutput()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_testnet_sentence_and_passphrase_and_witness_should_output_addresses() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs(new String[] {"--testnet", | ||||
|                                "wallet", | ||||
|                                "--sentence","icon issue absorb apology price atom bread toward worry final dune dial swing armor donkey", | ||||
|                                "--passphrase","apple", | ||||
|                                "listaddress", | ||||
|                                "--purpose","P2WPKH", | ||||
|                                "--account","6", | ||||
|                                "--address-chain","INTERNAL", | ||||
|                                "--address-seq","3", | ||||
|                                "--count","5"}); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals("CBACA81C:m/84'/1'/6'/1/3: tb1qs7cnr9sql9nef5e95jk5t7pr2qwpjsqtffnuxj" + EOL + | ||||
|                                "CBACA81C:m/84'/1'/6'/1/4: tb1qgl947tylg3tazgdy2rm3rs4gxheue707dkg3q2" + EOL + | ||||
|                                "CBACA81C:m/84'/1'/6'/1/5: tb1q03n6q2jjeks9zaneglqz5g37xkcrzy8nh8hn6s" + EOL + | ||||
|                                "CBACA81C:m/84'/1'/6'/1/6: tb1q50gylhzz2mrf76ffn7dc5zvymsd9lghj24t7ns" + EOL + | ||||
|                                "CBACA81C:m/84'/1'/6'/1/7: tb1qvzkez0zg6xm9z780n0358xp7nx96fh508awktw" + EOL, getOutput()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_testnet_sentence_and_passphrase_and_witness_can_sign_psbt() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs(new String[] {"--testnet", | ||||
|                                "wallet", | ||||
|                                "--sentence","vibrant delay limit tenant prepare reflect lonely pepper dragon calm dolphin prize slide inch purse term raw eternal twin kidney scan power magnet humble", | ||||
|                                "signpsbt", | ||||
|                                "cHNidP8BAHcCAAAAAS/zE49gxbTp0n4mTv0wPV7KuLtQlbzTL/E9s4b4JKl/AQAAAAD/////AkAfAAAAAAAAGXapFE9U1GY" + | ||||
|                                        "KflieNl1PUIpdTTzcXnUkiKxiyR4AAAAAABl2qRR6uJziLUo4i7ooJyzxIctMZfxb5IisAAAAAE8BBDWHzwOP9tWogAAAAN/zgjOY" + | ||||
|                                        "H39q085n7GG+5+/Jtovkvc8WQftwMSsBV7svAhPpi88y7pGhXzOdH0iJbXJsMq3F9GNRnRyqkQ7hVEtjEI2C0R8sAACAAQAAgAAAA" + | ||||
|                                        "IAAAQDeAQAAAAGVcHJZzeDV1JsQv869yoS/f8eSufNKON0o56ChVEnxCwEAAABqRzBEAiAD95mxgC+CHJNt1Ui+Y/raIymIiK1fyV" + | ||||
|                                        "c9wx0EALk/uQIgdQS27tjyujeItDg96m5GR7UD5tIz+95A4RQX4YOXXZoBIQNsvOVlbTqbZoKJbx8z94F6C/xS4faQ2XsHINcd50q" + | ||||
|                                        "T5P////8CQB8AAAAAAAAWABRzMqPZdHy0JMwPKY/YRhrODmyijpzpHgAAAAAAGXapFKmSAlpRxRJXr8ywpN500rXVsxIHiKwAAAAA" + | ||||
|                                        "IgYCUEaaspCm8AVu5WJ9Z7RG53iW66Btq6QODz7xfMlRogoYjYLRHywAAIABAACAAAAAgAEAAAADAAAAAAAA"}); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals("cHNidP8BAHcCAAAAAS/zE49gxbTp0n4mTv0wPV7KuLtQlbzTL/E9s4b4JKl/AQAAAAD/////AkAfAAAAAAAAGXapFE9U1GYKflieNl1PUIpdTTzcXnUkiKxiyR4AAAAAABl2qRR6uJziLUo4i7ooJyzxIctMZfxb5IisAAAAAE8BBDWHzwOP9tWogAAAAN/zgjOYH39q085n7GG+5+/Jtovkvc8WQftwMSsBV7svAhPpi88y7pGhXzOdH0iJbXJsMq3F9GNRnRyqkQ7hVEtjEI2C0R8sAACAAQAAgAAAAIAAAQDeAQAAAAGVcHJZzeDV1JsQv869yoS/f8eSufNKON0o56ChVEnxCwEAAABqRzBEAiAD95mxgC+CHJNt1Ui+Y/raIymIiK1fyVc9wx0EALk/uQIgdQS27tjyujeItDg96m5GR7UD5tIz+95A4RQX4YOXXZoBIQNsvOVlbTqbZoKJbx8z94F6C/xS4faQ2XsHINcd50qT5P////8CQB8AAAAAAAAWABRzMqPZdHy0JMwPKY/YRhrODmyijpzpHgAAAAAAGXapFKmSAlpRxRJXr8ywpN500rXVsxIHiKwAAAAAIgYCUEaaspCm8AVu5WJ9Z7RG53iW66Btq6QODz7xfMlRogoYjYLRHywAAIABAACAAAAAgAEAAAADAAAAIgICUEaaspCm8AVu5WJ9Z7RG53iW66Btq6QODz7xfMlRogpIMEUCIQCrgKRLUmzYL8edfZ7cltQWnLdVdU2Ta3BYbu5NR8JYwgIgDeOEdkOmOkPp7HpC4dkniXvXsvfzagmJ6pkBDd2DcgwBAAAA" + EOL, getOutput()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_sentence_without_passphrase_should_output_xpub() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs(new String[] {"wallet", | ||||
|                                "--sentence","ordinary debate stomach mix poverty upset amateur small sadness female general fabric", | ||||
|                                "listxpub"}); | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals("[A6932DE1:m/44'/0'/0']xpub6BjcsZDafUukJNMhD1Porw4SZB1RkiCE7EsQWJGVaMcZd9qXyawcjeVa28SJt5WNNAKJGGUbebfgzyrsWPTsLRkWmMNwrThaq8umcp7Yzn8" + EOL, getOutput()); | ||||
|     } | ||||
| 
 | ||||
|     @Test | ||||
|     void with_sentence_without_passphrase_should_output_xpub_qrcode() throws UnsupportedEncodingException { | ||||
| 
 | ||||
|         withArgs(new String[] {"wallet", | ||||
|                                "--sentence","ordinary debate stomach mix poverty upset amateur small sadness female general fabric", | ||||
|                                "listxpub", | ||||
|                                "--qrcode"}); | ||||
| 
 | ||||
|         String ExpectedQRCode = | ||||
|                 "█▀▀▀▀▀█ █  ▄    ██▀▄▄ ▀▀ ▀  █ ██▄ █▀▀▀▀▀█\n" + | ||||
|                 "█ ███ █ █▄  ▄▀▄█ ▄▄█▄▀███▄█▄▀█▄██ █ ███ █\n" + | ||||
|                 "█ ▀▀▀ █ ▄▄▄█ ▀▀ ▄ ▀█▀▄▀▀  ▄▀▀▀ █  █ ▀▀▀ █\n" + | ||||
|                 "▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▄█▄▀ ▀▄▀ ▀ ▀ ▀▄█▄█ ▀▀▀▀▀▀▀\n" + | ||||
|                 "▀▀▄ ▀▀▀▄ ▀ ▀█▄▄▄▄▀▀█  █ ▀█▄▀█▀▄█▀  █▄▀█▀▀\n" + | ||||
|                 "▀▄ ▀█ ▀▄▄▀  █ ▄▀██▄▄▀▄ ▀█▀ █▀█▀▄ █▀███  █\n" + | ||||
|                 "███▄▀▀▀▄█ █▀▀█▀▀▄▄▀█▀ █▄▀█  ▄█ ▄ ▀▄▄▄█ ▄▄\n" + | ||||
|                 " ▀   ▄▀▄█▄▄ ▄ ▄ ▀█▄█▄█▀██▀ █ █ ▄▀▄█  █▀▄█\n" + | ||||
|                 " ▀▄ ▄█▀█▀ ▀▀  ▀█▄█  ▀▄██▄▄▀█▀ ▄█▀▀▄████  \n" + | ||||
|                 "▄ ▄▀█▄▀  █████▀▀▄ ▀ ██▀██ ▀▀ ▄▄▄▀▄▀▄▀ ▀▄▀\n" + | ||||
|                 "  █▄▄ ▀▀█▄█ ▄█▄ ▀ ▄▄▄▀█▄▀▄▄ ▄ ▄  ▀▄▄▄▄ ██\n" + | ||||
|                 "▀▀█▀▀▄▀   ▄█▀▀ █▄█▄ ▀▄ ██ ▄█ █▀▄ ▀██▀▄▀▄█\n" + | ||||
|                 "▀▄█  ▄▀ ▄ █▄▄▄▀▄▄  █▀▄▀ ▀▀ ███▀▀▀▀▄█▄▀█ ▀\n" + | ||||
|                 "▀ ▄█ ▀▀ ▄▀ █▄ ▄▀ █▄ ▀▄█▀▀  ▀██▀ █ ▀▄▀▄▀▄▀\n" + | ||||
|                 "█▀█▄▀▀▀ ██▄ ▀▀▀ ▄▄▀█▀█▀▄ ▄▄▄▄  ▀ █▄▄▄▀███\n" + | ||||
|                 "   ▀██▀▀▀█▀ ▄   ▀█ █▄▄██ ▀ ▀ █▄ ▀▄▀  █▀ ▀\n" + | ||||
|                 "▀▀    ▀▀▄▀▀█ ▀ ███  ▀▄▀▀▀▄▄▄█   █▀▀▀███▀▀\n" + | ||||
|                 "█▀▀▀▀▀█ ▄█▀▀ █▀█▄ ▀▄▄▄▄█▀ ▄▀ ▄ ██ ▀ █▄▀ ▀\n" + | ||||
|                 "█ ███ █ ▀▀█▀▄█▄ ▀▄▄██▀▄█▄  ▀█▀▄▀██▀▀▀▀▄▀▄\n" + | ||||
|                 "█ ▀▀▀ █ ▄█ █▀▀▄█▄█▄  ▄ █▀▀▄▀▄█ █▄▄▀▄▄█▀▄█\n" + | ||||
|                 "▀▀▀▀▀▀▀ ▀ ▀ ▀▀   ▀    ▀ ▀  ▀ ▀  ▀ ▀   ▀▀ " + EOL; | ||||
| 
 | ||||
|         doMain(); | ||||
| 
 | ||||
|         assertEquals(ExpectedQRCode, getOutput()); | ||||
|     } | ||||
| 
 | ||||
|     private void doMain() { | ||||
| 
 | ||||
|         setInput(); | ||||
| 
 | ||||
|         ServiceLocator.DEFAULT_CHARSET = StandardCharsets.UTF_8; | ||||
| 
 | ||||
|         RudefoxCold.main(getArgs()); | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user