1 /** 2 * Handles management of key-pair, account-details device-pin 3 */ 4 module tagion.wallet.SecureWallet; 5 @safe: 6 import core.time : MonoTime; 7 import std.algorithm; 8 import std.array; 9 import std.format; 10 import std.range; 11 import std.string : representation; 12 import tagion.utils.Miscellaneous; 13 import tagion.utils.Result; 14 import std.exception : assumeWontThrow; 15 16 //import std.stdio; 17 import tagion.basic.Types : Buffer; 18 import tagion.basic.basic : basename, isinit; 19 import tagion.crypto.Types : Pubkey; 20 import tagion.dart.DARTBasic; 21 import tagion.hibon.Document : Document; 22 import tagion.hibon.HiBON : HiBON; 23 import tagion.hibon.HiBONException : HiBONRecordException; 24 import tagion.hibon.HiBONJSON; 25 import tagion.hibon.HiBONRecord : HiBONRecord; 26 import tagion.utils.StdTime; 27 28 // import tagion.script.prior.StandardRecords : SignedContract, globals, Script; 29 //import PriorStandardRecords = tagion.script.prior.StandardRecords; 30 31 import tagion.crypto.SecureInterfaceNet : SecureNet; 32 33 // import tagion.gossip.GossipNet : StdSecureNet, StdHashNet, scramble; 34 import tagion.Keywords; 35 import tagion.basic.Message; 36 import tagion.basic.tagionexceptions : Check; 37 import tagion.communication.HiRPC; 38 import tagion.crypto.Cipher; 39 import tagion.crypto.random.random; 40 import tagion.script.TagionCurrency; 41 import tagion.script.common; 42 import tagion.utils.Miscellaneous; 43 import tagion.utils.StdTime; 44 import tagion.wallet.AccountDetails; 45 import tagion.wallet.Basic : saltHash; 46 import tagion.wallet.KeyRecover; 47 import tagion.wallet.WalletException : WalletException; 48 import tagion.wallet.WalletRecords : DevicePIN, RecoverGenerator; 49 50 alias check = Check!(WalletException); 51 alias CiphDoc = Cipher.CipherDocument; 52 53 import tagion.communication.HiRPC; 54 55 /// Function and data to recover, sign transaction and hold the account information 56 struct SecureWallet(Net : SecureNet) { 57 protected RecoverGenerator _wallet; /// Information to recover the seed-generator 58 protected DevicePIN _pin; /// Information to check the Pin code 59 60 AccountDetails account; /// Account-details holding the bills and generator 61 protected SecureNet _net; 62 63 const(SecureNet) net() const pure nothrow @nogc { 64 return _net; 65 } 66 67 version (NET_HACK) { 68 void set_net(SecureNet copy_net) { 69 this._net = copy_net; 70 } 71 72 } 73 74 /** 75 * 76 * Params: 77 * pin = Devices pin code information 78 * wallet = Infomation to recover the pin-code 79 * account = Acount to hold bills and derivers 80 */ 81 this(DevicePIN pin, 82 RecoverGenerator wallet = RecoverGenerator.init, 83 AccountDetails account = AccountDetails.init) nothrow { 84 _wallet = wallet; 85 _pin = pin; 86 this.account = account; 87 } 88 89 this(const Document wallet_doc, 90 const Document pin_doc = Document.init) { 91 auto __wallet = RecoverGenerator(wallet_doc); 92 DevicePIN __pin; 93 if (!pin_doc.empty) { 94 __pin = DevicePIN(pin_doc); 95 } 96 this(__pin, __wallet); 97 } 98 99 /** 100 * 101 * Returns: Wallet recovery information 102 */ 103 @nogc const(RecoverGenerator) wallet() pure const nothrow { 104 return _wallet; 105 } 106 107 /** 108 * Retreive the device-pin generation 109 * Returns: Device PIN infomation 110 */ 111 @nogc const(DevicePIN) pin() pure const nothrow { 112 return _pin; 113 } 114 115 /** 116 * 117 * Returns: The confidence of the answers 118 */ 119 @nogc uint confidence() pure const nothrow { 120 return _wallet.confidence; 121 } 122 123 /** 124 * Creates a wallet from a list for questions and answers 125 * Params: 126 * questions = List of question 127 * answers = List of answers 128 * confidence = Cofindence of the answers 129 * pincode = Devices pin code 130 * Returns: 131 * Create an new wallet accouring with the input 132 */ 133 this( 134 scope const(string[]) questions, 135 scope const(char[][]) answers, 136 uint confidence, 137 const(char[]) pincode) 138 in (questions.length is answers.length, "Amount of questions should be same as answers") 139 do { 140 check(questions.length > 3, "Minimal amount of answers is 4"); 141 _net = new Net(); 142 // auto hashnet = new StdHashNet; 143 auto recover = KeyRecover(_net); 144 145 if (confidence == questions.length) { 146 pragma(msg, "fixme(cbr): Due to some bug in KeyRecover"); 147 // Due to some bug in KeyRecover 148 confidence--; 149 } 150 151 recover.createKey(questions, answers, confidence); 152 auto R = new ubyte[_net.hashSize]; 153 scope (exit) { 154 R[] = 0; 155 } 156 recover.findSecret(R, questions, answers); 157 _net.createKeyPair(R); 158 _wallet = RecoverGenerator(recover.toDoc); 159 set_pincode(R, pincode); 160 } 161 162 this(scope const(ubyte)[][] answers, uint confidence, const(char[]) pincode = null) { 163 164 _net = new Net(); 165 auto recover = KeyRecover(_net); 166 if (confidence == answers.length) { 167 confidence--; 168 } 169 recover.createKey(answers, confidence); 170 auto R = new ubyte[_net.hashSize]; 171 scope (exit) { 172 R[] = 0; 173 } 174 recover.quizSeed(R, answers, confidence); 175 _net.createKeyPair(R); 176 _wallet = RecoverGenerator(recover.toDoc); 177 if (!pincode.empty) { 178 set_pincode(R, pincode); 179 } 180 181 } 182 183 this( 184 scope const(char[]) passphrase, 185 scope const(char[]) pincode, 186 scope const(char[]) salt = null) { 187 _net = new Net; 188 enum size_of_privkey = 32; 189 ubyte[] R; 190 scope (exit) { 191 _wallet.S = _net.saltHash(R); 192 set_pincode(R, pincode); 193 R[] = 0; 194 } 195 _net.generateKeyPair(passphrase, salt, 196 (scope const(ubyte[]) data) { R = data[0 .. size_of_privkey].dup; }); 197 } 198 199 protected void set_pincode( 200 scope const(ubyte[]) R, 201 scope const(char[]) pincode) scope 202 in (!_net.isinit) 203 do { 204 auto seed = new ubyte[_net.hashSize]; 205 getRandom(seed); 206 _pin.setPin(_net, R, pincode.representation, seed.idup); 207 } 208 209 /** 210 * Checks that the answers to the question is correct 211 * Params: 212 * questions = List of questions 213 * answers = List of answers 214 * Returns: 215 * True of N=confidence number of answers is correct 216 */ 217 bool correct(const(string[]) questions, const(char[][]) answers) 218 in (questions.length is answers.length, "Amount of questions should be same as answers") 219 do { 220 _net = new Net; 221 auto recover = KeyRecover(_net, _wallet); 222 scope R = new ubyte[_net.hashSize]; 223 return recover.findSecret(R, questions, answers); 224 } 225 226 /** 227 * Recover the key-pair from the quiz or the device-pincode 228 * Params: 229 * questions = List of question 230 * answers = List of answers 231 * pincode = Devices pin code 232 * Returns: 233 * True if the key-pair has been recovered for the quiz or the pincode 234 */ 235 bool recover( 236 const(string[]) questions, 237 const(char[][]) answers, 238 const(char[]) pincode) 239 in (questions.length is answers.length, "Amount of questions should be same as answers") 240 do { 241 _net = new Net; 242 auto recover = KeyRecover(_net, _wallet); 243 auto R = new ubyte[_net.hashSize]; 244 scope (exit) { 245 R[] = 0; 246 } 247 const result = recover.findSecret(R, questions, answers); 248 if (result) { 249 set_pincode(R, pincode); 250 _net.createKeyPair(R); 251 return true; 252 } 253 _net = null; 254 return false; 255 } 256 257 bool recover(Buffer[] A, const(char[]) pincode = null) { 258 _net = new Net; 259 auto recover = KeyRecover(_net, _wallet); 260 auto R = new ubyte[_net.hashSize]; 261 scope (exit) { 262 R[] = 0; 263 } 264 const result = recover.findSecret(R, A); 265 if (result) { 266 if (!pincode.empty) { 267 set_pincode(R, pincode); 268 269 } 270 _net.createKeyPair(R); 271 return true; 272 } 273 _net = null; 274 return false; 275 } 276 /** 277 * Checks if the wallet contains a key-pair 278 * Returns: true if the wallet is loggin 279 */ 280 @nogc bool isLoggedin() pure const nothrow { 281 return _net !is null; 282 } 283 284 /** 285 * Throws a WalletExceptions if the wallet is not logged in 286 */ 287 protected void checkLogin() pure const { 288 check(isLoggedin(), "Need login first"); 289 } 290 291 /** 292 * Generates the key-pair from the pin code 293 * Params: 294 * pincode = device pincode 295 * Returns: true of the pin-code 296 */ 297 bool login(const(char[]) pincode) { 298 if (_pin.D) { 299 logout; 300 auto login_net = new Net; 301 auto R = new ubyte[login_net.hashSize]; 302 scope (exit) { 303 R[] = 0; 304 } 305 const recovered = _pin.recover(login_net, R, pincode.representation); 306 if (recovered) { 307 login_net.createKeyPair(R); 308 _net = login_net; 309 return true; 310 } 311 } 312 return false; 313 } 314 315 /** 316 * Removes the key-pair 317 */ 318 void logout() pure nothrow { 319 _net = null; 320 } 321 322 /** 323 * Checks if the pincode is correct 324 * Params: 325 * pincode = device pincode 326 * Returns: true if the pincode is correct 327 */ 328 bool checkPincode(const(char[]) pincode) { 329 const hashnet = (_net.isinit) ? new Net : _net; 330 331 scope R = new ubyte[hashnet.hashSize]; 332 scope (exit) { 333 R[] = 0; 334 } 335 _pin.recover(hashnet, R, pincode.representation); 336 return _pin.S == hashnet.saltHash(R); 337 } 338 339 /** 340 * Check the pincode 341 * Params: 342 * pincode = current device pincode 343 * new_pincode = new device pincode 344 * Returns: true of the pincode has been change succesfully 345 */ 346 bool changePincode(const(char[]) pincode, const(char[]) new_pincode) { 347 check(!_net.isinit, "Key pair has not been created"); 348 auto R = new ubyte[_net.hashSize]; 349 _pin.recover(_net, R, pincode.representation); 350 if (_pin.S == _net.saltHash(R)) { 351 set_pincode(R, new_pincode); 352 logout; 353 return true; 354 } 355 return false; 356 } 357 358 /** 359 * Register an invoice to the wallet 360 * Params: 361 * invoice = invoice to be registered 362 */ 363 void registerInvoice(ref Invoice invoice) { 364 account.derive_state = _net.HMAC(account.derive_state ~ _net.pubkey); 365 auto pkey = _net.derivePubkey(account.derive_state); 366 invoice.pkey = derivePubkey; 367 account.derivers[invoice.pkey] = account.derive_state; 368 account.requested_invoices ~= invoice; 369 } 370 /** 371 * Create a new invoice which can be send to a payee 372 * Params: 373 * label = Name of the invoice 374 * amount = Amount 375 * info = Invoce information 376 * Returns: The created invoice 377 */ 378 static Invoice createInvoice(string label, TagionCurrency amount, Document info = Document.init) { 379 Invoice new_invoice; 380 new_invoice.name = label; 381 new_invoice.amount = amount; 382 new_invoice.info = info; 383 return new_invoice; 384 } 385 386 Pubkey derivePubkey() { 387 checkLogin; 388 account.derive_state = _net.HMAC(account.derive_state ~ _net.pubkey); 389 return _net.derivePubkey(account.derive_state); 390 } 391 392 TagionBill[] invoices_to_bills(const(Invoice[]) orders) { 393 Buffer getNonce() { 394 scope nonce = new ubyte[4]; 395 getRandom(nonce); 396 return nonce.idup; 397 } 398 399 return orders.map!((order) => TagionBill(order.amount, currentTime, order.pkey, getNonce)).array; 400 } 401 402 /** 403 * Create a payment to a list of Invoices and produces a signed-contract 404 * Collect the bill need and sign them in the contract 405 * Params: 406 * orders = List of invoices 407 * result = Signed payment 408 * Returns: 409 */ 410 Result!bool payment(const(Invoice[]) orders, ref SignedContract signed_contract, out TagionCurrency fees) nothrow { 411 import tagion.utils.StdTime; 412 413 try { 414 checkLogin; 415 auto bills = invoices_to_bills(orders); 416 return createPayment(bills, signed_contract, fees); 417 } 418 catch (Exception e) { 419 return Result!bool(e); 420 } 421 return result(true); 422 } 423 424 /** 425 * Calculates the amount which can be activate 426 * Returns: the amount of available amount 427 */ 428 TagionCurrency available_balance() const pure { 429 return account.available; 430 } 431 432 /** 433 * Calcutales the locked amount in the network 434 * Returns: the locked amount 435 */ 436 TagionCurrency locked_balance() const pure { 437 return account.locked; 438 } 439 440 /** 441 * Calcutales the total amount 442 * Returns: total amount 443 */ 444 TagionCurrency total_balance() const pure { 445 return account.total; 446 } 447 448 /** 449 * Clear the locked bills 450 */ 451 @trusted 452 void unlockBills() { 453 account.activated.clear; 454 } 455 456 /** 457 * Creates HiRPC to request an wallet update 458 * Returns: The command to the the update 459 */ 460 const(HiRPC.Sender) getRequestUpdateWallet(HiRPC hirpc = HiRPC(null)) const { 461 auto h = new HiBON; 462 h = account.derivers.byKey.map!(p => cast(Buffer) p); 463 return hirpc.search(h); 464 } 465 466 const(DARTIndex[]) billIndexes(const(TagionBill)[] bills) const { 467 return bills 468 .map!(bill => net.dartIndex(bill)) 469 .array; 470 } 471 472 const(HiRPC.Sender) getRequestCheckWallet( 473 HiRPC hirpc = HiRPC(null), 474 const(TagionBill)[] to_check = null) 475 const { 476 import tagion.dart.DARTcrud; 477 478 if (to_check is null) { 479 to_check = account.bills ~ account.requested.values; 480 } 481 return dartCheckRead(billIndexes(to_check), hirpc); 482 483 } 484 485 const(SecureNet[]) collectNets(const(TagionBill[]) bills) { 486 return bills 487 .map!(bill => bill.owner in account.derivers) 488 .map!((deriver) => (deriver is null) ? _net.init : _net.derive(*deriver)) 489 .array; 490 } 491 /** 492 * Collects the bills for the amount 493 * Params: 494 * amount = the amount to be collected 495 * locked_bills = the list of bills 496 * Returns: true if wallet has enough to pay the amount 497 */ 498 private bool collect_bills(const TagionCurrency amount, out TagionBill[] locked_bills) { 499 import std.algorithm; 500 501 // import std.algorithm.sorting : isSorted, sort; 502 // import std.algorithm.iteration : cumulativeFold; 503 import std.range : takeOne, tee; 504 import std.stdio; 505 506 if (!account.bills.isSorted!q{a.value > b.value}) { 507 account.bills.sort!q{a.value > b.value}; 508 } 509 510 // Select all bills not in use 511 auto none_locked = account.bills.filter!(b => !(b.owner in account.activated)).array; 512 513 const enough = !none_locked 514 .map!(b => b.value) 515 .cumulativeFold!((a, b) => a + b) 516 .filter!(a => a >= amount) 517 .takeOne 518 .empty; 519 if (enough) { 520 TagionCurrency rest = amount; 521 locked_bills = none_locked 522 .filter!(b => b.value <= rest) // take all bills smaller than the rest 523 .until!(b => rest <= 0) // do it until the rest is smaller than or equal to zero 524 .tee!((b) => rest -= b.value) // subtract their values from the rest 525 .array; 526 527 // Check if there is any remaining rest 528 if (rest >= 0) { 529 TagionBill extra_bill; 530 // Find an appropriate extra_bill 531 auto extra_bills = none_locked 532 .filter!(b => !locked_bills.canFind(b) && b.value >= rest); // Only consider bills with enough value 533 if (!extra_bills.empty) { 534 extra_bill = extra_bills.front; // Select the first appropriate bill 535 locked_bills ~= extra_bill; 536 } 537 } 538 return true; 539 } 540 return false; 541 } 542 543 void lock_bills(const(TagionBill[]) locked_bills) { 544 locked_bills.each!(b => account.activated[b.owner] = true); 545 } 546 547 bool setResponseCheckRead(const(HiRPC.Receiver) receiver) { 548 import tagion.dart.DART; 549 550 if (!receiver.isResponse) { 551 return false; 552 } 553 554 auto not_in_dart = receiver.response.result[DART.Params.dart_indices].get!Document[].map!(d => d.get!Buffer); 555 556 foreach (not_found; not_in_dart) { 557 const bill_index = account.bills 558 .countUntil!(bill => net.dartIndex(bill) == not_found); 559 560 if (bill_index >= 0) { 561 562 auto used_bill = account.bills[bill_index]; 563 account.used_bills ~= used_bill; 564 account.bills = account.bills.remove(bill_index); 565 if (used_bill.owner in account.activated) { 566 account.activated.remove(used_bill.owner); 567 } 568 } 569 } 570 foreach (request_bill; account.requested.byValue.array.dup) { 571 if (!not_in_dart.canFind(net.dartIndex(request_bill))) { 572 account.bills ~= request_bill; 573 account.requested.remove(request_bill.owner); 574 } 575 } 576 return true; 577 } 578 /** 579 * Update the the wallet for a request update 580 * Params: 581 * receiver = response to the wallet 582 * Returns: ture if the wallet was updated 583 */ 584 @trusted 585 bool setResponseUpdateWallet(const(HiRPC.Receiver) receiver) { 586 import tagion.hibon.HiBONtoText; 587 588 if (!receiver.isResponse) { 589 return false; 590 } 591 592 auto found_bills = receiver.response 593 .result[] 594 .map!(e => TagionBill(e.get!Document)) 595 .array; 596 597 foreach (b; found_bills) { 598 if (b.owner !in account.derivers) { 599 import std.stdio; 600 601 writefln("Error, could not pubkey %(%02x%) in derivers", b.owner); 602 return false; 603 } 604 } 605 606 foreach (found; found_bills) { 607 if (!account.bills.canFind(found)) { 608 account.bills ~= found; 609 } 610 account.requested.remove(found.owner); 611 612 const invoice_index = account.requested_invoices 613 .countUntil!(invoice => invoice.pkey == found.owner); 614 615 if (invoice_index >= 0) { 616 account.requested_invoices = account.requested_invoices.remove(invoice_index); 617 } 618 619 } 620 621 auto locked_pkeys = account.activated 622 .byKeyValue 623 .filter!(a => a.value == true) 624 .map!(a => a.key) 625 .array; 626 627 auto found_owners = found_bills.map!(found => found.owner).array; 628 foreach (pkey; locked_pkeys) { 629 if (!(found_owners.canFind(pkey))) { 630 account.activated.remove(pkey); 631 auto bill_index = account.bills.countUntil!(b => b.owner == pkey); 632 if (bill_index >= 0) { 633 account.bills = account.bills.remove(bill_index); 634 } 635 } 636 } 637 638 // account.activated = new_activated; 639 640 // go through the locked bills 641 642 return true; 643 } 644 645 Result!bool getFee(const(TagionBill)[] to_pay, out TagionCurrency fees, bool print = false) nothrow { 646 import tagion.script.Currency : totalAmount; 647 import tagion.script.execute; 648 649 try { 650 PayScript pay_script; 651 pay_script.outputs = to_pay; 652 TagionBill[] collected_bills; 653 TagionCurrency amount_remainder = 0.TGN; 654 size_t previous_bill_count = size_t.max; 655 656 const amount_to_pay = pay_script.outputs 657 .map!(bill => bill.value) 658 .totalAmount; 659 const available_wallet_amount = available_balance(); 660 661 do { 662 collected_bills.length = 0; 663 664 const amount_to_collect = amount_to_pay + (-1 * (amount_remainder)) + fees; 665 check(amount_to_collect < available_wallet_amount, "Amount is too big with fees"); 666 667 const can_pay = collect_bills(amount_to_collect, collected_bills); 668 669 if (collected_bills.length == previous_bill_count) { 670 return result(false); 671 } 672 673 check(can_pay, format("Is unable to pay the amount %10.6fTGN available %10.6fTGN", amount_to_pay.value, available_balance 674 .value)); 675 const total_collected_amount = collected_bills 676 .map!(bill => bill.value) 677 .totalAmount; 678 fees = ContractExecution.billFees( 679 collected_bills.map!(bill => bill.toDoc), 680 pay_script.outputs.map!(bill => bill.toDoc), 681 snavs_byte_fee); 682 amount_remainder = total_collected_amount - amount_to_pay - fees; 683 684 previous_bill_count = collected_bills.length; 685 } 686 while (amount_remainder < 0); 687 } 688 catch (Exception e) { 689 return Result!bool(e); 690 } 691 return result(true); 692 } 693 694 Result!bool getFee(const(Invoice[]) orders, out TagionCurrency fees) nothrow { 695 696 auto bills = orders.map!((order) => TagionBill(order.amount, assumeWontThrow(currentTime), order.pkey, Buffer 697 .init)) 698 .array; 699 return getFee(bills, fees); 700 } 701 702 static immutable dummy_pubkey = Pubkey(new ubyte[33]); 703 static immutable dummy_nonce = new ubyte[4]; 704 705 Result!bool getFee(TagionCurrency amount, out TagionCurrency fees) nothrow { 706 auto bill = TagionBill(amount, assumeWontThrow(currentTime), dummy_pubkey, dummy_nonce); 707 return getFee([bill], fees); 708 } 709 710 // stupid function for testing 711 Result!bool createNFT( 712 Document nft_data, 713 ref SignedContract signed_contract) { 714 import tagion.script.execute; 715 import tagion.script.standardnames; 716 717 try { 718 HiBON dummy_input = new HiBON; 719 dummy_input[StdNames.owner] = net.pubkey; 720 dummy_input["NFT"] = nft_data; 721 Document[] inputs; 722 723 inputs ~= Document(dummy_input); 724 SecureNet[] nets; 725 nets ~= (() @trusted => cast(SecureNet) net)(); 726 727 signed_contract = sign( 728 nets, 729 inputs, 730 null, 731 Document.init); 732 } 733 catch (Exception e) { 734 return Result!bool(e); 735 } 736 return result(true); 737 } 738 739 enum long snavs_byte_fee = 100; 740 Result!bool createPayment(const(TagionBill)[] to_pay, ref SignedContract signed_contract, out TagionCurrency fees, bool print = false) nothrow { 741 import tagion.hibon.HiBONtoText; 742 import tagion.script.Currency : totalAmount; 743 import tagion.script.execute; 744 745 try { 746 PayScript pay_script; 747 pay_script.outputs = to_pay; 748 TagionBill[] collected_bills; 749 TagionCurrency amount_remainder = 0.TGN; 750 size_t previous_bill_count = size_t.max; 751 752 const amount_to_pay = pay_script.outputs 753 .map!(bill => bill.value) 754 .totalAmount; 755 check(amount_to_pay < available_balance, "The amount requested for payment should be smaller than the available balance"); 756 757 do { 758 collected_bills.length = 0; 759 const amount_to_collect = amount_to_pay + (-1 * (amount_remainder)) + fees; 760 const can_pay = collect_bills(amount_to_collect, collected_bills); 761 if (collected_bills.length == previous_bill_count || collected_bills.length == 0) { 762 return result(false); 763 } 764 check(can_pay, format("Is unable to pay the amount %10.6fTGN available %10.6fTGN", 765 amount_to_pay.value, 766 available_balance.value)); 767 const total_collected_amount = collected_bills 768 .map!(bill => bill.value) 769 .totalAmount; 770 771 if (print) { 772 import std.stdio; 773 774 writefln("calculated fee=%s", ContractExecution.billFees( 775 collected_bills.map!(bill => bill.toDoc), 776 pay_script.outputs.map!(bill => bill.toDoc), 777 snavs_byte_fee) 778 ); 779 } 780 fees = ContractExecution.billFees( 781 collected_bills.map!(bill => bill.toDoc), 782 pay_script.outputs.map!(bill => bill.toDoc), 783 snavs_byte_fee); 784 amount_remainder = total_collected_amount - amount_to_pay - fees; 785 previous_bill_count = collected_bills.length; 786 787 } 788 while (amount_remainder < 0); 789 790 const nets = collectNets(collected_bills); 791 check(nets.all!(net => net !is net.init), format("Missing deriver of some of the bills length=%s", collected_bills 792 .length)); 793 if (amount_remainder != 0) { 794 const bill_remain = requestBill(amount_remainder); 795 pay_script.outputs ~= bill_remain; 796 } 797 lock_bills(collected_bills); 798 check(nets.length == collected_bills.length, format("number of bills does not match number of signatures nets %s, collected_bills %s", nets 799 .length, collected_bills.length)); 800 801 signed_contract = sign( 802 nets, 803 collected_bills.map!(bill => bill.toDoc) 804 .array, 805 null, 806 pay_script.toDoc); 807 } 808 catch (Exception e) { 809 return Result!bool(e); 810 } 811 return result(true); 812 } 813 /** 814 * Calculates the amount in a list of bills 815 * Params: 816 * bills = list of bills 817 * Returns: total amount 818 */ 819 static TagionCurrency calcTotal(const(TagionBill[]) bills) pure { 820 return bills.map!(b => b.value).sum; 821 } 822 823 Buffer getPublicKey() { 824 import std.typecons; 825 826 const pkey = _net.pubkey; 827 return cast(TypedefType!Pubkey)(pkey); 828 } 829 830 struct DeriverState { 831 Buffer[Pubkey] derivers; 832 Buffer derive_state; 833 mixin HiBONRecord; 834 } 835 836 Buffer getDeriversState() { 837 return this.account.derive_state; 838 } 839 840 TagionBill requestBill(TagionCurrency amount, sdt_t bill_time = currentTime) { 841 check(amount > 0.TGN, format("Requested bill should have a positive value and not %10.6fTGN", amount.value)); 842 TagionBill bill; 843 bill.value = amount; 844 bill.time = bill_time; 845 auto nonce = new ubyte[4]; 846 getRandom(nonce); 847 bill.nonce = nonce.idup; 848 auto derive = _net.HMAC(bill.toDoc.serialize); 849 bill.owner = _net.derivePubkey(derive); 850 //account.bills ~= bill; 851 account.requestBill(bill, derive); 852 return bill; 853 } 854 855 TagionBill addBill(const Document doc) { 856 return account.add_bill(doc); 857 } 858 859 bool addBill(TagionBill bill) { 860 return account.add_bill(bill); 861 } 862 863 @trusted 864 const(CiphDoc) getEncrDerivers() { 865 DeriverState derive_state; 866 derive_state.derivers = this.account.derivers; 867 derive_state.derive_state = this.account.derive_state; 868 return Cipher.encrypt(this._net, derive_state.toDoc); 869 } 870 871 void setEncrDerivers(const(CiphDoc) cipher_doc) { 872 Cipher cipher; 873 const derive_state_doc = cipher.decrypt(this._net, cipher_doc); 874 DeriverState derive_state = DeriverState(derive_state_doc); 875 this.account.derivers = derive_state.derivers; 876 this.account.derive_state = derive_state.derive_state; 877 } 878 879 @trusted 880 const(CiphDoc) getEncrAccount() { 881 return Cipher.encrypt(this._net, this.account.toDoc); 882 } 883 884 void setEncrAccount(const(CiphDoc) cipher_doc) { 885 Cipher cipher; 886 const account_doc = cipher.decrypt(this._net, cipher_doc); 887 this.account = AccountDetails(account_doc); 888 } 889 890 unittest { 891 import std.format; 892 import std.range : iota; 893 import std.stdio; 894 import tagion.hibon.HiBONJSON; 895 896 const good_pin_code = "1234"; 897 898 // Create a new Wallet 899 enum { 900 num_of_questions = 5, 901 confidence = 3 902 } 903 const dummey_questions = num_of_questions.iota.map!(i => format("What %s", i)).array; 904 const dummey_amswers = num_of_questions.iota.map!(i => format("A %s", i)).array; 905 const wallet_doc = SecureWallet(dummey_questions, 906 dummey_amswers, confidence, good_pin_code).wallet.toDoc; 907 908 const pin_doc = SecureWallet( 909 dummey_questions, 910 dummey_amswers, 911 confidence, 912 good_pin_code).pin.toDoc; 913 914 auto secure_wallet = SecureWallet(wallet_doc, pin_doc); 915 const bad_pin_code = "3434"; 916 { // Login test 917 assert(!secure_wallet.isLoggedin); 918 secure_wallet.login(good_pin_code); 919 assert(secure_wallet.checkPincode(good_pin_code)); 920 assert(secure_wallet.isLoggedin); 921 secure_wallet.logout; 922 assert(secure_wallet.checkPincode(good_pin_code)); 923 assert(!secure_wallet.isLoggedin); 924 secure_wallet.login(bad_pin_code); 925 assert(!secure_wallet.isLoggedin); 926 // Check login pin 927 assert(secure_wallet.checkPincode(good_pin_code)); 928 assert(!secure_wallet.isLoggedin); 929 // Login again 930 assert(secure_wallet.login(good_pin_code)); 931 assert(secure_wallet.isLoggedin); 932 } 933 934 const pin_code_2 = "4217"; 935 { // Key Recover faild 936 auto test_answers = dummey_amswers.dup; 937 test_answers[0] = "Bad answer 0"; 938 test_answers[3] = "Bad answer 1"; 939 test_answers[4] = "Bad answer 2"; 940 941 const result = secure_wallet.recover(dummey_questions, test_answers, pin_code_2); 942 assert(!result); 943 assert(!secure_wallet.isLoggedin); 944 } 945 946 { // Key Recover test 947 auto test_answers = dummey_amswers.dup; 948 test_answers[2] = "Bad answer 0"; 949 test_answers[4] = "Bad answer 1"; 950 951 const result = secure_wallet.recover(dummey_questions, test_answers, pin_code_2); 952 assert(result); 953 assert(secure_wallet.isLoggedin); 954 } 955 956 { // Re-login 957 secure_wallet.logout; 958 assert(secure_wallet.checkPincode(pin_code_2)); 959 assert(!secure_wallet.isLoggedin); 960 secure_wallet.login(pin_code_2); 961 assert(secure_wallet.isLoggedin); 962 } 963 964 const new_pincode = "7851"; 965 { // Fail to change pin-code 966 const result = secure_wallet.changePincode(new_pincode, pin_code_2); 967 assert(!result); 968 assert(secure_wallet.isLoggedin); 969 } 970 971 { // Change pincode 972 const result = secure_wallet.changePincode(pin_code_2, new_pincode); 973 assert(result); 974 assert(!secure_wallet.isLoggedin); 975 secure_wallet.login(new_pincode); 976 assert(secure_wallet.isLoggedin); 977 } 978 979 } 980 981 unittest { // Test for account 982 import std.range : zip; 983 import tagion.utils.Miscellaneous; 984 985 auto sender_wallet = SecureWallet(DevicePIN.init, RecoverGenerator.init); 986 auto _net = new Net; 987 988 { // Add SecureNet to the wallet 989 immutable very_securet = "Very Secret password"; 990 _net.generateKeyPair(very_securet); 991 sender_wallet._net = _net; 992 } 993 994 { // Create a number of bills in the seneder_wallet 995 auto bill_amounts = [4, 1, 100, 40, 956, 42, 354, 7, 102355].map!(a => a.TGN); 996 const uint epoch = 42; 997 998 const label = "some_name"; 999 auto list_of_invoices = bill_amounts.map!(a => createInvoice(label, a)) 1000 .each!(invoice => sender_wallet.registerInvoice(invoice))(); 1001 1002 // Add the bulls to the account with the derive keys 1003 with (sender_wallet.account) { 1004 bills = zip(bill_amounts, derivers.byKey).map!(bill_derive => TagionBill( 1005 bill_derive[0], 1006 currentTime, 1007 bill_derive[1], 1008 Buffer.init)).array; 1009 } 1010 1011 assert(sender_wallet.available_balance == bill_amounts.sum); 1012 assert(sender_wallet.total_balance == bill_amounts.sum); 1013 assert(sender_wallet.locked_balance == 0.TGN); 1014 } 1015 1016 auto receiver_wallet = SecureWallet(DevicePIN.init, RecoverGenerator.init); 1017 { // Add securety to the receiver_wallet 1018 auto receiver_net = new Net; 1019 immutable very_securet = "Very Secret password for the receriver"; 1020 receiver_net.generateKeyPair(very_securet); 1021 receiver_wallet._net = receiver_net; 1022 } 1023 1024 pragma(msg, "fixme(cbr): The following test is not finished, Need to transfer to money to receiver"); 1025 SignedContract contract_1; 1026 { // The receiver_wallet creates an invoice to the sender_wallet 1027 auto invoice = SecureWallet.createInvoice("To sender 1", 13.TGN); 1028 receiver_wallet.registerInvoice(invoice); 1029 TagionCurrency fees; 1030 // Give the invoice to the sender_wallet and create payment 1031 TagionCurrency expected_fee; 1032 sender_wallet.getFee([invoice], expected_fee); 1033 1034 sender_wallet.payment([invoice], contract_1, fees); 1035 pragma(msg, "fixme: fix snavs for this to work"); 1036 version (none) { 1037 assert(expected_fee == fees, "fee for get fee and expected fee should be the same"); 1038 } 1039 } 1040 1041 SignedContract contract_2; 1042 { // The receiver_wallet creates an invoice to the sender_wallet 1043 auto invoice = SecureWallet.createInvoice("To sender 2", 53.TGN); 1044 receiver_wallet.registerInvoice(invoice); 1045 TagionCurrency fees; 1046 // Give the invoice to the sender_wallet and create payment 1047 sender_wallet.payment([invoice], contract_2, fees); 1048 } 1049 } 1050 } 1051 1052 version (unittest) { 1053 import std.exception; 1054 import tagion.crypto.SecureNet; 1055 1056 // import std.stdio; 1057 import tagion.script.execute; 1058 1059 alias StdSecureWallet = SecureWallet!StdSecureNet; 1060 1061 } 1062 1063 unittest { 1064 auto wallet = StdSecureWallet("secret", "1234"); 1065 const bill1 = wallet.requestBill(1000.TGN); 1066 wallet.addBill(bill1); 1067 assert(wallet.available_balance == 1000.TGN); 1068 const bill_to_pay = wallet.requestBill(1000.TGN); 1069 SignedContract signed_contract; 1070 TagionCurrency fees; 1071 assertThrown(wallet.createPayment([bill_to_pay], signed_contract, fees).get); 1072 1073 const smaller_bill = wallet.requestBill(999.TGN); 1074 1075 SignedContract signed_contract1; 1076 TagionCurrency fees1; 1077 assertThrown(wallet.createPayment([bill_to_pay], signed_contract1, fees1).get); 1078 } 1079 1080 unittest { 1081 import std.range; 1082 1083 //pay invoice to yourself with multiple bills as outputs 1084 auto wallet = StdSecureWallet("secret", "1234"); 1085 const bill1 = wallet.requestBill(1000.TGN); 1086 wallet.addBill(bill1); 1087 1088 auto invoice_to_pay = wallet.createInvoice("wowo", 10.TGN); 1089 auto invoice_to_pay2 = wallet.createInvoice("wowo2", 10.TGN); 1090 wallet.registerInvoice(invoice_to_pay); 1091 wallet.registerInvoice(invoice_to_pay2); 1092 1093 SignedContract signed_contract; 1094 TagionCurrency fees; 1095 1096 TagionBill[] bills = wallet.invoices_to_bills([invoice_to_pay, invoice_to_pay2]); 1097 wallet.createPayment(bills, signed_contract, fees); 1098 HiRPC hirpc = HiRPC(null); 1099 1100 assert(wallet.account.activated.byValue.filter!(b => b == true).walkLength == 1, "should have one locked bill"); 1101 assert(wallet.locked_balance == 1000.TGN); 1102 const req = wallet.getRequestUpdateWallet; 1103 const receiver = hirpc.receive(req.toDoc); 1104 1105 const number_of_bills = receiver.method.params[].array.length; 1106 assert(number_of_bills == 4, format("should contain three public keys had %s", number_of_bills)); 1107 1108 // create the response containing the two output bills without the original locked bill. 1109 HiBON params = new HiBON; 1110 1111 import std.stdio; 1112 import tagion.hibon.HiBONJSON; 1113 1114 TagionBill[] bills_in_dart = bills ~ wallet.account.requested.byValue.array; 1115 foreach (i, bill; bills_in_dart) { 1116 params[i] = bill.toHiBON; 1117 } 1118 auto dart_response = hirpc.result(receiver, Document(params)).toDoc; 1119 const received = hirpc.receive(dart_response); 1120 1121 wallet.setResponseUpdateWallet(received); 1122 1123 auto should_have = wallet.calcTotal(bills_in_dart); 1124 assert(should_have == wallet.total_balance, format("should have %s had %s", should_have, wallet.total_balance)); 1125 1126 } 1127 1128 unittest { 1129 import std.range; 1130 1131 // pay invoice to yourself. 1132 auto wallet = StdSecureWallet("secret", "1234"); 1133 const bill1 = wallet.requestBill(1000.TGN); 1134 wallet.addBill(bill1); 1135 1136 auto invoice_to_pay = wallet.createInvoice("wowo", 10.TGN); 1137 wallet.registerInvoice(invoice_to_pay); 1138 1139 SignedContract signed_contract; 1140 TagionCurrency fees; 1141 1142 TagionBill[] bills = wallet.invoices_to_bills([invoice_to_pay]); 1143 wallet.createPayment(bills, signed_contract, fees); 1144 HiRPC hirpc = HiRPC(null); 1145 1146 assert(wallet.account.activated.byValue.filter!(b => b == true).walkLength == 1, "should have one locked bill"); 1147 assert(wallet.locked_balance == 1000.TGN); 1148 const req = wallet.getRequestUpdateWallet; 1149 const receiver = hirpc.receive(req.toDoc); 1150 1151 const number_of_bills = receiver.method.params[].array.length; 1152 assert(number_of_bills == 3, format("should contain three public keys had %s", number_of_bills)); 1153 1154 // create the response containing the two output bills without the original locked bill. 1155 HiBON params = new HiBON; 1156 1157 import std.stdio; 1158 import tagion.hibon.HiBONJSON; 1159 1160 TagionBill[] bills_in_dart = bills ~ wallet.account.requested.byValue.array; 1161 foreach (i, bill; bills_in_dart) { 1162 params[i] = bill.toHiBON; 1163 } 1164 auto dart_response = hirpc.result(receiver, Document(params)).toDoc; 1165 const received = hirpc.receive(dart_response); 1166 1167 wallet.setResponseUpdateWallet(received); 1168 1169 auto should_have = wallet.calcTotal(bills_in_dart); 1170 assert(should_have == wallet.total_balance, format("should have %s had %s", should_have, wallet.total_balance)); 1171 1172 } 1173 1174 unittest { 1175 // check get fee greater than user amount 1176 1177 auto wallet1 = StdSecureWallet("some words", "1234"); 1178 const bill1 = wallet1.requestBill(1000.TGN); 1179 wallet1.addBill(bill1); 1180 1181 TagionCurrency fees; 1182 const res = wallet1.getFee(10_000.TGN, fees); 1183 1184 // should fail 1185 assert(res.value == false); 1186 } 1187 1188 unittest { 1189 1190 import std.algorithm; 1191 import std.exception; 1192 import std.stdio; 1193 import tagion.hibon.HiBONJSON; 1194 1195 auto wallet1 = StdSecureWallet("some words", "1234"); 1196 auto wallet2 = StdSecureWallet("some words2", "4321"); 1197 const bill1 = wallet1.requestBill(1000.TGN); 1198 const bill2 = wallet1.requestBill(2000.TGN); 1199 1200 wallet1.addBill(bill1); 1201 wallet1.addBill(bill2); 1202 assert(wallet1.available_balance == 3000.TGN); 1203 1204 auto payment_request = wallet2.requestBill(1500.TGN); 1205 auto too_big_request = wallet2.requestBill(10000.TGN); 1206 1207 TagionCurrency expected_fee; 1208 assert(!wallet1.getFee([too_big_request], expected_fee).value, "should throw on too big value"); 1209 assert(!wallet1.getFee(10000.TGN, expected_fee).value, "should throw on too big value"); 1210 assert(wallet1.getFee(100.TGN, expected_fee).value, "should be able to pay amount"); 1211 1212 assert(wallet1.getFee([payment_request], expected_fee).value, "error in getFee"); 1213 assert(wallet1.available_balance == 3000.TGN, "getfee should not change any balances"); 1214 1215 SignedContract signed_contract; 1216 TagionCurrency fee; 1217 assert(wallet1.createPayment([payment_request], signed_contract, fee).value, "error creating payment"); 1218 1219 assert(fee == expected_fee, format("fees not the same %s, %s", fee, expected_fee)); 1220 1221 assert(signed_contract.contract.inputs.uniq.array.length == signed_contract.contract.inputs.length, "signed contract inputs invalid"); 1222 } 1223 1224 unittest { 1225 1226 import std.algorithm; 1227 import std.exception; 1228 import std.stdio; 1229 import tagion.hibon.HiBONJSON; 1230 1231 auto wallet1 = StdSecureWallet("some words", "1234"); 1232 auto wallet2 = StdSecureWallet("some words2", "4321"); 1233 const bill1 = wallet1.requestBill(1000.TGN); 1234 const bill2 = wallet1.requestBill(2000.TGN); 1235 1236 wallet1.addBill(bill1); 1237 wallet1.addBill(bill2); 1238 assert(wallet1.available_balance == 3000.TGN); 1239 1240 // create a payment request that is the same size as one bill that the wallet has 1241 auto payment_request = wallet2.requestBill(1000.TGN); 1242 1243 SignedContract signed_contract; 1244 TagionCurrency fee; 1245 auto p = wallet1.createPayment([payment_request], signed_contract, fee); 1246 assert(p.value, format("ERROR: %s %s", p.value, p.msg)); 1247 1248 assert(signed_contract.contract.inputs.uniq.array.length == signed_contract.contract.inputs.length, 1249 "signed contract inputs invalid"); 1250 } 1251 1252 unittest { 1253 auto wallet1 = StdSecureWallet("some words", "1234"); 1254 const bill1 = wallet1.requestBill(1000.TGN); 1255 const bill2 = wallet1.requestBill(2000.TGN); 1256 const bill3 = wallet1.requestBill(3000.TGN); 1257 assert(wallet1.account.requested.length == 3); 1258 1259 assert(wallet1.account.bills.length == 0); 1260 wallet1.account.add_bill(bill1); 1261 assert(wallet1.account.bills.length == 1); 1262 assert(wallet1.total_balance == 1000.TGN); 1263 assert(wallet1.available_balance == 1000.TGN); 1264 assert(wallet1.locked_balance == 0.TGN); 1265 1266 { 1267 TagionBill[] locked_bills; 1268 const can_collect = wallet1.collect_bills(1200.TGN, locked_bills); 1269 assert(!can_collect); 1270 assert(locked_bills.length == 0); 1271 } 1272 1273 { 1274 TagionBill[] locked_bills; 1275 const can_collect = wallet1.collect_bills(500.TGN, locked_bills); 1276 assert(can_collect); 1277 assert(locked_bills.length == 1); 1278 const nets = wallet1.collectNets(locked_bills); 1279 1280 assert(nets.length == nets.length); 1281 assert(nets.all!(net => net !is net.init)); 1282 } 1283 1284 auto wallet2 = StdSecureWallet("some other words", "4321"); 1285 const w2_bill1 = wallet2.requestBill(1500.TGN); 1286 { /// faild not enouch money 1287 SignedContract signed_contract; 1288 TagionCurrency fees; 1289 const result = wallet1.createPayment([w2_bill1], signed_contract, fees); 1290 assert(!result); 1291 } 1292 wallet1.account.add_bill(bill2); 1293 assert(wallet1.available_balance == 3000.TGN); 1294 1295 pragma(msg, "fixme: remove snavs_byte_fee for this to work"); 1296 version (none) { /// succces payment 1297 1298 import std.stdio; 1299 1300 SignedContract signed_contract; 1301 TagionCurrency fees; 1302 const bills_to_pay = [w2_bill1]; 1303 writefln("PAY ==="); 1304 const can_pay = wallet1.createPayment(bills_to_pay, signed_contract, fees); 1305 1306 const pay_script = PayScript(signed_contract.contract.script); 1307 writefln("----- -----"); 1308 1309 auto input_docs = [bill1, bill2].map!(bill => bill.toDoc); 1310 auto output_docs = pay_script.outputs.map!(bill => bill.toDoc); 1311 1312 const expected_fees = ContractExecution.billFees( 1313 input_docs, 1314 output_docs, 1315 0 1316 ); 1317 1318 writefln("%s %s", bills_to_pay.map!(bill => bill.toDoc.full_size), pay_script.outputs.map!(bill => bill.toDoc 1319 .full_size)); 1320 writefln("fees=%s expected_fees=%s", fees, expected_fees); 1321 assert(fees == expected_fees); 1322 assert(wallet1.total_balance == 3000.TGN); 1323 assert(wallet1.locked_balance == 3000.TGN); 1324 assert(wallet1.available_balance == 0.TGN); 1325 1326 } 1327 1328 } 1329 1330 // check that the public key is deterministic 1331 unittest { 1332 const words = "long second damp volcano laptop friend noble citizen hip cake safe gown"; 1333 const pin = "1234"; 1334 1335 auto wallet1 = StdSecureWallet(words, pin); 1336 auto wallet2 = StdSecureWallet(words, pin); 1337 assert(wallet1.getPublicKey == wallet2.getPublicKey, "should have generated the same publickey"); 1338 1339 auto wallet3 = StdSecureWallet("Some other words", pin); 1340 assert(wallet1.getPublicKey != wallet3.getPublicKey); 1341 1342 auto wallet4 = StdSecureWallet(words, "5432"); 1343 assert(wallet1.getPublicKey == wallet4.getPublicKey, "should have generated the same publickey"); 1344 } 1345 1346 // check pubkey is the same after login/logout 1347 unittest { 1348 import std.stdio; 1349 1350 const words = "long second damp volcano laptop friend noble citizen hip cake safe gown"; 1351 const pin = "1234"; 1352 1353 auto wallet1 = StdSecureWallet(words, pin); 1354 auto pkey_before = wallet1.getPublicKey.idup; 1355 wallet1.logout; 1356 auto pindup = pin.dup; 1357 assert(wallet1.login(pindup)); 1358 auto pkey_after = wallet1.getPublicKey; 1359 assert(pkey_before == pkey_after, "public key not the same after login/logout"); 1360 } 1361 1362 // fee amount 1363 unittest { 1364 auto wallet1 = StdSecureWallet("some words", "1234"); 1365 const bill1 = wallet1.requestBill(1000.TGN); 1366 const bill2 = wallet1.requestBill(1000.TGN); 1367 const bill3 = wallet1.requestBill(1000.TGN); 1368 assert(wallet1.account.requested.length == 3); 1369 assert(wallet1.account.bills.length == 0); 1370 wallet1.account.add_bill(bill1); 1371 wallet1.account.add_bill(bill2); 1372 wallet1.account.add_bill(bill3); 1373 assert(wallet1.account.bills.length == 3); 1374 1375 const to_pay = [TagionBill(2000.TGN, sdt_t.init, Pubkey([1, 2, 3, 4]), Buffer.init)]; 1376 const to_pay2 = [TagionBill(1999.TGN, sdt_t.init, Pubkey([1, 2, 3, 4]), Buffer.init)]; 1377 1378 TagionCurrency fees; 1379 const res = wallet1.getFee(to_pay, fees); 1380 check(res.value == true, "Wallet should be able to pay 2000 TGN"); 1381 const res2 = wallet1.getFee(to_pay2, fees, true); 1382 check(res2.value == true, format("Wallet should be able to pay 1999 TGN fee: %s", fees)); 1383 1384 SignedContract signed_contract; 1385 1386 const can_pay = wallet1.createPayment(to_pay2, signed_contract, fees); 1387 check(can_pay.value == true, "should be able to create payment"); 1388 } 1389 1390 unittest { 1391 auto wallet1 = StdSecureWallet("some words", "1234"); 1392 foreach (i; 0 .. 20) { 1393 const bill = wallet1.requestBill(1000.TGN); 1394 wallet1.account.add_bill(bill); 1395 } 1396 // we pay 10000 should produce negative fee 1397 const to_pay = [TagionBill(10_000.TGN, sdt_t.init, Pubkey([1, 2, 3, 4]), Buffer.init)]; 1398 1399 TagionCurrency get_fees; 1400 const res = wallet1.getFee(to_pay, get_fees); 1401 check(res.value == true, "should be able to pay 10000 tgn"); 1402 TagionCurrency actual_fees; 1403 1404 SignedContract signed_contract; 1405 const can_pay = wallet1.createPayment(to_pay, signed_contract, actual_fees, true); 1406 check(can_pay.value == true, "should be able to create payment"); 1407 check(get_fees == actual_fees, "the fee should be the same"); 1408 check(actual_fees < 0, "should be a negatvie fee due to the contract being positive"); 1409 } 1410 1411 // amount test 1412 unittest { 1413 import std.stdio; 1414 1415 auto wallet1 = StdSecureWallet("some words", "1234"); 1416 const bill1 = wallet1.requestBill(10000.TGN); 1417 const bill2 = wallet1.requestBill(10000.TGN); 1418 const bill3 = wallet1.requestBill(10000.TGN); 1419 assert(wallet1.account.requested.length == 3); 1420 assert(wallet1.account.bills.length == 0); 1421 wallet1.account.add_bill(bill1); 1422 wallet1.account.add_bill(bill2); 1423 wallet1.account.add_bill(bill3); 1424 assert(wallet1.account.bills.length == 3); 1425 1426 const to_pay = [TagionBill(19949.TGN, sdt_t.init, Pubkey([1, 2, 3, 4]), Buffer.init)]; 1427 const to_pay2 = [TagionBill(30000.TGN, sdt_t.init, Pubkey([1, 2, 3, 4]), Buffer.init)]; 1428 1429 TagionCurrency fees; 1430 const res = wallet1.getFee(to_pay, fees, true); 1431 check(res.value == true, format("Wallet should be able to pay Amount: %s", res.msg)); 1432 const res2 = wallet1.getFee(to_pay2, fees, true); 1433 check(res2.value == false, format("Wallet should not be able to pay Amount")); 1434 1435 SignedContract signed_contract; 1436 const can_pay = wallet1.createPayment(to_pay, signed_contract, fees, true); 1437 check(can_pay.value == true, format("got error: %s", res.msg)); 1438 } 1439 1440 //get fee from amount 1441 unittest { 1442 import tagion.script.execute; 1443 1444 auto wallet1 = StdSecureWallet("some words", "1234"); 1445 const bill1 = wallet1.requestBill(1400.TGN); 1446 const to_pay = wallet1.requestBill(500.TGN); 1447 wallet1.account.add_bill(bill1); 1448 1449 TagionCurrency fee_amount; 1450 TagionCurrency fee_bill_amount; 1451 1452 const res = wallet1.getFee(500.TGN, fee_amount); 1453 const res1 = wallet1.getFee([to_pay], fee_bill_amount); 1454 assert(res.value); 1455 assert(res1.value); 1456 assert(fee_amount == fee_bill_amount, format("not the same bill_fee=%s, amount_fee=%s", fee_bill_amount, fee_amount)); 1457 // create the payment 1458 SignedContract contract; 1459 TagionCurrency actual_fee; 1460 1461 const payment = wallet1.createPayment([to_pay], contract, actual_fee); 1462 assert(payment.value); 1463 1464 // const pay_script = PayScript(contract.contract.script); 1465 const calc_fees = ContractExecution.billFees( 1466 [bill1.toDoc], 1467 [to_pay.toDoc], 1468 wallet1.snavs_byte_fee, 1469 ); 1470 assert(actual_fee == calc_fees, format("fees not the same actualFee=%s, calculatedFee=%s", actual_fee, calc_fees)); 1471 1472 assert(fee_amount == actual_fee, format("fees not the same getFee=%s, actualFee=%s", fee_amount, actual_fee)); 1473 }