Compare commits
654 Commits
refactoring
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a321edd97 | |||
| 1deddd1f04 | |||
| f52cdb51bc | |||
| 3ce00d7e36 | |||
| 0a2e572bf2 | |||
| cc37b88f54 | |||
| 9fc28d6a67 | |||
| 973e1290dc | |||
| 6764fa2a68 | |||
| 8c317938e4 | |||
| e8f6fd38f8 | |||
| dbfb4b5d19 | |||
| ca1daa7123 | |||
| 220b52c161 | |||
| 90e9d3830a | |||
| 0375e7e9ad | |||
| 6f36b4ee9c | |||
| 4274eca8c2 | |||
| fb1e0bd195 | |||
| 6703834e74 | |||
| ce8f5033e5 | |||
| 6455e76c43 | |||
| c933334550 | |||
| d2efea75fc | |||
| 8b8e03255a | |||
| e38fe4661e | |||
| 0078d6e0d7 | |||
| aa28d6ef42 | |||
| c6bb0d3645 | |||
| 0df769f86b | |||
| 274a2c5261 | |||
| c2a64000b4 | |||
| aca9045648 | |||
| b53098d3eb | |||
| 49c4137975 | |||
| 2bc0c83328 | |||
| f86c434d1f | |||
| b98ca6d496 | |||
| 2ec2fee225 | |||
| 9b280ce58c | |||
| 6f9f336bab | |||
| 9f29129168 | |||
| e862226d7f | |||
| 2a43809fac | |||
| 6e71b86fd0 | |||
| 056387a79e | |||
| 5488f23461 | |||
| 26aa66a52e | |||
| 6df69c750b | |||
| edeaf1f91a | |||
| d428f84985 | |||
| c8f503268e | |||
| 93133dc17e | |||
| 43b81ebf3b | |||
| 76640eb4da | |||
| b5e870050f | |||
| af5516f201 | |||
| 09aa04ff79 | |||
| a92549347c | |||
| 80887fe6c0 | |||
| c9fd8f012a | |||
| 5a3b3f7f8e | |||
| 636ab46f5b | |||
| d5a2b69f20 | |||
| ac9a088829 | |||
| 9c1c2ad2aa | |||
| b2fbc0b83f | |||
| 7518417578 | |||
| 741861c8f3 | |||
| 90b936cd47 | |||
| 9a4e02d2f3 | |||
| 64a2313ba9 | |||
| bc313d7c1a | |||
| 60e7c139bc | |||
| 877c4e2673 | |||
| 894f5d5dcc | |||
| eac2be21fd | |||
| e795ec521c | |||
| ba8d09ee52 | |||
| 86cd216417 | |||
| a68977db6f | |||
| afccab2bd9 | |||
| 6930978c91 | |||
| 6e512a3ff2 | |||
| 788b48f217 | |||
| dfe5b13c68 | |||
| 8f402d4e15 | |||
| dc81f97030 | |||
| b06d9bc5a1 | |||
| 9b34d78319 | |||
| 9eb99f52ac | |||
| 15f3f89189 | |||
| ca423995df | |||
| 5f66477a81 | |||
| 913e5c79a9 | |||
| 9d39001efd | |||
| 0f6afd874a | |||
| df2a5d73e5 | |||
| 42904783c3 | |||
| 7f4129ac72 | |||
| 74efc886cc | |||
| a3cbaf6441 | |||
| 119d53a3f0 | |||
| b6b4ebc1a8 | |||
| f685698b87 | |||
| e8597d65da | |||
| 03903f906e | |||
| f4cd49636b | |||
| 7f85320305 | |||
| 3f66b4bcf2 | |||
| 0dfda056f7 | |||
| 515d69b821 | |||
| bf6f5a761c | |||
| 5e5d9c3faf | |||
| ca84952391 | |||
| 42ac87962e | |||
| e980c64fcd | |||
| bfa6e3ac26 | |||
| 3f8a9ccf54 | |||
| edcc3274da | |||
| 81ad8361d8 | |||
| 6ef84a8afb | |||
| 3f584acbd0 | |||
| b9d5305ef6 | |||
| 418f4f373c | |||
| 6e7509e3cf | |||
| dfea0fd2bb | |||
| 2ffc46c0ec | |||
| b48079d295 | |||
| cc0fdf6fe9 | |||
| ab5d2e3bbc | |||
| 21d35c1f11 | |||
| 8df85ecec4 | |||
| 0015980718 | |||
| 13bed53acf | |||
| 258de53fd3 | |||
| ed78569d7a | |||
| f8751ba13c | |||
| dfa5757bff | |||
| cbf1f05258 | |||
| f16f4bb8a5 | |||
| 37ccc8883c | |||
| 3b034daa7c | |||
| 6f341a403e | |||
| 4670cd12c4 | |||
| 4bd0e932f2 | |||
| 0e76a70430 | |||
| 9bbd8b102e | |||
| 645ce066e4 | |||
| 8b28dadf18 | |||
| 74551cebde | |||
| 23aac206bf | |||
| 9a8161b864 | |||
| c24adc4ece | |||
| c5720accc8 | |||
| 4c62dcad48 | |||
| c681978462 | |||
| 72c4f0b4c6 | |||
| b8633b3f54 | |||
| 6a9bc68faa | |||
| 2e1158f790 | |||
| 77f79a91f0 | |||
| 8df87cd766 | |||
| 25c30715c6 | |||
| be52cc354f | |||
| d7a88aa5f1 | |||
| 1583762844 | |||
| 43155fe846 | |||
| 8be55c7ede | |||
| bd0a776129 | |||
| 3aa7409ac2 | |||
| 56e24b4a78 | |||
| 43762fafa6 | |||
| 73cf64e420 | |||
| b93191b335 | |||
| 2d1ce6b8fd | |||
| a48e179ebc | |||
| 86952acdaa | |||
| c436bc254f | |||
| 3136bee08f | |||
| 63597b81f3 | |||
| 4e6246e269 | |||
| 85e63aae6f | |||
| 497e9c4b72 | |||
| 962585298d | |||
| 08c01dedd3 | |||
| b455e5a154 | |||
| cb7efbe3b3 | |||
| 42ea52bcf3 | |||
| 3b117a010b | |||
| 0b33ba74de | |||
| ccd40b275c | |||
| 6cf8826e0d | |||
| d358c85c08 | |||
| 520e9ae9d3 | |||
| 27755cce87 | |||
| 675ff396a5 | |||
| 0b5853e069 | |||
| 221ec59015 | |||
| a4373d5ec5 | |||
| 75430ad4fd | |||
| cbf9fdd3c5 | |||
| 2ea9701a50 | |||
| 6cd670a9c2 | |||
| 1efc74a3f9 | |||
| 7c09b215da | |||
| 0e251a0a7d | |||
| 5a3c10db48 | |||
| e8b2b4dca3 | |||
| ba913ade2a | |||
| 23d37d4a88 | |||
| 328b941581 | |||
| 60dbf3c641 | |||
| e0318d5999 | |||
| 0846342d14 | |||
| eed4af91ef | |||
| e5c18ed350 | |||
| 4d22d97b36 | |||
| ce2de4f36c | |||
| 7c0b98c3ce | |||
| 0532086cb7 | |||
| 3521bb43b1 | |||
| e9a43c9595 | |||
| 9b9c1d9527 | |||
| fa176eaf9d | |||
| 0e11479be6 | |||
| 723abd03c6 | |||
| a34e0f32d1 | |||
| bcd58591ec | |||
| 98fa0aa835 | |||
| 029044058d | |||
| 9ecda74db9 | |||
| a985c7f524 | |||
| 5204f69be5 | |||
| ef66ee2473 | |||
| 95e9d937da | |||
| f2a9d3ab97 | |||
| f118c3db53 | |||
| 5a31865355 | |||
| b8d24912a0 | |||
| 80fbb82695 | |||
| fb55b3b5ba | |||
| 296bacdf9a | |||
| b77b486ab9 | |||
| ccb332c7b6 | |||
| 87d399b6cf | |||
| 711d3f474a | |||
| 437e21b841 | |||
| 822f39d103 | |||
| 8715aa5e0e | |||
| 00677ab22c | |||
| fdc037ad03 | |||
| 9db17e992c | |||
| 57f58be5aa | |||
| 18852a91a1 | |||
| 2ac95d779e | |||
| 36fbf8e9f2 | |||
| a88ff0b35a | |||
| 27076b15cd | |||
| e8646781b2 | |||
| e104b84db5 | |||
| 5f4d969618 | |||
| fe11ccf7d1 | |||
| 036f185194 | |||
| be2c6c1961 | |||
| 9abb34a4b4 | |||
| 2f47853a72 | |||
| 5bce821cf0 | |||
| 3885d5099c | |||
| c317b22a93 | |||
| 2b6277d3c6 | |||
| b502d2acd2 | |||
| 55ab759b6d | |||
| ada74ee74d | |||
| e00d8feb4c | |||
| 3480df6e5e | |||
| daf706eb36 | |||
| aae94ddc49 | |||
| 967ec6f9d5 | |||
| 2a2bef0946 | |||
| 3c082b5a97 | |||
| 6a9a488503 | |||
| c35b01614c | |||
| b61c219095 | |||
| dbc7c4c0f7 | |||
| 9f26d225c3 | |||
| f3bf66d565 | |||
| 1eff29e3b5 | |||
| 351e1f7997 | |||
| 671eb28e4f | |||
| ab8702c265 | |||
| 5fca5e7bab | |||
| 6b2444bfb4 | |||
| 5201889c76 | |||
| 2c4c5e858b | |||
| f474652e47 | |||
| 0fbc8833a4 | |||
| e56bab499f | |||
| fe531fb6c5 | |||
| d9cc0d43c6 | |||
| cf3872690b | |||
| b02caf33f2 | |||
| eb10cd8481 | |||
| 2d3c32709f | |||
| f4bd507f08 | |||
| 3cde524869 | |||
| c16d3bdd26 | |||
| e25df20907 | |||
| e27c464216 | |||
| a675ecfff2 | |||
| 70b04a31d8 | |||
| 0620258f4f | |||
| fc465325e6 | |||
| da3bcecc94 | |||
| 2a6b269316 | |||
| 8f2458b71c | |||
| 2fac50e903 | |||
| 2d7181942f | |||
| 539484bf59 | |||
| 0b8685b9f9 | |||
| 99efa4cbe5 | |||
| 3cc383b101 | |||
| 37dc41ed18 | |||
| 99701611f5 | |||
| 4d49b7235b | |||
| d922c68639 | |||
| 1031e2b344 | |||
| 6d06f8d928 | |||
| 14fb9a7fd5 | |||
| 5d5804357e | |||
| e925d8045a | |||
| 4dc3315f4e | |||
| 26718f1619 | |||
| 6588a26a27 | |||
| 3bde096265 | |||
| 8d2c261e3b | |||
| 34b695ed2d | |||
| 812456d2a9 | |||
| 541b97179e | |||
| bd5c0f3add | |||
| 38cd5a9e19 | |||
| 10c65f0bc6 | |||
| 5070a64f40 | |||
| 6b7737895a | |||
| ec04220aa0 | |||
| 5d10b7b1f3 | |||
| bdf31e10f2 | |||
| 5345b2884b | |||
| 28703c85c2 | |||
| f417c23ffb | |||
| 00f8b69799 | |||
| be6b3cd276 | |||
| 1d2532b138 | |||
| ad18dcb1a1 | |||
| 58de513bc1 | |||
| 0e30d0da79 | |||
| 307e486e1f | |||
| b35b352abe | |||
| ca504cb08b | |||
| e4a7f8aa0c | |||
| d782addf9f | |||
| b03c63bd9f | |||
| 85052c55f1 | |||
| 464f1cc182 | |||
| cd9be1dbec | |||
| 2f4c2edbd3 | |||
| c932f29f2a | |||
| 5cf6f2190e | |||
| d4fa122f5d | |||
| cc428dcfcc | |||
| e3a6643eb0 | |||
| 1b7a254106 | |||
| 5a43fb1b79 | |||
| 963cf15149 | |||
| 5743654658 | |||
| 8a072e714d | |||
| a132647daa | |||
| dc50893c99 | |||
| 0ee67de6aa | |||
| 57a1d29dfc | |||
| 6b536d86da | |||
| 4c50c2e30e | |||
| b393626df9 | |||
| cdf7aa27f6 | |||
| 411eb9510f | |||
| 55a36f48b7 | |||
| 00a8cfe1e5 | |||
| 7fea607a40 | |||
| 001c85357e | |||
| 2db21f4632 | |||
| 2c94a109c9 | |||
| dadb0ab265 | |||
| 3ef732f1f4 | |||
| 4e22b4dcbf | |||
| dc64504e60 | |||
| 98ea0ff49c | |||
| bfcafdee43 | |||
| e4e899d4d6 | |||
| 4b1062cc49 | |||
| a98e25177d | |||
| b14f23c52a | |||
| c20ad486c6 | |||
| 76a73b1841 | |||
| 56f64b2226 | |||
| 333211f4d6 | |||
| 41a2ca49d8 | |||
| 432d814d88 | |||
| 6628cf639f | |||
| f47a81fa89 | |||
| aeefedeafd | |||
| 6211ffd317 | |||
| 76c283263e | |||
| 399c9225a8 | |||
| d9e970e0b8 | |||
| 42d672c84b | |||
| fae1e8241c | |||
| 85c1285ea4 | |||
| f6706142a9 | |||
| 2d19546787 | |||
| 23b9e05807 | |||
| ac605de375 | |||
| e5b03b38de | |||
| be92d693e0 | |||
| 253a974f4a | |||
| 34864ebb7b | |||
| 6fd7bf4b13 | |||
| e6b096f6e6 | |||
| 0fcd756add | |||
| ebd339af25 | |||
| a3b5fed569 | |||
| 973065a9ee | |||
| ca0a0cb494 | |||
| 6d5ce05b98 | |||
| ee989e4aaa | |||
| 7c1e70da34 | |||
| 6b87a8fb91 | |||
| e03b2bfb73 | |||
| 12659f02ba | |||
| 8af4141ec4 | |||
| 0ef27447e9 | |||
| 47a3c5aac0 | |||
| 5468e093be | |||
| 83feaac990 | |||
| ea7b5f5fbd | |||
| 7d96161bd7 | |||
| 09944289f3 | |||
| 5129fad5ae | |||
| e1f10f4044 | |||
| 29b5f0ad80 | |||
| 6ec3a8ae1d | |||
| bf9d0ec9bc | |||
| 8f1314c87a | |||
| df450b50b4 | |||
| 18de060a8f | |||
| 7939ab13cc | |||
| 5ef3124fa3 | |||
| f0bdcbd69c | |||
| 5a5477b852 | |||
| 885dbe3067 | |||
| b73e135aa2 | |||
| 66aa113dd4 | |||
| 9dfa80bfb3 | |||
| fe6656c3ed | |||
| c8b1372f6c | |||
| c748281e5e | |||
| 13063262cb | |||
| 47e79bc85c | |||
| a721bf8416 | |||
| 4feaba4305 | |||
| 21a73e8da3 | |||
| e90cf92749 | |||
| a6dbebf835 | |||
| c830ffcf57 | |||
| e8764b76e2 | |||
| bb91cdd763 | |||
| b272f32c55 | |||
| 7c7cd6d0ee | |||
| 63f317dd70 | |||
| ec51ecaab5 | |||
| 38a6d36c91 | |||
| 4a25a43792 | |||
| d03bd5353a | |||
| 34f67d4c0f | |||
| 414c5bcb15 | |||
| e083211591 | |||
| 0aea752053 | |||
| 58b40ee976 | |||
| 0af7900e7b | |||
| 8f05b7ca62 | |||
| 13a5d74cae | |||
| cd3e0fa724 | |||
| aecf4c2547 | |||
| 89950e3024 | |||
| 0ac42cd017 | |||
| 9181a8dfcb | |||
| 70540cd7a2 | |||
| b2a41d5472 | |||
| 1672372e19 | |||
| 98598b6af1 | |||
| 8ee24471de | |||
| 52a0ee9d01 | |||
| 2305b9cab2 | |||
| 5b0260f004 | |||
| 83033ed052 | |||
| 9b54501b19 | |||
| c2afd848bb | |||
| 23bd0102a0 | |||
| 940f64eaec | |||
| c2783aff4c | |||
| 08321a2e3d | |||
| b34585a487 | |||
| 3a3db75331 | |||
| 32d667d55a | |||
| bba1824a08 | |||
| 1b8fefdf5a | |||
| 85176b43f7 | |||
| 6c45ec02f8 | |||
| 01c2aacc13 | |||
| 0676bcf49c | |||
| 8f0568d2c6 | |||
| aa3d10bd6b | |||
| 73eb93c14a | |||
| 2b01f7f731 | |||
| 0a74026258 | |||
| a030c4d871 | |||
| 19b064ae08 | |||
| f62e58dc66 | |||
| 3a69245723 | |||
| 911bfe19e9 | |||
| 05e545d8f4 | |||
| ff4677166d | |||
| d97d742997 | |||
| 361b1cc036 | |||
| a7be38c96e | |||
| 507f7fdf77 | |||
| 01b82ef107 | |||
| 6177c88ff7 | |||
| a722dfb167 | |||
| 71927e48f5 | |||
| cdb5128eaf | |||
| afddc4d85c | |||
| 6cdad7b683 | |||
| 02bd64b558 | |||
| 47beb2abcc | |||
| fe941babc8 | |||
| 031fe5bf12 | |||
| 638c3a072a | |||
| 4cb2ed9ac1 | |||
| db419691eb | |||
| 8310a9ff01 | |||
| 5846017b26 | |||
| 5f96a45770 | |||
| d39caa511d | |||
| e1a1fa6307 | |||
| 19b95bc1d9 | |||
| d85d7ae03d | |||
| 4ddd0b47f3 | |||
| c0e6946740 | |||
| b9bf6bcb8c | |||
| dcf25e6106 | |||
| edfb491f3d | |||
| 5c09962dbd | |||
| e2ef60e5fa | |||
| 8feec718b1 | |||
| 3435a05910 | |||
| 13af51a1c1 | |||
| e31978d529 | |||
| da7af84c8f | |||
| ebc519cbaf | |||
| 0aa9cfae03 | |||
| 623a9df19d | |||
| 82348e8f06 | |||
| bf7b2997b4 | |||
| 023793926c | |||
| ef96484dbb | |||
| 914c7a2c1f | |||
| 6e5f0aacd1 | |||
| bb588d8e71 | |||
| ca2a6888f7 | |||
| 624abcf187 | |||
| 14e5797425 | |||
| d07e38b571 | |||
| e064f72e53 | |||
| 2e358c49cc | |||
| 4d62bd64f8 | |||
| 4c1df2c6ab | |||
| 518fe9194c | |||
| 7db887675e | |||
| 6da443dbdc | |||
| a4c3f85e9d | |||
| fde17328ff | |||
| 7eec4ed44b | |||
| de2f4d8e8d | |||
| 5b88e2c763 | |||
| feb95933f0 | |||
| c4632cd65b | |||
| 05bcdfadfc | |||
| 9586acb576 | |||
| 8a8cece7bc | |||
| 8b2cb5ea53 | |||
| 7f6df3c484 | |||
| 85da277713 | |||
| 68f466c6c0 | |||
| d071af0536 | |||
| c81e365ab6 | |||
| 83f59087cc | |||
| 713409aa54 | |||
| edfa8e16b0 | |||
| 753cb4ac60 | |||
| e14baf9589 | |||
| a08b9e8eec | |||
| b6758613b0 | |||
| 97c68b96bf | |||
| b504642824 | |||
| 5d7ebab22c | |||
| a51048aaa6 | |||
| 1cc76eb17b | |||
| 89e7eb651f | |||
| a78c3a0734 | |||
| 84a3388d7c | |||
| 40e0d840d2 | |||
| a1fc40f5dd | |||
| 9aebbdc611 | |||
| cac0c6bccd | |||
| 4383d39d60 | |||
| 86a75f9b31 | |||
| 2c90971314 | |||
| 52fcd915bc | |||
| 327d0393ad | |||
| c713149e06 | |||
| a96e49181e | |||
| eed8f9c719 | |||
| cb24ac2b29 | |||
| 3dcee588b1 | |||
| 15ca62f7eb | |||
| 7da567d728 | |||
| 94f657c1e0 | |||
| ba6952712a | |||
| 235fadcc4c | |||
| 98f4b316fb | |||
| 75007f8c29 | |||
| 43c1d95a6b | |||
| 523ceff41e | |||
| 29086ab27a | |||
| 436f284a8d | |||
| 4f9328cb9e | |||
| fc9e3f18f2 | |||
| d239e1dbf0 | |||
| 4bf39173be | |||
| 87f68e0ce5 | |||
| 73e1fad281 | |||
| 0a69e0cbc9 | |||
| 4ea440f084 | |||
| 3f5c20dc18 |
@@ -93,3 +93,9 @@ iOSInjectionProject/
|
||||
Info.plist
|
||||
/PayCash.entitlements
|
||||
/PayCash.xcodeproj
|
||||
/ResultIPA/
|
||||
*.pbxproj
|
||||
*.xcscheme
|
||||
*.xcodeproj
|
||||
*.entitlements
|
||||
/iOS/Assets/Settings.bundle
|
||||
|
||||
+34
-18
@@ -1,5 +1,6 @@
|
||||
# Execute always
|
||||
before_script:
|
||||
- echo $CI_PIPELINE_IID
|
||||
- echo $GITLAB_USER_ID
|
||||
- echo ${CI_COMMIT_REF_SLUG}
|
||||
- id
|
||||
@@ -9,13 +10,12 @@ variables:
|
||||
|
||||
stages:
|
||||
- lint
|
||||
# - test
|
||||
- test
|
||||
- build
|
||||
- deploy
|
||||
- notification
|
||||
|
||||
# Linting stage
|
||||
|
||||
.linting: &linting
|
||||
tags:
|
||||
- malinka
|
||||
@@ -34,6 +34,25 @@ Lint:
|
||||
- /^feature/
|
||||
- merge_requests
|
||||
|
||||
# Testing stage
|
||||
.testing: &testing
|
||||
tags:
|
||||
- malinka
|
||||
stage: test
|
||||
when: always
|
||||
allow_failure: false
|
||||
script:
|
||||
- chmod +x ./toolchain/testing.sh
|
||||
- ./toolchain/testing.sh
|
||||
|
||||
Test:
|
||||
<<: *testing
|
||||
only:
|
||||
- develop
|
||||
- /^bugfix/
|
||||
- /^feature/
|
||||
- merge_requests
|
||||
|
||||
# Build IPA
|
||||
.BuildIPA: &BuildIPA
|
||||
tags:
|
||||
@@ -44,23 +63,20 @@ Lint:
|
||||
script:
|
||||
- rm -rf ./ResultIPA
|
||||
- chmod +x ./toolchain/build_iOS.sh
|
||||
# - ./toolchain/build_iOS.sh PRODUCTION
|
||||
- ./toolchain/build_iOS.sh BETA
|
||||
- ./toolchain/build_iOS.sh ${APPLICATION_DEPLOY_TYPE} ${APPLICATION_NAME} ${APPLICATION_SCHEME_NAME}
|
||||
|
||||
# Build stage
|
||||
|
||||
build:
|
||||
stage: build
|
||||
<<: *BuildIPA
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
only:
|
||||
- tags
|
||||
- merge_requests
|
||||
- develop
|
||||
artifacts:
|
||||
paths:
|
||||
- ./ResultIPA
|
||||
expire_in: 3 days
|
||||
expire_in: 2 days
|
||||
|
||||
buildLeaf:
|
||||
stage: build
|
||||
@@ -85,7 +101,7 @@ buildLeaf:
|
||||
IPA_PATH: ./ResultIPA
|
||||
script:
|
||||
- chmod +x ./toolchain/deploy_iOS.sh
|
||||
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH}
|
||||
- ./toolchain/deploy_iOS.sh ${IPA_DEPLOY_TARGET} ${IPA_PATH} ${APPLICATION_SCHEME_NAME}
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
@@ -95,9 +111,8 @@ deploy:
|
||||
<<: *DeployIPA
|
||||
when: on_success
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
- develop
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
@@ -122,28 +137,29 @@ deployLeaf:
|
||||
script:
|
||||
- chmod +x ./toolchain/slack_notification.sh
|
||||
- ./toolchain/slack_notification.sh ${BUILD_RESULT}
|
||||
needs:
|
||||
- job: deploy
|
||||
|
||||
# Develop
|
||||
FailureNotification:
|
||||
variables:
|
||||
BUILD_RESULT: "FAILURE"
|
||||
when: on_failure
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
- develop
|
||||
<<: *slacknotification
|
||||
needs:
|
||||
- job: deploy
|
||||
|
||||
SuccessNotification:
|
||||
variables:
|
||||
BUILD_RESULT: "SUCCESS"
|
||||
when: on_success
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
- tags
|
||||
- develop
|
||||
<<: *slacknotification
|
||||
needs:
|
||||
- job: deploy
|
||||
|
||||
# Merge checks
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletFoundation",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletFoundation",
|
||||
targets: ["WalletFoundation"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletFoundation",
|
||||
dependencies: ["KeyChainAccess"],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletFoundationTests",
|
||||
dependencies: ["WalletFoundation"],
|
||||
path: "Tests"//, // Test files
|
||||
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// ApplicationSettings.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
fileprivate enum ApplicationSettingsKeys {
|
||||
|
||||
// TODO: - Need remove after 1.4.0 version
|
||||
static let ignoreDeviceSatusKeyOld = "Settings.device_ignore"
|
||||
static let ignoreCreateAccountSatusKeyOld = "Status.createAccount_ignore"
|
||||
static let isNetworkLogEnabledKeyOld = "Settings.network_copy"
|
||||
static let isDeviceTokenCopyEnabledKeyOld = "Settings.device_token_copy_enable"
|
||||
static let apiEnvironmentKeyOld = "Settings.api_environment"
|
||||
static let apiPaycashEnvironmentKeyOld = "Settings.api_paycash_environment"
|
||||
static let deviceDescriptionKeyOld = "Settings.device_id"
|
||||
static let deviceTokenKeyOld = "Settings.device_token"
|
||||
|
||||
/// Ignore device status on register flow
|
||||
static let ignoreDeviceSatusKey = "Settings.application.device_ignore"
|
||||
/// Ignore create account status on register flow
|
||||
static let ignoreCreateAccountSatusKey = "Status.application.createAccount_ignore"
|
||||
/// log all network activity
|
||||
static let isNetworkLogEnabledKey = "Settings.application.network_copy"
|
||||
/// Copy device token to Settings fields
|
||||
static let isDeviceTokenCopyEnabledKey = "Settings.application.device_token_copy_enable"
|
||||
/// Field in Settings for device description
|
||||
static let deviceDescriptionKey = "Settings.application.device_id"
|
||||
/// Field in Settings for device token
|
||||
static let deviceTokenKey = "Settings.application.device_token"
|
||||
|
||||
static let apiUsernameEnvironmentKey = "Settings.application.environment.usernames"
|
||||
static let apiBackendEnvironmentKey = "Settings.application.environment.backend"
|
||||
static let otherEnvironmentKey = "Settings.application.environment.other"
|
||||
static let smartEnvironmentKey = "Settings.application.environment.smart"
|
||||
/// Field in Settings for Firebase token lifetime
|
||||
static let firebaseTokenLifetimeKey = "Settings.application.firebase.token.lifetime"
|
||||
}
|
||||
|
||||
public enum ApplicationSettings {
|
||||
|
||||
public static var ignoreDeviceSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey) }
|
||||
|
||||
public static var ignoreCreateAccountSatus: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey) }
|
||||
|
||||
public static var isNetworkLogEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey) }
|
||||
|
||||
public static var isDeviceTokenCopyEnabled: Bool { UserDefaults.standard.bool(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey) }
|
||||
|
||||
public static func device(description: String) { UserDefaults.standard.set(description, forKey: ApplicationSettingsKeys.deviceDescriptionKey) }
|
||||
|
||||
public static func device(token: String) { UserDefaults.standard.set(token, forKey: ApplicationSettingsKeys.deviceTokenKey) }
|
||||
|
||||
public static var apiUsernameEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey) }
|
||||
|
||||
public static var apiBackendEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey) }
|
||||
|
||||
public static var otherEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.otherEnvironmentKey) }
|
||||
|
||||
public static var smartsEnvironment: String? { UserDefaults.standard.string(forKey: ApplicationSettingsKeys.smartEnvironmentKey) }
|
||||
|
||||
public static var firebaseTokenLifetime: Int? { UserDefaults.standard.integer(forKey: ApplicationSettingsKeys.firebaseTokenLifetimeKey) }
|
||||
|
||||
public static func clearApiEnvironment() {
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiUsernameEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiBackendEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.otherEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.smartEnvironmentKey)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.firebaseTokenLifetimeKey)
|
||||
|
||||
// TODO: - Need remove after 1.4.0 version
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreDeviceSatusKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.ignoreCreateAccountSatusKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isNetworkLogEnabledKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.isDeviceTokenCopyEnabledKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiEnvironmentKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.apiPaycashEnvironmentKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceDescriptionKeyOld)
|
||||
UserDefaults.standard.removeObject(forKey: ApplicationSettingsKeys.deviceTokenKeyOld)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// CommonKey.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CommonKey: RawRepresentable, Hashable {
|
||||
|
||||
public var rawValue: String
|
||||
|
||||
public init?(rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public init(_ rawValue: String) {
|
||||
self.rawValue = rawValue
|
||||
}
|
||||
|
||||
public func with(_ suffix: String) -> Self { .init(rawValue + "." + suffix) }
|
||||
|
||||
public static func key(_ key: String) -> Self { .init(key) }
|
||||
public static func key(_ key: String, suffix: String) -> Self { Self.key(key).with(suffix) }
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// KeychainProtocol.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 16.02.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public typealias Key = CommonKey
|
||||
|
||||
public protocol KeychainProtocol {
|
||||
|
||||
/// Checks if a given common key have a password-saved value
|
||||
/// - Parameter key: Common Key
|
||||
/// - Returns: is value exists
|
||||
func exist(_ key: Key) -> Bool
|
||||
/// Checks if a given common key have a biometric-saved value
|
||||
/// - Parameter key: Common Key
|
||||
/// - Returns: is value exists
|
||||
func bioExist(_ key: Key) -> Bool
|
||||
/// Access to common key value by biometric authenfication
|
||||
subscript(biometric key: Key) -> String? { get }
|
||||
/// Access to common key value by password authenfication
|
||||
subscript(_ key: Key, password password: String) -> String? { get set }
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// WalletKeychain.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/27/22.
|
||||
//
|
||||
|
||||
import LocalAuthentication
|
||||
|
||||
final public class WalletKeychain: KeychainProtocol {
|
||||
|
||||
private enum KeychainLocals {
|
||||
public static let password = "PWD"
|
||||
public static let biometric = "BIO"
|
||||
}
|
||||
|
||||
public static let instance = WalletKeychain()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init() { }
|
||||
|
||||
// MARK: - Interface
|
||||
|
||||
public func exist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(KeychainLocals.password)) }
|
||||
|
||||
public func bioExist(_ key: Key) -> Bool { self.checkProtectedExist(key: key.with(KeychainLocals.biometric) ) }
|
||||
|
||||
public subscript(biometric key: Key) -> String? {
|
||||
self.loadBiometricProtected(key: key.with(KeychainLocals.biometric))
|
||||
.map({ String(data: $0, encoding: .utf8) }) ?? nil
|
||||
}
|
||||
|
||||
public subscript(_ key: Key, password password: String) -> String? {
|
||||
get { loadPassProtected(key: key.with(KeychainLocals.password), password: password).map { String(data: $0, encoding: .utf8) } ?? nil }
|
||||
set { update(key, password: password, newValue: newValue) }
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func getPwdSecAccessControl() -> SecAccessControl {
|
||||
var access: SecAccessControl?
|
||||
var error: Unmanaged<CFError>?
|
||||
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .applicationPassword, &error)
|
||||
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
|
||||
return access! // swiftlint:disable:this force_unwrapping
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func setPassProtected(key: Key, data: String, password: String) -> Bool {
|
||||
|
||||
let context = LAContext()
|
||||
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
|
||||
|
||||
let query = [
|
||||
kSecClass as String: kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecAttrAccessControl as String: getPwdSecAccessControl(),
|
||||
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
|
||||
kSecUseAuthenticationContext: context
|
||||
] as CFDictionary
|
||||
|
||||
let status: OSStatus = SecItemAdd(query, nil)
|
||||
if status == errSecSuccess {
|
||||
return true
|
||||
} else if status == errSecDuplicateItem {
|
||||
if removeProtected(key: key) {
|
||||
return setPassProtected(key: key, data: data, password: password)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func loadPassProtected(key: Key, password: String) -> Data? {
|
||||
let context = LAContext()
|
||||
context.setCredential(password.data(using: .utf8), type: .applicationPassword)
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecReturnData as String: kCFBooleanTrue!,
|
||||
kSecAttrAccessControl as String: getPwdSecAccessControl(),
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseAuthenticationContext as String: context,
|
||||
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
|
||||
]
|
||||
|
||||
var dataTypeRef: AnyObject?
|
||||
let result = SecItemCopyMatching(query as CFDictionary, &dataTypeRef)
|
||||
if result == noErr,
|
||||
let value = dataTypeRef as? Data {
|
||||
return value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MARK: - Biometric entries
|
||||
|
||||
private func getBiometricSecAccessControl() -> SecAccessControl {
|
||||
var access: SecAccessControl?
|
||||
var error: Unmanaged<CFError>?
|
||||
access = SecAccessControlCreateWithFlags(nil, kSecAttrAccessibleWhenUnlockedThisDeviceOnly, .biometryCurrentSet, &error)
|
||||
precondition(access != nil, "SecAccessControlCreateWithFlags failed")
|
||||
return access! // swiftlint:disable:this force_unwrapping
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func setBiometricEntry(key: Key, data: String) -> Bool {
|
||||
|
||||
let query = [
|
||||
kSecClass as String: kSecClassGenericPassword as String,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecAttrAccessControl as String: getBiometricSecAccessControl(),
|
||||
kSecValueData as String: (data.data(using: .utf8) ?? Data()) as NSData,
|
||||
] as CFDictionary
|
||||
|
||||
let status: OSStatus = SecItemAdd(query, nil)
|
||||
|
||||
if status == errSecSuccess {
|
||||
return true
|
||||
} else if status == errSecDuplicateItem {
|
||||
if removeProtected(key: key) {
|
||||
return setBiometricEntry(key: key, data: data)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func loadBiometricProtected(key: Key) -> Data? {
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecReturnData as String: kCFBooleanTrue as Any,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseOperationPrompt as String: "Access your data"
|
||||
]
|
||||
|
||||
var dataTypeRef: AnyObject?
|
||||
return SecItemCopyMatching(query as CFDictionary, &dataTypeRef) == noErr ? dataTypeRef as? Data : nil
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
private func update(_ key: Key, password: String, newValue: String?) {
|
||||
//TODO: Refactor later - updation of biometric entry only when it is available and needed
|
||||
if let value = newValue {
|
||||
setPassProtected(key: key.with(KeychainLocals.password), data: value, password: password)
|
||||
setBiometricEntry(key: key.with(KeychainLocals.biometric), data: value)
|
||||
} else {
|
||||
removeProtected(key: key.with(KeychainLocals.password))
|
||||
removeProtected(key: key.with(KeychainLocals.biometric))
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
private func removeProtected(key: Key) -> Bool {
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue
|
||||
]
|
||||
|
||||
return SecItemDelete(query as CFDictionary) == noErr
|
||||
}
|
||||
|
||||
private func checkProtectedExist(key: Key) -> Bool {
|
||||
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: key.rawValue,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
kSecUseAuthenticationUI as String: kSecUseAuthenticationUIFail
|
||||
]
|
||||
|
||||
let status = SecItemCopyMatching(query as CFDictionary, nil)
|
||||
switch status {
|
||||
case errSecSuccess, errSecInteractionNotAllowed: return true
|
||||
case errSecItemNotFound: return false
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//
|
||||
// Settings.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/29/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final public class Settings {
|
||||
|
||||
public static let shared = Settings()
|
||||
|
||||
private init() {}
|
||||
|
||||
public func contains(_ key: CommonKey) -> Bool { defaults.value(forKey: key.rawValue) != nil }
|
||||
|
||||
public subscript<T>(_ key: CommonKey) -> T? {
|
||||
get { defaults.value(forKey: key.rawValue) as? T }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
public subscript<T: Codable>(_ key: CommonKey) -> [T]? {
|
||||
get {
|
||||
let decoder = JSONDecoder()
|
||||
guard let data = defaults.value(forKey: key.rawValue) as? Data,
|
||||
let value = try? decoder.decode([T].self, from: data) else { return nil }
|
||||
return value
|
||||
}
|
||||
set {
|
||||
let encoder = JSONEncoder()
|
||||
guard let value = newValue,
|
||||
let data = try? encoder.encode(value) else { return }
|
||||
set(value: data, for: key)
|
||||
}
|
||||
}
|
||||
|
||||
public subscript(_ key: CommonKey) -> Bool {
|
||||
get { defaults.bool(forKey: key.rawValue) }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
public subscript(_ key: CommonKey) -> Int {
|
||||
get { (defaults.value(forKey: key.rawValue) as? Int) ?? 0 }
|
||||
set { set(value: newValue, for: key) }
|
||||
}
|
||||
|
||||
private func set(value: Any?, for key: CommonKey) {
|
||||
defaults.setValue(value, forKey: key.rawValue)
|
||||
defaults.synchronize()
|
||||
}
|
||||
|
||||
private var defaults: UserDefaults { UserDefaults.standard }
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// DeviceToken.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.Tech on 02.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import DeviceCheck
|
||||
import UIKit
|
||||
|
||||
public enum DeviceCheckTokenError: Error {
|
||||
case notSupported
|
||||
case generation
|
||||
}
|
||||
|
||||
public struct DeviceCheckToken {
|
||||
|
||||
public let token: String
|
||||
public let isSimulator: Bool
|
||||
|
||||
public static func generate() async -> Result<DeviceCheckToken, DeviceCheckTokenError> {
|
||||
|
||||
let token: String
|
||||
let isSimulator: Bool
|
||||
#if targetEnvironment(simulator)
|
||||
|
||||
isSimulator = true
|
||||
token = tokenConstant
|
||||
|
||||
#else
|
||||
|
||||
let device = DCDevice.current
|
||||
guard device.isSupported else { return .failure(.notSupported) }
|
||||
guard let data = try? await device.generateToken() else { return .failure(.generation) }
|
||||
|
||||
isSimulator = false
|
||||
token = data.base64EncodedString()
|
||||
|
||||
#endif
|
||||
|
||||
if ApplicationSettings.isDeviceTokenCopyEnabled {
|
||||
ApplicationSettings.device(token: token)
|
||||
}
|
||||
|
||||
return .success(DeviceCheckToken(token: token, isSimulator: isSimulator))
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
fileprivate let tokenConstant = """
|
||||
AgAAAIArgRs5gVfPAEHSra0ejd0EUNk0+me89vLfv5ZingpyOOkgXXXyjPzYTzWmWSu+BYqcD47byirLZ++3dJccpF99hWppT7G5xAuU+y56WpSYsATWySfuxbSSMT9JSoOWz4QtiDmmVmUHbzCfHbTP3Tr3hsG+86KBBaoSqHtDRy+dtKlV32kDRbuuniBy3nseZsZoCggAAKTXDFKWHDZ55Ya1Cp+8s+dGOyi4C08v0P/8rbQHcjjkOphpLUqKaBZCykAaf5Ue1c1ul57OKeoyaDy9ShXGvwIKIcZrvZBBds3wwEFQuZBNPTG1ZvpIZ3npXscHWaKRd228V/bboEKarukYi3+lsxafZj+R8laD0Ex3nP3WZaIU4990oerdiu6wbdELlaNyrVF6SRJcZQhhflfnnyXHMqw6c41Hk3toMHsUd9wzdzcYmZB1PI5rsgSfnWrxAbBy5rYpH+ZkGkhcHFJqQIO4TB8ZKU0KSjmS8mBnVDT/1VPgiNDz/qI+KWiZ1xcuEwwvaEmD+Fk6Pt9GMRsfxI+SvldTgk8REqr9dBXt69xwM2FHBHP9k4okkaMbsU1qpZPKCwsnTBrkbvLn0zVn2+tfTukLx0O+uCPRMluXn3EXDrQ0aRHFbUtgoMqOx0JP+7tj/BaOAKkc3C2GaCRfEes2YmqZkM4pMve4Qh1jAsIwYtGcDz7AeGQ61kVrc4CFj3xj2OgC+IbBi7naOZqNvr1Be1YPKt8Vpig9YF5GueY9p4DRGlOG6UX2RY4wsZTt38Lxw5uBBkWuINjRyqsiN5obPZ3xLagfzfNsFDBYgBsGrib8nURVfSgAqIgrozOl+cppRB7xoN9QRti+HAJqKNd6sqpsPkXzqnDWPBD2Jdg2WCJE2bjJhTqyJ6L3lHFguOdIPc2P6F1CGx4bn1GtegOLlFxexOjMzfU86gJOhYjkVGHt8GD96ohRl75/fsv1reCI16pWt2x8p+Bbh4kko8wP3FtXiun+i6gPDMBhE30Ye5ATsIUIHFZjHOA8UfntaEyCSAngQebQ0X4UERcue+GKY4RqVfPhuVqJa3RHt35Ci/2g7VWllNs1NKYsPofAgTNO2n/kwGrnnIL+gQPNsO6XjnqDOjjT/eDfZX88eK4k6+8+MnW3+l8IAZOIh5JT++JZvBadrNZPV7G/2ME6G1FCIAZ4icCidbMzpj4sGc8dlJOg/B3457/Vt8CHLulhajQIsXQuqGDotgzirELTVQ+Z2eh3a8W/Pu+g8iNIUL1MCEzHg7RefM6tUescNDH6uPEuEeXKmrsoVbHdUvhuLIVbQGHMCjuGbmxmczcfIcRAPWkjwuVQOKBpoaD/Zep2gM71inpz6056bmvUuTMM9MV6sM87Fqrv6TvIRT/ch/i7Flmv56ERn/NGryafdcvDIu+JMs5U3yvb3STTSZmbh8RXVGIJjOZ1FJYLREUxxK7eEGM2JLCD+CxR7LuLRuN4AtpI4GIhFrbdVdDkywqNpvEY6aGEOFnD9NP6neBuHRhK/AzqpDE83uFf+1JiPPY7aHYVoQhCxkPs8ex0qJnjHaveKiWfSmaZ6JfY/vVByzJNr5XD6ZSQlJQJ3+xjRb+blTR0XcZ5BHI9ovQQAmGQljWpGPnD5CZQ7ah5kVoK1SbPqtxY6J5zQUTjtTpSe3l6By/nopXH6HSQXJGotOzb+eMOOHFhDC9ypq3urHY+Q1jXB18eR/xkXEIlZQsPBmwCLhoNltD57faLzlqgiwinHjqslntnvfsMkoIpQnWFwLYKh0biW9KM5ZWv7CMIxcc+Wjl1FVnyUrMzhz/IIW6WshcNmZWFSMpaKzozxIyFQZ/IjZHPrE1SI33PwGn/Tro8ZRSf9mpKJbA3uidrygVz6WQlJJwR3ujdgZ9aJ9WZQWVehVYj5mMg5P5MB/BG1G/TQNTLn72Root26hSB+8WMCAo/EEY3L5Qox+JabfDV+kBsHjFtDfyo7ghp/AOvkVzudzzk+F3ruc1bIDJlSfOQ5WRTkjmQGhKDIfDvLIi4Mt8bQTzZ3KPkLaU+hwuFE7m9c/MgzmLWK24oDsZ5nZ4oBeUNc+lThUuz7qnWENE1sIXij3zFljsoH+HOPb+zSt8m7iwszxMZ6T84LvqxqWVGEkI4Il2s41ti5QH74nOzTaci+5dYRVWOnY/hAwI51HE1sb3Pe+NsSHcfgtDW0E4Xx+Wx0uLdqbTlriXRnUIOoi9PVNR6XOdJ4nDOxyiOMMhnUooQ7lRqm0hCxw9nAEw5+PvYWWFXxPaupUfeVsjH+9dXW6bzosCGzTbVpHcDjPWie70r+Nma+oOwA4ARKHmGsbKcoO43xos6sqfbZTCCP9BIPQnZ8XbUen9G7eMs9ESigKoynKKVGNmsBXK4lU8xM/qLXXudViMdSPOZ6mghjJCNK0yA1v9l/ipZRHiTPFOttELH6Ip+fKDtfqdeEqCiPrSnVtWzehUKOUhlNJtexkOZcb2Dtq4L7JlJ0GkJg80vCvEYvArM2JpPqKDVr8hCBNC87u6zk9T3E+L2dfL30aiNVAGTl44Qw0pPerIr1a6m9Jkj690Pi3OI7UAgWaoQjxYm2my7DZMqtkL6CrT0NW9KnihXw701ngJysdKcZ0JkMDT2LzP+2Nj10WIOwkLxASexSQgSoyGk7yTYLUAyvwN1rhRtspXaiyOcfyzDwgTIU9Sn/jMbC6fv7GPReCsiFR8Xa6VCj37eFPXgBiOpAYtj/zMz/3S/io3LTqs7QG1M14CX31xSxu21tASOzaRhbd2RB2QCHXgpqv4593psE5EPjbRZt5DN2toQ4XJJ1A1/EcyDkEJ8+1gu34aVrqC6ejm/07/MQ7ISmUuPrJyCaPIW+PbkxF0VpYU5lJ9HP+LD7WggPwi8NVz8zFWNtyTM6aUuTNL69sBHpWlYeqCwpkJ+EcJFuaTnT27N4pFvwA==
|
||||
"""
|
||||
#endif
|
||||
@@ -0,0 +1,346 @@
|
||||
//
|
||||
// DeviceUUID.swift
|
||||
//
|
||||
//
|
||||
// Created Nut.Tech on 02.08.2022.
|
||||
//
|
||||
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
import AppKit
|
||||
#endif
|
||||
import KeyChainAccess
|
||||
|
||||
/// Class allow retrive UUID with different life cycle.
|
||||
final public class DeviceUUID {
|
||||
|
||||
private enum Constants {
|
||||
|
||||
enum Keys {
|
||||
static let installationUUIDKey = "installationUUIDKey"
|
||||
static let deviceUUIDKey = "deviceUUIDKey"
|
||||
static let devicesUUIDsKey = "devicesUUIDsKey"
|
||||
static let devicesUUIDsToggleKey = "devicesUUIDsToggleKey"
|
||||
static let devicesUUIDs = "devicesUUIDs"
|
||||
}
|
||||
|
||||
enum Locals {
|
||||
static let DevicesUUIDsDidChangeNotification = NSNotification.Name(rawValue: "DevicesUUIDsDidChangeNotification")
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared instance for class
|
||||
static public var shared = DeviceUUID()
|
||||
|
||||
|
||||
/// Changes each time the app gets launched (persistent to session).
|
||||
lazy private(set) public var session: String = {
|
||||
self.generateUuid()
|
||||
}()
|
||||
|
||||
/// Changes each time the app gets installed (persistent to installation).
|
||||
lazy private(set) public var installation: String = {
|
||||
self.getValue(forKey: Constants.Keys.installationUUIDKey,
|
||||
defaultValue: nil,
|
||||
keychain: false,
|
||||
synchronizable: false)
|
||||
}()
|
||||
|
||||
/// Changes each time all the apps of the same vendor are uninstalled (this works exactly as identifierForVendor).
|
||||
lazy private(set) public var vendor: String? = {
|
||||
#if os(iOS)
|
||||
UIDevice.current
|
||||
.identifierForVendor?
|
||||
.uuidString
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: "-", with: "")
|
||||
#elseif os(macOS)
|
||||
return nil
|
||||
#endif
|
||||
}()
|
||||
|
||||
/// Changes only on system reset, this is the best replacement to the good old udid (persistent to device)
|
||||
lazy private(set) public var device: String = {
|
||||
self.getValue(forKey: Constants.Keys.deviceUUIDKey,
|
||||
defaultValue: nil,
|
||||
synchronizable: false)
|
||||
}()
|
||||
|
||||
/// List of all uuidForDevice of the same user.
|
||||
/// In this way it's possible manage guest accounts across multiple devices easily
|
||||
lazy private(set) public var devices: [String] = {
|
||||
let devicesString = self.getValue(forKey: Constants.Keys.devicesUUIDsKey,
|
||||
defaultValue: self.device)
|
||||
return devicesString.components(separatedBy: "|")
|
||||
}()
|
||||
|
||||
/// Changes each time (no persistent), but allows to keep in memory more temporary uuids.
|
||||
public func uuid(forKey key: String) -> String {
|
||||
guard let uuid = self.uuids[key] else {
|
||||
let value = self.generateUuid()
|
||||
self.uuids[key] = value
|
||||
return value
|
||||
}
|
||||
return uuid
|
||||
}
|
||||
|
||||
/// Changes each time (no persistent)
|
||||
public func generateUuid() -> String {
|
||||
let uuidRef = CFUUIDCreate(nil)
|
||||
let uuidStringRef = CFUUIDCreateString(nil, uuidRef)
|
||||
|
||||
return ((uuidStringRef as? String) ?? "")
|
||||
.lowercased()
|
||||
.replacingOccurrences(of: "-", with: "")
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private var uuids = [String: String]()
|
||||
private var isCloudAvailable = false
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
init() {
|
||||
self.initCloudUUIDsDevices()
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func getValue(forKey key: String,
|
||||
defaultValue: String? = nil,
|
||||
userDefaults: Bool = true,
|
||||
keychain: Bool = true,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil,
|
||||
synchronizable: Bool = true) -> String {
|
||||
|
||||
if let newValue = Self.getValue(forKey: key,
|
||||
userDefaults:
|
||||
userDefaults,
|
||||
keychain: keychain,
|
||||
service: service,
|
||||
accessGroup: accessGroup) {
|
||||
return newValue
|
||||
} else {
|
||||
let value = defaultValue ?? self.generateUuid()
|
||||
Self.setValue(value,
|
||||
forKey: key,
|
||||
userDefaults: userDefaults,
|
||||
keychain: keychain,
|
||||
service: service,
|
||||
accessGroup: accessGroup,
|
||||
synchronizable: synchronizable)
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
public func uuidForDeviceMigratingValue(forKey key: String,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil,
|
||||
commitMigration: Bool) -> String? {
|
||||
|
||||
if let uuidToMigrate = Self.getValue(forKey: key,
|
||||
service: service,
|
||||
accessGroup: accessGroup) {
|
||||
return self.uuid(forDeviceMigratingValue: uuidToMigrate, commitMigration: commitMigration)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateUUIDDevice(value: String) {
|
||||
self.device = value
|
||||
Self.setValue(value,
|
||||
forKey: Constants.Keys.deviceUUIDKey,
|
||||
synchronizable: false)
|
||||
}
|
||||
|
||||
func uuid(forDeviceMigratingValue value: String, commitMigration: Bool) -> String? {
|
||||
|
||||
if self.isValidUUID(value) {
|
||||
|
||||
let oldValue = self.device
|
||||
let newValue = value
|
||||
|
||||
guard oldValue != newValue else { return oldValue }
|
||||
|
||||
if commitMigration {
|
||||
self.updateUUIDDevice(value: newValue)
|
||||
|
||||
let deviceSet = NSMutableOrderedSet(array: self.devices)
|
||||
deviceSet.add(newValue)
|
||||
deviceSet.remove(oldValue)
|
||||
|
||||
if let uuidsArray = deviceSet.array as? [String] {
|
||||
self.updateUUIDsDevices(with: uuidsArray)
|
||||
}
|
||||
self.syncCloudUUIDsDevices()
|
||||
|
||||
return self.device
|
||||
} else {
|
||||
return oldValue
|
||||
}
|
||||
} else {
|
||||
|
||||
let exception = NSException(name: NSExceptionName(rawValue: "Invalid uuid to migrate"),
|
||||
reason: "uuid value should be a string of 32 or 36 characters.")
|
||||
exception.raise()
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private func initCloudUUIDsDevices() {
|
||||
|
||||
self.isCloudAvailable = false
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else { return }
|
||||
self.isCloudAvailable = true
|
||||
|
||||
NotificationCenter.default.addObserver(self,
|
||||
selector: #selector(self.changesCloudUUIDsDevicesNotification),
|
||||
name: NSUbiquitousKeyValueStore.didChangeExternallyNotification,
|
||||
object: nil)
|
||||
|
||||
self.syncCloudUUIDsDevices()
|
||||
}
|
||||
|
||||
private func syncCloudUUIDsDevices() {
|
||||
if self.isCloudAvailable {
|
||||
let iCloud = NSUbiquitousKeyValueStore.default
|
||||
//if keychain contains more device identifiers than icloud, maybe that icloud has been empty, so re-write these identifiers to iCloud
|
||||
for uuidOfUserDevice in self.devices {
|
||||
let uuidOfUserDeviceAsKey = "\(Constants.Keys.deviceUUIDKey)_\(uuidOfUserDevice)"
|
||||
|
||||
if iCloud.string(forKey: uuidOfUserDeviceAsKey) != uuidOfUserDevice {
|
||||
iCloud.set(uuidOfUserDevice, forKey: uuidOfUserDeviceAsKey)
|
||||
}
|
||||
}
|
||||
|
||||
//toggle a boolean value to force notification on other devices, useful for debug
|
||||
let uuidsOfUserDevicesToggler = !iCloud.bool(forKey: Constants.Keys.devicesUUIDsToggleKey)
|
||||
iCloud.set(uuidsOfUserDevicesToggler, forKey: Constants.Keys.devicesUUIDsToggleKey)
|
||||
iCloud.synchronize()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateUUIDsDevices(with value: [String]) {
|
||||
self.devices = value
|
||||
Self.setValue(value.joined(separator: "|"),
|
||||
forKey: Constants.Keys.devicesUUIDsKey)
|
||||
}
|
||||
|
||||
private func isValidUUID(_ value: String) -> Bool {
|
||||
let pattern = "^[0-9a-f]{32}|[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}$"
|
||||
guard let regExp = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let uuidValueRange = NSRange(location: 0, length: value.count)
|
||||
let matchRange = regExp.rangeOfFirstMatch(in: value, options: [], range: uuidValueRange)
|
||||
var matchValue: String?
|
||||
|
||||
if !NSEqualRanges(matchRange, NSRange(location: NSNotFound, length: 0)) {
|
||||
matchValue = (value as NSString).substring(with: matchRange)
|
||||
return matchValue == value ? true : false
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
private func changesCloudUUIDsDevicesNotification(_ notification: Notification?) {
|
||||
|
||||
if self.isCloudAvailable {
|
||||
let uuidsSet = NSMutableOrderedSet(array: self.devices)
|
||||
let uuidsCount = uuidsSet.count
|
||||
|
||||
let iCloud = NSUbiquitousKeyValueStore.default
|
||||
let iCloudDict = iCloud.dictionaryRepresentation as NSDictionary
|
||||
|
||||
iCloudDict.enumerateKeysAndObjects { key, obj, stop in
|
||||
let uuidKey = key as? NSString
|
||||
if uuidKey?.range(of: Constants.Keys.deviceUUIDKey).location == 0 {
|
||||
if let uuidValue = obj as? String {
|
||||
if uuidKey?.range(of: uuidValue).location != NSNotFound,
|
||||
self.isValidUUID(uuidValue) {
|
||||
uuidsSet.add(uuidValue)
|
||||
} else {
|
||||
print("invalid uuid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if uuidsSet.count > uuidsCount,
|
||||
let uuidsArray = uuidsSet.array as? [String] {
|
||||
self.updateUUIDsDevices(with: uuidsArray)
|
||||
let userInfo = [Constants.Keys.devicesUUIDs: self.devices]
|
||||
NotificationCenter.default.post(name: Constants.Locals.DevicesUUIDsDidChangeNotification,
|
||||
object: self,
|
||||
userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static private func getValue(forKey key: String,
|
||||
userDefaults: Bool = true,
|
||||
keychain: Bool = true,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil) -> String? {
|
||||
|
||||
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup)
|
||||
var value = try? keychainStore.getString(key)
|
||||
|
||||
if userDefaults,
|
||||
!value.isExist {
|
||||
value = UserDefaults.standard.string(forKey: key)
|
||||
}
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
static private func setValue(_ value: String?,
|
||||
forKey key: String,
|
||||
userDefaults: Bool = true,
|
||||
keychain: Bool = true,
|
||||
service: String? = nil,
|
||||
accessGroup: String? = nil,
|
||||
synchronizable: Bool = true) -> Error? {
|
||||
|
||||
if let value = value,
|
||||
userDefaults {
|
||||
UserDefaults.standard.set(value, forKey: key)
|
||||
UserDefaults.standard.synchronize()
|
||||
}
|
||||
|
||||
if let value = value, keychain {
|
||||
let keychainStore = Self.makeKeychain(service: service, accessGroup: accessGroup).synchronizable(synchronizable)
|
||||
do {
|
||||
try keychainStore.set(value, key: key)
|
||||
} catch {
|
||||
return error
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
static private func makeKeychain(service: String? = nil, accessGroup: String? = nil) -> Keychain {
|
||||
|
||||
if let service = service {
|
||||
if let accessGroup = accessGroup {
|
||||
return Keychain(service: service, accessGroup: accessGroup)
|
||||
} else {
|
||||
return Keychain(service: service)
|
||||
}
|
||||
} else if let accessGroup = accessGroup {
|
||||
return Keychain(accessGroup: accessGroup)
|
||||
}
|
||||
|
||||
return Keychain()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// Array+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 9/15/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Array {
|
||||
|
||||
subscript(safe index: Index) -> Element? {
|
||||
return (self.startIndex..<self.endIndex) ~= index ? self[index] : nil
|
||||
}
|
||||
}
|
||||
|
||||
public extension Array where Element: Hashable {
|
||||
|
||||
func distinct() -> Array {
|
||||
let set = Set(self)
|
||||
return Array(set)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// Data+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Data {
|
||||
|
||||
func jsonDecoded<T: Decodable>(type: T.Type, userInfo: [CodingUserInfoKey: Any]? = nil) -> T? {
|
||||
try? self.makeDecoder(userInfo: userInfo).decode(type, from: self)
|
||||
}
|
||||
|
||||
func jsonDecoded<T: Decodable>(type: T.Type, userInfo: [CodingUserInfoKey: Any]? = nil) -> [T]? {
|
||||
try? self.makeDecoder(userInfo: userInfo).decode([T].self, from: self)
|
||||
}
|
||||
|
||||
private func makeDecoder(userInfo: [CodingUserInfoKey: Any]?) -> JSONDecoder {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
userInfo >>- { jsonDecoder.userInfo = $0 }
|
||||
return jsonDecoder
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// Dictionary+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.Tech on 27.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Dictionary where Key == String, Value == Any {
|
||||
|
||||
func jsonSerialized(options: JSONSerialization.WritingOptions = []) -> Data? {
|
||||
try? JSONSerialization.data(withJSONObject: self, options: options)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// Encodable+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/6/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Encodable {
|
||||
|
||||
func jsonData() -> Data? {
|
||||
let encoder = JSONEncoder()
|
||||
return try? encoder.encode(self)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// Optional+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 08.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Optional {
|
||||
|
||||
var isExist: Bool {
|
||||
if case .some = self {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func orCreate(_ creation: @autoclosure () -> Wrapped) -> Wrapped {
|
||||
switch self {
|
||||
case let .some(value): return value
|
||||
case .none: return creation()
|
||||
}
|
||||
}
|
||||
|
||||
func orTypedCreate<Element: RawRepresentable>(_ creation: @autoclosure () -> Element) -> Element where Element.RawValue == Wrapped {
|
||||
switch self {
|
||||
case let .some(value): return Element(rawValue: value) ?? creation()
|
||||
case .none: return creation()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Publisher+Extension.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
public extension Publisher {
|
||||
|
||||
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is optional.
|
||||
/// The first time the upstream publisher emits an element, the previous element will be `nil`.
|
||||
///
|
||||
/// let range = (1...5)
|
||||
/// cancellable = range.publisher
|
||||
/// .withPrevious()
|
||||
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
|
||||
/// // Prints: "(nil, 1) (Optional(1), 2) (Optional(2), 3) (Optional(3), 4) (Optional(4), 5) ".
|
||||
///
|
||||
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
|
||||
func withPrevious() -> AnyPublisher<(previous: Output?, current: Output), Failure> {
|
||||
scan(Optional<(Output?, Output)>.none) { ($0?.1, $1) }
|
||||
.compactMap { $0 }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
/// Includes the current element as well as the previous element from the upstream publisher in a tuple where the previous element is not optional.
|
||||
/// The first time the upstream publisher emits an element, the previous element will be the `initialPreviousValue`.
|
||||
///
|
||||
/// let range = (1...5)
|
||||
/// cancellable = range.publisher
|
||||
/// .withPrevious(0)
|
||||
/// .sink { print ("(\($0.previous), \($0.current))", terminator: " ") }
|
||||
/// // Prints: "(0, 1) (1, 2) (2, 3) (3, 4) (4, 5) ".
|
||||
///
|
||||
/// - Parameter initialPreviousValue: The initial value to use as the "previous" value when the upstream publisher emits for the first time.
|
||||
/// - Returns: A publisher of a tuple of the previous and current elements from the upstream publisher.
|
||||
func withPrevious(_ initialPreviousValue: Output) -> AnyPublisher<(previous: Output, current: Output), Failure> {
|
||||
scan((initialPreviousValue, initialPreviousValue)) { ($0.1, $1) }.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
func trimCompact() -> String {
|
||||
let value = self
|
||||
.replacingOccurrences(of: "\n", with: "")
|
||||
.replacingOccurrences(of: "\r", with: "")
|
||||
|
||||
let regex = try! NSRegularExpression(pattern: "[ ]{2,}", options: .caseInsensitive)
|
||||
return regex.stringByReplacingMatches(in: value,
|
||||
options: [],
|
||||
range: NSRange(0..<value.utf16.count),
|
||||
withTemplate: " ")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Either.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum Either<T: Decodable, U: Decodable>: Decodable {
|
||||
case firstType(T)
|
||||
case secondType(U)
|
||||
|
||||
public func unwrap() -> Any {
|
||||
switch self {
|
||||
case .firstType(let objectOfTypeT): return objectOfTypeT
|
||||
case .secondType(let objectOfTypeU): return objectOfTypeU
|
||||
}
|
||||
}
|
||||
|
||||
public func map<V>(firstTypeTransform: (T) -> V, secondTypeTransform: (U) -> V) -> V {
|
||||
switch self {
|
||||
case .firstType(let value):
|
||||
return firstTypeTransform(value)
|
||||
case .secondType(let value):
|
||||
return secondTypeTransform(value)
|
||||
}
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
if let value = try? T(from: decoder) {
|
||||
self = .firstType(value)
|
||||
} else if let value = try? U(from: decoder) {
|
||||
self = .secondType(value)
|
||||
} else {
|
||||
let context = DecodingError.Context(
|
||||
codingPath: decoder.codingPath,
|
||||
debugDescription:
|
||||
"Cannot decode \(T.self) or \(U.self)")
|
||||
throw DecodingError.dataCorrupted(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Operations.swift
|
||||
// Jura
|
||||
//
|
||||
// Created by Jura on 8/14/19.
|
||||
// Copyright © 2019 Jura. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
precedencegroup MonadicPrecedence {
|
||||
associativity: left
|
||||
higherThan: BitwiseShiftPrecedence
|
||||
}
|
||||
|
||||
infix operator >>- : MonadicPrecedence
|
||||
|
||||
@inline(__always)
|
||||
@discardableResult
|
||||
public func >>-<T, U>(a: T?, f: (T) throws -> U?) rethrows -> U? {
|
||||
switch a {
|
||||
case .some(let x):
|
||||
return try f(x)
|
||||
case .none:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: <<< / >>>
|
||||
|
||||
precedencegroup FunctionApplicationPrecedenceLeft {
|
||||
lowerThan: AssignmentPrecedence
|
||||
associativity: left
|
||||
}
|
||||
|
||||
infix operator >>> : FunctionApplicationPrecedenceLeft
|
||||
|
||||
@inline(__always)
|
||||
public func >>><T, U>(x: T, f: (T) throws -> U) rethrows -> U {
|
||||
return try f(x)
|
||||
}
|
||||
|
||||
precedencegroup FunctionApplicationPrecedenceRight {
|
||||
lowerThan: AssignmentPrecedence
|
||||
associativity: right
|
||||
}
|
||||
|
||||
infix operator <<< : FunctionApplicationPrecedenceRight
|
||||
|
||||
@inline(__always)
|
||||
public func <<<<T, U>(f: (T) throws -> U, x: T) rethrows -> U {
|
||||
return try f(x)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// ArrayTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import WalletFoundation
|
||||
|
||||
final class ArrayTests: XCTestCase {
|
||||
|
||||
func testOuter() {
|
||||
let test = [0, 1, 2]
|
||||
XCTAssertNil(test[safe: 5])
|
||||
XCTAssertNil(test[safe: -1])
|
||||
}
|
||||
|
||||
func testInner() {
|
||||
let test = [1, 3, 5]
|
||||
var value = test[safe: 1]
|
||||
XCTAssertNotNil(value)
|
||||
XCTAssertEqual(value!, 3)
|
||||
|
||||
value = test[safe: 2]
|
||||
XCTAssertNotNil(value)
|
||||
XCTAssertEqual(value!, 5)
|
||||
|
||||
value = test[safe: 0]
|
||||
XCTAssertNotNil(value)
|
||||
XCTAssertEqual(value!, 1)
|
||||
}
|
||||
|
||||
func testDistinct() {
|
||||
let array = [1, 2, 5, 6, 5, 8, 9, 1, 2, 2, 5]
|
||||
let arrayWithUniqueElements = array.distinct()
|
||||
|
||||
XCTAssertEqual(arrayWithUniqueElements.sorted(), [1, 2, 5, 6, 8, 9])
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// OptionalsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11/8/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import WalletFoundation
|
||||
|
||||
final class OptionalsTests: XCTestCase {
|
||||
|
||||
func testIsExists() {
|
||||
|
||||
var value: Int?
|
||||
XCTAssertFalse(value.isExist)
|
||||
|
||||
value = 42
|
||||
XCTAssertTrue(value.isExist)
|
||||
}
|
||||
|
||||
func testOrCreate() {
|
||||
|
||||
var value: String?
|
||||
XCTAssertEqual(value.orCreate("Hello"), "Hello")
|
||||
|
||||
value = "world"
|
||||
XCTAssertEqual(value.orCreate("Hello"), "world")
|
||||
}
|
||||
|
||||
func testOrTypedCreate() {
|
||||
|
||||
enum TestCases: String {
|
||||
case first
|
||||
case second
|
||||
}
|
||||
|
||||
var value: String?
|
||||
// Create new value because nil
|
||||
XCTAssertEqual(value.orTypedCreate(TestCases.first), .first)
|
||||
XCTAssertNotEqual(value.orTypedCreate(TestCases.first), .second)
|
||||
|
||||
value = "Hello"
|
||||
// Create new value because not matched
|
||||
XCTAssertEqual(value.orTypedCreate(TestCases.second), .second)
|
||||
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .first)
|
||||
|
||||
value = "first"
|
||||
// Not create new value and just match
|
||||
XCTAssertEqual(value.orTypedCreate(TestCases.second), .first)
|
||||
XCTAssertNotEqual(value.orTypedCreate(TestCases.second), .second)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletKit",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletKit",
|
||||
targets: ["WalletKit"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
|
||||
.package(name: "KeyChainAccess", path: "../../Vendors/spm/KeyChainAccess"),
|
||||
.package(name: "WalletFoundation", path: "../WalletFoundation"),
|
||||
.package(name: "WalletNetwork", path: "../WalletNetwork"),
|
||||
.package(name: "CryptoSwift", path: "../../Vendors/spm/CryptoSwift-1.5.1")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletKit",
|
||||
dependencies: ["CryptoSwift", "eosswift", "KeyChainAccess", "WalletFoundation", "WalletNetwork"],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletKitTests",
|
||||
dependencies: ["WalletKit"],
|
||||
path: "Tests"//, // Test files
|
||||
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// DeepLink.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct DeepLink {
|
||||
|
||||
public typealias DataParams = [AnyHashable: Any]
|
||||
public typealias InfoParams = [String: Any]
|
||||
|
||||
public let action: DeepLinkAction
|
||||
let data: Data?
|
||||
public let info: [String: Any]?
|
||||
public let url: URL?
|
||||
|
||||
public static func create(params: DataParams?) -> DeepLink? {
|
||||
|
||||
guard let params = params,
|
||||
let rawAction = params["action"] as? String,
|
||||
let action = DeepLinkAction(rawValue: rawAction),
|
||||
let data = (params["params"] as? String)?.data(using: .utf8)
|
||||
?? (try? JSONSerialization.data(withJSONObject: params["params"] as? [String: Any], options: [])) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DeepLink(action: action, data: data, info: nil, url: URL(string: params["r"] as? String ?? ""))
|
||||
}
|
||||
|
||||
public static func create(info: DataParams?) -> DeepLink? {
|
||||
|
||||
guard let info = info as? [String: Any],
|
||||
let rawAction = info["action"] as? String,
|
||||
let action = DeepLinkAction(rawValue: rawAction) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return DeepLink(action: action, data: nil, info: info, url: nil)
|
||||
}
|
||||
|
||||
public func get<T: Decodable>(_ model: T.Type) -> T? {
|
||||
guard let data = self.data else { return nil }
|
||||
return try? JSONDecoder().decode(T.self, from: data)
|
||||
}
|
||||
|
||||
public func dictionary() -> [String: Any] {
|
||||
guard let data = self.data else { return [:] }
|
||||
return (try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]) ?? [:]
|
||||
}
|
||||
}
|
||||
+9
-10
@@ -1,14 +1,13 @@
|
||||
//
|
||||
// CommonModelDeepLink.swift
|
||||
// PayCash
|
||||
// DeepLinkAction.swift
|
||||
//
|
||||
//
|
||||
// Created by Igor on 15.10.2020.
|
||||
// Copyright © 2020 List. All rights reserved.
|
||||
// Created by Juraldinio on 8/9/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum DeepLinkAction: String {
|
||||
public enum DeepLinkAction: String {
|
||||
case connect = "connect_accounts"
|
||||
case chat = "chat"
|
||||
case transfer = "transfer"
|
||||
@@ -17,17 +16,17 @@ enum DeepLinkAction: String {
|
||||
case approveBuy = "approve_buy"
|
||||
case emission = "emission"
|
||||
case chatMessage = "chat.message"
|
||||
case transactionTransfer = "transactions.incoming_transfer"
|
||||
case transactionTransfer = "transaction.incoming_transfer"
|
||||
case transactionInheritance = "transactions.incoming_inheritance"
|
||||
case transactionEmission = "transactions.emission"
|
||||
case p2pDealNew = "orders.new_deal"
|
||||
case p2pDealComplete = "orders.completed_deal"
|
||||
case p2pDealCancel = "orders.cancelled_deal"
|
||||
case p2pDealDispute = "orders.dispute_deal"
|
||||
case cashDealNew = "guarantee_orders.new_deal"
|
||||
case cashDealConfirm = "guarantee_orders.confirmation_deal"
|
||||
case cashDealCancel = "guarantee_orders.cancelled_deal"
|
||||
case cashDealComplete = "guarantee_orders.completed_deal"
|
||||
// case cashDealNew = "guarantee_orders.new_deal"
|
||||
// case cashDealConfirm = "guarantee_orders.confirmation_deal"
|
||||
// case cashDealCancel = "guarantee_orders.cancelled_deal"
|
||||
// case cashDealComplete = "guarantee_orders.completed_deal"
|
||||
case news = "news"
|
||||
case competitivePrice = "competitive_price"
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// Village.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
final public class Village {
|
||||
|
||||
private static var keychainKeeper = PrivacyKeeper(keychain: WalletKeychain.instance)
|
||||
private static var villages = [Village]()
|
||||
|
||||
private let environment: NetworkEnvironment
|
||||
private var houses = [VillageHouse]()
|
||||
|
||||
private var foreigner: Foreigner? {
|
||||
didSet {
|
||||
guard let foreigner = self.foreigner else { return }
|
||||
ApplicationSettings.device(description: "\(foreigner)")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
// MARK: - Public
|
||||
|
||||
/// Create instance of Village.
|
||||
public static func get(with environment: NetworkEnvironment) -> Village {
|
||||
|
||||
if let village = Self.villages.first(where: { $0.environment.isEquals(other: environment) }) {
|
||||
return village
|
||||
}
|
||||
|
||||
let village = Village(environment: environment)
|
||||
Self.villages.append(village)
|
||||
return village
|
||||
}
|
||||
|
||||
/// Get device instance.
|
||||
public func device(force: Bool = false) async throws -> Device {
|
||||
|
||||
if force { self.foreigner = nil }
|
||||
|
||||
if let foreigner = self.foreigner { return foreigner }
|
||||
|
||||
if !force,
|
||||
let foreigner = Foreigner.restore() {
|
||||
do {
|
||||
try await foreigner.retrievStatus(using: self.environment)
|
||||
self.foreigner = foreigner
|
||||
return foreigner
|
||||
} catch { }
|
||||
}
|
||||
|
||||
do {
|
||||
let foreigner = try await Foreigner.create(for: DeviceUUID.shared.device, using: self.environment)
|
||||
foreigner.save()
|
||||
self.foreigner = foreigner
|
||||
return foreigner
|
||||
} catch {
|
||||
throw DeviceError.create
|
||||
}
|
||||
}
|
||||
|
||||
public func attestate(device: Device) async throws -> CertifiedDevice {
|
||||
try await Resident.permit(for: device, using: self.environment)
|
||||
}
|
||||
|
||||
/// Create wallet key.
|
||||
public func createWalletKey() throws -> WalletKey {
|
||||
try WalletKey.create()
|
||||
}
|
||||
|
||||
/// Create wallet case for hold wallet.
|
||||
public func createWalletCase(with name: String, key: WalletKey? = nil) async throws -> WalletCase {
|
||||
|
||||
let walletKey: WalletKey
|
||||
if let key = key {
|
||||
walletKey = key
|
||||
} else {
|
||||
walletKey = try WalletKey.create()
|
||||
}
|
||||
|
||||
return try await VillagerCoat.create(name: name, using: self.environment, key: walletKey)
|
||||
}
|
||||
|
||||
/// Create bank that contain Wallets.
|
||||
public func getBank(with password: String, on device: Device) throws -> Bank {
|
||||
|
||||
guard Self.keychainKeeper.accept(password: password) else {
|
||||
throw WalletError.passwordNotMatch
|
||||
}
|
||||
|
||||
if let bank = self.houses.first(where: { $0.isEquals(device: device, environment: self.environment, password: password) }) {
|
||||
return bank
|
||||
}
|
||||
let house = VillageHouse.create(password: password,
|
||||
keeper: Self.keychainKeeper,
|
||||
on: device,
|
||||
environment: self.environment)
|
||||
self.houses.append(house)
|
||||
|
||||
return house
|
||||
}
|
||||
|
||||
public static func reset() {
|
||||
VillageHouse.removeAllVillagers()
|
||||
Self.keychainKeeper.reset()
|
||||
}
|
||||
|
||||
// Keychain static methods
|
||||
|
||||
public static func accept(password: String) -> Bool {
|
||||
Self.keychainKeeper.accept(password: password)
|
||||
}
|
||||
|
||||
public static func passwordViaBiometrics() -> String? {
|
||||
Self.keychainKeeper.passwordByBiometrics()
|
||||
}
|
||||
|
||||
public static var isPasswordExists: Bool {
|
||||
Self.keychainKeeper.isPasswordExists
|
||||
}
|
||||
|
||||
public static func set(password: String, old: String? = nil) throws {
|
||||
try Self.keychainKeeper.update(password: password, old: old)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// Tractor.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
import KeyChainAccess
|
||||
|
||||
final class Foreigner: Device, Codable {
|
||||
|
||||
private enum Constant {
|
||||
static let saveKey = "village.tractor.instance"
|
||||
static let service = CodingUserInfoKey(rawValue: "service")!
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case uuid
|
||||
case id
|
||||
}
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(uuid: String, id: String, isTrusted: Bool) {
|
||||
self.uuid = uuid
|
||||
self.id = id
|
||||
self.isTrusted = isTrusted
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
self.uuid = try container.decode(String.self, forKey: .uuid)
|
||||
self.id = try container.decode(String.self, forKey: .id)
|
||||
}
|
||||
|
||||
// MARK: - Device
|
||||
|
||||
let uuid: String
|
||||
let id: String
|
||||
private(set) var isTrusted: Bool = false
|
||||
private(set) var availableAccounts: Int?
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String { "uuid: \(self.uuid), id: \(self.id)" }
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
func save() {
|
||||
guard let rawData = try? JSONEncoder().encode(self) else { return }
|
||||
let keychain = Keychain()
|
||||
try? keychain.set(rawData, key: Constant.saveKey)
|
||||
}
|
||||
|
||||
func retrievStatus(using environment: NetworkEnvironment) async throws {
|
||||
let service = DeviceService(environment: environment)
|
||||
do {
|
||||
let status = try await service.stateDevice(id: self.id)
|
||||
self.availableAccounts = status.availableAccounts
|
||||
self.isTrusted = status.isTrusted
|
||||
return
|
||||
} catch {
|
||||
throw DeviceError.status
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(for cid: String, using environment: NetworkEnvironment) async throws -> Foreigner {
|
||||
let service = DeviceService(environment: environment)
|
||||
do {
|
||||
let device = try await service.createDevice(uuid: cid)
|
||||
return Foreigner(uuid: device.uuid, id: device.id, isTrusted: device.isTrusted)
|
||||
} catch {
|
||||
throw DeviceError.create
|
||||
}
|
||||
}
|
||||
|
||||
static func restore() -> Foreigner? {
|
||||
let keychain = Keychain()
|
||||
guard let rawData = try? keychain.getData(Constant.saveKey) else { return nil }
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
return try? decoder.decode(Foreigner.self, from: rawData)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
//
|
||||
// PrivacyKeeper.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 07.02.2023.
|
||||
//
|
||||
|
||||
import WalletFoundation
|
||||
import Foundation
|
||||
import CryptoSwift
|
||||
|
||||
final class PrivacyKeeper {
|
||||
private typealias CipherData = (key: String, iv: String)
|
||||
private enum Constants {
|
||||
static let oldPasswordKey = CommonKey("PrivacyKeeper.password")
|
||||
static let passwordKey = "PrivacyKeeper.password"
|
||||
static let cipherKey = "PrivacyKeeper.ckey"
|
||||
static let ivKey = "PrivacyKeeper.ivkey"
|
||||
}
|
||||
|
||||
var isPasswordExists: Bool {
|
||||
if let encryptedPasswordKey {
|
||||
return self.keychain.exist(encryptedPasswordKey)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private var encryptedPasswordKey: CommonKey?
|
||||
private var cipherData: CipherData
|
||||
private var keychain: KeychainProtocol
|
||||
|
||||
init(keychain: KeychainProtocol) {
|
||||
self.keychain = keychain
|
||||
self.cipherData = Self.initCipherData()
|
||||
self.encryptedPasswordKey = Self.makeEncryptedPasswordCommonKey(cipherData: self.cipherData)
|
||||
}
|
||||
|
||||
/// Cheks if password correct
|
||||
func accept(password: String) -> Bool {
|
||||
self.getPassword(password) == password
|
||||
}
|
||||
|
||||
/// Get password via biometry
|
||||
func passwordByBiometrics() -> String? {
|
||||
guard let encryptedPasswordKey else { return nil }
|
||||
return self.keychain.bioExist(encryptedPasswordKey) ?
|
||||
self.keychain[biometric: encryptedPasswordKey] : nil
|
||||
}
|
||||
|
||||
/// Get private key from keychain
|
||||
/// - Parameters:
|
||||
/// - key: common key in keychain
|
||||
/// - password: current password
|
||||
/// - Returns: private key (if exists)
|
||||
func privateKey(for key: String, password: String?) -> String? {
|
||||
let pwd = password ?? self.passwordByBiometrics()
|
||||
if let pwd {
|
||||
return self.keychain[self.encryptPK(commonKey: key), password: pwd]
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Update password to new one.
|
||||
func update(password: String, old: String? = nil) throws {
|
||||
guard self.isPasswordExists else {
|
||||
try self.setPassword(password)
|
||||
return
|
||||
}
|
||||
guard let old, self.accept(password: old) else {
|
||||
throw WalletError.passwordNotMatch
|
||||
}
|
||||
|
||||
try self.setPassword(password)
|
||||
}
|
||||
|
||||
/// Update private key for current account. If you are using biometrics, pass nil in password.
|
||||
/// - Parameters:
|
||||
/// - privateKey: new private key
|
||||
/// - key: common key in keychain
|
||||
/// - password: current password
|
||||
func update(privateKey: String?, for key: String, password: String?) {
|
||||
let pwd = password ?? self.passwordByBiometrics()
|
||||
guard let pwd, self.accept(password: pwd) else { return }
|
||||
self.keychain[self.encryptPK(commonKey: key), password: pwd] = privateKey
|
||||
}
|
||||
|
||||
/// Migrates pasword and private key storage in keychain to new style
|
||||
/// - Parameters:
|
||||
/// - oldKey: old private key common key for keychain
|
||||
/// - newKey: new private key common key for keychain
|
||||
/// - password: current password
|
||||
func migrate(oldKey: CommonKey, newKey: CommonKey, password: String) {
|
||||
self.migratePassword(password: password)
|
||||
guard self.accept(password: password) else { return }
|
||||
self.migratePrivateKey(oldKey: oldKey, newKey: newKey, password: password)
|
||||
self.migrateToEncrypted(commonKey: newKey, password: password)
|
||||
}
|
||||
|
||||
/// Resets all data by making all old data inaccessible
|
||||
func reset() {
|
||||
self.cipherData = Self.createCipherData()
|
||||
self.encryptedPasswordKey = Self.makeEncryptedPasswordCommonKey(cipherData: self.cipherData)
|
||||
}
|
||||
|
||||
/// Gets password if exists. Pass nil to get biometrics password
|
||||
/// - Parameter password: current password
|
||||
/// - Returns: password string (if exists)
|
||||
private func getPassword(_ password: String? = nil) -> String? {
|
||||
if let password {
|
||||
guard let encryptedPasswordKey else { return nil }
|
||||
return self.keychain[encryptedPasswordKey, password: password]
|
||||
} else {
|
||||
return self.passwordByBiometrics()
|
||||
}
|
||||
}
|
||||
|
||||
/// Set password
|
||||
/// - Parameter password: new password
|
||||
private func setPassword(_ password: String) throws {
|
||||
guard let encryptedPasswordKey else {
|
||||
throw WalletError.passwordKeyNotExist
|
||||
}
|
||||
self.keychain[encryptedPasswordKey, password: password] = password
|
||||
}
|
||||
|
||||
/// Migrate private key in keychain from old to new for old versions
|
||||
/// - Parameters:
|
||||
/// - oldKey: old-style key
|
||||
/// - newKey: new-style key
|
||||
/// - password: current password
|
||||
private func migratePrivateKey(oldKey: CommonKey, newKey: CommonKey, password: String) {
|
||||
guard let privateKey = self.keychain[oldKey, password: password] else { return }
|
||||
self.keychain[oldKey, password: password] = nil
|
||||
self.keychain[newKey, password: password] = privateKey
|
||||
}
|
||||
|
||||
/// Migrate to encrypted keys for keychain
|
||||
/// - Parameters:
|
||||
/// - commonKey: old-style common key, without encryption
|
||||
/// - password: current password
|
||||
private func migrateToEncrypted(commonKey: CommonKey, password: String) {
|
||||
guard let privateKey = self.keychain[commonKey, password: password] else { return }
|
||||
self.keychain[commonKey, password: password] = nil
|
||||
self.keychain[self.encryptPK(commonKey: commonKey.rawValue), password: password] = privateKey
|
||||
}
|
||||
|
||||
/// Migrating from an old keychain password key to a new one
|
||||
/// - Parameter password: current password
|
||||
private func migratePassword(password: String) {
|
||||
guard self.keychain[Constants.oldPasswordKey, password: password] != nil,
|
||||
let encryptedPasswordKey else { return }
|
||||
self.keychain[Constants.oldPasswordKey, password: password] = nil
|
||||
self.keychain[encryptedPasswordKey, password: password] = password
|
||||
}
|
||||
|
||||
/// Makes encrypted password key for keychain
|
||||
private static func makeEncryptedPasswordCommonKey(cipherData: CipherData) -> CommonKey? {
|
||||
guard let encryptedCKeyString = try? Self.aesEncrypt(string: Constants.passwordKey,
|
||||
key: cipherData.key,
|
||||
iv: cipherData.iv)
|
||||
else { return nil }
|
||||
return CommonKey(encryptedCKeyString)
|
||||
}
|
||||
|
||||
/// Encrypt key for use in keychain
|
||||
/// - Parameter commonKey: common key, e.g. "user@wallet.privatekey"
|
||||
/// - Returns: encrypted CommonKey object
|
||||
private func encryptPK(commonKey: String) -> CommonKey {
|
||||
let encryptedCKeyString = try? Self.aesEncrypt(string: commonKey,
|
||||
key: self.cipherData.key,
|
||||
iv: self.cipherData.iv)
|
||||
return CommonKey(encryptedCKeyString ?? commonKey)
|
||||
}
|
||||
|
||||
/// Generates data for encoding keys
|
||||
private static func makeCipherData() -> CipherData {
|
||||
let ckey = UUID().uuidString.replacingOccurrences(of:"-",
|
||||
with: "",
|
||||
options: .literal)
|
||||
let iv = String(UUID().uuidString.replacingOccurrences(of:"-",
|
||||
with: "",
|
||||
options: .literal)
|
||||
.dropLast(16))
|
||||
return CipherData(key: ckey, iv: iv)
|
||||
}
|
||||
|
||||
/// Tries to load cipher data and creates it, if doesn't exist
|
||||
private static func initCipherData() -> CipherData {
|
||||
if let ckey = UserDefaults.standard.string(forKey: Constants.cipherKey),
|
||||
let iv = UserDefaults.standard.string(forKey: Constants.ivKey) {
|
||||
return CipherData(key: ckey, iv: iv)
|
||||
} else {
|
||||
return Self.createCipherData()
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates new data for encoding keychain keys and saves it to defaults
|
||||
private static func createCipherData() -> CipherData {
|
||||
let cipherData = Self.makeCipherData()
|
||||
UserDefaults.standard.set(cipherData.key, forKey: Constants.cipherKey)
|
||||
UserDefaults.standard.set(cipherData.iv, forKey: Constants.ivKey)
|
||||
return cipherData
|
||||
}
|
||||
|
||||
static func aesEncrypt(string: String, key: String, iv: String) throws -> String {
|
||||
guard let data = string.data(using: .utf8) else {
|
||||
throw WalletError.cannotEncryptNonUTF8Data
|
||||
}
|
||||
let encrypted = try AES(key: Array(key.utf8),
|
||||
blockMode: CBC(iv: Array(iv.utf8)))
|
||||
.encrypt([UInt8](data))
|
||||
let encryptedData = Data(encrypted)
|
||||
return encryptedData.base64EncodedString()
|
||||
}
|
||||
|
||||
static func aesDecrypt(string: String, key: String, iv: String) throws -> String {
|
||||
guard let data = Data(base64Encoded: string) else {
|
||||
throw WalletError.cannotDecryptNonUTF8Data
|
||||
}
|
||||
let decrypted = try AES(key: Array(key.utf8),
|
||||
blockMode: CBC(iv: Array(iv.utf8)))
|
||||
.decrypt([UInt8](data))
|
||||
let decryptedData = Data(decrypted)
|
||||
guard let decryptedString = String(bytes: decryptedData.bytes, encoding: .utf8) else {
|
||||
throw WalletError.cannotDecryptData
|
||||
}
|
||||
return decryptedString
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Resident.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 25.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
struct Resident: CertifiedDevice {
|
||||
|
||||
let device: Device
|
||||
let status: DeviceStatus
|
||||
|
||||
static func permit(for device: Device, using environment: NetworkEnvironment) async throws -> CertifiedDevice {
|
||||
|
||||
if device.isTrusted { return Resident(device: device, status: .valid) }
|
||||
|
||||
let result = await DeviceCheckToken.generate()
|
||||
guard let deviceToken = try? result.get() else {
|
||||
throw CertifiedDeviceError.token
|
||||
}
|
||||
|
||||
let service = DeviceService(environment: environment)
|
||||
do {
|
||||
let status = try await service.checkDevice(id: device.id, token: deviceToken.token)
|
||||
return Resident(device: device, status: status == .valid ? .valid : .invalid )
|
||||
} catch let NetworkServiceError.gqlApplication(error) {
|
||||
|
||||
let deviceError: CertifiedDeviceError
|
||||
switch error {
|
||||
case "Invalid": deviceError = .invalid
|
||||
case "DeviceNotFoundError": deviceError = .notFound
|
||||
case "DecryptError": deviceError = .decrypt
|
||||
case "UnexpectedDeviceKind": deviceError = .kind
|
||||
case "UnexpectedError": deviceError = .unknown
|
||||
default: deviceError = .unknown
|
||||
}
|
||||
|
||||
throw deviceError
|
||||
} catch {
|
||||
throw CertifiedDeviceError.unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
final class VillageHouse: Bank {
|
||||
private enum Constants {
|
||||
static let oldKey = "Account.Service.collection"
|
||||
static let key = "Wallet.bank.service"
|
||||
|
||||
enum Keys {
|
||||
static let current = CommonKey("Account.Service.current")
|
||||
static let collection = CommonKey("Account.Service.collection")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private let device: Device
|
||||
private let environment: NetworkEnvironment
|
||||
private let keychainKeeper: PrivacyKeeper
|
||||
private let activeSubject: CurrentValueSubject<Villager?, Never>
|
||||
private let villagersSubject: CurrentValueSubject<[Villager], Never>
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(password: String, keychain: PrivacyKeeper, device: Device, environment: NetworkEnvironment) {
|
||||
|
||||
self.device = device
|
||||
self.environment = environment
|
||||
self.keychainKeeper = keychain
|
||||
|
||||
let collection = Self.restore(keychain: keychain)
|
||||
Self.migrateIfNeeded(collection: collection, password: password)
|
||||
self.villagersSubject = CurrentValueSubject(collection)
|
||||
|
||||
let active = Self.active(in: collection, keychain: keychain)
|
||||
self.activeSubject = CurrentValueSubject(active)
|
||||
|
||||
self.villagersSubject
|
||||
.sink { [weak self] villagers in
|
||||
self?.save(villagers: villagers)
|
||||
}
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
||||
// MARK: - Bank
|
||||
|
||||
var active: Wallet? { self.activeSubject.value }
|
||||
|
||||
var wallets: [Wallet] { self.villagersSubject.value }
|
||||
|
||||
lazy var activePublisher: AnyPublisher<Wallet?, Never> = self.activeSubject.map { $0 }.eraseToAnyPublisher()
|
||||
|
||||
lazy var walletsPublisher: AnyPublisher<[Wallet], Never> = self.villagersSubject.map { $0 }.eraseToAnyPublisher()
|
||||
|
||||
func remove(wallet: Wallet) throws {
|
||||
|
||||
var villagers = self.villagersSubject.value
|
||||
guard let villager = self.villager(by: wallet),
|
||||
let index = villagers.firstIndex(where: { $0 == villager }) else {
|
||||
throw BankError.notOwned
|
||||
}
|
||||
|
||||
villagers.remove(at: index)
|
||||
villager.clear()
|
||||
|
||||
self.villagersSubject.value = villagers
|
||||
|
||||
if let active = self.active,
|
||||
villager.isEquals(other: active) {
|
||||
try self.activate(wallet: nil)
|
||||
}
|
||||
}
|
||||
|
||||
func activate(wallet: Wallet?) throws {
|
||||
|
||||
guard let wallet else {
|
||||
Settings.shared[Constants.Keys.current] = Data()
|
||||
self.activeSubject.value = nil
|
||||
return
|
||||
}
|
||||
|
||||
guard let villager = self.villager(by: wallet) else {
|
||||
throw BankError.notOwned
|
||||
}
|
||||
|
||||
// We can save only wallet in accepted state.
|
||||
if case .accepted = villager.state {
|
||||
Settings.shared[Constants.Keys.current] = villager.jsonData()
|
||||
}
|
||||
|
||||
self.activeSubject.value = villager
|
||||
}
|
||||
|
||||
func add(using walletCase: WalletCase, password: String) async throws -> Wallet {
|
||||
let villager = try await Villager.create(walletCase: walletCase,
|
||||
on: self.device,
|
||||
using: self.environment,
|
||||
keychain: self.keychainKeeper)
|
||||
self.add(wallets: [villager], password: password)
|
||||
return villager
|
||||
}
|
||||
|
||||
func add(using purses: [Purse], password: String) throws {
|
||||
guard self.keychainKeeper.accept(password: password) else {
|
||||
throw WalletError.passwordNotMatch
|
||||
}
|
||||
|
||||
let wallets = purses
|
||||
.filter { $0.bank?.isEquals(other: self, password: password) ?? false }
|
||||
.filter { purse in
|
||||
!self.wallets.contains(where: { $0.name == purse.name && $0.keyType.rawValue == purse.permission.permName })
|
||||
}
|
||||
.map { Villager.create(purse: $0,
|
||||
keeper: self.keychainKeeper) }
|
||||
|
||||
self.add(wallets: wallets, password: password)
|
||||
}
|
||||
|
||||
func restore(using keys: WalletKey) async throws -> PurseHolder {
|
||||
|
||||
let holder = PurseHolder()
|
||||
let service = AccountHyperionService(environment: self.environment)
|
||||
let eosService = EOSService(environment: self.environment)
|
||||
|
||||
return try await holder.load(using: service, eosService: eosService, keys: keys, in: self)
|
||||
}
|
||||
|
||||
func refreshStatus(wallet: Wallet?) async throws {
|
||||
|
||||
let villagers: [Villager]
|
||||
if let wallet,
|
||||
let villager = self.villager(by: wallet) {
|
||||
villagers = [villager]
|
||||
} else {
|
||||
villagers = self.villagersSubject.value
|
||||
}
|
||||
|
||||
_ = await withTaskGroup(of: Void.self) { group in
|
||||
|
||||
villagers.forEach { villager in
|
||||
|
||||
switch villager.state {
|
||||
case .pending,
|
||||
.creating:
|
||||
group.addTask {
|
||||
try? await villager.refresh(using: self.environment)
|
||||
}
|
||||
case .accepted,
|
||||
.declined:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
await group.waitForAll()
|
||||
|
||||
self.save(villagers: villagers)
|
||||
}
|
||||
}
|
||||
|
||||
func accept(password: String) -> Bool {
|
||||
self.keychainKeeper.accept(password: password)
|
||||
}
|
||||
|
||||
func isEquals(other: Bank, password: String) -> Bool {
|
||||
guard let house = other as? VillageHouse else { return false }
|
||||
return self.device.isEquals(other: house.device) &&
|
||||
self.environment.isEquals(other: house.environment) &&
|
||||
self.keychainKeeper.accept(password: password) &&
|
||||
other.accept(password: password)
|
||||
}
|
||||
|
||||
func switchPassword(_ new: String, old: String) throws {
|
||||
try self.keychainKeeper.update(password: new, old: old)
|
||||
self.wallets.forEach { $0.updatePrivateKeyEncryption(password: new, old: old) }
|
||||
}
|
||||
|
||||
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult] {
|
||||
|
||||
// TODO: - After move EOS to frameworks!
|
||||
|
||||
/*guard password == self.password else { throw BankError.passwordNotMatch }
|
||||
|
||||
let villagers = keyUpdates.compactMap({ self.villager(by: $0.wallet) })
|
||||
|
||||
guard villagers.count != keyUpdates.count else { throw BankError.notOwned }
|
||||
|
||||
guard let privateKey = self.privateKey(password) else {
|
||||
throw WalletError.privateKeyNotExists
|
||||
}
|
||||
|
||||
return WalletKeyUpdate(wallet: self, oldPrivateKey: "", transitionId: "")
|
||||
|
||||
return try await villager.update(key: key, password: self.password, using: self.environment)*/
|
||||
return []
|
||||
}
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func isEquals(device: Device, environment: NetworkEnvironment, password: String) -> Bool {
|
||||
return self.device.isEquals(other: device) &&
|
||||
self.environment.isEquals(other: environment) &&
|
||||
self.accept(password: password)
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private func add(wallets: [Villager], password: String) {
|
||||
wallets
|
||||
.filter { $0.key.privateKey.isExist }
|
||||
.forEach { $0.updateKeychain(key: $0.key, using: password) }
|
||||
|
||||
let villagers = self.villagersSubject.value
|
||||
self.villagersSubject.value = villagers + wallets
|
||||
}
|
||||
|
||||
private func save(villagers: [Villager]) {
|
||||
Settings.shared[Constants.Keys.collection] = villagers.jsonData()
|
||||
}
|
||||
|
||||
private static func restore(keychain: PrivacyKeeper) -> [Villager] {
|
||||
if let data: Data = Settings.shared[Constants.Keys.collection],
|
||||
let userInfoKey = Villager.keychainUserInfoKey,
|
||||
let villagers: [Villager] = data.jsonDecoded(type: Villager.self,
|
||||
userInfo: [userInfoKey: keychain]) {
|
||||
return villagers
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
private static func migrateIfNeeded(collection: [Villager], password: String) {
|
||||
guard collection.count > 0 else { return }
|
||||
collection.forEach { $0.migrate(password: password) }
|
||||
}
|
||||
|
||||
private func migrate() {
|
||||
|
||||
let migrateWallets: [Villager]
|
||||
if let data = UserDefaults.standard.value(forKey: Constants.oldKey) as? Data,
|
||||
let collection = try? JSONDecoder().decode([Villager.OldVillager].self, from: data) {
|
||||
migrateWallets = collection.compactMap { $0.covert(keeper: self.keychainKeeper) }
|
||||
} else {
|
||||
migrateWallets = []
|
||||
}
|
||||
|
||||
print(migrateWallets)
|
||||
|
||||
// Delete previous key
|
||||
// UserDefaults.standard.set(nil, forKey: Constants.oldKey)
|
||||
|
||||
}
|
||||
|
||||
private func villager(by wallet: Wallet) -> Villager? {
|
||||
guard let villager = wallet as? Villager,
|
||||
self.villagersSubject.value.contains(where: { $0 == villager }) else {
|
||||
return nil
|
||||
}
|
||||
return villager
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
public static func create(password: String, keeper: PrivacyKeeper, on device: Device, environment: NetworkEnvironment) -> VillageHouse {
|
||||
VillageHouse(password: password, keychain: keeper, device: device, environment: environment)
|
||||
}
|
||||
|
||||
public static func removeAllVillagers() {
|
||||
Settings.shared[Constants.Keys.collection] = Data()
|
||||
}
|
||||
|
||||
private static func active(in collection: [Villager], keychain: PrivacyKeeper) -> Villager? {
|
||||
|
||||
if let data: Data = Settings.shared[Constants.Keys.current],
|
||||
let userInfoKey = Villager.keychainUserInfoKey,
|
||||
let villager: Villager = data.jsonDecoded(type: Villager.self,
|
||||
userInfo: [userInfoKey: keychain]),
|
||||
collection.contains(where: { $0 == villager }) {
|
||||
return villager
|
||||
}
|
||||
|
||||
guard let rawValue: String = Settings.shared[Constants.Keys.current] else { return nil }
|
||||
|
||||
let name = rawValue.components(separatedBy: "@").first
|
||||
let keyTypeString = rawValue.components(separatedBy: "@").last ?? ""
|
||||
let keyType = WalletKeyType(rawValue: keyTypeString) ?? WalletKeyType.active
|
||||
|
||||
return collection.first(where: { $0.name == name && $0.keyType == keyType })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
//
|
||||
// Villager.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 16.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
|
||||
extension String {
|
||||
fileprivate static let privateKey = "privateKey"
|
||||
}
|
||||
|
||||
final class Villager: Wallet, Codable, CustomStringConvertible {
|
||||
|
||||
struct OldVillager: Codable {
|
||||
var username: String
|
||||
var publicKey: String
|
||||
var keyType: String
|
||||
|
||||
func covert(keeper: PrivacyKeeper) -> Villager? {
|
||||
guard let keyType = WalletKeyType(rawValue: self.keyType),
|
||||
let key = try? WalletKey.restore(using: self.username, type: keyType, publicKey: self.publicKey) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return Villager(name: self.username,
|
||||
key: key,
|
||||
keyType: keyType,
|
||||
state: .accepted,
|
||||
keychain: keeper)
|
||||
}
|
||||
}
|
||||
|
||||
private let stateSubject: CurrentValueSubject<WalletState, Never>
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(walletCase: WalletCase,
|
||||
keyType: WalletKeyType,
|
||||
state: WalletState,
|
||||
keychain: PrivacyKeeper) {
|
||||
self.name = walletCase.name
|
||||
self.key = walletCase.key
|
||||
self.keyType = keyType
|
||||
self.keychain = keychain
|
||||
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
|
||||
}
|
||||
|
||||
private init(name: String,
|
||||
key: WalletKey,
|
||||
keyType: WalletKeyType,
|
||||
state: WalletState,
|
||||
keychain: PrivacyKeeper) {
|
||||
self.name = name
|
||||
self.key = key
|
||||
self.keyType = keyType
|
||||
self.keychain = keychain
|
||||
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
// Old values
|
||||
case username
|
||||
case publicKey
|
||||
//
|
||||
case name
|
||||
case key
|
||||
case keyType
|
||||
case state
|
||||
}
|
||||
|
||||
static var keychainUserInfoKey: CodingUserInfoKey? {
|
||||
return CodingUserInfoKey(rawValue: "keychain")
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// For save old format with username field!
|
||||
let username = try container.decodeIfPresent(String.self, forKey: .username)
|
||||
if let username {
|
||||
self.name = username
|
||||
} else {
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
}
|
||||
|
||||
self.keyType = try container.decode(WalletKeyType.self, forKey: .keyType)
|
||||
|
||||
let state = try container.decode(WalletState.self, forKey: .state)
|
||||
self.stateSubject = CurrentValueSubject<WalletState, Never>(state)
|
||||
guard let keychainKey = Self.keychainUserInfoKey,
|
||||
let keeper = decoder.userInfo[keychainKey] as? PrivacyKeeper else {
|
||||
throw WalletError.decodingWithoutKeychainContext
|
||||
}
|
||||
self.keychain = keeper
|
||||
|
||||
// If first version we hold key on flat structure!
|
||||
if let key = try? WalletKey(from: decoder) {
|
||||
self.key = key
|
||||
} else {
|
||||
self.key = try container.decode(WalletKey.self, forKey: .key)
|
||||
}
|
||||
|
||||
// TODO: - NEED COMPLETETASK
|
||||
// self.key = try WalletKey.restore(using: self.name, type: self.keyType, publicKey: publicKey, password: "")
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
try container.encode(self.name, forKey: .name)
|
||||
try container.encode(self.key, forKey: .key)
|
||||
try container.encode(self.keyType, forKey: .keyType)
|
||||
try container.encode(self.stateSubject.value, forKey: .state)
|
||||
}
|
||||
|
||||
// MARK: - Wallet
|
||||
|
||||
let name: String
|
||||
private(set) var key: WalletKey
|
||||
let keyType: WalletKeyType
|
||||
var state: WalletState { self.stateSubject.value }
|
||||
private let keychain: PrivacyKeeper
|
||||
|
||||
lazy var statePublisher: AnyPublisher<WalletState, Never> = self.stateSubject.eraseToAnyPublisher()
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
static func == (lhs: Villager, rhs: Villager) -> Bool {
|
||||
return lhs.name == rhs.name
|
||||
&& lhs.key == rhs.key
|
||||
&& lhs.keyType == rhs.keyType
|
||||
&& lhs.state == rhs.state
|
||||
&& lhs.name == rhs.name
|
||||
|
||||
}
|
||||
|
||||
// MARK: - CustomStringConvertible
|
||||
|
||||
var description: String { "(name: \(name), keyType: \(keyType), state: \(state), public: \(key.publicKey)" }
|
||||
|
||||
// MARK: - Internal
|
||||
|
||||
func refresh(using environment: NetworkEnvironment) async throws {
|
||||
|
||||
let service = AccountService(environment: environment)
|
||||
let state: WalletState
|
||||
|
||||
switch self.state {
|
||||
case .creating(let orderId),
|
||||
.pending(let orderId):
|
||||
do {
|
||||
let order = try await service.order(with: orderId)
|
||||
state = WalletState(order: order)
|
||||
} catch {
|
||||
throw WalletError.network(error)
|
||||
}
|
||||
default:
|
||||
state = self.state
|
||||
}
|
||||
|
||||
self.stateSubject.value = state
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func updateKeychain(key: WalletKey, using password: String) -> Wallet {
|
||||
if self.keychain.accept(password: password) {
|
||||
self.key = key
|
||||
self.updatePrivateKey(password: password)
|
||||
}
|
||||
return self
|
||||
}
|
||||
|
||||
func privateKey(password: String?) -> String? {
|
||||
guard let password, self.keychain.accept(password: password) else { return nil }
|
||||
let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)
|
||||
return self.keychain.privateKey(for: key.rawValue, password: password)
|
||||
}
|
||||
|
||||
private func updatePrivateKey(password: String) {
|
||||
guard let privateKey = self.key.privateKey else { return }
|
||||
let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)
|
||||
self.keychain.update(privateKey: privateKey, for: key.rawValue, password: password)
|
||||
}
|
||||
|
||||
// TODO: - Need review
|
||||
|
||||
func migrate(password: String) {
|
||||
let oldKey = CommonKey.key(self.name, suffix: .privateKey)
|
||||
let newKey = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)
|
||||
self.keychain.migrate(oldKey: oldKey, newKey: newKey, password: password)
|
||||
}
|
||||
|
||||
/// Updates storage of private key in keychain for new password
|
||||
/// - Parameters:
|
||||
/// - password: new password
|
||||
/// - old: old password (which was stored with private key earlier)
|
||||
func updatePrivateKeyEncryption(password: String, old: String) {
|
||||
let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)
|
||||
let privateKey = self.privateKey(password: old)
|
||||
self.keychain.update(privateKey: nil, for: key.rawValue, password: old)
|
||||
self.keychain.update(privateKey: privateKey, for: key.rawValue, password: password)
|
||||
}
|
||||
|
||||
func clear() {
|
||||
let key = CommonKey.key("\(self.name)@\(self.keyType.rawValue)", suffix: .privateKey)
|
||||
self.keychain.update(privateKey: nil, for: key.rawValue, password: "")
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(purse: Purse, keeper: PrivacyKeeper) -> Villager {
|
||||
Villager(name: purse.name,
|
||||
key: purse.key,
|
||||
keyType: purse.keyType,
|
||||
state: .accepted,
|
||||
keychain: keeper)
|
||||
}
|
||||
|
||||
static func create(walletCase: WalletCase,
|
||||
on device: Device,
|
||||
using environment: NetworkEnvironment,
|
||||
keychain: PrivacyKeeper) async throws -> Villager {
|
||||
|
||||
let service = AccountService(environment: environment)
|
||||
do {
|
||||
// return Villager(walletCase: walletCase, state: .pending)
|
||||
let order = try await service.create(username: walletCase.name,
|
||||
pubKey: walletCase.key.publicKey,
|
||||
deviceId: device.id)
|
||||
|
||||
let state: WalletState
|
||||
switch order.status {
|
||||
case .creating: state = .creating(order.id)
|
||||
case .active, .executed: state = .pending(order.id)
|
||||
case .expired, .failed: state = .declined
|
||||
case .completed: state = .accepted
|
||||
}
|
||||
|
||||
return Villager(walletCase: walletCase,
|
||||
keyType: .owner,
|
||||
state: state,
|
||||
keychain: keychain)
|
||||
|
||||
} catch let NetworkServiceError.gqlApplication(error) {
|
||||
|
||||
if ApplicationSettings.ignoreCreateAccountSatus {
|
||||
return Villager(walletCase: walletCase,
|
||||
keyType: .owner,
|
||||
state: .creating("HELLO"),
|
||||
keychain: keychain)
|
||||
}
|
||||
|
||||
let walletError: WalletError
|
||||
switch error {
|
||||
case "invalid EOS public key": walletError = .invalidKey
|
||||
case "INVALID DEVICE ID": walletError = .invalidDevice
|
||||
case "UNKNOWN DEVICE": walletError = .device
|
||||
case "UNCHECKED": walletError = .unchecked
|
||||
case "UNTRUSTED": walletError = .untrusted
|
||||
case "DEVICE LIMIT REACHED": walletError = .deviceLimit
|
||||
case "DAILY LIMIT REACHED": walletError = .dailyLimit
|
||||
case "UNAVAILABLE": walletError = .unavailable
|
||||
case "ACCOUNT ALREADY EXISTS": walletError = .exists
|
||||
default: walletError = .unavailable
|
||||
}
|
||||
throw walletError
|
||||
|
||||
} catch {
|
||||
throw WalletError.unavailable
|
||||
}
|
||||
}
|
||||
|
||||
static func create(walletKey: WalletKey, using environment: NetworkEnvironment) async throws -> Villager {
|
||||
// TODO: - Add logic
|
||||
throw WalletError.unavailable
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// VillagerCoat.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 16.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletNetwork
|
||||
|
||||
final class VillagerCoat: WalletCase {
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
private init(name: String, key: WalletKey) {
|
||||
self.name = name
|
||||
self.key = key
|
||||
}
|
||||
|
||||
// MARK: - WalletCase
|
||||
|
||||
let name: String
|
||||
private(set) var key: WalletKey
|
||||
|
||||
@discardableResult
|
||||
func update(key: WalletKey) -> WalletKey {
|
||||
self.key = key
|
||||
return self.key
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func generateKey() -> WalletKey {
|
||||
if let key = try? WalletKey.create() {
|
||||
self.key = key
|
||||
}
|
||||
return self.key
|
||||
}
|
||||
|
||||
// MARK: - Static
|
||||
|
||||
static func create(name: String, using environment: NetworkEnvironment, key: WalletKey) async throws -> VillagerCoat {
|
||||
|
||||
let walletName = name.lowercased()
|
||||
let service = AccountService(environment: environment)
|
||||
let isAvailable = await service.isAvailable(username: walletName)
|
||||
switch isAvailable {
|
||||
case .available: return VillagerCoat(name: walletName, key: key)
|
||||
case .alreadyTaken: throw WalletCaseError.exists(name: walletName)
|
||||
case .system: throw WalletCaseError.system
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Bank.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/27/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
import WalletNetwork
|
||||
|
||||
public enum BankError: Error {
|
||||
case empty
|
||||
case notOwned
|
||||
}
|
||||
|
||||
/// Bank that contain wallets.
|
||||
public protocol Bank: AnyObject {
|
||||
/// Current active wallet.
|
||||
var active: Wallet? { get }
|
||||
/// Publisher for observe changes.
|
||||
var activePublisher: AnyPublisher<Wallet?, Never> { get }
|
||||
/// Wallets in bank.
|
||||
var wallets: [Wallet] { get }
|
||||
/// Publisher for observe wallet changes.
|
||||
var walletsPublisher: AnyPublisher<[Wallet], Never> { get }
|
||||
/// Activate wallet
|
||||
func activate(wallet: Wallet?) throws
|
||||
/// Create new wallet in bank.
|
||||
func add(using walletCase: WalletCase, password: String) async throws -> Wallet
|
||||
/// Add Purse to bank with converting to Wallet
|
||||
func add(using purses: [Purse], password: String) throws
|
||||
/// Restore wallet by key.
|
||||
func restore(using keys: WalletKey) async throws -> PurseHolder
|
||||
/// Remove wallet from bank.
|
||||
func remove(wallet: Wallet) throws
|
||||
/// Check wallet status
|
||||
func refreshStatus(wallet: Wallet?) async throws
|
||||
/// Update WalletKey for Wallet
|
||||
func update(_ keyUpdates: [WalletKeyUpdate], using password: String) async throws -> [WalletKeyUpdateResult]
|
||||
/// Compare two banks
|
||||
func isEquals(other: Bank, password: String) -> Bool
|
||||
/// Switch password
|
||||
func switchPassword(_ password: String, old: String) throws
|
||||
/// Check if password valid for this bank
|
||||
func accept(password: String) -> Bool
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// CertifiedDevice.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 25.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum CertifiedDeviceError: Error {
|
||||
/// Unavailable generate token for check
|
||||
case token
|
||||
/// Device not fount on server
|
||||
case notFound
|
||||
/// Invalid token
|
||||
case invalid
|
||||
/// Token decription error
|
||||
case decrypt
|
||||
/// Unexpected device kind
|
||||
case kind
|
||||
/// Unknown error
|
||||
case unknown
|
||||
/// Already in progress
|
||||
case progress
|
||||
}
|
||||
|
||||
/// Device status
|
||||
public enum DeviceStatus {
|
||||
/// Valid
|
||||
case valid
|
||||
/// Invalid
|
||||
case invalid
|
||||
}
|
||||
|
||||
public protocol CertifiedDevice {
|
||||
/// Device
|
||||
var device: Device { get }
|
||||
/// Device status
|
||||
var status: DeviceStatus { get }
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// Device.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
/// Errors for Device
|
||||
public enum DeviceError: Error {
|
||||
/// Failed while restore
|
||||
case restore
|
||||
/// Failed create device on server side
|
||||
case create
|
||||
/// Failed retrieve status
|
||||
case status
|
||||
}
|
||||
|
||||
public protocol Device: CustomStringConvertible {
|
||||
/// Device generated Identity UUID
|
||||
var uuid: String { get }
|
||||
/// Server generated Identity
|
||||
var id: String { get }
|
||||
/// Is device trusted
|
||||
var isTrusted: Bool { get }
|
||||
|
||||
func isEquals(other: Device) -> Bool
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
extension Device {
|
||||
func isEquals(other: Device) -> Bool {
|
||||
return self.uuid == other.uuid &&
|
||||
self.id == other.id &&
|
||||
self.isTrusted == other.isTrusted
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// ErrorDomain.swift
|
||||
//
|
||||
//
|
||||
// Created by user on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum ErrorDomain: Error {
|
||||
case noEnvironment
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
//
|
||||
// PurseHolder.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/4/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
import WalletNetwork
|
||||
import EosioSwift
|
||||
import Combine
|
||||
|
||||
public struct Purse {
|
||||
public let name: String
|
||||
public let key: WalletKey
|
||||
public let keyType: WalletKeyType
|
||||
let permission: Permission
|
||||
var bank: Bank?
|
||||
}
|
||||
|
||||
public final class PurseHolder {
|
||||
|
||||
public enum PurseError: Error {
|
||||
case empty
|
||||
}
|
||||
|
||||
public private(set) var purses: [Purse] = []
|
||||
private var count: Int = 0
|
||||
|
||||
func load(using service: AccountHyperionService, eosService: EOSService, keys: WalletKey, in bank: Bank) async throws -> PurseHolder {
|
||||
|
||||
let publicKey = keys.publicKey
|
||||
let collection = await service.fetchNamesHyperion(publicKey: publicKey)
|
||||
|
||||
guard !collection.isEmpty else {
|
||||
throw PurseError.empty
|
||||
}
|
||||
|
||||
let _ = await withCheckedContinuation { continuation in
|
||||
|
||||
self.count = collection.count
|
||||
|
||||
collection.forEach { name in
|
||||
|
||||
DispatchQueue.global().async {
|
||||
|
||||
eosService.getAccount(name) { response in
|
||||
|
||||
self.count -= 1
|
||||
|
||||
switch response {
|
||||
case let .success(accountInfo):
|
||||
|
||||
let permissions = accountInfo.permissions
|
||||
.filter { permission in permission.requiredAuth.keys.contains(where: { $0.key == publicKey }) }
|
||||
.sorted { $0.permName > $1.permName }
|
||||
|
||||
permissions.forEach { permission in
|
||||
|
||||
if (permission.permName == WalletKeyType.owner.rawValue) {
|
||||
self.purses.append(Purse(name: name,
|
||||
key: keys,
|
||||
keyType: .owner,
|
||||
permission: permission,
|
||||
bank: bank))
|
||||
}
|
||||
|
||||
let hasAddedOwnerKey = self.purses.contains(where: { account in
|
||||
account.name == name &&
|
||||
account.permission.requiredAuth.keys.first?.key == permission.requiredAuth.keys.first?.key
|
||||
})
|
||||
|
||||
if let type = WalletKeyType(rawValue: permission.permName),
|
||||
type != WalletKeyType.owner,
|
||||
!hasAddedOwnerKey {
|
||||
self.purses.append(Purse(name: name,
|
||||
key: keys,
|
||||
keyType: type,
|
||||
permission: permission,
|
||||
bank: bank))
|
||||
}
|
||||
}
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
if self.count == 0 {
|
||||
continuation.resume(returning: self.purses)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
//
|
||||
// Wallet.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 15.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletNetwork
|
||||
import Combine
|
||||
|
||||
/// Errors for wallet creation
|
||||
public enum WalletError: Error {
|
||||
/// Invalid EOS public key
|
||||
case invalidKey
|
||||
/// Device not found
|
||||
case invalidDevice
|
||||
/// Device not found
|
||||
case device
|
||||
/// Device does not do check phase
|
||||
case unchecked
|
||||
/// Device not trusted
|
||||
case untrusted
|
||||
/// Device limit reached
|
||||
case deviceLimit
|
||||
/// Daily limit reached
|
||||
case dailyLimit
|
||||
/// Unknown error
|
||||
case unavailable
|
||||
/// Network error
|
||||
case network(Error)
|
||||
/// Private key does not exists
|
||||
case privateKeyNotExists
|
||||
/// Wallet with such name already exists.
|
||||
case exists
|
||||
/// Input password doesn't match keychain password
|
||||
case passwordNotMatch
|
||||
/// Error creating access key for password in keychain
|
||||
case passwordKeyNotExist
|
||||
/// Failed encryption access key for keychain
|
||||
case cannotEncryptNonUTF8Data
|
||||
/// Failed decryption access key for keychain
|
||||
case cannotDecryptNonUTF8Data
|
||||
/// Common decrypt error
|
||||
case cannotDecryptData
|
||||
/// Trying to decode Wallet-protocol object without keychain data
|
||||
case decodingWithoutKeychainContext
|
||||
}
|
||||
|
||||
/// Wallet state
|
||||
public enum WalletState: RawRepresentable {
|
||||
/// Transaction sent to EOS.
|
||||
case creating(String)
|
||||
/// Creation in progress
|
||||
case pending(String)
|
||||
/// Created
|
||||
case accepted
|
||||
/// Creation failed
|
||||
case declined
|
||||
|
||||
private enum Constants {
|
||||
static let preffixCreate = "C"
|
||||
static let preffixPending = "P"
|
||||
}
|
||||
|
||||
// TODO:- remove public
|
||||
public init(order: WalletOrder) {
|
||||
switch order.status {
|
||||
case .completed:
|
||||
self = .accepted
|
||||
case .creating:
|
||||
self = .creating(order.id)
|
||||
case .executed, .active:
|
||||
self = .pending(order.id)
|
||||
case .failed, .expired:
|
||||
self = .declined
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - RawRepresentable
|
||||
|
||||
public init?(rawValue: String) {
|
||||
switch rawValue {
|
||||
case "accepted": self = .accepted
|
||||
case "declined": self = .declined
|
||||
default:
|
||||
let value = String(rawValue.dropFirst())
|
||||
let preffix = rawValue.first?.uppercased()
|
||||
if preffix == Constants.preffixPending {
|
||||
self = .pending(value)
|
||||
} else {
|
||||
self = .creating(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var rawValue: String {
|
||||
switch self {
|
||||
case .accepted: return "accepted"
|
||||
case .declined: return "declined"
|
||||
case let .pending(value): return "\(Constants.preffixPending)\(value)"
|
||||
case let .creating(value): return "\(Constants.preffixCreate)\(value)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension WalletState: Codable { }
|
||||
|
||||
public enum WalletKeyType: String, Codable, CustomStringConvertible {
|
||||
case owner
|
||||
case active
|
||||
|
||||
public var description: String { self.rawValue }
|
||||
}
|
||||
|
||||
public struct WalletKeyUpdateResult {
|
||||
public let wallet: Wallet
|
||||
public let oldPrivateKey: String
|
||||
public let transitionId: String
|
||||
}
|
||||
|
||||
public struct WalletKeyUpdate {
|
||||
public let wallet: Wallet
|
||||
public let key: WalletKey
|
||||
|
||||
public init(wallet: Wallet, key: WalletKey) {
|
||||
self.wallet = wallet
|
||||
self.key = key
|
||||
}
|
||||
}
|
||||
|
||||
/// Wallet
|
||||
public protocol Wallet: AnyObject {
|
||||
/// Name
|
||||
var name: String { get }
|
||||
/// Keys
|
||||
var key: WalletKey { get }
|
||||
/// Type
|
||||
var keyType: WalletKeyType { get }
|
||||
/// State
|
||||
var state: WalletState { get }
|
||||
|
||||
var statePublisher: AnyPublisher<WalletState, Never> { get }
|
||||
|
||||
@discardableResult
|
||||
func updateKeychain(key: WalletKey, using password: String) -> Wallet
|
||||
func privateKey(password: String?) -> String?
|
||||
func updatePrivateKeyEncryption(password: String, old: String)
|
||||
|
||||
/// Compare two Wallets
|
||||
func isEquals(other: Wallet) -> Bool
|
||||
}
|
||||
|
||||
public extension Wallet {
|
||||
|
||||
var isOnCreationState: Bool {
|
||||
switch self.state {
|
||||
case .declined,
|
||||
.accepted:
|
||||
return false
|
||||
case .creating,
|
||||
.pending:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
var isOnActiveState: Bool {
|
||||
switch self.state {
|
||||
case .creating,
|
||||
.accepted:
|
||||
return true
|
||||
case .declined,
|
||||
.pending:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isEquals(other: Wallet) -> Bool {
|
||||
self.name == other.name &&
|
||||
self.key == other.key &&
|
||||
self.keyType == other.keyType &&
|
||||
self.state == other.state
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// WalletCase.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 17.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Wallet Case errors
|
||||
public enum WalletCaseError: Error {
|
||||
/// System error
|
||||
case system
|
||||
/// Wallet with name already exists
|
||||
case exists(name: String)
|
||||
}
|
||||
|
||||
/// Wallet case. This mean case that can contain wallet.
|
||||
public protocol WalletCase: AnyObject {
|
||||
/// Name
|
||||
var name: String { get }
|
||||
/// Key
|
||||
var key: WalletKey { get }
|
||||
/// Update key
|
||||
@discardableResult
|
||||
func update(key: WalletKey) -> WalletKey
|
||||
/// Generate new key
|
||||
@discardableResult
|
||||
func generateKey() -> WalletKey
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// WalletKey.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 17.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import eosswift
|
||||
|
||||
/// Errors for WalletKey
|
||||
public enum WalletKeyError: Error {
|
||||
/// System error
|
||||
case system
|
||||
/// Invalid public key
|
||||
case invalidPublic
|
||||
/// Invalid private key
|
||||
case invalidPrivate
|
||||
/// Invalid pair public and private keys
|
||||
case invalidPair
|
||||
///
|
||||
case unlock
|
||||
}
|
||||
|
||||
/// Represents Wallet key.
|
||||
/// This keys must pass checks.
|
||||
public enum WalletKey: Codable, Equatable {
|
||||
|
||||
/// Public key
|
||||
case publicKey(String)
|
||||
/// Public and Private key
|
||||
case bunch(publicKey: String, privateKey: String)
|
||||
|
||||
public var publicKey: String {
|
||||
switch self {
|
||||
case let .publicKey(value): return value
|
||||
case let .bunch(publicKey: value, privateKey: _): return value
|
||||
}
|
||||
}
|
||||
|
||||
public var privateKey: String? {
|
||||
switch self {
|
||||
case let .bunch(publicKey: _, privateKey: value): return value
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
func save() {
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case publicKey
|
||||
case privateKey
|
||||
}
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
let publicKey = try container.decode(String.self, forKey: .publicKey)
|
||||
let privateKey = try container.decodeIfPresent(String.self, forKey: .privateKey)
|
||||
|
||||
if let privateKey {
|
||||
self = .bunch(publicKey: publicKey, privateKey: privateKey)
|
||||
} else {
|
||||
self = .publicKey(publicKey)
|
||||
}
|
||||
}
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
switch self {
|
||||
case let .publicKey(value):
|
||||
try container.encode(value, forKey: .publicKey)
|
||||
case let .bunch(publicKey: publicKey, privateKey: privateKey):
|
||||
try container.encode(publicKey, forKey: .publicKey)
|
||||
try container.encode(privateKey, forKey: .privateKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Public static
|
||||
|
||||
public static func create(public: String, private: String) throws -> WalletKey {
|
||||
|
||||
let privateKey: EOSPrivateKey
|
||||
do {
|
||||
privateKey = try EOSPrivateKey(base58: `private`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPrivate
|
||||
}
|
||||
|
||||
let publicKey: EOSPublicKey
|
||||
do {
|
||||
publicKey = try EOSPublicKey(base58: `public`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPublic
|
||||
}
|
||||
|
||||
guard privateKey.publicKey.base58 == publicKey.base58 else {
|
||||
throw WalletKeyError.invalidPair
|
||||
}
|
||||
|
||||
return .bunch(publicKey: publicKey.base58, privateKey: privateKey.base58)
|
||||
}
|
||||
|
||||
public static func create(public: String) throws -> WalletKey {
|
||||
let publicKey: EOSPublicKey
|
||||
do {
|
||||
publicKey = try EOSPublicKey(base58: `public`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPublic
|
||||
}
|
||||
|
||||
return .publicKey(publicKey.base58)
|
||||
}
|
||||
|
||||
public static func create(private: String) throws -> WalletKey {
|
||||
|
||||
let privateKey: EOSPrivateKey
|
||||
do {
|
||||
privateKey = try EOSPrivateKey(base58: `private`)
|
||||
} catch {
|
||||
throw WalletKeyError.invalidPrivate
|
||||
}
|
||||
|
||||
return .bunch(publicKey: privateKey.publicKey.base58, privateKey: privateKey.base58)
|
||||
}
|
||||
|
||||
/// Create key for wallet
|
||||
public static func create() throws -> WalletKey {
|
||||
guard let key = try? EOSPrivateKey() else { throw WalletKeyError.system }
|
||||
return .bunch(publicKey: key.publicKey.base58, privateKey: key.base58)
|
||||
}
|
||||
|
||||
// MARK: - Internal static
|
||||
|
||||
static func restore(using name: String, type: WalletKeyType, publicKey: String) throws -> WalletKey {
|
||||
|
||||
throw WalletKeyError.unlock
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// DeepLinkActionTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import WalletKit
|
||||
|
||||
final class DeepLinkActionTests: XCTestCase {
|
||||
|
||||
func testConvertFromString() {
|
||||
XCTAssertEqual(DeepLinkAction.connect.rawValue, "connect_accounts")
|
||||
XCTAssertEqual(DeepLinkAction.chat.rawValue, "chat")
|
||||
XCTAssertEqual(DeepLinkAction.transfer.rawValue, "transfer")
|
||||
XCTAssertEqual(DeepLinkAction.walletAuth.rawValue, "wallet_auth")
|
||||
XCTAssertEqual(DeepLinkAction.tokenization.rawValue, "accept_tokenization")
|
||||
XCTAssertEqual(DeepLinkAction.approveBuy.rawValue, "approve_buy")
|
||||
XCTAssertEqual(DeepLinkAction.emission.rawValue, "emission")
|
||||
XCTAssertEqual(DeepLinkAction.chatMessage.rawValue, "chat.message")
|
||||
XCTAssertEqual(DeepLinkAction.transactionTransfer.rawValue, "transaction.incoming_transfer")
|
||||
XCTAssertEqual(DeepLinkAction.transactionInheritance.rawValue, "transactions.incoming_inheritance")
|
||||
XCTAssertEqual(DeepLinkAction.transactionEmission.rawValue, "transactions.emission")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealNew.rawValue, "orders.new_deal")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealComplete.rawValue, "orders.completed_deal")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealCancel.rawValue, "orders.cancelled_deal")
|
||||
XCTAssertEqual(DeepLinkAction.p2pDealDispute.rawValue, "orders.dispute_deal")
|
||||
XCTAssertEqual(DeepLinkAction.news.rawValue, "news")
|
||||
XCTAssertEqual(DeepLinkAction.competitivePrice.rawValue, "competitive_price")
|
||||
}
|
||||
|
||||
func testConvertToString() {
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "connect_accounts"), .connect)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "chat"), .chat)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transfer"), .transfer)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "wallet_auth"), .walletAuth)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "accept_tokenization"), .tokenization)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "approve_buy"), .approveBuy)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "emission"), .emission)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "chat.message"), .chatMessage)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transaction.incoming_transfer"), .transactionTransfer)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.incoming_inheritance"), .transactionInheritance)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "transactions.emission"), .transactionEmission)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.new_deal"), .p2pDealNew)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.completed_deal"), .p2pDealComplete)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.cancelled_deal"), .p2pDealCancel)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "orders.dispute_deal"), .p2pDealDispute)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "news"), .news)
|
||||
XCTAssertEqual(DeepLinkAction(rawValue: "competitive_price"), .competitivePrice)
|
||||
}
|
||||
|
||||
func testConvertFailed() {
|
||||
XCTAssertNil(DeepLinkAction(rawValue: "news1"))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// DeepLinkTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.10.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
@testable
|
||||
import WalletKit
|
||||
|
||||
final class DeepLinkTests: XCTestCase {
|
||||
|
||||
func testCreateWithParamsFailed() {
|
||||
XCTAssertNil(DeepLink.create(params: nil))
|
||||
XCTAssertNil(DeepLink.create(params: [:]))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//
|
||||
// JSONParsingTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 17.02.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import WalletKit
|
||||
|
||||
fileprivate struct User: Decodable {
|
||||
let id: Int
|
||||
let name: String
|
||||
var employer: String?
|
||||
static var employerUserInfoKey: CodingUserInfoKey? {
|
||||
return CodingUserInfoKey(rawValue: "employer")
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case id
|
||||
case name
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let keyedContainer = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.id = try keyedContainer.decode(Int.self, forKey: .id)
|
||||
self.name = try keyedContainer.decode(String.self, forKey: .name)
|
||||
|
||||
guard let employerKey = Self.employerUserInfoKey,
|
||||
let employer = decoder.userInfo[employerKey] as? String else {
|
||||
return
|
||||
}
|
||||
self.employer = employer
|
||||
}
|
||||
}
|
||||
|
||||
final class JSONParsingTests: XCTestCase {
|
||||
|
||||
private enum Locals {
|
||||
static let username1 = "Username1"
|
||||
static let username2 = "Username2"
|
||||
static let userId1 = 1
|
||||
static let userId2 = 2
|
||||
static let companyName = "Company Name"
|
||||
}
|
||||
|
||||
func testJsonDecoded() {
|
||||
let jsonData = """
|
||||
{"id": \(Locals.userId1), "name": "\(Locals.username1)"}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
guard let user: User = jsonData.jsonDecoded(type: User.self) else {
|
||||
XCTFail("Error decoding data")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(user.id, Locals.userId1)
|
||||
XCTAssertEqual(user.name, Locals.username1)
|
||||
}
|
||||
|
||||
func testJsonDecodedArray() {
|
||||
let jsonData = """
|
||||
[{"id": \(Locals.userId1), "name": "\(Locals.username1)"}, {"id": \(Locals.userId2), "name": "\(Locals.username2)"}]
|
||||
""".data(using: .utf8)!
|
||||
|
||||
guard let users: [User] = jsonData.jsonDecoded(type: User.self) else {
|
||||
XCTFail("Error decoding data")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(users.count, 2)
|
||||
XCTAssertEqual(users[0].id, Locals.userId1)
|
||||
XCTAssertEqual(users[0].name, Locals.username1)
|
||||
XCTAssertEqual(users[1].id, Locals.userId2)
|
||||
XCTAssertEqual(users[1].name, Locals.username2)
|
||||
}
|
||||
|
||||
func testJsonDecodedWithUserInfo() {
|
||||
let jsonData = """
|
||||
{"id": \(Locals.userId1), "name": "\(Locals.username1)"}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
guard let key = CodingUserInfoKey(rawValue: "employer") else {
|
||||
XCTFail("Error creating user key")
|
||||
return
|
||||
}
|
||||
|
||||
let userInfo = [key: Locals.companyName]
|
||||
|
||||
guard let user: User = jsonData.jsonDecoded(type: User.self, userInfo: userInfo) else {
|
||||
XCTFail("Error decoding data")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertEqual(user.id, Locals.userId1)
|
||||
XCTAssertEqual(user.name, Locals.username1)
|
||||
XCTAssertEqual(user.employer, Locals.companyName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// KeychainMock.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.TECH on 16.02.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
|
||||
final class KeychainMock: KeychainProtocol {
|
||||
|
||||
private enum KeychainMockLocals {
|
||||
static let password = "PWD"
|
||||
static let biometric = "BIO"
|
||||
}
|
||||
|
||||
typealias Value = (value: String, password: String)
|
||||
|
||||
private var storage: [String: Value]
|
||||
|
||||
init(storage: [String: Value]? = nil) {
|
||||
self.storage = storage ?? [:]
|
||||
}
|
||||
|
||||
func exist(_ key: Key) -> Bool {
|
||||
let commonKey = key.with(KeychainMockLocals.password)
|
||||
return self.storage[commonKey.rawValue].isExist
|
||||
}
|
||||
|
||||
func bioExist(_ key: Key) -> Bool {
|
||||
let commonKey = key.with(KeychainMockLocals.biometric)
|
||||
return self.storage[commonKey.rawValue] != nil
|
||||
}
|
||||
|
||||
subscript(biometric key: Key) -> String? {
|
||||
let commonKey = key.with(KeychainMockLocals.biometric).rawValue
|
||||
let object = self.storage[commonKey]
|
||||
return object?.value
|
||||
}
|
||||
|
||||
subscript(key: Key, password password: String) -> String? {
|
||||
get {
|
||||
let commonKey = key.with(KeychainMockLocals.password).rawValue
|
||||
let object = self.storage[commonKey]
|
||||
return object?.password == password ? object?.value : nil
|
||||
}
|
||||
set {
|
||||
self.update(key, password: password, newValue: newValue)
|
||||
}
|
||||
}
|
||||
|
||||
private func update(_ key: Key, password: String, newValue: String?) {
|
||||
if let value = newValue {
|
||||
self.updateStorage(key: key.with(KeychainMockLocals.password), password: password, value: value)
|
||||
self.updateStorage(key: key.with(KeychainMockLocals.biometric), password: password, value: value)
|
||||
} else {
|
||||
self.updateStorage(key: key.with(KeychainMockLocals.password), password: password, value: nil)
|
||||
self.updateStorage(key: key.with(KeychainMockLocals.biometric), password: password, value: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateStorage(key: Key, password: String, value: String?) {
|
||||
let stringKey = key.rawValue
|
||||
if self.storage[stringKey] != nil, self.storage[stringKey]?.password == password {
|
||||
if let value {
|
||||
self.storage[stringKey]?.value = value
|
||||
} else {
|
||||
self.storage.removeValue(forKey: stringKey)
|
||||
}
|
||||
} else if let value {
|
||||
self.storage[stringKey] = Value(value: value, password: password)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
//
|
||||
// PrivacyKeeperTests.swift
|
||||
//
|
||||
//
|
||||
// Created by user on 16.02.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import WalletKit
|
||||
@testable import WalletFoundation
|
||||
|
||||
final class PrivacyKeeperTests: XCTestCase {
|
||||
|
||||
func testAES() throws {
|
||||
let commonKey = "PrivacyKeeper.password"
|
||||
let encryptedKey = "mdHBVh0lxLT8VBJnL9MYuM/pxWi/QmTCXmW8YZQO8Cs="
|
||||
let key = "96080AE5042241D9BFF30330C9625201"
|
||||
let iv = "7C515B5233314B42"
|
||||
do {
|
||||
let encryptedCommonKey = try PrivacyKeeper.aesEncrypt(string: commonKey, key: key, iv: iv)
|
||||
|
||||
let decryptedCommonKey = try PrivacyKeeper.aesDecrypt(string: encryptedCommonKey, key: key, iv: iv)
|
||||
|
||||
XCTAssertEqual(encryptedCommonKey, encryptedKey, "Incorrect encryption result")
|
||||
XCTAssertEqual(decryptedCommonKey, commonKey, "Incorrect decryption result")
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func testPassword() {
|
||||
let password = "0000"
|
||||
let keychainMock = KeychainMock()
|
||||
let keeper = PrivacyKeeper(keychain: keychainMock)
|
||||
do {
|
||||
XCTAssertFalse(keeper.isPasswordExists, "Incorrect isPasswordExists result")
|
||||
|
||||
try keeper.update(password: password)
|
||||
XCTAssertTrue(keeper.isPasswordExists, "Incorrect isPasswordExists result")
|
||||
XCTAssertTrue(keeper.accept(password: password), "Incorrect password")
|
||||
|
||||
let newPassword = "1111"
|
||||
try keeper.update(password: newPassword, old: password)
|
||||
XCTAssertTrue(keeper.accept(password: newPassword), "Incorrect password")
|
||||
|
||||
let incorrectPassword = "2222"
|
||||
XCTAssertFalse(keeper.accept(password: incorrectPassword), "Incorrect password check")
|
||||
|
||||
keeper.reset()
|
||||
XCTAssertFalse(keeper.accept(password: newPassword), "Incorrect password reset")
|
||||
XCTAssertFalse(keeper.isPasswordExists, "Incorrect isPasswordExists result")
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func testPrivateKey() {
|
||||
let password = "0000"
|
||||
let privateKey = "privateKey1111"
|
||||
let commonKey = "accountKey@malinka.privateKey"
|
||||
let keychainMock = KeychainMock()
|
||||
let keeper = PrivacyKeeper(keychain: keychainMock)
|
||||
|
||||
do {
|
||||
try keeper.update(password: password)
|
||||
XCTAssertTrue(keeper.accept(password: password), "Incorrect password")
|
||||
|
||||
keeper.update(privateKey: privateKey,
|
||||
for: commonKey,
|
||||
password: password)
|
||||
let readPrivateKey = keeper.privateKey(for: commonKey, password: password)
|
||||
XCTAssertNotNil(readPrivateKey, "Error setting private key to keychain")
|
||||
XCTAssertEqual(readPrivateKey, privateKey, "Incorrect private key stored in keychain")
|
||||
|
||||
keeper.reset()
|
||||
let nilPrivateKey = keeper.privateKey(for: commonKey, password: password)
|
||||
XCTAssertNil(nilPrivateKey, "Incorrect private key reset")
|
||||
} catch {
|
||||
XCTFail(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
func testMigration() {
|
||||
let password = "0000"
|
||||
let privateKey = "privateKey1111"
|
||||
let privateKey2 = "privateKey2222"
|
||||
let migratedCommonKey = "accountName@owner.privateKey"
|
||||
let keyToMigrate = CommonKey("accountKey2@malinka.privateKey")
|
||||
let oldKey = CommonKey.key("accountName", suffix: "privateKey")
|
||||
let newKey = CommonKey.key("accountName@owner", suffix: "privateKey")
|
||||
let storage = ["accountName.privateKey.PWD": KeychainMock.Value(value: privateKey, password: password),
|
||||
"PrivacyKeeper.password.PWD": KeychainMock.Value(value: password, password: password),
|
||||
"accountKey2@malinka.privateKey.PWD": KeychainMock.Value(value: privateKey2, password: password)]
|
||||
let keychainMock = KeychainMock(storage: storage)
|
||||
let keeper = PrivacyKeeper(keychain: keychainMock)
|
||||
keeper.migrate(oldKey: oldKey, newKey: newKey, password: password)
|
||||
XCTAssertNil(keeper.privateKey(for: oldKey.rawValue, password: password), "Incorrect private key")
|
||||
XCTAssertEqual(keeper.privateKey(for: migratedCommonKey, password: password), privateKey, "Incorrect private key")
|
||||
|
||||
keeper.migrate(oldKey: keyToMigrate, newKey: keyToMigrate, password: password)
|
||||
XCTAssertEqual(keeper.privateKey(for: keyToMigrate.rawValue, password: password), privateKey2, "Incorrect private key")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletNetwork",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletNetwork",
|
||||
targets: ["WalletNetwork"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(name: "eosswift", path: "../../Vendors/spm/eos-swift"),
|
||||
.package(name: "EosioSwift", path: "../Vendors/spm/eosio-swift-1.0.0"),
|
||||
.package(url: "https://github.com/Alamofire/Alamofire.git", .upToNextMajor(from: "5.6.2")),
|
||||
.package(name: "WalletFoundation", path: "../WalletFoundation"),
|
||||
.package(name: "Mocker", path: "../../Vendors/spm/Mocker-3.0.1")
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletNetwork",
|
||||
dependencies: ["EosioSwift", "eosswift", "Alamofire", "WalletFoundation"],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletNetworkTests",
|
||||
dependencies: ["WalletNetwork", "Mocker"],
|
||||
resources: [
|
||||
.process("Resources")
|
||||
])
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// AccountHyperionService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 12/4/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import WalletFoundation
|
||||
|
||||
public struct AccountHyperionService: NetworkService {
|
||||
|
||||
private struct Response: Codable {
|
||||
let accountNames: [String]
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accountNames = "account_names"
|
||||
}
|
||||
}
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
public func fetchNamesHyperion(publicKey: String) async -> [String] {
|
||||
|
||||
guard var components = URLComponents(url: self.environment.hyperion.url(), resolvingAgainstBaseURL: false) else {
|
||||
return []
|
||||
}
|
||||
|
||||
components.path = "/v2/state/get_key_accounts"
|
||||
components.queryItems = [URLQueryItem(name: "public_key", value: publicKey),
|
||||
URLQueryItem(name: "skip", value: "0"),
|
||||
URLQueryItem(name: "limit", value: "5000")]
|
||||
|
||||
guard let url = components.url else { return [] }
|
||||
|
||||
let response = try? await AF.request(
|
||||
url,
|
||||
method: .get,
|
||||
headers: self.environment.usernames.httpHeaders)
|
||||
.serializingDecodable(Response.self)
|
||||
.value
|
||||
|
||||
guard let response = response else { return [] }
|
||||
return response.accountNames
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
//
|
||||
// AccountService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import WalletFoundation
|
||||
|
||||
public struct AccountService: NetworkService {
|
||||
|
||||
public enum ServiceError: Error {
|
||||
case notificationEmptyAccount
|
||||
case notificationEmptyToken
|
||||
case notificationEmptyLanguage
|
||||
}
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
let session: Session
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
if let configuration = environment.configuration {
|
||||
configuration.headers = HTTPHeaders.default
|
||||
self.session = Session(configuration: configuration)
|
||||
} else {
|
||||
self.session = AF
|
||||
}
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
public func create(username: String, pubKey: String, deviceId: String) async throws -> WalletOrder {
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let variables = AccountCreateRequest.Variables(username: username, pubKey: pubKey, deviceId: deviceId)
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: AccountCreateRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
.responseString { print(">>>>>> \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<AccountCreateResponse>.self, decoder: decoder)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(response): return response.order
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
public func order(with id: String) async throws -> WalletOrder {
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ"
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(dateFormatter)
|
||||
|
||||
let variables = AccountOrderRequest.Variables(uid: id)
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: AccountOrderRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
.responseString { print(">>>>>> \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<AccountOrderResponse>.self, decoder: decoder)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(response): return response.order
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
public enum AvailableType {
|
||||
case available
|
||||
case alreadyTaken
|
||||
case system
|
||||
}
|
||||
|
||||
public func isAvailable(username: String) async -> AvailableType {
|
||||
|
||||
guard !username.isEmpty else { return .system }
|
||||
|
||||
let variables = AccountCheckNameRequest.Variables(username: username)
|
||||
|
||||
let response = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: AccountCheckNameRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders)
|
||||
.responseString { print("checkWalletName: \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<AccountCheckNameResponse>.self)
|
||||
.value
|
||||
|
||||
guard let response = response else { return .system }
|
||||
switch response.result {
|
||||
case .firstType: return .available
|
||||
case let .secondType(type):
|
||||
switch type {
|
||||
case let .application(error: errorType) where errorType == "ALREADY_TAKEN":
|
||||
return .alreadyTaken
|
||||
default: return .system
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func updateNotification(token: String, accounts: [String], language: String) async throws {
|
||||
|
||||
if accounts.isEmpty { throw ServiceError.notificationEmptyAccount }
|
||||
|
||||
if token.isEmpty { throw ServiceError.notificationEmptyToken }
|
||||
|
||||
if language.isEmpty { throw ServiceError.notificationEmptyLanguage }
|
||||
|
||||
let variables = AccountNotificationTokenRequest.Variables(
|
||||
deviceToken: token,
|
||||
deviceType: "IOS",
|
||||
eosAccounts: accounts,
|
||||
langCode: language,
|
||||
application: "MALINKA"
|
||||
)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.backend.url,
|
||||
method: .post,
|
||||
parameters: AccountNotificationTokenRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.backend.httpHeaders)
|
||||
// .responseString { print("updateNotification: \($0)") }
|
||||
.serializingDecodable(GraphQLResult<AccountNotificationTokenResponse>.self)
|
||||
.value
|
||||
|
||||
print(result ?? "")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//
|
||||
// AccountNodeService.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 16.01.2023.
|
||||
//
|
||||
|
||||
import Alamofire
|
||||
import Foundation
|
||||
|
||||
public struct NodeService: NetworkService {
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
let session: Session
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
if let configuration = environment.configuration {
|
||||
configuration.headers = HTTPHeaders.default
|
||||
self.session = Session(configuration: configuration)
|
||||
} else {
|
||||
self.session = AF
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchActions(account: String,
|
||||
limit: Int,
|
||||
skip: Int? = nil) async throws -> [NodeAction] {
|
||||
guard let url = URL(string: "https://eos.greymass.com") else { return [] } //self.environment.hyperion.url()
|
||||
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { return [] }
|
||||
|
||||
components.path = "/v1/history/get_actions"
|
||||
components.queryItems = [URLQueryItem(name: "account_name", value: account),
|
||||
URLQueryItem(name: "offset", value: "\(-limit)")]
|
||||
if let skip {
|
||||
components.queryItems?.append(URLQueryItem(name: "pos", value: "\(skip)"))
|
||||
}
|
||||
|
||||
do {
|
||||
let result = try await self.session.request(
|
||||
components,
|
||||
method: .get
|
||||
).serializingDecodable(NodeResponse.self)
|
||||
.value
|
||||
return result.actions
|
||||
} catch {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// AccountCheckNameRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 11.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AccountCheckNameRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let username: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "checkWalletName"
|
||||
let query: String = #"""
|
||||
query checkWalletName(
|
||||
$username: String!
|
||||
) {
|
||||
checkWalletName(username: $username) { errors }
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct AccountCheckNameResponse: GraphQLResponse {
|
||||
static let node = "checkWalletName"
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// AccountCreateRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 24.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AccountCreateRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let username: String
|
||||
let pubKey: String
|
||||
let deviceId: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "CreateAccount"
|
||||
let query: String = #"""
|
||||
mutation CreateAccount($username: String!, $pubKey: String!, $deviceId: String!) {
|
||||
createAccount(username: $username, pubKey: $pubKey, deviceId: $deviceId) {
|
||||
errors, order { timeout, id, modified, status }, id
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct AccountCreateResponse: GraphQLResponse {
|
||||
|
||||
static let node = "createAccount"
|
||||
|
||||
let id: Int
|
||||
let order: WalletOrder
|
||||
}
|
||||
+19
-11
@@ -1,24 +1,25 @@
|
||||
//
|
||||
// AddPushNotificationDeviceTokenGraphQLRequest.swift
|
||||
// PayCash
|
||||
// AccountNotificationTokenRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Saveliy Stavitsky on 7/30/21.
|
||||
// Copyright © 2021 AM. All rights reserved.
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
struct UpdatePushNotificationDeviceTokenGraphQLRequest: Encodable {
|
||||
|
||||
struct ResponseData: Decodable { }
|
||||
|
||||
struct AccountNotificationTokenRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let deviceToken: String
|
||||
let deviceType: String
|
||||
let eosAccounts: [String]
|
||||
let langCode: String
|
||||
let application: String
|
||||
}
|
||||
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "updatePushNotificationDeviceToken"
|
||||
let query: String = #"""
|
||||
@@ -27,15 +28,22 @@ struct UpdatePushNotificationDeviceTokenGraphQLRequest: Encodable {
|
||||
$deviceType: DeviceTypeEnum,
|
||||
$eosAccounts: [String]!,
|
||||
$langCode: Lang!
|
||||
$application: ApplicationEnum
|
||||
) {
|
||||
updatePushNotificationDeviceToken(
|
||||
deviceToken: $deviceToken,
|
||||
deviceType: $deviceType,
|
||||
eosAccounts: $eosAccounts,
|
||||
langCode: $langCode
|
||||
langCode: $langCode,
|
||||
application: $application
|
||||
) {
|
||||
errors
|
||||
}
|
||||
}
|
||||
"""#
|
||||
"""#.trimCompact()
|
||||
|
||||
}
|
||||
|
||||
struct AccountNotificationTokenResponse: GraphQLResponse {
|
||||
static let node = "updatePushNotificationDeviceToken"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// AccountOrderRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 29.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct AccountOrderRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let uid: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "accountOrder"
|
||||
let query: String = #"""
|
||||
query accountOrder(
|
||||
$uid: String!
|
||||
) {
|
||||
accountOrder(uid: $uid) { timeout, id, modified, status, created }
|
||||
}
|
||||
"""#.trimCompact()
|
||||
|
||||
}
|
||||
|
||||
|
||||
struct AccountOrderResponse: GraphQLResponse {
|
||||
|
||||
private enum Common: String, CodingKey {
|
||||
case order = "accountOrder"
|
||||
}
|
||||
|
||||
static let node = ""
|
||||
|
||||
let order: WalletOrder
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: Common.self)
|
||||
self.order = try container.decode(WalletOrder.self, forKey: .order)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 8/28/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct WalletOrder: Codable {
|
||||
|
||||
public enum Status: String, Codable {
|
||||
/// Order created.
|
||||
case active = "ACTIVE"
|
||||
/// Order expired.
|
||||
case expired = "EXPIRED"
|
||||
/// Sent to blockchain.
|
||||
case executed = "EXECUTED"
|
||||
/// Accepting EOS transaction. Already sent, but not all blocks created.
|
||||
case creating = "TRX_IN_CHAIN"
|
||||
/// Failed.
|
||||
case failed = "FAILED"
|
||||
/// Created and completed.
|
||||
case completed = "COMPLETED"
|
||||
}
|
||||
|
||||
public let timeout: Date
|
||||
public let modified: Date
|
||||
public let status: Status
|
||||
public let id: String
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// NodeAct.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeAct: Decodable {
|
||||
public let data: NodeData
|
||||
public let name: String
|
||||
public let authorization: [NodeActAuthorization]
|
||||
public let account: String
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// NodeActAuthorization.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeActAuthorization: Codable {
|
||||
public let actor: String
|
||||
public let permission: String
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// NodeAction.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeAction: Decodable {
|
||||
public let accountActionSeq: Int
|
||||
public let actionTrace: NodeActionTrace
|
||||
public let globalActionSeq: Int
|
||||
public let irreversible: Bool
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accountActionSeq = "account_action_seq"
|
||||
case actionTrace = "action_trace"
|
||||
case globalActionSeq = "global_action_seq"
|
||||
case irreversible
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// NodeActionTrace.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 11.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeActionTrace: Decodable {
|
||||
public let act: NodeAct
|
||||
public let blockNum: Int
|
||||
public let blockTime: String
|
||||
public let receiver: String?
|
||||
public let trxId: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case blockNum = "block_num"
|
||||
case blockTime = "block_time"
|
||||
case trxId = "trx_id"
|
||||
case receiver
|
||||
case act
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// NodeData.swift
|
||||
//
|
||||
// Created by NUT.TECH on 10.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeData: Decodable {
|
||||
public let data: [String: Any]
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Create a decoding container using DynamicCodingKeys
|
||||
// The container will contain all the JSON first level key
|
||||
let container = try decoder.container(keyedBy: DynamicCodingKeys.self)
|
||||
|
||||
var tempData = [String: Any]()
|
||||
|
||||
// Loop through each key
|
||||
for key in container.allKeys {
|
||||
if let value = try? container.decode(NodeData.self, forKey: key) {
|
||||
tempData[key.stringValue] = value.data
|
||||
} else if let value = try? container.decode(String.self, forKey: key) {
|
||||
tempData[key.stringValue] = value
|
||||
} else if let value = try? container.decode(Int.self, forKey: key) {
|
||||
tempData[key.stringValue] = value
|
||||
}
|
||||
}
|
||||
|
||||
self.data = tempData
|
||||
}
|
||||
|
||||
private struct DynamicCodingKeys: CodingKey {
|
||||
// Use for string-keyed dictionary
|
||||
var stringValue: String
|
||||
init?(stringValue: String) {
|
||||
self.stringValue = stringValue
|
||||
}
|
||||
|
||||
// Use for integer-keyed dictionary
|
||||
var intValue: Int?
|
||||
init?(intValue: Int) {
|
||||
// We are not using this, thus just return nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// NodeResponse.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 11.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct NodeResponse: Decodable {
|
||||
public let actions: [NodeAction]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// CommonEnvironment.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 04.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct CommonEnvironment: NetworkEnvironment {
|
||||
|
||||
public let configuration: URLSessionConfiguration?
|
||||
public let usernames: NetworkStaticAPIRecord
|
||||
public let hyperion: NetworkDynamicAPIRecord
|
||||
public let backend: NetworkStaticAPIRecord
|
||||
public let node: NetworkDynamicAPIRecord
|
||||
|
||||
public init(usernames: NetworkStaticAPIRecord,
|
||||
backend: NetworkStaticAPIRecord,
|
||||
hyperion: NetworkDynamicAPIRecord,
|
||||
node: NetworkDynamicAPIRecord) {
|
||||
self.configuration = nil
|
||||
self.usernames = usernames
|
||||
self.backend = backend
|
||||
self.hyperion = hyperion
|
||||
self.node = node
|
||||
}
|
||||
|
||||
public init(usernames: URL,
|
||||
backend: URL,
|
||||
hyperion: @escaping NetworkDynamicAPIRecord.Dynamic,
|
||||
node: @escaping NetworkDynamicAPIRecord.Dynamic,
|
||||
headers: [String: String] = [:]) {
|
||||
let userRecord = NetworkStaticAPIRecord(url: usernames, headers: headers)
|
||||
let backendRecord = NetworkStaticAPIRecord(url: backend, headers: headers)
|
||||
let hyperionRecord = NetworkDynamicAPIRecord(url: hyperion, headers: headers)
|
||||
let nodeRecord = NetworkDynamicAPIRecord(url: node, headers: headers)
|
||||
|
||||
self.init(usernames: userRecord, backend: backendRecord, hyperion: hyperionRecord, node: nodeRecord)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
//
|
||||
// GraphQLResult.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletFoundation
|
||||
|
||||
protocol GraphQLResponse: Decodable {
|
||||
static var node: String { get }
|
||||
}
|
||||
|
||||
struct GraphQLError: Decodable {
|
||||
let message: String
|
||||
}
|
||||
|
||||
enum GraphQLResponseError: Decodable {
|
||||
|
||||
enum Place: CustomStringConvertible {
|
||||
case node
|
||||
case failed
|
||||
|
||||
var description: String {
|
||||
switch self {
|
||||
case .node: return "node"
|
||||
case .failed: return "failed"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case system(errors: [GraphQLError])
|
||||
case application(error: String)
|
||||
case decode(Place)
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
self = .system(errors: [])
|
||||
}
|
||||
|
||||
// MARK: -
|
||||
|
||||
var networkServiceError: NetworkServiceError {
|
||||
switch self {
|
||||
case let .system(errors: errors): return .gqlSystem(errors.map { $0.message })
|
||||
case let .application(error: error): return .gqlApplication(error)
|
||||
case let .decode(place): return .gqlDecode(place.description)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let KEY_ERRORS = "errors"
|
||||
|
||||
struct GraphQLResult<Resp: GraphQLResponse>: Decodable {
|
||||
|
||||
let result: Either<Resp, GraphQLResponseError>
|
||||
|
||||
private enum Common: String, CodingKey {
|
||||
case data
|
||||
case errors
|
||||
}
|
||||
|
||||
private struct DynamicCodingKeys: CodingKey {
|
||||
var stringValue: String
|
||||
var intValue: Int?
|
||||
|
||||
init?(stringValue: String) { self.stringValue = stringValue }
|
||||
init?(intValue: Int) { return nil }
|
||||
}
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.container(keyedBy: Common.self)
|
||||
// System error catch
|
||||
if let errors = try? container.decode([GraphQLError].self, forKey: .errors) {
|
||||
self.result = .secondType(.system(errors: errors))
|
||||
return
|
||||
}
|
||||
|
||||
// If node is empty
|
||||
guard !Resp.node.isEmpty else {
|
||||
if let response = try? container.decode(Resp.self, forKey: .data) {
|
||||
self.result = .firstType(response)
|
||||
} else {
|
||||
self.result = .secondType(.decode(.failed))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
let dataContainer = try container.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: .data)
|
||||
|
||||
guard let key = DynamicCodingKeys(stringValue: Resp.node) else {
|
||||
self.result = .secondType(.decode(.node))
|
||||
return
|
||||
}
|
||||
|
||||
if let responseContainer = try? dataContainer.nestedContainer(keyedBy: DynamicCodingKeys.self, forKey: key),
|
||||
let errorKey = DynamicCodingKeys(stringValue: KEY_ERRORS),
|
||||
let error = try? responseContainer.decode(String.self, forKey: errorKey) {
|
||||
self.result = .secondType(.application(error: error))
|
||||
} else if let response = try? dataContainer.decode(Resp.self, forKey: key) {
|
||||
self.result = .firstType(response)
|
||||
} else {
|
||||
self.result = .secondType(.decode(.failed))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
//
|
||||
// NetworkService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 04.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
public struct NetworkStaticAPIRecord {
|
||||
|
||||
public typealias Headers = [String: String]
|
||||
|
||||
public let url: URL
|
||||
public let headers: Headers?
|
||||
|
||||
public init(url: URL, headers: Headers?) {
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
var httpHeaders: HTTPHeaders? {
|
||||
guard let headers = self.headers else { return nil }
|
||||
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
|
||||
}
|
||||
}
|
||||
|
||||
public struct NetworkDynamicAPIRecord {
|
||||
|
||||
public typealias Headers = [String: String]
|
||||
public typealias Dynamic = () -> URL
|
||||
|
||||
public let url: Dynamic
|
||||
public let headers: Headers?
|
||||
|
||||
public init(url: @escaping Dynamic, headers: Headers?) {
|
||||
self.url = url
|
||||
self.headers = headers
|
||||
}
|
||||
|
||||
var httpHeaders: HTTPHeaders? {
|
||||
guard let headers = self.headers else { return nil }
|
||||
return HTTPHeaders(headers.compactMap { key, value in HTTPHeader(name: key, value: value) })
|
||||
}
|
||||
}
|
||||
|
||||
public protocol NetworkEnvironment {
|
||||
var configuration: URLSessionConfiguration? { get }
|
||||
var usernames: NetworkStaticAPIRecord { get }
|
||||
var backend: NetworkStaticAPIRecord { get }
|
||||
var hyperion: NetworkDynamicAPIRecord { get }
|
||||
var node: NetworkDynamicAPIRecord { get }
|
||||
|
||||
func isEquals(other: NetworkEnvironment) -> Bool
|
||||
}
|
||||
|
||||
public protocol NetworkService {
|
||||
init(environment: NetworkEnvironment)
|
||||
}
|
||||
|
||||
public enum NetworkServiceError: Error {
|
||||
case invalidUrl
|
||||
case gqlSystem([String])
|
||||
case gqlApplication(String)
|
||||
case gqlDecode(String)
|
||||
case uncatched
|
||||
}
|
||||
|
||||
// MARK: - Equatable
|
||||
|
||||
extension NetworkEnvironment {
|
||||
public func isEquals(other: NetworkEnvironment) -> Bool {
|
||||
return self.configuration == other.configuration &&
|
||||
self.usernames.url == other.usernames.url &&
|
||||
self.usernames.headers == other.usernames.headers &&
|
||||
self.backend.url == other.backend.url &&
|
||||
self.backend.headers == other.backend.headers &&
|
||||
self.hyperion.url() == other.hyperion.url() &&
|
||||
self.hyperion.headers == other.hyperion.headers &&
|
||||
self.node.url() == other.node.url() &&
|
||||
self.node.headers == other.node.headers
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
//
|
||||
// DeviceService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
|
||||
public struct DeviceService: NetworkService {
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
let session: Session
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
if let configuration = environment.configuration {
|
||||
configuration.headers = HTTPHeaders.default
|
||||
self.session = Session(configuration: configuration)
|
||||
} else {
|
||||
self.session = AF
|
||||
}
|
||||
}
|
||||
|
||||
public func createDevice(uuid: String) async throws -> Device {
|
||||
|
||||
let variables = RegisterDeviceRequest.Variables(cid: uuid)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: RegisterDeviceRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
// .responseString { print($0) }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<RegisterDeviceResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(device): return Device(uuid: uuid, id: device.uid, isTrusted: device.isTrustedNow)
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
public func stateDevice(id: String) async throws -> StateDevice {
|
||||
|
||||
let variables = StateDeviceRequest.Variables(uid: id)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: StateDeviceRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
// .responseString { print(">>>>>> \($0)") }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<StateDeviceResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(state): return StateDevice(availableAccounts: state.availableAccounts,
|
||||
isTrusted: state.isTrustedNow)
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func checkDevice(id: String, token: String) async throws -> DeviceStatus {
|
||||
|
||||
let variables = CheckDeviceRequest.Variables(uid: id, token: token)
|
||||
|
||||
let result = try? await self.session.request(
|
||||
self.environment.usernames.url,
|
||||
method: .post,
|
||||
parameters: CheckDeviceRequest(variables: variables),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.usernames.httpHeaders
|
||||
)
|
||||
// .responseString { print($0) }
|
||||
// .cURLDescription { print($0) }
|
||||
.serializingDecodable(GraphQLResult<CheckDeviceResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(status): return status.status
|
||||
case let .secondType(error): throw error.networkServiceError
|
||||
default: throw NetworkServiceError.uncatched
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// CheckDeviceRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 24.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct CheckDeviceRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let uid: String
|
||||
let token: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let operationName = "CheckDevice"
|
||||
let query: String = #"""
|
||||
mutation CheckDevice($uid: String!, $token: String!) {
|
||||
checkDevice(uid: $uid, token: $token) {
|
||||
status
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
let variables: Variables
|
||||
}
|
||||
|
||||
struct CheckDeviceResponse: GraphQLResponse {
|
||||
|
||||
static let node = "checkDevice"
|
||||
|
||||
let status: DeviceStatus
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// RegisterDeviceRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 04.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct RegisterDeviceRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let cid: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let operationName = "RegisterDevice"
|
||||
let query: String = #"""
|
||||
mutation RegisterDevice($cid: String) {
|
||||
registerDevice(kind: IOS, cid: $cid) {
|
||||
isTrustedNow
|
||||
uid
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
let variables: Variables
|
||||
}
|
||||
|
||||
struct RegisterDeviceResponse: GraphQLResponse {
|
||||
|
||||
static let node = "registerDevice"
|
||||
|
||||
let isTrustedNow: Bool
|
||||
let uid: String
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// StateDeviceRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 29.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct StateDeviceRequest: Encodable {
|
||||
|
||||
struct Variables: Encodable {
|
||||
let uid: String
|
||||
}
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let variables: Variables
|
||||
let operationName = "device"
|
||||
let query: String = #"""
|
||||
query device(
|
||||
$uid: String!
|
||||
) {
|
||||
device(uid: $uid) { availableAccounts, isTrustedNow }
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct StateDeviceResponse: GraphQLResponse {
|
||||
static let node = "device"
|
||||
|
||||
let availableAccounts: Int
|
||||
let isTrustedNow: Bool
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Device.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
/// Model represent device
|
||||
public struct Device {
|
||||
/// Device generated Identity UUID
|
||||
public let uuid: String
|
||||
/// Server generated Identity
|
||||
public let id: String
|
||||
/// Is device trusted
|
||||
public let isTrusted: Bool
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// DeviceStatus.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 23.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum DeviceStatus: String, Decodable {
|
||||
case valid = "Valid"
|
||||
case invalid = "Invalid"
|
||||
case unexpected = "UnexpectedError"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// StateDevice.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 29.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct StateDevice {
|
||||
public let availableAccounts: Int
|
||||
public let isTrusted: Bool
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// EOSService.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 31.07.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Alamofire
|
||||
import WalletFoundation
|
||||
import EosioSwift
|
||||
import eosswift
|
||||
|
||||
public struct EOSService: NetworkService {
|
||||
|
||||
let environment: NetworkEnvironment
|
||||
|
||||
public init(environment: NetworkEnvironment) {
|
||||
self.environment = environment
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
public func requireNodes() async -> [String] {
|
||||
|
||||
let result = try? await AF.request(
|
||||
self.environment.backend.url,
|
||||
method: .post,
|
||||
parameters: EOSNodeRequest(),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.backend.httpHeaders)
|
||||
// .responseString(completionHandler: { print(">>>~~~>>> \($0)") })
|
||||
.serializingDecodable(GraphQLResult<EOSNodesResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(value):
|
||||
return value.nodes.map { $0.url }
|
||||
default:
|
||||
return []
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public func requireHyperions() async -> [String] {
|
||||
|
||||
let result = try? await AF.request(
|
||||
self.environment.backend.url,
|
||||
method: .post,
|
||||
parameters: EOSHyperionsRequest(),
|
||||
encoder: JSONParameterEncoder.default,
|
||||
headers: self.environment.backend.httpHeaders)
|
||||
.serializingDecodable(GraphQLResult<EOSHyperionsResponse>.self)
|
||||
.value
|
||||
|
||||
switch result?.result {
|
||||
case let .firstType(value):
|
||||
return value.hyperions.map { $0.url }
|
||||
default:
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
public func getAccount(_ account: String, completion: @escaping (EosioResult<EosioRpcAccountResponse, EosioError>) -> Void) {
|
||||
EosioRpcProvider(endpoint: self.environment.node.url(), headers: self.environment.node.headers)
|
||||
.getAccount(requestParameters: EosioRpcAccountRequest(accountName: account), completion: completion)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// EOSHyperionsRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by NUT.Tech on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EOSHyperionsRequest: Encodable {
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let query: String = #"""
|
||||
query {
|
||||
getEOSHyperions {
|
||||
hyperions {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
}
|
||||
|
||||
struct EOSHyperionsResponse: GraphQLResponse {
|
||||
|
||||
static let node = "getEOSHyperions"
|
||||
|
||||
struct Hyperion: Decodable {
|
||||
let url: String
|
||||
}
|
||||
|
||||
let hyperions: [Hyperion]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// EOSNodesQueryRequest.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 01.08.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct EOSNodeRequest: Encodable {
|
||||
|
||||
// MARK: - GraphQL
|
||||
|
||||
let operationName = "getEOSNodes"
|
||||
let query: String = #"""
|
||||
query getEOSNodes {
|
||||
getEOSNodes {
|
||||
nodes {
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
"""#.trimCompact()
|
||||
let variables: [String: String]? = nil
|
||||
}
|
||||
|
||||
struct EOSNodesResponse: GraphQLResponse {
|
||||
|
||||
static let node = "getEOSNodes"
|
||||
|
||||
struct Node: Decodable {
|
||||
let url: String
|
||||
}
|
||||
|
||||
let nodes: [Node]
|
||||
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// DeviceServiceTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 13.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import XCTest
|
||||
import WalletFoundation
|
||||
@testable
|
||||
import WalletNetwork
|
||||
import Mocker
|
||||
|
||||
final class DeviceServiceTests: XCTestCase {
|
||||
|
||||
override func tearDown() {
|
||||
Mocker.removeAll()
|
||||
}
|
||||
|
||||
// MARK: - CreateDevice
|
||||
|
||||
// When receive incorrect status we must throw uncatched exception.
|
||||
func testCreateDeviceFailUncatched() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/create")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 500,
|
||||
data: [ .post: Data() ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
_ = try await service.createDevice(uuid: uuid)
|
||||
XCTFail("Require to fail")
|
||||
} catch NetworkServiceError.uncatched {
|
||||
XCTAssertTrue(true)
|
||||
} catch {
|
||||
XCTFail("Require '\(NetworkServiceError.uncatched)' exception")
|
||||
}
|
||||
}
|
||||
|
||||
// When we receive incorrect data that we can't decode.
|
||||
func testCreateDeviceFailDecode() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/create")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.createDeviceDecodeFailed ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
_ = try await service.createDevice(uuid: uuid)
|
||||
} catch NetworkServiceError.gqlDecode(let message) {
|
||||
XCTAssertEqual(message, "failed")
|
||||
} catch {
|
||||
XCTFail("Catch exceprion '\(error)' but require NetworkServiceError.gqlDecode")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// When we successed create device.
|
||||
func testCreateDeviceSuccessTrusted() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/create")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.createDeviceTrusted ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let device = try await service.createDevice(uuid: uuid)
|
||||
XCTAssertTrue(device.isTrusted)
|
||||
XCTAssertEqual(device.id, "a88c4532a09024c245e9")
|
||||
XCTAssertEqual(device.uuid, uuid)
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Test send correct request.
|
||||
func testCreateDeviceSuccessRequest() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let uuid = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
let requestExpectation = XCTestExpectation(description: "Request")
|
||||
let completionExpectation = XCTestExpectation(description: "Completion")
|
||||
|
||||
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
|
||||
|
||||
XCTAssertEqual(url, mock.request.url)
|
||||
XCTAssertEqual(request.method, .post)
|
||||
|
||||
XCTAssertNotNil(object)
|
||||
|
||||
let registerDevice = RegisterDeviceRequest(variables: RegisterDeviceRequest.Variables(cid: uuid))
|
||||
|
||||
XCTAssertNotNil(object!["query"])
|
||||
object!["query"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, registerDevice.query)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["operationName"])
|
||||
object!["operationName"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, registerDevice.operationName)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["variables"])
|
||||
object!["variables"]!.map(firstTypeTransform: {
|
||||
XCTFail("Require '[String: String]' type but receive \($0)")
|
||||
}, secondTypeTransform: {
|
||||
XCTAssertNotNil($0["cid"])
|
||||
XCTAssertEqual($0["cid"], registerDevice.variables.cid)
|
||||
})
|
||||
|
||||
requestExpectation.fulfill()
|
||||
}
|
||||
mock.completion = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
|
||||
_ = try? await service.createDevice(uuid: uuid)
|
||||
|
||||
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
|
||||
|
||||
}
|
||||
|
||||
// MARK: - StateDevice
|
||||
|
||||
// Receive null values in fields - we must throw exception.
|
||||
func testQueryStateDeviceException() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.queryDeviceStateNull ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
_ = try await service.stateDevice(id: id)
|
||||
XCTFail("Require catch exceprion NetworkServiceError.gqlDecode")
|
||||
} catch {
|
||||
XCTAssertTrue(true)
|
||||
}
|
||||
}
|
||||
|
||||
// Receive untrusted status.
|
||||
func testQueryStateDeviceUntrusted() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.queryDeviceStateUntrusted ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let state = try await service.stateDevice(id: id)
|
||||
XCTAssertFalse(state.isTrusted)
|
||||
XCTAssertEqual(state.availableAccounts, 0)
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Receive trusted status.
|
||||
func testQueryStateDeviceTrusted() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .post: DeviceServiceData.queryDeviceStateTrusted ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let state = try await service.stateDevice(id: id)
|
||||
XCTAssertTrue(state.isTrusted)
|
||||
XCTAssertEqual(state.availableAccounts, 2)
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Check sent correct request.
|
||||
func testQueryStateDeviceRequest() async {
|
||||
|
||||
let url = URL(string: "https://malinka.life/query")!
|
||||
let id = "1234-0987-4567"
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubUsernames(configuration: configuration, url: url)
|
||||
let service = DeviceService(environment: env)
|
||||
|
||||
let requestExpectation = XCTestExpectation(description: "Request")
|
||||
let completionExpectation = XCTestExpectation(description: "Completion")
|
||||
|
||||
var mock = Mock(url: url, dataType: .json, statusCode: 200, data: [.post: Data()])
|
||||
mock.onRequestHandler = OnRequestHandler(httpBodyType: [String: Either<String, [String: String]>].self) { request, object in
|
||||
|
||||
XCTAssertEqual(url, mock.request.url)
|
||||
XCTAssertEqual(request.method, .post)
|
||||
|
||||
XCTAssertNotNil(object)
|
||||
|
||||
let stateRequest = StateDeviceRequest(variables: StateDeviceRequest.Variables(uid: id))
|
||||
|
||||
XCTAssertNotNil(object!["query"])
|
||||
object!["query"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, stateRequest.query)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["operationName"])
|
||||
object!["operationName"]!.map(firstTypeTransform: {
|
||||
XCTAssertEqual($0, stateRequest.operationName)
|
||||
}, secondTypeTransform: {
|
||||
XCTFail("Require 'String' type but receive \($0)")
|
||||
})
|
||||
|
||||
XCTAssertNotNil(object!["variables"])
|
||||
object!["variables"]!.map(firstTypeTransform: {
|
||||
XCTFail("Require '[String: String]' type but receive \($0)")
|
||||
}, secondTypeTransform: {
|
||||
XCTAssertNotNil($0["uid"])
|
||||
XCTAssertEqual($0["uid"], stateRequest.variables.uid)
|
||||
})
|
||||
|
||||
requestExpectation.fulfill()
|
||||
}
|
||||
mock.completion = {
|
||||
completionExpectation.fulfill()
|
||||
}
|
||||
mock.register()
|
||||
|
||||
_ = try? await service.stateDevice(id: id)
|
||||
|
||||
wait(for: [requestExpectation, completionExpectation], timeout: 2.0)
|
||||
}
|
||||
|
||||
// MARK: - CheckDevice
|
||||
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// DeviceServiceData.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 13.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
final class DeviceServiceData {
|
||||
|
||||
static let createDeviceTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessTrusted",
|
||||
withExtension: "json")!)
|
||||
static let createDeviceDecodeFailed = try! Data(contentsOf: Bundle.module.url(forResource: "CreateDeviceSuccessNull",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let queryDeviceStateNull = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessNull",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let queryDeviceStateUntrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessUntrusted",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let queryDeviceStateTrusted = try! Data(contentsOf: Bundle.module.url(forResource: "QueryDeviceSuccessTrusted",
|
||||
withExtension: "json")!)
|
||||
|
||||
static let nodeActionsExample = try! Data(contentsOf: Bundle.module.url(forResource: "v1historyTestmalinka1",
|
||||
withExtension: "json")!)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// NetworkEnvironmentImpl.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 13.01.2023.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import WalletNetwork
|
||||
|
||||
struct NetworkEnvironmentImpl: NetworkEnvironment {
|
||||
|
||||
let configuration: URLSessionConfiguration?
|
||||
let usernames: NetworkStaticAPIRecord
|
||||
let backend: NetworkStaticAPIRecord
|
||||
let hyperion: NetworkDynamicAPIRecord
|
||||
let node: NetworkDynamicAPIRecord
|
||||
|
||||
static func createStubUsernames(configuration: URLSessionConfiguration,
|
||||
url: URL,
|
||||
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
|
||||
let record = NetworkStaticAPIRecord(url: url, headers: headers)
|
||||
|
||||
let dumpUrl = URL(string: "Empty")!
|
||||
return NetworkEnvironmentImpl(configuration: configuration,
|
||||
usernames: record,
|
||||
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
|
||||
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
|
||||
node: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil))
|
||||
}
|
||||
|
||||
static func createStubNodes(configuration: URLSessionConfiguration,
|
||||
url: URL,
|
||||
headers: NetworkStaticAPIRecord.Headers? = nil) -> NetworkEnvironment {
|
||||
let record = NetworkDynamicAPIRecord(url: { url }, headers: headers)
|
||||
|
||||
let dumpUrl = URL(string: "Empty")!
|
||||
return NetworkEnvironmentImpl(configuration: configuration,
|
||||
usernames: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
|
||||
backend: NetworkStaticAPIRecord(url: dumpUrl, headers: nil),
|
||||
hyperion: NetworkDynamicAPIRecord(url: { dumpUrl }, headers: nil),
|
||||
node: record)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//
|
||||
// NodeActionModelsTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 13.01.2023.
|
||||
//
|
||||
|
||||
import WalletNetwork
|
||||
import Foundation
|
||||
import XCTest
|
||||
|
||||
final class NodeActionModelsTests: XCTestCase {
|
||||
|
||||
func testNodeActAuthorization() {
|
||||
let data = """
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
let nodeAct = try? JSONDecoder().decode(NodeActAuthorization.self, from: data)
|
||||
XCTAssertNotNil(nodeAct, "Error parsing NodeActAuthorization object")
|
||||
XCTAssertEqual(nodeAct?.actor, "testmalinka1", "Error parsing NodeActAuthorization.actor value")
|
||||
XCTAssertEqual(nodeAct?.permission, "owner", "Error parsing NodeActAuthorization.owner value")
|
||||
}
|
||||
|
||||
func testNodeData() {
|
||||
let data = """
|
||||
{
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let nodeData = try? JSONDecoder().decode(NodeData.self, from: data)
|
||||
XCTAssertNotNil(nodeData, "Error parsing NodeData object")
|
||||
guard let parsedData = nodeData?.data else {
|
||||
XCTFail("Error parsing data value")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(parsedData["from"] as? String, "testmalinka1", "Error parsing data value")
|
||||
XCTAssertEqual(parsedData["memo"] as? String, "buyram:testmalinka1", "Error parsing data value")
|
||||
XCTAssertEqual(parsedData["quantity"] as? String, "0.1000 EOS", "Error parsing data value")
|
||||
XCTAssertEqual(parsedData["to"] as? String, "malinkawallt", "Error parsing data value")
|
||||
}
|
||||
|
||||
func testNodeAct() {
|
||||
let data = """
|
||||
{
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
}
|
||||
""".data(using: .utf8)!
|
||||
|
||||
let nodeAct = try? JSONDecoder().decode(NodeAct.self, from: data)
|
||||
XCTAssertNotNil(nodeAct, "Error parsing NodeAct object")
|
||||
XCTAssertNotNil(nodeAct?.data, "Error parsing NodeAct.data object")
|
||||
XCTAssertNotNil(nodeAct?.authorization, "buyram:testmalinka1", file: "Error parsing NodeAct.authorization objects")
|
||||
XCTAssertEqual(nodeAct?.authorization.count, 1, "Error parsing NodeAct.authorization objects")
|
||||
XCTAssertEqual(nodeAct?.name, "transfer", "Error parsing NodeAct.name value")
|
||||
XCTAssertEqual(nodeAct?.account, "eosio.token", "Error parsing NodeAct.account value")
|
||||
}
|
||||
|
||||
func testNodeActionTrace() {
|
||||
guard let url = Bundle.module.url(forResource: "NodeActionTrace", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let result = try? JSONDecoder().decode(NodeActionTrace.self, from: data)
|
||||
else {
|
||||
XCTFail("Error reading JSON file")
|
||||
return
|
||||
}
|
||||
XCTAssertNotNil(result.act, "Error parsing NodeActionTrace.act object")
|
||||
XCTAssertEqual(result.blockNum, 288713481, "Error parsing NodeActionTrace.blockNum value")
|
||||
XCTAssertEqual(result.blockTime, "2023-01-12T15:27:01.500", "Error parsing NodeActionTrace.blockTime value")
|
||||
XCTAssertEqual(result.receiver, "malinkawallt", "Error parsing NodeActionTrace.receiver value")
|
||||
XCTAssertEqual(result.trxId, "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c", "Error parsing NodeActionTrace.trxId value")
|
||||
}
|
||||
|
||||
func testNodeAction() {
|
||||
guard let url = Bundle.module.url(forResource: "NodeAction", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let result = try? JSONDecoder().decode(NodeAction.self, from: data)
|
||||
else {
|
||||
XCTFail("Error reading JSON file")
|
||||
return
|
||||
}
|
||||
XCTAssertNotNil(result.actionTrace, "Error parsing NodeAction.actionTrace value")
|
||||
XCTAssertEqual(result.accountActionSeq, 2858, "Error parsing NodeAction.accountActionSeq value")
|
||||
XCTAssertEqual(result.globalActionSeq, 357585823156, "Error parsing NodeAction.globalActionSeq value")
|
||||
XCTAssertTrue(result.irreversible, "Error parsing NodeAction.irreversible value")
|
||||
}
|
||||
|
||||
func testNodeResponse() {
|
||||
guard let url = Bundle.module.url(forResource: "NodeResponse", withExtension: "json"),
|
||||
let data = try? Data(contentsOf: url),
|
||||
let result = try? JSONDecoder().decode(NodeResponse.self, from: data)
|
||||
else {
|
||||
XCTFail("Error reading JSON file")
|
||||
return
|
||||
}
|
||||
|
||||
XCTAssertNotNil(result.actions, "Error parsing NodeResponse")
|
||||
XCTAssertEqual(result.actions.count, 1, "Error parsing NodeResponse.actions")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// NodeServiceTests.swift
|
||||
//
|
||||
//
|
||||
// Created by Nut.Tech on 16.01.2023.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import WalletNetwork
|
||||
import Mocker
|
||||
|
||||
final class NodeServiceTests: XCTestCase {
|
||||
|
||||
override func tearDown() {
|
||||
Mocker.removeAll()
|
||||
}
|
||||
|
||||
func testFetchNodeActions() async {
|
||||
let accountName = "testmalinka3"
|
||||
let offset = -100
|
||||
let url = URL(string: "https://eos.greymass.com/v1/history/get_actions?account_name=\(accountName)&offset=\(offset)")!
|
||||
|
||||
let configuration = URLSessionConfiguration.af.default
|
||||
configuration.protocolClasses = [MockingURLProtocol.self]
|
||||
|
||||
let env = NetworkEnvironmentImpl.createStubNodes(configuration: configuration, url: url)
|
||||
let service = NodeService(environment: env)
|
||||
|
||||
Mock(url: url,
|
||||
ignoreQuery: true,
|
||||
dataType: .json,
|
||||
statusCode: 200,
|
||||
data: [ .get: DeviceServiceData.nodeActionsExample ]
|
||||
).register()
|
||||
|
||||
do {
|
||||
let state = try await service.fetchActions(account: accountName,
|
||||
limit: offset)
|
||||
guard let firstAction = state.first else {
|
||||
XCTFail("Error parsing NodeActions")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(state.count, 100, "Incorrect number of node actions")
|
||||
XCTAssertEqual(firstAction.accountActionSeq, 2759, "Error parsing NodeAction.accountActionSeq")
|
||||
XCTAssertEqual(firstAction.globalActionSeq, 357563493447, "Error parsing NodeAction.globalActionSeq")
|
||||
XCTAssertTrue(firstAction.irreversible, "Error parsing NodeAction.irreversible")
|
||||
XCTAssertNotNil(firstAction.actionTrace, "Error parsing NodeAction.actionTrace")
|
||||
XCTAssertEqual(firstAction.actionTrace.blockTime, "2023-01-09T13:55:06.000", "Error parsing NodeAction.actionTrace.blockTime")
|
||||
XCTAssertEqual(firstAction.actionTrace.blockNum, 288184258, "Error parsing NodeAction.actionTrace.blockNum")
|
||||
XCTAssertEqual(firstAction.actionTrace.trxId, "a4dfd44c82cf0a3be1930bf6e4ec6ea51c1b331a4e77864a0d2cae4d06e8683d", "Error parsing NodeAction.actionTrace.trxId")
|
||||
XCTAssertEqual(firstAction.actionTrace.receiver, "testmalinka1", "Error parsing NodeAction.actionTrace.receiver")
|
||||
let act = firstAction.actionTrace.act
|
||||
XCTAssertEqual(act.account, "eosio.token", "Error parsing NodeActionTrace.act.account")
|
||||
XCTAssertEqual(act.name, "transfer", "Error parsing NodeActionTrace.act.name")
|
||||
XCTAssertEqual(act.authorization.count, 1, "Error parsing NodeActionTrace.act.authorization")
|
||||
XCTAssertNotNil(act.data, "Error parsing NodeActionTrace.act.data")
|
||||
guard let authorization = act.authorization.first else {
|
||||
XCTFail("Error parsing NodeAction.actionTrace.act.authorization")
|
||||
return
|
||||
}
|
||||
XCTAssertEqual(authorization.permission, "owner", "Error parsing NodeActionTrace.act.authorization.permission")
|
||||
XCTAssertEqual(authorization.actor, "testmalinka1", "Error parsing NodeActionTrace.act.authorization.actor")
|
||||
} catch {
|
||||
XCTFail("Catch exceprion \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"data": {"registerDevice": {"isTrustedNow": false, "uid": null}}}
|
||||
+1
@@ -0,0 +1 @@
|
||||
{"data": {"registerDevice": {"isTrustedNow": true, "uid": "a88c4532a09024c245e9"}}}
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"account_action_seq": 2858,
|
||||
"action_trace": {
|
||||
"account_ram_deltas": [],
|
||||
"act": {
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
},
|
||||
"action_ordinal": 4,
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"closest_unnotified_ancestor_action_ordinal": 2,
|
||||
"context_free": false,
|
||||
"creator_action_ordinal": 2,
|
||||
"elapsed": 2,
|
||||
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
|
||||
"receipt": {
|
||||
"abi_sequence": 4,
|
||||
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
|
||||
"auth_sequence": [
|
||||
[
|
||||
"testmalinka1",
|
||||
2192
|
||||
]
|
||||
],
|
||||
"code_sequence": 4,
|
||||
"global_sequence": 357585823156,
|
||||
"receiver": "malinkawallt",
|
||||
"recv_sequence": 14905
|
||||
},
|
||||
"receiver": "malinkawallt",
|
||||
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
|
||||
},
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"global_action_seq": 357585823156,
|
||||
"irreversible": true
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"account_ram_deltas": [],
|
||||
"act": {
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
},
|
||||
"action_ordinal": 4,
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"closest_unnotified_ancestor_action_ordinal": 2,
|
||||
"context_free": false,
|
||||
"creator_action_ordinal": 2,
|
||||
"elapsed": 2,
|
||||
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
|
||||
"receipt": {
|
||||
"abi_sequence": 4,
|
||||
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
|
||||
"auth_sequence": [
|
||||
[
|
||||
"testmalinka1",
|
||||
2192
|
||||
]
|
||||
],
|
||||
"code_sequence": 4,
|
||||
"global_sequence": 357585823156,
|
||||
"receiver": "malinkawallt",
|
||||
"recv_sequence": 14905
|
||||
},
|
||||
"receiver": "malinkawallt",
|
||||
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"actions": [
|
||||
{
|
||||
"account_action_seq": 2858,
|
||||
"action_trace": {
|
||||
"account_ram_deltas": [],
|
||||
"act": {
|
||||
"account": "eosio.token",
|
||||
"authorization": [
|
||||
{
|
||||
"actor": "testmalinka1",
|
||||
"permission": "owner"
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"from": "testmalinka1",
|
||||
"memo": "buyram:testmalinka1",
|
||||
"quantity": "0.1000 EOS",
|
||||
"to": "malinkawallt"
|
||||
},
|
||||
"hex_data": "100c9c2e1a99b1ca906334dcc0e9a291e80300000000000004454f53000000001362757972616d3a746573746d616c696e6b6131",
|
||||
"name": "transfer"
|
||||
},
|
||||
"action_ordinal": 4,
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"closest_unnotified_ancestor_action_ordinal": 2,
|
||||
"context_free": false,
|
||||
"creator_action_ordinal": 2,
|
||||
"elapsed": 2,
|
||||
"producer_block_id": "11356b09099ac39c311d160b99824c40825a5b65f7ddc75fb1e4a5d4c57cde68",
|
||||
"receipt": {
|
||||
"abi_sequence": 4,
|
||||
"act_digest": "0d8c0d9d769f6b68d2b3b2cc1dd2b219eb1ddd7d52c83d221abd74541f6f687b",
|
||||
"auth_sequence": [
|
||||
[
|
||||
"testmalinka1",
|
||||
2192
|
||||
]
|
||||
],
|
||||
"code_sequence": 4,
|
||||
"global_sequence": 357585823156,
|
||||
"receiver": "malinkawallt",
|
||||
"recv_sequence": 14905
|
||||
},
|
||||
"receiver": "malinkawallt",
|
||||
"trx_id": "233cb97fcc9a6c8cc7943e021cd9705dd5ff7b82b1fa3a93afaf240f35f2c31c"
|
||||
},
|
||||
"block_num": 288713481,
|
||||
"block_time": "2023-01-12T15:27:01.500",
|
||||
"global_action_seq": 357585823156,
|
||||
"irreversible": true
|
||||
}
|
||||
],
|
||||
"head_block_num": 288838483,
|
||||
"last_irreversible_block": 288838158
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"device": {
|
||||
"availableAccounts": null,
|
||||
"isTrustedNow": null
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"device": {
|
||||
"availableAccounts": 2,
|
||||
"isTrustedNow": true
|
||||
}
|
||||
}
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"data": {
|
||||
"device": {
|
||||
"availableAccounts": 0,
|
||||
"isTrustedNow": false
|
||||
}
|
||||
}
|
||||
}
|
||||
+5276
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,30 @@
|
||||
// swift-tools-version:5.5
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "WalletUIComponents",
|
||||
platforms: [.iOS(.v13)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, and make them visible to other packages.
|
||||
.library(
|
||||
name: "WalletUIComponents",
|
||||
targets: ["WalletUIComponents"]),
|
||||
],
|
||||
dependencies: [ ],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
|
||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||
.target(
|
||||
name: "WalletUIComponents",
|
||||
dependencies: [ ],
|
||||
path: "./Sources"),
|
||||
.testTarget(
|
||||
name: "WalletUIComponentsTests",
|
||||
dependencies: ["WalletUIComponents"],
|
||||
path: "Tests"//, // Test files
|
||||
// resources: [.copy("TestData")] // The test data files, copy files without modifying them
|
||||
)
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// TextField.swift
|
||||
// Malinka
|
||||
//
|
||||
// Created by NUT.Tech on 08.02.2023.
|
||||
// Copyright © 2023 NUT.Tech. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Combine
|
||||
|
||||
/// Struct can be replaced to SwiftUI's native TextField after iOS 15.0.
|
||||
/// It was created for access to firstResponder logic which has no analogue
|
||||
/// until iOS 15.0 with @FocusState wrapper type and isFocused property of TextField.
|
||||
|
||||
public struct TextField: UIViewRepresentable {
|
||||
|
||||
@ObservedObject
|
||||
public var viewModel: TextFieldViewModel
|
||||
public let font: UIFont
|
||||
public let textColor: UIColor
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(viewModel: TextFieldViewModel, font: UIFont, textColor: UIColor) {
|
||||
self.viewModel = viewModel
|
||||
self.font = font
|
||||
self.textColor = textColor
|
||||
}
|
||||
|
||||
// MARK: - UIViewRepresentable
|
||||
|
||||
public func makeCoordinator() -> UITextFieldDelegate {
|
||||
TextFieldCoordinator(viewModel: self.viewModel)
|
||||
}
|
||||
|
||||
public func makeUIView(context: Context) -> UITextField {
|
||||
let view = UITextField()
|
||||
view.autocapitalizationType = .none
|
||||
view.autocorrectionType = .no
|
||||
view.clearButtonMode = .never
|
||||
view.font = font
|
||||
view.textColor = self.textColor
|
||||
view.keyboardType = self.viewModel.keyboardType
|
||||
view.placeholder = self.viewModel.placeholder
|
||||
view.addTarget(context.coordinator,
|
||||
action: #selector(TextFieldCoordinator.textViewDidChange),
|
||||
for: .editingChanged)
|
||||
view.delegate = context.coordinator
|
||||
return view
|
||||
}
|
||||
|
||||
public func updateUIView(_ uiView: UITextField, context: Context) {
|
||||
uiView.text = self.viewModel.text
|
||||
self.viewModel.isFirstResponder
|
||||
? uiView.becomeFirstResponder()
|
||||
: uiView.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// TextFieldCoordinator.swift
|
||||
// Malinka
|
||||
//
|
||||
// Created by NUT.Tech on 14.02.2023.
|
||||
// Copyright © 2023 NUT.Tech. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
final class TextFieldCoordinator: NSObject, UITextFieldDelegate {
|
||||
|
||||
private let viewModel: TextFieldViewModel
|
||||
|
||||
init(viewModel: TextFieldViewModel) {
|
||||
self.viewModel = viewModel
|
||||
}
|
||||
|
||||
@objc
|
||||
func textViewDidChange(_ textField: UITextField) {
|
||||
self.viewModel.text = textField.text ?? ""
|
||||
}
|
||||
|
||||
// MARK: - UITextFieldDelegate
|
||||
|
||||
func textFieldShouldBeginEditing(_ textField: UITextField) -> Bool {
|
||||
guard self.viewModel.isFirstResponder == textField.isFirstResponder else { return false }
|
||||
self.viewModel.isFirstResponder = true
|
||||
return true
|
||||
}
|
||||
|
||||
func textFieldShouldEndEditing(_ textField: UITextField) -> Bool {
|
||||
self.viewModel.isFirstResponder = false
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//
|
||||
// TextFieldViewModel.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 2/19/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Combine
|
||||
|
||||
public final class TextFieldViewModel: ObservableObject {
|
||||
|
||||
@Published
|
||||
public var text = ""
|
||||
|
||||
@Published
|
||||
public var isFirstResponder = false
|
||||
|
||||
@Published
|
||||
public var shouldClear: Bool = false
|
||||
|
||||
public let placeholder: String
|
||||
public let keyboardType: UIKeyboardType
|
||||
|
||||
public var isCloseButtonVisible: Bool { !self.text.isEmpty }
|
||||
|
||||
private var canclellables = Set<AnyCancellable>()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
public init(placeholder: String = "", keyboardType: UIKeyboardType = .default) {
|
||||
self.placeholder = placeholder
|
||||
self.keyboardType = keyboardType
|
||||
|
||||
self.$text
|
||||
.receive(on: DispatchQueue.main)
|
||||
.sink { [weak self] value in
|
||||
self?.shouldClear = value == ""
|
||||
}
|
||||
.store(in: &self.canclellables)
|
||||
}
|
||||
|
||||
// MARK: - Methods
|
||||
|
||||
public func clearButtonAction() {
|
||||
self.text = ""
|
||||
self.isFirstResponder = false
|
||||
self.shouldClear = false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Juraldinio on 2/16/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@@ -1,72 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>JMalinkaWallet.ipa</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>architectures</key>
|
||||
<array>
|
||||
<string>arm64</string>
|
||||
</array>
|
||||
<key>bitcode</key>
|
||||
<false/>
|
||||
<key>buildNumber</key>
|
||||
<string>12</string>
|
||||
<key>certificate</key>
|
||||
<dict>
|
||||
<key>SHA1</key>
|
||||
<string>35C003923C51D78DF01DBCBFF8DAC6666C09412D</string>
|
||||
<key>dateExpires</key>
|
||||
<string>8/4/22</string>
|
||||
<key>type</key>
|
||||
<string>iOS Distribution</string>
|
||||
</dict>
|
||||
<key>entitlements</key>
|
||||
<dict>
|
||||
<key>application-identifier</key>
|
||||
<string>GENPCTDS3G.com.juraldinio.wallet</string>
|
||||
<key>aps-environment</key>
|
||||
<string>production</string>
|
||||
<key>beta-reports-active</key>
|
||||
<true/>
|
||||
<key>com.apple.developer.associated-domains</key>
|
||||
<array>
|
||||
<string>applinks:paycashonline.test-app.link</string>
|
||||
<string>applinks:paycashonline-alternate.test-app.link</string>
|
||||
<string>applinks:paycashonline-alternate.app.link</string>
|
||||
<string>applinks:paycashonline.app.link</string>
|
||||
</array>
|
||||
<key>com.apple.developer.devicecheck.appattest-environment</key>
|
||||
<string>production</string>
|
||||
<key>com.apple.developer.team-identifier</key>
|
||||
<string>GENPCTDS3G</string>
|
||||
<key>get-task-allow</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>name</key>
|
||||
<string>JMalinkaWallet.app</string>
|
||||
<key>profile</key>
|
||||
<dict>
|
||||
<key>UUID</key>
|
||||
<string>215599e9-99cb-4fbc-832f-12be30a2bcb0</string>
|
||||
<key>dateExpires</key>
|
||||
<string>8/4/22</string>
|
||||
<key>name</key>
|
||||
<string>MalinkaWallet</string>
|
||||
</dict>
|
||||
<key>symbols</key>
|
||||
<true/>
|
||||
<key>team</key>
|
||||
<dict>
|
||||
<key>id</key>
|
||||
<string>GENPCTDS3G</string>
|
||||
<key>name</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
<key>versionNumber</key>
|
||||
<string>1.0.0</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,29 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>generateAppStoreInformation</key>
|
||||
<false/>
|
||||
<key>manageAppVersionAndBuildNumber</key>
|
||||
<true/>
|
||||
<key>method</key>
|
||||
<string>app-store</string>
|
||||
<key>provisioningProfiles</key>
|
||||
<dict>
|
||||
<key>com.juraldinio.wallet</key>
|
||||
<string>MalinkaWallet</string>
|
||||
</dict>
|
||||
<key>signingCertificate</key>
|
||||
<string>Apple Distribution</string>
|
||||
<key>signingStyle</key>
|
||||
<string>manual</string>
|
||||
<key>stripSwiftSymbols</key>
|
||||
<true/>
|
||||
<key>teamID</key>
|
||||
<string>GENPCTDS3G</string>
|
||||
<key>uploadBitcode</key>
|
||||
<false/>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,26 @@
|
||||
### Xcode ###
|
||||
.build/
|
||||
build/
|
||||
*.pbxuser
|
||||
!default.pbxuser
|
||||
*.mode1v3
|
||||
!default.mode1v3
|
||||
*.mode2v3
|
||||
!default.mode2v3
|
||||
*.perspectivev3
|
||||
!default.perspectivev3
|
||||
xcuserdata
|
||||
*.xccheckout
|
||||
*.moved-aside
|
||||
DerivedData
|
||||
*.xcuserstate
|
||||
timeline.xctimeline
|
||||
.swiftpm/xcode
|
||||
CryptoSwift.xcframework
|
||||
|
||||
/Framework
|
||||
.DS_Store
|
||||
Carthage/Build
|
||||
|
||||
.idea
|
||||
.vscode
|
||||
@@ -0,0 +1,5 @@
|
||||
Marcin Krzyzanowski <marcin.krzyzanowski@gmail.com> <758033+krzyzanowskim@users.noreply.github.com>
|
||||
Marcin Krzyzanowski <marcin.krzyzanowski@gmail.com> <krzyzanowskim@users.noreply.github.com>
|
||||
Marcin Krzyzanowski <marcin.krzyzanowski@gmail.com> <marcin@krzyzanowskim.com>
|
||||
Marcin Krzyzanowski <marcin.krzyzanowski@gmail.com> <marcin.krzyzanowski@gmail.com>
|
||||
Luis Reisewitz <reisewitz@me.com> <zweigraf@users.noreply.github.com>
|
||||
@@ -0,0 +1,36 @@
|
||||
|
||||
--exclude .build, Carthage, DerivedData, .git, Tests/LinuxMain.swift, Tests/CryptoSwiftTests/XCTestManifests.swift, Tests/TestsPerformance/XCTestManifests.swift
|
||||
|
||||
--swiftversion 5.0
|
||||
--allman false
|
||||
--commas always
|
||||
--comments indent
|
||||
--elseposition same-line
|
||||
--empty void
|
||||
--exponentcase lowercase
|
||||
--exponentgrouping disabled
|
||||
--fractiongrouping disabled
|
||||
--header ignore
|
||||
--octalgrouping 4,8
|
||||
--decimalgrouping 3,6
|
||||
--binarygrouping 4,8
|
||||
--hexgrouping ignore
|
||||
--hexliteralcase lowercase
|
||||
--ifdef indent
|
||||
--indent 2
|
||||
--indentcase true
|
||||
--importgrouping testable-bottom
|
||||
--linebreaks lf
|
||||
--operatorfunc spaced
|
||||
--patternlet inline
|
||||
--ranges no-space
|
||||
--self insert
|
||||
--semicolons inline
|
||||
--stripunusedargs closure-only
|
||||
--trimwhitespace always
|
||||
--wraparguments preserve
|
||||
--wrapcollections before-first
|
||||
|
||||
# rules
|
||||
|
||||
--rules indent, braces, isEmpty, redundantBreak, blankLinesAroundMark, blankLinesAtEndOfScope, blankLinesBetweenScopes, consecutiveBlankLines, consecutiveSpaces, duplicateImports, elseOnSameLine, leadingDelimiters, redundantBreak, redundantExtensionACL, redundantFileprivate, redundantGet, redundantInit, redundantLet, redundantNilInit, redundantObjc, redundantParens, redundantPattern, redundantRawValues, redundantReturn, redundantSelf, redundantVoidReturnType, semicolons, sortedImports, spaceAroundBraces, spaceAroundBrackets, spaceAroundComments, spaceAroundGenerics, spaceAroundOperators, spaceAroundParens, spaceInsideBraces, spaceInsideBrackets, specifiers, strongOutlets, strongifiedSelf, todos, void, wrapArguments, yodaConditions, trailingSpace
|
||||
@@ -0,0 +1,13 @@
|
||||
language: generic
|
||||
matrix:
|
||||
include:
|
||||
# Test Ubuntu Linux 14.04
|
||||
- os: linux
|
||||
dist: trusty
|
||||
sudo: required
|
||||
- os: osx
|
||||
osx_image: xcode11.4
|
||||
install:
|
||||
- eval "$(curl -sL https://swiftenv.fuller.li/install.sh)"
|
||||
script:
|
||||
- swift test -c release -Xswiftc -enable-testing -Xswiftc -DCI -Xswiftc -Xfrontend -Xswiftc -solver-memory-threshold -Xswiftc -Xfrontend -Xswiftc 999999999
|
||||
@@ -0,0 +1,342 @@
|
||||
1.5.1
|
||||
- Resolve type name clash by renaming BigInt -> BigInteger
|
||||
|
||||
1.5.0
|
||||
- RSA (@NathanFallet)
|
||||
- Workaround for Xcode 13.3.1
|
||||
|
||||
1.4.3
|
||||
- Fix PCBC mode.
|
||||
|
||||
1.4.2
|
||||
- Update Xcode project to Xcode 13
|
||||
- Add SHA3 support for HMAC
|
||||
- Update HMAC.Variant API (deprecate current cases)
|
||||
|
||||
1.4.1
|
||||
- Introduce ISO 10126 padding
|
||||
- fix various compiler warnings
|
||||
- Revert Xcode project deployment target
|
||||
|
||||
1.4.0
|
||||
- Customize CFB segment size (cfb8, cfb128).
|
||||
- Adapt Swift @inlineable for better code optimization
|
||||
|
||||
1.3.8
|
||||
- Revert xcframework revolution. Back to build from sources. (I'm sorry)
|
||||
|
||||
1.3.7
|
||||
- Re-release to workaround Swift Package Manager release
|
||||
|
||||
1.3.6
|
||||
- Fix macOS binary
|
||||
- Windows support
|
||||
|
||||
1.3.5
|
||||
- Re-release binary framework due to codesign issue
|
||||
|
||||
1.3.4
|
||||
- Distribute optimized binary (xcframework) via SPM for apple platforms
|
||||
|
||||
1.3.3
|
||||
- Adds OCB Authenticated-Encryption Algorithm (RFC7253)
|
||||
- build-framework.sh output CryptoSwift.xcframework
|
||||
- Xcode 12.2 maintenance updates
|
||||
- Removed CryptoSwift.playground (couldn't make it work since Xcode 12 update)
|
||||
|
||||
1.3.2
|
||||
- Swift 5.3 update (Xcode 12)
|
||||
- Bump target platform (iOS 9, macOS 10.12)
|
||||
- Allow CMAC with any Cipher
|
||||
- Remove CMAC key limit
|
||||
|
||||
1.3.1
|
||||
- Fix tests
|
||||
- Swift 5.2 update
|
||||
- Address possible timing issue
|
||||
|
||||
1.3.0
|
||||
- Adds ISO-78164 padding
|
||||
- Performance improvements
|
||||
- Swift 5.1 update
|
||||
|
||||
1.2.0
|
||||
- Performance improvements
|
||||
- Workaround Xcode test builds with Xcode 11
|
||||
|
||||
1.1.3
|
||||
- Fix build crash: https://bugs.swift.org/browse/SR-11630
|
||||
- Fixes Xcode project tests build
|
||||
- SwiftFormat all the things
|
||||
- Increase/fix SHA2 data length for big input by use Int64 for calculation
|
||||
|
||||
1.1.2
|
||||
- Fix Swift 5.0 build (for real this time)
|
||||
|
||||
1.1.1
|
||||
- Fix Swift 5.0 build
|
||||
|
||||
1.1.0
|
||||
- Replace RandomBytesSequence with Swift.RandomNumberGenerator
|
||||
- Fix CBC-MAC
|
||||
- Update SPM support
|
||||
- Update for Xcode 11 and Swift 5.1
|
||||
- Xcode: BUILD_LIBRARY_FOR_DISTRIBUTION = YES
|
||||
|
||||
1.0.0
|
||||
- Swift 5
|
||||
- Let's
|
||||
- Celebrate
|
||||
- This
|
||||
- Event
|
||||
- With
|
||||
- 1.0.0 release
|
||||
- After
|
||||
- 4 years
|
||||
- Thank you
|
||||
|
||||
0.15.0
|
||||
- Adds The scrypt Password-Based Key Derivation Function (https://tools.ietf.org/html/rfc7914)
|
||||
- Minor improvements
|
||||
|
||||
0.14.0
|
||||
- Fixed decryption of AES-GCM ciphertexts with custom tag length
|
||||
|
||||
0.13.1
|
||||
- Adds AES-GCM tag length configuration.
|
||||
- Fixes count check for initializing UInt64 from Data.
|
||||
|
||||
0.13.0
|
||||
- Adds CBC-MAC authenticator.
|
||||
- Adds AES-CCM operation mode.
|
||||
|
||||
0.12.0
|
||||
- Swift 4.2 maintenance update.
|
||||
|
||||
0.11.0
|
||||
- API: Cryptor.seek() is throwable
|
||||
- Adds proper stream support for CTR encryption with Updaptable interface.
|
||||
- Refactor internals for the stream cipher modes.
|
||||
- Set minimum deployment target to 8.0 (again).
|
||||
|
||||
0.10.0
|
||||
- API: BlockMode is no longer an enum. Please migrate to eg. CBC() etc...
|
||||
- Adds AES-GCM support. #97 - Feature sponsored by GesundheitsCloud (http://www.gesundheitscloud.de/)
|
||||
- Adds CRC32c support.
|
||||
- Improves AES variant validation.
|
||||
- Fixes empty password in PBKDF2.
|
||||
|
||||
0.9.0
|
||||
- Swift 4.1 compatibility
|
||||
- Added CMAC message authenticator https://tools.ietf.org/html/rfc4493
|
||||
- Added AEADChaCha20Poly1305 (AEAD_CHACHA20_POLY1305) https://tools.ietf.org/html/rfc7539#section-2.8.1
|
||||
|
||||
0.8.3
|
||||
- Fixes SHA3 padding.
|
||||
- Fixes Carthage builds.
|
||||
|
||||
0.8.2
|
||||
- Fixes SHA3 partial updates calculations.
|
||||
- Makes ChaCha20 processing faster again.
|
||||
|
||||
0.8.1
|
||||
- Adds Data(hex:) helper.
|
||||
- Adds HKDF (HMAC-based Extract-and-Expand Key Derivation Function)
|
||||
- Prevent ChaCha overflow error
|
||||
|
||||
0.8.0
|
||||
- Adds SHA3 Keccak variants
|
||||
- Adds String.bytes helper to convert String to array of bytes
|
||||
- Improves AES performance
|
||||
- Speeds up compilation times with Swift 4
|
||||
- Fixes: Blowfish minimum key size is 5
|
||||
- Removes Ciphers "iv" parameter (value moved to BlockMode)
|
||||
- BlockMode uses associated value for IV value where apply e.g. .CBC(iv: ivbytes)
|
||||
- Refactors internal hacks no longer needed with Swift 4
|
||||
|
||||
0.7.2
|
||||
- Adds Padding enum (.pkcs5, .pkcs7, .noPadding, .zeroPadding)
|
||||
- Removes Generics from the public API.
|
||||
- Slightly improves SHA1, SHA2, SHA3 performance.
|
||||
- Update SPM configuration for Swift 4
|
||||
|
||||
0.7.1
|
||||
- Swift 4.0 compatibility release
|
||||
|
||||
0.7.0
|
||||
- Swift 3.2 compatibility release
|
||||
|
||||
0.6.9
|
||||
- Fixed padding issue where padding was not properly added in CTR mode.
|
||||
- Fixed thrown error on decrypting empty string,
|
||||
- Fixed CI build script.
|
||||
- Added String.encryptToBase64()
|
||||
|
||||
0.6.8
|
||||
- Speed up MD5()
|
||||
- Faster Array(hex:)
|
||||
- Improve AES performance
|
||||
- Fix tvOS bitcode
|
||||
- Fix Blowfish CFB, OFB, CTR block modes.
|
||||
- Fix Blowfish for 32-bit arch.
|
||||
- Fix ChaCha20 preconditions
|
||||
|
||||
0.6.7
|
||||
- Release for Xcode 8.2
|
||||
- Fix playground example
|
||||
|
||||
0.6.6
|
||||
- Rework ChaCha20
|
||||
- Fix Poly1305
|
||||
|
||||
0.6.5
|
||||
- Significant performance improvement when processing lange amount of data.
|
||||
- Degeneric functions and change Sequence -> Collection in generic constraints.
|
||||
|
||||
0.6.4
|
||||
- More performance improvements
|
||||
- Add convenient Digest.sha2(bytes:variant)
|
||||
- New: Blowfish cipher
|
||||
|
||||
0.6.3
|
||||
- Hotfix release
|
||||
- Fixes bitPadding() that breaks Digests calculations, introduced in 0.6.2
|
||||
|
||||
0.6.2
|
||||
- SHA performance improvements by using less Swift in Swift
|
||||
- Fix public access to all digests classes
|
||||
|
||||
0.6.1
|
||||
- Update tests.
|
||||
- New: RandomBytesSequence urandom values on Linux.
|
||||
- Throw appropriate error for AES with invalid input where padding is needed.
|
||||
- Improve performance, especially to SHA-1, SHA-2, PBKDF and related.
|
||||
- Set deployment targets for all platform. Fixes Carthage builds.
|
||||
- New: SHA-3 implementation (request #291)
|
||||
- SHA-1 conforms to Updatable protocol and may be calculated incrementally.
|
||||
- SHA-2 conforms to Updatable protocol and may be calculated incrementally.
|
||||
|
||||
0.6.0
|
||||
- Remove bridge() workaround for Linux (not needed)
|
||||
- make MD5() public
|
||||
- Update README
|
||||
- Convenience HMAC initializer for String input
|
||||
|
||||
0.6.0-beta2
|
||||
- SHA-2 fix #319
|
||||
- HashProtocol -> Digest and refactor
|
||||
- MD5 conforms to Updatable protocol and may be calculated incrementally
|
||||
- Cipher protocol accepts Collection input now
|
||||
|
||||
0.6.0-beta1
|
||||
- Swift 3 compatibility
|
||||
- Multiplatform, Single-scheme Xcode Project
|
||||
- Swift Package Manager fully supported (build and tests)
|
||||
- Improved Linux support
|
||||
- Travis configuration added
|
||||
- Public interface tests added
|
||||
- enum Authenticator -> protocol Authenticator
|
||||
- CRC -> Checksum
|
||||
- String.encrypt() returns hex string instead of Array<UInt8>
|
||||
- removed String.decrypt()
|
||||
- enum Hash -> struct Hash
|
||||
- Convenience initializer of Array of bytes with Hex string. Array<UInt8>(hex: "0xb1b1b2b2")
|
||||
- Fix reusability of ChaCha20 instance
|
||||
- Replace optional initializers with throwable initializers
|
||||
- Allow to set initial counter explicitly (AES block modes). RandomAccessCryptor.seek()
|
||||
|
||||
0.5.2
|
||||
- Fix AES-CTR incremental updates. #287
|
||||
- Fixed PBKDF2 tests. #295
|
||||
- Fixed assertion check in PKCS7. #288
|
||||
- Updatable protocol accept SequenceType in place of Array
|
||||
|
||||
0.5.1
|
||||
- Fixed PBKDF2 not taking key length parameter into account
|
||||
- Switch to Array<> in code
|
||||
|
||||
0.5
|
||||
- Added PBKDF1 https://tools.ietf.org/html/rfc2898#section-5.1
|
||||
- Added PBKDF2 https://tools.ietf.org/html/rfc2898#section-5.2
|
||||
- UpdatableCryptor protocol allows incremental encryption stream of data
|
||||
- CryptoSwift.playground
|
||||
- Docs update
|
||||
- Added reflection control to CRC-32 (Luís Silva)
|
||||
- Fix AES.init() (Pascal Pfiffner)
|
||||
|
||||
0.4.1
|
||||
- fix NoPadding()
|
||||
|
||||
0.4
|
||||
- Padding setup is now part of cipher constructor
|
||||
- Added PBKDF2 http://tools.ietf.org/html/rfc2898#section-5.2
|
||||
- Add BlockCipher protocol
|
||||
- Rename Cipher -> CipherProtocol
|
||||
- Remove build-frameworks.sh script
|
||||
- Keep sensitive data in memory with SecureBytes
|
||||
- Allows direct use of HMAC and Poly1305
|
||||
- README update
|
||||
- Fix missing Foundation import on Linux
|
||||
|
||||
0.3.1
|
||||
- replace deprecated Bit with new enum.
|
||||
|
||||
0.3
|
||||
- Swift 2.2 support
|
||||
- use generators for cipher block modes should reduce memory overload.
|
||||
- add OFB block mode
|
||||
- add PCBC block mode
|
||||
- String.decryptBase64ToString to decrypt Base64 encoded strings
|
||||
- broke up complicated expressions which were taking ages to compile
|
||||
|
||||
0.2.3
|
||||
- enable bitcode setting for Debug on an Apple TV
|
||||
- faster compilation times
|
||||
- improve padding functions
|
||||
|
||||
0.2.2
|
||||
- Fix ChaCha20 cipher
|
||||
- Replace for(;;) with for-in
|
||||
- Workaround for "NSString are not yet implicitly convertible to String" on Linux
|
||||
|
||||
0.2.1
|
||||
- Fix linux build
|
||||
- re-add umbrella header
|
||||
|
||||
0.2
|
||||
- Rabbit cipher (RFC4503)
|
||||
- Linux Swift support
|
||||
- Swift Package Manager support
|
||||
- tvOS support
|
||||
- Add optional seed to CRC
|
||||
- Add umbrella header (CryptoSwift.h)
|
||||
- Fix AES in CTR mode
|
||||
- Fix no padding support for CTR and CFB block modes
|
||||
- Fix access to AES.Error and ChaCha20.Error
|
||||
|
||||
0.1.1
|
||||
- Fix Cococapods package (missing Foundation integration)
|
||||
|
||||
0.1.0
|
||||
- Major performance improvements.
|
||||
- Transition from Optionals to throw error.
|
||||
- Replace enum Cipher with protocol for ciphers.
|
||||
- Added CRC16
|
||||
- Fixed AES CFB decryption
|
||||
- Drop internal "Foundation" dependency, nonetheless it is supported as usual.
|
||||
|
||||
0.0.16
|
||||
- Critical fix for private "md5" selector issue (#135)
|
||||
|
||||
0.0.15
|
||||
- Fix 32-bit CTR block mode
|
||||
- Carthage support update
|
||||
- Mark as App-Extension-Safe API
|
||||
|
||||
0.0.14
|
||||
- hexString -> toHextString() #105
|
||||
- CTR (Counter mode)
|
||||
- Hex string is lowercase now
|
||||
- Carthage support
|
||||
- Tests update
|
||||
- Swift 2.0 support - overall update
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user