diff --git a/.gitignore b/.gitignore index 0a45e37a663c0c0a94a4c2c17866bf6689dfec63..31e2395d1a4c711d705552a44d63896f57305e2c 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ res/i18n/qm res/i18n/lang-* out .directory +temp diff --git a/.travis.yml b/.travis.yml index 41666ee505798ef8476a271efb07e9b6570fa759..c2bba1907b5c15bff1096903fbfb90737aa62c44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,6 +57,7 @@ install: - pip install base58 - pip install quamash - pip install asyncio + - pip install git+https://github.com/Insoleet/pretenders.git@develop - python gen_resources.py - python gen_translations.py - python setup.py build diff --git a/ci/appveyor/build.cmd b/ci/appveyor/build.cmd index 91e23da46c5ed6971035621b17bdd0bfc563cafa..79322896d4bb13d3a5c06a136fe680f9edb23fba 100644 --- a/ci/appveyor/build.cmd +++ b/ci/appveyor/build.cmd @@ -17,6 +17,7 @@ pip install requests pip install base58 pip install quamash pip install asyncio +pip install git+https://github.com/Insoleet/pretenders.git@develop python gen_resources.py if %errorlevel% neq 0 exit /b 1s diff --git a/res/icons/AUTHORS b/res/icons/AUTHORS index 4dc1d78f3193393fcd73bb1eec24f2c481b5d9a0..701840e29eb80d91e0847f992f5c19f87b7ca972 100644 --- a/res/icons/AUTHORS +++ b/res/icons/AUTHORS @@ -15,4 +15,12 @@ noun_5197_cc.svg : Created by Bibzee noun_38960_cc.svg : Created by Agarunov Oktay-Abraham noun_42425_cc.svg : Created by Luis Rodrigues noun_62146_cc.svg : Created by Sergey Krivoy -noun_2149_cc.svg : Created by Anand A Nair \ No newline at end of file +noun_2149_cc.svg : Created by Anand A Nair +noun_152997_cc.svg : Created by Pedro Ivo Hudson +noun_139613_cc.svg : Created by Aha-Soft +noun_19900_cc.svg : Created by by Stefan Parnarov +noun_178785_cc.svg : by Jevgeni Striganov +noun_41979_cc.svg : by by hunotika +noun_155533_cc.svg : by anbileru adaleru +noun_155520_cc.svg : by anbileru adaleru +noun_155540_cc.svg : by anbileru adaleru \ No newline at end of file diff --git a/res/icons/icons.qrc b/res/icons/icons.qrc index 6bef064abf71418f373ced63370badf17114d71a..8fe92ad92db25a2c85261a719b8b1844ae52af9f 100644 --- a/res/icons/icons.qrc +++ b/res/icons/icons.qrc @@ -1,5 +1,13 @@ <RCC> <qresource prefix="icons"> + <file alias="leave_icon">noun_155520_cc.svg</file> + <file alias="new_membership">noun_155540_cc.svg</file> + <file alias="payment_icon">noun_178785_cc.svg</file> + <file alias="renew_membership">noun_155533_cc.svg</file> + <file alias="certification_icon">noun_41979_cc.svg</file> + <file alias="logout">noun_19900_cc.svg</file> + <file alias="add_community">noun_139613_cc.svg</file> + <file alias="connect_icon">noun_152997_cc.svg</file> <file alias="home_icon">iconmonstr-home-icon.svg</file> <file alias="cutecoin_logo">logo.svg</file> <file alias="add_account_icon">noun_7440_cc.svg</file> diff --git a/res/icons/noun_139613_cc.svg b/res/icons/noun_139613_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..18a319b7c607bdfcd7ee5a358eef15e611bc7b34 --- /dev/null +++ b/res/icons/noun_139613_cc.svg @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + viewBox="0 0 100.00001 125" + xml:space="preserve" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_139613_cc.svg"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="1366" + inkscape:window-height="712" + id="namedview10" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="-42.425844" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="svg2" /><path + d="m 25.78125,24.656245 c -1.19804,0.03338 -2.390296,0.356511 -3.5,1 -3.551054,2.059166 -4.777916,6.605196 -2.71875,10.15625 2.059166,3.551054 6.605196,4.777916 10.15625,2.71875 0.456937,-0.264966 0.872582,-0.566293 1.25,-0.90625 l 11.90625,8.84375 c -0.102601,-0.265069 -0.210456,-0.53323 -0.28125,-0.8125 -0.377046,-0.995022 -0.216682,-1.991187 0.03125,-2.96875 l -9.9375,-7.34375 c 1.040295,-2.147247 1.036979,-4.749341 -0.25,-6.96875 -1.415677,-2.441349 -4.020561,-3.792192 -6.65625,-3.71875 z m 53.90625,3.09375 c -1.19804,0.03338 -2.421546,0.356511 -3.53125,1 -3.09834,1.796649 -4.395139,5.479306 -3.3125,8.75 l -11.125,6.65625 c -0.01921,1.463022 -0.639159,2.857354 -1.375,4.1875 L 74.25,39.999995 c 2.284876,2.676533 6.222001,3.422095 9.375,1.59375 3.551054,-2.059166 4.746666,-6.605196 2.6875,-10.15625 -1.415677,-2.441349 -3.989311,-3.760942 -6.625,-3.6875 z m -27.625,6.0625 c -6.491611,0 -6.40625,5.65625 -6.40625,5.65625 l -0.0625,3.03125 c -0.530352,-0.0072 -0.65625,0.514594 -0.65625,1.5625 0,1.570663 0.973035,3.213529 1.84375,4.0625 0.295074,1.234764 0.866408,2.327458 1.625,3.15625 -0.02641,0.0162 -0.06722,0.01506 -0.09375,0.03125 -2.587069,1.578079 -5.672508,2.649945 -7.6875,3.71875 -0.555696,0.29448 -1.144777,0.811066 -1.625,1.46875 -0.32697,0.452412 -0.598878,0.935264 -0.84375,1.4375 -0.009,0.01782 -0.0225,0.04466 -0.03125,0.0625 -0.0335,0.0697 -0.06169,0.148406 -0.09375,0.21875 -0.48672,1.041571 -0.840864,2.134504 -1.03125,3.25 -0.004,0.02072 0.0038,0.04182 0,0.0625 -0.0086,0.05238 -0.02313,0.10387 -0.03125,0.15625 -0.03411,0.193194 -0.06636,0.403903 -0.09375,0.59375 -0.02338,0.156384 -0.04306,0.493278 -0.03125,0.65625 -0.0018,0.0099 9e-4,0.02135 0,0.03125 -0.07182,1.018098 1.84375,1.34375 1.84375,1.34375 0.715321,0.211536 2.029012,0.44187 3.625,0.65625 2.847115,0.46989 5.761037,0.630802 8.625,0.71875 0.277992,0.0086 0.572546,-0.0011 0.875,0 l 0.03125,0 c 0.150498,0 0.31314,0.0018 0.46875,0 0.302454,-0.0018 0.59699,0.0086 0.875,0 2.862703,-0.08791 5.779109,-0.249346 8.625,-0.71875 1.598346,-0.21456 2.908924,-0.444498 3.625,-0.65625 0,0 1.915552,-0.325652 1.84375,-1.34375 -0.0018,-0.0099 9e-4,-0.02133 0,-0.03125 0.01188,-0.162972 -0.0079,-0.499866 -0.03125,-0.65625 -0.03917,-0.271261 -0.103166,-0.566172 -0.15625,-0.84375 -0.210852,-1.219968 -0.58763,-2.440343 -1.15625,-3.5625 -0.0063,-0.0126 0.0065,-0.01868 0,-0.03125 -0.23436,-0.469476 -0.505438,-0.918878 -0.8125,-1.34375 -0.480222,-0.657684 -1.069304,-1.17427 -1.625,-1.46875 -2.014992,-1.068805 -5.100449,-2.140671 -7.6875,-3.71875 -0.02651,-0.0162 -0.06734,-0.01497 -0.09375,-0.03125 0.75861,-0.828792 1.329925,-1.921486 1.625,-3.15625 0.870714,-0.848971 1.8125,-2.491837 1.8125,-4.0625 0,-1.047906 -0.12588,-1.569664 -0.65625,-1.5625 l -0.03125,-3.03125 c 0,0 0.08537,-5.65625 -6.40625,-5.65625 z m -13.46875,19.65625 -19.375,4.84375 C 19.115362,58.083617 19.003698,57.846936 18.875,57.624995 16.815834,54.073941 12.269804,52.878329 8.71875,54.937495 5.1676963,56.996661 3.9408337,61.542691 6,65.093745 c 2.059166,3.551054 6.605196,4.746666 10.15625,2.6875 2.450927,-1.42123 3.803575,-4.041145 3.71875,-6.6875 l 15.84375,-3.9375 c 0.07438,-0.159517 0.172762,-0.313302 0.25,-0.46875 0.603648,-1.339254 1.514598,-2.380579 2.625,-3.21875 z m 31.125,7.78125 c 0.20349,1.074546 0.137248,2.244139 -0.3125,3.21875 l 11.3125,7.34375 c -0.855939,2.066285 -0.802327,4.478297 0.40625,6.5625 2.059166,3.551054 6.605196,4.777916 10.15625,2.71875 3.551054,-2.059166 4.777916,-6.605196 2.71875,-10.15625 -2.059166,-3.551054 -6.605196,-4.777916 -10.15625,-2.71875 -0.574435,0.3331 -1.08329,0.738901 -1.53125,1.1875 l -12.59375,-8.15625 z m -25.3125,6.5 -7.96875,20.84375 c -1.689316,-0.317982 -3.52127,-0.05496 -5.125,0.875 -3.551054,2.059166 -4.746666,6.605196 -2.6875,10.15625 2.059166,3.551055 6.605196,4.777915 10.15625,2.718755 3.551054,-2.05917 4.777916,-6.605201 2.71875,-10.156255 -0.607794,-1.048147 -1.45443,-1.883708 -2.40625,-2.5 l 8.3125,-21.65625 c -0.996859,-0.07277 -2.0091,-0.172782 -3,-0.28125 z" + style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" + id="path4" + inkscape:connector-curvature="0" /></svg> \ No newline at end of file diff --git a/res/icons/noun_152997_cc.svg b/res/icons/noun_152997_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..3eb27d7384f0cb2f51e42695974d3f31e74e341a --- /dev/null +++ b/res/icons/noun_152997_cc.svg @@ -0,0 +1,44 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="-255 347 100 125" + enable-background="new -255 347 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_152997_cc.svg"><metadata + id="metadata14"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs12" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="743" + inkscape:window-height="480" + id="namedview10" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /><path + d="m -205,364 c -24.9,0 -45,20.1 -45,45 0,24.9 20.1,45 45,45 24.9,0 45,-20.1 45,-45 0,-24.9 -20.1,-45 -45,-45 z m -26,72.4 c 1.8,-0.8 3.6,-1.4 5.5,-1.9 5.2,-1.2 8.3,-2.9 9.3,-5.1 0.8,-1.7 0.3,-4 -1.2,-6.9 -9.6,-17.7 -7.9,-27.7 -4.8,-32.9 3.1,-5.3 9.1,-8.2 16.7,-8.2 7.6,0 13.5,2.9 16.6,8.1 3.1,5.2 4.8,15.2 -4.7,33 -1.6,3 -2,5.3 -1.2,7 1,2.1 4.1,3.8 9.3,5 1.8,0.4 4,1.1 6.3,2.1 -6.7,6.3 -15.8,10.1 -25.7,10.1 -10.2,0.1 -19.3,-3.9 -26.1,-10.3 z" + id="path4" + inkscape:connector-curvature="0" /></svg> \ No newline at end of file diff --git a/res/icons/noun_155520_cc.svg b/res/icons/noun_155520_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..1217dfbec87ef8d09662154329e267fb1f6e6512 --- /dev/null +++ b/res/icons/noun_155520_cc.svg @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 100 125" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_155520_cc.svg"> + <metadata + id="metadata16"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs14" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="743" + inkscape:window-height="480" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /> + <g + transform="matrix(1.3457509,0,0,1.3457509,-17.552378,-1285.5892)" + id="g4"> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 15.110586,975.08438 c -0.257006,0 -0.505174,0.0945 -0.703274,0.2579 l -0.01172,0.015 -0.0059,0 C 14.081837,975.61353 14,976.00871 14,976.40929 l 0,34.68311 c 6.2e-5,0.6214 0.503812,1.1252 1.125238,1.1252 l 33.801091,0 c 0.586855,9.7103 8.643142,17.4323 18.49903,17.4323 10.236198,0 18.560564,-8.3214 18.560564,-18.5575 -0.05194,-0.3616 0,-0.6773 0,-1.0052 l 0,-33.68677 c 0.108906,-0.68863 -0.428123,-1.30945 -1.125237,-1.30105 -23.24491,-0.099 -46.520652,0.13953 -69.7501,-0.015 z m 1.139889,2.26514 67.484972,0 0,24.88708 c -3.148794,-5.77627 -9.278805,-9.70217 -16.310089,-9.70217 -9.856474,0 -17.913031,7.72137 -18.49903,17.43237 l -32.675853,0 z m 4.688492,3.56034 c -1.521671,-0.021 -1.521671,2.27207 0,2.25057 l 58.107989,0 c 1.521671,0.021 1.521671,-2.27203 0,-2.25057 z m 0,5.81077 c -1.521685,-0.021 -1.521685,2.272 0,2.2506 l 34.864793,0 c 1.521686,0.021 1.521686,-2.2721 0,-2.2506 z m 0,5.8138 c -1.521685,-0.021 -1.521685,2.272 0,2.2504 l 23.243195,0 c 1.521684,0.021 1.521684,-2.2719 0,-2.2504 z m 46.486391,2.2504 c 9.019957,0 16.310089,7.28737 16.310089,16.30727 0,9.0199 -7.290132,16.3071 -16.310089,16.3071 -8.975386,0 -16.229773,-7.2172 -16.301298,-16.1753 0.0111,-0.09 0.0111,-0.1819 0,-0.2726 0.07611,-8.9541 7.328792,-16.16647 16.301298,-16.16647 z m -5.822521,9.35937 c -1.007872,0 -1.504007,1.2276 -0.78239,1.9312 l 5.013755,5.0167 -5.013755,5.0136 c -1.104574,1.0606 0.530609,2.6958 1.591156,1.5912 l 5.016685,-5.0166 5.013757,5.0166 c 1.060547,1.1046 2.695728,-0.5306 1.591156,-1.5912 l -5.016685,-5.0136 5.016685,-5.0167 c 0.73468,-0.7155 0.207553,-1.9606 -0.817556,-1.9312 -0.292229,0.015 -0.569676,0.1305 -0.7736,0.34 l -5.013757,5.0138 -5.016685,-5.0138 c -0.212505,-0.218 -0.504294,-0.3407 -0.808766,-0.34 z m -40.66387,0.015 c -1.521685,-0.021 -1.521685,2.2721 0,2.2506 l 11.621597,0 c 1.521684,0.021 1.521684,-2.272 0,-2.2506 z" + id="path6" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_155533_cc.svg b/res/icons/noun_155533_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..88e79c220345d71b075bedec7e217bf52ed7849a --- /dev/null +++ b/res/icons/noun_155533_cc.svg @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 100 125" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_155533_cc.svg"> + <metadata + id="metadata16"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs14" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="743" + inkscape:window-height="480" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /> + <g + transform="matrix(1.2812691,0,0,1.2812691,-14.189139,-1221.9757)" + id="g4"> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.49966645;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 15.110916,975.06807 c -0.621665,-0.004 -1.119023,0.51848 -1.110801,1.14011 -1.54e-4,0.0107 -1.54e-4,0.0215 0,0.0322 l 0,34.85682 c -7.6e-5,0.6217 0.503852,1.1256 1.125456,1.1255 l 33.807617,0 c 0.586097,9.717 8.644024,17.4446 18.502602,17.4446 10.238382,0 18.574502,-8.3278 18.564148,-18.5701 -0.01155,-11.42186 0,-23.20066 0,-34.71614 3.28e-4,-0.0313 -6.5e-4,-0.0626 -0.0029,-0.0938 l 0,-0.0117 0,-0.009 c 0.03354,-0.64318 -0.478479,-1.18327 -1.122524,-1.18408 -23.249396,-0.0987 -46.529588,0.13224 -69.763567,-0.0147 z m 1.14011,2.26556 67.498002,0 0,24.90357 c -3.149351,-5.78006 -9.280447,-9.70996 -16.313238,-9.70996 -9.858578,0 -17.916505,7.72756 -18.502602,17.44456 l -32.682162,0 z m 4.686466,3.56394 c -1.499511,10e-4 -1.499511,2.24977 0,2.25087 l 58.125071,0 c 1.499511,0 1.499511,-2.24977 0,-2.25087 z m 0,5.81487 c -1.500607,0 -1.500607,2.2509 0,2.2509 l 34.874456,0 c 1.500606,0 1.500606,-2.2509 0,-2.2509 z m 0,5.8148 c -1.500607,0 -1.500607,2.2509 0,2.2509 l 23.250614,0 c 1.500606,0 1.500606,-2.2509 0,-2.2509 z m 46.498298,2.2509 c 9.021606,0 16.313238,7.29246 16.313238,16.31906 0,9.0266 -7.291632,16.3191 -16.313238,16.3191 -9.021607,0 -16.310308,-7.2925 -16.310308,-16.3191 0,-9.0266 7.288701,-16.31906 16.310308,-16.31906 z m -0.07034,2.8108 c -1.170749,0.01 -2.351251,0.1892 -3.505324,0.5598 -4.616281,1.4824 -7.847018,5.67446 -8.112657,10.52466 -0.08958,1.5071 2.171195,1.6311 2.245049,0.1231 0.214979,-3.9251 2.817603,-7.303 6.553431,-8.5024 3.735824,-1.19976 7.812697,0.035 10.263918,3.1037 2.45122,3.0686 2.757006,7.325 0.770819,10.7153 -1.752047,2.9905 -4.962346,4.7388 -8.350056,4.666 l 1.679389,-2.0575 c 0.622368,-0.7447 0.07326,-1.8752 -0.896847,-1.8465 -0.331131,0.01 -0.641,0.1656 -0.847022,0.425 l -3.347057,4.1032 c -0.392984,0.4809 -0.322154,1.1892 0.158268,1.5827 l 4.09443,3.3529 c 1.160624,0.9516 2.58796,-0.7893 1.427335,-1.7409 l -1.925584,-1.5768 c 4.045379,-0.05 7.850052,-2.188 9.947383,-5.768 2.454098,-4.1888 2.073993,-9.4725 -0.955465,-13.2651 -2.272099,-2.84446 -5.687763,-4.41696 -9.20001,-4.39916 z m -46.427957,6.56796 c -1.500607,0 -1.500607,2.251 0,2.251 l 11.626771,0 c 1.500607,0 1.500607,-2.251 0,-2.251 z" + id="path6" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_155540_cc.svg b/res/icons/noun_155540_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..adeaf9491b56ed4f6a6efb81c4e1f1e60661b641 --- /dev/null +++ b/res/icons/noun_155540_cc.svg @@ -0,0 +1,59 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + viewBox="0 0 100 125" + version="1.1" + x="0px" + y="0px" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_155540_cc.svg"> + <metadata + id="metadata16"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs14" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="743" + inkscape:window-height="480" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="61.00568" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /> + <g + transform="matrix(1.3383945,0,0,1.3383945,-16.919727,-1280.9706)" + id="g4"> + <path + style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" + d="m 67.422429,975.07796 c -17.441637,0.0192 -34.88975,0.12012 -52.311843,0.006 -0.257006,-0.002 -0.504221,0.0953 -0.703273,0.25786 -0.006,0.005 -0.01173,0.01 -0.01758,0.0147 C 14.081844,975.61282 14,976.0079 14,976.4085 l 0,34.6831 c 6.2e-5,0.6214 0.503812,1.1252 1.125238,1.1253 l 33.801091,0 c 0.586857,9.7104 8.643143,17.4324 18.49903,17.4324 10.236196,0 18.569433,-8.3215 18.560564,-18.5577 -0.0099,-11.41928 0,-23.18198 0,-34.69189 0.108903,-0.68861 -0.428123,-1.30955 -1.125238,-1.30106 -5.811227,-0.0247 -11.624378,-0.0269 -17.438256,-0.0205 z m -51.171953,2.27098 67.484971,0 0,24.88706 c -3.148794,-5.77618 -9.278806,-9.70218 -16.310088,-9.70218 -9.856474,0 -17.913032,7.72128 -18.49903,17.43238 l -32.675853,0 z m 4.688491,3.56033 c -1.52167,-0.0215 -1.52167,2.27195 0,2.25045 l 58.107989,0 c 1.521671,0.021 1.521671,-2.27196 0,-2.25045 z m 0,5.81075 c -1.521685,-0.021 -1.521685,2.272 0,2.2505 l 34.864794,0 c 1.521684,0.022 1.521684,-2.272 0,-2.2505 z m 0,5.8138 c -1.521685,-0.022 -1.521685,2.272 0,2.2505 l 23.243195,0 c 1.521685,0.022 1.521685,-2.272 0,-2.2505 z m 46.486392,2.2505 c 9.019955,0 16.310088,7.28718 16.310088,16.30708 0,9.0199 -7.290133,16.3072 -16.310088,16.3072 -8.975387,0 -16.241481,-7.2171 -16.301299,-16.1753 -6.09e-4,-0.091 -6.56e-4,-0.1814 0,-0.2725 0.06436,-8.9543 7.328793,-16.16648 16.301299,-16.16648 z m -0.0117,6.45248 c -0.369837,10e-5 -0.716,0.182 -0.925976,0.4864 l -8.574079,8.5712 c -1.104572,1.0605 0.53061,2.6957 1.591157,1.5911 l 6.795383,-6.7924 0,14.713 c -0.02152,1.5217 2.271996,1.5217 2.250475,0 l 0,-14.716 6.795383,6.7954 c 1.060545,1.1046 2.695779,-0.5306 1.591155,-1.5911 l -8.650266,-8.6474 c -0.0123,-0.014 -0.02506,-0.028 -0.03809,-0.041 l -0.02638,-0.026 c -0.01425,-0.015 -0.0289,-0.03 -0.04396,-0.044 -0.01713,-0.015 -0.03472,-0.03 -0.05275,-0.044 -0.02455,-0.021 -0.04998,-0.04 -0.07619,-0.059 l -0.0147,-0.01 c -0.184168,-0.1222 -0.400227,-0.1874 -0.621226,-0.1876 z m -46.474689,2.9186 c -1.521685,-0.021 -1.521685,2.272 0,2.2504 l 11.621597,0 c 1.521685,0.022 1.521685,-2.2719 0,-2.2504 z" + id="path6" + inkscape:connector-curvature="0" /> + </g> +</svg> diff --git a/res/icons/noun_178785_cc.svg b/res/icons/noun_178785_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..62d3b75dc5b06632fd5033e5a088cd31b6e8b954 --- /dev/null +++ b/res/icons/noun_178785_cc.svg @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + style="enable-background:new 0 0 100 100;" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_178785_cc.svg"><metadata + id="metadata28"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs26" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="743" + inkscape:window-height="480" + id="namedview24" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="58.142293" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /><path + d="m 47.7,96 c 4.6,0 8.6,-1.4 11.5,-4.3 l 9.3,-9.3 c 3,-3 4.5,-7.4 4.3,-12.6 -0.1,-1.1 -1,-1.9 -2.1,-1.9 -1.1,0.1 -2,1 -1.9,2.1 0.2,4 -0.9,7.3 -3.1,9.6 -6.7,6.7 -22.4,2.1 -34.4,-9.9 -12,-12 -16.5,-27.8 -9.9,-34.4 5.6,-5.6 17.8,-3.5 29,5.1 0.9,0.7 2.1,0.5 2.8,-0.4 0.7,-0.9 0.5,-2.1 -0.4,-2.8 -13,-10 -27.1,-11.9 -34.3,-4.8 L 9.2,41.7 C 0.7,50.2 5.1,67.8 19.1,81.8 28.3,90.9 39.1,96 47.7,96 Z M 12.2,65.3 18.5,59 c 0.7,1.4 1.6,2.9 2.5,4.3 l -6.4,6.4 c -0.9,-1.5 -1.7,-2.9 -2.4,-4.4 z m -2.8,-8.5 5.9,-5.9 c 0.4,1.4 0.8,2.8 1.4,4.2 l -6.2,6.2 C 10,59.8 9.7,58.3 9.4,56.8 Z m 14,9.8 c 1.1,1.5 2.4,2.9 3.7,4.4 l -6.5,6.5 C 19.3,76.1 18,74.6 16.9,73.1 l 6.5,-6.5 z m 10.8,10.8 -6.5,6.5 c -1.5,-1.1 -2.9,-2.3 -4.3,-3.6 l 6.5,-6.5 c 1.5,1.3 2.9,2.5 4.3,3.6 z m 7.6,5 -6.4,6.4 C 33.9,88.1 32.5,87.3 31,86.3 l 6.4,-6.4 c 1.6,0.9 3,1.7 4.4,2.5 z m -2.3,8 6.2,-6.2 c 1.4,0.6 2.8,1 4.2,1.4 l -6,6 C 43.2,91.5 42.5,91.3 41.8,91.1 41,90.9 40.2,90.6 39.5,90.4 Z m 19.2,-3.9 -2.3,2.3 c -1.8,1.8 -4.2,2.8 -7.2,3.1 l 5.4,-5.4 c 0.8,0.1 1.7,0.1 2.5,0.1 0.5,0 1.1,0 1.6,-0.1 z M 14.4,46.1 9.1,51.4 c 0.3,-2.8 1.2,-5.2 3,-6.9 l 2.3,-2.3 c -0.1,1.3 -0.1,2.6 0,3.9 z" + id="path4" + inkscape:connector-curvature="0" /><path + d="M 98.4,52.7 79.7,33.8 c -0.8,-0.8 -2,-0.8 -2.8,0 -0.4,0.4 -0.6,1 -0.6,1.6 0,0.1 0,0.1 0,0.2 l 0,9.6 -6.4,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 8.4,0 c 1.1,0 2,-0.9 2,-2 l 0,-7.1 13.9,14 -13.9,14 0,-6.7 c 0,-1.1 -0.9,-2 -2,-2 l -8.4,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 6.4,0 0,9.6 c 0,0 0,0 0,0 0,0.1 0,0.3 0,0.4 0,0.1 0,0.1 0.1,0.2 0,0.1 0,0.1 0.1,0.2 0,0.1 0.1,0.1 0.1,0.2 0,0 0,0.1 0.1,0.1 0.1,0.1 0.2,0.2 0.3,0.3 l 0,0 c 0,0 0,0 0,0 0.1,0.1 0.2,0.2 0.3,0.2 0,0 0.1,0.1 0.1,0.1 0.1,0 0.1,0.1 0.2,0.1 0.1,0 0.1,0 0.2,0.1 0.1,0 0.1,0 0.2,0.1 0.1,0 0.3,0 0.4,0 0.1,0 0.3,0 0.4,0 0.1,0 0.1,0 0.2,-0.1 0.1,0 0.1,0 0.2,-0.1 0.1,0 0.1,-0.1 0.2,-0.1 0.1,0 0.1,0 0.2,-0.1 0.1,-0.1 0.2,-0.2 0.3,-0.3 l 0,0 18.7,-18.8 c 0.6,-0.8 0.6,-2.1 -0.2,-2.8 z" + id="path6" + inkscape:connector-curvature="0" /><path + d="m 63.1,59.3 -4.2,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 4.2,0 c 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2 z" + id="path8" + inkscape:connector-curvature="0" /><path + d="m 63.1,45.2 -4.2,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 4.2,0 c 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2 z" + id="path10" + inkscape:connector-curvature="0" /><path + d="m 49.5,59.3 -1.8,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 1.8,0 c 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2 z" + id="path12" + inkscape:connector-curvature="0" /><path + d="m 49.5,45.2 -1.8,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 1.8,0 c 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2 z" + id="path14" + inkscape:connector-curvature="0" /><path + d="m 36.7,59.3 -0.2,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 0.2,0 c 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2 z" + id="path16" + inkscape:connector-curvature="0" /><path + d="m 36.7,45.2 -0.2,0 c -1.1,0 -2,0.9 -2,2 0,1.1 0.9,2 2,2 l 0.2,0 c 1.1,0 2,-0.9 2,-2 0,-1.1 -0.9,-2 -2,-2 z" + id="path18" + inkscape:connector-curvature="0" /></svg> \ No newline at end of file diff --git a/res/icons/noun_19900_cc.svg b/res/icons/noun_19900_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..4eee44fb90a727ea36d57099d0e3c2e12ba46d78 --- /dev/null +++ b/res/icons/noun_19900_cc.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" x="0px" y="0px" viewBox="0 0 100 125" enable-background="new 0 0 100 100" xml:space="preserve"><g><path d="M56.49,47.086c-0.022-0.023-0.04-0.045-0.06-0.066L33.688,24.334c-1.549-1.546-4.052-1.545-5.588-0.001 c-1.537,1.544-1.527,4.052,0.021,5.596l16.344,16.304H3.936C1.754,46.231-0.008,48.003,0,50.188 c0.009,2.185,1.785,3.957,3.967,3.956l40.548,0.001L27.927,70.822c-1.538,1.544-1.527,4.051,0.022,5.595 c1.548,1.546,4.05,1.546,5.587,0l22.567-22.686c0.116-0.119,0.222-0.241,0.32-0.369c0.964-0.722,1.591-1.872,1.585-3.172 C58.006,48.931,57.409,47.81,56.49,47.086z"/><path d="M51.486,63.151l-7.861,7.903v17.939c0,3.433,2.918,6.214,6.519,6.214l7.724-0.002v-7.861l-6.382,0.002V63.151z"/><path d="M51.486,13.406l6.382,0V5.543l-7.724,0c-3.601,0-6.519,2.781-6.519,6.212v17.731l7.861,7.842V13.406z"/><path d="M97.492,8.293L65.182,0.071c-2.04-0.506-3.736,1.751-3.736,5.094v89.679c0,3.326,1.696,5.614,3.736,5.079l32.311-8.22 c1.396-0.352,2.508-2.825,2.508-5.502V13.788C100,11.097,98.888,8.65,97.492,8.293z M74.087,54.922 c-2.72,0-4.924-2.204-4.924-4.922c0-2.72,2.204-4.925,4.924-4.925c2.718,0,4.924,2.205,4.924,4.925 C79.011,52.718,76.805,54.922,74.087,54.922z"/></g><text x="0" y="115" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">Created by Stefan Parnarov</text><text x="0" y="120" fill="#000000" font-size="5px" font-weight="bold" font-family="'Helvetica Neue', Helvetica, Arial-Unicode, Arial, Sans-serif">from the Noun Project</text></svg> \ No newline at end of file diff --git a/res/icons/noun_41979_cc.svg b/res/icons/noun_41979_cc.svg new file mode 100644 index 0000000000000000000000000000000000000000..0f70e6ef4c48aa7df542cc69f0e72c55322a6e5e --- /dev/null +++ b/res/icons/noun_41979_cc.svg @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.1" + x="0px" + y="0px" + viewBox="0 0 100 125" + enable-background="new 0 0 100 100" + xml:space="preserve" + id="svg2" + inkscape:version="0.91 r13725" + sodipodi:docname="noun_41979_cc.svg"><metadata + id="metadata16"><rdf:RDF><cc:Work + rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs + id="defs14" /><sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="743" + inkscape:window-height="480" + id="namedview12" + showgrid="false" + inkscape:zoom="1.888" + inkscape:cx="50" + inkscape:cy="62.5" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg2" /><rect + width="100" + height="100" + id="rect4" + x="0" + y="12" + style="fill:none" /><path + d="m 29.241,43.591 c 4.499,0 8.16,-3.663 8.16,-8.167 0,-4.494 -3.661,-8.152 -8.16,-8.152 -4.505,0 -8.167,3.659 -8.167,8.152 10e-4,4.504 3.662,8.167 8.167,8.167 z m 0,-13.687 c 3.049,0 5.53,2.477 5.53,5.521 0,3.054 -2.481,5.535 -5.53,5.535 -3.056,0 -5.537,-2.481 -5.537,-5.535 0,-3.044 2.482,-5.521 5.537,-5.521 z m -11.911,32.23 23.821,0 c 0.725,0 1.316,-0.591 1.316,-1.318 l 0,-7.533 c 0,-0.061 -0.007,-0.126 -0.019,-0.188 -0.204,-4.484 -3.98,-8.105 -8.493,-8.105 l -9.445,0 c -4.545,0 -8.361,3.701 -8.495,8.255 0,0.014 0,0.361 0,0.375 l 0,7.195 c 10e-4,0.727 0.588,1.319 1.315,1.319 z m 1.315,-8.809 c 0.094,-3.147 2.728,-5.708 5.866,-5.708 l 9.445,0 c 3.154,0 5.788,2.561 5.87,5.704 0.005,0.099 0.019,0.197 0.042,0.291 l -0.033,5.892 -21.19,0 0,-5.884 -1.315,0 1.315,-0.295 z m 32.038,-19.49 c 0,-0.727 0.584,-1.318 1.315,-1.318 l 23.995,0 c 0.723,0 1.313,0.591 1.313,1.318 0,0.722 -0.591,1.313 -1.313,1.313 l -23.995,0 c -0.731,0 -1.315,-0.591 -1.315,-1.313 z m -4.274,8.86 c 0,-0.727 0.586,-1.313 1.313,-1.313 l 32.542,0 c 0.725,0 1.313,0.586 1.313,1.313 0,0.727 -0.588,1.318 -1.313,1.318 l -32.541,0 c -0.727,0.001 -1.314,-0.591 -1.314,-1.318 z M 16.016,70.638 c 0,-0.727 0.586,-1.318 1.313,-1.318 l 32.542,0 c 0.722,0 1.313,0.592 1.313,1.318 0,0.728 -0.591,1.313 -1.313,1.313 l -32.541,0 c -0.727,0 -1.314,-0.586 -1.314,-1.313 z M 92.294,17 7.707,17 c -1.45,0 -2.63,1.177 -2.63,2.627 l 0,64.68 c 0,1.454 1.18,2.631 2.629,2.631 l 44.487,0 c -2.013,4.265 -3.831,8.617 -5.393,13.073 -0.18,0.517 -0.023,1.089 0.394,1.44 0.422,0.352 1.013,0.412 1.489,0.141 3.021,-1.688 6.098,-3.251 9.164,-4.653 1.205,3.157 2.547,6.281 3.994,9.315 0.223,0.46 0.683,0.746 1.187,0.746 0.021,0 0.047,0 0.072,-0.005 0.528,-0.023 0.99,-0.37 1.171,-0.877 1.536,-4.386 3.368,-8.664 5.398,-12.843 2.018,4.175 3.835,8.452 5.371,12.843 0.176,0.502 0.638,0.849 1.166,0.877 0.026,0.005 0.053,0.005 0.073,0.005 0.505,0 0.964,-0.286 1.187,-0.746 1.431,-2.974 2.768,-6.103 3.99,-9.315 3.077,1.402 6.149,2.965 9.163,4.653 0.476,0.268 1.067,0.216 1.489,-0.136 0.417,-0.353 0.574,-0.929 0.394,-1.445 -1.557,-4.451 -3.36,-8.809 -5.37,-13.073 l 5.162,0 c 1.454,0 2.629,-1.177 2.629,-2.631 l 0,-64.68 C 94.923,18.177 93.748,17 92.294,17 Z m -29.408,85.239 c -1.103,-2.473 -2.13,-4.996 -3.062,-7.529 -0.126,-0.337 -0.389,-0.614 -0.722,-0.755 -0.167,-0.07 -0.341,-0.104 -0.517,-0.104 -0.178,0 -0.358,0.038 -0.527,0.113 -2.533,1.116 -5.075,2.35 -7.592,3.673 2.477,-6.549 5.507,-12.871 8.962,-18.955 1.078,1.243 2.373,2.274 3.837,3.063 1.756,2.801 3.405,5.667 4.925,8.603 -1.962,3.875 -3.764,7.829 -5.304,11.891 z M 61.313,69.817 c 0,-4.602 3.743,-8.345 8.343,-8.345 4.6,0 8.34,3.744 8.34,8.345 0,4.597 -3.74,8.34 -8.34,8.34 -4.6,0 -8.343,-3.743 -8.343,-8.34 z m 19.931,24.148 c -0.325,-0.146 -0.708,-0.15 -1.041,-0.01 -0.335,0.141 -0.598,0.418 -0.72,0.76 -0.945,2.57 -1.973,5.095 -3.054,7.529 -2.512,-6.628 -5.636,-12.994 -9.231,-19.077 0.795,0.146 1.613,0.248 2.458,0.248 4.102,0 7.737,-1.862 10.235,-4.742 3.45,6.089 6.473,12.416 8.949,18.96 -2.516,-1.319 -5.058,-2.552 -7.596,-3.668 z m 8.42,-12.285 -5.178,0 c -0.945,-1.811 -1.938,-3.593 -2.958,-5.361 1.063,-1.942 1.727,-4.138 1.727,-6.501 0,-7.501 -6.103,-13.604 -13.599,-13.604 -7.5,0 -13.603,6.103 -13.603,13.604 0,2.368 0.663,4.563 1.735,6.511 -1.021,1.764 -2.008,3.546 -2.953,5.352 l -44.497,0 0,-59.423 79.326,0 0,59.422 z" + id="path6" + inkscape:connector-curvature="0" /></svg> \ No newline at end of file diff --git a/res/ui/account_cfg.ui b/res/ui/account_cfg.ui index 836d77c280c9987f455f12c61696c614c5532d61..92ad6c2c4460f11b521f3cd7a927a8e0798a0a5f 100644 --- a/res/ui/account_cfg.ui +++ b/res/ui/account_cfg.ui @@ -57,33 +57,6 @@ </item> </layout> </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_7"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QLabel" name="label_2"> - <property name="text"> - <string>Wallets</string> - </property> - </widget> - </item> - <item> - <widget class="QSpinBox" name="spinbox_wallets"> - <property name="minimum"> - <number>1</number> - </property> - <property name="maximum"> - <number>100</number> - </property> - <property name="value"> - <number>1</number> - </property> - </widget> - </item> - </layout> - </item> <item> <layout class="QHBoxLayout" name="horizontalLayout_8"> <property name="topMargin"> @@ -475,22 +448,6 @@ </hint> </hints> </connection> - <connection> - <sender>spinbox_wallets</sender> - <signal>valueChanged(int)</signal> - <receiver>AccountConfigurationDialog</receiver> - <slot>action_edit_account_parameters()</slot> - <hints> - <hint type="sourcelabel"> - <x>285</x> - <y>127</y> - </hint> - <hint type="destinationlabel"> - <x>199</x> - <y>118</y> - </hint> - </hints> - </connection> <connection> <sender>button_delete</sender> <signal>clicked()</signal> diff --git a/res/ui/community_cfg.ui b/res/ui/community_cfg.ui index 0d18585d3a33202199ab2f5ae604aa3032e3099a..408cf47d40b91f5bc117fb7a2ed3ec55f1ca4752 100644 --- a/res/ui/community_cfg.ui +++ b/res/ui/community_cfg.ui @@ -25,7 +25,7 @@ <property name="currentIndex"> <number>0</number> </property> - <widget class="QWidget" name="page_init"> + <widget class="QWidget" name="page_node"> <layout class="QVBoxLayout" name="verticalLayout_4"> <item> <spacer name="verticalSpacer_2"> @@ -75,45 +75,66 @@ </layout> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> + <spacer name="verticalSpacer"> + <property name="orientation"> + <enum>Qt::Vertical</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>20</width> + <height>40</height> + </size> + </property> + </spacer> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout_5"> <property name="topMargin"> <number>6</number> </property> <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> + <widget class="QPushButton" name="button_register"> + <property name="text"> + <string>Register your account</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/new_membership</normaloff>:/icons/new_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_connect"> + <property name="text"> + <string>Connect using your account</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> </property> - <property name="sizeHint" stdset="0"> + <property name="iconSize"> <size> - <width>40</width> - <height>20</height> + <width>32</width> + <height>32</height> </size> </property> - </spacer> + </widget> </item> <item> - <widget class="QPushButton" name="button_checknode"> + <widget class="QLabel" name="label_error"> <property name="text"> - <string>Check node connectivity</string> + <string/> </property> </widget> </item> </layout> </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> - </property> - </spacer> - </item> </layout> </widget> <widget class="QWidget" name="page_add_nodes"> @@ -173,7 +194,7 @@ </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout_4"> + <layout class="QHBoxLayout" name="layout_previous_next"> <item> <widget class="QPushButton" name="button_previous"> <property name="enabled"> @@ -211,7 +232,9 @@ </item> </layout> </widget> - <resources/> + <resources> + <include location="../icons/icons.qrc"/> + </resources> <connections> <connection> <sender>button_add</sender> diff --git a/res/ui/community_tab.ui b/res/ui/community_tab.ui deleted file mode 100644 index 43eb5750042ac2493427e536b2e9837890c72624..0000000000000000000000000000000000000000 --- a/res/ui/community_tab.ui +++ /dev/null @@ -1,147 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<ui version="4.0"> - <class>CommunityTabWidget</class> - <widget class="QWidget" name="CommunityTabWidget"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>636</width> - <height>404</height> - </rect> - </property> - <property name="contextMenuPolicy"> - <enum>Qt::DefaultContextMenu</enum> - </property> - <property name="windowTitle"> - <string>communityTabWidget</string> - </property> - <layout class="QHBoxLayout" name="horizontalLayout"> - <item> - <widget class="QTabWidget" name="tabs_information"> - <property name="currentIndex"> - <number>0</number> - </property> - <property name="iconSize"> - <size> - <width>20</width> - <height>20</height> - </size> - </property> - <property name="elideMode"> - <enum>Qt::ElideNone</enum> - </property> - <widget class="QWidget" name="tab_members"> - <attribute name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/members_icon</normaloff>:/icons/members_icon</iconset> - </attribute> - <attribute name="title"> - <string>Identities</string> - </attribute> - <layout class="QVBoxLayout" name="verticalLayout"> - <item> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <property name="topMargin"> - <number>0</number> - </property> - <item> - <widget class="QLineEdit" name="edit_textsearch"> - <property name="placeholderText"> - <string>Research a pubkey, an uid...</string> - </property> - </widget> - </item> - <item> - <widget class="QToolButton" name="button_search"> - <property name="text"> - <string>Search</string> - </property> - <property name="popupMode"> - <enum>QToolButton::MenuButtonPopup</enum> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - </layout> - </item> - <item> - <widget class="QTableView" name="table_identities"> - <property name="contextMenuPolicy"> - <enum>Qt::CustomContextMenu</enum> - </property> - <property name="alternatingRowColors"> - <bool>true</bool> - </property> - <property name="sortingEnabled"> - <bool>true</bool> - </property> - <attribute name="horizontalHeaderShowSortIndicator" stdset="0"> - <bool>true</bool> - </attribute> - <attribute name="horizontalHeaderStretchLastSection"> - <bool>true</bool> - </attribute> - <attribute name="verticalHeaderVisible"> - <bool>false</bool> - </attribute> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </widget> - </item> - </layout> - </widget> - <resources> - <include location="../icons/icons.qrc"/> - </resources> - <connections> - <connection> - <sender>edit_textsearch</sender> - <signal>returnPressed()</signal> - <receiver>CommunityTabWidget</receiver> - <slot>search_text()</slot> - <hints> - <hint type="sourcelabel"> - <x>170</x> - <y>62</y> - </hint> - <hint type="destinationlabel"> - <x>215</x> - <y>184</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_search</sender> - <signal>clicked()</signal> - <receiver>CommunityTabWidget</receiver> - <slot>search_text()</slot> - <hints> - <hint type="sourcelabel"> - <x>371</x> - <y>62</y> - </hint> - <hint type="destinationlabel"> - <x>215</x> - <y>184</y> - </hint> - </hints> - </connection> - </connections> - <slots> - <slot>identity_context_menu(QPoint)</slot> - <slot>send_membership_demand()</slot> - <slot>send_membership_leaving()</slot> - <slot>search_text()</slot> - <slot>publish_uid()</slot> - <slot>revoke_uid()</slot> - </slots> -</ui> diff --git a/res/ui/community_view.ui b/res/ui/community_view.ui new file mode 100644 index 0000000000000000000000000000000000000000..45b86c534c47c9c4f877f78c6344554a8a402e8d --- /dev/null +++ b/res/ui/community_view.ui @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>CommunityWidget</class> + <widget class="QWidget" name="CommunityWidget"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>624</width> + <height>429</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QFrame" name="frame"> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> + </property> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> + </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="button_home"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Fixed" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="text"> + <string/> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/home_icon</normaloff>:/icons/home_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QLabel" name="label_currency"> + <property name="text"> + <string/> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_send_money"> + <property name="text"> + <string>Send money</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/payment_icon</normaloff>:/icons/payment_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_certification"> + <property name="text"> + <string>Certification</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/certification_icon</normaloff>:/icons/certification_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_membership"> + <property name="text"> + <string>Renew membership</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/renew_membership</normaloff>:/icons/renew_membership</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + </layout> + </widget> + </item> + <item> + <widget class="QTabWidget" name="tabs"/> + </item> + </layout> + </widget> + <resources> + <include location="../icons/icons.qrc"/> + </resources> + <connections/> +</ui> diff --git a/res/ui/homescreen.ui b/res/ui/homescreen.ui index 9b80e39c6b67b4d0e2e75c4eb0719dbc98729a14..f326a247071175967a5e8908579dd6ec4ee79212 100644 --- a/res/ui/homescreen.ui +++ b/res/ui/homescreen.ui @@ -1,17 +1,17 @@ <?xml version="1.0" encoding="UTF-8"?> <ui version="4.0"> - <class>HomeScreenWidget</class> - <widget class="QWidget" name="HomeScreenWidget"> + <class>HomescreenWidget</class> + <widget class="QWidget" name="HomescreenWidget"> <property name="geometry"> <rect> <x>0</x> <y>0</y> - <width>400</width> - <height>325</height> + <width>648</width> + <height>472</height> </rect> </property> <property name="sizePolicy"> - <sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding"> + <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> <horstretch>0</horstretch> <verstretch>0</verstretch> </sizepolicy> @@ -19,142 +19,174 @@ <property name="windowTitle"> <string>Form</string> </property> - <property name="styleSheet"> - <string notr="true">QLabel { - qproperty-alignment: AlignCenter; -} - -QToolButton { - font-size: 14pt; - font-weight: bold; -}</string> - </property> <layout class="QVBoxLayout" name="verticalLayout"> <item> - <spacer name="verticalSpacer_2"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> + <widget class="QFrame" name="frame_connected"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - </spacer> - </item> - <item> - <widget class="QLabel" name="label_welcome"> - <property name="text"> - <string><html><head/><body><p><br/></p></body></html></string> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> </property> - <property name="textFormat"> - <enum>Qt::RichText</enum> - </property> - <property name="openExternalLinks"> - <bool>true</bool> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> </property> + <layout class="QHBoxLayout" name="horizontalLayout"> + <property name="sizeConstraint"> + <enum>QLayout::SetMaximumSize</enum> + </property> + <item> + <widget class="QLabel" name="label_connected"> + <property name="styleSheet"> + <string notr="true"> font-size:12pt; font-weight:600;</string> + </property> + <property name="text"> + <string>Connected as</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_add_community"> + <property name="text"> + <string>Add a community</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/add_community</normaloff>:/icons/add_community</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="button_disconnect"> + <property name="text"> + <string>Disconnect</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/logout</normaloff>:/icons/logout</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> </widget> </item> <item> - <layout class="QHBoxLayout" name="horizontalLayout"> - <property name="topMargin"> - <number>6</number> - </property> - <item> - <widget class="QToolButton" name="button_new"> - <property name="text"> - <string>Create a new account</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/add_account_icon</normaloff>:/icons/add_account_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - <property name="autoRaise"> - <bool>false</bool> - </property> - <property name="arrowType"> - <enum>Qt::NoArrow</enum> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <property name="topMargin"> - <number>6</number> + <widget class="QFrame" name="frame_disconnected"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Preferred" vsizetype="Maximum"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> </property> - <item> - <widget class="QToolButton" name="button_import"> - <property name="text"> - <string>Import an existing account</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/import_icon</normaloff>:/icons/import_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - </layout> - </item> - <item> - <layout class="QHBoxLayout" name="horizontalLayout_3"> - <property name="topMargin"> - <number>6</number> + <property name="frameShape"> + <enum>QFrame::StyledPanel</enum> </property> - <item> - <widget class="QToolButton" name="button_info"> - <property name="text"> - <string>Get to know more about ucoin</string> - </property> - <property name="icon"> - <iconset resource="../icons/icons.qrc"> - <normaloff>:/icons/ucoin_info_icon</normaloff>:/icons/ucoin_info_icon</iconset> - </property> - <property name="iconSize"> - <size> - <width>32</width> - <height>32</height> - </size> - </property> - <property name="toolButtonStyle"> - <enum>Qt::ToolButtonTextBesideIcon</enum> - </property> - </widget> - </item> - </layout> - </item> - <item> - <spacer name="verticalSpacer"> - <property name="orientation"> - <enum>Qt::Vertical</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>20</width> - <height>40</height> - </size> + <property name="frameShadow"> + <enum>QFrame::Raised</enum> </property> - </spacer> + <layout class="QHBoxLayout" name="horizontalLayout_2"> + <item> + <widget class="QLabel" name="label_disconnected"> + <property name="text"> + <string><html><head/><body><p><span style=" font-size:12pt; font-weight:600;">Not Connected</span></p></body></html></string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="toolbutton_connect"> + <property name="text"> + <string>Connect</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/connect_icon</normaloff>:/icons/connect_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + <property name="popupMode"> + <enum>QToolButton::MenuButtonPopup</enum> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="toolbutton_new_account"> + <property name="text"> + <string>New account</string> + </property> + <property name="icon"> + <iconset resource="../icons/icons.qrc"> + <normaloff>:/icons/add_account_icon</normaloff>:/icons/add_account_icon</iconset> + </property> + <property name="iconSize"> + <size> + <width>32</width> + <height>32</height> + </size> + </property> + <property name="popupMode"> + <enum>QToolButton::MenuButtonPopup</enum> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + <property name="autoRaise"> + <bool>false</bool> + </property> + <property name="arrowType"> + <enum>Qt::NoArrow</enum> + </property> + </widget> + </item> + <item> + <spacer name="horizontalSpacer_5"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + <property name="sizeHint" stdset="0"> + <size> + <width>40</width> + <height>20</height> + </size> + </property> + </spacer> + </item> + </layout> + </widget> </item> </layout> </widget> diff --git a/res/ui/identities_tab.ui b/res/ui/identities_tab.ui new file mode 100644 index 0000000000000000000000000000000000000000..5f3f2e072c5778a32cbea7c6f2bfc1b77a0750c5 --- /dev/null +++ b/res/ui/identities_tab.ui @@ -0,0 +1,70 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>IdentitiesTab</class> + <widget class="QWidget" name="IdentitiesTab"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>400</width> + <height>300</height> + </rect> + </property> + <property name="windowTitle"> + <string>Form</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="horizontalLayout_3"> + <property name="topMargin"> + <number>0</number> + </property> + <item> + <widget class="QLineEdit" name="edit_textsearch"> + <property name="placeholderText"> + <string>Research a pubkey, an uid...</string> + </property> + </widget> + </item> + <item> + <widget class="QToolButton" name="button_search"> + <property name="text"> + <string>Search</string> + </property> + <property name="popupMode"> + <enum>QToolButton::MenuButtonPopup</enum> + </property> + <property name="toolButtonStyle"> + <enum>Qt::ToolButtonTextBesideIcon</enum> + </property> + </widget> + </item> + </layout> + </item> + <item> + <widget class="QTableView" name="table_identities"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + <property name="alternatingRowColors"> + <bool>true</bool> + </property> + <property name="sortingEnabled"> + <bool>true</bool> + </property> + <attribute name="horizontalHeaderShowSortIndicator" stdset="0"> + <bool>true</bool> + </attribute> + <attribute name="horizontalHeaderStretchLastSection"> + <bool>true</bool> + </attribute> + <attribute name="verticalHeaderVisible"> + <bool>false</bool> + </attribute> + </widget> + </item> + </layout> + </widget> + <resources/> + <connections/> +</ui> diff --git a/res/ui/mainwindow.ui b/res/ui/mainwindow.ui index 10a981179bce626465efa75b0e808ce99f8672bb..f30bd087938b923932b71797e41fda4dc494a2ec 100644 --- a/res/ui/mainwindow.ui +++ b/res/ui/mainwindow.ui @@ -14,18 +14,7 @@ <string notr="true">CuteCoin</string> </property> <widget class="QWidget" name="centralwidget"> - <layout class="QVBoxLayout" name="verticalLayout_6"> - <item> - <widget class="QTabWidget" name="currencies_tabwidget"> - <property name="iconSize"> - <size> - <width>24</width> - <height>24</height> - </size> - </property> - </widget> - </item> - </layout> + <layout class="QVBoxLayout" name="verticalLayout_6"/> </widget> <widget class="QMenuBar" name="menubar"> <property name="geometry"> @@ -33,7 +22,7 @@ <x>0</x> <y>0</y> <width>681</width> - <height>25</height> + <height>29</height> </rect> </property> <widget class="QMenu" name="menu_file"> @@ -48,11 +37,11 @@ </widget> <widget class="QMenu" name="menu_account"> <property name="title"> - <string>Account</string> + <string>Acco&unt</string> </property> <widget class="QMenu" name="menu_contacts_list"> <property name="title"> - <string>&Contacts</string> + <string>Co&ntacts</string> </property> <addaction name="separator"/> </widget> @@ -94,7 +83,7 @@ </action> <action name="action_add_a_contact"> <property name="text"> - <string>&Add a contact</string> + <string>A&dd a contact</string> </property> </action> <action name="actionSend_a_message"> @@ -164,7 +153,7 @@ </action> <action name="actionCertification"> <property name="text"> - <string>&Certification</string> + <string>C&ertification</string> </property> </action> <action name="action_set_as_default"> diff --git a/res/ui/transactions_tab.ui b/res/ui/transactions_tab.ui index 7b5f520604ea8c1dd224328f13ddc15434b6a46a..b459a6950f465ba297e35a1279cb9bd18fac0e67 100644 --- a/res/ui/transactions_tab.ui +++ b/res/ui/transactions_tab.ui @@ -14,6 +14,32 @@ <string>Form</string> </property> <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QGroupBox" name="groupBox_2"> + <property name="title"> + <string>Balance</string> + </property> + <layout class="QVBoxLayout" name="verticalLayout_4"> + <item> + <widget class="QLabel" name="label_balance"> + <property name="font"> + <font> + <pointsize>22</pointsize> + <weight>75</weight> + <bold>true</bold> + </font> + </property> + <property name="text"> + <string>label_balance</string> + </property> + <property name="alignment"> + <set>Qt::AlignHCenter|Qt::AlignTop</set> + </property> + </widget> + </item> + </layout> + </widget> + </item> <item> <layout class="QVBoxLayout" name="verticalLayout_3"> <item> @@ -75,37 +101,6 @@ </attribute> </widget> </item> - <item> - <layout class="QHBoxLayout" name="layout_balance"> - <item> - <widget class="QLabel" name="label_payment"> - <property name="text"> - <string>Payment:</string> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_deposit"> - <property name="text"> - <string>Deposit:</string> - </property> - <property name="alignment"> - <set>Qt::AlignCenter</set> - </property> - </widget> - </item> - <item> - <widget class="QLabel" name="label_balance"> - <property name="text"> - <string>Balance:</string> - </property> - <property name="alignment"> - <set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set> - </property> - </widget> - </item> - </layout> - </item> </layout> </item> </layout> diff --git a/res/ui/wallets_tab.ui b/res/ui/wallets_tab.ui index cef471bfba54f06dc89fb6eaa1b6d7ee03a642d8..16b4dd1c96d212b9773b06a202151676c417af27 100644 --- a/res/ui/wallets_tab.ui +++ b/res/ui/wallets_tab.ui @@ -29,91 +29,6 @@ QGroupBox::title { </property> <layout class="QGridLayout" name="gridLayout"> <item row="0" column="0"> - <widget class="QGroupBox" name="groupBox"> - <property name="sizePolicy"> - <sizepolicy hsizetype="Preferred" vsizetype="Preferred"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="styleSheet"> - <string notr="true"/> - </property> - <property name="title"> - <string>Account</string> - </property> - <property name="flat"> - <bool>true</bool> - </property> - <layout class="QGridLayout" name="gridLayout_2"> - <item row="0" column="0"> - <widget class="QLabel" name="label_general"> - <property name="text"> - <string>label_general</string> - </property> - <property name="textFormat"> - <enum>Qt::RichText</enum> - </property> - <property name="alignment"> - <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> - </property> - </widget> - </item> - <item row="1" column="0"> - <layout class="QHBoxLayout" name="horizontalLayout_2"> - <property name="leftMargin"> - <number>0</number> - </property> - <property name="topMargin"> - <number>5</number> - </property> - <item> - <spacer name="horizontalSpacer"> - <property name="orientation"> - <enum>Qt::Horizontal</enum> - </property> - <property name="sizeHint" stdset="0"> - <size> - <width>40</width> - <height>20</height> - </size> - </property> - </spacer> - </item> - <item> - <widget class="QPushButton" name="button_publish_uid"> - <property name="text"> - <string>Publish UID</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_revoke_uid"> - <property name="text"> - <string>Revoke UID</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_membership"> - <property name="text"> - <string>Renew membership</string> - </property> - </widget> - </item> - <item> - <widget class="QPushButton" name="button_leaving"> - <property name="text"> - <string>Send leaving demand</string> - </property> - </widget> - </item> - </layout> - </item> - </layout> - </widget> - </item> - <item row="1" column="0"> <widget class="QGroupBox" name="groupBox_2"> <property name="title"> <string>Balance</string> @@ -149,7 +64,7 @@ QGroupBox::title { </layout> </widget> </item> - <item row="2" column="0"> + <item row="1" column="0"> <widget class="QTableView" name="table_wallets"> <property name="sizePolicy"> <sizepolicy hsizetype="Expanding" vsizetype="Preferred"> @@ -202,70 +117,6 @@ QGroupBox::title { </hint> </hints> </connection> - <connection> - <sender>button_publish_uid</sender> - <signal>clicked()</signal> - <receiver>WalletsTab</receiver> - <slot>publish_uid()</slot> - <hints> - <hint type="sourcelabel"> - <x>430</x> - <y>69</y> - </hint> - <hint type="destinationlabel"> - <x>461</x> - <y>459</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_revoke_uid</sender> - <signal>clicked()</signal> - <receiver>WalletsTab</receiver> - <slot>revoke_uid()</slot> - <hints> - <hint type="sourcelabel"> - <x>533</x> - <y>69</y> - </hint> - <hint type="destinationlabel"> - <x>461</x> - <y>459</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_membership</sender> - <signal>clicked()</signal> - <receiver>WalletsTab</receiver> - <slot>send_membership_demand()</slot> - <hints> - <hint type="sourcelabel"> - <x>662</x> - <y>69</y> - </hint> - <hint type="destinationlabel"> - <x>461</x> - <y>459</y> - </hint> - </hints> - </connection> - <connection> - <sender>button_leaving</sender> - <signal>clicked()</signal> - <receiver>WalletsTab</receiver> - <slot>send_membership_leaving()</slot> - <hints> - <hint type="sourcelabel"> - <x>823</x> - <y>69</y> - </hint> - <hint type="destinationlabel"> - <x>461</x> - <y>459</y> - </hint> - </hints> - </connection> </connections> <slots> <slot>wallet_context_menu(QPoint)</slot> diff --git a/run_tests.py b/run_tests.py index 762b19590ced20c7eac8a20d0a81f73ab4445486..dc97800c0fa3bcf43e6dd5e5a1608d26c7fc4f1e 100644 --- a/run_tests.py +++ b/run_tests.py @@ -1,10 +1,33 @@ import sys import os +import signal import unittest +import subprocess +import time +import shlex +cmd = 'python -m pretenders.server.server --host 127.0.0.1 --port 50000' + +p = subprocess.Popen(shlex.split(cmd)) +time.sleep(2) +# Force saves to be done in temp directory +os.environ["XDG_CONFIG_HOME"] = os.path.join(os.path.dirname(__file__), 'temp') sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'lib'))) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'src'))) +try: + print("Run") + runner = unittest.TextTestRunner().run(unittest.defaultTestLoader.discover(start_dir='cutecoin.tests', pattern='test_*')) +finally: + print("Terminate") + os.kill(p.pid, signal.SIGINT) + time.sleep(2) + try: -runner = unittest.TextTestRunner().run(unittest.defaultTestLoader.discover(start_dir='cutecoin.tests', pattern='test_*')) + if sys.platform == "linux": + os.kill(p.pid, signal.SIGKILL) + p.kill() + print("Hard killed") + except OSError: + print("Terminated gracefully") sys.exit(not runner.wasSuccessful()) \ No newline at end of file diff --git a/setup.py b/setup.py index f3fe811a439d41cf1b1da335350ff66f1478a246..8381c99f10c33e2b372d154589ab156d3562c04a 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'src'))) print(sys.path) includes = ["sip", "re", "json", "logging", "hashlib", "os", "urllib", - "ucoinpy", "pylibscrypt", "requests"] + "ucoinpy", "pylibscrypt"] excludes = ['.git'] packages = ["libnacl", "encodings"] diff --git a/src/cutecoin/core/account.py b/src/cutecoin/core/account.py index b26ad98b4cd151f0b292ce906ace3f7d4d863e21..35bb49181f3623346d6aca6b2e982858cefea9b5 100644 --- a/src/cutecoin/core/account.py +++ b/src/cutecoin/core/account.py @@ -20,6 +20,7 @@ from .wallet import Wallet from .community import Community from .registry import LocalState from ..tools.exceptions import ContactAlreadyExists +from ..tools.decorators import asyncify from ..core.net.api import bma as qtbma from ..core.net.api.bma import PROTOCOL_VERSION @@ -32,7 +33,6 @@ class Account(QObject): """ loading_progressed = pyqtSignal(int, int) loading_finished = pyqtSignal(list) - inner_data_changed = pyqtSignal(str) wallets_changed = pyqtSignal() membership_broadcasted = pyqtSignal() certification_broadcasted = pyqtSignal() @@ -168,7 +168,7 @@ class Account(QObject): maximums = {} def progressing(value, maximum, hash): - logging.debug("Loading = {0} : {1} : {2}".format(value, maximum, loaded_wallets)) + #logging.debug("Loading = {0} : {1} : {2}".format(value, maximum, loaded_wallets)) values[hash] = value maximums[hash] = maximum account_value = sum(values.values()) @@ -196,6 +196,19 @@ class Account(QObject): def set_display_referential(self, index): self._current_ref = index + def set_scrypt_infos(self, salt, password): + """ + Change the size of the wallet pool + :param int size: The new size of the wallet pool + :param str password: The password of the account, same for all wallets + """ + self.salt = salt + self.pubkey = SigningKey(self.salt, password).pubkey + wallet = Wallet.create(0, self.salt, password, + "Wallet", self._identities_registry) + self.wallets.append(wallet) + + @asyncio.coroutine def identity(self, community): """ Get the account identity in the specified community @@ -203,7 +216,7 @@ class Account(QObject): :return: The account identity in the community :rtype: cutecoin.core.registry.Identity """ - identity = self._identities_registry.find(self.pubkey, community) + identity = yield from self._identities_registry.future_find(self.pubkey, community) if identity.local_state == LocalState.NOT_FOUND: identity.uid = self.name return identity @@ -212,23 +225,6 @@ class Account(QObject): def current_ref(self): return money.Referentials[self._current_ref] - def set_walletpool_size(self, size, password): - """ - Change the size of the wallet pool - - :param int size: The new size of the wallet pool - :param str password: The password of the account, same for all wallets - """ - logging.debug("Defining wallet pool size") - if len(self.wallets) < size: - for i in range(len(self.wallets), size): - wallet = Wallet.create(i, self.salt, password, - "Wallet {0}".format(i), self._identities_registry) - self.wallets.append(wallet) - else: - self.wallets = self.wallets[:size] - self.wallets_changed.emit() - def transfers(self, community): """ Get all transfers done in a community by all the wallets @@ -267,6 +263,7 @@ class Account(QObject): value += val return value + @asyncio.coroutine def amount(self, community): """ Get amount of money owned in a community by all the wallets @@ -277,7 +274,7 @@ class Account(QObject): """ value = 0 for w in self.wallets: - val = w.value(community) + val = yield from w.value(community) value += val return value @@ -298,9 +295,9 @@ class Account(QObject): key = SigningKey(self.salt, password) selfcert.sign([key]) logging.debug("Key publish : {0}".format(selfcert.signed_raw())) - replies = community.broadcast(qtbma.wot.Add, {}, {'pubkey': self.pubkey, + replies = community.bma_access.broadcast(qtbma.wot.Add, {}, {'pubkey': self.pubkey, 'self_': selfcert.signed_raw(), - 'other': []}) + 'other': ""}) for r in replies: r.finished.connect(lambda reply=r: self.__handle_selfcert_replies(replies, reply)) diff --git a/src/cutecoin/core/app.py b/src/cutecoin/core/app.py index 625f4a893823bdd8eea9398eae2c552919107ad3..75721e8b5a4689da4fbb012f1ca1dcd618f5bec4 100644 --- a/src/cutecoin/core/app.py +++ b/src/cutecoin/core/app.py @@ -157,6 +157,7 @@ class Application(QObject): Delete an account. Current account changes to None if it is deleted. """ + account.stop_coroutines() self.accounts.pop(account.name) if self.current_account == account: self.current_account = None @@ -178,7 +179,8 @@ class Application(QObject): self.stop_current_account() self.current_account = account - self.current_account.start_coroutines() + if self.current_account is not None: + self.current_account.start_coroutines() def stop_current_account(self): """ diff --git a/src/cutecoin/core/community.py b/src/cutecoin/core/community.py index c0a6903f29c8a5d0406e1b80af62bc472b532f44..18be5087c8972ee7f4a8fe1999eaaed8d5f6754e 100644 --- a/src/cutecoin/core/community.py +++ b/src/cutecoin/core/community.py @@ -12,9 +12,7 @@ import asyncio import math from PyQt5.QtCore import QObject, pyqtSignal -from requests.exceptions import RequestException -from ucoinpy.documents.block import Block from ..tools.exceptions import NoPeerAvailable from .net.network import Network from .net.api import bma as qtbma @@ -28,8 +26,6 @@ class Community(QObject): .. warning:: The currency name is supposed to be unique in cutecoin but nothing exists in ucoin to assert that a currency name is unique. """ - inner_data_changed = pyqtSignal(str) - def __init__(self, currency, network, bma_access): """ Initialize community attributes with a currency and a network. @@ -119,20 +115,20 @@ class Community(QObject): u = ord('\u24B6') + ord(letter) - ord('A') return chr(u) - @property + @asyncio.coroutine def dividend(self): """ Get the last generated community universal dividend. :return: The last UD or 1 if no UD was generated. """ - block = self.get_ud_block() + block = yield from self.get_ud_block() if block: return block['dividend'] else: return 1 - @property + @asyncio.coroutine def computed_dividend(self): """ Get the computed community universal dividend. @@ -143,8 +139,8 @@ class Community(QObject): :return: The computed UD or 1 if no UD was generated. """ - block = self.get_ud_block() - if block: + block = yield from self.get_ud_block() + if block and block != qtbma.blockchain.Block.null_value: return math.ceil( max( self.dividend, @@ -156,6 +152,7 @@ class Community(QObject): else: return 1 + @asyncio.coroutine def get_ud_block(self, x=0): """ Get a block with universal dividend @@ -163,19 +160,20 @@ class Community(QObject): :param int x: Get the 'x' older block with UD in it :return: The last block with universal dividend. """ - blocks = self.bma_access.get(self, qtbma.blockchain.UD)['result']['blocks'] + udblocks = yield from self.bma_access.future_request(qtbma.blockchain.UD) + blocks = udblocks['result']['blocks'] if len(blocks) > 0: index = len(blocks)-(1+x) if index < 0: index = 0 block_number = blocks[index] - block = self.bma_access.get(self, qtbma.blockchain.Block, + block = yield from self.bma_access.future_request(qtbma.blockchain.Block, req_args={'number': block_number}) return block else: return None - @property + @asyncio.coroutine def monetary_mass(self): """ Get the community monetary mass @@ -184,11 +182,11 @@ class Community(QObject): """ # Get cached block by block number block_number = self.network.latest_block_number - block = self.bma_access.get(self, qtbma.blockchain.Block, + block = yield from self.bma_access.future_request(self, qtbma.blockchain.Block, req_args={'number': block_number}) return block['monetaryMass'] - @property + @asyncio.coroutine def nb_members(self): """ Get the community members number @@ -198,7 +196,7 @@ class Community(QObject): try: # Get cached block by block number block_number = self.network.latest_block_number - block = self.bma_access.get(qtbma.blockchain.Block, + block = yield from self.bma_access.future_request(qtbma.blockchain.Block, req_args={'number': block_number}) return block['membersCount'] except ValueError as e: @@ -227,18 +225,20 @@ class Community(QObject): """ return self._bma_access - @property + @asyncio.coroutine def parameters(self): """ Return community parameters in bma format """ - return self.bma_access.get(self, qtbma.blockchain.Parameters) + return self.bma_access.future_request(qtbma.blockchain.Parameters) + @asyncio.coroutine def certification_expired(self, certtime): """ Return True if the certificaton time is too old """ - return time.time() - certtime > self.parameters['sigValidity'] + parameters = yield from self.parameters() + return time.time() - certtime > parameters['sigValidity'] def add_node(self, node): """ @@ -256,6 +256,7 @@ class Community(QObject): """ self._network.remove_root_node(index) + @asyncio.coroutine def get_block(self, number=None): """ Get a block @@ -263,10 +264,10 @@ class Community(QObject): :param int number: The block number. If none, returns current block. """ if number is None: - data = self.bma_access.get(self, qtbma.blockchain.Current) + data = self.bma_access.future_request(qtbma.blockchain.Current) else: logging.debug("Requesting block {0}".format(number)) - data = self.bma_access.get(self, qtbma.blockchain.Block, + data = self.bma_access.future_request(qtbma.blockchain.Block, req_args={'number': number}) return data @@ -283,13 +284,14 @@ class Community(QObject): block_number = block['number'] return {'number': block_number, 'hash': block_hash} + @asyncio.coroutine def members_pubkeys(self): """ Listing members pubkeys of a community :return: All members pubkeys. """ - memberships = self.bma_access.get(self, qtbma.wot.Members) + memberships = yield from self.bma_access.future_request(qtbma.wot.Members) return [m['pubkey'] for m in memberships["results"]] def start_coroutines(self): diff --git a/src/cutecoin/core/graph.py b/src/cutecoin/core/graph.py index 1665d83fabcead7d9225a05d9033ecb9decb7ccc..96576a1315c2925116e4f63af2b24fc04383e104 100644 --- a/src/cutecoin/core/graph.py +++ b/src/cutecoin/core/graph.py @@ -1,8 +1,9 @@ import logging import time -import datetime +import asyncio from PyQt5.QtCore import QLocale, QDateTime from ..core.registry import Identity, BlockchainState +from ..tools.decorators import asyncify from cutecoin.gui.views.wot import NODE_STATUS_HIGHLIGHTED, NODE_STATUS_OUT, ARC_STATUS_STRONG, ARC_STATUS_WEAK @@ -17,12 +18,18 @@ class Graph(object): """ self.app = app self.community = community - self.signature_validity = self.community.parameters['sigValidity'] - # arc considered strong during 75% of signature validity time - self.ARC_STATUS_STRONG_time = int(self.signature_validity * 0.75) + self.signature_validity = 0 + self.ARC_STATUS_STRONG_time = 0 # graph empty if None parameter self._graph = graph or (dict() and (graph is None)) + @asyncio.coroutine + def refresh_signature_validity(self): + parameters = yield from self.community.parameters() + self.signature_validity = parameters['sigValidity'] + # arc considered strong during 75% of signature validity time + self.ARC_STATUS_STRONG_time = int(self.signature_validity * 0.75) + def set(self, graph): """ Set the graph from dict @@ -38,6 +45,7 @@ class Graph(object): """ return self._graph + @asyncio.coroutine def get_shortest_path_between_members(self, from_identity, to_identity): """ Return path list of nodes from from_identity to to_identity @@ -50,17 +58,21 @@ class Graph(object): logging.debug("path between %s to %s..." % (from_identity.uid, to_identity.uid)) if from_identity.pubkey not in self._graph.keys(): self.add_identity(from_identity) - certifier_list = from_identity.certifiers_of(self.app.identities_registry, self.community) - self.add_certifier_list(certifier_list, from_identity, to_identity) - certified_list = from_identity.certified_by(self.app.identities_registry, self.community) - self.add_certified_list(certified_list, from_identity, to_identity) + certifier_list = yield from from_identity.certifiers_of(self.app.identities_registry, + self.community) + yield from self.add_certifier_list(certifier_list, from_identity, to_identity) + certified_list = yield from from_identity.certified_by(self.app.identities_registry, + self.community) + yield from self.add_certified_list(certified_list, from_identity, to_identity) if to_identity.pubkey not in self._graph.keys(): # recursively feed graph searching for account node... - self.explore_to_find_member(to_identity, self._graph[from_identity.pubkey]['connected'], list()) + yield from self.explore_to_find_member(to_identity, + self._graph[from_identity.pubkey]['connected'], list()) if len(self._graph[from_identity.pubkey]['connected']) > 0: # calculate path of nodes between identity and to_identity - path = self.find_shortest_path(self._graph[from_identity.pubkey], self._graph[to_identity.pubkey]) + path = yield from self.find_shortest_path(self._graph[from_identity.pubkey], + self._graph[to_identity.pubkey]) if path: logging.debug([node['text'] for node in path]) @@ -69,6 +81,7 @@ class Graph(object): return path + @asyncio.coroutine def explore_to_find_member(self, identity, connected=None, done=None): """ Scan graph recursively to find identity @@ -90,24 +103,29 @@ class Graph(object): if node['id'] in tuple(done): continue identity_selected = identity.from_handled_data(node['text'], node['id'], BlockchainState.VALIDATED) - certifier_list = identity_selected.unique_valid_certifiers_of(self.app.identities_registry, self.community) - self.add_certifier_list(certifier_list, identity_selected, identity) + certifier_list = yield from identity_selected.unique_valid_certifiers_of(self.app.identities_registry, + self.community) + yield from self.add_certifier_list(certifier_list, identity_selected, identity) if identity.pubkey in tuple(self._graph.keys()): return False - certified_list = identity_selected.unique_valid_certified_by(self.app.identities_registry, self.community) - self.add_certified_list(certified_list, identity_selected, identity) + certified_list = yield from identity_selected.unique_valid_certified_by(self.app.identities_registry, + self.community) + yield from self.add_certified_list(certified_list, identity_selected, identity) if identity.pubkey in tuple(self._graph.keys()): return False if node['id'] not in tuple(done): done.append(node['id']) if len(done) >= len(self._graph): return True - result = self.explore_to_find_member(identity, self._graph[identity_selected.pubkey]['connected'], done) + result = yield from self.explore_to_find_member(identity, + self._graph[identity_selected.pubkey]['connected'], + done) if not result: return False return True + @asyncio.coroutine def find_shortest_path(self, start, end, path=None): """ Find recursively the shortest path between two nodes @@ -126,12 +144,13 @@ class Graph(object): for pubkey in tuple(self._graph[start['id']]['connected']): node = self._graph[pubkey] if node not in path: - newpath = self.find_shortest_path(node, end, path) + newpath = yield from self.find_shortest_path(node, end, path) if newpath: if not shortest or len(newpath) < len(shortest): shortest = newpath return shortest + @asyncio.coroutine def add_certifier_list(self, certifier_list, identity, identity_account): """ Add list of certifiers to graph @@ -140,73 +159,78 @@ class Graph(object): :param identity identity_account: Account identity instance :return: """ - # add certifiers of uid - for certifier in tuple(certifier_list): - # add only valid certification... - if (time.time() - certifier['cert_time']) > self.signature_validity: - continue - # new node - if certifier['identity'].pubkey not in self._graph.keys(): - node_status = 0 - if certifier['identity'].pubkey == identity_account.pubkey: - node_status += NODE_STATUS_HIGHLIGHTED - if certifier['identity'].is_member(self.community) is False: - node_status += NODE_STATUS_OUT - self._graph[certifier['identity'].pubkey] = { - 'id': certifier['identity'].pubkey, - 'arcs': list(), - 'text': certifier['identity'].uid, - 'tooltip': certifier['identity'].pubkey, - 'status': node_status, - 'connected': [identity.pubkey] - } - - # keep only the latest certification - if self._graph[certifier['identity'].pubkey]['arcs']: - if certifier['cert_time'] < self._graph[certifier['identity'].pubkey]['arcs'][0]['cert_time']: + if self.community: + yield from self.refresh_signature_validity() + # add certifiers of uid + for certifier in tuple(certifier_list): + # add only valid certification... + if (time.time() - certifier['cert_time']) > self.signature_validity: continue - # display validity status - if (time.time() - certifier['cert_time']) > self.ARC_STATUS_STRONG_time: - arc_status = ARC_STATUS_WEAK - else: - arc_status = ARC_STATUS_STRONG + # new node + if certifier['identity'].pubkey not in self._graph.keys(): + node_status = 0 + is_member = yield from certifier['identity'].is_member(self.community) + if certifier['identity'].pubkey == identity_account.pubkey: + node_status += NODE_STATUS_HIGHLIGHTED + if is_member is False: + node_status += NODE_STATUS_OUT + self._graph[certifier['identity'].pubkey] = { + 'id': certifier['identity'].pubkey, + 'arcs': list(), + 'text': certifier['identity'].uid, + 'tooltip': certifier['identity'].pubkey, + 'status': node_status, + 'connected': [identity.pubkey] + } - arc = { - 'id': identity.pubkey, - 'status': arc_status, - 'tooltip': QLocale.toString( - QLocale(), - QDateTime.fromTime_t(certifier['cert_time'] + self.signature_validity).date(), - QLocale.dateFormat(QLocale(), QLocale.ShortFormat) - ), - 'cert_time': certifier['cert_time'] - } + # keep only the latest certification + if self._graph[certifier['identity'].pubkey]['arcs']: + if certifier['cert_time'] < self._graph[certifier['identity'].pubkey]['arcs'][0]['cert_time']: + continue + # display validity status + if (time.time() - certifier['cert_time']) > self.ARC_STATUS_STRONG_time: + arc_status = ARC_STATUS_WEAK + else: + arc_status = ARC_STATUS_STRONG - if certifier['block_number']: - current_validations = self.community.network.latest_block_number - certifier['block_number'] - else: - current_validations = 0 - max_validation = self.community.network.fork_window(self.community.members_pubkeys()) + 1 + arc = { + 'id': identity.pubkey, + 'status': arc_status, + 'tooltip': QLocale.toString( + QLocale(), + QDateTime.fromTime_t(certifier['cert_time'] + self.signature_validity).date(), + QLocale.dateFormat(QLocale(), QLocale.ShortFormat) + ), + 'cert_time': certifier['cert_time'] + } - # Current validation can be negative if self.community.network.latest_block_number - # is not refreshed yet - if max_validation > current_validations > 0: - if self.app.preferences['expert_mode']: - arc['validation_text'] = "{0}/{1}".format(current_validations, - max_validation) + if certifier['block_number']: + current_validations = self.community.network.latest_block_number - certifier['block_number'] else: - validation = current_validations / max_validation * 100 - arc['validation_text'] = "{0} %".format(QLocale().toString(float(validation), 'f', 0)) - else: - arc['validation_text'] = None + current_validations = 0 + members_pubkeys = yield from self.community.members_pubkeys() + max_validation = self.community.network.fork_window(members_pubkeys) + 1 + + # Current validation can be negative if self.community.network.latest_block_number + # is not refreshed yet + if max_validation > current_validations > 0: + if self.app.preferences['expert_mode']: + arc['validation_text'] = "{0}/{1}".format(current_validations, + max_validation) + else: + validation = current_validations / max_validation * 100 + arc['validation_text'] = "{0} %".format(QLocale().toString(float(validation), 'f', 0)) + else: + arc['validation_text'] = None - # add arc to certifier - self._graph[certifier['identity'].pubkey]['arcs'].append(arc) - # if certifier node not in identity nodes - if certifier['identity'].pubkey not in tuple(self._graph[identity.pubkey]['connected']): - # add certifier node to identity node - self._graph[identity.pubkey]['connected'].append(certifier['identity'].pubkey) + # add arc to certifier + self._graph[certifier['identity'].pubkey]['arcs'].append(arc) + # if certifier node not in identity nodes + if certifier['identity'].pubkey not in tuple(self._graph[identity.pubkey]['connected']): + # add certifier node to identity node + self._graph[identity.pubkey]['connected'].append(certifier['identity'].pubkey) + @asyncio.coroutine def add_certified_list(self, certified_list, identity, identity_account): """ Add list of certified from api to graph @@ -215,6 +239,7 @@ class Graph(object): :param identity identity_account: Account identity instance :return: """ + yield from self.refresh_signature_validity() # add certified by uid for certified in tuple(certified_list): # add only valid certification... @@ -222,9 +247,10 @@ class Graph(object): continue if certified['identity'].pubkey not in self._graph.keys(): node_status = 0 + is_member = yield from certified['identity'].is_member(self.community) if certified['identity'].pubkey == identity_account.pubkey: node_status += NODE_STATUS_HIGHLIGHTED - if certified['identity'].is_member(self.community) is False: + if is_member is False: node_status += NODE_STATUS_OUT self._graph[certified['identity'].pubkey] = { 'id': certified['identity'].pubkey, @@ -254,7 +280,8 @@ class Graph(object): current_validations = self.community.network.latest_block_number - certified['block_number'] else: current_validations = 0 - max_validations = self.community.network.fork_window(self.community.members_pubkeys()) + 1 + members_pubkeys = yield from self.community.members_pubkeys() + max_validations = self.community.network.fork_window(members_pubkeys) + 1 if max_validations > current_validations > 0: if self.app.preferences['expert_mode']: diff --git a/src/cutecoin/core/money/quant_zerosum.py b/src/cutecoin/core/money/quant_zerosum.py index 17a8dbc36027cd0686613100159ee6fcda7ccaf8..9c410877997a8a36c8a27c0ff7972cc1bd44f1ba 100644 --- a/src/cutecoin/core/money/quant_zerosum.py +++ b/src/cutecoin/core/money/quant_zerosum.py @@ -1,6 +1,6 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from . import Quantitative - +import asyncio class QuantitativeZSum: _NAME_STR_ = QT_TRANSLATE_NOOP('QuantitativeZSum', 'Quant Z-sum') @@ -24,6 +24,7 @@ class QuantitativeZSum: def diff_units(cls, currency): return QuantitativeZSum.units(currency) + @asyncio.coroutine def value(self): """ Return quantitative value of amount minus the average value @@ -32,18 +33,22 @@ class QuantitativeZSum: :param cutecoin.core.community.Community community: Community instance :return: int """ - ud_block = self.community.get_ud_block() + ud_block = yield from self.community.get_ud_block() if ud_block and ud_block['membersCount'] > 0: - average = self.community.monetary_mass / ud_block['membersCount'] + monetary_mass = yield from self.community.monetary_mass() + average = monetary_mass / ud_block['membersCount'] else: average = 0 return self.amount - average + @asyncio.coroutine def differential(self): - return Quantitative(self.amount, self.community, self.app).compute() + value = yield from Quantitative(self.amount, self.community, self.app).value() + return value + @asyncio.coroutine def localized(self, units=False, international_system=False): - value = self.value() + value = yield from self.value() if international_system: pass else: @@ -57,5 +62,7 @@ class QuantitativeZSum: else: return localized_value + @asyncio.coroutine def diff_localized(self, units=False, international_system=False): - return Quantitative(self.amount, self.community, self.app).localized(units, international_system) \ No newline at end of file + localized = yield from Quantitative(self.amount, self.community, self.app).localized(units, international_system) + return localized \ No newline at end of file diff --git a/src/cutecoin/core/money/quantitative.py b/src/cutecoin/core/money/quantitative.py index 7aac10fe79f60924521e3179e199be6f3aa37663..29d4a58bdf9bbb766b24d13678bff44ecca549f2 100644 --- a/src/cutecoin/core/money/quantitative.py +++ b/src/cutecoin/core/money/quantitative.py @@ -1,9 +1,9 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QObject, QLocale - +import asyncio class Quantitative(): _NAME_STR_ = QT_TRANSLATE_NOOP('Quantitative', 'Units') - _REF_STR_ = QT_TRANSLATE_NOOP('Quantitative', "{0} {1}") + _REF_STR_ = QT_TRANSLATE_NOOP('Quantitative', "{0} {1}{2}") _UNITS_STR_ = QT_TRANSLATE_NOOP('Quantitative', "{0}") def __init__(self, amount, community, app): @@ -23,6 +23,7 @@ class Quantitative(): def diff_units(cls, currency): return Quantitative.units(currency) + @asyncio.coroutine def value(self): """ Return quantitative value of amount @@ -33,8 +34,10 @@ class Quantitative(): """ return int(self.amount) + @asyncio.coroutine def differential(self): - return self.value() + value = yield from self.value() + return value def _to_si(self, value): prefixes = ['', 'k', 'M', 'G', 'T', 'P', 'E', 'Z', 'Y', 'z', 'y'] @@ -54,8 +57,9 @@ class Quantitative(): return localized_value, prefix + @asyncio.coroutine def localized(self, units=False, international_system=False): - value = self.value() + value = yield from self.value() prefix = "" if international_system: localized_value, prefix = self._to_si(value) @@ -71,8 +75,9 @@ class Quantitative(): else: return localized_value + @asyncio.coroutine def diff_localized(self, units=False, international_system=False): - value = self.differential() + value = yield from self.differential() prefix = "" if international_system: localized_value, prefix = self._to_si(value) diff --git a/src/cutecoin/core/money/relative.py b/src/cutecoin/core/money/relative.py index ea6492af150fc1d1e676784fa725e6e6d952e520..15cde10ef7bf7bf7193c7f1fa1784bd0819bad98 100644 --- a/src/cutecoin/core/money/relative.py +++ b/src/cutecoin/core/money/relative.py @@ -1,5 +1,5 @@ from PyQt5.QtCore import QObject, QCoreApplication, QT_TRANSLATE_NOOP, QLocale - +import asyncio class Relative(): _NAME_STR_ = QT_TRANSLATE_NOOP('Relative', 'UD') @@ -23,21 +23,24 @@ class Relative(): def diff_units(self, currency): return self.units(currency) + @asyncio.coroutine def value(self): """ - Return relaive value of amount - type + Return relative value of amount :param int amount: Value :param cutecoin.core.community.Community community: Community instance :return: float """ - if self.community.dividend > 0: - return self.amount / float(self.community.dividend) + dividend = yield from self.community.dividend() + if dividend > 0: + return self.amount / float(dividend) else: - return 0 + return self.amount + @asyncio.coroutine def differential(self): - return self.value() + value = yield from self.value() + return value def _to_si(self, value): prefixes = ['', 'd', 'c', 'm', 'µ', 'n', 'p', 'f', 'a', 'z', 'y'] @@ -45,7 +48,7 @@ class Relative(): prefix_index = 0 prefix = "" - while int(scientific_value) == 0: + while int(scientific_value) == 0 and scientific_value > 0.0: if prefix_index > 3: scientific_value *= 1000 else: @@ -60,8 +63,9 @@ class Relative(): return localized_value, prefix + @asyncio.coroutine def localized(self, units=False, international_system=False): - value = self.value() + value = yield from self.value() prefix = "" if international_system: localized_value, prefix = self._to_si(value) @@ -76,8 +80,9 @@ class Relative(): else: return localized_value + @asyncio.coroutine def diff_localized(self, units=False, international_system=False): - value = self.differential() + value = yield from self.differential() prefix = "" if international_system and value != 0: localized_value, prefix = self._to_si(value) diff --git a/src/cutecoin/core/money/relative_zerosum.py b/src/cutecoin/core/money/relative_zerosum.py index 788772acb957cca3d07a6db339add134a295520b..4202a72a5fd53a58fe301a76ea62ffeb4ab5f944 100644 --- a/src/cutecoin/core/money/relative_zerosum.py +++ b/src/cutecoin/core/money/relative_zerosum.py @@ -1,6 +1,6 @@ from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP, QLocale from .relative import Relative - +import asyncio class RelativeZSum: _NAME_STR_ = QT_TRANSLATE_NOOP('RelativeZSum', 'Relat Z-sum') @@ -24,6 +24,7 @@ class RelativeZSum: def diff_units(cls, currency): return RelativeZSum.units(currency) + @asyncio.coroutine def value(self): """ Return relative value of amount minus the average value @@ -32,20 +33,24 @@ class RelativeZSum: :param cutecoin.core.community.Community community: Community instance :return: float """ - ud_block = self.community.get_ud_block() + ud_block = yield from self.community.get_ud_block() if ud_block and ud_block['membersCount'] > 0: - median = self.community.monetary_mass / ud_block['membersCount'] - relative_value = self.amount / float(self.community.dividend) - relative_median = median / self.community.dividend + monetary_mass = yield from self.community.monetary_mass() + dividend = yield from self.community.dividend() + median = monetary_mass / ud_block['membersCount'] + relative_value = self.amount / float(dividend) + relative_median = median / dividend else: relative_median = 0 return relative_value - relative_median + @asyncio.coroutine def differential(self): return Relative(self.amount, self.community, self.app).value() + @asyncio.coroutine def localized(self, units=False, international_system=False): - value = self.value() + value = yield from self.value() if international_system: pass else: diff --git a/src/cutecoin/core/net/api/bma/__init__.py b/src/cutecoin/core/net/api/bma/__init__.py index 00af271a8ce89d969998265520ea61ba72879fae..9e6132c085043cb39514de46798e72763aa138a7 100644 --- a/src/cutecoin/core/net/api/bma/__init__.py +++ b/src/cutecoin/core/net/api/bma/__init__.py @@ -6,6 +6,7 @@ from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtCore import QUrl, QUrlQuery, QTimer, QObject, pyqtSlot import logging import asyncio +import json logger = logging.getLogger("ucoin") @@ -126,7 +127,14 @@ class API(object): logging.debug("POST : {0}".format(kwargs)) post_data = QUrlQuery() for k, v in kwargs.items(): - post_data.addQueryItem(k.replace("+", "%2b"), v.replace("+", "%2b")) + if type(k) is str: + k = k.replace("+", "%2b") + if type(v) is str: + v = v.replace("+", "%2b") + else: + v = json.dumps(v) + v = v.replace("+", "%2b") + post_data.addQueryItem(k, v) url = QUrl(self.reverse_url(path)) url.setQuery(post_data) diff --git a/src/cutecoin/core/net/api/bma/access.py b/src/cutecoin/core/net/api/bma/access.py index b77dd961edbedee64a2c42cc555f8a2caf71290f..1ee9ab476890087366aa0b3bef1750eb580f6749 100644 --- a/src/cutecoin/core/net/api/bma/access.py +++ b/src/cutecoin/core/net/api/bma/access.py @@ -130,74 +130,14 @@ class BmaAccess(QObject): self._data[cache_key] = {'metadata': {}, 'value': {}} + self._data[cache_key]['metadata']['block_number'] = self._network.latest_block_number + self._data[cache_key]['metadata']['block_hash'] = self._network.latest_block_hash + self._data[cache_key]['metadata']['cutecoin_version'] = __version__ if not self._compare_json(self._data[cache_key]['value'], data): - self._data[cache_key]['metadata']['block_number'] = self._network.latest_block_number - self._data[cache_key]['metadata']['block_hash'] = self._network.latest_block_hash - self._data[cache_key]['metadata']['cutecoin_version'] = __version__ self._data[cache_key]['value'] = data return True return False - def get(self, caller, request, req_args={}, get_args={}, tries=0): - """ - Get Json data from the specified URL and emit "inner_data_changed" - on the caller if the data changed. - - :param PyQt5.QtCore.QObject caller: The objet calling - :param class request: A bma request class calling for data - :param dict req_args: Arguments to pass to the request constructor - :param dict get_args: Arguments to pass to the request __get__ method - :return: The cached data - :rtype: dict - """ - - data = self._get_from_cache(request, req_args, get_args) - need_reload = data[0] - ret_data = data[1] - cache_key = BmaAccess._gen_cache_key(request, req_args, get_args) - - if need_reload: - # Move to network nstead of community - # after removing qthreads - if cache_key in self._pending_requests: - if caller not in self._pending_requests[cache_key]: - logging.debug("New caller".format(caller)) - self._pending_requests[cache_key].append(caller) - logging.debug("Callers".format(self._pending_requests[cache_key])) - else: - reply = self.simple_request(request, req_args, get_args) - logging.debug("New pending request {0}, caller {1}".format(cache_key, caller)) - self._pending_requests[cache_key] = [caller] - reply.finished.connect(lambda: self.handle_reply(request, req_args, get_args, tries)) - return ret_data - - @pyqtSlot(int, dict, dict, int) - def handle_reply(self, request, req_args, get_args, tries): - reply = self.sender() - logging.debug("Handling QtNetworkReply for {0}".format(str(request))) - cache_key = BmaAccess._gen_cache_key(request, req_args, get_args) - - if reply.error() == QNetworkReply.NoError: - strdata = bytes(reply.readAll()).decode('utf-8') - json_data = json.loads(strdata) - # If data changed, we emit a change signal to all callers - if self._update_cache(request, req_args, get_args, json_data): - logging.debug(self._pending_requests.keys()) - for caller in self._pending_requests[cache_key]: - logging.debug("Emit change for {0} : {1} ".format(caller, request)) - caller.inner_data_changed.emit(str(request)) - self._pending_requests.pop(cache_key) - else: - logging.debug("Error in reply : {0}".format(reply.error())) - if tries < 3: - tries += 1 - try: - pending_request = self._pending_requests.pop(cache_key) - for caller in pending_request: - self.get(caller, request, req_args, get_args, tries=tries) - except KeyError: - logging.debug("{0} is not present anymore in pending requests".format(cache_key)) - def future_request(self, request, req_args={}, get_args={}): """ Start a request to the network and returns a future. @@ -213,8 +153,9 @@ class BmaAccess(QObject): strdata = bytes(reply.readAll()).decode('utf-8') json_data = json.loads(strdata) self._update_cache(request, req_args, get_args, json_data) - future_data.set_result(json_data) - else: + if not future_data.cancelled(): + future_data.set_result(json_data) + elif not future_data.cancelled(): future_data.set_result(request.null_value) future_data = asyncio.Future() @@ -230,7 +171,7 @@ class BmaAccess(QObject): reply = req.get(**get_args) reply.finished.connect(lambda: handle_future_reply(reply)) else: - raise NoPeerAvailable(self.currency, len(nodes)) + raise NoPeerAvailable("", len(nodes)) else: future_data.set_result(data[1]) return future_data @@ -251,7 +192,7 @@ class BmaAccess(QObject): reply = req.get(**get_args) return reply else: - raise NoPeerAvailable(self.currency, len(nodes)) + raise NoPeerAvailable("", len(nodes)) def broadcast(self, request, req_args={}, post_args={}): """ diff --git a/src/cutecoin/core/net/network.py b/src/cutecoin/core/net/network.py index 9369afe5420f426af0058bb07184f867ca40031d..62b26dcf1689041346cfaf8a09f058aae2d409cf 100644 --- a/src/cutecoin/core/net/network.py +++ b/src/cutecoin/core/net/network.py @@ -12,7 +12,9 @@ import asyncio from ucoinpy.documents.peer import Peer from ucoinpy.documents.block import Block +from .api import bma as qtbma from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QTimer +from collections import Counter class Network(QObject): @@ -71,8 +73,8 @@ class Network(QObject): other_node._uid = node.uid other_node._version = node.version other_node._software = node.software - if other_node.block_hash != node.block_hash: - other_node.set_block(node.block_number, node.block_hash) + if other_node.block['hash'] != node.block['hash']: + other_node.set_block(node.block) other_node.last_change = node.last_change other_node.state = node.state @@ -163,8 +165,12 @@ class Network(QObject): Get the latest block considered valid It is the most frequent last block of every known nodes """ - blocks = [n.block_number for n in self.nodes] - return max(set(blocks), key=blocks.count) + blocks_numbers = [n.block['number'] for n in self.synced_nodes + if n.block != qtbma.blockchain.Block.null_value] + if len(blocks_numbers) > 0: + return blocks_numbers[0] + else: + return 0 @property def latest_block_hash(self): @@ -172,27 +178,88 @@ class Network(QObject): Get the latest block considered valid It is the most frequent last block of every known nodes """ - blocks = [n.block_hash for n in self.nodes if n.block_hash != Block.Empty_Hash] - if len(blocks) > 0: - return max(set(blocks), key=blocks.count) + blocks_hash = [n.block['hash'] for n in self.synced_nodes + if n.block != qtbma.blockchain.Block.null_value] + if len(blocks_hash) > 0: + return blocks_hash[0] else: return Block.Empty_Hash + def check_nodes_sync(self): + """ + Check nodes sync with the following rules : + 1 : The block of the majority + 2 : The more last different issuers + 3 : The more difficulty + 4 : The biggest number or timestamp + """ + # rule number 1 : block of the majority + blocks = [n.block['hash'] for n in self.nodes if n.block != qtbma.blockchain.Block.null_value] + blocks_occurences = Counter(blocks) + blocks_by_occurences = {} + for key, value in blocks_occurences.items(): + the_block = [n.block for n in self.nodes if n.block['hash'] == key][0] + if value not in blocks_by_occurences: + blocks_by_occurences[value] = [the_block] + else: + blocks_by_occurences[value].append(the_block) + + if len(blocks_by_occurences) == 0: + for n in [n for n in self._nodes if n.state in (Node.ONLINE, Node.DESYNCED)]: + n.state = Node.ONLINE + return + + most_present = max(blocks_by_occurences.keys()) + + if len(blocks_by_occurences[most_present]) > 1: + # rule number 2 : more last different issuers + # not possible atm + blocks_by_issuers = blocks_by_occurences.copy() + most_issuers = max(blocks_by_issuers.keys()) + if len(blocks_by_issuers[most_issuers]) > 1: + # rule number 3 : biggest PowMin + blocks_by_powmin = {} + for block in blocks_by_issuers[most_issuers]: + if block['powMin'] in blocks_by_powmin: + blocks_by_powmin[block['powMin']].append(block) + else: + blocks_by_powmin[block['powMin']] = [block] + bigger_powmin = max(blocks_by_powmin.keys()) + if len(blocks_by_powmin[bigger_powmin]) > 1: + # rule number 3 : latest timestamp + blocks_by_ts = {} + for block in blocks_by_powmin[bigger_powmin]: + blocks_by_ts[block['time']] = block + latest_ts = max(blocks_by_ts.keys()) + synced_block_hash = blocks_by_ts[latest_ts]['hash'] + else: + synced_block_hash = blocks_by_powmin[bigger_powmin][0]['hash'] + else: + synced_block_hash = blocks_by_issuers[most_issuers][0]['hash'] + else: + synced_block_hash = blocks_by_occurences[most_present][0]['hash'] + + for n in [n for n in self._nodes if n.state in (Node.ONLINE, Node.DESYNCED)]: + if n.block['hash'] == synced_block_hash: + n.state = Node.ONLINE + else: + n.state = Node.DESYNCED + def fork_window(self, members_pubkeys): """ Get the medium of the fork window of the nodes members of a community :return: the medium fork window of knew network """ - fork_windows = [n.fork_window for n in self.nodes if n.software != "" + fork_windows = [n.fork_window for n in self.online_nodes if n.software != "" and n.pubkey in members_pubkeys] if len(fork_windows) > 0: - return statistics.median(fork_windows) + return int(statistics.median(fork_windows)) else: return 0 def add_node(self, node): """ - Add a node to the network. + Add a nod to the network. """ self._nodes.append(node) node.changed.connect(self.handle_change) @@ -256,8 +323,7 @@ class Network(QObject): def handle_change(self): node = self.sender() if node.state in (Node.ONLINE, Node.DESYNCED): - for nd in [n for n in self._nodes if n.state in (Node.ONLINE, Node.DESYNCED)]: - nd.check_sync(self.latest_block_hash) + self.check_nodes_sync() self.nodes_changed.emit() else: if node.last_change + 3600 < time.time(): @@ -269,4 +335,5 @@ class Network(QObject): if self._block_found != self.latest_block_hash and node.state == Node.ONLINE: logging.debug("Latest block changed : {0}".format(self.latest_block_number)) self._block_found = self.latest_block_hash + # Do not emit block change for empty block self.new_block_mined.emit(self.latest_block_number) diff --git a/src/cutecoin/core/net/node.py b/src/cutecoin/core/net/node.py index bf7fe9a6c2e552d5c6fb3f3a3a66eef721a1ee03..38aaf17d5989986e0a063ba1af6f72d8b3e359ec 100644 --- a/src/cutecoin/core/net/node.py +++ b/src/cutecoin/core/net/node.py @@ -38,7 +38,7 @@ class Node(QObject): changed = pyqtSignal() neighbour_found = pyqtSignal(Peer, str) - def __init__(self, network_manager, currency, endpoints, uid, pubkey, block_number, block_hash, + def __init__(self, network_manager, currency, endpoints, uid, pubkey, block, state, last_change, last_merkle, software, version, fork_window): """ Constructor @@ -48,8 +48,7 @@ class Node(QObject): self._endpoints = endpoints self._uid = uid self._pubkey = pubkey - self._block_number = block_number - self._block_hash = block_hash + self._block = block self._state = state self._neighbours = [] self._currency = currency @@ -95,7 +94,7 @@ class Node(QObject): node = cls(network_manager, peer.currency, [Endpoint.from_inline(e.inline()) for e in peer.endpoints], - "", peer.pubkey, 0, Block.Empty_Hash, Node.ONLINE, time.time(), + "", peer.pubkey, qtbma.blockchain.Block.null_value, Node.ONLINE, time.time(), {'root': "", 'leaves': []}, "", "", 0) logging.debug("Node from address : {:}".format(str(node))) return node @@ -117,7 +116,7 @@ class Node(QObject): node = cls(network_manager, peer.currency, [Endpoint.from_inline(e.inline()) for e in peer.endpoints], - "", pubkey, 0, Block.Empty_Hash, + "", pubkey, qtbma.blockchain.Block.null_value, Node.ONLINE, time.time(), {'root': "", 'leaves': []}, "", "", 0) @@ -132,8 +131,7 @@ class Node(QObject): software = "" version = "" fork_window = 0 - block_number = 0 - block_hash = Block.Empty_Hash + block = qtbma.blockchain.Block.null_value last_change = time.time() state = Node.ONLINE logging.debug(data) @@ -152,11 +150,8 @@ class Node(QObject): if 'last_change' in data: last_change = data['last_change'] - if 'block_number' in data: - block_number = data['block_number'] - - if 'block_hash' in data: - block_hash = data['block_hash'] + if 'block' in data: + block = data['block'] if 'state' in data: state = data['state'] @@ -171,7 +166,7 @@ class Node(QObject): fork_window = data['fork_window'] node = cls(network_manager, currency, endpoints, - uid, pubkey, block_number, block_hash, + uid, pubkey, block, state, last_change, {'root': "", 'leaves': []}, software, version, fork_window) @@ -196,8 +191,7 @@ class Node(QObject): 'currency': self._currency, 'state': self._state, 'last_change': self._last_change, - 'block_number': self.block_number, - 'block_hash': self.block_hash, + 'block': self.block, 'software': self._software, 'version': self._version, 'fork_window': self._fork_window @@ -217,16 +211,11 @@ class Node(QObject): return next((e for e in self._endpoints if type(e) is BMAEndpoint)) @property - def block_number(self): - return self._block_number - - @property - def block_hash(self): - return self._block_hash + def block(self): + return self._block - def set_block(self, block_number, block_hash): - self._block_number = block_number - self._block_hash = block_hash + def set_block(self, block): + self._block = block @property def state(self): @@ -292,13 +281,6 @@ class Node(QObject): self._fork_window = new_fork_window self.changed.emit() - def check_sync(self, block_hash): - logging.debug("Check sync") - if self.block_hash != block_hash: - self.state = Node.DESYNCED - else: - self.state = Node.ONLINE - def check_noerror(self, error_code, status_code): if error_code == QNetworkReply.NoError: if status_code in (200, 404): @@ -337,15 +319,14 @@ class Node(QObject): if status_code == 200: strdata = bytes(reply.readAll()).decode('utf-8') block_data = json.loads(strdata) - block_number = block_data['number'] block_hash = block_data['hash'] elif status_code == 404: - self.set_block(0, Block.Empty_Hash) + self.set_block(qtbma.blockchain.Block.null_value) - if block_hash != self.block_hash: - self.set_block(block_number, block_hash) - logging.debug("Changed block {0} -> {1}".format(self.block_number, - block_number)) + if block_hash != self.block['hash']: + self.set_block(block_data) + logging.debug("Changed block {0} -> {1}".format(self.block['number'], + block_data['number'])) self.changed.emit() else: @@ -488,5 +469,5 @@ class Node(QObject): self.changed.emit() def __str__(self): - return ','.join([str(self.pubkey), str(self.endpoint.server), str(self.endpoint.port), str(self.block_number), + return ','.join([str(self.pubkey), str(self.endpoint.server), str(self.endpoint.ipv4), str(self.endpoint.port), str(self.block['number']), str(self.currency), str(self.state), str(self.neighbours)]) diff --git a/src/cutecoin/core/registry/identities.py b/src/cutecoin/core/registry/identities.py index 65657deeb68a749f783c0bbc7dc8dcbe7abc41ff..eea2f3d778697ee7dde857170215c25b1dee3072 100644 --- a/src/cutecoin/core/registry/identities.py +++ b/src/cutecoin/core/registry/identities.py @@ -42,35 +42,14 @@ class IdentitiesRegistry: identities_json.append(identity.jsonify()) return {'registry': identities_json} - def find(self, pubkey, community): - """ - Get a person from the pubkey found in a community - - :param str pubkey: The person pubkey - :param cutecoin.core.community.Community community: The community in which to look for the pubkey - :param bool cached: True if the person should be searched in the - cache before requesting the community. - - :return: A new person if the pubkey was unknown or\ - the known instance if pubkey was already known. - :rtype: cutecoin.core.registry.Identity - """ - if pubkey in self._instances: - identity = self._instances[pubkey] - self._instances[pubkey] = identity - else: - identity = Identity.empty(pubkey) - self._instances[pubkey] = identity - reply = community.bma_access.simple_request(qtbma.wot.CertifiersOf, req_args={'search': pubkey}) - reply.finished.connect(lambda: self.handle_certifiersof(reply, identity, community)) - return identity - @asyncio.coroutine def future_find(self, pubkey, community): def handle_certifiersof_reply(reply, tries=0): err = reply.error() # https://github.com/ucoin-io/ucoin/issues/146 - if reply.error() == QNetworkReply.NoError or reply.error() == QNetworkReply.ProtocolInvalidOperationError: + if reply.error() == QNetworkReply.NoError \ + or reply.error() == QNetworkReply.ContentNotFoundError \ + or reply.error() == QNetworkReply.ProtocolInvalidOperationError: status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) if status_code == 200: strdata = bytes(reply.readAll()).decode('utf-8') @@ -80,7 +59,8 @@ class IdentitiesRegistry: identity.local_state = LocalState.PARTIAL identity.blockchain_state = BlockchainState.VALIDATED logging.debug("Lookup : found {0}".format(identity)) - future_identity.set_result(True) + if not future_identity.cancelled(): + future_identity.set_result(identity) else: reply = community.bma_access.simple_request(qtbma.wot.Lookup, req_args={'search': pubkey}) @@ -88,8 +68,8 @@ class IdentitiesRegistry: elif tries < 3: reply = community.bma_access.simple_request(qtbma.wot.CertifiersOf, req_args={'search': pubkey}) reply.finished.connect(lambda: handle_certifiersof_reply(reply, tries=tries+1)) - else: - future_identity.set_result(True) + elif not future_identity.cancelled(): + future_identity.set_result(identity) def handle_lookup_reply(reply, tries=0): status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) @@ -110,88 +90,28 @@ class IdentitiesRegistry: identity.blockchain_state = BlockchainState.BUFFERED identity.local_state = LocalState.PARTIAL logging.debug("Lookup : found {0}".format(identity)) - future_identity.set_result(True) + if not future_identity.cancelled(): + future_identity.set_result(identity) return - future_identity.set_result(True) + if not future_identity.cancelled(): + future_identity.set_result(identity) elif tries < 3: reply = community.bma_access.simple_request(qtbma.wot.Lookup, req_args={'search': pubkey}) reply.finished.connect(lambda: handle_lookup_reply(reply, tries=tries+1)) - else: - future_identity.set_result(True) + elif not future_identity.cancelled(): + future_identity.set_result(identity) future_identity = asyncio.Future() if pubkey in self._instances: identity = self._instances[pubkey] - future_identity.set_result(True) + if not future_identity.cancelled(): + future_identity.set_result(identity) else: identity = Identity.empty(pubkey) self._instances[pubkey] = identity reply = community.bma_access.simple_request(qtbma.wot.CertifiersOf, req_args={'search': pubkey}) reply.finished.connect(lambda: handle_certifiersof_reply(reply)) - yield from future_identity - return identity - - def handle_certifiersof(self, reply, identity, community, tries=0): - """ - :param PyQt5.QtNetwork.QNetworkReply reply - :param cutecoin.core.registry.identity.Identity identity: The looked up identity - :return: - """ - if reply.error() == QNetworkReply.NoError: - status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) - if status_code == 200: - strdata = bytes(reply.readAll()).decode('utf-8') - data = json.loads(strdata) - identity.uid = data['uid'] - identity.local_state = LocalState.PARTIAL - identity.blockchain_state = BlockchainState.VALIDATED - logging.debug("Lookup : found {0}".format(identity)) - identity.inner_data_changed.emit(str(qtbma.wot.CertifiersOf)) - else: - reply = community.bma_access.simple_request(qtbma.wot.Lookup, - req_args={'search': identity.pubkey}) - reply.finished.connect(lambda: self.handle_lookup(reply, identity, - community, tries=0)) - else: - logging.debug("Error in reply : {0}".format(reply.error())) - if tries < 3: - tries += 1 - reply = community.bma_access.simple_request(qtbma.wot.CertifiersOf, - req_args={'search': identity.pubkey}) - reply.finished.connect(lambda: self.handle_certifiersof(reply, identity, - community, tries=tries)) - - def handle_lookup(self, reply, identity, community, tries=0): - """ - :param cutecoin.core.registry.identity.Identity identity: The looked up identity - :return: - """ - - if reply.error() == QNetworkReply.NoError: - strdata = bytes(reply.readAll()).decode('utf-8') - data = json.loads(strdata) - - timestamp = 0 - for result in data['results']: - if result["pubkey"] == identity.pubkey: - uids = result['uids'] - identity_uid = "" - for uid_data in uids: - if uid_data["meta"]["timestamp"] > timestamp: - timestamp = uid_data["meta"]["timestamp"] - identity_uid = uid_data["uid"] - identity.uid = identity_uid - identity.status = Identity.FOUND - logging.debug("Lookup : found {0}".format(identity)) - identity.inner_data_changed.emit(str(qtbma.wot.Lookup)) - else: - logging.debug("Error in reply : {0}".format(reply.error())) - if tries < 3: - tries += 1 - reply = community.bma_access.simple_request(qtbma.wot.Lookup, - req_args={'search': identity.pubkey}) - reply.finished.connect(lambda: self.handle_lookup(reply, identity, - community, tries=tries)) + return future_identity def from_handled_data(self, uid, pubkey, blockchain_state): """ diff --git a/src/cutecoin/core/registry/identity.py b/src/cutecoin/core/registry/identity.py index d5930684cad896617404aa661be3abaabbf50eea..fb65737c7cf07982ea385fe2968737ab47dc5c5b 100644 --- a/src/cutecoin/core/registry/identity.py +++ b/src/cutecoin/core/registry/identity.py @@ -10,10 +10,10 @@ import asyncio from enum import Enum from ucoinpy.documents.certification import SelfCertification -from cutecoin.tools.exceptions import Error, NoPeerAvailable,\ +from ...tools.exceptions import Error, NoPeerAvailable,\ MembershipNotFoundError -from cutecoin.core.net.api import bma as qtbma -from cutecoin.core.net.api.bma import PROTOCOL_VERSION +from ..net.api import bma as qtbma +from ..net.api.bma import PROTOCOL_VERSION from PyQt5.QtCore import QObject, pyqtSignal @@ -48,8 +48,6 @@ class Identity(QObject): """ A person with a uid and a pubkey """ - inner_data_changed = pyqtSignal(str) - def __init__(self, uid, pubkey, local_state, blockchain_state): """ Initializing a person object. @@ -117,6 +115,7 @@ class Identity(QObject): signature) return None + @asyncio.coroutine def get_join_date(self, community): """ Get the person join date. @@ -125,11 +124,11 @@ class Identity(QObject): :param cutecoin.core.community.Community community: The community target to request the join date :return: A datetime object """ - search = community.bma_access.get(self, qtbma.blockchain.Membership, {'search': self.pubkey}) + search = yield from community.bma_access.future_request(qtbma.blockchain.Membership, {'search': self.pubkey}) if search != qtbma.blockchain.Membership.null_value: if len(search['memberships']) > 0: membership_data = search['memberships'][0] - block = community.bma_access.get(self, qtbma.blockchain.Block, + block = yield from community.bma_access.future_request(qtbma.blockchain.Block, req_args={'number': membership_data['blockNumber']}) if block != qtbma.blockchain.Block.null_value: return block['medianTime'] @@ -137,14 +136,16 @@ class Identity(QObject): else: raise MembershipNotFoundError(self.pubkey, community.name) + @asyncio.coroutine def get_expiration_date(self, community): try: - join_block_number = self.membership(community)['blockNumber'] + membership = yield from self.membership(community) + join_block_number = membership['blockNumber'] try: - join_block = community.bma_access.get(self, qtbma.blockchain.Block, + join_block = yield from community.bma_access.future_request(qtbma.blockchain.Block, req_args={'number': join_block_number}) - parameters = community.bma_access.get(self, qtbma.blockchain.Parameters) + parameters = yield from community.bma_access.future_request(qtbma.blockchain.Parameters) if join_block != qtbma.blockchain.Block.null_value \ and parameters != qtbma.blockchain.Parameters.null_value: join_date = join_block['medianTime'] @@ -159,6 +160,7 @@ class Identity(QObject): #TODO: Manage 'OUT' memberships ? Maybe ? + @asyncio.coroutine def membership(self, community): """ Get the person last membership document. @@ -166,7 +168,7 @@ class Identity(QObject): :param cutecoin.core.community.Community community: The community target to request the join date :return: The membership data in BMA json format """ - search = community.bma_access.get(self, qtbma.blockchain.Membership, + search = yield from community.bma_access.future_request(qtbma.blockchain.Membership, {'search': self.pubkey}) if search != qtbma.blockchain.Membership.null_value: block_number = -1 @@ -182,8 +184,9 @@ class Identity(QObject): else: raise MembershipNotFoundError(self.pubkey, community.name) + @asyncio.coroutine def published_uid(self, community): - data = community.bma_access.get(self, qtbma.wot.Lookup, + data = yield from community.bma_access.future_request(qtbma.wot.Lookup, req_args={'search': self.pubkey}) if data != qtbma.wot.Lookup.null_value: timestamp = 0 @@ -200,6 +203,7 @@ class Identity(QObject): return True return False + @asyncio.coroutine def is_member(self, community): """ Check if the person is a member of a community @@ -207,11 +211,12 @@ class Identity(QObject): :param cutecoin.core.community.Community community: The community target to request the join date :return: True if the person is a member of a community """ - certifiers = community.bma_access.get(self, qtbma.wot.CertifiersOf, {'search': self.pubkey}) + certifiers = yield from community.bma_access.future_request(qtbma.wot.CertifiersOf, {'search': self.pubkey}) if certifiers != qtbma.wot.CertifiersOf.null_value: return certifiers['isMember'] return False + @asyncio.coroutine def certifiers_of(self, identities_registry, community): """ Get the list of this person certifiers @@ -220,13 +225,13 @@ class Identity(QObject): :param cutecoin.core.community.Community community: The community target to request the join date :return: The list of the certifiers of this community """ - data = community.bma_access.get(self, qtbma.wot.CertifiersOf, {'search': self.pubkey}) + data = yield from community.bma_access.future_request(qtbma.wot.CertifiersOf, {'search': self.pubkey}) certifiers = list() if data == qtbma.wot.CertifiersOf.null_value: logging.debug('bma.wot.CertifiersOf request error') - data = community.bma_access.get(self, qtbma.wot.Lookup, {'search': self.pubkey}) + data = yield from community.bma_access.future_request(qtbma.wot.Lookup, {'search': self.pubkey}) if data == qtbma.wot.Lookup.null_value: logging.debug('bma.wot.Lookup request error') else: @@ -240,7 +245,7 @@ class Identity(QObject): certifier['identity'] = identities_registry.from_handled_data(uid, certifier_data['pubkey'], BlockchainState.BUFFERED) - block = community.bma_access.get(self, qtbma.blockchain.Block, + block = yield from community.bma_access.future_request(qtbma.blockchain.Block, {'number': certifier_data['meta']['block_number']}) certifier['cert_time'] = block['medianTime'] certifier['block_number'] = None @@ -257,13 +262,15 @@ class Identity(QObject): certifiers.append(certifier) return certifiers + @asyncio.coroutine def unique_valid_certifiers_of(self, identities_registry, community): - certifier_list = self.certifiers_of(identities_registry, community) + certifier_list = yield from self.certifiers_of(identities_registry, community) unique_valid = [] # add certifiers of uid for certifier in tuple(certifier_list): # add only valid certification... - if community.certification_expired(certifier['cert_time']): + cert_expired = yield from community.certification_expired(certifier['cert_time']) + if cert_expired: continue # keep only the latest certification @@ -276,6 +283,7 @@ class Identity(QObject): unique_valid.append(certifier) return unique_valid + @asyncio.coroutine def certified_by(self, identities_registry, community): """ Get the list of persons certified by this person @@ -283,11 +291,11 @@ class Identity(QObject): :param cutecoin.core.community.Community community: The community target to request the join date :return: The list of the certified persons of this community in BMA json format """ - data = community.bma_access.get(self, qtbma.wot.CertifiedBy, {'search': self.pubkey}) + data = yield from community.bma_access.future_request(qtbma.wot.CertifiedBy, {'search': self.pubkey}) certified_list = list() if data == qtbma.wot.CertifiedBy.null_value: logging.debug('bma.wot.CertifiersOf request error') - data = community.bma_access.get(self, qtbma.wot.Lookup, {'search': self.pubkey}) + data = yield from community.bma_access.future_request(qtbma.wot.Lookup, {'search': self.pubkey}) if data == qtbma.wot.Lookup.null_value: logging.debug('bma.wot.Lookup request error') else: @@ -312,13 +320,15 @@ class Identity(QObject): certified_list.append(certified) return certified_list + @asyncio.coroutine def unique_valid_certified_by(self, identities_registry, community): - certified_list = self.certified_by(identities_registry, community) + certified_list = yield from self.certified_by(identities_registry, community) unique_valid = [] # add certifiers of uid for certified in tuple(certified_list): # add only valid certification... - if community.certification_expired(certified['cert_time']): + cert_expired = yield from community.certification_expired(certified['cert_time']) + if cert_expired: continue # keep only the latest certification @@ -331,10 +341,13 @@ class Identity(QObject): unique_valid.append(certified) return unique_valid + @asyncio.coroutine def membership_expiration_time(self, community): - join_block = self.membership(community)['blockNumber'] - join_date = community.get_block(join_block)['medianTime'] - parameters = community.parameters + membership = yield from self.membership(community) + join_block = membership['blockNumber'] + block = yield from community.get_block(join_block) + join_date = block['medianTime'] + parameters = yield from community.parameters() expiration_date = join_date + parameters['sigValidity'] current_time = time.time() return expiration_date - current_time diff --git a/src/cutecoin/core/txhistory.py b/src/cutecoin/core/txhistory.py index 24d993d083732f5ab41b451d584354360da01d9a..6105ac21ca128a912199a21318cce90a7df08fb9 100644 --- a/src/cutecoin/core/txhistory.py +++ b/src/cutecoin/core/txhistory.py @@ -1,7 +1,9 @@ import asyncio import logging +import hashlib from .transfer import Transfer from ucoinpy.documents.transaction import InputSource, OutputSource +from ucoinpy.documents.block import Block from ..tools.exceptions import LookupFailureError from .net.api import bma as qtbma @@ -71,29 +73,29 @@ class TxHistory(): self._stop_coroutines = True @staticmethod + @asyncio.coroutine def _validation_state(community, block_number, current_block): - if block_number + community.network.fork_window(community.members_pubkeys()) + 1 < current_block["number"]: + members_pubkeys = yield from community.members_pubkeys() + if block_number + community.network.fork_window(members_pubkeys) + 1 < current_block["number"]: state = Transfer.VALIDATED else: state = Transfer.VALIDATING return state @asyncio.coroutine - def _parse_transaction(self, community, txdata, received_list, txid, current_block): - tx_outputs = [OutputSource.from_inline(o) for o in txdata['outputs']] - receivers = [o.pubkey for o in tx_outputs - if o.pubkey != txdata['issuers'][0]] - - block_number = txdata['block_number'] + def _parse_transaction(self, community, tx, block_number, + mediantime, received_list, + current_block, txid): + receivers = [o.pubkey for o in tx.outputs + if o.pubkey != tx.issuers[0]] - mediantime = txdata['time'] - state = TxHistory._validation_state(community, block_number, current_block) + state = yield from TxHistory._validation_state(community, block_number, current_block) if len(receivers) == 0: - receivers = [txdata['issuers'][0]] + receivers = [tx.issuers[0]] try: - issuer = yield from self.wallet._identities_registry.future_find(txdata['issuers'][0], community) + issuer = yield from self.wallet._identities_registry.future_find(tx.issuers[0], community) issuer_uid = issuer.uid except LookupFailureError: issuer_uid = "" @@ -106,57 +108,96 @@ class TxHistory(): metadata = {'block': block_number, 'time': mediantime, - 'comment': txdata['comment'], - 'issuer': txdata['issuers'][0], + 'comment': tx.comment, + 'issuer': tx.issuers[0], 'issuer_uid': issuer_uid, 'receiver': receivers[0], 'receiver_uid': receiver_uid, 'txid': txid} - in_issuers = len([i for i in txdata['issuers'] + in_issuers = len([i for i in tx.issuers if i == self.wallet.pubkey]) > 0 - in_outputs = len([o for o in tx_outputs + in_outputs = len([o for o in tx.outputs if o.pubkey == self.wallet.pubkey]) > 0 awaiting = [t for t in self._transfers if t.state in (Transfer.AWAITING, Transfer.VALIDATING)] # We check if the transaction correspond to one we sent # but not from this cutecoin Instance - if txdata['hash'] not in [t.hash for t in awaiting]: + tx_hash = hashlib.sha1(tx.signed_raw().encode("ascii")).hexdigest().upper() + if tx_hash not in [t.hash for t in awaiting]: # If the wallet pubkey is in the issuers we sent this transaction if in_issuers: - outputs = [o for o in tx_outputs + outputs = [o for o in tx.outputs if o.pubkey != self.wallet.pubkey] amount = 0 for o in outputs: amount += o.amount metadata['amount'] = amount - transfer = Transfer.create_from_blockchain(txdata['hash'], + transfer = Transfer.create_from_blockchain(tx_hash, state, metadata.copy()) return transfer # If we are not in the issuers, # maybe it we are in the recipients of this transaction elif in_outputs: - outputs = [o for o in tx_outputs + outputs = [o for o in tx.outputs if o.pubkey == self.wallet.pubkey] amount = 0 for o in outputs: amount += o.amount metadata['amount'] = amount - if txdata['hash'] not in [t.hash for t in awaiting]: - transfer = Transfer.create_from_blockchain(txdata['hash'], + if tx_hash not in [t.hash for t in awaiting]: + transfer = Transfer.create_from_blockchain(tx_hash, state, metadata.copy()) received_list.append(transfer) return transfer else: - transfer = [t for t in awaiting if t.hash == txdata['hash']][0] - transfer.check_registered(txdata['hash'], current_block['number'], mediantime, + transfer = [t for t in awaiting if t.hash == tx_hash][0] + + transfer.check_registered(tx_hash, current_block['number'], mediantime, community.network.fork_window(community.members_pubkeys()) + 1) return None + @asyncio.coroutine + def _parse_block(self, community, block_number, received_list, current_block, txmax): + block = yield from community.bma_access.future_request(qtbma.blockchain.Block, + req_args={'number': block_number}) + signed_raw = "{0}{1}\n".format(block['raw'], + block['signature']) + transfers = [] + try: + block_doc = Block.from_signed_raw(signed_raw) + except: + logging.debug("Error in {0}".format(block_number)) + raise + for (txid, tx) in enumerate(block_doc.transactions): + transfer = yield from self._parse_transaction(community, tx, block_number, + block_doc.mediantime, received_list, + current_block, txid+txmax) + if transfer != None: + logging.debug("Transfer amount : {0}".format(transfer.metadata['amount'])) + transfers.append(transfer) + else: + logging.debug("None transfer") + return transfers + + @asyncio.coroutine + def request_dividends(self, community, parsed_block): + dividends_data = qtbma.ud.History.null_value + for i in range(0, 6): + if dividends_data == qtbma.ud.History.null_value: + dividends_data = yield from community.bma_access.future_request(qtbma.ud.History, + req_args={'pubkey': self.wallet.pubkey}) + + dividends = dividends_data['history']['history'] + for d in dividends: + if d['block_number'] < parsed_block: + dividends.remove(d) + return dividends + @asyncio.coroutine def refresh(self, community, received_list): """ @@ -167,43 +208,27 @@ class TxHistory(): """ current_block = yield from community.bma_access.future_request(qtbma.blockchain.Block, req_args={'number': community.network.latest_block_number}) - + members_pubkeys = yield from community.members_pubkeys() # We look for the first block to parse, depending on awaiting and validating transfers and ud... - blocks = [tx.metadata['block_number'] for tx in self._transfers + blocks = [tx.metadata['block'] for tx in self._transfers if tx.state in (Transfer.AWAITING, Transfer.VALIDATING)] +\ [ud['block_number'] for ud in self._dividends if ud['state'] in (Transfer.AWAITING, Transfer.VALIDATING)] +\ - [max(0, self.latest_block - community.network.fork_window(community.members_pubkeys()))] + [max(0, self.latest_block - community.network.fork_window(members_pubkeys))] parsed_block = min(set(blocks)) logging.debug("Refresh from : {0} to {1}".format(self.latest_block, current_block['number'])) - dividends_data = qtbma.ud.History.null_value - while dividends_data == qtbma.ud.History.null_value: - dividends_data = yield from community.bma_access.future_request(qtbma.ud.History, - req_args={'pubkey': self.wallet.pubkey}) - - dividends = dividends_data['history']['history'] - for d in dividends: - if d['block_number'] < parsed_block: - dividends.remove(d) - + dividends = yield from self.request_dividends(community, parsed_block) + with_tx_data = yield from community.bma_access.future_request(qtbma.blockchain.TX) + blocks_with_tx = with_tx_data['result']['blocks'] new_transfers = [] new_dividends = [] # Lets look if transactions took too long to be validated awaiting = [t for t in self._transfers if t.state == Transfer.AWAITING] while parsed_block < current_block['number']: - tx_history = qtbma.tx.history.Blocks.null_value - while tx_history == qtbma.tx.history.Blocks.null_value: - tx_history = yield from community.bma_access.future_request(qtbma.tx.history.Blocks, - req_args={'pubkey': self.wallet.pubkey, - 'from_':str(parsed_block), - 'to_': str(parsed_block + 99)}) - if self._stop_coroutines: - return - udid = 0 - for d in [ud for ud in dividends if ud['block_number'] in range(parsed_block, parsed_block+100)]: - state = TxHistory._validation_state(community, d['block_number'], current_block) + for d in [ud for ud in dividends if ud['block_number'] == parsed_block]: + state = yield from TxHistory._validation_state(community, d['block_number'], current_block) if d['block_number'] not in [ud['block_number'] for ud in self._dividends]: d['id'] = udid @@ -216,22 +241,14 @@ class TxHistory(): known_dividend['state'] = state # We parse only blocks with transactions - transactions = tx_history['history']['received'] + tx_history['history']['sent'] - for (txid, txdata) in enumerate(transactions): - if self._stop_coroutines: - return - if len(txdata['issuers']) == 0: - logging.debug("Error with : {0}, from {1} to {2}".format(self.wallet.pubkey, - parsed_block, - current_block['number'])) - else: - transfer = yield from self._parse_transaction(community, txdata, received_list, - udid + txid, current_block) - if transfer: - new_transfers.append(transfer) + if parsed_block in blocks_with_tx: + transfers = yield from self._parse_block(community, parsed_block, + received_list, current_block, + udid + len(new_transfers)) + new_transfers += transfers self.wallet.refresh_progressed.emit(parsed_block, current_block['number'], self.wallet.pubkey) - parsed_block += 100 + parsed_block += 1 if current_block['number'] > self.latest_block: self.available_sources = yield from self.wallet.future_sources(community) diff --git a/src/cutecoin/core/txhistory_indexation.py b/src/cutecoin/core/txhistory_indexation.py new file mode 100644 index 0000000000000000000000000000000000000000..399f1f064bf7f5a385b6f156a55dae6fce5b34de --- /dev/null +++ b/src/cutecoin/core/txhistory_indexation.py @@ -0,0 +1,255 @@ +import asyncio +import logging +from .transfer import Transfer +from ucoinpy.documents.transaction import InputSource, OutputSource +from ..tools.exceptions import LookupFailureError +from .net.api import bma as qtbma + + +class TxHistory(): + def __init__(self, app, wallet): + self._latest_block = 0 + self.wallet = wallet + self.app = app + self._stop_coroutines = False + + self._transfers = [] + self.available_sources = [] + self._dividends = [] + + @property + def latest_block(self): + return self._latest_block + + @latest_block.setter + def latest_block(self, value): + self._latest_block = value + + def load_from_json(self, data): + self._transfers = [] + + data_sent = data['transfers'] + for s in data_sent: + self._transfers.append(Transfer.load(s)) + + for s in data['sources']: + self.available_sources.append(InputSource.from_inline(s['inline'])) + + for d in data['dividends']: + self._dividends.append(d) + + self.latest_block = data['latest_block'] + + def jsonify(self): + data_transfer = [] + for s in self.transfers: + data_transfer.append(s.jsonify()) + + data_sources = [] + for s in self.available_sources: + s.index = 0 + data_sources.append({'inline': "{0}\n".format(s.inline())}) + + data_dividends = [] + for d in self._dividends: + data_dividends.append(d) + + return {'latest_block': self.latest_block, + 'transfers': data_transfer, + 'sources': data_sources, + 'dividends': data_dividends} + + @property + def transfers(self): + return [t for t in self._transfers if t.state != Transfer.DROPPED] + + @property + def dividends(self): + return self._dividends.copy() + + def stop_coroutines(self): + self._stop_coroutines = True + + @staticmethod + @asyncio.coroutine + def _validation_state(community, block_number, current_block): + members_pubkeys = yield from community.members_pubkeys() + if block_number + community.network.fork_window(members_pubkeys) + 1 < current_block["number"]: + state = Transfer.VALIDATED + else: + state = Transfer.VALIDATING + return state + + @asyncio.coroutine + def _parse_transaction(self, community, txdata, received_list, txid, current_block): + tx_outputs = [OutputSource.from_inline(o) for o in txdata['outputs']] + receivers = [o.pubkey for o in tx_outputs + if o.pubkey != txdata['issuers'][0]] + + block_number = txdata['block_number'] + + mediantime = txdata['time'] + state = yield from TxHistory._validation_state(community, block_number, current_block) + + if len(receivers) == 0: + receivers = [txdata['issuers'][0]] + + try: + issuer = yield from self.wallet._identities_registry.future_find(txdata['issuers'][0], community) + issuer_uid = issuer.uid + except LookupFailureError: + issuer_uid = "" + + try: + receiver = yield from self.wallet._identities_registry.future_find(receivers[0], community) + receiver_uid = receiver.uid + except LookupFailureError: + receiver_uid = "" + + metadata = {'block': block_number, + 'time': mediantime, + 'comment': txdata['comment'], + 'issuer': txdata['issuers'][0], + 'issuer_uid': issuer_uid, + 'receiver': receivers[0], + 'receiver_uid': receiver_uid, + 'txid': txid} + + in_issuers = len([i for i in txdata['issuers'] + if i == self.wallet.pubkey]) > 0 + in_outputs = len([o for o in tx_outputs + if o.pubkey == self.wallet.pubkey]) > 0 + awaiting = [t for t in self._transfers + if t.state in (Transfer.AWAITING, Transfer.VALIDATING)] + + # We check if the transaction correspond to one we sent + # but not from this cutecoin Instance + if txdata['hash'] not in [t.hash for t in awaiting]: + # If the wallet pubkey is in the issuers we sent this transaction + if in_issuers: + outputs = [o for o in tx_outputs + if o.pubkey != self.wallet.pubkey] + amount = 0 + for o in outputs: + amount += o.amount + metadata['amount'] = amount + transfer = Transfer.create_from_blockchain(txdata['hash'], + state, + metadata.copy()) + return transfer + # If we are not in the issuers, + # maybe it we are in the recipients of this transaction + elif in_outputs: + outputs = [o for o in tx_outputs + if o.pubkey == self.wallet.pubkey] + amount = 0 + for o in outputs: + amount += o.amount + metadata['amount'] = amount + + if txdata['hash'] not in [t.hash for t in awaiting]: + transfer = Transfer.create_from_blockchain(txdata['hash'], + state, + metadata.copy()) + received_list.append(transfer) + return transfer + else: + transfer = [t for t in awaiting if t.hash == txdata['hash']][0] + transfer.check_registered(txdata['hash'], current_block['number'], mediantime, + community.network.fork_window(community.members_pubkeys()) + 1) + + @asyncio.coroutine + def refresh(self, community, received_list): + """ + Refresh last transactions + + :param cutecoin.core.Community community: The community + :param list received_list: List of transactions received + """ + current_block = yield from community.bma_access.future_request(qtbma.blockchain.Block, + req_args={'number': community.network.latest_block_number}) + members_pubkeys = yield from community.members_pubkeys() + # We look for the first block to parse, depending on awaiting and validating transfers and ud... + blocks = [tx.metadata['block'] for tx in self._transfers + if tx.state in (Transfer.AWAITING, Transfer.VALIDATING)] +\ + [ud['block_number'] for ud in self._dividends + if ud['state'] in (Transfer.AWAITING, Transfer.VALIDATING)] +\ + [max(0, self.latest_block - community.network.fork_window(members_pubkeys))] + parsed_block = min(set(blocks)) + logging.debug("Refresh from : {0} to {1}".format(self.latest_block, current_block['number'])) + dividends_data = qtbma.ud.History.null_value + for i in range(0, 6): + if dividends_data == qtbma.ud.History.null_value: + dividends_data = yield from community.bma_access.future_request(qtbma.ud.History, + req_args={'pubkey': self.wallet.pubkey}) + + dividends = dividends_data['history']['history'] + for d in dividends: + if d['block_number'] < parsed_block: + dividends.remove(d) + + new_transfers = [] + new_dividends = [] + # Lets look if transactions took too long to be validated + awaiting = [t for t in self._transfers + if t.state == Transfer.AWAITING] + while parsed_block < current_block['number']: + udid = 0 + for d in [ud for ud in dividends if ud['block_number'] in range(parsed_block, parsed_block+100)]: + state = yield from TxHistory._validation_state(community, d['block_number'], current_block) + + if d['block_number'] not in [ud['block_number'] for ud in self._dividends]: + d['id'] = udid + d['state'] = state + new_dividends.append(d) + udid += 1 + else: + known_dividend = [ud for ud in self._dividends + if ud['block_number'] == d['block_number']][0] + known_dividend['state'] = state + + tx_history = qtbma.tx.history.Blocks.null_value + for i in range(0, 6): + if tx_history == qtbma.tx.history.Blocks.null_value: + tx_history = yield from community.bma_access.future_request(qtbma.tx.history.Blocks, + req_args={'pubkey': self.wallet.pubkey, + 'from_':str(parsed_block), + 'to_': str(parsed_block + 99)}) + # If after 6 requests we still get wrong data + # we continue to next loop + if tx_history == qtbma.tx.history.Blocks.null_value: + continue + + # We parse only blocks with transactions + transactions = tx_history['history']['received'] + tx_history['history']['sent'] + for (txid, txdata) in enumerate(transactions): + if self._stop_coroutines: + return + if len(txdata['issuers']) == 0: + logging.debug("Error with : {0}, from {1} to {2}".format(self.wallet.pubkey, + parsed_block, + current_block['number'])) + else: + transfer = yield from self._parse_transaction(community, txdata, received_list, + udid + txid, current_block) + if transfer: + new_transfers.append(transfer) + + self.wallet.refresh_progressed.emit(parsed_block, current_block['number'], self.wallet.pubkey) + parsed_block += 100 + + if current_block['number'] > self.latest_block: + self.available_sources = yield from self.wallet.future_sources(community) + if self._stop_coroutines: + return + self.latest_block = current_block['number'] + + for transfer in awaiting: + transfer.check_refused(current_block['medianTime'], + community.parameters['avgGenTime'], + community.parameters['medianTimeBlocks']) + + self._transfers = self._transfers + new_transfers + self._dividends = self._dividends + new_dividends + + self.wallet.refresh_finished.emit(received_list) diff --git a/src/cutecoin/core/wallet.py b/src/cutecoin/core/wallet.py index f7ea800bfd29ea624281bb3350439dd43e9bc25c..faa268b8085d264aa73ea57eec32a5cc9b1eec89 100644 --- a/src/cutecoin/core/wallet.py +++ b/src/cutecoin/core/wallet.py @@ -24,8 +24,6 @@ class Wallet(QObject): """ A wallet is used to manage money with a unique key. """ - - inner_data_changed = pyqtSignal(str) refresh_progressed = pyqtSignal(int, int, str) refresh_finished = pyqtSignal(list) transfer_broadcasted = pyqtSignal(str) @@ -60,7 +58,7 @@ class Wallet(QObject): if walletid == 0: key = SigningKey(salt, password) else: - key = SigningKey("{0}{1}".format(salt, walletid), password) + key = SigningKey(b"{0}{1}".format(salt, walletid), password) return cls(walletid, key.pubkey, name, identities_registry) @classmethod @@ -132,6 +130,7 @@ class Wallet(QObject): key = SigningKey("{0}{1}".format(salt, self.walletid), password) return (key.pubkey == self.pubkey) + @asyncio.coroutine def relative_value(self, community): """ Get wallet value relative to last generated UD @@ -139,25 +138,12 @@ class Wallet(QObject): :param community: The community to get value :return: The wallet relative value """ - value = self.value(community) + value = yield from self.value(community) ud = community.dividend relative_value = value / float(ud) return relative_value @asyncio.coroutine - def future_value(self, community): - """ - Get wallet absolute value - - :param community: The community to get value - :return: The wallet absolute value - """ - value = 0 - sources = yield from self.future_sources(community) - for s in sources: - value += s.amount - return value - def value(self, community): """ Get wallet absolute value @@ -166,7 +152,7 @@ class Wallet(QObject): :return: The wallet absolute value """ value = 0 - sources = self.sources(community) + sources = yield from self.sources(community) for s in sources: value += s.amount return value @@ -305,6 +291,7 @@ class Wallet(QObject): tx.append(InputSource.from_bma(s)) return tx + @asyncio.coroutine def sources(self, community): """ Get available sources in a given community @@ -312,7 +299,7 @@ class Wallet(QObject): :param cutecoin.core.community.Community community: The community where we want available sources :return: List of InputSource ucoinpy objects """ - data = community.bma_access.get(self, qtbma.tx.Sources, + data = yield from community.bma_access.future_request(qtbma.tx.Sources, req_args={'pubkey': self.pubkey}) tx = [] for s in data['sources']: diff --git a/src/cutecoin/gui/certification.py b/src/cutecoin/gui/certification.py index 4ebd81fd08f6c0f6bdf3b51f025770ba9449050f..cd2baf04c42615f0f39132f7622adf2c0dd2db55 100644 --- a/src/cutecoin/gui/certification.py +++ b/src/cutecoin/gui/certification.py @@ -5,19 +5,20 @@ Created on 24 dec. 2014 """ from PyQt5.QtWidgets import QDialog, QMessageBox, QDialogButtonBox, QApplication from PyQt5.QtCore import Qt, pyqtSlot -import quamash from ..gen_resources.certification_uic import Ui_CertificationDialog from . import toast +from ..core.net.api import bma as qtbma +from ..tools.decorators import asyncify import asyncio +import logging class CertificationDialog(QDialog, Ui_CertificationDialog): - """ classdocs """ - def __init__(self, certifier, app, password_asker): + def __init__(self, app, certifier, password_asker): """ Constructor """ @@ -34,6 +35,14 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): for contact in certifier.contacts: self.combo_contact.addItem(contact['name']) + @staticmethod + def certify_identity(app, account, password_asker, community, identity): + dialog = CertificationDialog(app, account, password_asker) + dialog.combo_community.setCurrentText(community.name) + dialog.edit_pubkey.setText(identity.pubkey) + dialog.radio_pubkey.setChecked(True) + return dialog.exec_() + def accept(self): if self.radio_contact.isChecked(): index = self.combo_contact.currentIndex() @@ -64,17 +73,25 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): def handle_error(self, error_code, text): if self.app.preferences['notifications']: toast.display(self.tr("Error"), self.tr("{0} : {1}".format(error_code, text))) - else: - QMessageBox.Critical(self, self.tr("Error", self.tr("{0} : {1}".format(error_code, text)))) + #else: + # QMessageBox.Critical(self, self.tr("Error", self.tr("{0} : {1}".format(error_code, text)))) self.account.certification_broadcasted.disconnect() self.account.broadcast_error.disconnect(self.handle_error) QApplication.restoreOverrideCursor() def change_current_community(self, index): self.community = self.account.communities[index] - if self.account.pubkey in self.community.members_pubkeys(): + self.refresh() + + @asyncify + @asyncio.coroutine + def refresh(self): + account_identity = yield from self.account.identity(self.community) + is_member = yield from account_identity.is_member(self.community) + block_0 = yield from self.community.get_block(0) + if is_member or block_0 == qtbma.blockchain.Block.null_value: self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) - self.button_box.button(QDialogButtonBox.Ok).setText(self.tr("Ok")) + self.button_box.button(QDialogButtonBox.Ok).setText(self.tr("&Ok")) else: self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) self.button_box.button(QDialogButtonBox.Ok).setText(self.tr("Not a member")) @@ -82,3 +99,9 @@ class CertificationDialog(QDialog, Ui_CertificationDialog): def recipient_mode_changed(self, pubkey_toggled): self.edit_pubkey.setEnabled(pubkey_toggled) self.combo_contact.setEnabled(not pubkey_toggled) + + def async_exec(self): + future = asyncio.Future() + self.finished.connect(lambda r: future.set_result(r)) + self.open() + return future \ No newline at end of file diff --git a/src/cutecoin/gui/community_tab.py b/src/cutecoin/gui/community_tab.py deleted file mode 100644 index e9e3653fbf713161fe3e28ff84f09fd8d65779e5..0000000000000000000000000000000000000000 --- a/src/cutecoin/gui/community_tab.py +++ /dev/null @@ -1,286 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" - -import logging -from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QIcon, QCursor -from PyQt5.QtWidgets import QWidget, QMessageBox, QAction, QMenu, QDialog, \ - QAbstractItemView -from cutecoin.models.identities import IdentitiesFilterProxyModel, IdentitiesTableModel -from ..gen_resources.community_tab_uic import Ui_CommunityTabWidget -from cutecoin.gui.contact import ConfigureContactDialog -from cutecoin.gui.member import MemberDialog -from .wot_tab import WotTabWidget -from .transfer import TransferMoneyDialog -from .certification import CertificationDialog -from . import toast -import asyncio -from ..core.net.api import bma as qtbma - - -class CommunityTabWidget(QWidget, Ui_CommunityTabWidget): - - """ - classdocs - """ - - def __init__(self, app, account, community, password_asker, parent): - """ - Init - :param cutecoin.core.account.Account account: Account instance - :param cutecoin.core.community.Community community: Community instance - :param cutecoin.gui.password_asker.PasswordAskerDialog password_asker: Password asker dialog - :param cutecoin.gui.currency_tab.CurrencyTabWidget parent: TabWidget instance - :return: - """ - super().__init__() - self.parent = parent - self.app = app - self.community = community - self.account = account - self.password_asker = password_asker - - self.setupUi(self) - - identities_model = IdentitiesTableModel(self.community) - proxy = IdentitiesFilterProxyModel() - proxy.setSourceModel(identities_model) - self.table_identities.setModel(proxy) - self.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) - self.table_identities.customContextMenuRequested.connect(self.identity_context_menu) - self.table_identities.sortByColumn(0, Qt.AscendingOrder) - self.table_identities.resizeColumnsToContents() - - self.wot_tab = WotTabWidget(self.app, self.account, self.community, self.password_asker, self) - self.tabs_information.addTab(self.wot_tab, QIcon(':/icons/wot_icon'), self.tr("Web of Trust")) - members_action = QAction(self.tr("Members"), self) - members_action.triggered.connect(self.search_members) - self.button_search.addAction(members_action) - direct_connections = QAction(self.tr("Direct connections"), self) - direct_connections.triggered.connect(self.search_direct_connections) - self.button_search.addAction(direct_connections) - - self.account.identity(self.community).inner_data_changed.connect(self.handle_account_identity_change) - self.search_direct_connections() - self.account.membership_broadcasted.connect(self.handle_membership_broadcasted) - self.account.revoke_broadcasted.connect(self.handle_revoke_broadcasted) - self.account.selfcert_broadcasted.connect(self.handle_selfcert_broadcasted) - - def handle_membership_broadcasted(self): - if self.app.preferences['notifications']: - toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) - else: - QMessageBox.information(self, self.tr("Membership"), self.tr("Success sending Membership demand")) - - def handle_revoke_broadcasted(self): - if self.app.preferences['notifications']: - toast.display(self.tr("Revoke"), self.tr("Success sending Revoke demand")) - else: - QMessageBox.information(self, self.tr("Revoke"), self.tr("Success sending Revoke demand")) - - def handle_selfcert_broadcasted(self): - if self.app.preferences['notifications']: - toast.display(self.tr("Self Certification"), self.tr("Success sending Self Certification document")) - else: - QMessageBox.information(self.tr("Self Certification"), self.tr("Success sending Self Certification document")) - - def identity_context_menu(self, point): - index = self.table_identities.indexAt(point) - model = self.table_identities.model() - if index.row() < model.rowCount(): - source_index = model.mapToSource(index) - pubkey_col = model.sourceModel().columns_ids.index('pubkey') - pubkey_index = model.sourceModel().index(source_index.row(), - pubkey_col) - pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) - identity = self.app.identities_registry.find(pubkey, self.community) - menu = QMenu(self) - - informations = QAction(self.tr("Informations"), self) - informations.triggered.connect(self.menu_informations) - informations.setData(identity) - add_contact = QAction(self.tr("Add as contact"), self) - add_contact.triggered.connect(self.menu_add_as_contact) - add_contact.setData(identity) - - send_money = QAction(self.tr("Send money"), self) - send_money.triggered.connect(self.menu_send_money) - send_money.setData(identity) - - certify = QAction(self.tr("Certify identity"), self) - certify.triggered.connect(self.menu_certify_member) - certify.setData(identity) - - view_wot = QAction(self.tr("View in Web of Trust"), self) - view_wot.triggered.connect(self.view_wot) - view_wot.setData(identity) - - menu.addAction(informations) - menu.addAction(add_contact) - menu.addAction(send_money) - menu.addAction(certify) - menu.addAction(view_wot) - - # Show the context menu. - menu.exec_(QCursor.pos()) - - def menu_informations(self): - person = self.sender().data() - self.identity_informations(person) - - def menu_add_as_contact(self): - person = self.sender().data() - self.add_identity_as_contact({'name': person.uid, - 'pubkey': person.pubkey}) - - def menu_send_money(self): - person = self.sender().data() - self.send_money_to_identity(person) - - def menu_certify_member(self): - person = self.sender().data() - self.certify_identity(person) - - def identity_informations(self, person): - dialog = MemberDialog(self.app, self.account, self.community, person) - dialog.exec_() - - def add_identity_as_contact(self, person): - dialog = ConfigureContactDialog(self.account, self.window(), person) - result = dialog.exec_() - if result == QDialog.Accepted: - self.window().refresh_contacts() - - def send_money_to_identity(self, person): - if isinstance(person, str): - pubkey = person - else: - pubkey = person.pubkey - dialog = TransferMoneyDialog(self.app, self.account, self.password_asker) - dialog.edit_pubkey.setText(pubkey) - dialog.combo_community.setCurrentText(self.community.name) - dialog.radio_pubkey.setChecked(True) - if dialog.exec_() == QDialog.Accepted: - currency_tab = self.window().currencies_tabwidget.currentWidget() - currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() - - def certify_identity(self, identity): - dialog = CertificationDialog(self.account, self.app, self.password_asker) - dialog.combo_community.setCurrentText(self.community.name) - dialog.edit_pubkey.setText(identity.pubkey) - dialog.radio_pubkey.setChecked(True) - dialog.exec_() - - def view_wot(self): - person = self.sender().data() - # redraw WoT with this identity selected - self.wot_tab.draw_graph({'text': person.uid, 'id': person.pubkey}) - # change page to WoT - index_community_tab = self.parent.tabs_account.indexOf(self) - self.parent.tabs_account.setCurrentIndex(index_community_tab) - index_wot_tab = self.tabs_information.indexOf(self.wot_tab) - self.tabs_information.setCurrentIndex(index_wot_tab) - - @asyncio.coroutine - def _execute_search_text(self, text): - response = yield from self.community.bma_access.future_request(qtbma.wot.Lookup, {'search': text}) - identities = [] - for identity_data in response['results']: - identity = yield from self.app.identities_registry.future_find(identity_data['pubkey'], self.community) - identities.append(identity) - - self_identity = self.account.identity(self.community) - try: - self_identity.inner_data_changed.disconnect(self.handle_account_identity_change) - self.community.inner_data_changed.disconnect(self.handle_community_change) - except TypeError as e: - if "disconnect() failed" in str(e): - pass - else: - raise - - self.edit_textsearch.clear() - self.refresh_identities(identities) - - def search_text(self): - """ - Search text and display found identities - """ - text = self.edit_textsearch.text() - - if len(text) < 2: - return False - else: - asyncio.async(self._execute_search_text(text)) - - @pyqtSlot(str) - def handle_community_change(self, origin): - logging.debug("Handle account community {0}".format(origin)) - if origin == qtbma.wot.Members: - self.search_members() - - @pyqtSlot(str) - def handle_account_identity_change(self, origin): - logging.debug("Handle account identity change {0}".format(origin)) - if origin in (str(qtbma.wot.CertifiedBy), str(qtbma.wot.CertifiersOf)): - self.search_direct_connections() - - def search_members(self): - """ - Search members of community and display found members - """ - pubkeys = self.community.members_pubkeys() - identities = [] - for p in pubkeys: - identities.append(self.app.identities_registry.find(p, self.community)) - - self_identity = self.account.identity(self.community) - - try: - self_identity.inner_data_changed.disconnect(self.handle_account_identity_change) - self.community.inner_data_changed.connect(self.handle_community_change) - except TypeError as e: - if "disconnect() failed" in str(e): - pass - else: - raise - - self.edit_textsearch.clear() - self.refresh_identities(identities) - - def search_direct_connections(self): - """ - Search members of community and display found members - """ - self_identity = self.account.identity(self.community) - try: - self.community.inner_data_changed.disconnect(self.handle_community_change) - self_identity.inner_data_changed.connect(self.handle_account_identity_change) - except TypeError as e: - if "disconnect() failed" in str(e): - logging.debug("Could not disconnect community") - else: - raise - - account_connections = [] - for p in self_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community): - account_connections.append(p['identity']) - certifiers_of = [p for p in account_connections] - for p in self_identity.unique_valid_certified_by(self.app.identities_registry, self.community): - account_connections.append(p['identity']) - certified_by = [p for p in account_connections - if p.pubkey not in [i.pubkey for i in certifiers_of]] - identities = certifiers_of + certified_by - self.refresh_identities(identities) - - def refresh_identities(self, identities): - """ - Refresh the table with specified identities. - If no identities is passed, use the account connections. - """ - self.table_identities.model().sourceModel().refresh_identities(identities) - self.table_identities.resizeColumnsToContents() - diff --git a/src/cutecoin/gui/community_tile.py b/src/cutecoin/gui/community_tile.py new file mode 100644 index 0000000000000000000000000000000000000000..f6c3df18f7769a908f2b29eb1687822975c7cb12 --- /dev/null +++ b/src/cutecoin/gui/community_tile.py @@ -0,0 +1,78 @@ +""" +@author: inso +""" + +from PyQt5.QtWidgets import QFrame, QLabel, QVBoxLayout, QLayout, QPushButton +from PyQt5.QtGui import QPalette +from PyQt5.QtCore import QEvent, QSize, pyqtSignal +from ..tools.decorators import asyncify +import asyncio + + +class CommunityTile(QFrame): + clicked = pyqtSignal() + + def __init__(self, parent, app, community): + super().__init__(parent) + self.app = app + self.community = community + self.text_label = QLabel() + self.setLayout(QVBoxLayout()) + self.layout().setSizeConstraint(QLayout.SetFixedSize) + self.layout().addWidget(self.text_label) + self.setFrameShape(QFrame.StyledPanel) + self.setFrameShadow(QFrame.Raised) + self.refresh() + + def sizeHint(self): + return QSize(250, 250) + + @asyncify + @asyncio.coroutine + def refresh(self): + current_block = yield from self.community.get_block(self.community.network.latest_block_number) + members_pubkeys = yield from self.community.members_pubkeys() + amount = yield from self.app.current_account.amount(self.community) + localized_amount = yield from self.app.current_account.current_ref(amount, + self.community, self.app).localized(units=True, + international_system=self.app.preferences['international_system_of_units']) + if current_block['monetaryMass']: + localized_monetary_mass = yield from self.app.current_account.current_ref(current_block['monetaryMass'], + self.community, self.app).localized(units=True, + international_system=self.app.preferences['international_system_of_units']) + else: + localized_monetary_mass = "" + status = self.tr("Member") if self.app.current_account.pubkey in members_pubkeys \ + else self.tr("Non-Member") + description = """<html> + <body> + <p> + <span style=" font-size:16pt; font-weight:600;">{currency}</span> + </p> + <p>{nb_members} {members_label}</p> + <p><span style=" font-weight:600;">{monetary_mass_label}</span> : {monetary_mass}</p> + <p><span style=" font-weight:600;">{status_label}</span> : {status}</p> + <p><span style=" font-weight:600;">{balance_label}</span> : {balance}</p> + </body> + </html>""".format(currency=self.community.currency, + nb_members=len(members_pubkeys), + members_label=self.tr("members"), + monetary_mass_label=self.tr("Monetary mass"), + monetary_mass=localized_monetary_mass, + status_label=self.tr("Status"), + status=status, + balance_label=self.tr("Balance"), + balance=localized_amount) + self.text_label.setText(description) + + def mousePressEvent(self, event): + self.clicked.emit() + return super().mousePressEvent(event) + + def enterEvent(self, event): + self.setStyleSheet("color: rgb(0, 115, 173);") + return super().enterEvent(event) + + def leaveEvent(self, event): + self.setStyleSheet("") + return super().leaveEvent(event) \ No newline at end of file diff --git a/src/cutecoin/gui/community_view.py b/src/cutecoin/gui/community_view.py new file mode 100644 index 0000000000000000000000000000000000000000..39b822482af2c7cf635916942b928a286c1250ed --- /dev/null +++ b/src/cutecoin/gui/community_view.py @@ -0,0 +1,350 @@ +""" +Created on 2 févr. 2014 + +@author: inso +""" + +import time +import logging +from PyQt5.QtWidgets import QWidget, QMessageBox, QDialog +from PyQt5.QtCore import QModelIndex, pyqtSlot, QDateTime, QLocale, QEvent +from PyQt5.QtGui import QIcon + +from ..core.net.api import bma as qtbma +from .wot_tab import WotTabWidget +from .identities_tab import IdentitiesTabWidget +from .transactions_tab import TransactionsTabWidget +from .network_tab import NetworkTabWidget +from . import toast +import asyncio +from ..tools.exceptions import MembershipNotFoundError, LookupFailureError, NoPeerAvailable +from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task +from ..gen_resources.community_view_uic import Ui_CommunityWidget + + +class CommunityWidget(QWidget, Ui_CommunityWidget): + + """ + classdocs + """ + + def __init__(self, app, status_label): + """ + Constructor + """ + super().__init__() + self.app = app + self.account = None + self.community = None + self.password_asker = None + self.status_label = status_label + + self.status_info = [] + self.status_infotext = {'membership_expire_soon': + self.tr("Warning : Your membership is expiring soon."), + 'warning_certifications': + self.tr("Warning : Your could miss certifications soon.") + } + + super().setupUi(self) + + self.tab_wot = WotTabWidget(self.app) + + self.tab_identities = IdentitiesTabWidget(self.app) + + self.tab_history = TransactionsTabWidget(self.app) + + self.tab_network = NetworkTabWidget(self.app) + self.tab_identities.view_in_wot.connect(self.tab_wot.draw_graph) + self.tab_identities.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot)) + self.tab_history.view_in_wot.connect(self.tab_wot.draw_graph) + self.tab_history.view_in_wot.connect(lambda: self.tabs.setCurrentWidget(self.tab_wot)) + + self.tabs.addTab(self.tab_history, + QIcon(':/icons/tx_icon'), + self.tr("Transactions")) + + self.tabs.addTab(self.tab_wot, + QIcon(':/icons/wot_icon'), + self.tr("Web of Trust")) + + self.tabs.addTab(self.tab_identities, + QIcon(':/icons/members_icon'), + self.tr("Search Identities")) + + self.tabs.addTab(self.tab_network, + QIcon(":/icons/network_icon"), + self.tr("Network")) + + self.button_membership.clicked.connect(self.send_membership_demand) + + def cancel_once_tasks(self): + cancel_once_task(self, self.refresh_block) + cancel_once_task(self, self.refresh_status) + cancel_once_task(self, self.refresh_quality_buttons) + + def change_account(self, account, password_asker): + self.cancel_once_tasks() + if self.account: + self.account.broadcast_error.disconnect(self.handle_broadcast_error) + self.account.membership_broadcasted.disconnect(self.handle_membership_broadcasted) + self.account.selfcert_broadcasted.disconnect(self.handle_selfcert_broadcasted) + + self.account = account + + if self.account: + self.account.broadcast_error.connect(self.handle_broadcast_error) + self.account.membership_broadcasted.connect(self.handle_membership_broadcasted) + self.account.selfcert_broadcasted.connect(self.handle_selfcert_broadcasted) + + self.password_asker = password_asker + self.tab_wot.change_account(account, self.password_asker) + self.tab_identities.change_account(account, self.password_asker) + self.tab_history.change_account(account, self.password_asker) + + def change_community(self, community): + self.cancel_once_tasks() + + self.tab_network.change_community(community) + self.tab_wot.change_community(community) + self.tab_history.change_community(community) + self.tab_identities.change_community(community) + + if self.community: + self.community.network.new_block_mined.disconnect(self.refresh_block) + self.community.network.nodes_changed.disconnect(self.refresh_status) + if community: + community.network.new_block_mined.connect(self.refresh_block) + community.network.nodes_changed.connect(self.refresh_status) + self.label_currency.setText(community.currency) + self.community = community + self.refresh_quality_buttons() + + @pyqtSlot(str) + def display_error(self, error): + QMessageBox.critical(self, ":(", + error, + QMessageBox.Ok) + + @once_at_a_time + @asyncify + @asyncio.coroutine + def refresh_block(self, block_number): + """ + When a new block is found, start handling data. + @param: block_number: The number of the block mined + """ + logging.debug("Refresh block") + self.status_info.clear() + try: + person = yield from self.app.identities_registry.future_find(self.app.current_account.pubkey, self.community) + expiration_time = yield from person.membership_expiration_time(self.community) + parameters = yield from self.community.parameters() + sig_validity = parameters['sigValidity'] + warning_expiration_time = int(sig_validity / 3) + will_expire_soon = (expiration_time < warning_expiration_time) + + logging.debug("Try") + if will_expire_soon: + days = int(expiration_time / 3600 / 24) + if days > 0: + self.status_info.append('membership_expire_soon') + + if self.app.preferences['notifications']: + toast.display(self.tr("Membership expiration"), + self.tr("<b>Warning : Membership expiration in {0} days</b>").format(days)) + + certifiers_of = yield from person.unique_valid_certifiers_of(self.app.identities_registry, + self.community) + if len(certifiers_of) < parameters['sigQty']: + self.status_info.append('warning_certifications') + if self.app.preferences['notifications']: + toast.display(self.tr("Certifications number"), + self.tr("<b>Warning : You are certified by only {0} persons, need {1}</b>") + .format(len(certifiers_of), + parameters['sigQty'])) + + except MembershipNotFoundError as e: + pass + + self.tab_history.start_progress() + self.refresh_data() + + def refresh_data(self): + """ + Refresh data + """ + self.tab_history.refresh_balance() + self.refresh_status() + + @once_at_a_time + @asyncify + @asyncio.coroutine + def refresh_status(self): + """ + Refresh status bar + """ + logging.debug("Refresh status") + if self.community: + text = self.tr(" Block {0}").format(self.community.network.latest_block_number) + + block = yield from self.community.get_block(self.community.network.latest_block_number) + if block != qtbma.blockchain.Block.null_value: + text += " ( {0} )".format(QLocale.toString( + QLocale(), + QDateTime.fromTime_t(block['medianTime']), + QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) + )) + + if self.community.network.quality > 0.66: + icon = '<img src=":/icons/connected" width="12" height="12"/>' + elif self.community.network.quality > 0.33: + icon = '<img src=":/icons/weak_connect" width="12" height="12"/>' + else: + icon = '<img src=":/icons/disconnected" width="12" height="12"/>' + status_infotext = " - ".join([self.status_infotext[info] for info in self.status_info]) + label_text = "{0}{1}".format(icon, text) + if status_infotext != "": + label_text += " - {0}".format(status_infotext) + + if self.app.preferences['expert_mode']: + label_text += self.tr(" - Median fork window : {0}").format(self.community.network.fork_window(self.community.members_pubkeys())) + + self.status_label.setText(label_text) + + @once_at_a_time + @asyncify + @asyncio.coroutine + def refresh_quality_buttons(self): + if self.account and self.community: + try: + account_identity = yield from self.account.identity(self.community) + published_uid = account_identity.published_uid(self.community) + if published_uid: + logging.debug("UID Published") + is_member = account_identity.is_member(self.community) + if is_member: + self.button_membership.setText(self.tr("Renew membership")) + self.button_membership.show() + self.button_certification.show() + else: + logging.debug("Not a member") + self.button_membership.setText(self.tr("Send membership demand")) + self.button_membership.show() + if self.community.get_block(0) != qtbma.blockchain.Block.null_value: + self.button_certification.hide() + else: + logging.debug("UID not published") + self.button_membership.hide() + self.button_certification.hide() + except LookupFailureError: + self.button_membership.hide() + self.button_certification.hide() + + def showEvent(self, event): + self.refresh_status() + + def referential_changed(self): + if self.community and self.tab_history.table_history.model(): + self.tab_history.table_history.model().sourceModel().refresh_transfers() + self.tab_history.refresh_balance() + + def send_membership_demand(self): + password = self.password_asker.exec_() + if self.password_asker.result() == QDialog.Rejected: + return + asyncio.async(self.account.send_membership(password, self.community, 'IN')) + + def send_membership_leaving(self): + reply = QMessageBox.warning(self, self.tr("Warning"), + self.tr("""Are you sure ? +Sending a leaving demand cannot be canceled. +The process to join back the community later will have to be done again.""") +.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + password = self.password_asker.exec_() + if self.password_asker.result() == QDialog.Rejected: + return + + asyncio.async(self.account.send_membership(password, self.community, 'OUT')) + + def publish_uid(self): + reply = QMessageBox.warning(self, self.tr("Warning"), + self.tr("""Are you sure ? +Publishing your UID can be canceled by Revoke UID.""") +.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + password = self.password_asker.exec_() + if self.password_asker.result() == QDialog.Rejected: + return + + try: + self.account.send_selfcert(password, self.community) + toast.display(self.tr("UID Publishing"), + self.tr("Success publishing your UID")) + except ValueError as e: + QMessageBox.critical(self, self.tr("Publish UID error"), + str(e)) + except NoPeerAvailable as e: + QMessageBox.critical(self, self.tr("Network error"), + self.tr("Couldn't connect to network : {0}").format(e), + QMessageBox.Ok) + except Exception as e: + QMessageBox.critical(self, self.tr("Error"), + "{0}".format(e), + QMessageBox.Ok) + + def revoke_uid(self): + reply = QMessageBox.warning(self, self.tr("Warning"), + self.tr("""Are you sure ? +Revoking your UID can only success if it is not already validated by the network.""") +.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) + if reply == QMessageBox.Ok: + password = self.password_asker.exec_() + if self.password_asker.result() == QDialog.Rejected: + return + + asyncio.async(self.account.revoke(password, self.community)) + + def handle_membership_broadcasted(self): + if self.app.preferences['notifications']: + toast.display(self.tr("Membership"), self.tr("Success sending Membership demand")) + else: + QMessageBox.information(self, self.tr("Membership"), self.tr("Success sending Membership demand")) + + def handle_revoke_broadcasted(self): + if self.app.preferences['notifications']: + toast.display(self.tr("Revoke"), self.tr("Success sending Revoke demand")) + else: + QMessageBox.information(self, self.tr("Revoke"), self.tr("Success sending Revoke demand")) + + def handle_selfcert_broadcasted(self): + if self.app.preferences['notifications']: + toast.display(self.tr("Self Certification"), self.tr("Success sending Self Certification document")) + else: + QMessageBox.information(self.tr("Self Certification"), self.tr("Success sending Self Certification document")) + + def handle_broadcast_error(self, error, strdata): + if self.app.preferences['notifications']: + toast.display(error, strdata) + else: + QMessageBox.error(error, strdata) + + def showEvent(self, QShowEvent): + """ + + :param QShowEvent: + :return: + """ + self.refresh_status() + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh_status() + return super(CommunityWidget, self).changeEvent(event) diff --git a/src/cutecoin/gui/contact.py b/src/cutecoin/gui/contact.py index 8e26de9708940be7b1212853d6e42f004d327564..bc6f550e84995a28f26d5a1c9fbd9a3817c2f721 100644 --- a/src/cutecoin/gui/contact.py +++ b/src/cutecoin/gui/contact.py @@ -27,13 +27,7 @@ class ConfigureContactDialog(QDialog, Ui_ConfigureContactDialog): self.account = account self.main_window = parent self.index_edit = index_edit - if type(contact) is Person: - self.contact = {'name': contact.uid, - 'pubkey': contact.pubkey} - elif type(contact) is dict: - self.contact = contact - else: - self.contact = None + self.contact = contact if index_edit is not None: self.contact = account.contacts[index_edit] diff --git a/src/cutecoin/gui/currency_tab.py b/src/cutecoin/gui/currency_tab.py deleted file mode 100644 index 4767d36d205c48eaea236956685e707c1699c49e..0000000000000000000000000000000000000000 --- a/src/cutecoin/gui/currency_tab.py +++ /dev/null @@ -1,204 +0,0 @@ -""" -Created on 2 févr. 2014 - -@author: inso -""" - -import time -import logging -from PyQt5.QtWidgets import QWidget, QMessageBox -from PyQt5.QtCore import QModelIndex, pyqtSlot, QDateTime, QLocale -from PyQt5.QtGui import QIcon -from ..gen_resources.currency_tab_uic import Ui_CurrencyTabWidget - -from ..core.net.api import bma as qtbma -from .community_tab import CommunityTabWidget -from .wallets_tab import WalletsTabWidget -from .transactions_tab import TransactionsTabWidget -from .network_tab import NetworkTabWidget -from .informations_tab import InformationsTabWidget -from . import toast -import asyncio -from ..tools.exceptions import MembershipNotFoundError -from ..core.registry import IdentitiesRegistry - - -class CurrencyTabWidget(QWidget, Ui_CurrencyTabWidget): - - """ - classdocs - """ - - def __init__(self, app, community, password_asker, status_label): - """ - Constructor - """ - super().__init__() - self.app = app - self.community = community - self.password_asker = password_asker - self.status_label = status_label - - self.status_info = [] - self.status_infotext = {'membership_expire_soon': - self.tr("Warning : Your membership is expiring soon."), - 'warning_certifications': - self.tr("Warning : Your could miss certifications soon.") - } - - super().setupUi(self) - - self.tab_community = CommunityTabWidget(self.app, - self.app.current_account, - self.community, - self.password_asker, - self) - - self.tab_wallets = WalletsTabWidget(self.app, - self.app.current_account, - self.community, - self.password_asker) - - self.tab_history = TransactionsTabWidget(self.app, - self.community, - self.password_asker, - self) - - self.tab_informations = InformationsTabWidget(self.app, - self.community) - - self.tab_network = NetworkTabWidget(self.app, - self.community) - - self.tabs_account.addTab(self.tab_wallets, - QIcon(':/icons/wallet_icon'), - self.tr("Wallets")) - - self.tabs_account.addTab(self.tab_history, - QIcon(':/icons/tx_icon'), - self.tr("Transactions")) - - self.tabs_account.addTab(self.tab_community, - QIcon(':/icons/community_icon'), - self.tr("Community")) - - self.tabs_account.addTab(self.tab_network, - QIcon(":/icons/network_icon"), - self.tr("Network")) - - self.tabs_account.addTab(self.tab_informations, - QIcon(':/icons/informations_icon'), - self.tr("Informations")) - - self.community.network.new_block_mined.connect(self.refresh_block) - self.community.network.nodes_changed.connect(self.refresh_status) - self.community.inner_data_changed.connect(self.refresh_status) - - @pyqtSlot(str) - def display_error(self, error): - QMessageBox.critical(self, ":(", - error, - QMessageBox.Ok) - - @pyqtSlot(int) - def refresh_block(self, block_number): - """ - When a new block is found, start handling data. - @param: block_number: The number of the block mined - """ - logging.debug("Refresh block") - self.status_info.clear() - try: - person = self.app.identities_registry.find(self.app.current_account.pubkey, self.community) - expiration_time = person.membership_expiration_time(self.community) - sig_validity = self.community.parameters['sigValidity'] - warning_expiration_time = int(sig_validity / 3) - will_expire_soon = (expiration_time < warning_expiration_time) - - logging.debug("Try") - if will_expire_soon: - days = int(expiration_time / 3600 / 24) - if days > 0: - self.status_info.append('membership_expire_soon') - - if self.app.preferences['notifications']: - toast.display(self.tr("Membership expiration"), - self.tr("<b>Warning : Membership expiration in {0} days</b>").format(days)) - - certifiers_of = person.unique_valid_certifiers_of(self.app.identities_registry, self.community) - if len(certifiers_of) < self.community.parameters['sigQty']: - self.status_info.append('warning_certifications') - if self.app.preferences['notifications']: - toast.display(self.tr("Certifications number"), - self.tr("<b>Warning : You are certified by only {0} persons, need {1}</b>") - .format(len(certifiers_of), - self.community.parameters['sigQty'])) - - except MembershipNotFoundError as e: - pass - - self.tab_history.start_progress() - self.refresh_data() - - def refresh_wallets(self): - if self.tab_wallets: - self.tab_wallets.refresh() - - def refresh_data(self): - """ - Refresh data when the blockchain watcher finished handling datas - """ - if self.tab_wallets: - self.tab_wallets.refresh() - - self.tab_history.refresh_balance() - self.refresh_status() - - @pyqtSlot() - def refresh_status(self): - """ - Refresh status bar - """ - logging.debug("Refresh status") - text = self.tr(" Block {0}").format(self.community.network.latest_block_number) - - block = self.community.get_block(self.community.network.latest_block_number) - if block != qtbma.blockchain.Block.null_value: - text += " ( {0} )".format(QLocale.toString( - QLocale(), - QDateTime.fromTime_t(block['medianTime']), - QLocale.dateTimeFormat(QLocale(), QLocale.NarrowFormat) - )) - - if self.community.network.quality > 0.66: - icon = '<img src=":/icons/connected" width="12" height="12"/>' - elif self.community.network.quality > 0.33: - icon = '<img src=":/icons/weak_connect" width="12" height="12"/>' - else: - icon = '<img src=":/icons/disconnected" width="12" height="12"/>' - status_infotext = " - ".join([self.status_infotext[info] for info in self.status_info]) - label_text = "{0}{1}".format(icon, text) - if status_infotext != "": - label_text += " - {0}".format(status_infotext) - - if self.app.preferences['expert_mode']: - label_text += self.tr(" - Median fork window : {0}").format(self.community.network.fork_window(self.community.members_pubkeys())) - - self.status_label.setText(label_text) - - def showEvent(self, event): - self.refresh_status() - - def referential_changed(self): - if self.tab_history.table_history.model(): - self.tab_history.table_history.model().dataChanged.emit( - QModelIndex(), - QModelIndex(), - []) - self.tab_history.refresh_balance() - - if self.tab_wallets: - self.tab_wallets.refresh() - - if self.tab_informations: - self.tab_informations.refresh() diff --git a/src/cutecoin/gui/homescreen.py b/src/cutecoin/gui/homescreen.py index 15ffa516e54578667eb2fee82e557d663b13b27c..85f21a1671999dc048c77dea6bfd764be1cb4474 100644 --- a/src/cutecoin/gui/homescreen.py +++ b/src/cutecoin/gui/homescreen.py @@ -4,42 +4,96 @@ Created on 31 janv. 2015 @author: vit """ -from PyQt5.QtWidgets import QWidget -from ..gen_resources.homescreen_uic import Ui_HomeScreenWidget -from ..__init__ import __version__ -from . import toast +from PyQt5.QtWidgets import QWidget, QFrame, QGridLayout, QAction +from PyQt5.QtCore import QEvent, Qt, pyqtSlot, pyqtSignal +from ..gen_resources.homescreen_uic import Ui_HomescreenWidget +from .community_tile import CommunityTile +from ..core.community import Community +import logging -class HomeScreenWidget(QWidget, Ui_HomeScreenWidget): +class FrameCommunities(QFrame): + community_tile_clicked = pyqtSignal(Community) + + def __init__(self, parent): + super().__init__(parent) + self.grid_layout = QGridLayout() + self.setLayout(self.grid_layout) + self.grid_layout.setAlignment(Qt.AlignLeft | Qt.AlignTop) + self.setFrameShape(QFrame.StyledPanel) + self.setFrameShadow(QFrame.Raised) + self.tiles = [] + + def sizeHint(self): + return self.parentWidget().size() + + def refresh(self, app): + for t in self.tiles: + t.setParent(None) + self.tiles = [] + if app.current_account: + for c in app.current_account.communities: + community_tile = CommunityTile(self, app, c) + community_tile.clicked.connect(self.click_on_tile) + self.layout().addWidget(community_tile) + self.tiles.append(community_tile) + + def refresh_content(self): + for t in self.tiles: + t.refresh() + + @pyqtSlot() + def click_on_tile(self): + tile = self.sender() + logging.debug("Click on tile") + self.community_tile_clicked.emit(tile.community) + + +class HomeScreenWidget(QWidget, Ui_HomescreenWidget): """ classdocs """ - def __init__(self, app): + def __init__(self, app, status_label): """ Constructor """ super().__init__() self.setupUi(self) self.app = app - self.refresh_text() - self.app.version_requested.connect(self.refresh_text) - - def refresh_text(self): - latest = self.app.available_version - version_info = "" - version_url = "" - if not latest[0]: - version_info = self.tr("Please get the latest release {version}") \ - .format(version=latest[1]) - version_url = latest[2] - - self.label_welcome.setText( - self.tr(""" - <h1>Welcome to Cutecoin {version}</h1> - <h2>{version_info}</h2> - <h3><a href={version_url}>Download link</a></h3> - """).format(version=latest[1], - version_info=version_info, - version_url=version_url)) + self.frame_communities = FrameCommunities(self) + self.layout().addWidget(self.frame_communities) + self.status_label = status_label + + def refresh(self): + self.frame_communities.refresh(self.app) + if self.app.current_account: + self.frame_connected.show() + self.label_connected.setText(self.tr("Connected as {0}".format(self.app.current_account.name))) + self.frame_disconnected.hide() + else: + self.frame_disconnected.show() + self.frame_connected.hide() + + def referential_changed(self): + self.frame_communities.refresh_content() + + def showEvent(self, QShowEvent): + """ + + :param QShowEvent: + :return: + """ + self.status_label.setText("") + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + return super(HomeScreenWidget, self).changeEvent(event) + diff --git a/src/cutecoin/gui/identities_tab.py b/src/cutecoin/gui/identities_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..d8b40b4a70b6483e7457b0d95228f770b9b84d14 --- /dev/null +++ b/src/cutecoin/gui/identities_tab.py @@ -0,0 +1,244 @@ +""" +Created on 2 févr. 2014 + +@author: inso +""" + +import logging +from PyQt5.QtCore import Qt, pyqtSignal, QEvent +from PyQt5.QtGui import QIcon, QCursor +from PyQt5.QtWidgets import QWidget, QMessageBox, QAction, QMenu, QDialog, \ + QAbstractItemView +from ..models.identities import IdentitiesFilterProxyModel, IdentitiesTableModel +from ..gen_resources.identities_tab_uic import Ui_IdentitiesTab +from .contact import ConfigureContactDialog +from .member import MemberDialog +from .transfer import TransferMoneyDialog +from .certification import CertificationDialog +import asyncio +from ..core.net.api import bma as qtbma +from ..core.registry import Identity +from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task + + +class IdentitiesTabWidget(QWidget, Ui_IdentitiesTab): + + """ + classdocs + """ + view_in_wot = pyqtSignal(Identity) + + def __init__(self, app): + """ + Init + :param cutecoin.core.account.Account account: Account instance + :param cutecoin.core.community.Community community: Community instance + :param cutecoin.gui.password_asker.PasswordAskerDialog password_asker: Password asker dialog + :return: + """ + super().__init__() + self.app = app + self.community = None + self.account = None + self.password_asker = None + + self.setupUi(self) + + identities_model = IdentitiesTableModel(self.community) + proxy = IdentitiesFilterProxyModel() + proxy.setSourceModel(identities_model) + self.table_identities.setModel(proxy) + self.table_identities.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_identities.customContextMenuRequested.connect(self.identity_context_menu) + self.table_identities.sortByColumn(0, Qt.AscendingOrder) + self.table_identities.resizeColumnsToContents() + + members_action = QAction(self.tr("Members"), self) + members_action.triggered.connect(self._async_search_members) + self.button_search.addAction(members_action) + direct_connections = QAction(self.tr("Direct connections"), self) + direct_connections.triggered.connect(self._async_search_direct_connections) + self.button_search.addAction(direct_connections) + self.button_search.clicked.connect(self._async_execute_search_text) + + def cancel_once_tasks(self): + cancel_once_task(self, self.identity_context_menu) + cancel_once_task(self, self._async_execute_search_text) + cancel_once_task(self, self._async_search_members) + cancel_once_task(self, self._async_search_direct_connections) + + def change_account(self, account, password_asker): + self.cancel_once_tasks() + self.account = account + self.password_asker = password_asker + if self.account is None: + self.community = None + + def change_community(self, community): + self.cancel_once_tasks() + self.community = community + self.table_identities.model().change_community(community) + self._async_search_direct_connections() + + @once_at_a_time + @asyncify + @asyncio.coroutine + def identity_context_menu(self, point): + index = self.table_identities.indexAt(point) + model = self.table_identities.model() + if index.row() < model.rowCount(): + source_index = model.mapToSource(index) + pubkey_col = model.sourceModel().columns_ids.index('pubkey') + pubkey_index = model.sourceModel().index(source_index.row(), + pubkey_col) + pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) + identity = yield from self.app.identities_registry.future_find(pubkey, self.community) + menu = QMenu(self) + + informations = QAction(self.tr("Informations"), self) + informations.triggered.connect(self.menu_informations) + informations.setData(identity) + add_contact = QAction(self.tr("Add as contact"), self) + add_contact.triggered.connect(self.menu_add_as_contact) + add_contact.setData(identity) + + send_money = QAction(self.tr("Send money"), self) + send_money.triggered.connect(self.menu_send_money) + send_money.setData(identity) + + certify = QAction(self.tr("Certify identity"), self) + certify.triggered.connect(self.menu_certify_member) + certify.setData(identity) + + view_wot = QAction(self.tr("View in Web of Trust"), self) + view_wot.triggered.connect(self.view_wot) + view_wot.setData(identity) + + menu.addAction(informations) + menu.addAction(add_contact) + menu.addAction(send_money) + menu.addAction(certify) + menu.addAction(view_wot) + + # Show the context menu. + menu.popup(QCursor.pos()) + + def menu_informations(self): + person = self.sender().data() + self.identity_informations(person) + + def menu_add_as_contact(self): + person = self.sender().data() + self.add_identity_as_contact({'name': person.uid, + 'pubkey': person.pubkey}) + + def menu_send_money(self): + person = self.sender().data() + self.send_money_to_identity(person) + + def menu_certify_member(self): + person = self.sender().data() + self.certify_identity(person) + + def identity_informations(self, person): + dialog = MemberDialog(self.app, self.account, self.community, person) + dialog.exec_() + + def add_identity_as_contact(self, person): + dialog = ConfigureContactDialog(self.account, self.window(), person) + result = dialog.exec_() + if result == QDialog.Accepted: + self.window().refresh_contacts() + + def send_money_to_identity(self, identity): + if isinstance(identity, str): + pubkey = identity + else: + pubkey = identity.pubkey + result = TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, + self.community, identity) + if result == QDialog.Accepted: + currency_tab = self.window().currencies_tabwidget.currentWidget() + currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() + + def certify_identity(self, identity): + CertificationDialog.certify_identity(self.app, self.account, self.password_asker, + self.community, identity) + + def view_wot(self): + identity = self.sender().data() + self.view_in_wot.emit(identity) + + @once_at_a_time + @asyncify + @asyncio.coroutine + def _async_execute_search_text(self, checked): + text = self.edit_textsearch.text() + if len(text) < 2: + return + response = yield from self.community.bma_access.future_request(qtbma.wot.Lookup, {'search': text}) + identities = [] + for identity_data in response['results']: + identity = yield from self.app.identities_registry.future_find(identity_data['pubkey'], self.community) + identities.append(identity) + + self.edit_textsearch.clear() + self.refresh_identities(identities) + + @once_at_a_time + @asyncify + @asyncio.coroutine + def _async_search_members(self, checked=False): + """ + Search members of community and display found members + """ + if self.community: + pubkeys = yield from self.community.members_pubkeys() + identities = [] + for p in pubkeys: + identity = yield from self.app.identities_registry.future_find(p, self.community) + identities.append(identity) + + self.edit_textsearch.clear() + self.refresh_identities(identities) + + @once_at_a_time + @asyncify + @asyncio.coroutine + def _async_search_direct_connections(self, checked=False): + """ + Search members of community and display found members + """ + if self.account and self.community: + self_identity = yield from self.account.identity(self.community) + account_connections = [] + certs_of = yield from self_identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) + for p in certs_of: + account_connections.append(p['identity']) + certifiers_of = [p for p in account_connections] + certs_by = yield from self_identity.unique_valid_certified_by(self.app.identities_registry, self.community) + for p in certs_by: + account_connections.append(p['identity']) + certified_by = [p for p in account_connections + if p.pubkey not in [i.pubkey for i in certifiers_of]] + identities = certifiers_of + certified_by + self.refresh_identities(identities) + + def refresh_identities(self, identities): + """ + Refresh the table with specified identities. + If no identities is passed, use the account connections. + """ + self.table_identities.model().sourceModel().refresh_identities(identities) + self.table_identities.resizeColumnsToContents() + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + return super(IdentitiesTabWidget, self).changeEvent(event) + diff --git a/src/cutecoin/gui/informations_tab.py b/src/cutecoin/gui/informations_tab.py index 5d494e1a5b03de02d45c955ec3f139c9bd9f2b8d..6826e8b6a17eedd45f54df1d0812d4670bea6b19 100644 --- a/src/cutecoin/gui/informations_tab.py +++ b/src/cutecoin/gui/informations_tab.py @@ -6,7 +6,7 @@ Created on 31 janv. 2015 import logging import math -from PyQt5.QtCore import QLocale, QDateTime +from PyQt5.QtCore import QLocale, QDateTime, QEvent from PyQt5.QtWidgets import QWidget from ..gen_resources.informations_tab_uic import Ui_InformationsTabWidget @@ -65,7 +65,7 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): self.community, self.app).diff_localized() localized_mass = self.account.current_ref(block_ud['monetaryMass'], - self.community, self.app) + self.community, self.app).diff_localized() if block_ud_minus_1: mass_minus_1 = (float(0) if block_ud['membersCount'] == 0 else block_ud_minus_1['monetaryMass'] / block_ud['membersCount']) @@ -218,3 +218,14 @@ class InformationsTabWidget(QWidget, Ui_InformationsTabWidget): self.tr('Maximum distance between each WoT member and a newcomer'), ) ) + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh() + return super(InformationsTabWidget, self).changeEvent(event) diff --git a/src/cutecoin/gui/mainwindow.py b/src/cutecoin/gui/mainwindow.py index a893c9dbe819e0e0ab88618d6a53ba1df6e856e2..9a716c1698c91c0ecde31ac74afb6842cabdee7e 100644 --- a/src/cutecoin/gui/mainwindow.py +++ b/src/cutecoin/gui/mainwindow.py @@ -8,23 +8,27 @@ from ..gen_resources.about_uic import Ui_AboutPopup from PyQt5.QtWidgets import QMainWindow, QAction, QFileDialog, QProgressBar, \ QMessageBox, QLabel, QComboBox, QDialog, QApplication -from PyQt5.QtCore import QSignalMapper, QObject, QLocale, \ +from PyQt5.QtCore import QSignalMapper, pyqtSlot, QLocale, QEvent, \ pyqtSlot, pyqtSignal, QDate, QDateTime, QTimer, QUrl, Qt, QCoreApplication from PyQt5.QtGui import QIcon, QDesktopServices from .process_cfg_account import ProcessConfigureAccount from .transfer import TransferMoneyDialog -from .currency_tab import CurrencyTabWidget +from .community_view import CommunityWidget from .contact import ConfigureContactDialog from .import_account import ImportAccountDialog from .certification import CertificationDialog from .password_asker import PasswordAskerDialog from .preferences import PreferencesDialog +from .process_cfg_community import ProcessConfigureCommunity from .homescreen import HomeScreenWidget from ..core import money +from ..core.community import Community +from ..tools.decorators import asyncify from ..__init__ import __version__ from . import toast +import asyncio import logging @@ -64,12 +68,21 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.combo_referential.currentIndexChanged.connect(self.referential_changed) self.statusbar.addPermanentWidget(self.combo_referential) - self.homescreen = HomeScreenWidget(self.app) + self.homescreen = HomeScreenWidget(self.app, self.status_label) + self.homescreen.frame_communities.community_tile_clicked.connect(self.change_community) + self.homescreen.toolbutton_new_account.clicked.connect(self.open_add_account_dialog) + self.homescreen.toolbutton_new_account.addAction(self.action_add_account) + self.homescreen.toolbutton_new_account.addAction(self.action_import) + self.homescreen.button_add_community.clicked.connect(self.action_open_add_community) + self.homescreen.button_disconnect.clicked.connect(lambda :self.action_change_account("")) self.centralWidget().layout().addWidget(self.homescreen) - self.homescreen.button_new.clicked.connect(self.open_add_account_dialog) - self.homescreen.button_import.clicked.connect(self.import_account) - self.open_ucoin_info = lambda: QDesktopServices.openUrl(QUrl("http://ucoin.io/theoretical/")) - self.homescreen.button_info.clicked.connect(self.open_ucoin_info) + self.homescreen.toolbutton_connect.setMenu(self.menu_change_account) + + self.community_view = CommunityWidget(self.app, self.status_label) + self.community_view.button_home.clicked.connect(lambda: self.change_community(None)) + self.community_view.button_certification.clicked.connect(self.open_certification_dialog) + self.community_view.button_send_money.clicked.connect(self.open_transfer_money_dialog) + self.centralWidget().layout().addWidget(self.community_view) def startup(self): self.update_time() @@ -78,13 +91,29 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.showMaximized() else: self.show() + if self.app.current_account: + self.password_asker = PasswordAskerDialog(self.app.current_account) + self.community_view.change_account(self.app.current_account, self.password_asker) self.refresh() - def open_add_account_dialog(self): + @asyncify + @asyncio.coroutine + def open_add_account_dialog(self, checked=False): dialog = ProcessConfigureAccount(self.app, None) - result = dialog.exec_() + result = yield from dialog.async_exec() if result == QDialog.Accepted: - self.action_change_account(self.app.current_account) + self.action_change_account(self.app.current_account.name) + + @asyncify + @asyncio.coroutine + def open_configure_account_dialog(self, checked=False): + dialog = ProcessConfigureAccount(self.app, self.app.current_account) + result = yield from dialog.async_exec() + if result == QDialog.Accepted: + if self.app.current_account: + self.action_change_account(self.app.current_account.name) + else: + self.refresh() @pyqtSlot(str) def display_error(self, error): @@ -96,8 +125,9 @@ class MainWindow(QMainWindow, Ui_MainWindow): def referential_changed(self, index): if self.app.current_account: self.app.current_account.set_display_referential(index) - if self.currencies_tabwidget.currentWidget(): - self.currencies_tabwidget.currentWidget().referential_changed() + if self.community_view: + self.community_view.referential_changed() + self.homescreen.referential_changed() @pyqtSlot() def update_time(self): @@ -127,31 +157,30 @@ class MainWindow(QMainWindow, Ui_MainWindow): def action_change_account(self, account_name): self.app.change_current_account(self.app.get_account(account_name)) + self.password_asker = PasswordAskerDialog(self.app.current_account) + self.community_view.change_account(self.app.current_account, self.password_asker) self.refresh() @pyqtSlot() - def loader_finished(self): - logging.debug("Finished loading") - self.refresh() - self.busybar.hide() - QApplication.setOverrideCursor(Qt.ArrowCursor) - try: - self.app.disconnect() - except: - logging.debug("Disconnect of app failed") - - QApplication.processEvents() + def action_open_add_community(self): + dialog = ProcessConfigureCommunity(self.app, + self.app.current_account, None, + self.password_asker) + if dialog.exec_() == QDialog.Accepted: + self.app.save(self.app.current_account) + dialog.community.start_coroutines() + self.homescreen.refresh() def open_transfer_money_dialog(self): - dialog = TransferMoneyDialog(self.app, self.app.current_account, + dialog = TransferMoneyDialog(self.app, + self.app.current_account, self.password_asker) - dialog.accepted.connect(self.refresh_wallets) if dialog.exec_() == QDialog.Accepted: - currency_tab = self.currencies_tabwidget.currentWidget() - currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() + self.community_view.tab_history.table_history.model().sourceModel().refresh_transfers() def open_certification_dialog(self): - dialog = CertificationDialog(self.app.current_account, + dialog = CertificationDialog(self.app, + self.app.current_account, self.password_asker) dialog.exec_() @@ -161,15 +190,6 @@ class MainWindow(QMainWindow, Ui_MainWindow): if result == QDialog.Accepted: self.window().refresh_contacts() - def open_configure_account_dialog(self): - dialog = ProcessConfigureAccount(self.app, self.app.current_account) - result = dialog.exec_() - if result == QDialog.Accepted: - if self.app.current_account: - self.action_change_account(self.app.current_account.name) - else: - self.refresh() - def open_preferences_dialog(self): dialog = PreferencesDialog(self.app) result = dialog.exec_() @@ -233,33 +253,26 @@ class MainWindow(QMainWindow, Ui_MainWindow): version_info=version_info, version_url=version_url)) - def refresh_wallets(self): - currency_tab = self.currencies_tabwidget.currentWidget() - if currency_tab: - currency_tab.refresh_wallets() + @pyqtSlot(Community) + def change_community(self, community): + if self.community_view.community: + self.community_view.community.stop_coroutines() - def refresh_communities(self): - logging.debug("CLEAR") - self.currencies_tabwidget.clear() - if self.app.current_account: - for community in self.app.current_account.communities: - tab_currency = CurrencyTabWidget(self.app, community, - self.password_asker, - self.status_label) - self.currencies_tabwidget.addTab(tab_currency, - QIcon(":/icons/currency_icon"), - community.name) + if community: + self.homescreen.hide() + self.community_view.show() + else: + self.community_view.hide() + self.homescreen.show() + + self.community_view.change_community(community) def refresh_accounts(self): self.menu_change_account.clear() - signal_mapper = QSignalMapper(self) - for account_name in sorted(self.app.accounts.keys()): action = QAction(account_name, self) + action.triggered.connect(lambda checked, account_name=account_name: self.action_change_account(account_name)) self.menu_change_account.addAction(action) - signal_mapper.setMapping(action, account_name) - action.triggered.connect(signal_mapper.map) - signal_mapper.mapped[str].connect(self.action_change_account) def refresh_contacts(self): self.menu_contacts_list.clear() @@ -281,22 +294,22 @@ class MainWindow(QMainWindow, Ui_MainWindow): """ logging.debug("Refresh started") self.refresh_accounts() + self.homescreen.show() + self.community_view.hide() + self.homescreen.refresh() if self.app.current_account is None: - self.currencies_tabwidget.hide() - self.homescreen.show() self.setWindowTitle(self.tr("CuteCoin {0}").format(__version__)) - self.menu_account.setEnabled(False) + self.action_add_a_contact.setEnabled(False) + self.actionCertification.setEnabled(False) + self.actionTransfer_money.setEnabled(False) self.action_configure_parameters.setEnabled(False) self.action_set_as_default.setEnabled(False) + self.menu_contacts_list.setEnabled(False) self.combo_referential.setEnabled(False) self.status_label.setText(self.tr("")) self.password_asker = None else: - logging.debug("Show currencies loading") - self.currencies_tabwidget.show() - logging.debug("Hide homescreen") - self.homescreen.hide() self.password_asker = PasswordAskerDialog(self.app.current_account) self.combo_referential.blockSignals(True) @@ -308,13 +321,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.combo_referential.blockSignals(False) logging.debug(self.app.preferences) self.combo_referential.setCurrentIndex(self.app.preferences['ref']) - self.menu_account.setEnabled(True) + self.action_add_a_contact.setEnabled(True) + self.actionCertification.setEnabled(True) + self.actionTransfer_money.setEnabled(True) + self.menu_contacts_list.setEnabled(True) self.action_configure_parameters.setEnabled(True) self.setWindowTitle(self.tr("CuteCoin {0} - Account : {1}").format(__version__, self.app.current_account.name)) - self.refresh_communities() - self.refresh_wallets() self.refresh_contacts() def import_account(self): @@ -351,3 +365,14 @@ class MainWindow(QMainWindow, Ui_MainWindow): self.app.stop() super().closeEvent(event) + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh() + return super(MainWindow, self).changeEvent(event) + diff --git a/src/cutecoin/gui/member.py b/src/cutecoin/gui/member.py index 3f3139abe35e7581a99b880b6b986289fffb2f5c..d3ca7c1a83a44ba8101b42b25bc5b79e95d525f5 100644 --- a/src/cutecoin/gui/member.py +++ b/src/cutecoin/gui/member.py @@ -1,8 +1,10 @@ import datetime +import asyncio from PyQt5.QtWidgets import QDialog from ..core.graph import Graph +from ..tools.decorators import asyncify from ..gen_resources.member_uic import Ui_DialogMember from ..tools.exceptions import MembershipNotFoundError @@ -29,9 +31,14 @@ class MemberDialog(QDialog, Ui_DialogMember): self.account = account self.identity = identity self.label_uid.setText(identity.uid) + self.refresh() + + @asyncify + @asyncio.coroutine + def refresh(self): try: - join_date = self.identity.get_join_date(self.community) + join_date = yield from self.identity.get_join_date(self.community) except MembershipNotFoundError: join_date = None @@ -44,9 +51,11 @@ class MemberDialog(QDialog, Ui_DialogMember): graph = Graph(self.app, self.community) path = None # if selected member is not the account member... - if identity.pubkey != self.account.pubkey: + if self.identity.pubkey != self.account.pubkey: # add path from selected member to account member - path = graph.get_shortest_path_between_members(identity, self.account.identity(self.community)) + account_identity = yield from self.account.identity(self.community) + path = yield from graph.get_shortest_path_between_members(self.identity, + account_identity) text = self.tr(""" <table cellpadding="5"> @@ -62,8 +71,8 @@ class MemberDialog(QDialog, Ui_DialogMember): if path: distance = len(path) - 1 text += self.tr( - """<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""".format(self.tr('Distance'), - distance)) + """<tr><td align="right"><b>{:}</b></div></td><td>{:}</td></tr>""" + .format(self.tr('Distance'), distance)) if distance > 1: index = 0 for node in path: @@ -83,4 +92,3 @@ class MemberDialog(QDialog, Ui_DialogMember): # set text in label self.label_properties.setText(text) - diff --git a/src/cutecoin/gui/network_tab.py b/src/cutecoin/gui/network_tab.py index 73349efb34f90896c25bc1d5fe57977a0df3c19c..44dfea169ae55a6883cbed141f97276827d496a4 100644 --- a/src/cutecoin/gui/network_tab.py +++ b/src/cutecoin/gui/network_tab.py @@ -9,7 +9,7 @@ import asyncio from PyQt5.QtGui import QCursor, QDesktopServices from PyQt5.QtWidgets import QWidget, QMenu, QAction -from PyQt5.QtCore import Qt, QModelIndex, pyqtSlot, QUrl +from PyQt5.QtCore import Qt, QModelIndex, pyqtSlot, QUrl, QEvent from ..models.network import NetworkTableModel, NetworkFilterProxyModel from ..core.net.api import bma as qtbma from ..gen_resources.network_tab_uic import Ui_NetworkTabWidget @@ -20,32 +20,39 @@ class NetworkTabWidget(QWidget, Ui_NetworkTabWidget): classdocs """ - def __init__(self, app, community): + def __init__(self, app): """ Constructore of a network tab. :param cutecoin.core.Application app: The application - :param cutecoin.core.Community community: The community :return: A new network tab. :rtype: NetworkTabWidget """ super().__init__() self.app = app - self.community = community + self.community = None self.setupUi(self) - model = NetworkTableModel(community) + model = NetworkTableModel(self.community) proxy = NetworkFilterProxyModel(self) proxy.setSourceModel(model) self.table_network.setModel(proxy) self.table_network.sortByColumn(0, Qt.DescendingOrder) self.table_network.resizeColumnsToContents() - community.network.nodes_changed.connect(self.refresh_nodes) + + def change_community(self, community): + if self.community: + self.community.network.nodes_changed.disconnect(self.refresh_nodes) + if community: + community.network.nodes_changed.connect(self.refresh_nodes) + + self.community = community + self.table_network.model().change_community(community) @pyqtSlot() def refresh_nodes(self): logging.debug("Refresh nodes") - self.table_network.model().sourceModel().modelReset.emit() + self.table_network.model().sourceModel().refresh_nodes() def node_context_menu(self, point): index = self.table_network.indexAt(point) @@ -98,4 +105,13 @@ class NetworkTabWidget(QWidget, Ui_NetworkTabWidget): def manual_nodes_refresh(self): self.community.network.refresh_once() - + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh_nodes() + return super(NetworkTabWidget, self).changeEvent(event) diff --git a/src/cutecoin/gui/password_asker.py b/src/cutecoin/gui/password_asker.py index dcb65ddd1851dab1c0ae2cd21365f3a06a2979a8..04a7a4f92b9e262bbaafc47500f3da5ea492ded3 100644 --- a/src/cutecoin/gui/password_asker.py +++ b/src/cutecoin/gui/password_asker.py @@ -6,7 +6,8 @@ Created on 24 dec. 2014 import logging import re - +import asyncio +from PyQt5.QtCore import QEvent from PyQt5.QtWidgets import QDialog, QMessageBox from ..gen_resources.password_asker_uic import Ui_PasswordAskerDialog @@ -28,6 +29,21 @@ class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): self.password = "" self.remember = False + def future_exec(self): + future = asyncio.Future() + if not self.remember: + def future_show(): + pwd = self.password + if not self.remember: + self.password = "" + future.set_result(pwd) + self.open() + self.finished.connect(future_show) + else: + self.setResult(QDialog.Accepted) + future.set_result(self.password) + return future + def exec_(self): if not self.remember: super().exec_() @@ -67,6 +83,16 @@ class PasswordAskerDialog(QDialog, Ui_PasswordAskerDialog): self.password = "" super().reject() + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + return super(PasswordAskerDialog, self).changeEvent(event) + def detect_non_printable(data): control_chars = ''.join(map(chr, list(range(0, 32)) + list(range(127, 160)))) diff --git a/src/cutecoin/gui/preferences.py b/src/cutecoin/gui/preferences.py index 0372232d729004eaa1215f7da98e50eba0392aa8..919d50de092ee9435132737ead8351d74ac8eba7 100644 --- a/src/cutecoin/gui/preferences.py +++ b/src/cutecoin/gui/preferences.py @@ -78,6 +78,8 @@ class PreferencesDialog(QDialog, Ui_PreferencesDialog): 'proxy_port': self.spinbox_proxy_port.value(), 'international_system_of_units': self.checkbox_international_system.isChecked()} self.app.save_preferences(pref) + # change UI translation + self.app.switch_language() toast.display(self.tr("Preferences"), self.tr("A restart is needed to apply your new preferences.")) super().accept() diff --git a/src/cutecoin/gui/process_cfg_account.py b/src/cutecoin/gui/process_cfg_account.py index 5906db17f4bd003e747350dd19ddf6fcae45baef..02e6ffcc9c7edf0daf460ff9c350d41158a05473 100644 --- a/src/cutecoin/gui/process_cfg_account.py +++ b/src/cutecoin/gui/process_cfg_account.py @@ -4,8 +4,7 @@ Created on 6 mars 2014 @author: inso """ import logging -import requests -from ucoinpy.documents.peer import Peer +import asyncio from ucoinpy.key import SigningKey from ..gen_resources.account_cfg_uic import Ui_AccountConfigurationDialog from ..gui.process_cfg_community import ProcessConfigureCommunity @@ -50,8 +49,6 @@ class StepPageInit(Step): self.config_dialog.edit_account_name.setText(self.config_dialog.account.name) model = CommunitiesListModel(self.config_dialog.account) self.config_dialog.list_communities.setModel(model) - nb_wallets = len(self.config_dialog.account.wallets) - self.config_dialog.spinbox_wallets.setValue(nb_wallets) self.config_dialog.password_asker = PasswordAskerDialog(self.config_dialog.account) self.config_dialog.button_previous.setEnabled(False) @@ -95,8 +92,7 @@ class StepPageKey(Step): def process_next(self): salt = self.config_dialog.edit_salt.text() password = self.config_dialog.edit_password.text() - self.config_dialog.account.salt = salt - self.config_dialog.account.pubkey = SigningKey(salt, password).pubkey + self.config_dialog.account.set_scrypt_infos(salt, password) self.config_dialog.password_asker = PasswordAskerDialog(self.config_dialog.account) model = CommunitiesListModel(self.config_dialog.account) self.config_dialog.list_communities.setModel(model) @@ -122,12 +118,9 @@ class StepPageCommunities(Step): if self.config_dialog.password_asker.result() == QDialog.Rejected: return - nb_wallets = self.config_dialog.spinbox_wallets.value() - self.config_dialog.account.set_walletpool_size(nb_wallets, password) - self.config_dialog.app.add_account(self.config_dialog.account) if len(self.config_dialog.app.accounts) == 1: - self.config_dialog.app.default_account = self.config_dialog.account.name + self.config_dialog.app.preferences['account'] = self.config_dialog.account.name self.config_dialog.app.save(self.config_dialog.account) self.config_dialog.app.current_account = self.config_dialog.account @@ -196,17 +189,14 @@ class ProcessConfigureAccount(QDialog, Ui_AccountConfigurationDialog): self.list_communities.setModel(CommunitiesListModel(self.account)) def action_edit_account_key(self): - if self.step.is_valid(): - self.button_next.setEnabled(True) - else: - self.button_next.setEnabled(False) + self.button_generate.setEnabled(self.step.is_valid()) + self.button_next.setEnabled(self.step.is_valid()) def action_show_pubkey(self): salt = self.edit_salt.text() password = self.edit_password.text() pubkey = SigningKey(salt, password).pubkey - QMessageBox.information(self, self.tr("Public key"), - self.tr("These parameters pubkeys are : {0}").format(pubkey)) + self.label_info.setText(pubkey) def action_edit_account_parameters(self): if self.step.is_valid(): @@ -255,5 +245,11 @@ Are you sure ?""")) self.stacked_pages.setCurrentIndex(previous_index) self.step.display_page() + def async_exec(self): + future = asyncio.Future() + self.finished.connect(lambda r: future.set_result(r)) + self.open() + return future + def accept(self): super().accept() diff --git a/src/cutecoin/gui/process_cfg_community.py b/src/cutecoin/gui/process_cfg_community.py index 9f37d5eda232bfe842704ceb75f6f24938f4678d..9e6625960a595767f39dd06b803ec80d81f385fa 100644 --- a/src/cutecoin/gui/process_cfg_community.py +++ b/src/cutecoin/gui/process_cfg_community.py @@ -9,18 +9,19 @@ import asyncio from PyQt5.QtWidgets import QDialog, QMenu, QMessageBox, QApplication from PyQt5.QtGui import QCursor -from PyQt5.QtCore import pyqtSlot +from PyQt5.QtCore import pyqtSlot, pyqtSignal, QObject from ..gen_resources.community_cfg_uic import Ui_CommunityConfigurationDialog from ..models.peering import PeeringTreeModel from ..core import Community -from ..core.registry import Identity +from ..core.registry.identity import BlockchainState from ..core.net import Node from . import toast -class Step(): +class Step(QObject): def __init__(self, config_dialog, previous_step=None, next_step=None): + super().__init__() self.previous_step = previous_step self.next_step = next_step self.config_dialog = config_dialog @@ -34,25 +35,99 @@ class StepPageInit(Step): super().__init__(config_dialog) self.node = None logging.debug("Init") - self.config_dialog.button_next.setEnabled(False) - self.config_dialog.button_checknode.clicked.connect(self.check_node) + self.config_dialog.button_connect.clicked.connect(self.check_connect) + self.config_dialog.button_register.clicked.connect(self.check_register) + + @property + def app(self): + return self.config_dialog.app + + @property + def account(self): + return self.config_dialog.account + + @property + def community(self): + return self.config_dialog.community + + @property + def password_asker(self): + return self.config_dialog.password_asker + + @asyncio.coroutine + def coroutine_check_connect(self): + server = self.config_dialog.lineedit_server.text() + port = self.config_dialog.spinbox_port.value() + logging.debug("Is valid ? ") + self.node = yield from Node.from_address(self.config_dialog.app.network_manager, None, server, port) + if self.node: + community = Community.create(self.app.network_manager, self.node) + identity = yield from self.app.identities_registry.future_find(self.account.pubkey, community) + if identity.blockchain_state == BlockchainState.NOT_FOUND: + self.config_dialog.label_error.setText(self.tr("Could not find your identity on the network.")) + else: + self.config_dialog.community = community + self.config_dialog.next() + else: + self.config_dialog.label_error.setText(self.tr("Could not connect.")) + + @pyqtSlot() + def check_connect(self): + logging.debug("Check node") + asyncio.async(self.coroutine_check_connect()) @asyncio.coroutine - def coroutine_check_node(self): + def coroutine_check_register(self): server = self.config_dialog.lineedit_server.text() port = self.config_dialog.spinbox_port.value() logging.debug("Is valid ? ") self.node = yield from Node.from_address(self.config_dialog.app.network_manager, None, server, port) if self.node: - self.config_dialog.button_next.setEnabled(True) - self.config_dialog.button_check_node.setText("Ok !") + community = Community.create(self.app.network_manager, self.node) + identity = yield from self.app.identities_registry.future_find(self.account.pubkey, community) + if identity.blockchain_state == BlockchainState.NOT_FOUND: + password = yield from self.password_asker.future_exec() + if self.password_asker.result() == QDialog.Rejected: + return + self.config_dialog.label_error.setText(self.tr("Broadcasting identity...")) + self.account.selfcert_broadcasted.connect(self.handle_broadcast) + self.account.broadcast_error.connect(self.handle_error) + yield from self.account.send_selfcert(password, community) + self.config_dialog.community = community + else: + self.config_dialog.label_error.setText(self.tr("Pubkey already exists on the network")) else: - self.config_dialog.button_next.setEnabled(False) - self.config_dialog.button_check_node.setText("Could not connect.") + self.config_dialog.label_error.setText(self.tr("Could not connect.")) @pyqtSlot() - def check_node(self): - asyncio.async(self.coroutine_check_node()) + def check_register(self): + logging.debug("Check node") + asyncio.async(self.coroutine_check_register()) + + @pyqtSlot(int, str) + def handle_broadcast(self): + if self.app.preferences['notifications']: + toast.display(self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) + # Disabled : https://github.com/harvimt/quamash/issues/41 + # else: + # QMessageBox.information(self, self.tr("UID broadcast"), self.tr("Identity broadcasted to the network")) + self.account.selfcert_broadcasted.disconnect() + self.account.broadcast_error.disconnect(self.handle_error) + QApplication.restoreOverrideCursor() + self.config_dialog.next() + + @pyqtSlot(int, str) + def handle_error(self, error_code, text): + self.config_dialog.label_error.setText(self.tr("Error") + " " + \ + self.tr("{0} : {1}".format(error_code, text))) + if self.app.preferences['notifications']: + toast.display(self.tr("Error"), self.tr("{0} : {1}".format(error_code, text))) + # Disabled : https://github.com/harvimt/quamash/issues/41 + # else: + # QMessageBox.critical(self, self.tr("Error"), self.tr("{0} : {1}".format(error_code, text))) + self.account.selfcert_broadcasted.disconnect() + self.account.broadcast_error.disconnect(self.handle_error) + QApplication.restoreOverrideCursor() def is_valid(self): return self.node is not None @@ -66,7 +141,8 @@ class StepPageInit(Step): self.config_dialog.community = Community.create(self.config_dialog.app.network_manager, self.node) def display_page(self): - self.config_dialog.button_previous.setEnabled(False) + self.config_dialog.button_next.hide() + self.config_dialog.button_previous.hide() class StepPageAddpeers(Step): @@ -83,6 +159,8 @@ class StepPageAddpeers(Step): pass def display_page(self): + self.config_dialog.button_next.show() + self.config_dialog.button_previous.show() # We add already known peers to the displayed list self.config_dialog.nodes = self.config_dialog.community.network.root_nodes tree_model = PeeringTreeModel(self.config_dialog.community) @@ -96,6 +174,7 @@ class ProcessConfigureCommunity(QDialog, Ui_CommunityConfigurationDialog): """ Dialog to configure or add a community """ + community_added = pyqtSignal() def __init__(self, app, account, community, password_asker): """ @@ -115,6 +194,7 @@ class ProcessConfigureCommunity(QDialog, Ui_CommunityConfigurationDialog): self.step = None self.nodes = [] + self.community_added.connect(self.add_community_and_close) step_init = StepPageInit(self) step_add_peers = StepPageAddpeers(self) @@ -139,7 +219,7 @@ class ProcessConfigureCommunity(QDialog, Ui_CommunityConfigurationDialog): self.stacked_pages.setCurrentIndex(next_index) self.step.display_page() else: - asyncio.async(self.final()) + self.add_community_and_close() def previous(self): if self.step.previous_step is not None: @@ -196,49 +276,13 @@ class ProcessConfigureCommunity(QDialog, Ui_CommunityConfigurationDialog): action.setEnabled(False) menu.exec_(QCursor.pos()) - def selfcert_sent(self, pubkey, currency): - if self.app.preferences['notifications']: - toast.display(self.tr("UID Publishing"), - self.tr("Success publishing your UID").format(pubkey, currency)) - else: - QMessageBox.information(self, self.tr("UID Publishing"), - self.tr("Success publishing your UID").format(pubkey, currency)) - self.account.certification_broadcasted.disconnect() - self.account.broadcast_error.disconnect(self.handle_error) - QApplication.restoreOverrideCursor() - self.add_community_and_close() - - @pyqtSlot(int, str) - def handle_error(self, error_code, text): - if self.app.preferences['notifications']: - toast.display(self.tr("Error"), self.tr("{0} : {1}".format(error_code, text))) - else: - QMessageBox.critical(self, self.tr("Error"), self.tr("{0} : {1}".format(error_code, text))) - self.account.certification_broadcasted.disconnect() - self.account.broadcast_error.disconnect(self.handle_error) - QApplication.restoreOverrideCursor() + def async_exec(self): + future = asyncio.Future() + self.finished.connect(lambda r: future.set_result(r)) + self.open() + return future def add_community_and_close(self): if self.community not in self.account.communities: self.account.add_community(self.community) self.accept() - - @asyncio.coroutine - def final(self): - identity = yield from self.app.identities_registry.future_find(self.account.pubkey, self.community) - if identity.status == Identity.NOT_FOUND: - reply = QMessageBox.question(self, self.tr("Pubkey not found"), - self.tr("""The public key of your account wasn't found in the community. :\n -{0}\n -Would you like to publish the key ?""").format(self.account.pubkey)) - if reply == QMessageBox.Yes: - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - self.account.selfcert_broadcasted.connect(self.handle_broadcast) - self.account.broadcast_error.connect(self.handle_error) - asyncio.async(self.account.send_selfcert(password, self.community)) - else: - self.add_community_and_close() - else: - self.add_community_and_close() diff --git a/src/cutecoin/gui/toast.py b/src/cutecoin/gui/toast.py index afd30d7776087dab85ee0b9420ffd8eba5ae2927..505e4a9eb332e3afce17096919bab4f0cdf8590f 100644 --- a/src/cutecoin/gui/toast.py +++ b/src/cutecoin/gui/toast.py @@ -16,12 +16,17 @@ window = None # global def display(title, msg): logging.debug("NOTIFY DISPLAY") if sys.platform == "linux": - import notify2 - if not notify2.is_initted(): - logging.debug("Initialising notify2") - notify2.init("cutecoin") - n = notify2.Notification(title, - msg) + try: + import notify2 + if not notify2.is_initted(): + logging.debug("Initialising notify2") + notify2.init("cutecoin") + n = notify2.Notification(title, + msg) + n.show() + except ImportError: + _Toast(title, msg) + # fixme: https://bugs.python.org/issue11587 # # Not working... Empty icon at the moment. @@ -42,7 +47,6 @@ def display(title, msg): # ) # n.set_hint('icon_data', icon_struct) # n.set_timeout(5000) - n.show() else: _Toast(title, msg) diff --git a/src/cutecoin/gui/transactions_tab.py b/src/cutecoin/gui/transactions_tab.py index 2758daa9dee0649396df3dac6b0c2e439e7a51a3..ac380a11539cc3e33b599c10ac7891138f3d39cc 100644 --- a/src/cutecoin/gui/transactions_tab.py +++ b/src/cutecoin/gui/transactions_tab.py @@ -1,46 +1,84 @@ from PyQt5.QtWidgets import QWidget, QAbstractItemView, QHeaderView, QDialog, \ QMenu, QAction, QApplication, QMessageBox -from PyQt5.QtCore import Qt, QDateTime, QTime, QModelIndex, QLocale, QCoreApplication, pyqtSlot +from PyQt5.QtCore import Qt, QDateTime, QTime, QModelIndex, pyqtSignal, pyqtSlot, QEvent from PyQt5.QtGui import QCursor from ..gen_resources.transactions_tab_uic import Ui_transactionsTabWidget from ..models.txhistory import HistoryTableModel, TxFilterProxyModel from ..core.transfer import Transfer +from .contact import ConfigureContactDialog +from .member import MemberDialog +from .transfer import TransferMoneyDialog +from .certification import CertificationDialog from ..core.wallet import Wallet from ..core.registry import Identity +from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from .transfer import TransferMoneyDialog from . import toast import logging +import asyncio class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): """ classdocs """ + view_in_wot = pyqtSignal(Identity) - def __init__(self, app, community, password_asker, currency_tab): + def __init__(self, app): """ Init :param cutecoin.core.app.Application app: Application instance - :param cutecoin.core.community.Community community: Community instance - :param cutecoin.gui.password_asker.PasswordAskerDialog password_asker: Password dialog instance - :param cutecoin.gui.currency_tab.CurrencyTabWidget currency_tab: Currency tab widget :return: """ super().__init__() self.setupUi(self) self.app = app - self.community = community - self.password_asker = password_asker - self.currency_tab = currency_tab + self.account = None + self.community = None + self.password_asker = None + + ts_from = self.date_from.dateTime().toTime_t() + ts_to = self.date_to.dateTime().toTime_t() + model = HistoryTableModel(self.app, self.account, self.community) + proxy = TxFilterProxyModel(ts_from, ts_to) + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + proxy.setSortRole(Qt.DisplayRole) + + self.table_history.setModel(proxy) + self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) + self.table_history.setSortingEnabled(True) + self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) + self.table_history.resizeColumnsToContents() self.progressbar.hide() - self.community.inner_data_changed.connect(self.refresh_minimum_maximum) self.refresh() + def cancel_once_tasks(self): + cancel_once_task(self, self.refresh_minimum_maximum) + cancel_once_task(self, self.refresh_balance) + cancel_once_task(self, self.history_context_menu) + + def change_account(self, account, password_asker): + self.cancel_once_tasks() + self.account = account + self.password_asker = password_asker + self.table_history.model().sourceModel().change_account(account) + + def change_community(self, community): + self.cancel_once_tasks() + self.community = community + self.table_history.model().sourceModel().change_community(self.community) + self.refresh() + self.stop_progress([]) + + @once_at_a_time + @asyncify + @asyncio.coroutine def refresh_minimum_maximum(self): - block = self.community.get_block(1) + block = yield from self.community.get_block(1) minimum_datetime = QDateTime() minimum_datetime.setTime_t(block['medianTime']) minimum_datetime.setTime(QTime(0, 0)) @@ -56,23 +94,10 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): def refresh(self): #TODO: Use resetmodel instead of destroy/create - self.refresh_minimum_maximum() - ts_from = self.date_from.dateTime().toTime_t() - ts_to = self.date_to.dateTime().toTime_t() + if self.community: + self.refresh_minimum_maximum() - model = HistoryTableModel(self.app, self.community) - proxy = TxFilterProxyModel(ts_from, ts_to) - proxy.setSourceModel(model) - proxy.setDynamicSortFilter(True) - proxy.setSortRole(Qt.DisplayRole) - - self.table_history.setModel(proxy) - self.table_history.setSelectionBehavior(QAbstractItemView.SelectRows) - self.table_history.setSortingEnabled(True) - self.table_history.horizontalHeader().setSectionResizeMode(QHeaderView.Interactive) - self.table_history.resizeColumnsToContents() - - self.refresh_balance() + self.refresh_balance() def start_progress(self): def progressing(value, maximum): @@ -99,36 +124,26 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): self.table_history.model().sourceModel().refresh_transfers() self.table_history.resizeColumnsToContents() + @once_at_a_time + @asyncify + @asyncio.coroutine def refresh_balance(self): - # if referential is "units" - if self.app.current_account._current_ref == 0: - self.label_balance.show() - self.label_deposit.show() - self.label_payment.show() - else: - self.label_balance.hide() - self.label_deposit.hide() - self.label_payment.hide() - - proxy = self.table_history.model() - balance = proxy.deposits - proxy.payments - localized_deposits = self.app.current_account.current_ref(proxy.deposits, self.community, self.app).diff_localized() - localized_payments = self.app.current_account.current_ref(proxy.payments, self.community, self.app).diff_localized() - localized_balance = self.app.current_account.current_ref(balance, self.community, self.app).diff_localized() - - self.label_deposit.setText(QCoreApplication.translate("TransactionsTabWidget", "<b>Deposits</b> {:} {:}").format( - localized_deposits, - self.app.current_account.current_ref.units(self.community.short_currency) - )) - self.label_payment.setText(QCoreApplication.translate("TransactionsTabWidget", "<b>Payments</b> {:} {:}").format( - localized_payments, - self.app.current_account.current_ref.units(self.community.short_currency) - )) - self.label_balance.setText(QCoreApplication.translate("TransactionsTabWidget", "<b>Balance</b> {:} {:}").format( - localized_balance, - self.app.current_account.current_ref.units(self.community.short_currency) - )) - + amount = yield from self.app.current_account.amount(self.community) + localized_amount = yield from self.app.current_account.current_ref(amount, self.community, + self.app).localized(units=True, + international_system=self.app.preferences['international_system_of_units']) + + # set infos in label + self.label_balance.setText( + self.tr("{:}") + .format( + localized_amount + ) + ) + + @once_at_a_time + @asyncify + @asyncio.coroutine def history_context_menu(self, point): index = self.table_history.indexAt(point) model = self.table_history.model() @@ -141,9 +156,11 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): state_data = model.sourceModel().data(state_index, Qt.DisplayRole) pubkey_col = model.sourceModel().columns_types.index('pubkey') - identity_index = model.sourceModel().index(source_index.row(), + pubkey_index = model.sourceModel().index(source_index.row(), pubkey_col) - identity = model.sourceModel().data(identity_index, Qt.DisplayRole) + pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) + identity = yield from self.app.identities_registry.future_find(pubkey, self.community) + transfer = model.sourceModel().transfers[source_index.row()] if state_data == Transfer.REFUSED or state_data == Transfer.TO_SEND: send_back = QAction(self.tr("Send again"), self) @@ -158,23 +175,23 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): else: if isinstance(identity, Identity): informations = QAction(self.tr("Informations"), self) - informations.triggered.connect(self.currency_tab.tab_community.menu_informations) + informations.triggered.connect(self.menu_informations) informations.setData(identity) menu.addAction(informations) add_as_contact = QAction(self.tr("Add as contact"), self) - add_as_contact.triggered.connect(self.currency_tab.tab_community.menu_add_as_contact) + add_as_contact.triggered.connect(self.menu_add_as_contact) add_as_contact.setData(identity) menu.addAction(add_as_contact) send_money = QAction(self.tr("Send money"), self) - send_money.triggered.connect(self.currency_tab.tab_community.menu_send_money) + send_money.triggered.connect(self.menu_send_money) send_money.setData(identity) menu.addAction(send_money) if isinstance(identity, Identity): view_wot = QAction(self.tr("View in Web of Trust"), self) - view_wot.triggered.connect(self.currency_tab.tab_community.view_wot) + view_wot.triggered.connect(self.view_wot) view_wot.setData(identity) menu.addAction(view_wot) @@ -184,7 +201,7 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): menu.addAction(copy_pubkey) # Show the context menu. - menu.exec_(QCursor.pos()) + menu.popup(QCursor.pos()) def copy_pubkey_to_clipboard(self): data = self.sender().data() @@ -196,11 +213,52 @@ class TransactionsTabWidget(QWidget, Ui_transactionsTabWidget): elif data.__class__ is str: clipboard.setText(data) + def menu_informations(self): + person = self.sender().data() + self.identity_informations(person) + + def menu_add_as_contact(self): + person = self.sender().data() + self.add_identity_as_contact({'name': person.uid, + 'pubkey': person.pubkey}) + + def menu_send_money(self): + identity = self.sender().data() + self.send_money_to_identity(identity) + + def identity_informations(self, person): + dialog = MemberDialog(self.app, self.account, self.community, person) + dialog.exec_() + + def add_identity_as_contact(self, person): + dialog = ConfigureContactDialog(self.account, self.window(), person) + result = dialog.exec_() + if result == QDialog.Accepted: + self.window().refresh_contacts() + + def send_money_to_identity(self, identity): + if isinstance(identity, str): + pubkey = identity + else: + pubkey = identity.pubkey + result = TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, + self.community, identity) + if result == QDialog.Accepted: + currency_tab = self.window().currencies_tabwidget.currentWidget() + currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() + + def certify_identity(self, identity): + CertificationDialog.certify_identity(self.app, self.account, self.password_asker, + self.community, identity) + + def view_wot(self): + identity = self.sender().data() + self.view_in_wot.emit(identity) + def send_again(self): transfer = self.sender().data() dialog = TransferMoneyDialog(self.app, self.app.current_account, self.password_asker) - dialog.accepted.connect(self.currency_tab.refresh_wallets) sender = transfer.metadata['issuer'] wallet_index = [w.pubkey for w in self.app.current_account.wallets].index(sender) dialog.combo_wallets.setCurrentIndex(wallet_index) @@ -237,3 +295,14 @@ QMessageBox.Ok | QMessageBox.Cancel) self.table_history.model().set_period(ts_from, ts_to) self.refresh_balance() + + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh() + return super(TransactionsTabWidget, self).changeEvent(event) diff --git a/src/cutecoin/gui/transfer.py b/src/cutecoin/gui/transfer.py index d6ef4c738d22e0fc406cc4d8ba904051a19372be..1258314f18d4f0e4531a12b3c8f4545f51573c3d 100644 --- a/src/cutecoin/gui/transfer.py +++ b/src/cutecoin/gui/transfer.py @@ -9,6 +9,7 @@ from PyQt5.QtGui import QRegExpValidator from ..gen_resources.transfer_uic import Ui_TransferMoneyDialog from . import toast +from ..tools.decorators import asyncify import asyncio @@ -35,7 +36,6 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): self.wallet = None self.community = self.account.communities[0] self.wallet = self.account.wallets[0] - self.dividend = self.community.dividend regexp = QRegExp('^([ a-zA-Z0-9-_:/;*?\[\]\(\)\\\?!^+=@&~#{}|<>%.]{0,255})$') validator = QRegExpValidator(regexp) @@ -55,6 +55,14 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): self.radio_contact.setEnabled(False) self.radio_pubkey.setChecked(True) + @staticmethod + def send_money_to_identity(app, account, password_asker, community, identity): + dialog = TransferMoneyDialog(app, account, password_asker) + dialog.edit_pubkey.setText(identity.pubkey) + dialog.combo_community.setCurrentText(community.name) + dialog.radio_pubkey.setChecked(True) + return dialog.exec() + def accept(self): comment = self.edit_message.text() @@ -105,60 +113,64 @@ class TransferMoneyDialog(QDialog, Ui_TransferMoneyDialog): self.wallet.broadcast_error.disconnect(self.handle_error) QApplication.restoreOverrideCursor() - def amount_changed(self): - amount = self.spinbox_amount.value() - relative = amount / self.dividend + @asyncify + @asyncio.coroutine + def amount_changed(self, value): + dividend = yield from self.community.dividend() + relative = value / dividend self.spinbox_relative.blockSignals(True) self.spinbox_relative.setValue(relative) self.spinbox_relative.blockSignals(False) - def relative_amount_changed(self): - relative = self.spinbox_relative.value() - amount = relative * self.dividend + @asyncify + @asyncio.coroutine + def relative_amount_changed(self, value): + dividend = yield from self.community.dividend() + amount = value * dividend self.spinbox_amount.blockSignals(True) self.spinbox_amount.setValue(amount) self.spinbox_amount.blockSignals(False) + @asyncify + @asyncio.coroutine def change_current_community(self, index): self.community = self.account.communities[index] - self.dividend = self.community.dividend - amount = self.wallet.value(self.community) - ref_amount = self.account.units_to_ref(amount, self.community) - ref_name = self.account.ref_name(self.community.currency) - # if referential type is quantitative... - if self.account.ref_type() == 'q': - # display int values - ref_amount = QLocale().toString(float(ref_amount), 'f', 0) - else: - # display float values - ref_amount = QLocale().toString(float(ref_amount), 'f', 6) - self.label_total.setText("{0} {1}".format(ref_amount, ref_name)) + amount = yield from self.wallet.value(self.community) + + ref_text = yield from self.account.current_ref(amount, self.community, self.app)\ + .diff_localized(units=True, + international_system=self.app.preferences['international_system_of_units']) + self.label_total.setText("{0}".format(ref_text)) self.spinbox_amount.setSuffix(" " + self.community.currency) self.spinbox_amount.setValue(0) - amount = self.wallet.value(self.community) - relative = amount / self.dividend + amount = yield from self.wallet.value(self.community) + dividend = yield from self.community.dividend() + relative = amount / dividend self.spinbox_amount.setMaximum(amount) self.spinbox_relative.setMaximum(relative) + @asyncify + @asyncio.coroutine def change_displayed_wallet(self, index): self.wallet = self.account.wallets[index] - amount = self.wallet.value(self.community) - ref_amount = self.account.units_to_ref(amount, self.community) - ref_name = self.account.ref_name(self.community.currency) - # if referential type is quantitative... - if self.account.ref_type() == 'q': - # display int values - ref_amount = QLocale().toString(float(ref_amount), 'f', 0) - else: - # display float values - ref_amount = QLocale().toString(float(ref_amount), 'f', 6) - self.label_total.setText("{0} {1}".format(ref_amount, ref_name)) + amount = yield from self.wallet.value(self.community) + ref_text = yield from self.account.current_ref(amount, self.community, self.app)\ + .diff_localized(units=True, + international_system=self.app.preferences['international_system_of_units']) + self.label_total.setText("{0}".format(ref_text)) self.spinbox_amount.setValue(0) - amount = self.wallet.value(self.community) - relative = amount / self.dividend + amount = yield from self.wallet.value(self.community) + dividend = yield from self.community.dividend() + relative = amount / dividend self.spinbox_amount.setMaximum(amount) self.spinbox_relative.setMaximum(relative) def recipient_mode_changed(self, pubkey_toggled): self.edit_pubkey.setEnabled(pubkey_toggled) self.combo_contact.setEnabled(not pubkey_toggled) + + def async_exec(self): + future = asyncio.Future() + self.finished.connect(lambda r: future.set_result(r)) + self.open() + return future diff --git a/src/cutecoin/gui/wallets_tab.py b/src/cutecoin/gui/wallets_tab.py deleted file mode 100644 index 32e601f33e3359a3655f1319e8bf8cc8caedc71e..0000000000000000000000000000000000000000 --- a/src/cutecoin/gui/wallets_tab.py +++ /dev/null @@ -1,342 +0,0 @@ -""" -Created on 15 févr. 2015 - -@author: inso -""" -import asyncio -import logging - -from PyQt5.QtWidgets import QWidget, QMenu, QAction, QApplication, QDialog, QMessageBox -from PyQt5.QtCore import QDateTime, QModelIndex, Qt, QLocale -from PyQt5.QtGui import QCursor - -from ..core.registry import Identity -from ..core.wallet import Wallet -from cutecoin.gui import toast -from ..gui.password_asker import PasswordAskerDialog -from ..models.wallets import WalletsTableModel, WalletsFilterProxyModel -from .transfer import TransferMoneyDialog -from ..tools.exceptions import MembershipNotFoundError, NoPeerAvailable, LookupFailureError -from ..gen_resources.wallets_tab_uic import Ui_WalletsTab - - -class WalletsTabWidget(QWidget, Ui_WalletsTab): - """ - classdocs - """ - - def __init__(self, app, account, community, password_asker): - """ - Init - :param cutecoin.core.app.Application app: Application instance - :param cutecoin.core.account.Account account: Account instance - :param cutecoin.core.community.Community community: Community instance - :param cutecoin.gui.password_asker.PasswordAskerDialog password_asker: PasswordAskerDialog instance - """ - super().__init__() - self.setupUi(self) - self.app = app - self.account = account - self.community = community - self.password_asker = password_asker - self.setup_connections() - - def setup_connections(self): - self.account.inner_data_changed.connect(self.refresh_informations_frame) - self.community.inner_data_changed.connect(self.refresh_informations_frame) - - def refresh(self): - self.refresh_informations_frame() - self.refresh_wallets() - self.refresh_quality_buttons() - - def refresh_wallets(self): - # TODO: Using reset model instead of destroy/create - wallets_model = WalletsTableModel(self.app, self.community) - proxy_model = WalletsFilterProxyModel() - proxy_model.setSourceModel(wallets_model) - wallets_model.dataChanged.connect(self.wallet_changed) - self.table_wallets.setModel(proxy_model) - self.table_wallets.resizeColumnsToContents() - - def refresh_informations_frame(self): - parameters = self.community.parameters - try: - identity = self.account.identity(self.community) - membership = identity.membership(self.community) - renew_block = membership['blockNumber'] - last_renewal = self.community.get_block(renew_block)['medianTime'] - expiration = last_renewal + parameters['sigValidity'] - except MembershipNotFoundError: - last_renewal = None - expiration = None - - certified = identity.unique_valid_certified_by(self.app.identities_registry, self.community) - certifiers = identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) - if last_renewal and expiration: - date_renewal = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(last_renewal).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat) - ) - date_expiration = QLocale.toString( - QLocale(), - QDateTime.fromTime_t(expiration).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat) - ) - - if self.account.pubkey in self.community.members_pubkeys(): - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.account.name, self.account.pubkey, - self.tr("Membership"), - self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration), - self.tr("Your web of trust"), - self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), - len(certified)) - ) - ) - else: - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.account.name, self.account.pubkey, - self.tr("Not a member"), - self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration), - self.tr("Your web of trust"), - self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), - len(certified)) - ) - ) - else: - # set infos in label - self.label_general.setText( - self.tr(""" - <table cellpadding="5"> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - <tr><td align="right"><b>{:}</b></td></tr> - <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> - </table> - """).format( - self.account.name, self.account.pubkey, - self.tr("Not a member"), - self.tr("Your web of trust"), - self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), - len(certified)) - ) - ) - - amount = self.account.amount(self.community) - maximum = self.community.monetary_mass - # if referential type is quantitative... - # display int values - localized_amount = self.account.current_ref(amount, self.community, self.app).localized(units=True) - localized_minimum = self.account.current_ref(0, self.community, self.app).localized(units=True) - localized_maximum = self.account.current_ref(maximum, self.community, self.app).localized(units=True) - - # set infos in label - self.label_balance.setText( - self.tr("{:}") - .format( - localized_amount - ) - ) - self.label_balance_range.setText( - self.tr("in [{:} ; {:}]") - .format( - localized_minimum, - localized_maximum - ) - ) - - def wallet_context_menu(self, point): - index = self.table_wallets.indexAt(point) - model = self.table_wallets.model() - if index.row() < model.rowCount(QModelIndex()): - source_index = model.mapToSource(index) - - name_col = model.sourceModel().columns_types.index('name') - name_index = model.index(index.row(), - name_col) - - pubkey_col = model.sourceModel().columns_types.index('pubkey') - pubkey_index = model.sourceModel().index(source_index.row(), - pubkey_col) - - pubkey = model.sourceModel().data(pubkey_index, Qt.DisplayRole) - menu = QMenu(model.data(index, Qt.DisplayRole), self) - - new_wallet = QAction(self.tr("New Wallet"), self) - new_wallet.triggered.connect(self.new_wallet) - - rename = QAction(self.tr("Rename"), self) - rename.triggered.connect(self.rename_wallet) - rename.setData(name_index) - - copy_pubkey = QAction(self.tr("Copy pubkey to clipboard"), self) - copy_pubkey.triggered.connect(self.copy_pubkey_to_clipboard) - copy_pubkey.setData(pubkey) - - transfer_to = QMenu() - transfer_to.setTitle(self.tr("Transfer to...")) - for w in self.account.wallets: - if w == self.account.wallets[source_index.row()]: - continue - transfer_action = QAction(w.name, self) - transfer_action.triggered.connect(self.transfer_to_wallet) - wallets = (self.account.wallets[source_index.row()], w) - transfer_action.setData(wallets) - transfer_to.addAction(transfer_action) - - menu.addAction(new_wallet) - menu.addAction(rename) - menu.addAction(copy_pubkey) - menu.addMenu(transfer_to) - # Show the context menu. - menu.exec_(QCursor.pos()) - - def new_wallet(self): - """ - Create a new wallet - """ - password_asker = PasswordAskerDialog(self.app.current_account) - password = password_asker.exec_() - if password_asker.result() == QDialog.Rejected: - return None - # create new wallet by increasing wallet pool size - self.account.set_walletpool_size(len(self.account.wallets) + 1, password) - # capture new wallet - wallet = self.account.wallets[len(self.account.wallets) - 1] - # feed cache data of the wallet - wallet.refresh_cache(self.community, list()) - # save wallet cache on disk - self.app.save_wallet(self.account, self.account.wallets[len(self.account.wallets) - 1]) - # save account cache on disk (update number of wallets) - self.app.save(self.account) - # refresh wallet list in gui - self.refresh() - - def rename_wallet(self): - index = self.sender().data() - self.table_wallets.edit(index) - - def wallet_changed(self): - self.table_wallets.resizeColumnsToContents() - self.app.save(self.app.current_account) - - def copy_pubkey_to_clipboard(self): - data = self.sender().data() - clipboard = QApplication.clipboard() - if data.__class__ is Wallet: - clipboard.setText(data.pubkey) - elif data.__class__ is Identity: - clipboard.setText(data.pubkey) - elif data.__class__ is str: - clipboard.setText(data) - - def transfer_to_wallet(self): - wallets = self.sender().data() - dialog = TransferMoneyDialog(self.app, self.account, self.password_asker) - dialog.edit_pubkey.setText(wallets[1].pubkey) - dialog.combo_community.setCurrentText(self.community.name) - dialog.combo_wallets.setCurrentText(wallets[0].name) - dialog.radio_pubkey.setChecked(True) - if dialog.exec_() == QDialog.Accepted: - currency_tab = self.window().currencies_tabwidget.currentWidget() - currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() - - def send_membership_demand(self): - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - asyncio.async(self.account.send_membership(password, self.community, 'IN')) - - def send_membership_leaving(self): - reply = QMessageBox.warning(self, self.tr("Warning"), - self.tr("""Are you sure ? -Sending a leaving demand cannot be canceled. -The process to join back the community later will have to be done again.""") -.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) - if reply == QMessageBox.Ok: - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - - asyncio.async(self.account.send_membership(password, self.community, 'OUT')) - - def publish_uid(self): - reply = QMessageBox.warning(self, self.tr("Warning"), - self.tr("""Are you sure ? -Publishing your UID can be canceled by Revoke UID.""") -.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) - if reply == QMessageBox.Ok: - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - - try: - self.account.send_selfcert(password, self.community) - toast.display(self.tr("UID Publishing"), - self.tr("Success publishing your UID")) - except ValueError as e: - QMessageBox.critical(self, self.tr("Publish UID error"), - str(e)) - except NoPeerAvailable as e: - QMessageBox.critical(self, self.tr("Network error"), - self.tr("Couldn't connect to network : {0}").format(e), - QMessageBox.Ok) - # except Exception as e: - # QMessageBox.critical(self, self.tr("Error"), - # "{0}".format(e), - # QMessageBox.Ok) - - def revoke_uid(self): - reply = QMessageBox.warning(self, self.tr("Warning"), - self.tr("""Are you sure ? -Revoking your UID can only success if it is not already validated by the network.""") -.format(self.account.pubkey), QMessageBox.Ok | QMessageBox.Cancel) - if reply == QMessageBox.Ok: - password = self.password_asker.exec_() - if self.password_asker.result() == QDialog.Rejected: - return - - asyncio.async(self.account.revoke(password, self.community)) - - def refresh_quality_buttons(self): - try: - if self.account.identity(self.community).published_uid(self.community): - logging.debug("UID Published") - if self.account.identity(self.community).is_member(self.community): - self.button_membership.setText(self.tr("Renew membership")) - self.button_membership.show() - self.button_publish_uid.hide() - self.button_leaving.show() - self.button_revoke_uid.hide() - else: - logging.debug("Not a member") - self.button_membership.setText(self.tr("Send membership demand")) - self.button_membership.show() - self.button_revoke_uid.show() - self.button_leaving.hide() - self.button_publish_uid.hide() - else: - logging.debug("UID not published") - self.button_membership.hide() - self.button_leaving.hide() - self.button_publish_uid.show() - self.button_revoke_uid.hide() - except LookupFailureError: - self.button_membership.hide() - self.button_leaving.hide() - self.button_publish_uid.show() diff --git a/src/cutecoin/gui/wot_tab.py b/src/cutecoin/gui/wot_tab.py index ec8d5754f830fa4bec443814d1360f328ea96b89..b3457d2fe1e7272882702f10e926909d66cd1b0c 100644 --- a/src/cutecoin/gui/wot_tab.py +++ b/src/cutecoin/gui/wot_tab.py @@ -1,28 +1,31 @@ # -*- coding: utf-8 -*- import logging -from cutecoin.core.graph import Graph -from PyQt5.QtWidgets import QWidget, QComboBox, QLineEdit -from PyQt5.QtCore import pyqtSlot -from cutecoin.core.net.api import bma -from cutecoin.core.registry import BlockchainState +import asyncio +from PyQt5.QtWidgets import QWidget, QComboBox, QDialog +from PyQt5.QtCore import pyqtSlot, QEvent, QLocale, QDateTime + +from ..tools.exceptions import MembershipNotFoundError +from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task +from ..core.net.api import bma +from ..core.graph import Graph +from ..core.registry import BlockchainState +from .member import MemberDialog +from .certification import CertificationDialog +from .transfer import TransferMoneyDialog +from .contact import ConfigureContactDialog from ..gen_resources.wot_tab_uic import Ui_WotTabWidget from cutecoin.gui.views.wot import NODE_STATUS_HIGHLIGHTED, NODE_STATUS_SELECTED, NODE_STATUS_OUT, ARC_STATUS_STRONG, \ ARC_STATUS_WEAK class WotTabWidget(QWidget, Ui_WotTabWidget): - def __init__(self, app, account, community, password_asker, parent=None): + def __init__(self, app): """ :param cutecoin.core.app.Application app: Application instance - :param cutecoin.core.account.Account account: Account instance - :param cutecoin.core.community.Community community: Community instance - :param QWidget parent: Parent :return: """ - super().__init__(parent) - self.parent = parent - + super().__init__() # construct from qtDesigner self.setupUi(self) @@ -41,18 +44,115 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): self.graphicsView.scene().node_contact.connect(self.add_node_as_contact) self.graphicsView.scene().node_member.connect(self.identity_informations) - self.account = account - self.community = community - self.password_asker = password_asker + self.account = None + self.community = None + self.password_asker = None self.app = app + self.draw_task = None # nodes list for menu from search self.nodes = list() # create node metadata from account self._current_identity = None - self.draw_graph(self.account.identity(self.community)) - self.community.network.new_block_mined.connect(self.refresh) + + def cancel_once_tasks(self): + cancel_once_task(self, self.draw_graph) + cancel_once_task(self, self.refresh_informations_frame) + cancel_once_task(self, self.reset) + + def change_account(self, account, password_asker): + self.account = account + self.password_asker = password_asker + + def change_community(self, community): + if self.community: + self.community.network.new_block_mined.disconnect(self.refresh) + if community: + community.network.new_block_mined.connect(self.refresh) + self.community = community + self.reset() + + @once_at_a_time + @asyncify + @asyncio.coroutine + def refresh_informations_frame(self): + parameters = self.community.parameters + try: + identity = yield from self.account.identity(self.community) + membership = identity.membership(self.community) + renew_block = membership['blockNumber'] + last_renewal = self.community.get_block(renew_block)['medianTime'] + expiration = last_renewal + parameters['sigValidity'] + except MembershipNotFoundError: + last_renewal = None + expiration = None + + certified = yield from identity.unique_valid_certified_by(self.app.identities_registry, self.community) + certifiers = yield from identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) + if last_renewal and expiration: + date_renewal = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(last_renewal).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat) + ) + date_expiration = QLocale.toString( + QLocale(), + QDateTime.fromTime_t(expiration).date(), QLocale.dateFormat(QLocale(), QLocale.LongFormat) + ) + + if self.account.pubkey in self.community.members_pubkeys(): + # set infos in label + self.label_general.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + </table> + """).format( + self.account.name, self.account.pubkey, + self.tr("Membership"), + self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration), + self.tr("Your web of trust"), + self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), + len(certified)) + ) + ) + else: + # set infos in label + self.label_general.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + </table> + """).format( + self.account.name, self.account.pubkey, + self.tr("Not a member"), + self.tr("Last renewal on {:}, expiration on {:}").format(date_renewal, date_expiration), + self.tr("Your web of trust"), + self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), + len(certified)) + ) + ) + else: + # set infos in label + self.label_general.setText( + self.tr(""" + <table cellpadding="5"> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + <tr><td align="right"><b>{:}</b></td></tr> + <tr><td align="right"><b>{:}</b></td><td>{:}</td></tr> + </table> + """).format( + self.account.name, self.account.pubkey, + self.tr("Not a member"), + self.tr("Your web of trust"), + self.tr("Certified by {:} members; Certifier of {:} members").format(len(certifiers), + len(certified)) + ) + ) @pyqtSlot(dict) def handle_node_click(self, metadata): @@ -64,6 +164,9 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): ) ) + @once_at_a_time + @asyncify + @asyncio.coroutine def draw_graph(self, identity): """ Draw community graph centered on the identity @@ -72,69 +175,65 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): """ logging.debug("Draw graph - " + identity.uid) - identity_account = self.account.identity(self.community) - - # Disconnect old identity - try: - if self._current_identity and self._current_identity != identity: - self._current_identity.inner_data_changed.disconnect(self.handle_identity_change) - except TypeError as e: - if "disconnect()" in str(e): - logging.debug("Disconnect of old identity failed.") - - #Connect new identity - if self._current_identity != identity: - self._current_identity = identity - identity.inner_data_changed.connect(self.handle_identity_change) - - # create Identity from node metadata - certifier_list = identity.unique_valid_certifiers_of(self.app.identities_registry, self.community) - certified_list = identity.unique_valid_certified_by(self.app.identities_registry, self.community) - - # create empty graph instance - graph = Graph(self.app, self.community) - - # add wallet node - node_status = 0 - if identity == identity_account: - node_status += NODE_STATUS_HIGHLIGHTED - if identity.is_member(self.community) is False: - node_status += NODE_STATUS_OUT - node_status += NODE_STATUS_SELECTED - graph.add_identity(identity, node_status) - - # populate graph with certifiers-of - graph.add_certifier_list(certifier_list, identity, identity_account) - # populate graph with certified-by - graph.add_certified_list(certified_list, identity, identity_account) - - # draw graph in qt scene - self.graphicsView.scene().update_wot(graph.get()) - - # if selected member is not the account member... - if identity.pubkey != identity_account.pubkey: - # add path from selected member to account member - path = graph.get_shortest_path_between_members(identity, identity_account) - if path: - self.graphicsView.scene().update_path(path) - - def reset(self): + if self.community: + identity_account = yield from self.account.identity(self.community) + + #Connect new identity + if self._current_identity != identity: + self._current_identity = identity + + # create Identity from node metadata + certifier_list = yield from identity.unique_valid_certifiers_of(self.app.identities_registry, + self.community) + certified_list = yield from identity.unique_valid_certified_by(self.app.identities_registry, + self.community) + + # create empty graph instance + graph = Graph(self.app, self.community) + + # add wallet node + node_status = 0 + if identity == identity_account: + node_status += NODE_STATUS_HIGHLIGHTED + if identity.is_member(self.community) is False: + node_status += NODE_STATUS_OUT + node_status += NODE_STATUS_SELECTED + graph.add_identity(identity, node_status) + + # populate graph with certifiers-of + yield from graph.add_certifier_list(certifier_list, identity, identity_account) + # populate graph with certified-by + yield from graph.add_certified_list(certified_list, identity, identity_account) + + # draw graph in qt scene + self.graphicsView.scene().update_wot(graph.get()) + + # if selected member is not the account member... + if identity.pubkey != identity_account.pubkey: + # add path from selected member to account member + path = yield from graph.get_shortest_path_between_members(identity, identity_account) + if path: + self.graphicsView.scene().update_path(path) + + @once_at_a_time + @asyncify + @asyncio.coroutine + def reset(self, checked=False): """ Reset graph scene to wallet identity """ - self.draw_graph( - self.account.identity(self.community) - ) + if self.account: + identity = yield from self.account.identity(self.community) + self.draw_graph(identity) def refresh(self): """ Refresh graph scene to current metadata """ - self.draw_graph(self._current_identity) - - @pyqtSlot(str) - def handle_identity_change(self, request): - self.refresh() + if self._current_identity: + self.draw_graph(self._current_identity) + else: + self.reset() def search(self): """ @@ -185,7 +284,8 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): metadata['id'], BlockchainState.VALIDATED ) - self.parent.identity_informations(identity) + dialog = MemberDialog(self.app, self.account, self.community, identity) + dialog.exec_() def sign_node(self, metadata): identity = self.app.identities_registry.from_handled_data( @@ -193,7 +293,8 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): metadata['id'], BlockchainState.VALIDATED ) - self.parent.certify_identity(identity) + CertificationDialog.certify_identity(self.app, self.account, self.password_asker, + self.community, identity) def send_money_to_node(self, metadata): identity = self.app.identities_registry.from_handled_data( @@ -201,20 +302,30 @@ class WotTabWidget(QWidget, Ui_WotTabWidget): metadata['id'], BlockchainState.VALIDATED ) - self.parent.send_money_to_identity(identity) + result = TransferMoneyDialog.send_money_to_identity(self.app, self.account, self.password_asker, + self.community, identity) + if result == QDialog.Accepted: + currency_tab = self.window().currencies_tabwidget.currentWidget() + currency_tab.tab_history.table_history.model().sourceModel().refresh_transfers() def add_node_as_contact(self, metadata): # check if contact already exists... if metadata['id'] == self.account.pubkey \ or metadata['id'] in [contact['pubkey'] for contact in self.account.contacts]: return False - self.parent.add_identity_as_contact({'name': metadata['text'], - 'pubkey': metadata['id']}) + dialog = ConfigureContactDialog(self.account, self.window(), {'name': metadata['text'], + 'pubkey': metadata['id']}) + result = dialog.exec_() + if result == QDialog.Accepted: + self.window().refresh_contacts() - def get_block_mediantime(self, number): - try: - block = self.community.get_block(number) - except Exception as e: - logging.debug('community.get_block request error : ' + str(e)) - return False - return block.mediantime + def changeEvent(self, event): + """ + Intercepte LanguageChange event to translate UI + :param QEvent QEvent: Event + :return: + """ + if event.type() == QEvent.LanguageChange: + self.retranslateUi(self) + self.refresh() + return super(WotTabWidget, self).changeEvent(event) diff --git a/src/cutecoin/main.py b/src/cutecoin/main.py index 1d4a211e351da6b53d6d84a779e1c478c5072fbf..fec1fbbbdd19ad8d3973a8ff365df2effba3845e 100755 --- a/src/cutecoin/main.py +++ b/src/cutecoin/main.py @@ -8,6 +8,8 @@ import sys import asyncio import logging import os +# To force cx_freeze import +import PyQt5.QtSvg from quamash import QEventLoop from PyQt5.QtWidgets import QApplication diff --git a/src/cutecoin/models/identities.py b/src/cutecoin/models/identities.py index 1a66f0f92b18518a5cb59bc0066a6992cbf0a690..431416c03d8edc0b199c0ea6264618c0e26d7350 100644 --- a/src/cutecoin/models/identities.py +++ b/src/cutecoin/models/identities.py @@ -6,10 +6,12 @@ Created on 5 févr. 2014 from ..core.net.api import bma as qtbma from ..tools.exceptions import NoPeerAvailable, MembershipNotFoundError +from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from PyQt5.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt, \ QDateTime, QModelIndex, QLocale from PyQt5.QtGui import QColor import logging +import asyncio class IdentitiesFilterProxyModel(QSortFilterProxyModel): @@ -21,6 +23,10 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): self.community = sourceModel.community super().setSourceModel(sourceModel) + def change_community(self, community): + self.community = community + self.sourceModel().change_community(community) + def lessThan(self, left, right): """ Sort table by given column number. @@ -39,7 +45,7 @@ class IdentitiesFilterProxyModel(QSortFilterProxyModel): expiration_index = self.sourceModel().index(source_index.row(), expiration_col) expiration_data = self.sourceModel().data(expiration_index, Qt.DisplayRole) current_time = QDateTime().currentDateTime().toMSecsSinceEpoch() - sig_validity = self.community.parameters['sigValidity'] + sig_validity = self.sourceModel().sig_validity() warning_expiration_time = int(sig_validity / 3) #logging.debug("{0} > {1}".format(current_time, expiration_data)) if expiration_data is not None: @@ -73,12 +79,12 @@ class IdentitiesTableModel(QAbstractTableModel): A Qt abstract item model to display communities in a tree """ - def __init__(self, community, parent=None): + def __init__(self, parent=None): """ Constructor """ super().__init__(parent) - self.community = community + self.community = None self.columns_titles = {'uid': self.tr('UID'), 'pubkey': self.tr('Pubkey'), 'renewed': self.tr('Renewed'), @@ -86,8 +92,14 @@ class IdentitiesTableModel(QAbstractTableModel): 'validation': self.tr('Validation')} self.columns_ids = ('uid', 'pubkey', 'renewed', 'expiration') self.identities_data = [] - self._identities = [] - self._refresh_slots = [] + self._sig_validity = 0 + + def change_community(self, community): + cancel_once_task(self, self.refresh_identities) + self.community = community + + def sig_validity(self): + return self._sig_validity @property def pubkeys(self): @@ -96,16 +108,20 @@ class IdentitiesTableModel(QAbstractTableModel): """ return [i[1] for i in self.identities_data] + @asyncio.coroutine def identity_data(self, identity): try: - join_date = identity.get_join_date(self.community) - expiration_date = identity.get_expiration_date(self.community) + join_date = yield from identity.get_join_date(self.community) + expiration_date = yield from identity.get_expiration_date(self.community) except MembershipNotFoundError: join_date = None expiration_date = None return (identity.uid, identity.pubkey, join_date, expiration_date) + @once_at_a_time + @asyncify + @asyncio.coroutine def refresh_identities(self, identities): """ Change the identities to display @@ -113,49 +129,15 @@ class IdentitiesTableModel(QAbstractTableModel): :param cutecoin.core.registry.IdentitiesRegistry identities: The new identities to display """ logging.debug("Refresh {0} identities".format(len(identities))) - - # We disconnect identities from their local slots - for (index, identity) in enumerate(self._identities): - identity.inner_data_changed.disconnect(self._refresh_slots[index]) - - self.identities_data = [] - self._identities = [] - self._refresh_slots = [] self.beginResetModel() + self.identities_data = [] for identity in identities: - logging.debug(identity) - - # Connection - refresh_slot = lambda req, identity=identity: self.refresh_identity(req, identity) - identity.inner_data_changed.connect(refresh_slot) - - self._identities.append(identity) - self._refresh_slots.append(refresh_slot) - self.identities_data.append(self.identity_data(identity)) + data = yield from self.identity_data(identity) + self.identities_data.append(data) + parameters = yield from self.community.parameters() + self._sig_validity = parameters['sigValidity'] self.endResetModel() - def refresh_identity(self, request, identity): - """ - Refresh an identity when its inner_data changed - :param cutecoin.core.registry.Identity identity: The refreshed identity - """ - logging.debug("Refresh {0} because of {1}".format(identity, request)) - try: - index = self._identities.index(identity) - self.identities_data[index] = self.identity_data(identity) - if request == str(qtbma.wot.Lookup): - model_index_0 = self.createIndex(index, self.columns_ids.index('uid')) - model_index_max = self.createIndex(index, self.columns_ids.index('pubkey')) - self.dataChanged.emit(model_index_0, model_index_max) - elif request in (str(qtbma.blockchain.Membership), - str(qtbma.blockchain.Block), - str(qtbma.blockchain.Parameters)): - model_index_0 = self.createIndex(index, self.columns_ids.index('renewed')) - model_index_max = self.createIndex(index, self.columns_ids.index('expiration')) - self.dataChanged.emit(model_index_0, model_index_max) - except ValueError: - logging.debug("Identity {0} is not in list : {1}".format(identity, self.identities_data)) - def rowCount(self, parent): return len(self.identities_data) diff --git a/src/cutecoin/models/network.py b/src/cutecoin/models/network.py index 899752684bd10b345958709650c9503b96dccd8b..fdf42e0ec616491ebe05c9093c1a7c6055f9dd4d 100644 --- a/src/cutecoin/models/network.py +++ b/src/cutecoin/models/network.py @@ -5,11 +5,13 @@ Created on 5 févr. 2014 """ import logging +import asyncio from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel from PyQt5.QtGui import QColor, QFont from ..tools.exceptions import NoPeerAvailable +from ..tools.decorators import asyncify from cutecoin.core.net.node import Node @@ -19,7 +21,11 @@ class NetworkFilterProxyModel(QSortFilterProxyModel): self.community = None def columnCount(self, parent): - return self.sourceModel().columnCount(None) - 1 + return self.sourceModel().columnCount(None) - 2 + + def change_community(self, community): + self.community = community + self.sourceModel().change_community(community) def setSourceModel(self, sourceModel): self.community = sourceModel.community @@ -66,6 +72,13 @@ class NetworkFilterProxyModel(QSortFilterProxyModel): and role == Qt.DisplayRole: return source_data[:5] + if index.column() == source_model.columns_types.index('current_block') \ + and role == Qt.DisplayRole: + if source_data == -1: + return "" + else: + return source_data + if index.column() == source_model.columns_types.index('current_hash') \ and role == Qt.DisplayRole: return source_data[:10] @@ -108,7 +121,8 @@ class NetworkTableModel(QAbstractTableModel): 'pubkey', 'software', 'version', - 'is_root' + 'is_root', + 'state' ) self.node_colors = { Node.ONLINE: QColor('#99ff99'), @@ -122,23 +136,13 @@ class NetworkTableModel(QAbstractTableModel): Node.DESYNCED: self.tr('Unsynchronized'), Node.CORRUPTED: self.tr('Corrupted') } + self.nodes_data = [] - @property - def nodes(self): - return self.community.network.nodes - - def rowCount(self, parent): - return len(self.nodes) - - def columnCount(self, parent): - return len(self.columns_types) - - def headerData(self, section, orientation, role): - if role != Qt.DisplayRole: - return QVariant() - - return self.columns_types[section] + def change_community(self, community): + self.community = community + self.refresh_nodes() + @asyncio.coroutine def data_node(self, node: Node) -> tuple: """ Return node data tuple @@ -146,7 +150,8 @@ class NetworkTableModel(QAbstractTableModel): :return: """ try: - is_member = node.pubkey in self.community.members_pubkeys() + members_pubkey = yield from self.community.members_pubkeys() + is_member = node.pubkey in members_pubkey except NoPeerAvailable as e: logging.error(e) is_member = None @@ -162,8 +167,31 @@ class NetworkTableModel(QAbstractTableModel): is_root = self.community.network.is_root_node(node) - return (address, port, node.block_number, node.block_hash, node.uid, - is_member, node.pubkey, node.software, node.version, is_root) + return (address, port, node.block['number'], node.block['hash'], node.uid, + is_member, node.pubkey, node.software, node.version, is_root, node.state) + + @asyncify + @asyncio.coroutine + def refresh_nodes(self): + self.beginResetModel() + self.nodes_data = [] + if self.community: + for node in self.community.network.nodes: + data = yield from self.data_node(node) + self.nodes_data.append(data) + self.endResetModel() + + def rowCount(self, parent): + return len(self.nodes_data) + + def columnCount(self, parent): + return len(self.columns_types) + + def headerData(self, section, orientation, role): + if role != Qt.DisplayRole: + return QVariant() + + return self.columns_types[section] def data(self, index, role): row = index.row() @@ -172,13 +200,13 @@ class NetworkTableModel(QAbstractTableModel): if not index.isValid(): return QVariant() - node = self.nodes[row] + node = self.nodes_data[row] if role == Qt.DisplayRole: - return self.data_node(node)[col] + return node[col] if role == Qt.BackgroundColorRole: - return self.node_colors[node.state] + return self.node_colors[node[self.columns_types.index('state')]] if role == Qt.ToolTipRole: - return self.node_states[node.state] + return self.node_states[node[self.columns_types.index('state')]] return QVariant() diff --git a/src/cutecoin/models/txhistory.py b/src/cutecoin/models/txhistory.py index 4576cdaf82905efdca9e996902fb27b9f441d944..bd22eccb338102552f416e9b0f4f278962fc3c2d 100644 --- a/src/cutecoin/models/txhistory.py +++ b/src/cutecoin/models/txhistory.py @@ -6,8 +6,9 @@ Created on 5 févr. 2014 import datetime import logging -from ..core import money +import asyncio from ..core.transfer import Transfer +from ..tools.decorators import asyncify, once_at_a_time, cancel_once_task from PyQt5.QtCore import QAbstractTableModel, Qt, QVariant, QSortFilterProxyModel, \ QDateTime, QLocale, QModelIndex @@ -51,14 +52,14 @@ class TxFilterProxyModel(QSortFilterProxyModel): if in_period(date): # calculate sum total payments payment = source_model.data( - source_model.index(sourceRow, source_model.columns_types.index('payment')), + source_model.index(sourceRow, source_model.columns_types.index('amount')), Qt.DisplayRole ) if payment: self.payments += int(payment) # calculate sum total deposits deposit = source_model.data( - source_model.index(sourceRow, source_model.columns_types.index('deposit')), + source_model.index(sourceRow, source_model.columns_types.index('amount')), Qt.DisplayRole ) if deposit: @@ -67,7 +68,7 @@ class TxFilterProxyModel(QSortFilterProxyModel): return in_period(date) def columnCount(self, parent): - return self.sourceModel().columnCount(None) - 4 + return self.sourceModel().columnCount(None) - 5 def setSourceModel(self, sourceModel): self.community = sourceModel.community @@ -111,9 +112,7 @@ class TxFilterProxyModel(QSortFilterProxyModel): ) if source_index.column() == model.columns_types.index('payment') or \ source_index.column() == model.columns_types.index('deposit'): - if source_data is not "": - return self.account.current_ref(source_data, self.community, self.app)\ - .diff_localized(international_system=self.app.preferences['international_system_of_units']) + return source_data if role == Qt.FontRole: font = QFont() @@ -153,7 +152,7 @@ class TxFilterProxyModel(QSortFilterProxyModel): current_validations = self.community.network.latest_block_number - block_data else: current_validations = 0 - max_validations = self.community.network.fork_window(self.community.members_pubkeys()) + 1 + max_validations = self.sourceModel().max_validations() if self.app.preferences['expert_mode']: return self.tr("{0} / {1} validations").format(current_validations, max_validations) @@ -172,16 +171,17 @@ class HistoryTableModel(QAbstractTableModel): A Qt abstract item model to display communities in a tree """ - def __init__(self, app, community, parent=None): + def __init__(self, app, account, community, parent=None): """ Constructor """ super().__init__(parent) self.app = app + self.account = account self.community = community - self.account._current_ref self.transfers_data = [] self.refresh_transfers() + self._max_validations = 0 self.columns_types = ( 'date', @@ -192,7 +192,8 @@ class HistoryTableModel(QAbstractTableModel): 'state', 'txid', 'pubkey', - 'block_number' + 'block_number', + 'amount' ) self.column_headers = ( @@ -207,16 +208,25 @@ class HistoryTableModel(QAbstractTableModel): 'Block Number' ) - @property - def account(self): - return self.app.current_account + def change_account(self, account): + cancel_once_task(self, self.refresh_transfers) + self.account = account + + def change_community(self, community): + cancel_once_task(self, self.refresh_transfers) + self.community = community - @property def transfers(self): - return self.account.transfers(self.community) + self.account.dividends(self.community) + if self.account: + return self.account.transfers(self.community) + self.account.dividends(self.community) + else: + return [] + @asyncio.coroutine def data_received(self, transfer): amount = transfer.metadata['amount'] + deposit = yield from self.account.current_ref(transfer.metadata['amount'], self.community, self.app)\ + .diff_localized(international_system=self.app.preferences['international_system_of_units']) comment = "" if transfer.metadata['comment'] != "": comment = transfer.metadata['comment'] @@ -229,12 +239,15 @@ class HistoryTableModel(QAbstractTableModel): txid = transfer.metadata['txid'] block_number = transfer.metadata['block'] - return (date_ts, sender, "", amount, + return (date_ts, sender, "", deposit, comment, transfer.state, txid, - transfer.metadata['issuer'], block_number) + transfer.metadata['issuer'], block_number, amount) + @asyncio.coroutine def data_sent(self, transfer): amount = transfer.metadata['amount'] + paiment = yield from self.account.current_ref(transfer.metadata['amount'], self.community, self.app)\ + .diff_localized(international_system=self.app.preferences['international_system_of_units']) comment = "" if transfer.metadata['comment'] != "": comment = transfer.metadata['comment'] @@ -247,12 +260,15 @@ class HistoryTableModel(QAbstractTableModel): txid = transfer.metadata['txid'] block_number = transfer.metadata['block'] - return (date_ts, receiver, amount, + return (date_ts, receiver, paiment, "", comment, transfer.state, txid, - transfer.metadata['receiver'], block_number) + transfer.metadata['receiver'], block_number, amount) + @asyncio.coroutine def data_dividend(self, dividend): amount = dividend['amount'] + deposit = yield from self.account.current_ref(dividend['amount'], self.community, self.app)\ + .diff_localized(international_system=self.app.preferences['international_system_of_units']) comment = "" receiver = self.account.name date_ts = dividend['time'] @@ -261,22 +277,34 @@ class HistoryTableModel(QAbstractTableModel): state = dividend['state'] return (date_ts, receiver, "", - amount, "", state, id, - self.account.pubkey, block_number) + deposit, "", state, id, + self.account.pubkey, block_number, amount) + @once_at_a_time + @asyncify + @asyncio.coroutine def refresh_transfers(self): self.beginResetModel() self.transfers_data = [] - for transfer in self.transfers: - if type(transfer) is Transfer: - if transfer.metadata['issuer'] == self.account.pubkey: - self.transfers_data.append(self.data_sent(transfer)) - else: - self.transfers_data.append(self.data_received(transfer)) - elif type(transfer) is dict: - self.transfers_data.append(self.data_dividend(transfer)) + if self.community: + for transfer in self.transfers(): + data = None + if type(transfer) is Transfer: + if transfer.metadata['issuer'] == self.account.pubkey: + data = yield from self.data_sent(transfer) + else: + data = yield from self.data_received(transfer) + elif type(transfer) is dict: + data = yield from self.data_dividend(transfer) + if data: + self.transfers_data.append(data) + members_pubkeys = yield from self.community.members_pubkeys() + self._max_validations = self.community.network.fork_window(members_pubkeys) + 1 self.endResetModel() + def max_validations(self): + return self._max_validations + def rowCount(self, parent): return len(self.transfers_data) @@ -284,14 +312,15 @@ class HistoryTableModel(QAbstractTableModel): return len(self.columns_types) def headerData(self, section, orientation, role): - if role == Qt.DisplayRole: - if self.columns_types[section] == 'payment' or self.columns_types[section] == 'deposit': - return '{:}\n({:})'.format( - self.column_headers[section], - self.account.current_ref.diff_units(self.community.short_currency) - ) - - return self.column_headers[section] + if self.account and self.community: + if role == Qt.DisplayRole: + if self.columns_types[section] == 'payment' or self.columns_types[section] == 'deposit': + return '{:}\n({:})'.format( + self.column_headers[section], + self.account.current_ref.diff_units(self.community.short_currency) + ) + + return self.column_headers[section] def data(self, index, role): row = index.row() diff --git a/src/cutecoin/tests/core/__init__.py b/src/cutecoin/tests/core/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..39ab2a0b56350baad834cb7fb0cfecb8223e1fcd --- /dev/null +++ b/src/cutecoin/tests/core/__init__.py @@ -0,0 +1 @@ +__author__ = 'inso' diff --git a/src/cutecoin/tests/core/txhistory/__init__.py b/src/cutecoin/tests/core/txhistory/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..39ab2a0b56350baad834cb7fb0cfecb8223e1fcd --- /dev/null +++ b/src/cutecoin/tests/core/txhistory/__init__.py @@ -0,0 +1 @@ +__author__ = 'inso' diff --git a/src/cutecoin/tests/core/txhistory/test_txhistory_loading.py b/src/cutecoin/tests/core/txhistory/test_txhistory_loading.py new file mode 100644 index 0000000000000000000000000000000000000000..26e38fa3d3cb43c40f140984a069cc15ec1d94e9 --- /dev/null +++ b/src/cutecoin/tests/core/txhistory/test_txhistory_loading.py @@ -0,0 +1,78 @@ +import sys +import unittest +import asyncio +import quamash +import time +import logging +from ucoinpy.documents.peer import BMAEndpoint as PyBMAEndpoint +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from cutecoin.tests.mocks.bma import nice_blockchain +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.core.app import Application +from cutecoin.core import Account, Community, Wallet +from cutecoin.core.net import Network, Node +from cutecoin.core.net.endpoint import BMAEndpoint +from cutecoin.core.net.api.bma.access import BmaAccess +from cutecoin.tests import get_application +from cutecoin.core.net.api import bma as qtbma + + +class TestTxHistory(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry({}) + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + + self.endpoint = BMAEndpoint(PyBMAEndpoint("", "127.0.0.1", "", 50000)) + self.node = Node(self.network_manager, "test_currency", [self.endpoint], + "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + nice_blockchain.bma_blockchain_current, Node.ONLINE, + time.time(), {}, "ucoin", "0.14.0", 0) + self.network = Network.create(self.network_manager, self.node) + self.bma_access = BmaAccess.create(self.network) + self.community = Community("test_currency", self.network, self.bma_access) + + self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "Wallet 1", self.identities_registry) + self.wallet.init_cache(self.application, self.community) + + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", [self.community], [self.wallet], [], self.identities_registry) + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + # this test fails with old algorithm + def notest_txhistory_reload(self): + mock = nice_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + received_list = [] + self.lp.run_until_complete(self.wallet.caches[self.community.currency]. + refresh(self.community, received_list)) + self.assertEquals(len(received_list), 2) + received_value = sum([r.metadata['amount'] for r in received_list]) + self.assertEqual(received_value, 60) + self.assertEqual(len(self.wallet.dividends(self.community)), 2) + dividends_value = sum([ud['amount'] for ud in self.wallet.dividends(self.community)]) + self.assertEqual(dividends_value, 15) + mock.delete_mock() + +if __name__ == '__main__': + logging.basicConfig(stream=sys.stderr) + logging.getLogger().setLevel(logging.DEBUG) + unittest.main() diff --git a/src/cutecoin/tests/gui/__init__.py b/src/cutecoin/tests/gui/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..39ab2a0b56350baad834cb7fb0cfecb8223e1fcd --- /dev/null +++ b/src/cutecoin/tests/gui/__init__.py @@ -0,0 +1 @@ +__author__ = 'inso' diff --git a/src/cutecoin/tests/main_window/__init__.py b/src/cutecoin/tests/gui/certification/__init__.py similarity index 100% rename from src/cutecoin/tests/main_window/__init__.py rename to src/cutecoin/tests/gui/certification/__init__.py diff --git a/src/cutecoin/tests/gui/certification/test_certification.py b/src/cutecoin/tests/gui/certification/test_certification.py new file mode 100644 index 0000000000000000000000000000000000000000..71c634b046050d18fdf4a71ca4bd7e91843aaa43 --- /dev/null +++ b/src/cutecoin/tests/gui/certification/test_certification.py @@ -0,0 +1,99 @@ +import sys +import unittest +import asyncio +import quamash +import time +import logging +from ucoinpy.documents.peer import BMAEndpoint as PyBMAEndpoint +from PyQt5.QtWidgets import QDialog, QDialogButtonBox +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from cutecoin.tests.mocks.bma import init_new_community +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.gui.certification import CertificationDialog +from cutecoin.gui.password_asker import PasswordAskerDialog +from cutecoin.core.app import Application +from cutecoin.core import Account, Community, Wallet +from cutecoin.core.net import Network, Node +from cutecoin.core.net.endpoint import BMAEndpoint +from cutecoin.core.net.api.bma.access import BmaAccess +from cutecoin.tests import get_application +from cutecoin.core.net.api import bma as qtbma + + +class TestCertificationDialog(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry({}) + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + + self.endpoint = BMAEndpoint(PyBMAEndpoint("", "127.0.0.1", "", 50000)) + self.node = Node(self.network_manager, "test_currency", [self.endpoint], + "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + qtbma.blockchain.Block.null_value, Node.ONLINE, + time.time(), {}, "ucoin", "0.14.0", 0) + self.network = Network.create(self.network_manager, self.node) + self.bma_access = BmaAccess.create(self.network) + self.community = Community("test_currency", self.network, self.bma_access) + + self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "Wallet 1", self.identities_registry) + + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", [self.community], [self.wallet], [], self.identities_registry) + + self.password_asker = PasswordAskerDialog(self.account) + self.password_asker.password = "testcutecoin" + self.password_asker.remember = True + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + def test_certification_init_community(self): + mock = init_new_community.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + certification_dialog = CertificationDialog(self.application, + self.account, + self.password_asker) + + @asyncio.coroutine + def open_dialog(certification_dialog): + result = yield from certification_dialog.async_exec() + self.assertEqual(result, QDialog.Accepted) + + def close_dialog(): + if certification_dialog.isVisible(): + certification_dialog.close() + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(1) + self.assertEqual(certification_dialog.button_box.button(QDialogButtonBox.Ok).text(), "&Ok") + QTest.mouseClick(certification_dialog.radio_pubkey, Qt.LeftButton) + QTest.keyClicks(certification_dialog.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + QTest.mouseClick(certification_dialog.button_box.button(QDialogButtonBox.Ok), Qt.LeftButton) + + self.lp.call_later(15, close_dialog) + asyncio.async(exec_test()) + self.lp.run_until_complete(open_dialog(certification_dialog)) + mock.delete_mock() + + +if __name__ == '__main__': + logging.basicConfig(stream=sys.stderr) + logging.getLogger().setLevel(logging.DEBUG) + unittest.main() diff --git a/src/cutecoin/tests/gui/identities_tab/__init__.py b/src/cutecoin/tests/gui/identities_tab/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/cutecoin/tests/gui/identities_tab/test_identities_table.py b/src/cutecoin/tests/gui/identities_tab/test_identities_table.py new file mode 100644 index 0000000000000000000000000000000000000000..83e54e923d69619a75c5f6dbe27785a61e386160 --- /dev/null +++ b/src/cutecoin/tests/gui/identities_tab/test_identities_table.py @@ -0,0 +1,119 @@ +import sys +import unittest +import asyncio +import quamash +import logging +import time +from ucoinpy.documents.peer import BMAEndpoint as PyBMAEndpoint +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import QLocale, Qt, QPoint +from PyQt5.QtTest import QTest +from cutecoin.core.net.api import bma as qtbma +from cutecoin.tests.mocks.bma import nice_blockchain +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.gui.identities_tab import IdentitiesTabWidget +from cutecoin.gui.password_asker import PasswordAskerDialog +from cutecoin.core.app import Application +from cutecoin.core import Account, Community, Wallet +from cutecoin.core.net import Network, Node +from cutecoin.core.net.endpoint import BMAEndpoint +from cutecoin.core.net.api.bma.access import BmaAccess +from cutecoin.tests import get_application + + +class TestIdentitiesTable(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry() + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + + self.endpoint = BMAEndpoint(PyBMAEndpoint("", "127.0.0.1", "", 50000)) + self.node = Node(self.network_manager, "test_currency", [self.endpoint], + "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + qtbma.blockchain.Block.null_value, Node.ONLINE, + time.time(), {}, "ucoin", "0.14.0", 0) + self.network = Network.create(self.network_manager, self.node) + self.bma_access = BmaAccess.create(self.network) + self.community = Community("test_currency", self.network, self.bma_access) + + self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "Wallet 1", self.identities_registry) + + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", [self.community], [self.wallet], [], self.identities_registry) + + self.password_asker = PasswordAskerDialog(self.account) + self.password_asker.password = "testcutecoin" + self.password_asker.remember = True + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + def test_search_identity_found(self): + mock = nice_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + identities_tab = IdentitiesTabWidget(self.application) + identities_tab.change_account(self.account, self.password_asker) + identities_tab.change_community(self.community) + future = asyncio.Future() + + def open_widget(): + identities_tab.show() + return future + + def close_dialog(): + if identities_tab.isVisible(): + identities_tab.close() + future.set_result(True) + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(2) + urls = [mock.get_request(i).url for i in range(0, 3)] + self.assertTrue('/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ' in urls, + msg="Not found in {0}".format(urls)) + self.assertTrue('/wot/certified-by/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ' in urls, + msg="Not found in {0}".format(urls)) + + # requests 1 to 3 are for getting certifiers-of and certified-by + # on john, + a lookup + + QTest.keyClicks(identities_tab.edit_textsearch, "doe") + QTest.mouseClick(identities_tab.button_search, Qt.LeftButton) + yield from asyncio.sleep(2) + self.assertEqual(mock.get_request(3).method, 'GET') + self.assertEqual(mock.get_request(3).url, + '/blockchain/parameters') + self.assertEqual(mock.get_request(4).method, 'GET') + self.assertEqual(mock.get_request(4).url, + '/wot/lookup/doe') + self.assertEqual(mock.get_request(5).method, 'GET') + self.assertEqual(mock.get_request(5).url, + '/wot/certifiers-of/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn') + self.assertEqual(identities_tab.table_identities.model().rowCount(), 1) + yield from asyncio.sleep(2) + self.lp.call_soon(close_dialog) + + asyncio.async(exec_test()) + self.lp.call_later(15, close_dialog) + self.lp.run_until_complete(open_widget()) + mock.delete_mock() + +if __name__ == '__main__': + logging.basicConfig( stream=sys.stderr ) + logging.getLogger().setLevel( logging.DEBUG ) + unittest.main() diff --git a/src/cutecoin/tests/gui/main_window/__init__.py b/src/cutecoin/tests/gui/main_window/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/cutecoin/tests/main_window/test_main_window_dialogs.py b/src/cutecoin/tests/gui/main_window/test_main_window_dialogs.py similarity index 100% rename from src/cutecoin/tests/main_window/test_main_window_dialogs.py rename to src/cutecoin/tests/gui/main_window/test_main_window_dialogs.py diff --git a/src/cutecoin/tests/main_window/test_main_window_menus.py b/src/cutecoin/tests/gui/main_window/test_main_window_menus.py similarity index 100% rename from src/cutecoin/tests/main_window/test_main_window_menus.py rename to src/cutecoin/tests/gui/main_window/test_main_window_menus.py diff --git a/src/cutecoin/tests/gui/process_cfg_account/__init__.py b/src/cutecoin/tests/gui/process_cfg_account/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/cutecoin/tests/gui/process_cfg_account/test_add_account.py b/src/cutecoin/tests/gui/process_cfg_account/test_add_account.py new file mode 100644 index 0000000000000000000000000000000000000000..cd6808c164b50e1668a6b507bc436946e1199220 --- /dev/null +++ b/src/cutecoin/tests/gui/process_cfg_account/test_add_account.py @@ -0,0 +1,106 @@ +import sys +import unittest +import asyncio +import quamash +import logging +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from cutecoin.tests.mocks.bma import new_blockchain +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.gui.process_cfg_account import ProcessConfigureAccount +from cutecoin.gui.password_asker import PasswordAskerDialog +from cutecoin.core.app import Application +from cutecoin.core.account import Account +from cutecoin.tests import get_application + + +class ProcessAddCommunity(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry({}) + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "test", [], [], [], self.identities_registry) + self.password_asker = PasswordAskerDialog(self.account) + self.password_asker.password = "testcutecoin" + self.password_asker.remember = True + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + def test_create_account(self): + process_account = ProcessConfigureAccount(self.application, + None) + + @asyncio.coroutine + def open_dialog(process_account): + result = yield from process_account.async_exec() + self.assertEqual(result, QDialog.Accepted) + + def close_dialog(): + if process_account.isVisible(): + process_account.close() + + @asyncio.coroutine + def exec_test(): + QTest.keyClicks(process_account.edit_account_name, "test") + self.assertEqual(process_account.stacked_pages.currentWidget(), + process_account.page_init, + msg="Current widget : {0}".format(process_account.stacked_pages.currentWidget().objectName())) + QTest.mouseClick(process_account.button_next, Qt.LeftButton) + + self.assertEqual(process_account.stacked_pages.currentWidget(), + process_account.page_gpg, + msg="Current widget : {0}".format(process_account.stacked_pages.currentWidget().objectName())) + + QTest.keyClicks(process_account.edit_salt, "testcutecoin") + self.assertFalse(process_account.button_next.isEnabled()) + self.assertFalse(process_account.button_generate.isEnabled()) + QTest.keyClicks(process_account.edit_password, "testcutecoin") + self.assertFalse(process_account.button_next.isEnabled()) + self.assertFalse(process_account.button_generate.isEnabled()) + QTest.keyClicks(process_account.edit_password_repeat, "wrongpassword") + self.assertFalse(process_account.button_next.isEnabled()) + self.assertFalse(process_account.button_generate.isEnabled()) + process_account.edit_password_repeat.setText("") + QTest.keyClicks(process_account.edit_password_repeat, "testcutecoin") + self.assertTrue(process_account.button_next.isEnabled()) + self.assertTrue(process_account.button_generate.isEnabled()) + QTest.mouseClick(process_account.button_generate, Qt.LeftButton) + self.assertEqual(process_account.label_info.text(), + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ") + QTest.mouseClick(process_account.button_next, Qt.LeftButton) + + self.assertEqual(process_account.stacked_pages.currentWidget(), + process_account.page__communities, + msg="Current widget : {0}".format(process_account.stacked_pages.currentWidget().objectName())) + process_account.password_asker.password = "testcutecoin" + process_account.password_asker.remember = True + yield from asyncio.sleep(1) + QTest.mouseClick(process_account.button_next, Qt.LeftButton) + self.assertEqual(len(self.application.accounts), 1) + self.assertEqual(self.application.current_account.name, "test") + self.assertEqual(self.application.preferences['account'], "test") + self.assertEqual(len(self.application.current_account.wallets), 1) + + self.lp.call_later(10, close_dialog) + asyncio.async(exec_test()) + self.lp.run_until_complete(open_dialog(process_account)) + +if __name__ == '__main__': + logging.basicConfig( stream=sys.stderr ) + logging.getLogger().setLevel( logging.DEBUG ) + unittest.main() diff --git a/src/cutecoin/tests/gui/process_cfg_community/__init__.py b/src/cutecoin/tests/gui/process_cfg_community/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/cutecoin/tests/gui/process_cfg_community/test_add_community.py b/src/cutecoin/tests/gui/process_cfg_community/test_add_community.py new file mode 100644 index 0000000000000000000000000000000000000000..dfb224a1995140fbaaedcae1dcd627429fcc674e --- /dev/null +++ b/src/cutecoin/tests/gui/process_cfg_community/test_add_community.py @@ -0,0 +1,204 @@ +import sys +import unittest +import asyncio +import quamash +import logging +import time +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from cutecoin.tests.mocks.bma import new_blockchain, nice_blockchain +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.gui.process_cfg_community import ProcessConfigureCommunity +from cutecoin.gui.password_asker import PasswordAskerDialog +from cutecoin.core.app import Application +from cutecoin.core.account import Account +from cutecoin.tests import get_application + + +class ProcessAddCommunity(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry({}) + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "test", [], [], [], self.identities_registry) + self.password_asker = PasswordAskerDialog(self.account) + self.password_asker.password = "testcutecoin" + self.password_asker.remember = True + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + def test_register_community_empty_blockchain(self): + mock = new_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + process_community = ProcessConfigureCommunity(self.application, + self.account, + None, self.password_asker) + + @asyncio.coroutine + def open_dialog(process_community): + result = yield from process_community.async_exec() + self.assertEqual(result, QDialog.Accepted) + + def close_dialog(): + if process_community.isVisible(): + process_community.close() + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(1) + QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) + QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") + QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) + process_community.spinbox_port.setValue(50000) + self.assertEqual(process_community.stacked_pages.currentWidget(), + process_community.page_node, + msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) + self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") + self.assertEqual(process_community.spinbox_port.value(), 50000) + QTest.mouseClick(process_community.button_register, Qt.LeftButton) + yield from asyncio.sleep(1) + self.assertEqual(mock.get_request(0).method, 'GET') + self.assertEqual(mock.get_request(0).url, '/network/peering') + self.assertEqual(mock.get_request(1).method, 'GET') + self.assertEqual(mock.get_request(1).url, + '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') + for i in range(2, 5): + self.assertEqual(mock.get_request(i).method, 'GET') + self.assertEqual(mock.get_request(i).url, + '/wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') + + self.assertEqual(mock.get_request(6).method, 'POST') + self.assertEqual(mock.get_request(6).url[:8], '/wot/add') + self.assertEqual(process_community.label_error.text(), "Broadcasting identity...") + yield from asyncio.sleep(1) + + self.assertEqual(process_community.stacked_pages.currentWidget(), + process_community.page_add_nodes, + msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) + QTest.mouseClick(process_community.button_next, Qt.LeftButton) + + self.lp.call_later(15, close_dialog) + asyncio.async(exec_test()) + self.lp.run_until_complete(open_dialog(process_community)) + mock.delete_mock() + + def test_connect_community_empty_blockchain(self): + mock = new_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + process_community = ProcessConfigureCommunity(self.application, + self.account, + None, self.password_asker) + + @asyncio.coroutine + def open_dialog(process_community): + result = yield from process_community.async_exec() + self.assertEqual(result, QDialog.Rejected) + + def close_dialog(): + if process_community.isVisible(): + process_community.close() + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(1) + QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) + QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") + QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) + process_community.spinbox_port.setValue(50000) + self.assertEqual(process_community.stacked_pages.currentWidget(), + process_community.page_node, + msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) + self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") + self.assertEqual(process_community.spinbox_port.value(), 50000) + QTest.mouseClick(process_community.button_connect, Qt.LeftButton) + yield from asyncio.sleep(1) + self.assertEqual(mock.get_request(0).method, 'GET') + self.assertEqual(mock.get_request(0).url, '/network/peering') + self.assertEqual(mock.get_request(1).method, 'GET') + self.assertEqual(mock.get_request(1).url, + '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') + for i in range(2, 5): + self.assertEqual(mock.get_request(i).method, 'GET') + self.assertEqual(mock.get_request(i).url, + '/wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') + self.assertEqual(process_community.stacked_pages.currentWidget(), + process_community.page_node, + msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) + self.assertEqual(process_community.label_error.text(), "Could not find your identity on the network.") + process_community.close() + + self.lp.call_later(15, close_dialog) + asyncio.async(exec_test()) + self.lp.run_until_complete(open_dialog(process_community)) + mock.delete_mock() + + def test_connect_community_nice_blockchain(self): + mock = nice_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + process_community = ProcessConfigureCommunity(self.application, + self.account, + None, self.password_asker) + + @asyncio.coroutine + def open_dialog(process_community): + result = yield from process_community.async_exec() + self.assertEqual(result, QDialog.Accepted) + + def close_dialog(): + if process_community.isVisible(): + process_community.close() + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(1) + QTest.mouseClick(process_community.lineedit_server, Qt.LeftButton) + QTest.keyClicks(process_community.lineedit_server, "127.0.0.1") + QTest.mouseDClick(process_community.spinbox_port, Qt.LeftButton) + process_community.spinbox_port.setValue(50000) + self.assertEqual(process_community.stacked_pages.currentWidget(), + process_community.page_node, + msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) + self.assertEqual(process_community.lineedit_server.text(), "127.0.0.1") + self.assertEqual(process_community.spinbox_port.value(), 50000) + QTest.mouseClick(process_community.button_connect, Qt.LeftButton) + yield from asyncio.sleep(1) + self.assertEqual(mock.get_request(0).method, 'GET') + self.assertEqual(mock.get_request(0).url, '/network/peering') + self.assertEqual(mock.get_request(1).method, 'GET') + self.assertEqual(mock.get_request(1).url, + '/wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ') + self.assertEqual(process_community.stacked_pages.currentWidget(), + process_community.page_add_nodes, + msg="Current widget : {0}".format(process_community.stacked_pages.currentWidget().objectName())) + QTest.mouseClick(process_community.button_next, Qt.LeftButton) + + self.lp.call_later(15, close_dialog) + asyncio.async(exec_test()) + self.lp.run_until_complete(open_dialog(process_community)) + mock.delete_mock() + +if __name__ == '__main__': + logging.basicConfig( stream=sys.stderr ) + logging.getLogger().setLevel( logging.DEBUG ) + unittest.main() diff --git a/src/cutecoin/tests/gui/transfer/__init__.py b/src/cutecoin/tests/gui/transfer/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/src/cutecoin/tests/gui/transfer/test_transfer.py b/src/cutecoin/tests/gui/transfer/test_transfer.py new file mode 100644 index 0000000000000000000000000000000000000000..55452ccbbfe274193e90aa4d4b533674edb2a44e --- /dev/null +++ b/src/cutecoin/tests/gui/transfer/test_transfer.py @@ -0,0 +1,98 @@ +import sys +import unittest +import asyncio +import quamash +import time +import logging +from ucoinpy.documents.peer import BMAEndpoint as PyBMAEndpoint +from PyQt5.QtWidgets import QDialog, QDialogButtonBox +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from cutecoin.tests.mocks.bma import nice_blockchain +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.gui.transfer import TransferMoneyDialog +from cutecoin.gui.password_asker import PasswordAskerDialog +from cutecoin.core.app import Application +from cutecoin.core import Account, Community, Wallet +from cutecoin.core.net import Network, Node +from cutecoin.core.net.endpoint import BMAEndpoint +from cutecoin.core.net.api.bma.access import BmaAccess +from cutecoin.tests import get_application +from cutecoin.core.net.api import bma as qtbma + + +class TestTransferDialog(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry({}) + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + + self.endpoint = BMAEndpoint(PyBMAEndpoint("", "127.0.0.1", "", 50000)) + self.node = Node(self.network_manager, "test_currency", [self.endpoint], + "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + qtbma.blockchain.Block.null_value, Node.ONLINE, + time.time(), {}, "ucoin", "0.14.0", 0) + self.network = Network.create(self.network_manager, self.node) + self.bma_access = BmaAccess.create(self.network) + self.community = Community("test_currency", self.network, self.bma_access) + + self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "Wallet 1", self.identities_registry) + + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", [self.community], [self.wallet], [], self.identities_registry) + + self.password_asker = PasswordAskerDialog(self.account) + self.password_asker.password = "testcutecoin" + self.password_asker.remember = True + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + def test_transfer_nice_community(self): + mock = nice_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + transfer_dialog = TransferMoneyDialog(self.application, + self.account, + self.password_asker) + + @asyncio.coroutine + def open_dialog(certification_dialog): + result = yield from certification_dialog.async_exec() + self.assertEqual(result, QDialog.Rejected) + + def close_dialog(): + if transfer_dialog.isVisible(): + transfer_dialog.close() + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(1) + QTest.mouseClick(transfer_dialog.radio_pubkey, Qt.LeftButton) + QTest.keyClicks(transfer_dialog.edit_pubkey, "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn") + QTest.mouseClick(transfer_dialog.button_box.button(QDialogButtonBox.Cancel), Qt.LeftButton) + + self.lp.call_later(15, close_dialog) + asyncio.async(exec_test()) + self.lp.run_until_complete(open_dialog(transfer_dialog)) + mock.delete_mock() + + +if __name__ == '__main__': + logging.basicConfig(stream=sys.stderr) + logging.getLogger().setLevel(logging.DEBUG) + unittest.main() diff --git a/src/cutecoin/tests/gui/wot_tab/__init__.py b/src/cutecoin/tests/gui/wot_tab/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..39ab2a0b56350baad834cb7fb0cfecb8223e1fcd --- /dev/null +++ b/src/cutecoin/tests/gui/wot_tab/__init__.py @@ -0,0 +1 @@ +__author__ = 'inso' diff --git a/src/cutecoin/tests/gui/wot_tab/test_wot_tab.py b/src/cutecoin/tests/gui/wot_tab/test_wot_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..ce6584278c161b0e5e7e208d5a020142682961ee --- /dev/null +++ b/src/cutecoin/tests/gui/wot_tab/test_wot_tab.py @@ -0,0 +1,99 @@ +import sys +import unittest +import asyncio +import quamash +import logging +import time +from ucoinpy.documents.peer import BMAEndpoint as PyBMAEndpoint +from PyQt5.QtWidgets import QDialog +from PyQt5.QtCore import QLocale, Qt +from PyQt5.QtTest import QTest +from cutecoin.core.net.api import bma as qtbma +from cutecoin.tests.mocks.bma import nice_blockchain +from cutecoin.tests.mocks.access_manager import MockNetworkAccessManager +from cutecoin.core.registry.identities import IdentitiesRegistry +from cutecoin.gui.wot_tab import WotTabWidget +from cutecoin.gui.password_asker import PasswordAskerDialog +from cutecoin.core.app import Application +from cutecoin.core import Account, Community, Wallet +from cutecoin.core.net import Network, Node +from cutecoin.core.net.endpoint import BMAEndpoint +from cutecoin.core.net.api.bma.access import BmaAccess +from cutecoin.tests import get_application + + +class TestIdentitiesTable(unittest.TestCase): + def setUp(self): + self.qapplication = get_application() + self.network_manager = MockNetworkAccessManager() + QLocale.setDefault(QLocale("en_GB")) + self.lp = quamash.QEventLoop(self.qapplication) + asyncio.set_event_loop(self.lp) + self.identities_registry = IdentitiesRegistry() + + self.application = Application(self.qapplication, self.lp, self.network_manager, self.identities_registry) + self.application.preferences['notifications'] = False + + self.endpoint = BMAEndpoint(PyBMAEndpoint("", "127.0.0.1", "", 50000)) + self.node = Node(self.network_manager, "test_currency", [self.endpoint], + "", "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + qtbma.blockchain.Block.null_value, Node.ONLINE, + time.time(), {}, "ucoin", "0.14.0", 0) + self.network = Network.create(self.network_manager, self.node) + self.bma_access = BmaAccess.create(self.network) + self.community = Community("test_currency", self.network, self.bma_access) + + self.wallet = Wallet(0, "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "Wallet 1", self.identities_registry) + + # Salt/password : "testcutecoin/testcutecoin" + # Pubkey : 7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ + self.account = Account("testcutecoin", "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "john", [self.community], [self.wallet], [], self.identities_registry) + + self.password_asker = PasswordAskerDialog(self.account) + self.password_asker.password = "testcutecoin" + self.password_asker.remember = True + + def tearDown(self): + try: + self.lp.close() + finally: + asyncio.set_event_loop(None) + + def test_empty_wot_tab(self): + mock = nice_blockchain.get_mock() + time.sleep(2) + logging.debug(mock.pretend_url) + self.network_manager.set_mock_path(mock.pretend_url) + wot_tab = WotTabWidget(self.application) + future = asyncio.Future() + + def open_widget(): + wot_tab.show() + return future + + @asyncio.coroutine + def async_open_widget(): + yield from open_widget() + + def close_dialog(): + if wot_tab.isVisible(): + wot_tab.close() + future.set_result(True) + + @asyncio.coroutine + def exec_test(): + yield from asyncio.sleep(1) + self.assertTrue(wot_tab.isVisible()) + self.lp.call_soon(close_dialog) + + asyncio.async(exec_test()) + self.lp.call_later(15, close_dialog) + self.lp.run_until_complete(async_open_widget()) + mock.delete_mock() + +if __name__ == '__main__': + logging.basicConfig( stream=sys.stderr ) + logging.getLogger().setLevel( logging.DEBUG ) + unittest.main() diff --git a/src/cutecoin/tests/mocks/__init__.py b/src/cutecoin/tests/mocks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7e4cdeb7cb59ef2876f24361d303fcc281ec4ef8 --- /dev/null +++ b/src/cutecoin/tests/mocks/__init__.py @@ -0,0 +1 @@ +__author__ = 'ggoinvic' diff --git a/src/cutecoin/tests/mocks/access_manager.py b/src/cutecoin/tests/mocks/access_manager.py new file mode 100644 index 0000000000000000000000000000000000000000..754909841f6dd3e102e5a06d75d0beead89310c3 --- /dev/null +++ b/src/cutecoin/tests/mocks/access_manager.py @@ -0,0 +1,27 @@ +from PyQt5.QtNetwork import QNetworkAccessManager +from PyQt5.QtCore import QUrl + +class MockNetworkAccessManager(QNetworkAccessManager): + def __init__(self): + super().__init__() + self.mock_path = "" + + def set_mock_path(self, mock_url): + url = QUrl(mock_url) + self.mock_path = url.path() + + def get(self, request): + url = request.url() + path = url.path() + path = self.mock_path + path + url.setPath(path) + request.setUrl(url) + return super().get(request) + + def post(self, request, post_data): + url = request.url() + path = url.path() + path = self.mock_path + path + url.setPath(path) + request.setUrl(url) + return super().post(request, post_data) \ No newline at end of file diff --git a/src/cutecoin/tests/mocks/bma/__init__.py b/src/cutecoin/tests/mocks/bma/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..7e4cdeb7cb59ef2876f24361d303fcc281ec4ef8 --- /dev/null +++ b/src/cutecoin/tests/mocks/bma/__init__.py @@ -0,0 +1 @@ +__author__ = 'ggoinvic' diff --git a/src/cutecoin/tests/mocks/bma/init_new_community.py b/src/cutecoin/tests/mocks/bma/init_new_community.py new file mode 100644 index 0000000000000000000000000000000000000000..cb4700e37e42d8ef03668902e64cf690dd8ae5a6 --- /dev/null +++ b/src/cutecoin/tests/mocks/bma/init_new_community.py @@ -0,0 +1,146 @@ +from pretenders.client.http import HTTPMock +from pretenders.common.constants import FOREVER + +bma_peering = b"""{ + "version": 1, + "currency": "test_currency", + "endpoints": [ + "BASIC_MERKLED_API localhost 127.0.0.1 50000" + ], + "status": "UP", + "block": "30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3", + "signature": "cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA==", + "raw": "Version: 1\\nType: Peer\\nCurrency: meta_brouzouf\\nPublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\\nBlock: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3\\nEndpoints:\\nBASIC_MERKLED_API localhost 127.0.0.1 50000\\n", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" +}""" + +bma_lookup_test_john = b"""{ + "partial": false, + "results": [ + { + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "uids": [ + { + "uid": "john", + "meta": { + "timestamp": 1441130831 + }, + "self": "ZrHK0cCqrxWReROK0ciiSb45+dRphJa68qFaSjdve8bBdnGAu7+DIu0d+u/fXrNRXuObihOKMBIawaIVPNHqDw==", + "others": [] + } + ], + "signed": [] + } + ] +}""" + +bma_lookup_test_doe = b"""{ + "partial": false, + "results": [ + { + "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "uids": [ + { + "uid": "doe", + "meta": { + "timestamp": 1441130831 + }, + "self": "cIkHPQQ5+xTb4cKWv85rcYcZT+E3GDtX8B2nCK9Vs12p2Yz4bVaZiMvBBwisAAy2WBOaqHS3ydpXGtADchOICw==", + "others": [] + } + ], + "signed": [] + } + ] +}""" + +bma_lookup_test_patrick = b"""{ + "partial": false, + "results": [ + { + "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "uids": [ + { + "uid": "patrick", + "meta": { + "timestamp": 1441130831 + }, + "self": "QNX2HDAxcHawc47TnMqb5/ou2lwa+zYOyeNk0a52dQDJX/NWmeTzGfTjdCtjpXmSCuPSg0F1mOnLQVd60xAzDA==", + "others": [] + } + ], + "signed": [] + } + ] +}""" + + +def get_mock(): + mock = HTTPMock('127.0.0.1', 50000) + + mock.when('GET /network/peering')\ + .reply(body=bma_peering, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/block/0')\ + .reply(body=b"Block not found", + status=404, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/current')\ + .reply(body=b"Block not found", + status=404, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=b"No member matching this pubkey or uid", + status=404, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/john')\ + .reply(body=bma_lookup_test_john, + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=bma_lookup_test_john, + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/doe')\ + .reply(body=bma_lookup_test_doe, + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn')\ + .reply(body=bma_lookup_test_doe, + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/patrick')\ + .reply(body=bma_lookup_test_patrick, + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn')\ + .reply(body=bma_lookup_test_patrick, + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('POST /wot/add.*')\ + .reply(body=b"{}", + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + return mock diff --git a/src/cutecoin/tests/mocks/bma/new_blockchain.py b/src/cutecoin/tests/mocks/bma/new_blockchain.py new file mode 100644 index 0000000000000000000000000000000000000000..fe43d5d23588847d02cb084c47eb42aa6c9f71de --- /dev/null +++ b/src/cutecoin/tests/mocks/bma/new_blockchain.py @@ -0,0 +1,69 @@ +from pretenders.client.http import HTTPMock +from pretenders.common.constants import FOREVER + +bma_peering = b"""{ + "version": 1, + "currency": "test_currency", + "endpoints": [ + "BASIC_MERKLED_API localhost 127.0.0.1 50000" + ], + "status": "UP", + "block": "30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3", + "signature": "cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA==", + "raw": "Version: 1\\nType: Peer\\nCurrency: meta_brouzouf\\nPublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\\nBlock: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3\\nEndpoints:\\nBASIC_MERKLED_API localhost 127.0.0.1 50000\\n", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" +}""" + +bma_wot_add = b"""{ + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "uids": [ + { + "uid": "test", + "meta": { + "timestamp": 1409990782 + }, + "self": "J3G9oM5AKYZNLAB5Wx499w61NuUoS57JVccTShUbGpCMjCqj9yXXqNq7dyZpDWA6BxipsiaMZhujMeBfCznzyci", + "others": [ + ] + } + ] +}""" + +def get_mock(): + mock = HTTPMock('127.0.0.1', 50000) + + mock.when('GET /network/peering')\ + .reply(body=bma_peering, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/block/0')\ + .reply(body=b"Block not found", + status=404, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/current')\ + .reply(body=b"Block not found", + status=404, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=b"No member matching this pubkey or uid", + status=404, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=b"No member matching this pubkey or uid", + status=404, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('POST /wot/add.*')\ + .reply(body=bma_wot_add, + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + return mock diff --git a/src/cutecoin/tests/mocks/bma/nice_blockchain.py b/src/cutecoin/tests/mocks/bma/nice_blockchain.py new file mode 100644 index 0000000000000000000000000000000000000000..76a77edfcc009182df832f6fb04bfc32685b132e --- /dev/null +++ b/src/cutecoin/tests/mocks/bma/nice_blockchain.py @@ -0,0 +1,348 @@ +import json +from pretenders.client.http import HTTPMock +from pretenders.common.constants import FOREVER + +bma_peering = { + "version": 1, + "currency": "test_currency", + "endpoints": [ + "BASIC_MERKLED_API localhost 127.0.0.1 50000" + ], + "status": "UP", + "block": "30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3", + "signature": "cXuqZuDfyHvxYAEUkPH1TQ1M+8YNDpj8kiHGYi3LIaMqEdVqwVc4yQYGivjxFMYyngRfxXkyvqBKZA6rKOulCA==", + "raw": "Version: 1\nType: Peer\nCurrency: meta_brouzouf\nPublicKey: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nBlock: 30152-00003E7F9234E7542FCF669B69B0F84FF79CCCD3\nEndpoints:\nBASIC_MERKLED_API localhost 127.0.0.1 50000\n", + "pubkey": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk" +} + +bma_lookup_john = { + "partial": False, + "results": [ + { + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "uids": [ + { + "uid": "john", + "meta": { + "timestamp": 1441130831 + }, + "self": "ZrHK0cCqrxWReROK0ciiSb45+dRphJa68qFaSjdve8bBdnGAu7+DIu0d+u/fXrNRXuObihOKMBIawaIVPNHqDw==", + "others": [] + } + ], + "signed": [] + } + ] +} + +bma_lookup_doe = { + "partial": False, + "results": [ + { + "pubkey": "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn", + "uids": [ + { + "uid": "doe", + "meta": { + "timestamp": 1441130831 + }, + "self": "cIkHPQQ5+xTb4cKWv85rcYcZT+E3GDtX8B2nCK9Vs12p2Yz4bVaZiMvBBwisAAy2WBOaqHS3ydpXGtADchOICw==", + "others": [] + } + ], + "signed": [] + } + ] +} + +bma_certifiers_of_john = { + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "uid": "john", + "isMember": True, + "certifications": [ + ] +} + +bma_certified_by_john = { + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "uid": "john", + "isMember": True, + "certifications": [ + ] +} + +bma_parameters = { + "currency": "test_currency", + "c": 0.1, + "dt": 86400, + "ud0": 100, + "sigDelay": 604800, + "sigValidity": 2629800, + "sigQty": 3, + "sigWoT": 3, + "msValidity": 2629800, + "stepMax": 3, + "medianTimeBlocks": 11, + "avgGenTime": 600, + "dtDiffEval": 20, + "blocksRot": 144, + "percentRot": 0.67 +} + +bma_blockchain_current = { + "version": 1, + "nonce": 6909, + "number": 15, + "powMin": 4, + "time": 1441618206, + "medianTime": 1441614759, + "membersCount": 20, + "monetaryMass": 11711349901120, + "currency": "test_currency", + "issuer": "EPs9qX7HmCDy6ptUoMLpTzbh9toHu4au488pBTU9DN6y", + "signature": "kz/34w1cG+8tYacuPXf3FPmsFwrvtWkwp1POLJuX1P0zYaB9Tuu7iyYJzMQS0Xa3vwuWRqfz+fgyoCGnBjBLBQ==", + "hash": "0000CB4E9CCDE6F579135331C97F13903E8B6E21", + "parameters": "", + "previousHash": "00003BDA844D77EEE7CF32A6C3C87F2ACBFCFCBB", + "previousIssuer": "HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk", + "dividend": None, + "membersChanges": [ ], + "identities": [ ], + "joiners": [ ], + "actives": [ ], + "leavers": [ ], + "excluded": [ ], + "certifications": [ ], + "transactions": [ ], + "raw": "Version: 1\nType: Block\nCurrency: meta_brouzouf\nNonce: 6909\nNumber: 30898\nPoWMin: 4\nTime: 1441618206\nMedianTime: 1441614759\nIssuer: EPs9qX7HmCDy6ptUoMLpTzbh9toHu4au488pBTU9DN6y\nPreviousHash: 00003BDA844D77EEE7CF32A6C3C87F2ACBFCFCBB\nPreviousIssuer: HnFcSms8jzwngtVomTTnzudZx7SHUQY8sVE1y8yBmULk\nMembersCount: 20\nIdentities:\nJoiners:\nActives:\nLeavers:\nExcluded:\nCertifications:\nTransactions:\n" +} + +# Sent 6, received 20 + 30 +bma_txhistory_john = { + "currency": "test_currency", + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "history": + { + "sent": + [ + { + "version": 1, + "issuers": + [ + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ" + ], + "inputs": + [ + "0:D:1:000A8362AE0C1B8045569CE07735DE4C18E81586:8" + ], + "outputs": + [ + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ:2", + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn:6" + ], + "comment": "", + "signatures": + [ + "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" + ], + "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", + "block_number": 2, + "time": 1421932545 + }, +], +"received": + [ + { + "version": 1, + "issuers": + [ + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + ], + "inputs": + [ + "0:D:1:000A8362AE0C1B8045569CE07735DE4C18E81586:8" + ], + "outputs": + [ + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn:2", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ:20" + ], + "comment": "", + "signatures": + [ + "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" + ], + "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", + "block_number": 2, + "time": 1421932545 + }, + { + "version": 1, + "issuers": + [ + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn" + ], + "inputs": + [ + "0:D:1:000A8362AE0C1B8045569CE07735DE4C18E81586:8" + ], + "outputs": + [ + "FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn:5", + "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ:40" + ], + "comment": "", + "signatures": + [ + "1Mn8q3K7N+R4GZEpAUm+XSyty1Uu+BuOy5t7BIRqgZcKqiaxfhAUfDBOcuk2i4TJy1oA5Rntby8hDN+cUCpvDg==" + ], + "hash": "5FB3CB80A982E2BDFBB3EA94673A74763F58CB2A", + "block_number": 12, + "time": 1421932454 + } + ], + "sending": [ ], + "receiving": [ ] + } +} + +bma_udhistory_john = { + "currency": "test_currency", + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "history": +{ + "history": + [ + { + "block_number": 2, + "consumed": False, + "time": 1435749971, + "amount": 5 + }, + { + + "block_number": 10, + "consumed": False, + "time": 1435836032, + "amount": 10 + + } + ] +}} + +bma_txsources_john = { + "currency": "test_currency", + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "sources": +[ +{ + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "type": "D", + "number": 2, + "fingerprint": "4A317E3D676E9800E1E92AA2A7255BCEEFF31185", + "amount": 7 +}, + { + "pubkey": "7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ", + "type": "D", + "number": 4, + "fingerprint": "4A317E3D676E9800E1E92AA2A7255BCEEFF31185", + "amount": 9 +} +]} + + +def get_mock(): + mock = HTTPMock('127.0.0.1', 50000) + + mock.when('GET /network/peering')\ + .reply(body=bytes(json.dumps(bma_peering), "utf-8"), + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/parameters')\ + .reply(body=bytes(json.dumps(bma_parameters), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/current')\ + .reply(body=bytes(json.dumps(bma_blockchain_current), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/block/15')\ + .reply(body=bytes(json.dumps(bma_blockchain_current), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /tx/history/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ/blocks/0/99')\ + .reply(body=bytes(json.dumps(bma_txhistory_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /tx/sources/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=bytes(json.dumps(bma_txsources_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + + mock.when('GET /ud/history/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=bytes(json.dumps(bma_udhistory_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/certifiers-of/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=bytes(json.dumps(bma_certifiers_of_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/certified-by/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=bytes(json.dumps(bma_certified_by_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/john')\ + .reply(body=bytes(json.dumps(bma_lookup_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/7Aqw6Efa9EzE7gtsc8SveLLrM7gm6NEGoywSv4FJx6pZ')\ + .reply(body=bytes(json.dumps(bma_lookup_john), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/doe')\ + .reply(body=bytes(json.dumps(bma_lookup_doe), "utf-8"), + status=200, + times=1, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/lookup/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn')\ + .reply(body=bytes(json.dumps(bma_lookup_doe), "utf-8"), + status=200, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /wot/certifiers-of/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn')\ + .reply(body=b"No member matching this pubkey or uid", + status=404, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + mock.when('GET /blockchain/memberships/FADxcH5LmXGmGFgdixSes6nWnC4Vb4pRUBYT81zQRhjn')\ + .reply(body=b"No member matching this pubkey or uid", + status=404, + times=FOREVER, + headers={'Content-Type': 'application/json'}) + + return mock diff --git a/src/cutecoin/tests/qapp.py b/src/cutecoin/tests/qapp.py index d3e1a8f657a8d8a72ca386ba3b2e8e7313d44fbb..75c314f569a977251e69d5c815009111d705d685 100644 --- a/src/cutecoin/tests/qapp.py +++ b/src/cutecoin/tests/qapp.py @@ -1,9 +1,10 @@ _application_ = [] + def get_application(): """Get the singleton QApplication""" - from PyQt5.QtWidgets import QApplication + from quamash import QApplication if not len(_application_): application = QApplication.instance() if not application: diff --git a/src/cutecoin/tools/decorators.py b/src/cutecoin/tools/decorators.py new file mode 100644 index 0000000000000000000000000000000000000000..3842dbd419aa04b4275fe5f303ac48a5e2d8b443 --- /dev/null +++ b/src/cutecoin/tools/decorators.py @@ -0,0 +1,34 @@ +import asyncio +import functools +import logging + + +def cancel_once_task(object, fn): + if getattr(object, "__tasks", None): + tasks = getattr(object, "__tasks") + if fn.__name__ in tasks and not tasks[fn.__name__].done(): + getattr(object, "__tasks")[fn.__name__].cancel() + + +def once_at_a_time(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + if getattr(args[0], "__tasks", None) is None: + setattr(args[0], "__tasks", {}) + if fn.__name__ in args[0].__tasks: + if not args[0].__tasks[fn.__name__].done(): + args[0].__tasks[fn.__name__].cancel() + try: + args[0].__tasks[fn.__name__] = fn(*args, **kwargs) + except asyncio.CancelledError: + logging.debug("Cancelled asyncified : {0}".format(fn.__name__)) + return args[0].__tasks[fn.__name__] + return wrapper + + +def asyncify(fn): + @functools.wraps(fn) + def wrapper(*args, **kwargs): + return asyncio.async(asyncio.coroutine(fn)(*args, **kwargs)) + + return wrapper