1 module tagion.tools.wallet.geldbeutel; 2 import core.thread; 3 import std.algorithm; 4 import std.array; 5 import std.file : exists, mkdir; 6 import std.format; 7 import std.getopt; 8 import std.path; 9 import std.range; 10 import std.stdio; 11 import std.typecons; 12 import std.exception : ifThrown; 13 import core.stdc.stdio : printf; 14 import tagion.basic.Message; 15 import tagion.basic.Types : FileExtension, hasExtension; 16 import tagion.basic.tagionexceptions; 17 import tagion.hibon.Document; 18 import tagion.hibon.HiBONFile : fread, fwrite; 19 import tagion.hibon.HiBONRecord : isHiBONRecord; 20 import tagion.communication.HiRPC; 21 import tagion.network.ReceiveBuffer; 22 import tagion.script.TagionCurrency; 23 import tagion.tools.Basic; 24 import tagion.tools.revision; 25 import tagion.tools.wallet.WalletInterface; 26 import tagion.tools.wallet.WalletOptions; 27 import tagion.utils.Term; 28 import tagion.wallet.AccountDetails; 29 import tagion.wallet.KeyRecover; 30 import tagion.wallet.SecureWallet; 31 import tagion.wallet.WalletRecords; 32 import tagion.wallet.BIP39; 33 import tagion.basic.Types : encodeBase64; 34 import tagion.hibon.HiBONException; 35 36 mixin Main!(_main, "wallet"); 37 38 import tagion.crypto.SecureNet; 39 import Wallet = tagion.wallet.SecureWallet; 40 41 /** 42 * @brief build file path if needed file with folder long path 43 * @param file - input/output parameter with filename 44 * @param path - forlders destination to file 45 */ 46 @safe 47 static void set_path(ref string file, string path) { 48 file = buildPath(path, file.baseName); 49 } 50 51 int _main(string[] args) { 52 import tagion.wallet.SecureWallet : check; 53 54 immutable program = args[0]; 55 bool version_switch; 56 bool overwrite_switch; /// Overwrite the config file 57 bool create_account; 58 bool change_pin; 59 60 string path; 61 string pincode; 62 uint bip39; 63 bool bip39_recover; 64 bool wallet_ui; 65 bool show_info; 66 bool pubkey_info; 67 bool list; 68 bool sum; 69 string _passphrase; 70 string _salt; 71 char[] passphrase; 72 char[] salt; 73 string account_name; 74 scope (exit) { 75 passphrase[] = 0; 76 salt[] = 0; 77 } 78 GetoptResult main_args; 79 WalletOptions options; 80 WalletInterface.Switch wallet_switch; 81 const user_config_file = args 82 .countUntil!(file => file.hasExtension(FileExtension.json) && file.exists); 83 auto config_file = (user_config_file < 0) ? default_wallet_config_filename : args[user_config_file]; 84 if (config_file.exists) { 85 options.load(config_file); 86 } 87 else { 88 options.setDefault; 89 } 90 91 try { 92 main_args = getopt(args, std.getopt.config.caseSensitive, 93 std.getopt.config.bundling, 94 "version", "display the version", &version_switch, 95 "v|verbose", "Enable verbose print-out", &__verbose_switch, 96 "O|overwrite", "Overwrite the config file and exits", &overwrite_switch, 97 "path", format("Set the path for the wallet files : default %s", path), &path, 98 "wallet", format("Wallet file : default %s", options.walletfile), &options.walletfile, 99 "device", format("Device file : default %s", options.devicefile), &options.devicefile, 100 "quiz", format("Quiz file : default %s", options.quizfile), &options.quizfile, 101 "C|create", "Create a new account", &create_account, 102 "c|changepin", "Change pin-code", &change_pin, 103 "o|output", "Output filename", &wallet_switch.output_filename, 104 "l|list", "List wallet content", &list, 105 "s|sum", "Sum of the wallet", &sum, 106 "send", "Send a contract to the shell", &wallet_switch.send, 107 "sendkernel", "Send a contract to the kernel", &wallet_switch.sendkernel, 108 "P|passphrase", "Set the wallet passphrase", &_passphrase, 109 "create-invoice", "Create invoice by format LABEL:PRICE. Example: Foreign_invoice:1000", &wallet_switch 110 .invoice, 111 "x|pin", "Pincode", &pincode, 112 "amount", "Create an payment request in tagion", &wallet_switch.amount, 113 "force", "Force input bill", &wallet_switch.force, 114 "pay", "Creates a payment contract", &wallet_switch.pay, 115 "dry", "Dry-run this will not save the wallet", &__dry_switch, 116 "req", "List all requested bills", &wallet_switch.request, 117 "update", "Request a wallet updated", &wallet_switch.update, 118 "trt-update", "Request a update on all derivers", &wallet_switch.trt_update, 119 120 "address", format( 121 "Sets the address default: %s", options.contract_address), 122 &options.addr, 123 "faucet", "request money from the faucet", &wallet_switch.faucet, 124 "bip39", "Generate bip39 set the number of words", &bip39, 125 "recover", "Recover bip39 from word list", &bip39_recover, 126 "name", "Sets the account name", &account_name, 127 "info", "Prints the public key and the name of the account", &show_info, 128 "pubkey", "Prints the public key", &pubkey_info, 129 130 ); 131 if (version_switch) { 132 revision_text.writeln; 133 return 0; 134 } 135 if (main_args.helpWanted) { 136 // writeln(logo); 137 defaultGetoptPrinter( 138 [ 139 // format("%s version %s", program, REVNO), 140 "Documentation: https://tagion.org/", 141 "", 142 "Usage:", 143 format("%s [<option>...] <config.json> <files>", program), 144 "", 145 146 "<option>:", 147 148 ].join("\n"), 149 main_args.options); 150 return 0; 151 } 152 153 verbose("Config file %s", config_file); 154 const new_config = (!config_file.exists || overwrite_switch); 155 if (path) { 156 if (!new_config) { 157 writefln("To change the path you need to use the overwrite switch -O"); 158 return 10; 159 } 160 options.walletfile.set_path(path); 161 options.quizfile.set_path(path); 162 options.devicefile.set_path(path); 163 options.accountfile.set_path(path); 164 options.billsfile.set_path(path); 165 options.paymentrequestsfile.set_path(path); 166 const dir = options.walletfile.dirName; 167 if (!dir.exists) { 168 dir.mkdir; 169 } 170 } 171 if (new_config) { 172 const new_config_file = args 173 .countUntil!(file => file.hasExtension(FileExtension.json) && !file.exists); 174 config_file = (new_config_file < 0) ? config_file : args[new_config_file]; 175 options.save(config_file); 176 if (overwrite_switch) { 177 return 0; 178 } 179 } 180 auto wallet_interface = WalletInterface(options); 181 if (!_salt.empty) { 182 auto salt_tmp = (() @trusted => cast(char[]) _salt)(); 183 scope (exit) { 184 salt_tmp[] = 0; 185 } 186 salt ~= WordList.presalt ~ _salt; 187 } 188 if (bip39 > 0 || bip39_recover) { 189 wallet_interface.load; 190 import std.uni; 191 import tagion.tools.secretinput; 192 import tagion.wallet.bip39_english : words; 193 194 if (bip39_recover) { 195 auto line = stdin.readln().dup; 196 toLowerInPlace(line); 197 auto word_list = line.split; 198 passphrase = word_list.join(" "); 199 bip39 = cast(uint) word_list.length; 200 } 201 const number_of_words = [12, 24]; 202 check(number_of_words.canFind(bip39), format("Invalid number of word %d should be (%(%d, %))", bip39, number_of_words)); 203 char[] pincode_1; 204 char[] pincode_2; 205 scope (exit) { 206 pincode_1[] = 0; 207 pincode_2[] = 0; 208 } 209 210 if (!dry_switch) { 211 info("Press ctrl-C to break"); 212 info("Press ctrl-A to show the pincode"); 213 while (pincode_1.length == 0) { 214 const keycode = getSecret("Pincode: ", pincode_1); 215 if (keycode == KeyStroke.KeyCode.CTRL_C) { 216 error("Wallet has not been created"); 217 return 1; 218 } 219 } 220 info("Repeat the pincode"); 221 for (;;) { 222 const keycode = getSecret("Pincode: ", pincode_2); 223 if (keycode == KeyStroke.KeyCode.CTRL_C) { 224 error("Wallet has not been created"); 225 return 1; 226 } 227 if (pincode_1 == pincode_2) { 228 break; 229 } 230 error("Pincode did not match"); 231 } 232 good("Pin-codes matches"); 233 } 234 if (!bip39_recover) { 235 const wordlist = WordList(words); 236 passphrase = wordlist.passphrase(bip39); 237 238 good("This is the recovery words"); 239 printf("%.*s\n", cast(int) passphrase.length, &passphrase[0]); 240 good("Write them down"); 241 } 242 const recovered = wallet_interface.generateSeedFromPassphrase(passphrase, pincode_1, _salt); 243 check(recovered, "Wallet was not recovered"); 244 good("Wallet was recovered"); 245 if (!dry_switch) { 246 wallet_interface.save(false); 247 } 248 return 0; 249 } 250 else { 251 (() @trusted { passphrase = cast(char[]) _passphrase; }()); 252 } 253 if (!passphrase.empty) { 254 check(!pincode.empty, "Missing pincode"); 255 wallet_interface.generateSeedFromPassphrase(passphrase, pincode); 256 wallet_interface.save(false); 257 return 0; 258 } 259 if (!wallet_interface.load) { 260 create_account = true; 261 writefln("Wallet dont't exists"); 262 WalletInterface.pressKey; 263 } 264 bool info_only; 265 if (show_info) { 266 if (wallet_interface.secure_wallet.account.name.empty) { 267 writefln("%sAccount name has not been set (use --name)%s", YELLOW, RESET); 268 return 0; 269 } 270 writefln("%s,%s", 271 wallet_interface.secure_wallet.account.name, 272 wallet_interface.secure_wallet.account.owner.encodeBase64); 273 info_only = true; 274 } 275 if (pubkey_info) { 276 if (wallet_interface.secure_wallet.account.owner.empty) { 277 warn("Account pubkey has not been set (use --name)"); 278 return 0; 279 } 280 writefln("%s", 281 wallet_interface.secure_wallet.account.owner.encodeBase64); 282 info_only = true; 283 } 284 if (list) { 285 const hash_net = new StdHashNet; 286 wallet_interface.listAccount(vout, hash_net); 287 wallet_interface.listInvoices(vout); 288 sum = true; 289 } 290 if (sum) { 291 wallet_interface.sumAccount(vout); 292 info_only = true; 293 } 294 if (info_only) { 295 return 0; 296 } 297 298 if (create_account) { 299 wallet_interface.generateSeed(wallet_interface.quiz.questions, false); 300 return 0; 301 } 302 else if (change_pin) { 303 wallet_interface.loginPincode(changepin : true); 304 check(wallet_interface.secure_wallet.isLoggedin, "Failed to login"); 305 good("Pincode correct"); 306 return 0; 307 } 308 309 if (wallet_interface.secure_wallet !is WalletInterface.StdSecureWallet.init) { 310 if (!pincode.empty) { 311 const flag = wallet_interface.secure_wallet.login(pincode); 312 313 if (!flag) { 314 error("Wrong pincode"); 315 return 3; 316 } 317 good("Loggedin"); 318 } 319 else if (!wallet_interface.loginPincode(changepin : false)) { 320 wallet_ui = true; 321 warn("Wallet not loggedin"); 322 return 4; 323 } 324 } 325 foreach (file; args.filter!(file => file.hasExtension(FileExtension.hibon))) { 326 check(file.exists, format("File %s not found", file)); 327 328 const hirpc_response = file.fread!(HiRPC.Receiver).ifThrown!HiBONException(HiRPC.Receiver.init); 329 if (hirpc_response is HiRPC.Receiver.init) { continue; } 330 writefln("File %s %s", file, hirpc_response.toPretty); 331 const ok = wallet_interface.secure_wallet.setResponseUpdateWallet(hirpc_response) 332 .ifThrown!HiBONException( 333 wallet_interface.secure_wallet.setResponseCheckRead(hirpc_response) 334 ); 335 336 check(ok, format("HiPRC %s is not a valid response", file)); 337 wallet_switch.save_wallet = true; 338 } 339 if (!account_name.empty) { 340 wallet_interface.secure_wallet.account.name = account_name; 341 wallet_interface.secure_wallet.account.owner = wallet_interface.secure_wallet.net.pubkey; 342 wallet_switch.save_wallet = true; 343 344 } 345 346 wallet_interface.operate(wallet_switch, args); 347 } 348 catch (GetOptException e) { 349 error(e.msg); 350 return 1; 351 } 352 catch (Exception e) { 353 error(e); 354 return 1; 355 } 356 return 0; 357 }