1 module tagion.tools.auszahlung.auszahlung; 2 import core.thread; 3 import std.algorithm; 4 import std.array; 5 import std.file : exists, mkdir, mkdirRecurse, setAttributes; 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.conv : to, octal; 13 import std.array; 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.network.ReceiveBuffer; 20 import tagion.script.TagionCurrency; 21 import tagion.tools.Basic; 22 import tagion.tools.revision; 23 import tagion.tools.wallet.WalletInterface; 24 import tagion.tools.wallet.WalletOptions; 25 import tagion.utils.Term; 26 import tagion.utils.Term; 27 import tagion.wallet.AccountDetails; 28 import tagion.wallet.KeyRecover; 29 import tagion.wallet.SecureWallet; 30 import tagion.wallet.WalletRecords; 31 import tagion.wallet.BIP39; 32 import tagion.basic.Types : encodeBase64, Buffer; 33 import tagion.basic.range : eatOne; 34 import tagion.basic.basic : isinit; 35 import tagion.crypto.SecureNet; 36 import tagion.crypto.Types; 37 import tagion.wallet.SecureWallet; 38 import tagion.hibon.BigNumber; 39 import tagion.hibon.HiBONtoText; 40 import tagion.script.common; 41 import tagion.crypto.random.random; 42 import tagion.utils.StdTime; 43 import tagion.communication.HiRPC; 44 import std.csv; 45 import std.string : representation; 46 47 mixin Main!(_main, "payout"); 48 49 import tagion.crypto.SecureNet; 50 import Wallet = tagion.wallet.SecureWallet; 51 52 /** 53 * @brief build file path if needed file with folder long path 54 * @param file - input/output parameter with filename 55 * @param path - forlders destination to file 56 */ 57 @safe 58 static void set_path(ref string file, string path) { 59 file = buildPath(path, file.baseName); 60 } 61 62 enum file_protect = octal!444; 63 enum MIN_WALLETS = 3; 64 int _main(string[] args) { 65 import tagion.wallet.SecureWallet : check; 66 67 immutable program = args[0]; 68 bool version_switch; 69 bool list; 70 bool sum; 71 bool force; 72 bool update; 73 string response_name; 74 // string path; 75 uint confidence; 76 double amount; 77 GetoptResult main_args; 78 WalletOptions options; 79 WalletInterface[] wallet_interfaces; 80 auto config_files = args 81 .filter!(file => file.hasExtension(FileExtension.json)); 82 auto config_file = default_wallet_config_filename; 83 try { 84 if (!config_files.empty) { 85 config_file = config_files.eatOne; 86 } 87 if (config_file.exists) { 88 options.load(config_file); 89 } 90 else { 91 options.setDefault; 92 } 93 94 main_args = getopt(args, std.getopt.config.caseSensitive, 95 std.getopt.config.bundling, 96 "version", "display the version", &version_switch, 97 "v|verbose", "Enable verbose print-out", &__verbose_switch, 98 "dry", "Dry-run this will not save the wallet", &__dry_switch, 99 "C|create", "Create the wallet an set the confidence", &confidence, 100 "l|list", "List wallet content", &list, 101 "s|sum", "Sum of the wallet", &sum, 102 "amount", "Create an payment request in tagion", &amount, //"path", "File path", &path, 103 "update", "Update wallet", &update, 104 "response", "Response from update (response.hibon)", &response_name, 105 "force", "Force input bill", &force, 106 107 ); 108 if (version_switch) { 109 revision_text.writeln; 110 return 0; 111 } 112 if (main_args.helpWanted) { 113 // writeln(logo); 114 defaultGetoptPrinter( 115 [ 116 // format("%s version %s", program, REVNO), 117 "Documentation: https://tagion.org/", 118 "", 119 "Usage:", 120 format("%s [<option>...] <wallet.json> [<bill.hibon>] ", program), 121 "", 122 123 "<option>:", 124 125 ].join("\n"), 126 main_args.options); 127 return 0; 128 } 129 check(config_file.exists, format("Wallet config %s not found", config_file)); 130 const(HashNet) hash_net = new StdHashNet; 131 WalletOptions[] all_options; 132 auto common_wallet_interface = WalletInterface(options); 133 common_wallet_interface.load; 134 if (list) { 135 common_wallet_interface.listAccount(stdout, hash_net); 136 common_wallet_interface.listInvoices(stdout); 137 sum = true; 138 } 139 if (sum) { 140 common_wallet_interface.sumAccount(stdout); 141 return 0; 142 } 143 if (config_files.empty) { 144 return 0; 145 } 146 foreach (file; config_files) { 147 verbose("file %s", file); 148 check(file.hasExtension(FileExtension.json), format("%s is not a %s file", file, FileExtension.json)); 149 check(file.exists, format("File %s not found", file)); 150 WalletOptions wallet_options; 151 wallet_options.load(file); 152 all_options ~= wallet_options; 153 } 154 verbose("Number of wallets %d", all_options.length); 155 156 foreach (wallet_option; all_options) { 157 auto wallet_interface = WalletInterface(wallet_option); 158 wallet_interface.load; 159 wallet_interfaces ~= wallet_interface; 160 } 161 import tagion.tools.secretinput; 162 163 info("Press ctrl-C to break"); 164 info("Press ctrl-D to skip the wallet"); 165 info("Press ctrl-A to show the pincode"); 166 { 167 auto wallets = wallet_interfaces[]; 168 while (!wallets.empty) { 169 170 writefln("Name %s", wallets.front.secure_wallet.account.name); 171 char[] pincode; 172 scope (exit) { 173 pincode[] = 0; 174 } 175 const keycode = getSecret("Pincode: ", pincode); 176 with (KeyStroke.KeyCode) { 177 switch (keycode) { 178 case CTRL_C: 179 error("Break the wallet login"); 180 return 1; 181 case CTRL_D: 182 warn("Skip %s", wallets.front.secure_wallet.account.name); 183 wallets.popFront; 184 continue; 185 default: 186 if (wallets.front.secure_wallet.login(pincode)) { 187 good("Pincode correct"); 188 wallets.popFront; 189 } 190 else { 191 error("Incorrect pincode"); 192 } 193 } 194 } 195 } 196 } 197 if (!confidence.isinit) { 198 check(common_wallet_interface.secure_wallet.wallet.isinit, 199 "Common wallet has already been created"); 200 check(wallet_interfaces.length >= MIN_WALLETS, format("More than %d wallets needed", MIN_WALLETS)); 201 check(wallet_interfaces.all!(wallet => wallet.secure_wallet.isLoggedin), 202 "The pincode of some of the wallet is not correct"); 203 check(confidence <= wallet_interfaces.length, format( 204 "Confidence can not be greater than number of wallets %d", wallet_interfaces.length)); 205 verbose("Confidence is %d", confidence); 206 Buffer[] answers; 207 foreach (ref wallet; wallet_interfaces) { 208 ubyte[] privkey; 209 scope (exit) { 210 privkey[] = 0; 211 } 212 const __net = cast(StdSecureNet)(wallet.secure_wallet.net); 213 __net.__expose(privkey); 214 answers ~= hash_net.rawCalcHash(privkey); 215 } 216 auto key_recover = KeyRecover(hash_net); 217 key_recover.createKey(answers, confidence); 218 common_wallet_interface.secure_wallet = 219 WalletInterface.StdSecureWallet(DevicePIN.init, key_recover.generator); 220 common_wallet_interface.secure_wallet.recover(answers); 221 222 verbose("Write wallet %s", common_wallet_interface.secure_wallet.isLoggedin); 223 options.questions = null; 224 225 common_wallet_interface.save(true); 226 options.save(config_file); 227 return 0; 228 } 229 { 230 common_wallet_interface.load; 231 confidence = common_wallet_interface.secure_wallet.wallet.confidence; 232 const number_of_loggins = wallet_interfaces 233 .count!((wallet_iface) => wallet_iface.secure_wallet.isLoggedin); 234 verbose("Loggedin %d", number_of_loggins); 235 check(confidence <= number_of_loggins, format("At least %d wallet need to open the transaction", confidence)); 236 237 Buffer[] answers; 238 foreach (wallet; wallet_interfaces) { 239 ubyte[] privkey; 240 scope (exit) { 241 privkey[] = 0; 242 } 243 const __net = cast(StdSecureNet)(wallet.secure_wallet.net); 244 if (__net) { 245 __net.__expose(privkey); 246 } 247 answers ~= hash_net.rawCalcHash(privkey); 248 } 249 common_wallet_interface.secure_wallet.recover(answers); 250 check(common_wallet_interface.secure_wallet.isLoggedin, "Wallet could not be activated"); 251 good("Wallet activated"); 252 } 253 if (!response_name.empty) { 254 verbose("Response %s", response_name); 255 scope (success) { 256 if (!dry_switch) { 257 common_wallet_interface.save(false); 258 } 259 } 260 const received = response_name.fread!(HiRPC.Receiver); 261 common_wallet_interface.secure_wallet.setResponseCheckRead(received); 262 return 0; 263 } 264 if (amount > 0) { 265 verbose("amount %s", amount); 266 scope (success) { 267 if (!dry_switch) { 268 common_wallet_interface.save(false); 269 } 270 } 271 check(wallet_interfaces.all!(wiface => wiface.secure_wallet.isLoggedin), 272 "All wallets must be loggedin to add amount"); 273 const amount_tgn = TagionCurrency(amount); 274 const bill = common_wallet_interface.secure_wallet.requestBill(amount_tgn); 275 string bill_path = buildPath(options.billsfile.dirName, "bills"); 276 mkdirRecurse(bill_path); 277 string filename; 278 uint bill_no; 279 do { 280 filename = buildPath(bill_path, format("bill_%s", bill_no)).setExtension(FileExtension.hibon); 281 bill_no++; 282 } 283 while (filename.exists); 284 /* 285 const filename = buildPath(bill_path, format("bill_%s", common_wallet_interface.secure_wallet.account.name)) 286 .setExtension(FileExtension.hibon); 287 */ 288 good("bill file %s", filename); 289 filename.fwrite(bill); 290 scope (success) { 291 if (!dry_switch) { 292 filename.setAttributes(file_protect); 293 } 294 } 295 return 0; 296 } 297 if (force) { 298 scope (success) { 299 if (!dry_switch) { 300 common_wallet_interface.save(false); 301 } 302 } 303 check(wallet_interfaces.all!(wiface => wiface.secure_wallet.isLoggedin), 304 "All wallets must be loggedin to force the bill"); 305 foreach (arg; args[1 .. $].filter!(file => file.hasExtension(FileExtension.hibon))) { 306 const bill = arg.fread!TagionBill; 307 with (common_wallet_interface) { 308 good("%s", toText(hash_net, bill)); 309 verbose("%s", show(bill)); 310 const added = secure_wallet.addBill(bill); 311 check(added, "Bill was not found"); 312 } 313 } 314 common_wallet_interface.listAccount(stdout, hash_net); 315 common_wallet_interface.listInvoices(stdout); 316 317 return 0; 318 } 319 auto csv_files = args[1 .. $].filter!(file => file.hasExtension(FileExtension.csv)); 320 const contracts = buildPath(options.accountfile.dirName, "contracts"); 321 if (!csv_files.empty) { 322 mkdirRecurse(contracts); 323 } 324 foreach (filename; csv_files) { 325 scope (success) { 326 if (!dry_switch) { 327 common_wallet_interface.save(false); 328 } 329 } 330 verbose("CVS %s", filename); 331 auto fin = File(filename, "r"); 332 scope (exit) { 333 fin.close; 334 } 335 enum payee_name = "Name"; 336 enum pubkey_name = "PUBKey"; 337 enum amount_name = "Amount"; 338 TagionBill[] to_pay; 339 TagionCurrency total_amount; 340 foreach (record; csvReader!(string[string])(fin.byLine.joiner("\n"), null, ';')) { 341 const pubkey = Pubkey(record[pubkey_name].decode); 342 const amount_tgn = record[amount_name].to!double.TGN; 343 total_amount += amount_tgn; 344 auto nonce = new ubyte[4]; 345 getRandom(nonce); 346 to_pay ~= TagionBill(amount_tgn, currentTime, pubkey, nonce.idup); 347 info("%10s %37s %-14sTGN", record[payee_name], pubkey.encodeBase64, amount_tgn); 348 } 349 SignedContract signed_contract; 350 TagionCurrency fees; 351 with (common_wallet_interface) { 352 secure_wallet.createPayment(to_pay, signed_contract, fees); 353 const contract_filename = buildPath(contracts, filename.baseName).setExtension(FileExtension.hibon); 354 const message = secure_wallet.net.calcHash(signed_contract); 355 const contract_net = secure_wallet.net.derive(message); 356 const hirpc = HiRPC(contract_net); 357 const hirpc_submit = hirpc.submit(signed_contract); 358 verbose("submit\n%s", show(hirpc_submit)); 359 secure_wallet.account.hirpcs ~= hirpc_submit.toDoc; 360 contract_filename.fwrite(hirpc_submit); 361 scope (success) { 362 if (!dry_switch) { 363 contract_filename.setAttributes(file_protect); 364 } 365 } 366 } 367 good("Total %sTGN", total_amount); 368 update = true; 369 } 370 if (update) { 371 verbose("Update"); 372 check(common_wallet_interface.secure_wallet.isLoggedin, "Wallet should be loggedin"); 373 auto basename = "update"; 374 if (!csv_files.empty) { 375 basename = csv_files.front.baseName; 376 } 377 with (common_wallet_interface) { 378 const message = secure_wallet.net.calcHash(WalletInterface.update_tag.representation); 379 const update_net = secure_wallet.net.derive(message); 380 const hirpc = HiRPC(update_net); 381 const req = secure_wallet.getRequestCheckWallet(hirpc); 382 const update_file = [basename, update_tag].join("_"); 383 const update_req = update_file.setExtension(FileExtension.hibon); 384 385 update_req.fwrite(req); 386 scope (success) { 387 if (!dry_switch) { 388 update_req.setAttributes(file_protect); 389 } 390 } 391 } 392 } 393 } 394 395 catch (GetOptException e) { 396 error(e.msg); 397 return 1; 398 } 399 catch (Exception e) { 400 error("%s", e.msg); 401 verbose("%s", e.toString); 402 return 1; 403 } 404 return 0; 405 }