Compare commits
859 Commits
check_wall
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d39cce124 | ||
|
|
5975c105d6 | ||
|
|
9ab1aab6f8 | ||
|
|
0de954546a | ||
|
|
92bf409bf0 | ||
|
|
25a460f855 | ||
|
|
52520490c5 | ||
|
|
5313591c28 | ||
|
|
7b8114f865 | ||
|
|
f60d1e7cb6 | ||
|
|
c725c05947 | ||
|
|
87c596fa1d | ||
|
|
0e78e15fa4 | ||
|
|
4e9dd679ab | ||
|
|
2867c2ef7a | ||
|
|
ec86850a2e | ||
|
|
e2eb051eed | ||
|
|
c8562f5362 | ||
|
|
086372f68a | ||
|
|
019884a98b | ||
|
|
2174fc0676 | ||
|
|
38ab7ee554 | ||
|
|
fd62ba874b | ||
|
|
026448837f | ||
|
|
8072ad1ad9 | ||
|
|
ebeed4736f | ||
|
|
c23b869d3c | ||
|
|
5aafcb2875 | ||
|
|
cd097d6bb8 | ||
|
|
eb96d422f7 | ||
|
|
d78537c8c4 | ||
|
|
df9087970b | ||
|
|
699562c78d | ||
|
|
01eaf0fe4e | ||
|
|
b06b8753e6 | ||
|
|
1da1f0bfea | ||
|
|
8f4967f7d0 | ||
|
|
beb9f63274 | ||
|
|
58c2c15266 | ||
|
|
fc72e661de | ||
|
|
89bb49e117 | ||
|
|
2c71b9da0c | ||
|
|
9beabc0311 | ||
|
|
ba08b2279d | ||
|
|
6fb974227b | ||
|
|
6ade5903dc | ||
|
|
7bb3e5336a | ||
|
|
4ed8787433 | ||
|
|
8f2a730b3b | ||
|
|
d6986347e6 | ||
|
|
a5ddb42bfd | ||
|
|
2de7fd5466 | ||
|
|
001b815c18 | ||
|
|
5a1778b7fe | ||
|
|
67d080b34a | ||
|
|
6926b8b2d4 | ||
|
|
68cd37282e | ||
|
|
9e58d56e6d | ||
|
|
8412b53ed5 | ||
|
|
9013f6d59e | ||
|
|
bc2a421d87 | ||
|
|
76ff2f53c5 | ||
|
|
66de511828 | ||
|
|
a754f9fe10 | ||
|
|
47b07f19b9 | ||
|
|
ca931f476f | ||
|
|
43487910c7 | ||
|
|
905e271db9 | ||
|
|
52d602b6c1 | ||
|
|
add3b36f32 | ||
|
|
819c3b81e3 | ||
|
|
4fa87d8595 | ||
|
|
7ea01e9e91 | ||
|
|
7266ecc2b8 | ||
|
|
f846d1d59a | ||
|
|
f05aabd802 | ||
|
|
16bac5fd73 | ||
|
|
185d02d9df | ||
|
|
3ad6f738bd | ||
|
|
8716bc8cfb | ||
|
|
7f3de8241c | ||
|
|
e7d3fd32fb | ||
|
|
3ca1b710d6 | ||
|
|
d4967faf28 | ||
|
|
8310195453 | ||
|
|
cdaf874228 | ||
|
|
1a111b98d4 | ||
|
|
c399693049 | ||
|
|
9bbea9bf2f | ||
|
|
0cd2f6e170 | ||
|
|
55e6830cfc | ||
|
|
e89ec0d1d5 | ||
|
|
0f0cee422e | ||
|
|
27299092df | ||
|
|
0429fe5960 | ||
|
|
eb418f1d3e | ||
|
|
2737744bfe | ||
|
|
e28b8806ac | ||
|
|
4c6379a936 | ||
|
|
dcd3d6c3d1 | ||
|
|
15bc097f55 | ||
|
|
501e725a47 | ||
|
|
d820f9ad37 | ||
|
|
f9b0c66843 | ||
|
|
31c08db909 | ||
|
|
501371c001 | ||
|
|
138c98d7d8 | ||
|
|
53310690a5 | ||
|
|
94b7eb3015 | ||
|
|
1e1caf602d | ||
|
|
b085d7cc59 | ||
|
|
0f0bdfe7f0 | ||
|
|
c4774d7a8e | ||
|
|
0bfda7c8c7 | ||
|
|
34c99c3b36 | ||
|
|
5613f9b903 | ||
|
|
4b560250a6 | ||
|
|
4be4444d6b | ||
|
|
ed1421da8e | ||
|
|
8f6a1880e9 | ||
|
|
889d133abd | ||
|
|
5f05469852 | ||
|
|
54eb89ccaf | ||
|
|
2cda8b266f | ||
|
|
b3d8a81e15 | ||
|
|
920d4c2b27 | ||
|
|
f994cd4a5d | ||
|
|
f2ad116b0b | ||
|
|
5fc715cdee | ||
|
|
9cff42328d | ||
|
|
e39e2ed8f1 | ||
|
|
c995d5364e | ||
|
|
5403ae7687 | ||
|
|
7ffd928e80 | ||
|
|
d77e4d8f5d | ||
|
|
44a2ceab3c | ||
|
|
4e10f9e301 | ||
|
|
c7f3adb67e | ||
|
|
dc19cf1fa1 | ||
|
|
d5c8a0e0d0 | ||
|
|
5ec330680e | ||
|
|
f66ab7ce57 | ||
|
|
0caf8e30cd | ||
|
|
019566b383 | ||
|
|
c60583293a | ||
|
|
89cc1efc41 | ||
|
|
424430723b | ||
|
|
2f789468ea | ||
|
|
8fd84f77c7 | ||
|
|
4d0030363b | ||
|
|
fd5ad9ac70 | ||
|
|
192ec8596d | ||
|
|
8702ed096b | ||
|
|
bde655ae00 | ||
|
|
549d71fb0b | ||
|
|
091b8bc73f | ||
|
|
bd1f7b539e | ||
|
|
cd8be3f586 | ||
|
|
3c0f05ae93 | ||
|
|
ccec45a564 | ||
|
|
0bce96d2de | ||
|
|
5469e3668e | ||
|
|
dac5af8eca | ||
|
|
30845ee776 | ||
|
|
649f8414a9 | ||
|
|
d0a1bae68f | ||
|
|
5dc240d4ed | ||
|
|
5248613e9d | ||
|
|
b491a30dd9 | ||
|
|
300241428c | ||
|
|
1d303fa9d2 | ||
|
|
744bfc1eeb | ||
|
|
7773443c17 | ||
|
|
43461a1866 | ||
|
|
85b712967f | ||
|
|
b1b6b250d1 | ||
|
|
2e078493a7 | ||
|
|
96b66b7e4f | ||
|
|
58a9fa0ad5 | ||
|
|
8e5331e5b2 | ||
|
|
caae9f8a6a | ||
|
|
1b7672f70e | ||
|
|
fc18912ecd | ||
|
|
4225e79456 | ||
|
|
df15571b82 | ||
|
|
383b517405 | ||
|
|
1ee8d6a380 | ||
|
|
b2d635060c | ||
|
|
d5591da682 | ||
|
|
f0f73380a2 | ||
|
|
0247802479 | ||
|
|
4360d07918 | ||
|
|
ba33bc4ad8 | ||
|
|
bec1860197 | ||
|
|
f54c387172 | ||
|
|
f160f4bf67 | ||
|
|
fa33d1880c | ||
|
|
ba4af29bf8 | ||
|
|
8d1cb3c36a | ||
|
|
8f5f0e46aa | ||
|
|
fa389a4e06 | ||
|
|
0c9a03ac54 | ||
|
|
c59ac49fea | ||
|
|
f0868f5a51 | ||
|
|
e7e9f8e7f2 | ||
|
|
f969edcf50 | ||
|
|
94afd7a9ea | ||
|
|
91ef367176 | ||
|
|
5fd83722d8 | ||
|
|
e1ba962fe1 | ||
|
|
ff2cdf9f16 | ||
|
|
b41a83ceda | ||
|
|
0657bb1b36 | ||
|
|
664b0c234e | ||
|
|
1e8b34e63e | ||
|
|
27caa683fe | ||
|
|
75f6ab9133 | ||
|
|
95cb2fbebf | ||
|
|
8b775fd24a | ||
|
|
78f5afff74 | ||
|
|
c09ac41b27 | ||
|
|
7a4270f5a4 | ||
|
|
67b2aebed6 | ||
|
|
14363f8f2f | ||
|
|
3184d6f369 | ||
|
|
ef94af950c | ||
|
|
9bbfd610be | ||
|
|
363dd12a2a | ||
|
|
dd848304e6 | ||
|
|
4681ac8c23 | ||
|
|
502a4819b6 | ||
|
|
467e40b555 | ||
|
|
040b5b3f88 | ||
|
|
84519752c3 | ||
|
|
852f2a0d65 | ||
|
|
7b90d69443 | ||
|
|
eeea4fcb31 | ||
|
|
df59a43300 | ||
|
|
5a93bf054e | ||
|
|
0ec7005f90 | ||
|
|
4e7b2f3ea3 | ||
|
|
53b64a6367 | ||
|
|
0ddccd56c7 | ||
|
|
4791c7f424 | ||
|
|
b0631f90f8 | ||
|
|
e35ed17200 | ||
|
|
059fb51893 | ||
|
|
ca1043ffda | ||
|
|
5be6966462 | ||
|
|
0d755b86ab | ||
|
|
a99b92f613 | ||
|
|
696db310a5 | ||
|
|
b1e15751d6 | ||
|
|
65e8eef87f | ||
|
|
8bb930dd04 | ||
|
|
3c3fac7ca4 | ||
|
|
5e61ad09c1 | ||
|
|
48e119b59e | ||
|
|
e023d8abdd | ||
|
|
1c0c21159b | ||
|
|
d2ddb255ef | ||
|
|
3960070a50 | ||
|
|
4eb4b341db | ||
|
|
5b9b6a931d | ||
|
|
9607854b67 | ||
|
|
62e352a2a8 | ||
|
|
b3ff173b45 | ||
|
|
762082e13d | ||
|
|
f59a4f85db | ||
|
|
6c20340338 | ||
|
|
0294844c11 | ||
|
|
258b504000 | ||
|
|
c9482b5ea2 | ||
|
|
c017f788ac | ||
|
|
e1f4865844 | ||
|
|
0169ec880c | ||
|
|
9a3f2e8fcc | ||
|
|
31a5d0c2f0 | ||
|
|
503bd357f4 | ||
|
|
8c3920a0db | ||
|
|
1546d65ebe | ||
|
|
20fa7fc2f7 | ||
|
|
9e86bc586c | ||
|
|
605982a2b7 | ||
|
|
2f7573850e | ||
|
|
8999e92f76 | ||
|
|
a62e5d39ca | ||
|
|
993374dce7 | ||
|
|
e8a8a17217 | ||
|
|
8e681c1723 | ||
|
|
43acd09df8 | ||
|
|
8571cafcc8 | ||
|
|
c3deb16a7d | ||
|
|
cc0db41879 | ||
|
|
e35f2c5bed | ||
|
|
923a9c36cb | ||
|
|
960855d0aa | ||
|
|
ebea5b0159 | ||
|
|
bd5c82404d | ||
|
|
5ae9365f77 | ||
|
|
92a9cda4fc | ||
|
|
059beab700 | ||
|
|
ea235a1468 | ||
|
|
8973bb6f71 | ||
|
|
d69ef890c0 | ||
|
|
0677ce6d52 | ||
|
|
72957f4d51 | ||
|
|
5473320ce4 | ||
|
|
9350709f13 | ||
|
|
ff454ab29d | ||
|
|
e1fb75a81d | ||
|
|
dc46149f1c | ||
|
|
4386799fb0 | ||
|
|
d2374d62aa | ||
|
|
2f4b9aa1f0 | ||
|
|
74f6ac27af | ||
|
|
ec5f406f49 | ||
|
|
fe6367cbcd | ||
|
|
ed22f968f9 | ||
|
|
73e2b09ba8 | ||
|
|
2484c52611 | ||
|
|
1165d3f330 | ||
|
|
86e42a9081 | ||
|
|
bddea809ec | ||
|
|
863ee984fe | ||
|
|
ee287740a7 | ||
|
|
1253e3db1d | ||
|
|
124d2e23b7 | ||
|
|
f4513c12eb | ||
|
|
f0a59f06cd | ||
|
|
d7bf8826fc | ||
|
|
db89286ec3 | ||
|
|
64733e93d9 | ||
|
|
d0e6b8c89d | ||
|
|
243a0e3cf1 | ||
|
|
505cb2f65d | ||
|
|
99325618a6 | ||
|
|
fc2972e977 | ||
|
|
04571d3b20 | ||
|
|
d062548e41 | ||
|
|
e12af33626 | ||
|
|
4a7ce238fd | ||
|
|
d4d5e32c91 | ||
|
|
c5b8706225 | ||
|
|
6bf48d0506 | ||
|
|
37b009a342 | ||
|
|
b040db26a7 | ||
|
|
c33c907330 | ||
|
|
5411ad9633 | ||
|
|
a34d42492d | ||
|
|
12c6a4043b | ||
|
|
b21064f16f | ||
|
|
29b697df1a | ||
|
|
f095b35663 | ||
|
|
d296a1be65 | ||
|
|
a53dded50f | ||
|
|
d7c5949365 | ||
|
|
67abea567f | ||
|
|
0dd3a58a63 | ||
|
|
f04e5fbed6 | ||
|
|
65ce3deeaa | ||
|
|
141ff99580 | ||
|
|
a8e6eaa247 | ||
|
|
dd6e5ae9ac | ||
|
|
34e84858c8 | ||
|
|
ecea61d9ca | ||
|
|
55963bd092 | ||
|
|
36f64d1ad9 | ||
|
|
5376d37c24 | ||
|
|
32af83b7ae | ||
|
|
eba97f74b4 | ||
|
|
4d62963efe | ||
|
|
f767d41409 | ||
|
|
75e30ddc9d | ||
|
|
e1c66488b1 | ||
|
|
f7f4fef156 | ||
|
|
e059867314 | ||
|
|
a266de6735 | ||
|
|
e1b85327be | ||
|
|
e04e8d2365 | ||
|
|
48b0de7871 | ||
|
|
aceb022f9d | ||
|
|
a6a003a345 | ||
|
|
2ab8234e9c | ||
|
|
d905f0e55e | ||
|
|
436f6a4870 | ||
|
|
71ac3bb305 | ||
|
|
f55db2f90b | ||
|
|
2b8d801b36 | ||
|
|
bd32b88f62 | ||
|
|
dace2e5495 | ||
|
|
b3a21f53bc | ||
|
|
47b6d3c52c | ||
|
|
3d4773b161 | ||
|
|
7d114ff32d | ||
|
|
39fb5b8f58 | ||
|
|
6d5b28a9c5 | ||
|
|
8b61d18a9f | ||
|
|
5d52dc204c | ||
|
|
1686a97ece | ||
|
|
1b46866e34 | ||
|
|
a89e67eeed | ||
|
|
160bc93e26 | ||
|
|
7a46bd1089 | ||
|
|
908c979338 | ||
|
|
1a5c77aa82 | ||
|
|
e37da62a1c | ||
|
|
5c4a6c0f2b | ||
|
|
c2ecfaf239 | ||
|
|
ca8eae919f | ||
|
|
0862fdb9a9 | ||
|
|
386e0d560e | ||
|
|
4f7283a3b0 | ||
|
|
1c63bca2c7 | ||
|
|
5b4fada2a0 | ||
|
|
f53b480f1c | ||
|
|
f819e9b6f4 | ||
|
|
af232223ee | ||
|
|
5e0179dac4 | ||
|
|
9037f25da1 | ||
|
|
34569d172f | ||
|
|
917b7fa898 | ||
|
|
416b687054 | ||
|
|
78258a3a95 | ||
|
|
bcdb0c46fc | ||
|
|
263c9265ae | ||
|
|
92d16e8b10 | ||
|
|
2aefc8440a | ||
|
|
791e0e1a67 | ||
|
|
99d18a48f2 | ||
|
|
082a83dd85 | ||
|
|
a88a2dea82 | ||
|
|
c61e13c1e9 | ||
|
|
07a06b5d15 | ||
|
|
361ffc0620 | ||
|
|
0e6160bf2d | ||
|
|
b68729115a | ||
|
|
2a60a701bf | ||
|
|
2d352bc3f0 | ||
|
|
c4e09fa874 | ||
|
|
81cc20039e | ||
|
|
6958c0ccc3 | ||
|
|
085cd9919e | ||
|
|
ef2a6359e4 | ||
|
|
637e65efe3 | ||
|
|
c47533f6cf | ||
|
|
bf18e2bbc9 | ||
|
|
10a4c7a6ed | ||
|
|
e8bc025f5c | ||
|
|
14b4955a6f | ||
|
|
d418976f5e | ||
|
|
1526fd3722 | ||
|
|
2cc77c1c7d | ||
|
|
60f8cf665e | ||
|
|
0e59bc1bc5 | ||
|
|
1af225015a | ||
|
|
7c4d6c6801 | ||
|
|
5afdc14913 | ||
|
|
8fa6bd2aac | ||
|
|
e573c6d385 | ||
|
|
e3b372946a | ||
|
|
ab441a507a | ||
|
|
372921b423 | ||
|
|
9ce3814d8b | ||
|
|
684e69763a | ||
|
|
cd5152a02d | ||
|
|
1233309ebd | ||
|
|
37206ec08e | ||
|
|
1ef804c652 | ||
|
|
cd5453e477 | ||
|
|
150e27608b | ||
|
|
e975727075 | ||
|
|
bb9871ded7 | ||
|
|
f037f06e74 | ||
|
|
87b05e1c9e | ||
|
|
c7833b8bc0 | ||
|
|
ad503daaca | ||
|
|
cc18f66793 | ||
|
|
508793b010 | ||
|
|
dc1a31d802 | ||
|
|
f3f2534877 | ||
|
|
70cca3bad9 | ||
|
|
b37695f9c8 | ||
|
|
1d7bf698f2 | ||
|
|
decb8bfd52 | ||
|
|
d759546b32 | ||
|
|
02f108d927 | ||
|
|
788b5b04fe | ||
|
|
a61953673a | ||
|
|
da9d1e6001 | ||
|
|
7dd4032cce | ||
|
|
4653a1007c | ||
|
|
3f4e632cc4 | ||
|
|
626828e980 | ||
|
|
4d43d12abf | ||
|
|
ab1ec57429 | ||
|
|
8aebb8249a | ||
|
|
70c32590a9 | ||
|
|
ce5cc135cd | ||
|
|
91273f7114 | ||
|
|
4b1b6e3adf | ||
|
|
6a2d2fbc3d | ||
|
|
53fd6a2df5 | ||
|
|
5e4a4ae16b | ||
|
|
32d5305295 | ||
|
|
826a56311c | ||
|
|
071bc27016 | ||
|
|
12e79ecd60 | ||
|
|
3e2c5e8656 | ||
|
|
4984890265 | ||
|
|
6b8ad2d126 | ||
|
|
3b9a55fab4 | ||
|
|
c4f3fbaca0 | ||
|
|
deda6535e0 | ||
|
|
33d14e4238 | ||
|
|
9d7cf12244 | ||
|
|
952e9b87e1 | ||
|
|
7cc628dc79 | ||
|
|
2ce11fa83b | ||
|
|
4253dd27eb | ||
|
|
4c8103af3b | ||
|
|
3be5b4b00f | ||
|
|
1294608571 | ||
|
|
172ddf4aaf | ||
|
|
55b582511e | ||
|
|
e4fd5ec1ae | ||
|
|
002b8a99e2 | ||
|
|
eccb8ec2d6 | ||
|
|
61b5ce0451 | ||
|
|
d50b36d314 | ||
|
|
9586157479 | ||
|
|
cedd518aea | ||
|
|
855a70bc66 | ||
|
|
cbd91ba5b1 | ||
|
|
8ee1f140d8 | ||
|
|
f9a5f2e183 | ||
|
|
8caab35d90 | ||
|
|
9161e8c8f4 | ||
|
|
7e1a784fca | ||
|
|
96b699e534 | ||
|
|
6f0dceb152 | ||
|
|
924ee1a672 | ||
|
|
ae501ca8ed | ||
|
|
d840804818 | ||
|
|
adc91eb75e | ||
|
|
916cdebacb | ||
|
|
a2ed08615c | ||
|
|
39db32c3ce | ||
|
|
af63913189 | ||
|
|
fef15f9c02 | ||
|
|
825d7c2cbd | ||
|
|
3ec0ceba3e | ||
|
|
67d3d6b5b5 | ||
|
|
01246b0d97 | ||
|
|
533bd97a05 | ||
|
|
c8f82c71c9 | ||
|
|
11bf084a1f | ||
|
|
24ec7ce6b8 | ||
|
|
7221fb3231 | ||
|
|
b3a2bce213 | ||
|
|
435efb47d0 | ||
|
|
1b95cced5d | ||
|
|
e5e3ac0364 | ||
|
|
aee2d8e120 | ||
|
|
dcab22dcc7 | ||
|
|
78488ebcbf | ||
|
|
4360a785ad | ||
|
|
7dc5665ab1 | ||
|
|
4d502eb2bf | ||
|
|
9c919e6478 | ||
|
|
1d711eeadc | ||
|
|
58a5346d72 | ||
|
|
27e42b4826 | ||
|
|
3fc9326c43 | ||
|
|
da23e71db1 | ||
|
|
ab94a47b8e | ||
|
|
1635bc8cb3 | ||
|
|
a9197236a2 | ||
|
|
2453872a09 | ||
|
|
6f5a4a5502 | ||
|
|
482259df8b | ||
|
|
beb37aafc5 | ||
|
|
2a958499b6 | ||
|
|
f38ec93ae9 | ||
|
|
6ccd83397c | ||
|
|
d1f11f5fe9 | ||
|
|
f05f3b430a | ||
|
|
bdecef0eaf | ||
|
|
2bd5e0f25d | ||
|
|
2e61359d50 | ||
|
|
23f56ffa8a | ||
|
|
e4bd445a38 | ||
|
|
64ab8222f7 | ||
|
|
819044221b | ||
|
|
78e9152723 | ||
|
|
43664d5f11 | ||
|
|
1f14894c43 | ||
|
|
a9fcf2fabf | ||
|
|
c93d137c5e | ||
|
|
c40468a8d3 | ||
|
|
2e18e3c62b | ||
|
|
a3fb865db0 | ||
|
|
6452582a17 | ||
|
|
e7fa42ce3e | ||
|
|
cad4fb80c1 | ||
|
|
47a97279af | ||
|
|
2039c07a2d | ||
|
|
1419a5c60d | ||
|
|
6f7a065081 | ||
|
|
3842205b8a | ||
|
|
152c6abb86 | ||
|
|
9505a203d8 | ||
|
|
15b21abc99 | ||
|
|
ce4608ae76 | ||
|
|
f2e93e323a | ||
|
|
8cd08cc0fa | ||
|
|
9e0777a7b4 | ||
|
|
ab3c3c5ed7 | ||
|
|
a5b3f809ce | ||
|
|
014c0d3a41 | ||
|
|
518c6280e9 | ||
|
|
6b9a83ae80 | ||
|
|
bed35a65c7 | ||
|
|
9ffd2de492 | ||
|
|
ecc296cf67 | ||
|
|
8b8ca14c6d | ||
|
|
e829d6bbcf | ||
|
|
19d4bd4837 | ||
|
|
4e0d179937 | ||
|
|
09dfb0fd1d | ||
|
|
3b6af914e1 | ||
|
|
1728dff576 | ||
|
|
557334aa36 | ||
|
|
20957ac4d9 | ||
|
|
a4396f4f13 | ||
|
|
278a6bcb4d | ||
|
|
19e244a85e | ||
|
|
54cc822227 | ||
|
|
e2338581eb | ||
|
|
b279d351d8 | ||
|
|
fffec71fb3 | ||
|
|
3e3d387161 | ||
|
|
061231494d | ||
|
|
e8f87d2e62 | ||
|
|
66777d4566 | ||
|
|
d8d2f3329b | ||
|
|
526319630e | ||
|
|
999ae1f713 | ||
|
|
6b2509b106 | ||
|
|
b2547601a5 | ||
|
|
97ea4679a7 | ||
|
|
3d424077fd | ||
|
|
ecf4ea9ba7 | ||
|
|
b381a7fdbf | ||
|
|
48a5b8527a | ||
|
|
096b3e6026 | ||
|
|
e3fb991b1b | ||
|
|
cdca74aa39 | ||
|
|
2f224819ac | ||
|
|
57cac47944 | ||
|
|
86733279f6 | ||
|
|
c5bedbd3ef | ||
|
|
77d86f074f | ||
|
|
b33b2c0945 | ||
|
|
c49e563881 | ||
|
|
4a88ca1a3a | ||
|
|
86bc59cd60 | ||
|
|
c9ffffc526 | ||
|
|
57e66324cb | ||
|
|
ddee03d324 | ||
|
|
136df7e5ee | ||
|
|
32528d6aa6 | ||
|
|
64a03c245c | ||
|
|
7500b1fbee | ||
|
|
56c3c76d8b | ||
|
|
fd40dee337 | ||
|
|
26172686b8 | ||
|
|
1fa07c920c | ||
|
|
52b877ac3d | ||
|
|
8f4b57f718 | ||
|
|
617103bb2a | ||
|
|
dc51e82f54 | ||
|
|
e5cd2ed52f | ||
|
|
96fb75ffc6 | ||
|
|
8467f95a28 | ||
|
|
8fe066707a | ||
|
|
77aefdfe71 | ||
|
|
573760daf0 | ||
|
|
73bf7a92a2 | ||
|
|
be50394f11 | ||
|
|
0ad504bdf0 | ||
|
|
6e80ba7b4f | ||
|
|
5ef04a039b | ||
|
|
234273809a | ||
|
|
0142e0fa22 | ||
|
|
d367199553 | ||
|
|
9c363db440 | ||
|
|
4d95452ae7 | ||
|
|
2187615c08 | ||
|
|
c89020725b | ||
|
|
14a032a0b1 | ||
|
|
3f0d79f07d | ||
|
|
2157eae499 | ||
|
|
e9ceeb85af | ||
|
|
19387ff911 | ||
|
|
f12074397f | ||
|
|
a4ffa0b22a | ||
|
|
6700364ac8 | ||
|
|
9543a108be | ||
|
|
5117a520ae | ||
|
|
9bfb5fe71f | ||
|
|
8f36c9167d | ||
|
|
b120584f97 | ||
|
|
f733cb8947 | ||
|
|
c53caecd1e | ||
|
|
89a01a6463 | ||
|
|
8080a713b2 | ||
|
|
97ea0fc439 | ||
|
|
40ceabff79 | ||
|
|
69a204d726 | ||
|
|
44fbd8330b | ||
|
|
ecbbfdd10c | ||
|
|
951fd8a47f | ||
|
|
1e3c3a528c | ||
|
|
73e367dc3b | ||
|
|
0da1e904fe | ||
|
|
6c7bfe613f | ||
|
|
e9f1888321 | ||
|
|
9220545e60 | ||
|
|
1ec71cbaca | ||
|
|
7d84409628 | ||
|
|
bc35c82619 | ||
|
|
28c49d2c48 | ||
|
|
56e7ba41c4 | ||
|
|
1bb1fc37f4 | ||
|
|
c42f0dac53 | ||
|
|
6ee689345f | ||
|
|
aac7a34405 | ||
|
|
c99007bda7 | ||
|
|
1b19cdd0f4 | ||
|
|
0137626a63 | ||
|
|
c25baf3dd7 | ||
|
|
262d431ff5 | ||
|
|
91c369e392 | ||
|
|
9279f30363 | ||
|
|
2a5f108d4a | ||
|
|
04c1b522d6 | ||
|
|
0bcea80bdf | ||
|
|
bc6010303a | ||
|
|
1e6edd0abc | ||
|
|
7044f1145f | ||
|
|
941df4153b | ||
|
|
f3f5b8a5d6 | ||
|
|
db834800c0 | ||
|
|
3089edd3a2 | ||
|
|
f7166e95c4 | ||
|
|
b7178f2d21 | ||
|
|
88fc62e8f7 | ||
|
|
5f3408dd70 | ||
|
|
b4b1de088a | ||
|
|
52a4810752 | ||
|
|
93578d9be2 | ||
|
|
820316e745 | ||
|
|
c0daef2657 | ||
|
|
e7ef4aa4f6 | ||
|
|
deee29228e | ||
|
|
576c2718c8 | ||
|
|
91bcc5f560 | ||
|
|
059a4fff5c | ||
|
|
a64228ba2c | ||
|
|
85bae50e62 | ||
|
|
ca30705c69 | ||
|
|
95b7aef579 | ||
|
|
e0d33279de | ||
|
|
f008f2e4dc | ||
|
|
a7cfa56621 | ||
|
|
9228cb5b8e | ||
|
|
2a9f5db576 | ||
|
|
531cdeffa9 | ||
|
|
7307c800d7 | ||
|
|
6b42e8448c | ||
|
|
cf14d7b346 | ||
|
|
6192bfce46 | ||
|
|
194ee395e7 | ||
|
|
2eb72d496f | ||
|
|
f64062b6f1 | ||
|
|
32f5feff8f | ||
|
|
a29e2218c8 | ||
|
|
41e088693d | ||
|
|
861640949e | ||
|
|
c9c8b7656d | ||
|
|
e1b2195cf7 | ||
|
|
629b9cb3b5 | ||
|
|
8e69174374 | ||
|
|
eaf72aa951 | ||
|
|
0d05b84dc3 | ||
|
|
02c30e3d52 | ||
|
|
6899ca2527 | ||
|
|
a799a00dc5 | ||
|
|
579d48cf0c | ||
|
|
61aa19539c | ||
|
|
53130da682 | ||
|
|
d2abaf54e8 | ||
|
|
89aa9eb0a7 | ||
|
|
a830747f83 | ||
|
|
4284f4feb3 | ||
|
|
f8e13c5c33 | ||
|
|
281805a0a4 | ||
|
|
f7dce426cb | ||
|
|
597295e359 | ||
|
|
801d3113ab | ||
|
|
cb6bde49b4 | ||
|
|
0100af9389 | ||
|
|
01193be241 | ||
|
|
cc77ba523f | ||
|
|
1fb0b6d7bd | ||
|
|
f9f6ea4365 | ||
|
|
b96b5af101 | ||
|
|
0025073b24 | ||
|
|
780b2d067c | ||
|
|
8f17f38b02 | ||
|
|
0186f09c27 | ||
|
|
aa86440866 | ||
|
|
27b36486df | ||
|
|
e5661156f0 | ||
|
|
bdb8220a1a | ||
|
|
1e715113ab | ||
|
|
b4b862b0cc | ||
|
|
e3888752d6 | ||
|
|
69dc762a5a | ||
|
|
21204fc552 | ||
|
|
4b74f9c7fb | ||
|
|
c856633b9c | ||
|
|
cf7caa7ef9 | ||
|
|
8ba70ee0e4 | ||
|
|
88eb2390e6 | ||
|
|
8bb59fcc3c | ||
|
|
b8ab36546d | ||
|
|
f8ee203225 | ||
|
|
b44aca1654 | ||
|
|
87f6aa09df | ||
|
|
b29c2a0abd | ||
|
|
77940148fa | ||
|
|
1cc1c8a051 | ||
|
|
097ac144d9 | ||
|
|
30a7952cbb | ||
|
|
5d462f9555 | ||
|
|
e3d8edd0a0 | ||
|
|
0ddbd2e575 | ||
|
|
94dc214982 | ||
|
|
3f4687d3e4 | ||
|
|
73896bad72 | ||
|
|
48356a03e6 | ||
|
|
b1cd260aa9 | ||
|
|
eb44ef327d | ||
|
|
358722b9cc | ||
|
|
40a43afa12 | ||
|
|
a51940fac0 | ||
|
|
73fee2fefa |
14
.gitignore
vendored
@ -4,20 +4,24 @@
|
||||
build/
|
||||
dist/
|
||||
*.egg/
|
||||
/electrum.py
|
||||
contrib/pyinstaller/
|
||||
Electrum.egg-info/
|
||||
gui/qt/icons_rc.py
|
||||
locale/
|
||||
electrum/locale/
|
||||
.devlocaltmp/
|
||||
*_trial_temp
|
||||
packages
|
||||
env/
|
||||
.tox/
|
||||
.buildozer/
|
||||
bin/
|
||||
/app.fil
|
||||
.idea
|
||||
|
||||
# tox files
|
||||
# icons
|
||||
electrum/gui/kivy/theming/light-0.png
|
||||
electrum/gui/kivy/theming/light.atlas
|
||||
|
||||
# tests/tox
|
||||
.tox/
|
||||
.cache/
|
||||
.coverage
|
||||
.pytest_cache
|
||||
|
||||
6
.gitmodules
vendored
@ -1,6 +1,6 @@
|
||||
[submodule "contrib/deterministic-build/electrum-icons"]
|
||||
path = contrib/deterministic-build/electrum-icons
|
||||
url = https://github.com/spesmilo/electrum-icons
|
||||
[submodule "contrib/deterministic-build/electrum-locale"]
|
||||
path = contrib/deterministic-build/electrum-locale
|
||||
url = https://github.com/spesmilo/electrum-locale
|
||||
[submodule "contrib/CalinsQRReader"]
|
||||
path = contrib/osx/CalinsQRReader
|
||||
url = https://github.com/spesmilo/CalinsQRReader
|
||||
|
||||
58
.travis.yml
@ -1,14 +1,19 @@
|
||||
sudo: false
|
||||
sudo: true
|
||||
dist: xenial
|
||||
language: python
|
||||
python:
|
||||
- 3.5
|
||||
- 3.6
|
||||
- 3.7
|
||||
git:
|
||||
depth: false
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- sourceline: 'ppa:tah83/secp256k1'
|
||||
packages:
|
||||
- libsecp256k1-0
|
||||
before_install:
|
||||
- git tag
|
||||
install:
|
||||
- pip install -r contrib/requirements/requirements-travis.txt
|
||||
cache:
|
||||
@ -18,11 +23,12 @@ cache:
|
||||
script:
|
||||
- tox
|
||||
after_success:
|
||||
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install pycurl requests && contrib/make_locale; fi
|
||||
- if [ "$TRAVIS_BRANCH" = "master" ]; then pip install requests && contrib/make_locale; fi
|
||||
- coveralls
|
||||
jobs:
|
||||
include:
|
||||
- stage: binary builds
|
||||
name: "Windows build"
|
||||
sudo: true
|
||||
language: c
|
||||
python: false
|
||||
@ -31,26 +37,58 @@ jobs:
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/
|
||||
- sudo docker build --no-cache -t electrum-wine-builder-img ./contrib/build-wine/docker/
|
||||
script:
|
||||
- sudo docker run --name electrum-wine-builder-cont -v $PWD:/opt/electrum --rm --workdir /opt/electrum/contrib/build-wine electrum-wine-builder-img ./build.sh $TRAVIS_COMMIT
|
||||
- sudo docker run --name electrum-wine-builder-cont -v $PWD:/opt/wine64/drive_c/electrum --rm --workdir /opt/wine64/drive_c/electrum/contrib/build-wine electrum-wine-builder-img ./build.sh
|
||||
after_success: true
|
||||
- os: osx
|
||||
- name: "Android build"
|
||||
language: python
|
||||
python: 3.7
|
||||
env:
|
||||
# reset API key to not have make_locale upload stuff here
|
||||
- crowdin_api_key=
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- pip install requests && ./contrib/make_locale
|
||||
- ./contrib/make_packages
|
||||
- sudo docker build --no-cache -t electrum-android-builder-img electrum/gui/kivy/tools
|
||||
script:
|
||||
- sudo chown -R 1000:1000 .
|
||||
# Output something every minute or Travis kills the job
|
||||
- while sleep 60; do echo "=====[ $SECONDS seconds still running ]====="; done &
|
||||
- sudo docker run -it -u 1000:1000 --rm --name electrum-android-builder-cont -v $PWD:/home/user/wspace/electrum --workdir /home/user/wspace/electrum electrum-android-builder-img ./contrib/make_apk
|
||||
# kill background sleep loop
|
||||
- kill %1
|
||||
- ls -la bin
|
||||
- if [ $(ls bin | grep -c Electrum-*) -eq 0 ]; then exit 1; fi
|
||||
after_success: true
|
||||
- name: "MacOS build"
|
||||
os: osx
|
||||
language: c
|
||||
env:
|
||||
- TARGET_OS=macOS
|
||||
python: false
|
||||
install:
|
||||
- git fetch --all --tags
|
||||
- git fetch origin --unshallow
|
||||
script: ./contrib/build-osx/make_osx
|
||||
script: ./contrib/osx/make_osx
|
||||
after_script: ls -lah dist && md5 dist/*
|
||||
after_success: true
|
||||
- name: "AppImage build"
|
||||
sudo: true
|
||||
language: c
|
||||
python: false
|
||||
services:
|
||||
- docker
|
||||
install:
|
||||
- sudo docker build --no-cache -t electrum-appimage-builder-img ./contrib/build-linux/appimage/
|
||||
script:
|
||||
- sudo docker run --name electrum-appimage-builder-cont -v $PWD:/opt/electrum --rm --workdir /opt/electrum/contrib/build-linux/appimage electrum-appimage-builder-img ./build.sh
|
||||
after_success: true
|
||||
- stage: release check
|
||||
install:
|
||||
- git fetch --all --tags
|
||||
- git fetch origin --unshallow
|
||||
script:
|
||||
- ./contrib/deterministic-build/check_submodules.sh
|
||||
after_success: true
|
||||
if: tag IS present
|
||||
if: tag IS present
|
||||
|
||||
13
AUTHORS
@ -1,3 +1,5 @@
|
||||
Electrum-BTC
|
||||
------------
|
||||
ThomasV - Creator and maintainer.
|
||||
Animazing / Tachikoma - Styled the new GUI. Mac version.
|
||||
Azelphur - GUI stuff.
|
||||
@ -9,4 +11,13 @@ Genjix - Porting pro-mode functionality to lite-gui and worked on server
|
||||
Slush - Work on the server. Designed the original Stratum spec.
|
||||
Julian Toash (Tuxavant) - Various fixes to the client.
|
||||
rdymac - Website and translations.
|
||||
kyuupichan - Miscellaneous.
|
||||
kyuupichan - Miscellaneous.
|
||||
|
||||
|
||||
FLO-Electrum
|
||||
------------
|
||||
vivekteega - Maintainer and remaining stuff
|
||||
Bitspill - Bootstraped the project with core FLO changes
|
||||
Rohit Tripathy - Ideation and problem solving
|
||||
akhil2015 - Flodata and scrypt hashing
|
||||
|
||||
|
||||
22
Info.plist
@ -1,22 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>bitcoin</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>bitcoin</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>LSArchitecturePriority</key>
|
||||
<array>
|
||||
<string>x86_64</string>
|
||||
<string>i386</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
16
MANIFEST.in
@ -1,17 +1,17 @@
|
||||
include LICENCE RELEASE-NOTES AUTHORS
|
||||
include README.rst
|
||||
include electrum.conf.sample
|
||||
include electrum.desktop
|
||||
include *.py
|
||||
include electrum
|
||||
include run_electrum
|
||||
include contrib/requirements/requirements.txt
|
||||
include contrib/requirements/requirements-hw.txt
|
||||
recursive-include lib *.py
|
||||
recursive-include gui *.py
|
||||
recursive-include plugins *.py
|
||||
recursive-include packages *.py
|
||||
recursive-include packages cacert.pem
|
||||
include icons.qrc
|
||||
recursive-include icons *
|
||||
recursive-include scripts *
|
||||
|
||||
graft electrum
|
||||
prune electrum/tests
|
||||
|
||||
global-exclude __pycache__
|
||||
global-exclude *.py[co~]
|
||||
global-exclude *.py.orig
|
||||
global-exclude *.py.rej
|
||||
|
||||
42
README.rst
@ -5,7 +5,7 @@ Electrum - Lightweight Bitcoin client
|
||||
|
||||
Licence: MIT Licence
|
||||
Author: Thomas Voegtlin
|
||||
Language: Python
|
||||
Language: Python (>= 3.6)
|
||||
Homepage: https://electrum.org/
|
||||
|
||||
|
||||
@ -15,9 +15,9 @@ Electrum - Lightweight Bitcoin client
|
||||
.. image:: https://coveralls.io/repos/github/spesmilo/electrum/badge.svg?branch=master
|
||||
:target: https://coveralls.io/github/spesmilo/electrum?branch=master
|
||||
:alt: Test coverage statistics
|
||||
.. image:: https://img.shields.io/badge/help-translating-blue.svg
|
||||
.. image:: https://d322cqt584bo4o.cloudfront.net/electrum/localized.svg
|
||||
:target: https://crowdin.com/project/electrum
|
||||
:alt: Help translating Electrum online
|
||||
:alt: Help translate Electrum online
|
||||
|
||||
|
||||
|
||||
@ -32,19 +32,19 @@ Qt interface, install the Qt dependencies::
|
||||
sudo apt-get install python3-pyqt5
|
||||
|
||||
If you downloaded the official package (tar.gz), you can run
|
||||
Electrum from its root directory, without installing it on your
|
||||
Electrum from its root directory without installing it on your
|
||||
system; all the python dependencies are included in the 'packages'
|
||||
directory. To run Electrum from its root directory, just do::
|
||||
|
||||
./electrum
|
||||
./run_electrum
|
||||
|
||||
You can also install Electrum on your system, by running this command::
|
||||
|
||||
sudo apt-get install python3-setuptools
|
||||
pip3 install .[fast]
|
||||
python3 -m pip install .[fast]
|
||||
|
||||
This will download and install the Python dependencies used by
|
||||
Electrum, instead of using the 'packages' directory.
|
||||
Electrum instead of using the 'packages' directory.
|
||||
The 'fast' extra contains some optional dependencies that we think
|
||||
are often useful but they are not strictly needed.
|
||||
|
||||
@ -64,21 +64,13 @@ Check out the code from GitHub::
|
||||
|
||||
Run install (this should install dependencies)::
|
||||
|
||||
pip3 install .[fast]
|
||||
python3 -m pip install .[fast]
|
||||
|
||||
Render the SVG icons to PNGs (optional)::
|
||||
|
||||
for i in lock unlock confirmed status_lagging status_disconnected status_connected_proxy status_connected status_waiting preferences; do convert -background none icons/$i.svg icons/$i.png; done
|
||||
|
||||
Compile the icons file for Qt::
|
||||
|
||||
sudo apt-get install pyqt5-dev-tools
|
||||
pyrcc5 icons.qrc -o gui/qt/icons_rc.py
|
||||
|
||||
Compile the protobuf description file::
|
||||
|
||||
sudo apt-get install protobuf-compiler
|
||||
protoc --proto_path=lib/ --python_out=lib/ lib/paymentrequest.proto
|
||||
protoc --proto_path=electrum --python_out=electrum electrum/paymentrequest.proto
|
||||
|
||||
Create translations (optional)::
|
||||
|
||||
@ -91,25 +83,25 @@ Create translations (optional)::
|
||||
Creating Binaries
|
||||
=================
|
||||
|
||||
Linux
|
||||
-----
|
||||
|
||||
To create binaries, create the 'packages' directory::
|
||||
See :code:`contrib/build-linux/README.md`.
|
||||
|
||||
./contrib/make_packages
|
||||
|
||||
This directory contains the python dependencies used by Electrum.
|
||||
|
||||
Mac OS X / macOS
|
||||
--------
|
||||
----------------
|
||||
|
||||
See :code:`contrib/osx/README.md`.
|
||||
|
||||
See `contrib/build-osx/`.
|
||||
|
||||
Windows
|
||||
-------
|
||||
|
||||
See `contrib/build-wine/`.
|
||||
See :code:`contrib/build-wine/docker/README.md`.
|
||||
|
||||
|
||||
Android
|
||||
-------
|
||||
|
||||
See `gui/kivy/Readme.txt` file.
|
||||
See :code:`electrum/gui/kivy/Readme.md`.
|
||||
|
||||
102
RELEASE-NOTES
@ -1,3 +1,95 @@
|
||||
# Release 3.3.4 - (February 13, 2019)
|
||||
|
||||
* AppImage: we now also distribute self-contained binaries for x86_64
|
||||
Linux in the form of an AppImage (#5042). The Python interpreter,
|
||||
PyQt5, libsecp256k1, PyCryptodomex, zbar, hidapi/libusb (including
|
||||
hardware wallet libraries) are all bundled. Note that users of
|
||||
hw wallets still need to set udev rules themselves.
|
||||
* hw wallets: fix a regression during transaction signing that prompts
|
||||
the user too many times for confirmations (commit 2729909)
|
||||
* transactions now set nVersion to 2, to mimic Bitcoin Core
|
||||
* fix Qt bug that made all hw wallets unusable on Windows 8.1 (#4960)
|
||||
* fix bugs in wallet creation wizard that resulted in corrupted
|
||||
wallets being created in rare cases (#5082, #5057)
|
||||
* fix compatibility with Qt 5.12 (#5109)
|
||||
|
||||
|
||||
# Release 3.3.3 - (January 25, 2019)
|
||||
|
||||
* Do not expose users to server error messages (#4968)
|
||||
* Notify users of new releases. Release announcements must be signed,
|
||||
and they are verified byElectrum using a hardcoded Bitcoin address.
|
||||
* Hardware wallet fixes (#4991, #4993, #5006)
|
||||
* Display only QR code in QRcode Window
|
||||
* Fixed code signing on MacOS
|
||||
* Randomise locktime of transactions
|
||||
|
||||
|
||||
# Release 3.3.2 - (December 21, 2018)
|
||||
|
||||
* Fix Qt history export bug
|
||||
* Improve network timeouts
|
||||
* Prepend server transaction_broadcast error messages with
|
||||
explanatory message. Render error messages as plain text.
|
||||
|
||||
|
||||
# Release 3.3.1 - (December 20, 2018)
|
||||
|
||||
* Qt: Fix invoices tab crash (#4941)
|
||||
* Android: Minor GUI improvements
|
||||
|
||||
|
||||
# Release 3.3.0 - Hodler's Edition (December 19, 2018)
|
||||
|
||||
* The network layer has been rewritten using asyncio and aiorpcx.
|
||||
In addition to easier maintenance, this makes the client
|
||||
more robust against misbehaving servers.
|
||||
* The minimum python version was increased to 3.6
|
||||
* The blockchain headers and fork handling logic has been generalized.
|
||||
Clients by default now follow chain based on most work, not length.
|
||||
* New wallet creation defaults to native segwit (bech32).
|
||||
* Segwit 2FA: TrustedCoin now supports native segwit p2wsh
|
||||
two-factor wallets.
|
||||
* RBF batching (opt-in): If the wallet has an unconfirmed RBF
|
||||
transaction, new payments will be added to that transaction,
|
||||
instead of creating new transactions.
|
||||
* MacOS: support QR code scanner in binaries.
|
||||
* Android APK:
|
||||
- build using Google NDK instead of Crystax NDK
|
||||
- target API 28
|
||||
- do not use external storage (previously for block headers)
|
||||
* hardware wallets:
|
||||
- Coldcard now supports spending from p2wpkh-p2sh,
|
||||
fixed p2pkh signing for fw 1.1.0
|
||||
- Archos Safe-T mini: fix #4726 signing issue
|
||||
- KeepKey: full segwit support
|
||||
- Trezor: refactoring and compat with python-trezor 0.11
|
||||
- Digital BitBox: support firmware v5.0.0
|
||||
* fix bitcoin URI handling when app already running (#4796)
|
||||
* Qt listings rewritten:
|
||||
the History tab now uses QAbstractItemModel, the other tabs use
|
||||
QStandardItemModel. Performance should be better for large wallets.
|
||||
* Several other minor bugfixes and usability improvements.
|
||||
|
||||
|
||||
# Release 3.2.3 - (September 3, 2018)
|
||||
|
||||
* hardware wallet: the Safe-T mini from Archos is now supported.
|
||||
* hardware wallet: the Coldcard from Coinkite is now supported.
|
||||
* BIP39 seeds: if a seed extension (aka passphrase) contained
|
||||
multiple consecutive whitespaces or leading/trailing whitespaces
|
||||
then the derived addresses were not following spec. This has been
|
||||
fixed, and affected should move their coins. The wizard will show a
|
||||
warning in this case. (#4566)
|
||||
* Revealer: the PRNG used has been changed (#4649)
|
||||
* fix Linux distributables: 'typing' was not bundled, needed for python 3.4
|
||||
* fix #4626: fix spending from segwit multisig wallets involving a Trezor
|
||||
cosigner when using a custom derivation path
|
||||
* fix #4491: on Android, if user had set "uBTC" as base unit, app crashed
|
||||
* fix #4497: on Android, paying bip70 invoices from cold start did not work
|
||||
* Several other minor bugfixes and usability improvements.
|
||||
|
||||
|
||||
# Release 3.2.2 - (July 2nd, 2018)
|
||||
|
||||
* Fix DNS resolution on Windows
|
||||
@ -205,7 +297,7 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||
* Qt GUI: sweeping now uses the Send tab, allowing fees to be set
|
||||
* Windows: if using the installer binary, there is now a separate shortcut
|
||||
for "Electrum Testnet"
|
||||
* Digital Bitbox: added suport for p2sh-segwit
|
||||
* Digital Bitbox: added support for p2sh-segwit
|
||||
* OS notifications for incoming transactions
|
||||
* better transaction size estimation:
|
||||
- fees for segwit txns were somewhat underestimated (#3347)
|
||||
@ -433,7 +525,7 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||
|
||||
# Release 2.7.7
|
||||
* Fix utf8 encoding bug with old wallet seeds (issue #1967)
|
||||
* Fix delete request from menu (isue #1968)
|
||||
* Fix delete request from menu (issue #1968)
|
||||
|
||||
# Release 2.7.6
|
||||
* Fixes a critical bug with imported private keys (issue #1966). Keys
|
||||
@ -796,7 +888,7 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||
* New 'Receive' tab in the GUI:
|
||||
- create and manage payment requests, with QR Codes
|
||||
- the former 'Receive' tab was renamed to 'Addresses'
|
||||
- the former Point of Sale plugin is replaced by a resizeable
|
||||
- the former Point of Sale plugin is replaced by a resizable
|
||||
window that pops up if you click on the QR code
|
||||
|
||||
* The 'Send' tab in the Qt GUI supports transactions with multiple
|
||||
@ -819,7 +911,7 @@ issue #3374. Users should upgrade to 3.0.5.
|
||||
|
||||
* The client accepts servers with a CA-signed SSL certificate.
|
||||
|
||||
* ECIES encrypt/decrypt methods, availabe in the GUI and using
|
||||
* ECIES encrypt/decrypt methods, available in the GUI and using
|
||||
the command line:
|
||||
encrypt <pubkey> <message>
|
||||
decrypt <pubkey> <message>
|
||||
@ -892,7 +984,7 @@ bugfixes: connection problems, transactions staying unverified
|
||||
|
||||
# Release 1.8.1
|
||||
|
||||
* Notification option when receiving new tranactions
|
||||
* Notification option when receiving new transactions
|
||||
* Confirm dialogue before sending large amounts
|
||||
* Alternative datafile location for non-windows systems
|
||||
* Fix offline wallet creation
|
||||
|
||||
20
contrib/build-linux/README.md
Normal file
@ -0,0 +1,20 @@
|
||||
Source tarballs
|
||||
===============
|
||||
|
||||
1. Build locale files
|
||||
|
||||
```
|
||||
contrib/make_locale
|
||||
```
|
||||
|
||||
2. Prepare python dependencies used by Electrum.
|
||||
|
||||
```
|
||||
contrib/make_packages
|
||||
```
|
||||
|
||||
3. Create source tarball.
|
||||
|
||||
```
|
||||
contrib/make_tgz
|
||||
```
|
||||
25
contrib/build-linux/appimage/Dockerfile
Normal file
@ -0,0 +1,25 @@
|
||||
FROM ubuntu:14.04@sha256:cac55e5d97fad634d954d00a5c2a56d80576a08dcc01036011f26b88263f1578
|
||||
|
||||
ENV LC_ALL=C.UTF-8 LANG=C.UTF-8
|
||||
|
||||
RUN apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
git \
|
||||
wget \
|
||||
make \
|
||||
autotools-dev \
|
||||
autoconf \
|
||||
libtool \
|
||||
xz-utils \
|
||||
libssl-dev \
|
||||
zlib1g-dev \
|
||||
libffi6 \
|
||||
libffi-dev \
|
||||
libusb-1.0-0-dev \
|
||||
libudev-dev \
|
||||
gettext \
|
||||
libzbar0 \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean
|
||||
41
contrib/build-linux/appimage/README.md
Normal file
@ -0,0 +1,41 @@
|
||||
AppImage binary for Electrum
|
||||
============================
|
||||
|
||||
This assumes an Ubuntu host, but it should not be too hard to adapt to another
|
||||
similar system. The docker commands should be executed in the project's root
|
||||
folder.
|
||||
|
||||
1. Install Docker
|
||||
|
||||
```
|
||||
$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
|
||||
$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install -y docker-ce
|
||||
```
|
||||
|
||||
2. Build image
|
||||
|
||||
```
|
||||
$ sudo docker build --no-cache -t electrum-appimage-builder-img contrib/build-linux/appimage
|
||||
```
|
||||
|
||||
3. Build binary
|
||||
|
||||
```
|
||||
$ sudo docker run -it \
|
||||
--name electrum-appimage-builder-cont \
|
||||
-v $PWD:/opt/electrum \
|
||||
--rm \
|
||||
--workdir /opt/electrum/contrib/build-linux/appimage \
|
||||
electrum-appimage-builder-img \
|
||||
./build.sh
|
||||
```
|
||||
|
||||
4. The generated binary is in `./dist`.
|
||||
|
||||
|
||||
## FAQ
|
||||
|
||||
### How can I see what is included in the AppImage?
|
||||
Execute the binary as follows: `./electrum*.AppImage --appimage-extract`
|
||||
11
contrib/build-linux/appimage/apprun.sh
Executable file
@ -0,0 +1,11 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
APPDIR="$(dirname "$(readlink -e "$0")")"
|
||||
|
||||
export LD_LIBRARY_PATH="${APPDIR}/usr/lib/:${APPDIR}/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}"
|
||||
export PATH="${APPDIR}/usr/bin:${PATH}"
|
||||
export LDFLAGS="-L${APPDIR}/usr/lib/x86_64-linux-gnu -L${APPDIR}/usr/lib"
|
||||
|
||||
exec "${APPDIR}/usr/bin/python3.6" -s "${APPDIR}/usr/bin/electrum" "$@"
|
||||
197
contrib/build-linux/appimage/build.sh
Executable file
@ -0,0 +1,197 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
PROJECT_ROOT="$(dirname "$(readlink -e "$0")")/../../.."
|
||||
CONTRIB="$PROJECT_ROOT/contrib"
|
||||
DISTDIR="$PROJECT_ROOT/dist"
|
||||
BUILDDIR="$CONTRIB/build-linux/appimage/build/appimage"
|
||||
APPDIR="$BUILDDIR/electrum.AppDir"
|
||||
CACHEDIR="$CONTRIB/build-linux/appimage/.cache/appimage"
|
||||
|
||||
# pinned versions
|
||||
PYTHON_VERSION=3.6.8
|
||||
PKG2APPIMAGE_COMMIT="83483c2971fcaa1cb0c1253acd6c731ef8404381"
|
||||
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
|
||||
|
||||
|
||||
VERSION=`git describe --tags --dirty --always`
|
||||
APPIMAGE="$DISTDIR/electrum-$VERSION-x86_64.AppImage"
|
||||
|
||||
rm -rf "$BUILDDIR"
|
||||
mkdir -p "$APPDIR" "$CACHEDIR" "$DISTDIR"
|
||||
|
||||
|
||||
. "$CONTRIB"/build_tools_util.sh
|
||||
|
||||
|
||||
info "downloading some dependencies."
|
||||
download_if_not_exist "$CACHEDIR/functions.sh" "https://raw.githubusercontent.com/AppImage/pkg2appimage/$PKG2APPIMAGE_COMMIT/functions.sh"
|
||||
verify_hash "$CACHEDIR/functions.sh" "a73a21a6c1d1e15c0a9f47f017ae833873d1dc6aa74a4c840c0b901bf1dcf09c"
|
||||
|
||||
download_if_not_exist "$CACHEDIR/appimagetool" "https://github.com/probonopd/AppImageKit/releases/download/11/appimagetool-x86_64.AppImage"
|
||||
verify_hash "$CACHEDIR/appimagetool" "c13026b9ebaa20a17e7e0a4c818a901f0faba759801d8ceab3bb6007dde00372"
|
||||
|
||||
download_if_not_exist "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "https://www.python.org/ftp/python/$PYTHON_VERSION/Python-$PYTHON_VERSION.tar.xz"
|
||||
verify_hash "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" "35446241e995773b1bed7d196f4b624dadcadc8429f26282e756b2fb8a351193"
|
||||
|
||||
|
||||
|
||||
info "building python."
|
||||
tar xf "$CACHEDIR/Python-$PYTHON_VERSION.tar.xz" -C "$BUILDDIR"
|
||||
(
|
||||
cd "$BUILDDIR/Python-$PYTHON_VERSION"
|
||||
export SOURCE_DATE_EPOCH=1530212462
|
||||
./configure \
|
||||
--cache-file="$CACHEDIR/python.config.cache" \
|
||||
--prefix="$APPDIR/usr" \
|
||||
--enable-ipv6 \
|
||||
--enable-shared \
|
||||
--with-threads \
|
||||
-q
|
||||
make -s
|
||||
make -s install > /dev/null
|
||||
)
|
||||
|
||||
|
||||
info "building libsecp256k1."
|
||||
(
|
||||
git clone https://github.com/bitcoin-core/secp256k1 "$CACHEDIR"/secp256k1 || (cd "$CACHEDIR"/secp256k1 && git pull)
|
||||
cd "$CACHEDIR"/secp256k1
|
||||
git reset --hard "$LIBSECP_VERSION"
|
||||
git clean -f -x -q
|
||||
export SOURCE_DATE_EPOCH=1530212462
|
||||
./autogen.sh
|
||||
echo "LDFLAGS = -no-undefined" >> Makefile.am
|
||||
./configure \
|
||||
--prefix="$APPDIR/usr" \
|
||||
--enable-module-recovery \
|
||||
--enable-experimental \
|
||||
--enable-module-ecdh \
|
||||
--disable-jni \
|
||||
-q
|
||||
make -s
|
||||
make -s install > /dev/null
|
||||
)
|
||||
|
||||
|
||||
appdir_python() {
|
||||
env \
|
||||
PYTHONNOUSERSITE=1 \
|
||||
LD_LIBRARY_PATH="$APPDIR/usr/lib:$APPDIR/usr/lib/x86_64-linux-gnu${LD_LIBRARY_PATH+:$LD_LIBRARY_PATH}" \
|
||||
"$APPDIR/usr/bin/python3.6" "$@"
|
||||
}
|
||||
|
||||
python='appdir_python'
|
||||
|
||||
|
||||
info "installing pip."
|
||||
"$python" -m ensurepip
|
||||
|
||||
|
||||
info "preparing electrum-locale."
|
||||
(
|
||||
cd "$PROJECT_ROOT"
|
||||
git submodule update --init
|
||||
|
||||
pushd "$CONTRIB"/deterministic-build/electrum-locale
|
||||
if ! which msgfmt > /dev/null 2>&1; then
|
||||
echo "Please install gettext"
|
||||
exit 1
|
||||
fi
|
||||
for i in ./locale/*; do
|
||||
dir="$PROJECT_ROOT/electrum/$i/LC_MESSAGES"
|
||||
mkdir -p $dir
|
||||
msgfmt --output-file="$dir/electrum.mo" "$i/electrum.po" || true
|
||||
done
|
||||
popd
|
||||
)
|
||||
|
||||
|
||||
info "installing electrum and its dependencies."
|
||||
mkdir -p "$CACHEDIR/pip_cache"
|
||||
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements.txt"
|
||||
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-binaries.txt"
|
||||
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" -r "$CONTRIB/deterministic-build/requirements-hw.txt"
|
||||
"$python" -m pip install --cache-dir "$CACHEDIR/pip_cache" "$PROJECT_ROOT"
|
||||
|
||||
|
||||
info "copying zbar"
|
||||
cp "/usr/lib/libzbar.so.0" "$APPDIR/usr/lib/libzbar.so.0"
|
||||
|
||||
|
||||
info "desktop integration."
|
||||
cp "$PROJECT_ROOT/electrum.desktop" "$APPDIR/electrum.desktop"
|
||||
cp "$PROJECT_ROOT/electrum/gui/icons/electrum.png" "$APPDIR/electrum.png"
|
||||
|
||||
|
||||
# add launcher
|
||||
cp "$CONTRIB/build-linux/appimage/apprun.sh" "$APPDIR/AppRun"
|
||||
|
||||
info "finalizing AppDir."
|
||||
(
|
||||
export PKG2AICOMMIT="$PKG2APPIMAGE_COMMIT"
|
||||
. "$CACHEDIR/functions.sh"
|
||||
|
||||
cd "$APPDIR"
|
||||
# copy system dependencies
|
||||
# note: temporarily move PyQt5 out of the way so
|
||||
# we don't try to bundle its system dependencies.
|
||||
mv "$APPDIR/usr/lib/python3.6/site-packages/PyQt5" "$BUILDDIR"
|
||||
copy_deps; copy_deps; copy_deps
|
||||
move_lib
|
||||
mv "$BUILDDIR/PyQt5" "$APPDIR/usr/lib/python3.6/site-packages"
|
||||
|
||||
# apply global appimage blacklist to exclude stuff
|
||||
# move usr/include out of the way to preserve usr/include/python3.6m.
|
||||
mv usr/include usr/include.tmp
|
||||
delete_blacklisted
|
||||
mv usr/include.tmp usr/include
|
||||
)
|
||||
|
||||
|
||||
info "stripping binaries from debug symbols."
|
||||
strip_binaries()
|
||||
{
|
||||
chmod u+w -R "$APPDIR"
|
||||
{
|
||||
printf '%s\0' "$APPDIR/usr/bin/python3.6"
|
||||
find "$APPDIR" -type f -regex '.*\.so\(\.[0-9.]+\)?$' -print0
|
||||
} | xargs -0 --no-run-if-empty --verbose -n1 strip
|
||||
}
|
||||
strip_binaries
|
||||
|
||||
remove_emptydirs()
|
||||
{
|
||||
find "$APPDIR" -type d -empty -print0 | xargs -0 --no-run-if-empty rmdir -vp --ignore-fail-on-non-empty
|
||||
}
|
||||
remove_emptydirs
|
||||
|
||||
|
||||
info "removing some unneeded stuff to decrease binary size."
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/test
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/translations/qtwebengine_locales
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/resources/qtwebengine_*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/qml
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Web*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Designer*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Qml*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Quick*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Location*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Test*
|
||||
rm -rf "$APPDIR"/usr/lib/python3.6/site-packages/PyQt5/Qt/lib/libQt5Xml*
|
||||
|
||||
|
||||
info "creating the AppImage."
|
||||
(
|
||||
cd "$BUILDDIR"
|
||||
chmod +x "$CACHEDIR/appimagetool"
|
||||
"$CACHEDIR/appimagetool" --appimage-extract
|
||||
env VERSION="$VERSION" ./squashfs-root/AppRun --no-appstream --verbose "$APPDIR" "$APPIMAGE"
|
||||
)
|
||||
|
||||
|
||||
info "done."
|
||||
ls -la "$DISTDIR"
|
||||
sha256sum "$DISTDIR"/*
|
||||
@ -1,36 +0,0 @@
|
||||
Building Mac OS binaries
|
||||
========================
|
||||
|
||||
This guide explains how to build Electrum binaries for macOS systems.
|
||||
|
||||
The build process consists of two steps:
|
||||
|
||||
## 1. Building the binary
|
||||
|
||||
This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it on High Sierra
|
||||
makes the binaries incompatible with older versions.
|
||||
|
||||
Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`).
|
||||
|
||||
|
||||
cd electrum
|
||||
./contrib/build-osx/make_osx
|
||||
|
||||
This creates a folder named Electrum.app.
|
||||
|
||||
## 2. Building the image
|
||||
The usual way to distribute macOS applications is to use image files containing the
|
||||
application. Although these images can be created on a Mac with the built-in `hdiutil`,
|
||||
they are not deterministic.
|
||||
|
||||
Instead, we use the toolchain that Bitcoin uses: genisoimage and libdmg-hfsplus.
|
||||
These tools do not work on macOS, so you need a separate Linux machine (or VM).
|
||||
|
||||
Copy the Electrum.app directory over and install the dependencies, e.g.:
|
||||
|
||||
apt install libcap-dev cmake make gcc faketime
|
||||
|
||||
Then you can just invoke `package.sh` with the path to the app:
|
||||
|
||||
cd electrum
|
||||
./contrib/build-osx/package.sh ~/Electrum.app/
|
||||
@ -1,12 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0,34m'
|
||||
NC='\033[0m' # No Color
|
||||
function info {
|
||||
printf "\r💬 ${BLUE}INFO:${NC} ${1}\n"
|
||||
}
|
||||
function fail {
|
||||
printf "\r🗯 ${RED}ERROR:${NC} ${1}\n"
|
||||
exit 1
|
||||
}
|
||||
@ -1,94 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Parameterize
|
||||
PYTHON_VERSION=3.6.4
|
||||
BUILDDIR=/tmp/electrum-build
|
||||
PACKAGE=Electrum
|
||||
GIT_REPO=https://github.com/spesmilo/electrum
|
||||
LIBSECP_VERSION=452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4
|
||||
|
||||
. $(dirname "$0")/base.sh
|
||||
|
||||
src_dir=$(dirname "$0")
|
||||
cd $src_dir/../..
|
||||
|
||||
export PYTHONHASHSEED=22
|
||||
VERSION=`git describe --tags --dirty`
|
||||
|
||||
which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue"
|
||||
|
||||
info "Installing Python $PYTHON_VERSION"
|
||||
export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH"
|
||||
if [ -d "~/.pyenv" ]; then
|
||||
pyenv update
|
||||
else
|
||||
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash > /dev/null 2>&1
|
||||
fi
|
||||
PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -s $PYTHON_VERSION && \
|
||||
pyenv global $PYTHON_VERSION || \
|
||||
fail "Unable to use Python $PYTHON_VERSION"
|
||||
|
||||
|
||||
info "Installing pyinstaller"
|
||||
python3 -m pip install git+https://github.com/ecdsa/pyinstaller@fix_2952 -I --user || fail "Could not install pyinstaller"
|
||||
|
||||
info "Using these versions for building $PACKAGE:"
|
||||
sw_vers
|
||||
python3 --version
|
||||
echo -n "Pyinstaller "
|
||||
pyinstaller --version
|
||||
|
||||
rm -rf ./dist
|
||||
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
rm -rf $BUILDDIR > /dev/null 2>&1
|
||||
mkdir $BUILDDIR
|
||||
|
||||
cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./lib/locale/
|
||||
cp ./contrib/deterministic-build/electrum-icons/icons_rc.py ./gui/qt/
|
||||
|
||||
|
||||
info "Downloading libusb..."
|
||||
curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \
|
||||
tar xz --directory $BUILDDIR
|
||||
cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/build-osx
|
||||
|
||||
info "Building libsecp256k1"
|
||||
brew install autoconf automake libtool
|
||||
git clone https://github.com/bitcoin-core/secp256k1 $BUILDDIR/secp256k1
|
||||
pushd $BUILDDIR/secp256k1
|
||||
git reset --hard $LIBSECP_VERSION
|
||||
git clean -f -x -q
|
||||
./autogen.sh
|
||||
./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni
|
||||
make
|
||||
popd
|
||||
cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/build-osx
|
||||
|
||||
|
||||
info "Installing requirements..."
|
||||
python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user && \
|
||||
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user || \
|
||||
fail "Could not install requirements"
|
||||
|
||||
info "Installing hardware wallet requirements..."
|
||||
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \
|
||||
fail "Could not install hardware wallet requirements"
|
||||
|
||||
info "Building $PACKAGE..."
|
||||
python3 setup.py install --user > /dev/null || fail "Could not build $PACKAGE"
|
||||
|
||||
info "Faking timestamps..."
|
||||
for d in ~/Library/Python/ ~/.pyenv .; do
|
||||
pushd $d
|
||||
find . -exec touch -t '200101220000' {} +
|
||||
popd
|
||||
done
|
||||
|
||||
info "Building binary"
|
||||
pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/build-osx/osx.spec || fail "Could not build binary"
|
||||
|
||||
info "Creating .DMG"
|
||||
hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG"
|
||||
@ -1,97 +0,0 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
PACKAGE='Electrum'
|
||||
PYPKG='electrum'
|
||||
MAIN_SCRIPT='electrum'
|
||||
ICONS_FILE='electrum.icns'
|
||||
|
||||
for i, x in enumerate(sys.argv):
|
||||
if x == '--name':
|
||||
VERSION = sys.argv[i+1]
|
||||
break
|
||||
else:
|
||||
raise Exception('no version')
|
||||
|
||||
electrum = os.path.abspath(".") + "/"
|
||||
block_cipher = None
|
||||
|
||||
# see https://github.com/pyinstaller/pyinstaller/issues/2005
|
||||
hiddenimports = []
|
||||
hiddenimports += collect_submodules('trezorlib')
|
||||
hiddenimports += collect_submodules('btchip')
|
||||
hiddenimports += collect_submodules('keepkeylib')
|
||||
hiddenimports += collect_submodules('websocket')
|
||||
|
||||
datas = [
|
||||
(electrum+'lib/*.json', PYPKG),
|
||||
(electrum+'lib/wordlist/english.txt', PYPKG + '/wordlist'),
|
||||
(electrum+'lib/locale', PYPKG + '/locale'),
|
||||
(electrum+'plugins', PYPKG + '_plugins'),
|
||||
]
|
||||
datas += collect_data_files('trezorlib')
|
||||
datas += collect_data_files('btchip')
|
||||
datas += collect_data_files('keepkeylib')
|
||||
|
||||
# Add libusb so Trezor will work
|
||||
binaries = [(electrum + "contrib/build-osx/libusb-1.0.dylib", ".")]
|
||||
binaries += [(electrum + "contrib/build-osx/libsecp256k1.0.dylib", ".")]
|
||||
|
||||
# Workaround for "Retro Look":
|
||||
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
|
||||
|
||||
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
|
||||
a = Analysis([electrum+MAIN_SCRIPT,
|
||||
electrum+'gui/qt/main_window.py',
|
||||
electrum+'gui/text.py',
|
||||
electrum+'lib/util.py',
|
||||
electrum+'lib/wallet.py',
|
||||
electrum+'lib/simple_config.py',
|
||||
electrum+'lib/bitcoin.py',
|
||||
electrum+'lib/dnssec.py',
|
||||
electrum+'lib/commands.py',
|
||||
electrum+'plugins/cosigner_pool/qt.py',
|
||||
electrum+'plugins/email_requests/qt.py',
|
||||
electrum+'plugins/trezor/client.py',
|
||||
electrum+'plugins/trezor/qt.py',
|
||||
electrum+'plugins/keepkey/qt.py',
|
||||
electrum+'plugins/ledger/qt.py',
|
||||
],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[])
|
||||
|
||||
# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
name=PACKAGE,
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
icon=electrum+ICONS_FILE,
|
||||
console=False)
|
||||
|
||||
app = BUNDLE(exe,
|
||||
version = VERSION,
|
||||
name=PACKAGE + '.app',
|
||||
icon=electrum+ICONS_FILE,
|
||||
bundle_identifier=None,
|
||||
info_plist={
|
||||
'NSHighResolutionCapable': 'True',
|
||||
'NSSupportsAutomaticGraphicsSwitching': 'True'
|
||||
}
|
||||
)
|
||||
@ -2,7 +2,8 @@ Windows Binary Builds
|
||||
=====================
|
||||
|
||||
These scripts can be used for cross-compilation of Windows Electrum executables from Linux/Wine.
|
||||
Produced binaries are deterministic, so you should be able to generate binaries that match the official releases.
|
||||
|
||||
For reproducible builds, see the `docker` folder.
|
||||
|
||||
|
||||
Usage:
|
||||
@ -34,49 +35,3 @@ The binaries are also built by Travis CI, so if you are having problems,
|
||||
2. Make sure `/opt` is writable by the current user.
|
||||
3. Run `build.sh`.
|
||||
4. The generated binaries are in `./dist`.
|
||||
|
||||
|
||||
Code Signing
|
||||
============
|
||||
|
||||
Electrum Windows builds are signed with a Microsoft Authenticode™ code signing
|
||||
certificate in addition to the GPG-based signatures.
|
||||
|
||||
The advantage of using Authenticode is that Electrum users won't receive a
|
||||
Windows SmartScreen warning when starting it.
|
||||
|
||||
The release signing procedure involves a signer (the holder of the
|
||||
certificate/key) and one or multiple trusted verifiers:
|
||||
|
||||
|
||||
| Signer | Verifier |
|
||||
|-----------------------------------------------------------|-----------------------------------|
|
||||
| Build .exe files using `build.sh` | |
|
||||
| Sign .exe with `./sign.sh` | |
|
||||
| Upload signed files to download server | |
|
||||
| | Build .exe files using `build.sh` |
|
||||
| | Compare files using `unsign.sh` |
|
||||
| | Sign .exe file using `gpg -b` |
|
||||
|
||||
| Signer and verifiers:
|
||||
| Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc` |
|
||||
|
||||
|
||||
|
||||
|
||||
Verify Integrity of signed binary
|
||||
=================================
|
||||
|
||||
Every user can verify that the official binary was created from the source code in this
|
||||
repository. To do so, the Authenticode signature needs to be stripped since the signature
|
||||
is not reproducible.
|
||||
|
||||
This procedure removes the differences between the signed and unsigned binary:
|
||||
|
||||
1. Remove the signature from the signed binary using osslsigncode or signtool.
|
||||
2. Set the COFF image checksum for the signed binary to 0x0. This is necessary
|
||||
because pyinstaller doesn't generate a checksum.
|
||||
3. Append null bytes to the _unsigned_ binary until the byte count is a multiple
|
||||
of 8.
|
||||
|
||||
The script `unsign.sh` performs these steps.
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
#!/bin/bash
|
||||
|
||||
NAME_ROOT=electrum
|
||||
PYTHON_VERSION=3.5.4
|
||||
|
||||
# These settings probably don't need any change
|
||||
export WINEPREFIX=/opt/wine64
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
export PYTHONHASHSEED=22
|
||||
|
||||
PYHOME=c:/python$PYTHON_VERSION
|
||||
PYHOME=c:/python3
|
||||
PYTHON="wine $PYHOME/python.exe -OO -B"
|
||||
|
||||
|
||||
@ -19,29 +18,13 @@ set -e
|
||||
mkdir -p tmp
|
||||
cd tmp
|
||||
|
||||
if [ -d ./electrum ]; then
|
||||
rm ./electrum -rf
|
||||
fi
|
||||
pushd $WINEPREFIX/drive_c/electrum
|
||||
|
||||
git clone https://github.com/spesmilo/electrum -b master
|
||||
|
||||
pushd electrum
|
||||
if [ ! -z "$1" ]; then
|
||||
# a commit/tag/branch was specified
|
||||
if ! git cat-file -e "$1" 2> /dev/null
|
||||
then # can't find target
|
||||
# try pull requests
|
||||
git config --local --add remote.origin.fetch '+refs/pull/*/merge:refs/remotes/origin/pr/*'
|
||||
git fetch --all
|
||||
fi
|
||||
git checkout $1
|
||||
fi
|
||||
|
||||
# Load electrum-icons and electrum-locale for this release
|
||||
# Load electrum-locale for this release
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
VERSION=`git describe --tags --dirty`
|
||||
VERSION=`git describe --tags --dirty --always`
|
||||
echo "Last commit: $VERSION"
|
||||
|
||||
pushd ./contrib/deterministic-build/electrum-locale
|
||||
@ -50,7 +33,7 @@ if ! which msgfmt > /dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
for i in ./locale/*; do
|
||||
dir=$i/LC_MESSAGES
|
||||
dir=$WINEPREFIX/drive_c/electrum/electrum/$i/LC_MESSAGES
|
||||
mkdir -p $dir
|
||||
msgfmt --output-file=$dir/electrum.mo $i/electrum.po || true
|
||||
done
|
||||
@ -59,11 +42,7 @@ popd
|
||||
find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
|
||||
popd
|
||||
|
||||
rm -rf $WINEPREFIX/drive_c/electrum
|
||||
cp -r electrum $WINEPREFIX/drive_c/electrum
|
||||
cp electrum/LICENCE .
|
||||
cp -r ./electrum/contrib/deterministic-build/electrum-locale/locale $WINEPREFIX/drive_c/electrum/lib/
|
||||
cp ./electrum/contrib/deterministic-build/electrum-icons/icons_rc.py $WINEPREFIX/drive_c/electrum/gui/qt/
|
||||
cp $WINEPREFIX/drive_c/electrum/LICENCE .
|
||||
|
||||
# Install frozen dependencies
|
||||
$PYTHON -m pip install -r ../../deterministic-build/requirements.txt
|
||||
@ -71,7 +50,7 @@ $PYTHON -m pip install -r ../../deterministic-build/requirements.txt
|
||||
$PYTHON -m pip install -r ../../deterministic-build/requirements-hw.txt
|
||||
|
||||
pushd $WINEPREFIX/drive_c/electrum
|
||||
$PYTHON setup.py install
|
||||
$PYTHON -m pip install .
|
||||
popd
|
||||
|
||||
cd ..
|
||||
@ -79,7 +58,7 @@ cd ..
|
||||
rm -rf dist/
|
||||
|
||||
# build standalone and portable versions
|
||||
wine "C:/python$PYTHON_VERSION/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec
|
||||
wine "$PYHOME/scripts/pyinstaller.exe" --noconfirm --ascii --clean --name $NAME_ROOT-$VERSION -w deterministic.spec
|
||||
|
||||
# set timestamps in dist, in order to make the installer reproducible
|
||||
pushd dist
|
||||
@ -95,4 +74,4 @@ mv electrum-setup.exe $NAME_ROOT-$VERSION-setup.exe
|
||||
cd ..
|
||||
|
||||
echo "Done."
|
||||
md5sum dist/electrum*exe
|
||||
sha256sum dist/electrum*exe
|
||||
|
||||
@ -29,7 +29,8 @@ else
|
||||
git pull
|
||||
fi
|
||||
|
||||
git reset --hard 452d8e4d2a2f9f1b5be6b02e18f1ba102e5ca0b4
|
||||
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
|
||||
git reset --hard "$LIBSECP_VERSION"
|
||||
git clean -f -x -q
|
||||
|
||||
build_dll i686-w64-mingw32 # 64-bit would be: x86_64-w64-mingw32
|
||||
|
||||
@ -2,10 +2,6 @@
|
||||
# Lucky number
|
||||
export PYTHONHASHSEED=22
|
||||
|
||||
if [ ! -z "$1" ]; then
|
||||
to_build="$1"
|
||||
fi
|
||||
|
||||
here=$(dirname "$0")
|
||||
test -n "$here" -a -d "$here" || exit
|
||||
|
||||
@ -28,5 +24,5 @@ find -exec touch -d '2000-11-11T11:11:11+00:00' {} +
|
||||
popd
|
||||
ls -l /opt/wine64/drive_c/python*
|
||||
|
||||
$here/build-electrum-git.sh $to_build && \
|
||||
$here/build-electrum-git.sh && \
|
||||
echo "Done."
|
||||
|
||||
@ -10,17 +10,23 @@ for i, x in enumerate(sys.argv):
|
||||
else:
|
||||
raise Exception('no name')
|
||||
|
||||
PYTHON_VERSION = '3.5.4'
|
||||
PYHOME = 'c:/python' + PYTHON_VERSION
|
||||
PYHOME = 'c:/python3'
|
||||
|
||||
home = 'C:\\electrum\\'
|
||||
|
||||
# see https://github.com/pyinstaller/pyinstaller/issues/2005
|
||||
hiddenimports = []
|
||||
hiddenimports += collect_submodules('trezorlib')
|
||||
hiddenimports += collect_submodules('safetlib')
|
||||
hiddenimports += collect_submodules('btchip')
|
||||
hiddenimports += collect_submodules('keepkeylib')
|
||||
hiddenimports += collect_submodules('websocket')
|
||||
hiddenimports += collect_submodules('ckcc')
|
||||
|
||||
# safetlib imports PyQt5.Qt. We use a local updated copy of pinmatrix.py until they
|
||||
# release a new version that includes https://github.com/archos-safe-t/python-safet/commit/b1eab3dba4c04fdfc1fcf17b66662c28c5f2380e
|
||||
hiddenimports.remove('safetlib.qt.pinmatrix')
|
||||
|
||||
|
||||
# Add libusb binary
|
||||
binaries = [(PYHOME+"/libusb-1.0.dll", ".")]
|
||||
@ -31,32 +37,37 @@ binaries += [b for b in collect_dynamic_libs('PyQt5') if 'qwindowsvista' in b[0]
|
||||
binaries += [('C:/tmp/libsecp256k1.dll', '.')]
|
||||
|
||||
datas = [
|
||||
(home+'lib/*.json', 'electrum'),
|
||||
(home+'lib/wordlist/english.txt', 'electrum/wordlist'),
|
||||
(home+'lib/locale', 'electrum/locale'),
|
||||
(home+'plugins', 'electrum_plugins'),
|
||||
('C:\\Program Files (x86)\\ZBar\\bin\\', '.')
|
||||
(home+'electrum/*.json', 'electrum'),
|
||||
(home+'electrum/wordlist/english.txt', 'electrum/wordlist'),
|
||||
(home+'electrum/locale', 'electrum/locale'),
|
||||
(home+'electrum/plugins', 'electrum/plugins'),
|
||||
('C:\\Program Files (x86)\\ZBar\\bin\\', '.'),
|
||||
(home+'electrum/gui/icons', 'electrum/gui/icons'),
|
||||
]
|
||||
datas += collect_data_files('trezorlib')
|
||||
datas += collect_data_files('safetlib')
|
||||
datas += collect_data_files('btchip')
|
||||
datas += collect_data_files('keepkeylib')
|
||||
datas += collect_data_files('ckcc')
|
||||
|
||||
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
|
||||
a = Analysis([home+'electrum',
|
||||
home+'gui/qt/main_window.py',
|
||||
home+'gui/text.py',
|
||||
home+'lib/util.py',
|
||||
home+'lib/wallet.py',
|
||||
home+'lib/simple_config.py',
|
||||
home+'lib/bitcoin.py',
|
||||
home+'lib/dnssec.py',
|
||||
home+'lib/commands.py',
|
||||
home+'plugins/cosigner_pool/qt.py',
|
||||
home+'plugins/email_requests/qt.py',
|
||||
home+'plugins/trezor/client.py',
|
||||
home+'plugins/trezor/qt.py',
|
||||
home+'plugins/keepkey/qt.py',
|
||||
home+'plugins/ledger/qt.py',
|
||||
a = Analysis([home+'run_electrum',
|
||||
home+'electrum/gui/qt/main_window.py',
|
||||
home+'electrum/gui/text.py',
|
||||
home+'electrum/util.py',
|
||||
home+'electrum/wallet.py',
|
||||
home+'electrum/simple_config.py',
|
||||
home+'electrum/bitcoin.py',
|
||||
home+'electrum/dnssec.py',
|
||||
home+'electrum/commands.py',
|
||||
home+'electrum/plugins/cosigner_pool/qt.py',
|
||||
home+'electrum/plugins/email_requests/qt.py',
|
||||
home+'electrum/plugins/trezor/qt.py',
|
||||
home+'electrum/plugins/safe_t/client.py',
|
||||
home+'electrum/plugins/safe_t/qt.py',
|
||||
home+'electrum/plugins/keepkey/qt.py',
|
||||
home+'electrum/plugins/ledger/qt.py',
|
||||
home+'electrum/plugins/coldcard/qt.py',
|
||||
#home+'packages/requests/utils.py'
|
||||
],
|
||||
binaries=binaries,
|
||||
@ -68,10 +79,28 @@ a = Analysis([home+'electrum',
|
||||
|
||||
# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
|
||||
# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815
|
||||
qt_bins2remove=('qt5web', 'qt53d', 'qt5game', 'qt5designer', 'qt5quick',
|
||||
'qt5location', 'qt5test', 'qt5xml', r'pyqt5\qt\qml\qtquick')
|
||||
print("Removing Qt binaries:", *qt_bins2remove)
|
||||
for x in a.binaries.copy():
|
||||
for r in qt_bins2remove:
|
||||
if x[0].lower().startswith(r):
|
||||
a.binaries.remove(x)
|
||||
print('----> Removed x =', x)
|
||||
|
||||
qt_data2remove=(r'pyqt5\qt\translations\qtwebengine_locales', )
|
||||
print("Removing Qt datas:", *qt_data2remove)
|
||||
for x in a.datas.copy():
|
||||
for r in qt_data2remove:
|
||||
if x[0].lower().startswith(r):
|
||||
a.datas.remove(x)
|
||||
print('----> Removed x =', x)
|
||||
|
||||
# hotfix for #3171 (pre-Win10 binaries)
|
||||
a.binaries = [x for x in a.binaries if not x[1].lower().startswith(r'c:\windows')]
|
||||
|
||||
@ -85,12 +114,12 @@ exe_standalone = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
a.datas,
|
||||
name=os.path.join('build\\pyi.win32\\electrum', cmdline_name + ".exe"),
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
icon=home+'icons/electrum.ico',
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
console=False)
|
||||
# console=True makes an annoying black box pop up, but it does make Electrum output command line commands, with this turned off no output will be given but commands can still be used
|
||||
|
||||
@ -103,7 +132,7 @@ exe_portable = EXE(
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
icon=home+'icons/electrum.ico',
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
console=False)
|
||||
|
||||
#####
|
||||
@ -117,7 +146,7 @@ exe_dependent = EXE(
|
||||
debug=False,
|
||||
strip=None,
|
||||
upx=False,
|
||||
icon=home+'icons/electrum.ico',
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
console=False)
|
||||
|
||||
coll = COLLECT(
|
||||
@ -128,6 +157,6 @@ coll = COLLECT(
|
||||
strip=None,
|
||||
upx=True,
|
||||
debug=False,
|
||||
icon=home+'icons/electrum.ico',
|
||||
icon=home+'electrum/gui/icons/electrum.ico',
|
||||
console=False,
|
||||
name=os.path.join('dist', 'electrum'))
|
||||
|
||||
@ -6,28 +6,36 @@ RUN dpkg --add-architecture i386 && \
|
||||
apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
wget=1.19.4-1ubuntu2.1 \
|
||||
gnupg2=2.2.4-1ubuntu1.1 \
|
||||
dirmngr=2.2.4-1ubuntu1.1 \
|
||||
software-properties-common=0.96.24.32.3 \
|
||||
&& \
|
||||
wget -nc https://dl.winehq.org/wine-builds/Release.key && \
|
||||
apt-key add Release.key && \
|
||||
apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \
|
||||
apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
wine-stable-amd64:amd64=3.0.1~bionic \
|
||||
wine-stable-i386:i386=3.0.1~bionic \
|
||||
wine-stable:amd64=3.0.1~bionic \
|
||||
winehq-stable:amd64=3.0.1~bionic \
|
||||
git=1:2.17.1-1ubuntu0.1 \
|
||||
gnupg2=2.2.4-1ubuntu1.2 \
|
||||
dirmngr=2.2.4-1ubuntu1.2 \
|
||||
python3-software-properties=0.96.24.32.1 \
|
||||
software-properties-common=0.96.24.32.1
|
||||
|
||||
RUN apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
git=1:2.17.1-1ubuntu0.4 \
|
||||
p7zip-full=16.02+dfsg-6 \
|
||||
make=4.1-9.1ubuntu1 \
|
||||
mingw-w64=5.0.3-1 \
|
||||
autotools-dev=20180224.1 \
|
||||
autoconf=2.69-11 \
|
||||
libtool=2.4.6-2 \
|
||||
gettext=0.19.8.1-6 \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/* && \
|
||||
gettext=0.19.8.1-6
|
||||
|
||||
RUN wget -nc https://dl.winehq.org/wine-builds/Release.key && \
|
||||
echo "c51bcb8cc4a12abfbd7c7660eaf90f49674d15e222c262f27e6c96429111b822 Release.key" | sha256sum -c - && \
|
||||
apt-key add Release.key && \
|
||||
wget -nc https://dl.winehq.org/wine-builds/winehq.key && \
|
||||
echo "78b185fabdb323971d13bd329fefc8038e08559aa51c4996de18db0639a51df6 winehq.key" | sha256sum -c - && \
|
||||
apt-key add winehq.key && \
|
||||
apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/ && \
|
||||
apt-get update -q && \
|
||||
apt-get install -qy \
|
||||
wine-stable-amd64:amd64=4.0~bionic \
|
||||
wine-stable-i386:i386=4.0~bionic \
|
||||
wine-stable:amd64=4.0~bionic \
|
||||
winehq-stable:amd64=4.0~bionic
|
||||
|
||||
RUN rm -rf /var/lib/apt/lists/* && \
|
||||
apt-get autoremove -y && \
|
||||
apt-get clean
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
Deterministic Windows binaries with Docker
|
||||
==========================================
|
||||
|
||||
Produced binaries are deterministic, so you should be able to generate
|
||||
binaries that match the official releases.
|
||||
|
||||
This assumes an Ubuntu host, but it should not be too hard to adapt to another
|
||||
similar system. The docker commands should be executed in the project's root
|
||||
folder.
|
||||
@ -17,25 +20,84 @@ folder.
|
||||
2. Build image
|
||||
|
||||
```
|
||||
$ sudo docker build --no-cache -t electrum-wine-builder-img contrib/build-wine/docker
|
||||
$ sudo docker build -t electrum-wine-builder-img contrib/build-wine/docker
|
||||
```
|
||||
|
||||
Note: see [this](https://stackoverflow.com/a/40516974/7499128) if having dns problems
|
||||
|
||||
3. Build Windows binaries
|
||||
|
||||
It's recommended to build from a fresh clone
|
||||
(but you can skip this if reproducibility is not necessary).
|
||||
|
||||
```
|
||||
$ TARGET=master
|
||||
$ sudo docker run \
|
||||
$ FRESH_CLONE=contrib/build-wine/fresh_clone && \
|
||||
rm -rf $FRESH_CLONE && \
|
||||
mkdir -p $FRESH_CLONE && \
|
||||
cd $FRESH_CLONE && \
|
||||
git clone https://github.com/spesmilo/electrum.git && \
|
||||
cd electrum
|
||||
```
|
||||
|
||||
And then build from this directory:
|
||||
```
|
||||
$ git checkout $REV
|
||||
$ sudo docker run -it \
|
||||
--name electrum-wine-builder-cont \
|
||||
-v .:/opt/electrum \
|
||||
-v $PWD:/opt/wine64/drive_c/electrum \
|
||||
--rm \
|
||||
--workdir /opt/electrum/contrib/build-wine \
|
||||
--workdir /opt/wine64/drive_c/electrum/contrib/build-wine \
|
||||
electrum-wine-builder-img \
|
||||
./build.sh $TARGET
|
||||
./build.sh
|
||||
```
|
||||
4. The generated binaries are in `./contrib/build-wine/dist`.
|
||||
|
||||
|
||||
|
||||
Note: the `setup` binary (NSIS installer) is not deterministic yet.
|
||||
|
||||
|
||||
Code Signing
|
||||
============
|
||||
|
||||
Electrum Windows builds are signed with a Microsoft Authenticode™ code signing
|
||||
certificate in addition to the GPG-based signatures.
|
||||
|
||||
The advantage of using Authenticode is that Electrum users won't receive a
|
||||
Windows SmartScreen warning when starting it.
|
||||
|
||||
The release signing procedure involves a signer (the holder of the
|
||||
certificate/key) and one or multiple trusted verifiers:
|
||||
|
||||
|
||||
| Signer | Verifier |
|
||||
|-----------------------------------------------------------|-----------------------------------|
|
||||
| Build .exe files using `build.sh` | |
|
||||
| Sign .exe with `./sign.sh` | |
|
||||
| Upload signed files to download server | |
|
||||
| | Build .exe files using `build.sh` |
|
||||
| | Compare files using `unsign.sh` |
|
||||
| | Sign .exe file using `gpg -b` |
|
||||
|
||||
| Signer and verifiers: |
|
||||
|-----------------------------------------------------------------------------------------------|
|
||||
| Upload signatures to 'electrum-signatures' repo, as `$version/$filename.$builder.asc` |
|
||||
|
||||
|
||||
|
||||
Verify Integrity of signed binary
|
||||
=================================
|
||||
|
||||
Every user can verify that the official binary was created from the source code in this
|
||||
repository. To do so, the Authenticode signature needs to be stripped since the signature
|
||||
is not reproducible.
|
||||
|
||||
This procedure removes the differences between the signed and unsigned binary:
|
||||
|
||||
1. Remove the signature from the signed binary using osslsigncode or signtool.
|
||||
2. Set the COFF image checksum for the signed binary to 0x0. This is necessary
|
||||
because pyinstaller doesn't generate a checksum.
|
||||
3. Append null bytes to the _unsigned_ binary until the byte count is a multiple
|
||||
of 8.
|
||||
|
||||
The script `unsign.sh` performs these steps.
|
||||
|
||||
@ -58,7 +58,7 @@
|
||||
VIAddVersionKey ProductName "${PRODUCT_NAME} Installer"
|
||||
VIAddVersionKey Comments "The installer for ${PRODUCT_NAME}"
|
||||
VIAddVersionKey CompanyName "${PRODUCT_NAME}"
|
||||
VIAddVersionKey LegalCopyright "2013-2016 ${PRODUCT_PUBLISHER}"
|
||||
VIAddVersionKey LegalCopyright "2013-2018 ${PRODUCT_PUBLISHER}"
|
||||
VIAddVersionKey FileDescription "${PRODUCT_NAME} Installer"
|
||||
VIAddVersionKey FileVersion ${PRODUCT_VERSION}
|
||||
VIAddVersionKey ProductVersion ${PRODUCT_VERSION}
|
||||
@ -72,7 +72,7 @@
|
||||
!define MUI_ABORTWARNING
|
||||
!define MUI_ABORTWARNING_TEXT "Are you sure you wish to abort the installation of ${PRODUCT_NAME}?"
|
||||
|
||||
!define MUI_ICON "tmp\electrum\icons\electrum.ico"
|
||||
!define MUI_ICON "c:\electrum\electrum\gui\icons\electrum.ico"
|
||||
|
||||
;--------------------------------
|
||||
;Pages
|
||||
@ -111,7 +111,7 @@ Section
|
||||
|
||||
;Files to pack into the installer
|
||||
File /r "dist\electrum\*.*"
|
||||
File "..\..\icons\electrum.ico"
|
||||
File "c:\electrum\electrum\gui\icons\electrum.ico"
|
||||
|
||||
;Store installation folder
|
||||
WriteRegStr HKCU "Software\${PRODUCT_NAME}" "" $INSTDIR
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Please update these carefully, some versions won't work under Wine
|
||||
NSIS_FILENAME=nsis-3.03-setup.exe
|
||||
NSIS_FILENAME=nsis-3.04-setup.exe
|
||||
NSIS_URL=https://prdownloads.sourceforge.net/nsis/$NSIS_FILENAME?download
|
||||
NSIS_SHA256=bd3b15ab62ec6b0c7a00f46022d441af03277be893326f6fea8e212dc2d77743
|
||||
NSIS_SHA256=4e1db5a7400e348b1b46a4a11b6d9557fd84368e4ad3d4bc4c1be636c89638aa
|
||||
|
||||
ZBAR_FILENAME=zbarw-20121031-setup.exe
|
||||
ZBAR_URL=https://sourceforge.net/projects/zbarw/files/$ZBAR_FILENAME/download
|
||||
@ -13,79 +13,26 @@ LIBUSB_FILENAME=libusb-1.0.22.7z
|
||||
LIBUSB_URL=https://prdownloads.sourceforge.net/project/libusb/libusb-1.0/libusb-1.0.22/$LIBUSB_FILENAME?download
|
||||
LIBUSB_SHA256=671f1a420757b4480e7fadc8313d6fb3cbb75ca00934c417c1efa6e77fb8779b
|
||||
|
||||
PYTHON_VERSION=3.5.4
|
||||
PYTHON_VERSION=3.6.8
|
||||
|
||||
## These settings probably don't need change
|
||||
export WINEPREFIX=/opt/wine64
|
||||
#export WINEARCH='win32'
|
||||
|
||||
PYHOME=c:/python$PYTHON_VERSION
|
||||
PYTHON_FOLDER="python3"
|
||||
PYHOME="c:/$PYTHON_FOLDER"
|
||||
PYTHON="wine $PYHOME/python.exe -OO -B"
|
||||
|
||||
|
||||
# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg
|
||||
verify_signature() {
|
||||
local file=$1 keyring=$2 out=
|
||||
if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) &&
|
||||
echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then
|
||||
return 0
|
||||
else
|
||||
echo "$out" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
verify_hash() {
|
||||
local file=$1 expected_hash=$2
|
||||
actual_hash=$(sha256sum $file | awk '{print $1}')
|
||||
if [ "$actual_hash" == "$expected_hash" ]; then
|
||||
return 0
|
||||
else
|
||||
echo "$file $actual_hash (unexpected hash)" >&2
|
||||
rm "$file"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
download_if_not_exist() {
|
||||
local file_name=$1 url=$2
|
||||
if [ ! -e $file_name ] ; then
|
||||
wget -O $PWD/$file_name "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
|
||||
retry() {
|
||||
local result=0
|
||||
local count=1
|
||||
while [ $count -le 3 ]; do
|
||||
[ $result -ne 0 ] && {
|
||||
echo -e "\nThe command \"$@\" failed. Retrying, $count of 3.\n" >&2
|
||||
}
|
||||
! { "$@"; result=$?; }
|
||||
[ $result -eq 0 ] && break
|
||||
count=$(($count + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
[ $count -gt 3 ] && {
|
||||
echo -e "\nThe command \"$@\" failed 3 times.\n" >&2
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
# Let's begin!
|
||||
here=$(dirname $(readlink -e $0))
|
||||
here="$(dirname "$(readlink -e "$0")")"
|
||||
set -e
|
||||
|
||||
# Clean up Wine environment
|
||||
echo "Cleaning $WINEPREFIX"
|
||||
rm -rf $WINEPREFIX
|
||||
echo "done"
|
||||
. $here/../build_tools_util.sh
|
||||
|
||||
wine 'wineboot'
|
||||
|
||||
|
||||
cd /tmp/electrum-build
|
||||
|
||||
# Install Python
|
||||
@ -96,8 +43,7 @@ KEYRING_PYTHON_DEV="keyring-electrum-build-python-dev.gpg"
|
||||
for server in $(shuf -e ha.pool.sks-keyservers.net \
|
||||
hkp://p80.pool.sks-keyservers.net:80 \
|
||||
keyserver.ubuntu.com \
|
||||
hkp://keyserver.ubuntu.com:80 \
|
||||
pgp.mit.edu) ; do
|
||||
hkp://keyserver.ubuntu.com:80) ; do
|
||||
retry gpg --no-default-keyring --keyring $KEYRING_PYTHON_DEV --keyserver "$server" --recv-keys $KEYLIST_PYTHON_DEV \
|
||||
&& break || : ;
|
||||
done
|
||||
@ -106,31 +52,21 @@ for msifile in core dev exe lib pip tools; do
|
||||
wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi"
|
||||
wget -N -c "https://www.python.org/ftp/python/$PYTHON_VERSION/win32/${msifile}.msi.asc"
|
||||
verify_signature "${msifile}.msi.asc" $KEYRING_PYTHON_DEV
|
||||
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=C:/python$PYTHON_VERSION
|
||||
wine msiexec /i "${msifile}.msi" /qb TARGETDIR=$PYHOME
|
||||
done
|
||||
|
||||
# upgrade pip
|
||||
$PYTHON -m pip install pip --upgrade
|
||||
|
||||
# Install pywin32-ctypes (needed by pyinstaller)
|
||||
$PYTHON -m pip install pywin32-ctypes==0.1.2
|
||||
|
||||
# install PySocks
|
||||
$PYTHON -m pip install win_inet_pton==1.0.1
|
||||
|
||||
$PYTHON -m pip install -r $here/../deterministic-build/requirements-binaries.txt
|
||||
# Install dependencies specific to binaries
|
||||
# note that this also installs pinned versions of both pip and setuptools
|
||||
$PYTHON -m pip install -r "$here"/../deterministic-build/requirements-binaries.txt
|
||||
|
||||
# Install PyInstaller
|
||||
$PYTHON -m pip install https://github.com/ecdsa/pyinstaller/archive/fix_2952.zip
|
||||
$PYTHON -m pip install pyinstaller==3.4 --no-use-pep517
|
||||
|
||||
# Install ZBar
|
||||
download_if_not_exist $ZBAR_FILENAME "$ZBAR_URL"
|
||||
verify_hash $ZBAR_FILENAME "$ZBAR_SHA256"
|
||||
wine "$PWD/$ZBAR_FILENAME" /S
|
||||
|
||||
# Upgrade setuptools (so Electrum can be installed later)
|
||||
$PYTHON -m pip install setuptools --upgrade
|
||||
|
||||
# Install NSIS installer
|
||||
download_if_not_exist $NSIS_FILENAME "$NSIS_URL"
|
||||
verify_hash $NSIS_FILENAME "$NSIS_SHA256"
|
||||
@ -140,10 +76,7 @@ download_if_not_exist $LIBUSB_FILENAME "$LIBUSB_URL"
|
||||
verify_hash $LIBUSB_FILENAME "$LIBUSB_SHA256"
|
||||
7z x -olibusb $LIBUSB_FILENAME -aoa
|
||||
|
||||
cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/python$PYTHON_VERSION/
|
||||
|
||||
# add dlls needed for pyinstaller:
|
||||
cp $WINEPREFIX/drive_c/python$PYTHON_VERSION/Lib/site-packages/PyQt5/Qt/bin/* $WINEPREFIX/drive_c/python$PYTHON_VERSION/
|
||||
cp libusb/MS32/dll/libusb-1.0.dll $WINEPREFIX/drive_c/$PYTHON_FOLDER/
|
||||
|
||||
mkdir -p $WINEPREFIX/drive_c/tmp
|
||||
cp secp256k1/libsecp256k1.dll $WINEPREFIX/drive_c/tmp/
|
||||
|
||||
69
contrib/build_tools_util.sh
Executable file
@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
RED='\033[0;31m'
|
||||
BLUE='\033[0;34m'
|
||||
YELLOW='\033[0;33m'
|
||||
NC='\033[0m' # No Color
|
||||
function info {
|
||||
printf "\r💬 ${BLUE}INFO:${NC} ${1}\n"
|
||||
}
|
||||
function fail {
|
||||
printf "\r🗯 ${RED}ERROR:${NC} ${1}\n"
|
||||
exit 1
|
||||
}
|
||||
function warn {
|
||||
printf "\r⚠️ ${YELLOW}WARNING:${NC} ${1}\n"
|
||||
}
|
||||
|
||||
|
||||
# based on https://superuser.com/questions/497940/script-to-verify-a-signature-with-gpg
|
||||
function verify_signature() {
|
||||
local file=$1 keyring=$2 out=
|
||||
if out=$(gpg --no-default-keyring --keyring "$keyring" --status-fd 1 --verify "$file" 2>/dev/null) &&
|
||||
echo "$out" | grep -qs "^\[GNUPG:\] VALIDSIG "; then
|
||||
return 0
|
||||
else
|
||||
echo "$out" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function verify_hash() {
|
||||
local file=$1 expected_hash=$2
|
||||
actual_hash=$(sha256sum $file | awk '{print $1}')
|
||||
if [ "$actual_hash" == "$expected_hash" ]; then
|
||||
return 0
|
||||
else
|
||||
echo "$file $actual_hash (unexpected hash)" >&2
|
||||
rm "$file"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
function download_if_not_exist() {
|
||||
local file_name=$1 url=$2
|
||||
if [ ! -e $file_name ] ; then
|
||||
wget -O $file_name "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
# https://github.com/travis-ci/travis-build/blob/master/lib/travis/build/templates/header.sh
|
||||
function retry() {
|
||||
local result=0
|
||||
local count=1
|
||||
while [ $count -le 3 ]; do
|
||||
[ $result -ne 0 ] && {
|
||||
echo -e "\nThe command \"$@\" failed. Retrying, $count of 3.\n" >&2
|
||||
}
|
||||
! { "$@"; result=$?; }
|
||||
[ $result -eq 0 ] && break
|
||||
count=$(($count + 1))
|
||||
sleep 1
|
||||
done
|
||||
|
||||
[ $count -gt 3 ] && {
|
||||
echo -e "\nThe command \"$@\" failed 3 times.\n" >&2
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
@ -18,13 +18,6 @@ function get_git_mtime {
|
||||
|
||||
fail=0
|
||||
|
||||
for f in icons/* "icons.qrc"; do
|
||||
if (( $(get_git_mtime "$f") > $(get_git_mtime "contrib/deterministic-build/electrum-icons/") )); then
|
||||
echo "Modification time of $f (" $(get_git_mtime --readable "$f") ") is newer than"\
|
||||
"last update of electrum-icons"
|
||||
fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ $(date +%s -d "2 weeks ago") -gt $(get_git_mtime "contrib/deterministic-build/electrum-locale/") ]; then
|
||||
echo "Last update from electrum-locale is older than 2 weeks."\
|
||||
|
||||
@ -1 +0,0 @@
|
||||
Subproject commit 5af76dc1b04c782e622ac409cd802c483c529c1f
|
||||
@ -1 +1 @@
|
||||
Subproject commit de999ceffd2a864df54451d23f290ef5f333e8ea
|
||||
Subproject commit ff5ad3a4436dddcc82799f8a91793013240c3b7b
|
||||
@ -1,7 +1,10 @@
|
||||
#!/usr/bin/env python3
|
||||
import sys
|
||||
|
||||
import requests
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
|
||||
|
||||
|
||||
def check_restriction(p, r):
|
||||
|
||||
@ -1,55 +1,56 @@
|
||||
pip==10.0.1 \
|
||||
--hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \
|
||||
--hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68
|
||||
pycryptodomex==3.6.1 \
|
||||
--hash=sha256:1869d7735f445bbf1681afa2acce10ad829857cfb7a4a7b702e484f222021892 \
|
||||
--hash=sha256:24e054190d2b11ad3b8517d186c0b3df6f902a5f5a91be8e4bb6a3fcdc65b2cf \
|
||||
--hash=sha256:26967d31fabb0d80cb2b254a7c0f55f8dec9931e8676891edd24aa5aaeb0d021 \
|
||||
--hash=sha256:2a341b57bb5844d53b8f632f79277cd534762f502fb73bff5dc1a2f615ff91ed \
|
||||
--hash=sha256:43d6eb014aba7be354f3e8fe2693fe96446f6791da2b9570e8d54d481e3ab224 \
|
||||
--hash=sha256:4c271577f4f8c5cced55a60f4504b34545121c14facb8fc357f89c24089c81fc \
|
||||
--hash=sha256:59721f2853df9cf2265304d3b6d6d8cebe3a86b1fddc00f2bfbf18eb2a48fb78 \
|
||||
--hash=sha256:63a77a1b27d12ed1c42f4e539d9dbe588a88b70ec64b55271cdf1f56c1223bd6 \
|
||||
--hash=sha256:6d04640386c55b9f44015747496c3b6582360b5b3b4e42f9ce3fc7c6840f80d0 \
|
||||
--hash=sha256:730bd75d90e16975a112ea79863ce1faa7703d3b54f10d77656e7dadf6be0ef6 \
|
||||
--hash=sha256:75a300aa86c56e9c19a7b476c397cb22fda3be7af4cf2f105990fdd94c52f486 \
|
||||
--hash=sha256:7c6f67005c6e421f02fd7fe9d95876094307b31628d728adc6c2e038e2ed9c09 \
|
||||
--hash=sha256:82b758f870c8dd859f9b58bc9cff007403b68742f9e0376e2cbd8aa2ad3baa83 \
|
||||
--hash=sha256:8528a958b746c4da767bfba5ac370250dcb741f4c69e55873bd6efe89ac07291 \
|
||||
--hash=sha256:93582ea5bc3e8f95cb36d9dd752c01452085b54b396e3ed775ac1793b8dc486a \
|
||||
--hash=sha256:94e0105ad8d82d3bf5a032c92fc03b01e3bc9ea40b58308c2da42f8cf8c16c47 \
|
||||
--hash=sha256:a65889424bf10a884ff031e7f3fd12273dd5b420ee08ca8fcfd431a2f6cbabc1 \
|
||||
--hash=sha256:a8467982d26bfb90089f50c3c5d9ed541b7fe9f9df20803fede70d5046cd4ff1 \
|
||||
--hash=sha256:ab497d4e7361511ede562ed3cd4528f46c005781bc23b1b943612d27bfb078c3 \
|
||||
--hash=sha256:bb05caf3f6cf41d964c01e08dfaddfe48086c7b3e96708d50647f0a29ff33f56 \
|
||||
--hash=sha256:c4643647f5656855975b2aaf70fe3aa1e0c1558f8d1b5de0c9a8ccac65114c57 \
|
||||
--hash=sha256:c550e20834b679ed0b7608c345a816f97047d2297aab4f4599f95edee5d16e99 \
|
||||
--hash=sha256:cc797712add76cd658110585481c380833637b68df1404190777ba715a81c9b9 \
|
||||
--hash=sha256:dff0c883d495bf45d18acc74938d1de4d6a08b3345acb9177a46c6997a578c44 \
|
||||
--hash=sha256:e4f69af1f5b46255ec7b8116a853879a55e8e6b595a73c39f14ca430c410c469 \
|
||||
--hash=sha256:f61d0d83e9dd974849f9b0826ec20f49dbd9ed233fd90bf2592be1337231418e \
|
||||
--hash=sha256:f65f21d2b616c30ad4ba801504343eb768fd0a2894c5f587e784201320556543
|
||||
PyQt5==5.10.1 \
|
||||
--hash=sha256:1e652910bd1ffd23a3a48c510ecad23a57a853ed26b782cd54b16658e6f271ac \
|
||||
--hash=sha256:4db7113f464c733a99fcb66c4c093a47cf7204ad3f8b3bda502efcc0839ac14b \
|
||||
--hash=sha256:9c17ab3974c1fc7bbb04cc1c9dae780522c0ebc158613f3025fccae82227b5f7 \
|
||||
--hash=sha256:f6035baa009acf45e5f460cf88f73580ad5dc0e72330029acd99e477f20a5d61
|
||||
setuptools==39.2.0 \
|
||||
--hash=sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926 \
|
||||
--hash=sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2
|
||||
SIP==4.19.8 \
|
||||
--hash=sha256:09f9a4e6c28afd0bafedb26ffba43375b97fe7207bd1a0d3513f79b7d168b331 \
|
||||
--hash=sha256:105edaaa1c8aa486662226360bd3999b4b89dd56de3e314d82b83ed0587d8783 \
|
||||
--hash=sha256:1bb10aac55bd5ab0e2ee74b3047aa2016cfa7932077c73f602a6f6541af8cd51 \
|
||||
--hash=sha256:265ddf69235dd70571b7d4da20849303b436192e875ce7226be7144ca702a45c \
|
||||
--hash=sha256:52074f7cb5488e8b75b52f34ec2230bc75d22986c7fe5cd3f2d266c23f3349a7 \
|
||||
--hash=sha256:5ff887a33839de8fc77d7f69aed0259b67a384dc91a1dc7588e328b0b980bde2 \
|
||||
--hash=sha256:74da4ddd20c5b35c19cda753ce1e8e1f71616931391caeac2de7a1715945c679 \
|
||||
--hash=sha256:7d69e9cf4f8253a3c0dfc5ba6bb9ac8087b8239851f22998e98cb35cfe497b68 \
|
||||
--hash=sha256:97bb93ee0ef01ba90f57be2b606e08002660affd5bc380776dd8b0fcaa9e093a \
|
||||
--hash=sha256:cf98150a99e43fda7ae22abe655b6f202e491d6291486548daa56cb15a2fcf85 \
|
||||
--hash=sha256:d9023422127b94d11c1a84bfa94933e959c484f2c79553c1ef23c69fe00d25f8 \
|
||||
--hash=sha256:e72955e12f4fccf27aa421be383453d697b8a44bde2cc26b08d876fd492d0174
|
||||
wheel==0.31.1 \
|
||||
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
|
||||
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
|
||||
pip==19.0.1 \
|
||||
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \
|
||||
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5
|
||||
pycryptodomex==3.7.3 \
|
||||
--hash=sha256:0bda549e20db1eb8e29fb365d10acf84b224d813b1131c828fc830b2ce313dcd \
|
||||
--hash=sha256:1210c0818e5334237b16d99b5785aa0cee815d9997ee258bd5e2936af8e8aa50 \
|
||||
--hash=sha256:2090dc8cd7843eae75bd504b9be86792baa171fc5a758ea3f60188ab67ca95cf \
|
||||
--hash=sha256:22e6784b65dfdd357bf9a8a842db445192b227103e2c3137a28c489c46742135 \
|
||||
--hash=sha256:2edb8c3965a77e3092b5c5c1233ffd32de083f335202013f52d662404191ac79 \
|
||||
--hash=sha256:310fe269ac870135ff610d272e88dcb594ee58f40ac237a688d7c972cbca43e8 \
|
||||
--hash=sha256:456136b7d459f000794a67b23558351c72e21f0c2d4fcaa09fc99dae7844b0ef \
|
||||
--hash=sha256:463e49a9c5f1fa7bd36aff8debae0b5c487868c1fb66704529f2ad7e92f0cc9f \
|
||||
--hash=sha256:4a33b2828799ef8be789a462e6645ea6fe2c42b0df03e6763ccbfd1789c453e6 \
|
||||
--hash=sha256:5ff02dff1b03929e6339226b318aa59bd0b5c362f96e3e0eb7f3401d30594ed3 \
|
||||
--hash=sha256:6b1db8234b8ee2b30435d9e991389c2eeae4d45e09e471ffe757ba1dfae682bb \
|
||||
--hash=sha256:6eb67ee02de143cd19e36a52bd3869a9dc53e9184cd6bed5c39ff71dee2f6a45 \
|
||||
--hash=sha256:6f42eea5afc7eee29494fdfddc6bb7173953d4197d9200e4f67096c2a24bc21b \
|
||||
--hash=sha256:87bc8082e2de2247df7d0b161234f8edb1384294362cc0c8db9324463097578b \
|
||||
--hash=sha256:8df93d34bc0e3a28a27652070164683a07d8a50c628119d6e0f7710f4d01b42f \
|
||||
--hash=sha256:989952c39e8fef1c959f0a0f85656e29c41c01162e33a3f5fd8ce71e47262ae9 \
|
||||
--hash=sha256:a4a203077e2f312ec8677dde80a5c4e6fe5a82a46173a8edc8da668602a3e073 \
|
||||
--hash=sha256:a793c1242dffd39f585ae356344e8935d30f01f6be7d4c62ffc87af376a2f5f9 \
|
||||
--hash=sha256:b70fe991564e178af02ccf89435a8f9e8d052707a7c4b95bf6027cb785da3175 \
|
||||
--hash=sha256:b83594196e3661cb78c97b80a62fbfbba2add459dfd532b58e7a7c62dd06aab4 \
|
||||
--hash=sha256:ba27725237d0a3ea66ec2b6b387259471840908836711a3b215160808dffed0f \
|
||||
--hash=sha256:d1ab8ad1113cdc553ca50c4d5f0142198c317497364c0c70443d69f7ad1c9288 \
|
||||
--hash=sha256:dce039a8a8a318d7af83cae3fd08d58cefd2120075dfac0ae14d706974040f63 \
|
||||
--hash=sha256:e3213037ea33c85ab705579268cbc8a4433357e9fb99ec7ce9fdcc4d4eec1d50 \
|
||||
--hash=sha256:ec8d8023d31ef72026d46e9fb301ff8759eff5336bcf3d1510836375f53f96a9 \
|
||||
--hash=sha256:ece65730d50aa57a1330d86d81582a2d1587b2ca51cb34f586da8551ddc68fee \
|
||||
--hash=sha256:ed21fc515e224727793e4cc3fb3d00f33f59e3a167d3ad6ac1475ab3b05c2f9e \
|
||||
--hash=sha256:eec1132d878153d61a05424f35f089f951bd6095a4f6c60bdd2ef8919d44425e
|
||||
PyQt5==5.11.3 \
|
||||
--hash=sha256:517e4339135c4874b799af0d484bc2e8c27b54850113a68eec40a0b56534f450 \
|
||||
--hash=sha256:ac1eb5a114b6e7788e8be378be41c5e54b17d5158994504e85e43b5fca006a39 \
|
||||
--hash=sha256:d2309296a5a79d0a1c0e6c387c30f0398b65523a6dcc8a19cc172e46b949e00d \
|
||||
--hash=sha256:e85936bae1581bcb908847d2038e5b34237a5e6acc03130099a78930770e7ead
|
||||
PyQt5-sip==4.19.13 \
|
||||
--hash=sha256:125f77c087572c9272219cda030a63c2f996b8507592b2a54d7ef9b75f9f054d \
|
||||
--hash=sha256:14c37b06e3fb7c2234cb208fa461ec4e62b4ba6d8b32ca3753c0b2cfd61b00e3 \
|
||||
--hash=sha256:1cb2cf52979f9085fc0eab7e0b2438eb4430d4aea8edec89762527e17317175b \
|
||||
--hash=sha256:4babef08bccbf223ec34464e1ed0a23caeaeea390ca9a3529227d9a57f0d6ee4 \
|
||||
--hash=sha256:53cb9c1208511cda0b9ed11cffee992a5a2f5d96eb88722569b2ce65ecf6b960 \
|
||||
--hash=sha256:549449d9461d6c665cbe8af4a3808805c5e6e037cd2ce4fd93308d44a049bfac \
|
||||
--hash=sha256:5f5b3089b200ff33de3f636b398e7199b57a6b5c1bb724bdb884580a072a14b5 \
|
||||
--hash=sha256:a4d9bf6e1fa2dd6e73f1873f1a47cee11a6ba0cf9ba8cf7002b28c76823600d0 \
|
||||
--hash=sha256:a4ee6026216f1fbe25c8847f9e0fbce907df5b908f84816e21af16ec7666e6fe \
|
||||
--hash=sha256:a91a308a5e0cc99de1e97afd8f09f46dd7ca20cfaa5890ef254113eebaa1adff \
|
||||
--hash=sha256:b0342540da479d2713edc68fb21f307473f68da896ad5c04215dae97630e0069 \
|
||||
--hash=sha256:f997e21b4e26a3397cb7b255b8d1db5b9772c8e0c94b6d870a5a0ab5c27eacaa
|
||||
setuptools==40.8.0 \
|
||||
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \
|
||||
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab
|
||||
wheel==0.32.3 \
|
||||
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
|
||||
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44
|
||||
|
||||
@ -1,50 +1,56 @@
|
||||
btchip-python==0.1.27 \
|
||||
--hash=sha256:e58a941abbb2d8901bf4858baa18012537c60812c7f895f9a039113ecce3032b
|
||||
certifi==2018.4.16 \
|
||||
--hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 \
|
||||
--hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0
|
||||
btchip-python==0.1.28 \
|
||||
--hash=sha256:da09d0d7a6180d428833795ea9a233c3b317ddfcccea8cc6f0eba59435e5dd83
|
||||
certifi==2018.11.29 \
|
||||
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
|
||||
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033
|
||||
chardet==3.0.4 \
|
||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
|
||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
|
||||
click==6.7 \
|
||||
--hash=sha256:29f99fc6125fbc931b758dc053b3114e55c77a6e4c6c3a2674a2dc986016381d \
|
||||
--hash=sha256:f15516df478d5a56180fbf80e68f206010e6d160fc39fa508b65e035fd75130b
|
||||
Cython==0.28.3 \
|
||||
--hash=sha256:0344e9352b0915910e212c38403b63f902ce1cba75dde7a43a9112ff960eb2a5 \
|
||||
--hash=sha256:0a390c39e912fc5f82d5feae2d16ea061971407099e1efb0fecb255cb96fbeff \
|
||||
--hash=sha256:0f2b2e09f94c498f555935e732b7321b5f62f00e7a789238f6c5ddd66987a54d \
|
||||
--hash=sha256:15614592616b6dd5e919e158796350ebeba6cb6b5d2998cfff41b53f568c8355 \
|
||||
--hash=sha256:1aae6d6e9858888144cea147eb5e677830f45faaff3d305d77378c3cba55f526 \
|
||||
--hash=sha256:200583297f23e558744bc4688d8a2b2605ab6ad7d1494a9fd8c8094ad65ebf3c \
|
||||
--hash=sha256:295facc211a6b55db9979455b856180f2839be22ab767ffdea55986bee83ca9f \
|
||||
--hash=sha256:36c16bf39280fe857213d8da31c07a6179d3878c3dc2e435dce0974b9f8f0729 \
|
||||
--hash=sha256:3fef8dfa9cf86ab7814ca31369374ddd5b9524f54406aa83b53b5937965b8e88 \
|
||||
--hash=sha256:439d233d3214e3d69c033a9a93516758f2c8a03e83ea51ae14b6eed13687d224 \
|
||||
--hash=sha256:455ab39c6c0849a6c008fcdf2fae42475f18d0801a3be229e8f75367bbe3b325 \
|
||||
--hash=sha256:56821e3791209e6a11992e294afbf7e3dcda7d4fd54d06396dd521928d3d14fe \
|
||||
--hash=sha256:62b594584889b33bbea7e71f9d7c5c6539091b341334ef7ca1ae7e30a9dd3e15 \
|
||||
--hash=sha256:70f81a75fb25c1c3c61843e3a6fe771a76c4ebf4d154455a7eff0740ad47dff4 \
|
||||
--hash=sha256:8011090beb09251cb4ece1e14263e574b38eda696b788552b369ad343373d0e9 \
|
||||
--hash=sha256:80d6a0369333a162fc32a22637f5870f3e87fb038c7b58860bbe00b05b58aa62 \
|
||||
--hash=sha256:85b04e32af58a3c008c0ba8169017770aaa342a5972b748f81d043d66363e437 \
|
||||
--hash=sha256:9ed273d82116fa148c92901b9639030e087979d455982bd7bf727fb486c0bd17 \
|
||||
--hash=sha256:a1af59e6c9b4acc07c429d8495fc016a35e0a1270f28c57317352f512df7e214 \
|
||||
--hash=sha256:b894ff4daf8dfaf657bf2d5e7190a4de11b2400b1e0fb0902974d35c23a26dea \
|
||||
--hash=sha256:c2659981150b4de04397dcfd4bff64e384d3ba25af60d1b22820fdf108298cb2 \
|
||||
--hash=sha256:c981a750858f1727995acf861ab030b267d264ca6efda2f01104941187a3675f \
|
||||
--hash=sha256:cc4152b19ec168391f7815d24b70c8911829ba281bd5fcd98cab9dc21abe62ff \
|
||||
--hash=sha256:d0f5b1668e7f7f6fc9849f49a20c5db10562a0ab29cd66818894dfebbca7b304 \
|
||||
--hash=sha256:d7152006ed1a3adb8f978077b57d237ddafa188240af53cd72b5c79e4ed000e3 \
|
||||
--hash=sha256:e5f877472993474296125c22b84c334b550010815e513cccce73da854a132d64 \
|
||||
--hash=sha256:e7c2c87ff2f99ed4be1bb046d6eddfb388af627928037f9e0a420c05daaf14ed \
|
||||
--hash=sha256:edd7d499685655031be5b4d33005096b6345f81eeb7ab9d2dd415db0c7bcf64e \
|
||||
--hash=sha256:f99a777fda569a88deea863eac2722b5e88957c4d5f4413949740da791857ac9
|
||||
ckcc-protocol==0.7.2 \
|
||||
--hash=sha256:31ee5178cfba8895eb2a6b8d06dc7830b51461a0ff767a670a64707c63e6b264 \
|
||||
--hash=sha256:498db4ccdda018cd9f40210f5bd02ddcc98e7df583170b2eab4035c86c3cc03b
|
||||
click==7.0 \
|
||||
--hash=sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13 \
|
||||
--hash=sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7
|
||||
construct==2.9.45 \
|
||||
--hash=sha256:2271a0efd0798679dea825ff47e22a4c550456a5db0ba8baa82f7eae0af0118c
|
||||
Cython==0.29.4 \
|
||||
--hash=sha256:004eeb2fc64e9db4a3bc0d65583d69769c7242d29d9335121cbab776688dc122 \
|
||||
--hash=sha256:028ee8571884a129e0d5c4d48296f6b3ea679668c096bb65fe8b2ff7ac29d707 \
|
||||
--hash=sha256:162b8b794ca9210c7039d54b6d96cd342e0404e41e7e467baae69f0252d7e52a \
|
||||
--hash=sha256:1aba4cf581d203e8fa3b6a7b432b09416e4f93c0d1f7744834acacfe3e9db424 \
|
||||
--hash=sha256:1be8f08c87b92a880f2fd19f93293e738ca8647834ad05625635320cec9ecad4 \
|
||||
--hash=sha256:21c707a811912aeb65abe8a66e5adebc759889661c8f4cf677523cd33c609084 \
|
||||
--hash=sha256:234de250ef09ba667fc6a8f6ba07712d3fe5bb8d92d70d2b958d4c56e3172c4a \
|
||||
--hash=sha256:33dad82003df518e1242ac3b0592fc63c49d65d0d37b696cb43b7d35085e6bd5 \
|
||||
--hash=sha256:54ee6cbc1397b27670e598ae15cab36e826a01605f63bf267a5fd2642bd8a147 \
|
||||
--hash=sha256:6058c57657d2704c9fad8a56458173d2f525dce4083ca46e9b99b1b35da2b27f \
|
||||
--hash=sha256:6d3065f39ea1354eba4807e2752e97d57f26d6f68bc4a4c561264ca4300c46cb \
|
||||
--hash=sha256:7059e5acac1d7a82e75e553924d9ea59b0e79203adf903cb999287fbcc8f50f1 \
|
||||
--hash=sha256:71c31e01f20a3a7273f6f38760d29170ee89e895be540481130cb173ef6b7246 \
|
||||
--hash=sha256:89225447801e8bd0f6d8e2c0807ded83af8ad7bf4086b5ecf1f22c5a68d1b3e3 \
|
||||
--hash=sha256:9783f11fe4a4af66b0aa0da68fda833c10b95edd9099a6dbe710d03bcb96adf2 \
|
||||
--hash=sha256:9a0be0aac30d71fe490a2b0377fca6e13a5242ecc01d09c7a358f1f2fcb07a80 \
|
||||
--hash=sha256:9a2cccc26dcf2df1e0048cdf63bd714f1d5dfad457f03b9938c5cc3eef74c9ab \
|
||||
--hash=sha256:b0889310f8558eb406a4a853d63553b90c621476f1b5b80b46b1ff57eef198cf \
|
||||
--hash=sha256:c46ef7b771c88512435399e5ffbc3a70079d4945123d6fbfc6211b4cfdc4e546 \
|
||||
--hash=sha256:c71a77c1047d65e5b4e614053cbb7b567c36359b2bc1d27fba23b984ab6dddd0 \
|
||||
--hash=sha256:c9361811a1a49db11efce54fedd01a5544af8db074fce471c720bdb85ec9c7a8 \
|
||||
--hash=sha256:d021a8326a1d2cdb182b0dd7f49bb42d8a4e6ddfb3c8d388ee5be26d57d49f3b \
|
||||
--hash=sha256:d1ee3d39c73a094ae5b6e2f9263ae0dc61af1b549a0869ade8c3c30325ed9f26 \
|
||||
--hash=sha256:d49d7cf82192edc6e386262a07ceb3515028afbd9009dd8ec669d2c0a9f20128 \
|
||||
--hash=sha256:dc5fc1fa072a98f152e46465aaf3e02b3ea36a9d3b8c79bfabd47b0e3ad9226c \
|
||||
--hash=sha256:e290fed7fe73860657af564e596fff87e75cfda861c067e89212970a47826cc6 \
|
||||
--hash=sha256:fcf9a9a566ab98495db641eefee471eb03df71e394ee51fdfa9b4c0b9f6928eb \
|
||||
--hash=sha256:fe8c1d2538867bf2753988a4a2d548bcb211fcbba125aa3e9092391b16f47b56
|
||||
ecdsa==0.13 \
|
||||
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
|
||||
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
|
||||
hidapi==0.7.99.post21 \
|
||||
--hash=sha256:1ac170f4d601c340f2cd52fd06e85c5e77bad7ceac811a7bb54b529f7dc28c24 \
|
||||
--hash=sha256:6424ad75da0021ce8c1bcd78056a04adada303eff3c561f8d132b85d0a914cb3 \
|
||||
--hash=sha256:8d3be666f464347022e2b47caf9132287885d9eacc7895314fc8fefcb4e42946 \
|
||||
--hash=sha256:92878bad7324dee619b7832fbfc60b5360d378aa7c5addbfef0a410d8fd342c7 \
|
||||
--hash=sha256:b4b1f6aff0192e9be153fe07c1b7576cb7a1ff52e78e3f76d867be95301a8e87 \
|
||||
--hash=sha256:bf03f06f586ce7d8aeb697a94b7dba12dc9271aae92d7a8d4486360ff711a660 \
|
||||
--hash=sha256:c76de162937326fcd57aa399f94939ce726242323e65c15c67e183da1f6c26f7 \
|
||||
@ -52,36 +58,42 @@ hidapi==0.7.99.post21 \
|
||||
--hash=sha256:d4b5787a04613503357606bb10e59c3e2c1114fa00ee328b838dd257f41cbd7b \
|
||||
--hash=sha256:e0be1aa6566979266a8fc845ab0e18613f4918cf2c977fe67050f5dc7e2a9a97 \
|
||||
--hash=sha256:edfb16b16a298717cf05b8c8a9ad1828b6ff3de5e93048ceccd74e6ae4ff0922
|
||||
idna==2.7 \
|
||||
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
|
||||
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
|
||||
keepkey==4.0.2 \
|
||||
--hash=sha256:cddee60ae405841cdff789cbc54168ceaeb2282633420f2be155554c25c69138
|
||||
libusb1==1.6.4 \
|
||||
--hash=sha256:8c930d9c1d037d9c83924c82608aa6a1adcaa01ca0e4a23ee0e8e18d7eee670d
|
||||
idna==2.8 \
|
||||
--hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
|
||||
--hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c
|
||||
keepkey==6.0.2 \
|
||||
--hash=sha256:3236dd701bde74768c41a92e724e322ea5e01b90985e2e6215eb85b77f9a0ae1 \
|
||||
--hash=sha256:677e07deacc2ff97bee313b8dd7ae55faebab02e7d17b9a8e49b889996a36010 \
|
||||
--hash=sha256:af107f610fb0e2417fc7a9d87a2fa22aac9b80b79559370d178be424bb85489a
|
||||
libusb1==1.7 \
|
||||
--hash=sha256:9d4f66d2ed699986b06bc3082cd262101cb26af7a76a34bd15b7eb56cba37e0f
|
||||
mnemonic==0.18 \
|
||||
--hash=sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d
|
||||
pbkdf2==1.3 \
|
||||
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
|
||||
pip==10.0.1 \
|
||||
--hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \
|
||||
--hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68
|
||||
protobuf==3.6.0 \
|
||||
--hash=sha256:12985d9f40c104da2f44ec089449214876809b40fdc5d9e43b93b512b9e74056 \
|
||||
--hash=sha256:12c97fe27af12fc5d66b23f905ab09dd4fb0c68d5a74a419d914580e6d2e71e3 \
|
||||
--hash=sha256:327fb9d8a8247bc780b9ea7ed03c0643bc0d22c139b761c9ec1efc7cc3f0923e \
|
||||
--hash=sha256:3895319db04c0b3baed74fb66be7ba9f4cd8e88a432b8e71032cdf08b2dfee23 \
|
||||
--hash=sha256:695072063e256d32335d48b9484451f7c7948edc3dbd419469d6a778602682fc \
|
||||
--hash=sha256:7d786f3ef5b33a04e6538089674f244a3b0f588155016559d950989010af97d0 \
|
||||
--hash=sha256:8bf82bb7a466a54be7272dcb492f71d55a2453a58d862fb74c3f2083f2768543 \
|
||||
--hash=sha256:9bbc1ae1c33c1bd3a2fc05a3aec328544d2b039ff0ce6f000063628a32fad777 \
|
||||
--hash=sha256:9e992c68103ab5635728d29fcf132c669cb4e2db24d012685210276185009d17 \
|
||||
--hash=sha256:9f1087abb67b34e55108bc610936b34363a7aac692023bcbb17e065c253a1f80 \
|
||||
--hash=sha256:9fefcb92a3784b446abf3641d9a14dad815bee88e0edd10b9a9e0e144d01a991 \
|
||||
--hash=sha256:a37836aa47d1b81c2db1a6b7a5e79926062b5d76bd962115a0e615551be2b48d \
|
||||
--hash=sha256:cca22955443c55cf86f963a4ad7057bca95e4dcde84d6a493066d380cfab3bb0 \
|
||||
--hash=sha256:d7ac50bc06d31deb07ace6de85556c1d7330e5c0958f3b2af85037d6d1182abf \
|
||||
--hash=sha256:dfe6899304b898538f4dc94fa0b281b56b70e40f58afa4c6f807805261cbe2e8
|
||||
pip==19.0.1 \
|
||||
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \
|
||||
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5
|
||||
protobuf==3.6.1 \
|
||||
--hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
|
||||
--hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
|
||||
--hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
|
||||
--hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
|
||||
--hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
|
||||
--hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \
|
||||
--hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \
|
||||
--hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
|
||||
--hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
|
||||
--hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
|
||||
--hash=sha256:685bc4ec61a50f7360c9fd18e277b65db90105adbf9c79938bd315435e526b90 \
|
||||
--hash=sha256:92e8418976e52201364a3174e40dc31f5fd8c147186d72380cbda54e0464ee19 \
|
||||
--hash=sha256:9335f79d1940dfb9bcaf8ec881fb8ab47d7a2c721fb8b02949aab8bbf8b68625 \
|
||||
--hash=sha256:a7ee3bb6de78185e5411487bef8bc1c59ebd97e47713cba3c460ef44e99b3db9 \
|
||||
--hash=sha256:ceec283da2323e2431c49de58f80e1718986b79be59c266bb0509cbf90ca5b9e \
|
||||
--hash=sha256:e7a5ccf56444211d79e3204b05087c1460c212a2c7d62f948b996660d0165d68 \
|
||||
--hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10
|
||||
pyaes==1.6.1 \
|
||||
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
|
||||
pyblake2==1.1.2 \
|
||||
--hash=sha256:3757f7ad709b0e1b2a6b3919fa79fe3261f166fc375cd521f2be480f8319dde9 \
|
||||
--hash=sha256:407e02c7f8f36fcec1b7aa114ddca0c1060c598142ea6f6759d03710b946a7e3 \
|
||||
@ -92,24 +104,31 @@ pyblake2==1.1.2 \
|
||||
--hash=sha256:baa2190bfe549e36163aa44664d4ee3a9080b236fc5d42f50dc6fd36bbdc749e \
|
||||
--hash=sha256:c53417ee0bbe77db852d5fd1036749f03696ebc2265de359fe17418d800196c4 \
|
||||
--hash=sha256:fbc9fcde75713930bc2a91b149e97be2401f7c9c56d735b46a109210f58d7358
|
||||
requests==2.19.1 \
|
||||
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
|
||||
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
|
||||
setuptools==39.2.0 \
|
||||
--hash=sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926 \
|
||||
--hash=sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2
|
||||
six==1.11.0 \
|
||||
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
|
||||
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
|
||||
trezor==0.10.1 \
|
||||
--hash=sha256:09b4edfa83b787975c6f30728c13bb413621d5bdf722231748758ba0181b8a60 \
|
||||
--hash=sha256:5bcad3e97129fccd6f8b4cf08f81862e423373617c857feb492cfa1b1807844e
|
||||
urllib3==1.23 \
|
||||
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
|
||||
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
|
||||
websocket-client==0.48.0 \
|
||||
--hash=sha256:18f1170e6a1b5463986739d9fd45c4308b0d025c1b2f9b88788d8f69e8a5eb4a \
|
||||
--hash=sha256:db70953ae4a064698b27ae56dcad84d0ee68b7b43cb40940f537738f38f510c1
|
||||
wheel==0.31.1 \
|
||||
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
|
||||
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
|
||||
requests==2.21.0 \
|
||||
--hash=sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e \
|
||||
--hash=sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b
|
||||
safet==0.1.4 \
|
||||
--hash=sha256:522c257910f9472e9c77c487425ed286f6721c314653e232bc41c6cedece1bb1 \
|
||||
--hash=sha256:b152874acdc89ff0c8b2d680bfbf020b3e53527c2ad3404489dd61a548aa56a1
|
||||
setuptools==40.8.0 \
|
||||
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \
|
||||
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab
|
||||
six==1.12.0 \
|
||||
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
|
||||
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
|
||||
trezor==0.11.1 \
|
||||
--hash=sha256:6043f321d856e1b45b9df0c37810264f08d065bb56cd999f61a05fe2906e9e18 \
|
||||
--hash=sha256:6119b30cf9a136667753935bd06c5f341e78950b35e8ccbadaecc65c12f1946d
|
||||
typing-extensions==3.7.2 \
|
||||
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \
|
||||
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \
|
||||
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71
|
||||
urllib3==1.24.1 \
|
||||
--hash=sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39 \
|
||||
--hash=sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22
|
||||
websocket-client==0.54.0 \
|
||||
--hash=sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786 \
|
||||
--hash=sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849
|
||||
wheel==0.32.3 \
|
||||
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
|
||||
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44
|
||||
|
||||
@ -1,64 +1,155 @@
|
||||
certifi==2018.4.16 \
|
||||
--hash=sha256:13e698f54293db9f89122b0581843a782ad0934a4fe0172d2a980ba77fc61bb7 \
|
||||
--hash=sha256:9fa520c1bacfb634fa7af20a76bcbd3d5fb390481724c597da32c719a7dca4b0
|
||||
aiohttp==3.5.4 \
|
||||
--hash=sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55 \
|
||||
--hash=sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed \
|
||||
--hash=sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10 \
|
||||
--hash=sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5 \
|
||||
--hash=sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1 \
|
||||
--hash=sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939 \
|
||||
--hash=sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390 \
|
||||
--hash=sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa \
|
||||
--hash=sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc \
|
||||
--hash=sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5 \
|
||||
--hash=sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d \
|
||||
--hash=sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf \
|
||||
--hash=sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6 \
|
||||
--hash=sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72 \
|
||||
--hash=sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12 \
|
||||
--hash=sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366 \
|
||||
--hash=sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4 \
|
||||
--hash=sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300 \
|
||||
--hash=sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d \
|
||||
--hash=sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303 \
|
||||
--hash=sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6 \
|
||||
--hash=sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889
|
||||
aiohttp-socks==0.2.2 \
|
||||
--hash=sha256:e473ee222b001fe33798957b9ce3352b32c187cf41684f8e2259427925914993 \
|
||||
--hash=sha256:eebd8939a7c3c1e3e7e1b2552c60039b4c65ef6b8b2351efcbdd98290538e310
|
||||
aiorpcX==0.10.4 \
|
||||
--hash=sha256:7130105d31230f069b0eea4e1893c7199cfe2d89a52a31aec718d37f4449935d \
|
||||
--hash=sha256:e6dfd584f597ee3aa6a8d4cb5755c8ffbbe42754f32728561d9e5940379d5096
|
||||
async_timeout==3.0.1 \
|
||||
--hash=sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f \
|
||||
--hash=sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3
|
||||
attrs==18.2.0 \
|
||||
--hash=sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69 \
|
||||
--hash=sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb
|
||||
certifi==2018.11.29 \
|
||||
--hash=sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7 \
|
||||
--hash=sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033
|
||||
chardet==3.0.4 \
|
||||
--hash=sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae \
|
||||
--hash=sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691
|
||||
dnspython==1.15.0 \
|
||||
--hash=sha256:40f563e1f7a7b80dc5a4e76ad75c23da53d62f1e15e6e517293b04e1f84ead7c \
|
||||
--hash=sha256:861e6e58faa730f9845aaaa9c6c832851fbf89382ac52915a51f89c71accdd31
|
||||
dnspython==1.16.0 \
|
||||
--hash=sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01 \
|
||||
--hash=sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d
|
||||
ecdsa==0.13 \
|
||||
--hash=sha256:40d002cf360d0e035cf2cb985e1308d41aaa087cbfc135b2dc2d844296ea546c \
|
||||
--hash=sha256:64cf1ee26d1cde3c73c6d7d107f835fed7c6a2904aef9eac223d57ad800c43fa
|
||||
idna==2.7 \
|
||||
--hash=sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e \
|
||||
--hash=sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16
|
||||
jsonrpclib-pelix==0.3.1 \
|
||||
--hash=sha256:5417b1508d5a50ec64f6e5b88907f111155d52607b218ff3ba9a777afb2e49e3 \
|
||||
--hash=sha256:bd89a6093bc4d47dc8a096197aacb827359944a4533be5193f3845f57b9f91b4
|
||||
pbkdf2==1.3 \
|
||||
--hash=sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979
|
||||
pip==10.0.1 \
|
||||
--hash=sha256:717cdffb2833be8409433a93746744b59505f42146e8d37de6c62b430e25d6d7 \
|
||||
--hash=sha256:f2bd08e0cd1b06e10218feaf6fef299f473ba706582eb3bd9d52203fdbd7ee68
|
||||
protobuf==3.6.0 \
|
||||
--hash=sha256:12985d9f40c104da2f44ec089449214876809b40fdc5d9e43b93b512b9e74056 \
|
||||
--hash=sha256:12c97fe27af12fc5d66b23f905ab09dd4fb0c68d5a74a419d914580e6d2e71e3 \
|
||||
--hash=sha256:327fb9d8a8247bc780b9ea7ed03c0643bc0d22c139b761c9ec1efc7cc3f0923e \
|
||||
--hash=sha256:3895319db04c0b3baed74fb66be7ba9f4cd8e88a432b8e71032cdf08b2dfee23 \
|
||||
--hash=sha256:695072063e256d32335d48b9484451f7c7948edc3dbd419469d6a778602682fc \
|
||||
--hash=sha256:7d786f3ef5b33a04e6538089674f244a3b0f588155016559d950989010af97d0 \
|
||||
--hash=sha256:8bf82bb7a466a54be7272dcb492f71d55a2453a58d862fb74c3f2083f2768543 \
|
||||
--hash=sha256:9bbc1ae1c33c1bd3a2fc05a3aec328544d2b039ff0ce6f000063628a32fad777 \
|
||||
--hash=sha256:9e992c68103ab5635728d29fcf132c669cb4e2db24d012685210276185009d17 \
|
||||
--hash=sha256:9f1087abb67b34e55108bc610936b34363a7aac692023bcbb17e065c253a1f80 \
|
||||
--hash=sha256:9fefcb92a3784b446abf3641d9a14dad815bee88e0edd10b9a9e0e144d01a991 \
|
||||
--hash=sha256:a37836aa47d1b81c2db1a6b7a5e79926062b5d76bd962115a0e615551be2b48d \
|
||||
--hash=sha256:cca22955443c55cf86f963a4ad7057bca95e4dcde84d6a493066d380cfab3bb0 \
|
||||
--hash=sha256:d7ac50bc06d31deb07ace6de85556c1d7330e5c0958f3b2af85037d6d1182abf \
|
||||
--hash=sha256:dfe6899304b898538f4dc94fa0b281b56b70e40f58afa4c6f807805261cbe2e8
|
||||
idna==2.8 \
|
||||
--hash=sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407 \
|
||||
--hash=sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c
|
||||
idna_ssl==1.1.0 \
|
||||
--hash=sha256:a933e3bb13da54383f9e8f35dc4f9cb9eb9b3b78c6b36f311254d6d0d92c6c7c
|
||||
jsonrpclib-pelix==0.4.0 \
|
||||
--hash=sha256:19c558e169a51480b39548783067ca55046b62b2409ab4559931255e12f635de \
|
||||
--hash=sha256:a966d17f2f739ee89031cf5c807d85d92db6b2715fb2b2f8a88bbfc87f468b12
|
||||
multidict==4.5.2 \
|
||||
--hash=sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f \
|
||||
--hash=sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3 \
|
||||
--hash=sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef \
|
||||
--hash=sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b \
|
||||
--hash=sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73 \
|
||||
--hash=sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc \
|
||||
--hash=sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3 \
|
||||
--hash=sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd \
|
||||
--hash=sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351 \
|
||||
--hash=sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941 \
|
||||
--hash=sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d \
|
||||
--hash=sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1 \
|
||||
--hash=sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b \
|
||||
--hash=sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a \
|
||||
--hash=sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3 \
|
||||
--hash=sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7 \
|
||||
--hash=sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0 \
|
||||
--hash=sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0 \
|
||||
--hash=sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014 \
|
||||
--hash=sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5 \
|
||||
--hash=sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036 \
|
||||
--hash=sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d \
|
||||
--hash=sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a \
|
||||
--hash=sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce \
|
||||
--hash=sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1 \
|
||||
--hash=sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a \
|
||||
--hash=sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9 \
|
||||
--hash=sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7 \
|
||||
--hash=sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b
|
||||
pip==19.0.1 \
|
||||
--hash=sha256:aae79c7afe895fb986ec751564f24d97df1331bb99cdfec6f70dada2f40c0044 \
|
||||
--hash=sha256:e81ddd35e361b630e94abeda4a1eddd36d47a90e71eb00f38f46b57f787cd1a5
|
||||
protobuf==3.6.1 \
|
||||
--hash=sha256:10394a4d03af7060fa8a6e1cbf38cea44be1467053b0aea5bbfcb4b13c4b88c4 \
|
||||
--hash=sha256:1489b376b0f364bcc6f89519718c057eb191d7ad6f1b395ffd93d1aa45587811 \
|
||||
--hash=sha256:1931d8efce896981fe410c802fd66df14f9f429c32a72dd9cfeeac9815ec6444 \
|
||||
--hash=sha256:196d3a80f93c537f27d2a19a4fafb826fb4c331b0b99110f985119391d170f96 \
|
||||
--hash=sha256:46e34fdcc2b1f2620172d3a4885128705a4e658b9b62355ae5e98f9ea19f42c2 \
|
||||
--hash=sha256:4b92e235a3afd42e7493b281c8b80c0c65cbef45de30f43d571d1ee40a1f77ef \
|
||||
--hash=sha256:574085a33ca0d2c67433e5f3e9a0965c487410d6cb3406c83bdaf549bfc2992e \
|
||||
--hash=sha256:59cd75ded98094d3cf2d79e84cdb38a46e33e7441b2826f3838dcc7c07f82995 \
|
||||
--hash=sha256:5ee0522eed6680bb5bac5b6d738f7b0923b3cafce8c4b1a039a6107f0841d7ed \
|
||||
--hash=sha256:65917cfd5da9dfc993d5684643063318a2e875f798047911a9dd71ca066641c9 \
|
||||
--hash=sha256:685bc4ec61a50f7360c9fd18e277b65db90105adbf9c79938bd315435e526b90 \
|
||||
--hash=sha256:92e8418976e52201364a3174e40dc31f5fd8c147186d72380cbda54e0464ee19 \
|
||||
--hash=sha256:9335f79d1940dfb9bcaf8ec881fb8ab47d7a2c721fb8b02949aab8bbf8b68625 \
|
||||
--hash=sha256:a7ee3bb6de78185e5411487bef8bc1c59ebd97e47713cba3c460ef44e99b3db9 \
|
||||
--hash=sha256:ceec283da2323e2431c49de58f80e1718986b79be59c266bb0509cbf90ca5b9e \
|
||||
--hash=sha256:e7a5ccf56444211d79e3204b05087c1460c212a2c7d62f948b996660d0165d68 \
|
||||
--hash=sha256:fcfc907746ec22716f05ea96b7f41597dfe1a1c088f861efb8a0d4f4196a6f10
|
||||
pyaes==1.6.1 \
|
||||
--hash=sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f
|
||||
PySocks==1.6.8 \
|
||||
--hash=sha256:3fe52c55890a248676fd69dc9e3c4e811718b777834bcaab7a8125cf9deac672
|
||||
qrcode==6.0 \
|
||||
--hash=sha256:037b0db4c93f44586e37f84c3da3f763874fcac85b2974a69a98e399ac78e1bf \
|
||||
--hash=sha256:de4ffc15065e6ff20a551ad32b6b41264f3c75275675406ddfa8e3530d154be3
|
||||
requests==2.19.1 \
|
||||
--hash=sha256:63b52e3c866428a224f97cab011de738c36aec0185aa91cfacd418b5d58911d1 \
|
||||
--hash=sha256:ec22d826a36ed72a7358ff3fe56cbd4ba69dd7a6718ffd450ff0e9df7a47ce6a
|
||||
setuptools==39.2.0 \
|
||||
--hash=sha256:8fca9275c89964f13da985c3656cb00ba029d7f3916b37990927ffdf264e7926 \
|
||||
--hash=sha256:f7cddbb5f5c640311eb00eab6e849f7701fa70bf6a183fc8a2c33dd1d1672fb2
|
||||
six==1.11.0 \
|
||||
--hash=sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9 \
|
||||
--hash=sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb
|
||||
urllib3==1.23 \
|
||||
--hash=sha256:a68ac5e15e76e7e5dd2b8f94007233e01effe3e50e8daddf69acfd81cb686baf \
|
||||
--hash=sha256:b5725a0bd4ba422ab0e66e89e030c806576753ea3ee08554382c14e685d117b5
|
||||
wheel==0.31.1 \
|
||||
--hash=sha256:0a2e54558a0628f2145d2fc822137e322412115173e8a2ddbe1c9024338ae83c \
|
||||
--hash=sha256:80044e51ec5bbf6c894ba0bc48d26a8c20a9ba629f4ca19ea26ecfcf87685f5f
|
||||
colorama==0.3.9 \
|
||||
--hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \
|
||||
--hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1
|
||||
QDarkStyle==2.5.4 \
|
||||
--hash=sha256:3eb60922b8c4d9cedecb6897ca4c9f8a259d81bdefe5791976ccdf12432de1f0 \
|
||||
--hash=sha256:51331fc6490b38c376e6ba8d8c814320c8d2d1c2663055bc396321a7c28fa8be
|
||||
qrcode==6.1 \
|
||||
--hash=sha256:3996ee560fc39532910603704c82980ff6d4d5d629f9c3f25f34174ce8606cf5 \
|
||||
--hash=sha256:505253854f607f2abf4d16092c61d4e9d511a3b4392e60bff957a68592b04369
|
||||
setuptools==40.8.0 \
|
||||
--hash=sha256:6e4eec90337e849ade7103723b9a99631c1f0d19990d6e8412dc42f5ae8b304d \
|
||||
--hash=sha256:e8496c0079f3ac30052ffe69b679bd876c5265686127a3159cfa415669b7f9ab
|
||||
six==1.12.0 \
|
||||
--hash=sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c \
|
||||
--hash=sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73
|
||||
typing-extensions==3.7.2 \
|
||||
--hash=sha256:07b2c978670896022a43c4b915df8958bec4a6b84add7f2c87b2b728bda3ba64 \
|
||||
--hash=sha256:f3f0e67e1d42de47b5c67c32c9b26641642e9170fe7e292991793705cd5fef7c \
|
||||
--hash=sha256:fb2cd053238d33a8ec939190f30cfd736c00653a85a2919415cecf7dc3d9da71
|
||||
wheel==0.32.3 \
|
||||
--hash=sha256:029703bf514e16c8271c3821806a1c171220cc5bdd325cbf4e7da1e056a01db6 \
|
||||
--hash=sha256:1e53cdb3f808d5ccd0df57f964263752aa74ea7359526d3da6c02114ec1e1d44
|
||||
yarl==1.3.0 \
|
||||
--hash=sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9 \
|
||||
--hash=sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f \
|
||||
--hash=sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb \
|
||||
--hash=sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320 \
|
||||
--hash=sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842 \
|
||||
--hash=sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0 \
|
||||
--hash=sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829 \
|
||||
--hash=sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310 \
|
||||
--hash=sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4 \
|
||||
--hash=sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8 \
|
||||
--hash=sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1
|
||||
colorama==0.4.1 \
|
||||
--hash=sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d \
|
||||
--hash=sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48
|
||||
pylibscrypt==1.7.1 \
|
||||
--hash=sha256:7aa9424e211a12106c67ea884ccfe609856289372b900d3702faaf66e87f79ac
|
||||
scrypt==0.8.6 \
|
||||
--hash=sha256:85919f023148cd9fb01d75ad4e3e061928c298fa6249a0cd6cd469c4b947595e \
|
||||
--hash=sha256:4ad7188f2e42dbee2ff1cd72e3da40b170ba41847effbf0d726444f62ae60f3a \
|
||||
--hash=sha256:bc131f74a688fa09993c518ca666a2ebd4268b207e039cbab03a034228140d3e \
|
||||
--hash=sha256:232acdbc3434d2de55def8d5dbf1bc4b9bfc50da7c5741df2a6eebc4e18d3720 \
|
||||
--hash=sha256:971db040d3963ebe4b919a203fe10d7d6659951d3644066314330983dc175ed4 \
|
||||
--hash=sha256:475ac80239b3d788ae71a09c3019ca915e149aaa339adcdd1c9eef121293dc88 \
|
||||
--hash=sha256:18ccbc63d87c6f89b753194194bb37aeaf1abc517e4b989461d115c1d93ce128 \
|
||||
--hash=sha256:c23daecee405cb036845917295c76f8d747fc890158df40cb304b4b3c3640079 \
|
||||
--hash=sha256:f8239b2d47fa1d40bc27efd231dc7083695d10c1c2ac51a99380360741e0362d
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
#!/bin/bash
|
||||
# Run this after a new release to update dependencies
|
||||
|
||||
set -e
|
||||
|
||||
venv_dir=~/.electrum-venv
|
||||
contrib=$(dirname "$0")
|
||||
|
||||
|
||||
@ -1,6 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
pushd ./gui/kivy/
|
||||
set -e
|
||||
|
||||
CONTRIB="$(dirname "$(readlink -e "$0")")"
|
||||
ROOT_FOLDER="$CONTRIB"/..
|
||||
PACKAGES="$ROOT_FOLDER"/packages/
|
||||
LOCALE="$ROOT_FOLDER"/electrum/locale/
|
||||
|
||||
if [ ! -d "$LOCALE" ]; then
|
||||
echo "Run make_locale first!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PACKAGES" ]; then
|
||||
echo "Run make_packages first!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pushd ./electrum/gui/kivy/
|
||||
|
||||
make theming
|
||||
|
||||
if [[ -n "$1" && "$1" == "release" ]] ; then
|
||||
echo -n Keystore Password:
|
||||
|
||||
@ -24,6 +24,7 @@ string = string.replace("##VERSION_APK##", APK_VERSION)
|
||||
|
||||
files = {
|
||||
'tgz': "Electrum-%s.tar.gz" % version,
|
||||
'appimage': "electrum-%s-x86_64.AppImage" % version,
|
||||
'zip': "Electrum-%s.zip" % version,
|
||||
'mac': "electrum-%s.dmg" % version_mac,
|
||||
'win': "electrum-%s.exe" % version_win,
|
||||
|
||||
@ -3,13 +3,17 @@ import os
|
||||
import subprocess
|
||||
import io
|
||||
import zipfile
|
||||
import requests
|
||||
import sys
|
||||
|
||||
try:
|
||||
import requests
|
||||
except ImportError as e:
|
||||
sys.exit(f"Error: {str(e)}. Try 'sudo python3 -m pip install <module-name>'")
|
||||
|
||||
os.chdir(os.path.dirname(os.path.realpath(__file__)))
|
||||
os.chdir('..')
|
||||
|
||||
code_directories = 'gui plugins lib'
|
||||
cmd = "find {} -type f -name '*.py' -o -name '*.kv'".format(code_directories)
|
||||
cmd = "find electrum -type f -name '*.py' -o -name '*.kv'"
|
||||
|
||||
files = subprocess.check_output(cmd, shell=True)
|
||||
|
||||
@ -19,13 +23,13 @@ with open("app.fil", "wb") as f:
|
||||
print("Found {} files to translate".format(len(files.splitlines())))
|
||||
|
||||
# Generate fresh translation template
|
||||
if not os.path.exists('lib/locale'):
|
||||
os.mkdir('lib/locale')
|
||||
cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=lib/locale/messages.pot'
|
||||
if not os.path.exists('electrum/locale'):
|
||||
os.mkdir('electrum/locale')
|
||||
cmd = 'xgettext -s --from-code UTF-8 --language Python --no-wrap -f app.fil --output=electrum/locale/messages.pot'
|
||||
print('Generate template')
|
||||
os.system(cmd)
|
||||
|
||||
os.chdir('lib')
|
||||
os.chdir('electrum')
|
||||
|
||||
crowdin_identifier = 'electrum'
|
||||
crowdin_file_name = 'files[electrum-client/messages.pot]'
|
||||
@ -55,7 +59,7 @@ if crowdin_api_key:
|
||||
|
||||
# Download & unzip
|
||||
print('Download translations')
|
||||
s = requests.request('GET', 'https://crowdin.com/download/project/' + crowdin_identifier + '.zip').content
|
||||
s = requests.request('GET', 'https://crowdin.com/backend/download/project/' + crowdin_identifier + '.zip').content
|
||||
zfobj = zipfile.ZipFile(io.BytesIO(s))
|
||||
|
||||
print('Unzip translations')
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
#!/bin/bash
|
||||
|
||||
contrib=$(dirname "$0")
|
||||
test -n "$contrib" -a -d "$contrib" || exit
|
||||
CONTRIB="$(dirname "$0")"
|
||||
test -n "$CONTRIB" -a -d "$CONTRIB" || exit
|
||||
|
||||
whereis pip3
|
||||
if [ $? -ne 0 ] ; then echo "Install pip3" ; exit ; fi
|
||||
|
||||
rm "$contrib"/../packages/ -r
|
||||
rm "$CONTRIB"/../packages/ -r
|
||||
|
||||
#Install pure python modules in electrum directory
|
||||
pip3 install -r $contrib/deterministic-build/requirements.txt -t $contrib/../packages
|
||||
python3 -m pip install -r "$CONTRIB"/deterministic-build/requirements.txt -t "$CONTRIB"/../packages
|
||||
|
||||
|
||||
@ -1 +1,31 @@
|
||||
python3 setup.py sdist --format=zip,gztar
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
CONTRIB="$(dirname "$(readlink -e "$0")")"
|
||||
ROOT_FOLDER="$CONTRIB"/..
|
||||
PACKAGES="$ROOT_FOLDER"/packages/
|
||||
LOCALE="$ROOT_FOLDER"/electrum/locale/
|
||||
|
||||
if [ ! -d "$LOCALE" ]; then
|
||||
echo "Run make_locale first!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d "$PACKAGES" ]; then
|
||||
echo "Run make_packages first!"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
(
|
||||
cd "$ROOT_FOLDER"
|
||||
|
||||
echo "'git clean -fx' would delete the following files: >>>"
|
||||
git clean -fx --dry-run
|
||||
echo "<<<"
|
||||
|
||||
# we could build the kivy atlas potentially?
|
||||
#(cd electrum/gui/kivy/; make theming) || echo "building kivy atlas failed! skipping."
|
||||
|
||||
python3 setup.py --quiet sdist --format=zip,gztar
|
||||
)
|
||||
|
||||
1
contrib/osx/CalinsQRReader
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 59dfc03272751cd29ee311456fa34c40f7ebb7c0
|
||||
66
contrib/osx/README.md
Normal file
@ -0,0 +1,66 @@
|
||||
Building Mac OS binaries
|
||||
========================
|
||||
|
||||
This guide explains how to build Electrum binaries for macOS systems.
|
||||
|
||||
|
||||
## 1. Building the binary
|
||||
|
||||
This needs to be done on a system running macOS or OS X. We use El Capitan (10.11.6) as building it
|
||||
on High Sierra (or later)
|
||||
makes the binaries [incompatible with older versions](https://github.com/pyinstaller/pyinstaller/issues/1191).
|
||||
|
||||
Before starting, make sure that the Xcode command line tools are installed (e.g. you have `git`).
|
||||
|
||||
#### 1.1a Get Xcode
|
||||
|
||||
Building the QR scanner (CalinsQRReader) requires full Xcode (not just command line tools).
|
||||
|
||||
The last Xcode version compatible with El Capitan is Xcode 8.2.1
|
||||
|
||||
Get it from [here](https://developer.apple.com/download/more/).
|
||||
|
||||
Unfortunately, you need an "Apple ID" account.
|
||||
|
||||
After downloading, uncompress it.
|
||||
|
||||
Make sure it is the "selected" xcode (e.g.):
|
||||
|
||||
sudo xcode-select -s $HOME/Downloads/Xcode.app/Contents/Developer/
|
||||
|
||||
#### 1.1b Build QR scanner separately on newer Mac
|
||||
|
||||
Alternatively, you can try building just the QR scanner on newer macOS.
|
||||
|
||||
On newer Mac, run:
|
||||
|
||||
pushd contrib/osx/CalinsQRReader; xcodebuild; popd
|
||||
cp -r contrib/osx/CalinsQRReader/build prebuilt_qr
|
||||
|
||||
Move `prebuilt_qr` to El Capitan: `contrib/osx/CalinsQRReader/prebuilt_qr`.
|
||||
|
||||
|
||||
#### 1.2 Build Electrum
|
||||
|
||||
cd electrum
|
||||
./contrib/osx/make_osx
|
||||
|
||||
This creates both a folder named Electrum.app and the .dmg file.
|
||||
|
||||
|
||||
## 2. Building the image deterministically (WIP)
|
||||
The usual way to distribute macOS applications is to use image files containing the
|
||||
application. Although these images can be created on a Mac with the built-in `hdiutil`,
|
||||
they are not deterministic.
|
||||
|
||||
Instead, we use the toolchain that Bitcoin uses: genisoimage and libdmg-hfsplus.
|
||||
These tools do not work on macOS, so you need a separate Linux machine (or VM).
|
||||
|
||||
Copy the Electrum.app directory over and install the dependencies, e.g.:
|
||||
|
||||
apt install libcap-dev cmake make gcc faketime
|
||||
|
||||
Then you can just invoke `package.sh` with the path to the app:
|
||||
|
||||
cd electrum
|
||||
./contrib/osx/package.sh ~/Electrum.app/
|
||||
23
contrib/osx/base.sh
Normal file
@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
. $(dirname "$0")/../build_tools_util.sh
|
||||
|
||||
|
||||
function DoCodeSignMaybe { # ARGS: infoName fileOrDirName codesignIdentity
|
||||
infoName="$1"
|
||||
file="$2"
|
||||
identity="$3"
|
||||
deep=""
|
||||
if [ -z "$identity" ]; then
|
||||
# we are ok with them not passing anything; master script calls us unconditionally even if no identity is specified
|
||||
return
|
||||
fi
|
||||
if [ -d "$file" ]; then
|
||||
deep="--deep"
|
||||
fi
|
||||
if [ -z "$infoName" ] || [ -z "$file" ] || [ -z "$identity" ] || [ ! -e "$file" ]; then
|
||||
fail "Argument error to internal function DoCodeSignMaybe()"
|
||||
fi
|
||||
info "Code signing ${infoName}..."
|
||||
codesign -f -v $deep -s "$identity" "$file" || fail "Could not code sign ${infoName}"
|
||||
}
|
||||
143
contrib/osx/make_osx
Executable file
@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Parameterize
|
||||
PYTHON_VERSION=3.6.4
|
||||
BUILDDIR=/tmp/electrum-build
|
||||
PACKAGE=Electrum
|
||||
GIT_REPO=https://github.com/spesmilo/electrum
|
||||
LIBSECP_VERSION="b408c6a8b287003d1ade5709e6f7bc3c7f1d5be7"
|
||||
|
||||
. $(dirname "$0")/base.sh
|
||||
|
||||
src_dir=$(dirname "$0")
|
||||
cd $src_dir/../..
|
||||
|
||||
export PYTHONHASHSEED=22
|
||||
VERSION=`git describe --tags --dirty --always`
|
||||
|
||||
which brew > /dev/null 2>&1 || fail "Please install brew from https://brew.sh/ to continue"
|
||||
which xcodebuild > /dev/null 2>&1 || fail "Please install Xcode and xcode command line tools to continue"
|
||||
|
||||
# Code Signing: See https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/Procedures/Procedures.html
|
||||
APP_SIGN=""
|
||||
if [ -n "$1" ]; then
|
||||
# Test the identity is valid for signing by doing this hack. There is no other way to do this.
|
||||
cp -f /bin/ls ./CODESIGN_TEST
|
||||
codesign -s "$1" --dryrun -f ./CODESIGN_TEST > /dev/null 2>&1
|
||||
res=$?
|
||||
rm -f ./CODESIGN_TEST
|
||||
if ((res)); then
|
||||
fail "Code signing identity \"$1\" appears to be invalid."
|
||||
fi
|
||||
unset res
|
||||
APP_SIGN="$1"
|
||||
info "Code signing enabled using identity \"$APP_SIGN\""
|
||||
else
|
||||
warn "Code signing DISABLED. Specify a valid macOS Developer identity installed on the system as the first argument to this script to enable signing."
|
||||
fi
|
||||
|
||||
info "Installing Python $PYTHON_VERSION"
|
||||
export PATH="~/.pyenv/bin:~/.pyenv/shims:~/Library/Python/3.6/bin:$PATH"
|
||||
if [ -d "~/.pyenv" ]; then
|
||||
pyenv update
|
||||
else
|
||||
curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash > /dev/null 2>&1
|
||||
fi
|
||||
PYTHON_CONFIGURE_OPTS="--enable-framework" pyenv install -s $PYTHON_VERSION && \
|
||||
pyenv global $PYTHON_VERSION || \
|
||||
fail "Unable to use Python $PYTHON_VERSION"
|
||||
|
||||
|
||||
info "install dependencies specific to binaries"
|
||||
# note that this also installs pinned versions of both pip and setuptools
|
||||
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-binaries.txt --user \
|
||||
|| fail "Could not install pyinstaller"
|
||||
|
||||
|
||||
info "Installing pyinstaller"
|
||||
python3 -m pip install -I --user pyinstaller==3.4 --no-use-pep517 || fail "Could not install pyinstaller"
|
||||
|
||||
info "Using these versions for building $PACKAGE:"
|
||||
sw_vers
|
||||
python3 --version
|
||||
echo -n "Pyinstaller "
|
||||
pyinstaller --version
|
||||
|
||||
rm -rf ./dist
|
||||
|
||||
git submodule init
|
||||
git submodule update
|
||||
|
||||
rm -rf $BUILDDIR > /dev/null 2>&1
|
||||
mkdir $BUILDDIR
|
||||
|
||||
cp -R ./contrib/deterministic-build/electrum-locale/locale/ ./electrum/locale/
|
||||
|
||||
|
||||
info "Downloading libusb..."
|
||||
curl https://homebrew.bintray.com/bottles/libusb-1.0.22.el_capitan.bottle.tar.gz | \
|
||||
tar xz --directory $BUILDDIR
|
||||
cp $BUILDDIR/libusb/1.0.22/lib/libusb-1.0.dylib contrib/osx
|
||||
echo "82c368dfd4da017ceb32b12ca885576f325503428a4966cc09302cbd62702493 contrib/osx/libusb-1.0.dylib" | \
|
||||
shasum -a 256 -c || fail "libusb checksum mismatched"
|
||||
|
||||
info "Building libsecp256k1"
|
||||
brew install autoconf automake libtool
|
||||
git clone https://github.com/bitcoin-core/secp256k1 $BUILDDIR/secp256k1
|
||||
pushd $BUILDDIR/secp256k1
|
||||
git reset --hard $LIBSECP_VERSION
|
||||
git clean -f -x -q
|
||||
./autogen.sh
|
||||
./configure --enable-module-recovery --enable-experimental --enable-module-ecdh --disable-jni
|
||||
make
|
||||
popd
|
||||
cp $BUILDDIR/secp256k1/.libs/libsecp256k1.0.dylib contrib/osx
|
||||
|
||||
info "Building CalinsQRReader..."
|
||||
d=contrib/osx/CalinsQRReader
|
||||
pushd $d
|
||||
rm -fr build
|
||||
# prefer building using xcode ourselves. otherwise fallback to prebuilt binary
|
||||
xcodebuild || cp -r prebuilt_qr build || fail "Could not build CalinsQRReader"
|
||||
popd
|
||||
DoCodeSignMaybe "CalinsQRReader.app" "${d}/build/Release/CalinsQRReader.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop
|
||||
|
||||
|
||||
info "Installing requirements..."
|
||||
python3 -m pip install -Ir ./contrib/deterministic-build/requirements.txt --user || \
|
||||
fail "Could not install requirements"
|
||||
|
||||
info "Installing hardware wallet requirements..."
|
||||
python3 -m pip install -Ir ./contrib/deterministic-build/requirements-hw.txt --user || \
|
||||
fail "Could not install hardware wallet requirements"
|
||||
|
||||
info "Building $PACKAGE..."
|
||||
python3 -m pip install --user . > /dev/null || fail "Could not build $PACKAGE"
|
||||
|
||||
info "Faking timestamps..."
|
||||
for d in ~/Library/Python/ ~/.pyenv .; do
|
||||
pushd $d
|
||||
find . -exec touch -t '200101220000' {} +
|
||||
popd
|
||||
done
|
||||
|
||||
info "Building binary"
|
||||
APP_SIGN="$APP_SIGN" pyinstaller --noconfirm --ascii --clean --name $VERSION contrib/osx/osx.spec || fail "Could not build binary"
|
||||
|
||||
info "Adding bitcoin URI types to Info.plist"
|
||||
plutil -insert 'CFBundleURLTypes' \
|
||||
-xml '<array><dict> <key>CFBundleURLName</key> <string>bitcoin</string> <key>CFBundleURLSchemes</key> <array><string>bitcoin</string></array> </dict></array>' \
|
||||
-- dist/$PACKAGE.app/Contents/Info.plist \
|
||||
|| fail "Could not add keys to Info.plist. Make sure the program 'plutil' exists and is installed."
|
||||
|
||||
DoCodeSignMaybe "app bundle" "dist/${PACKAGE}.app" "$APP_SIGN" # If APP_SIGN is empty will be a noop
|
||||
|
||||
info "Creating .DMG"
|
||||
hdiutil create -fs HFS+ -volname $PACKAGE -srcfolder dist/$PACKAGE.app dist/electrum-$VERSION.dmg || fail "Could not create .DMG"
|
||||
|
||||
DoCodeSignMaybe ".DMG" "dist/electrum-${VERSION}.dmg" "$APP_SIGN" # If APP_SIGN is empty will be a noop
|
||||
|
||||
if [ -z "$APP_SIGN" ]; then
|
||||
warn "App was built successfully but was not code signed. Users may get security warnings from macOS."
|
||||
warn "Specify a valid code signing identity as the first argument to this script to enable code signing."
|
||||
fi
|
||||
162
contrib/osx/osx.spec
Normal file
@ -0,0 +1,162 @@
|
||||
# -*- mode: python -*-
|
||||
|
||||
from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs
|
||||
|
||||
import sys, os
|
||||
|
||||
PACKAGE='Electrum'
|
||||
PYPKG='electrum'
|
||||
MAIN_SCRIPT='run_electrum'
|
||||
ICONS_FILE=PYPKG + '/gui/icons/electrum.icns'
|
||||
APP_SIGN = os.environ.get('APP_SIGN', '')
|
||||
|
||||
def fail(*msg):
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
print("\r🗯 {}ERROR:{}".format(RED, NC), *msg)
|
||||
sys.exit(1)
|
||||
|
||||
def codesign(identity, binary):
|
||||
d = os.path.dirname(binary)
|
||||
saved_dir=None
|
||||
if d:
|
||||
# switch to directory of the binary so codesign verbose messages don't include long path
|
||||
saved_dir = os.path.abspath(os.path.curdir)
|
||||
os.chdir(d)
|
||||
binary = os.path.basename(binary)
|
||||
os.system("codesign -v -f -s '{}' '{}'".format(identity, binary))==0 or fail("Could not code sign " + binary)
|
||||
if saved_dir:
|
||||
os.chdir(saved_dir)
|
||||
|
||||
def monkey_patch_pyinstaller_for_codesigning(identity):
|
||||
# Monkey-patch PyInstaller so that we app-sign all binaries *after* they are modified by PyInstaller
|
||||
# If we app-sign before that point, the signature will be invalid because PyInstaller modifies
|
||||
# @loader_path in the Mach-O loader table.
|
||||
try:
|
||||
import PyInstaller.depend.dylib
|
||||
_saved_func = PyInstaller.depend.dylib.mac_set_relative_dylib_deps
|
||||
except (ImportError, NameError, AttributeError):
|
||||
# Hmm. Likely wrong PyInstaller version.
|
||||
fail("Could not monkey-patch PyInstaller for code signing. Please ensure that you are using PyInstaller 3.4.")
|
||||
_signed = set()
|
||||
def my_func(fn, distname):
|
||||
_saved_func(fn, distname)
|
||||
if (fn, distname) not in _signed:
|
||||
codesign(identity, fn)
|
||||
_signed.add((fn,distname)) # remember we signed it so we don't sign again
|
||||
PyInstaller.depend.dylib.mac_set_relative_dylib_deps = my_func
|
||||
|
||||
|
||||
for i, x in enumerate(sys.argv):
|
||||
if x == '--name':
|
||||
VERSION = sys.argv[i+1]
|
||||
break
|
||||
else:
|
||||
raise Exception('no version')
|
||||
|
||||
electrum = os.path.abspath(".") + "/"
|
||||
block_cipher = None
|
||||
|
||||
# see https://github.com/pyinstaller/pyinstaller/issues/2005
|
||||
hiddenimports = []
|
||||
hiddenimports += collect_submodules('trezorlib')
|
||||
hiddenimports += collect_submodules('safetlib')
|
||||
hiddenimports += collect_submodules('btchip')
|
||||
hiddenimports += collect_submodules('keepkeylib')
|
||||
hiddenimports += collect_submodules('websocket')
|
||||
hiddenimports += collect_submodules('ckcc')
|
||||
|
||||
# safetlib imports PyQt5.Qt. We use a local updated copy of pinmatrix.py until they
|
||||
# release a new version that includes https://github.com/archos-safe-t/python-safet/commit/b1eab3dba4c04fdfc1fcf17b66662c28c5f2380e
|
||||
hiddenimports.remove('safetlib.qt.pinmatrix')
|
||||
|
||||
|
||||
datas = [
|
||||
(electrum + PYPKG + '/*.json', PYPKG),
|
||||
(electrum + PYPKG + '/wordlist/english.txt', PYPKG + '/wordlist'),
|
||||
(electrum + PYPKG + '/locale', PYPKG + '/locale'),
|
||||
(electrum + PYPKG + '/plugins', PYPKG + '/plugins'),
|
||||
(electrum + PYPKG + '/gui/icons', PYPKG + '/gui/icons'),
|
||||
]
|
||||
datas += collect_data_files('trezorlib')
|
||||
datas += collect_data_files('safetlib')
|
||||
datas += collect_data_files('btchip')
|
||||
datas += collect_data_files('keepkeylib')
|
||||
datas += collect_data_files('ckcc')
|
||||
|
||||
# Add the QR Scanner helper app
|
||||
datas += [(electrum + "contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app", "./contrib/osx/CalinsQRReader/build/Release/CalinsQRReader.app")]
|
||||
|
||||
# Add libusb so Trezor and Safe-T mini will work
|
||||
binaries = [(electrum + "contrib/osx/libusb-1.0.dylib", ".")]
|
||||
binaries += [(electrum + "contrib/osx/libsecp256k1.0.dylib", ".")]
|
||||
|
||||
# Workaround for "Retro Look":
|
||||
binaries += [b for b in collect_dynamic_libs('PyQt5') if 'macstyle' in b[0]]
|
||||
|
||||
# We don't put these files in to actually include them in the script but to make the Analysis method scan them for imports
|
||||
a = Analysis([electrum+ MAIN_SCRIPT,
|
||||
electrum+'electrum/gui/qt/main_window.py',
|
||||
electrum+'electrum/gui/text.py',
|
||||
electrum+'electrum/util.py',
|
||||
electrum+'electrum/wallet.py',
|
||||
electrum+'electrum/simple_config.py',
|
||||
electrum+'electrum/bitcoin.py',
|
||||
electrum+'electrum/dnssec.py',
|
||||
electrum+'electrum/commands.py',
|
||||
electrum+'electrum/plugins/cosigner_pool/qt.py',
|
||||
electrum+'electrum/plugins/email_requests/qt.py',
|
||||
electrum+'electrum/plugins/trezor/qt.py',
|
||||
electrum+'electrum/plugins/safe_t/client.py',
|
||||
electrum+'electrum/plugins/safe_t/qt.py',
|
||||
electrum+'electrum/plugins/keepkey/qt.py',
|
||||
electrum+'electrum/plugins/ledger/qt.py',
|
||||
electrum+'electrum/plugins/coldcard/qt.py',
|
||||
],
|
||||
binaries=binaries,
|
||||
datas=datas,
|
||||
hiddenimports=hiddenimports,
|
||||
hookspath=[])
|
||||
|
||||
# http://stackoverflow.com/questions/19055089/pyinstaller-onefile-warning-pyconfig-h-when-importing-scipy-or-scipy-signal
|
||||
for d in a.datas:
|
||||
if 'pyconfig' in d[0]:
|
||||
a.datas.remove(d)
|
||||
break
|
||||
|
||||
# Strip out parts of Qt that we never use. Reduces binary size by tens of MBs. see #4815
|
||||
qt_bins2remove=('qtweb', 'qt3d', 'qtgame', 'qtdesigner', 'qtquick', 'qtlocation', 'qttest', 'qtxml')
|
||||
print("Removing Qt binaries:", *qt_bins2remove)
|
||||
for x in a.binaries.copy():
|
||||
for r in qt_bins2remove:
|
||||
if x[0].lower().startswith(r):
|
||||
a.binaries.remove(x)
|
||||
print('----> Removed x =', x)
|
||||
|
||||
# If code signing, monkey-patch in a code signing step to pyinstaller. See: https://github.com/spesmilo/electrum/issues/4994
|
||||
if APP_SIGN:
|
||||
monkey_patch_pyinstaller_for_codesigning(APP_SIGN)
|
||||
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.datas,
|
||||
name=PACKAGE,
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
icon=electrum+ICONS_FILE,
|
||||
console=False)
|
||||
|
||||
app = BUNDLE(exe,
|
||||
version = VERSION,
|
||||
name=PACKAGE + '.app',
|
||||
icon=electrum+ICONS_FILE,
|
||||
bundle_identifier=None,
|
||||
info_plist={
|
||||
'NSHighResolutionCapable': 'True',
|
||||
'NSSupportsAutomaticGraphicsSwitching': 'True'
|
||||
}
|
||||
)
|
||||
@ -85,4 +85,4 @@ dmg dmg Electrum_uncompressed.dmg electrum-$VERSION.dmg || fail "Unable to creat
|
||||
rm Electrum_uncompressed.dmg
|
||||
|
||||
echo "Done."
|
||||
md5sum electrum-$VERSION.dmg
|
||||
sha256sum electrum-$VERSION.dmg
|
||||
@ -1,6 +1,8 @@
|
||||
Cython>=0.27
|
||||
trezor[hidapi]>=0.9.0
|
||||
trezor[hidapi]>=0.11.1
|
||||
safet[hidapi]>=0.1.0
|
||||
keepkey
|
||||
btchip-python
|
||||
btchip-python>=0.1.26
|
||||
ckcc-protocol>=0.7.2
|
||||
websocket-client
|
||||
hidapi
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
pyaes>=0.1a1
|
||||
ecdsa>=0.9
|
||||
pbkdf2
|
||||
requests
|
||||
qrcode
|
||||
protobuf
|
||||
dnspython
|
||||
jsonrpclib-pelix
|
||||
PySocks>=1.6.6
|
||||
qdarkstyle<3.0
|
||||
qdarkstyle<2.6
|
||||
aiorpcx>=0.9,<0.11
|
||||
aiohttp>=3.3.0
|
||||
aiohttp_socks
|
||||
certifi
|
||||
pylibscrypt==1.7.1
|
||||
|
||||
4
contrib/sign_version
Executable file
@ -0,0 +1,4 @@
|
||||
#!/bin/bash
|
||||
version=`python3 -c "import electrum; print(electrum.version.ELECTRUM_VERSION)"`
|
||||
sig=`./run_electrum -w $SIGNING_WALLET signmessage $SIGNING_ADDRESS $version`
|
||||
echo "{ \"version\":\"$version\", \"signatures\":{ \"$SIGNING_ADDRESS\":\"$sig\"}}"
|
||||
@ -17,11 +17,11 @@ if [ -e ./env/bin/activate ]; then
|
||||
else
|
||||
virtualenv env -p `which python3`
|
||||
source ./env/bin/activate
|
||||
python3 setup.py install
|
||||
python3 -m pip install .[fast]
|
||||
fi
|
||||
|
||||
export PYTHONPATH="/usr/local/lib/python${PYTHON_VER}/site-packages:$PYTHONPATH"
|
||||
|
||||
./electrum "$@"
|
||||
./run_electrum "$@"
|
||||
|
||||
deactivate
|
||||
|
||||
@ -1,16 +0,0 @@
|
||||
# Configuration file for the Electrum client
|
||||
# Settings defined here are shared across wallets
|
||||
#
|
||||
# copy this file to /etc/electrum.conf if you want read-only settings
|
||||
|
||||
[client]
|
||||
server = electrum.novit.ro:50001:t
|
||||
proxy = None
|
||||
gap_limit = 5
|
||||
# booleans use python syntax
|
||||
use_change = True
|
||||
gui = qt
|
||||
num_zeros = 2
|
||||
# default transaction fee is in Satoshis
|
||||
fee = 10000
|
||||
winpos-qt = [799, 226, 877, 435]
|
||||
@ -3,19 +3,20 @@
|
||||
|
||||
[Desktop Entry]
|
||||
Comment=Lightweight Bitcoin Client
|
||||
Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum %u"
|
||||
Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum %u"
|
||||
GenericName[en_US]=Bitcoin Wallet
|
||||
GenericName=Bitcoin Wallet
|
||||
Icon=electrum
|
||||
Name[en_US]=Electrum Bitcoin Wallet
|
||||
Name=Electrum Bitcoin Wallet
|
||||
Categories=Finance;Network;
|
||||
StartupNotify=false
|
||||
StartupNotify=true
|
||||
StartupWMClass=electrum
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=x-scheme-handler/bitcoin;
|
||||
Actions=Testnet;
|
||||
|
||||
[Desktop Action Testnet]
|
||||
Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\" electrum --testnet %u"
|
||||
Exec=sh -c "PATH=\"\\$HOME/.local/bin:\\$PATH\"; electrum --testnet %u"
|
||||
Name=Testnet mode
|
||||
|
||||
BIN
electrum.icns
@ -1,14 +1,17 @@
|
||||
from .version import ELECTRUM_VERSION
|
||||
from .util import format_satoshis, print_msg, print_error, set_verbosity
|
||||
from .wallet import Synchronizer, Wallet
|
||||
from .wallet import Wallet
|
||||
from .storage import WalletStorage
|
||||
from .coinchooser import COIN_CHOOSERS
|
||||
from .network import Network, pick_random_server
|
||||
from .interface import Connection, Interface
|
||||
from .interface import Interface
|
||||
from .simple_config import SimpleConfig, get_config, set_config
|
||||
from . import bitcoin
|
||||
from . import transaction
|
||||
from . import daemon
|
||||
from .transaction import Transaction
|
||||
from .plugins import BasePlugin
|
||||
from .plugin import BasePlugin
|
||||
from .commands import Commands, known_commands
|
||||
|
||||
|
||||
__version__ = ELECTRUM_VERSION
|
||||
877
electrum/address_synchronizer.py
Normal file
@ -0,0 +1,877 @@
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2018 The Electrum Developers
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import threading
|
||||
import asyncio
|
||||
import itertools
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from . import bitcoin
|
||||
from .bitcoin import COINBASE_MATURITY, TYPE_ADDRESS, TYPE_PUBKEY
|
||||
from .util import PrintError, profiler, bfh, TxMinedInfo
|
||||
from .transaction import Transaction, TxOutput
|
||||
from .synchronizer import Synchronizer
|
||||
from .verifier import SPV
|
||||
from .blockchain import hash_header
|
||||
from .i18n import _
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .storage import WalletStorage
|
||||
from .network import Network
|
||||
|
||||
|
||||
TX_HEIGHT_LOCAL = -2
|
||||
TX_HEIGHT_UNCONF_PARENT = -1
|
||||
TX_HEIGHT_UNCONFIRMED = 0
|
||||
|
||||
class AddTransactionException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnrelatedTransactionException(AddTransactionException):
|
||||
def __str__(self):
|
||||
return _("Transaction is unrelated to this wallet.")
|
||||
|
||||
|
||||
class AddressSynchronizer(PrintError):
|
||||
"""
|
||||
inherited by wallet
|
||||
"""
|
||||
|
||||
def __init__(self, storage: 'WalletStorage'):
|
||||
self.storage = storage
|
||||
self.network = None # type: Network
|
||||
# verifier (SPV) and synchronizer are started in start_network
|
||||
self.synchronizer = None # type: Synchronizer
|
||||
self.verifier = None # type: SPV
|
||||
# locks: if you need to take multiple ones, acquire them in the order they are defined here!
|
||||
self.lock = threading.RLock()
|
||||
self.transaction_lock = threading.RLock()
|
||||
# address -> list(txid, height)
|
||||
self.history = storage.get('addr_history',{})
|
||||
# Verified transactions. txid -> TxMinedInfo. Access with self.lock.
|
||||
verified_tx = storage.get('verified_tx3', {})
|
||||
self.verified_tx = {} # type: Dict[str, TxMinedInfo]
|
||||
for txid, (height, timestamp, txpos, header_hash, flodata) in verified_tx.items():
|
||||
self.verified_tx[txid] = TxMinedInfo(height=height,
|
||||
conf=None,
|
||||
timestamp=timestamp,
|
||||
txpos=txpos,
|
||||
header_hash=header_hash,
|
||||
flodata=flodata)
|
||||
# Transactions pending verification. txid -> tx_height. Access with self.lock.
|
||||
self.unverified_tx = defaultdict(int)
|
||||
# true when synchronized
|
||||
self.up_to_date = False
|
||||
# thread local storage for caching stuff
|
||||
self.threadlocal_cache = threading.local()
|
||||
|
||||
self.load_and_cleanup()
|
||||
|
||||
def with_transaction_lock(func):
|
||||
def func_wrapper(self, *args, **kwargs):
|
||||
with self.transaction_lock:
|
||||
return func(self, *args, **kwargs)
|
||||
return func_wrapper
|
||||
|
||||
def load_and_cleanup(self):
|
||||
self.load_transactions()
|
||||
self.load_local_history()
|
||||
self.check_history()
|
||||
self.load_unverified_transactions()
|
||||
self.remove_local_transactions_we_dont_have()
|
||||
|
||||
def is_mine(self, address):
|
||||
return address in self.history
|
||||
|
||||
def get_addresses(self):
|
||||
return sorted(self.history.keys())
|
||||
|
||||
def get_address_history(self, addr):
|
||||
h = []
|
||||
# we need self.transaction_lock but get_tx_height will take self.lock
|
||||
# so we need to take that too here, to enforce order of locks
|
||||
with self.lock, self.transaction_lock:
|
||||
related_txns = self._history_local.get(addr, set())
|
||||
for tx_hash in related_txns:
|
||||
tx_height = self.get_tx_height(tx_hash).height
|
||||
h.append((tx_hash, tx_height))
|
||||
return h
|
||||
|
||||
def get_address_history_len(self, addr: str) -> int:
|
||||
"""Return number of transactions where address is involved."""
|
||||
return len(self._history_local.get(addr, ()))
|
||||
|
||||
def get_txin_address(self, txi):
|
||||
addr = txi.get('address')
|
||||
if addr and addr != "(pubkey)":
|
||||
return addr
|
||||
prevout_hash = txi.get('prevout_hash')
|
||||
prevout_n = txi.get('prevout_n')
|
||||
dd = self.txo.get(prevout_hash, {})
|
||||
for addr, l in dd.items():
|
||||
for n, v, is_cb in l:
|
||||
if n == prevout_n:
|
||||
return addr
|
||||
return None
|
||||
|
||||
def get_txout_address(self, txo: TxOutput):
|
||||
if txo.type == TYPE_ADDRESS:
|
||||
addr = txo.address
|
||||
elif txo.type == TYPE_PUBKEY:
|
||||
addr = bitcoin.public_key_to_p2pkh(bfh(txo.address))
|
||||
else:
|
||||
addr = None
|
||||
return addr
|
||||
|
||||
def load_unverified_transactions(self):
|
||||
# review transactions that are in the history
|
||||
for addr, hist in self.history.items():
|
||||
for tx_hash, tx_height in hist:
|
||||
# add it in case it was previously unconfirmed
|
||||
self.add_unverified_tx(tx_hash, tx_height)
|
||||
|
||||
def start_network(self, network):
|
||||
self.network = network
|
||||
if self.network is not None:
|
||||
self.synchronizer = Synchronizer(self)
|
||||
self.verifier = SPV(self.network, self)
|
||||
|
||||
def stop_threads(self, write_to_disk=True):
|
||||
if self.network:
|
||||
if self.synchronizer:
|
||||
asyncio.run_coroutine_threadsafe(self.synchronizer.stop(), self.network.asyncio_loop)
|
||||
self.synchronizer = None
|
||||
if self.verifier:
|
||||
asyncio.run_coroutine_threadsafe(self.verifier.stop(), self.network.asyncio_loop)
|
||||
self.verifier = None
|
||||
self.storage.put('stored_height', self.get_local_height())
|
||||
if write_to_disk:
|
||||
self.save_transactions()
|
||||
self.save_verified_tx()
|
||||
self.storage.write()
|
||||
|
||||
def add_address(self, address):
|
||||
if address not in self.history:
|
||||
self.history[address] = []
|
||||
self.set_up_to_date(False)
|
||||
if self.synchronizer:
|
||||
self.synchronizer.add(address)
|
||||
|
||||
def get_conflicting_transactions(self, tx_hash, tx):
|
||||
"""Returns a set of transaction hashes from the wallet history that are
|
||||
directly conflicting with tx, i.e. they have common outpoints being
|
||||
spent with tx. If the tx is already in wallet history, that will not be
|
||||
reported as a conflict.
|
||||
"""
|
||||
conflicting_txns = set()
|
||||
with self.transaction_lock:
|
||||
for txin in tx.inputs():
|
||||
if txin['type'] == 'coinbase':
|
||||
continue
|
||||
prevout_hash = txin['prevout_hash']
|
||||
prevout_n = txin['prevout_n']
|
||||
spending_tx_hash = self.spent_outpoints[prevout_hash].get(prevout_n)
|
||||
if spending_tx_hash is None:
|
||||
continue
|
||||
# this outpoint has already been spent, by spending_tx
|
||||
assert spending_tx_hash in self.transactions
|
||||
conflicting_txns |= {spending_tx_hash}
|
||||
if tx_hash in conflicting_txns:
|
||||
# this tx is already in history, so it conflicts with itself
|
||||
if len(conflicting_txns) > 1:
|
||||
raise Exception('Found conflicting transactions already in wallet history.')
|
||||
conflicting_txns -= {tx_hash}
|
||||
return conflicting_txns
|
||||
|
||||
def add_transaction(self, tx_hash, tx, allow_unrelated=False):
|
||||
assert tx_hash, tx_hash
|
||||
assert tx, tx
|
||||
assert tx.is_complete()
|
||||
# assert tx_hash == tx.txid() # disabled as expensive; test done by Synchronizer.
|
||||
# we need self.transaction_lock but get_tx_height will take self.lock
|
||||
# so we need to take that too here, to enforce order of locks
|
||||
with self.lock, self.transaction_lock:
|
||||
# NOTE: returning if tx in self.transactions might seem like a good idea
|
||||
# BUT we track is_mine inputs in a txn, and during subsequent calls
|
||||
# of add_transaction tx, we might learn of more-and-more inputs of
|
||||
# being is_mine, as we roll the gap_limit forward
|
||||
is_coinbase = tx.inputs()[0]['type'] == 'coinbase'
|
||||
tx_height = self.get_tx_height(tx_hash).height
|
||||
if not allow_unrelated:
|
||||
# note that during sync, if the transactions are not properly sorted,
|
||||
# it could happen that we think tx is unrelated but actually one of the inputs is is_mine.
|
||||
# this is the main motivation for allow_unrelated
|
||||
is_mine = any([self.is_mine(self.get_txin_address(txin)) for txin in tx.inputs()])
|
||||
is_for_me = any([self.is_mine(self.get_txout_address(txo)) for txo in tx.outputs()])
|
||||
if not is_mine and not is_for_me:
|
||||
raise UnrelatedTransactionException()
|
||||
# Find all conflicting transactions.
|
||||
# In case of a conflict,
|
||||
# 1. confirmed > mempool > local
|
||||
# 2. this new txn has priority over existing ones
|
||||
# When this method exits, there must NOT be any conflict, so
|
||||
# either keep this txn and remove all conflicting (along with dependencies)
|
||||
# or drop this txn
|
||||
conflicting_txns = self.get_conflicting_transactions(tx_hash, tx)
|
||||
if conflicting_txns:
|
||||
existing_mempool_txn = any(
|
||||
self.get_tx_height(tx_hash2).height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT)
|
||||
for tx_hash2 in conflicting_txns)
|
||||
existing_confirmed_txn = any(
|
||||
self.get_tx_height(tx_hash2).height > 0
|
||||
for tx_hash2 in conflicting_txns)
|
||||
if existing_confirmed_txn and tx_height <= 0:
|
||||
# this is a non-confirmed tx that conflicts with confirmed txns; drop.
|
||||
return False
|
||||
if existing_mempool_txn and tx_height == TX_HEIGHT_LOCAL:
|
||||
# this is a local tx that conflicts with non-local txns; drop.
|
||||
return False
|
||||
# keep this txn and remove all conflicting
|
||||
to_remove = set()
|
||||
to_remove |= conflicting_txns
|
||||
for conflicting_tx_hash in conflicting_txns:
|
||||
to_remove |= self.get_depending_transactions(conflicting_tx_hash)
|
||||
for tx_hash2 in to_remove:
|
||||
self.remove_transaction(tx_hash2)
|
||||
# add inputs
|
||||
def add_value_from_prev_output():
|
||||
dd = self.txo.get(prevout_hash, {})
|
||||
# note: this nested loop takes linear time in num is_mine outputs of prev_tx
|
||||
for addr, outputs in dd.items():
|
||||
# note: instead of [(n, v, is_cb), ...]; we could store: {n -> (v, is_cb)}
|
||||
for n, v, is_cb in outputs:
|
||||
if n == prevout_n:
|
||||
if addr and self.is_mine(addr):
|
||||
if d.get(addr) is None:
|
||||
d[addr] = set()
|
||||
d[addr].add((ser, v))
|
||||
return
|
||||
self.txi[tx_hash] = d = {}
|
||||
for txi in tx.inputs():
|
||||
if txi['type'] == 'coinbase':
|
||||
continue
|
||||
prevout_hash = txi['prevout_hash']
|
||||
prevout_n = txi['prevout_n']
|
||||
ser = prevout_hash + ':%d' % prevout_n
|
||||
self.spent_outpoints[prevout_hash][prevout_n] = tx_hash
|
||||
add_value_from_prev_output()
|
||||
# add outputs
|
||||
self.txo[tx_hash] = d = {}
|
||||
for n, txo in enumerate(tx.outputs()):
|
||||
v = txo[2]
|
||||
ser = tx_hash + ':%d'%n
|
||||
addr = self.get_txout_address(txo)
|
||||
if addr and self.is_mine(addr):
|
||||
if d.get(addr) is None:
|
||||
d[addr] = []
|
||||
d[addr].append((n, v, is_coinbase))
|
||||
# give v to txi that spends me
|
||||
next_tx = self.spent_outpoints[tx_hash].get(n)
|
||||
if next_tx is not None:
|
||||
dd = self.txi.get(next_tx, {})
|
||||
if dd.get(addr) is None:
|
||||
dd[addr] = set()
|
||||
if (ser, v) not in dd[addr]:
|
||||
dd[addr].add((ser, v))
|
||||
self._add_tx_to_local_history(next_tx)
|
||||
# add to local history
|
||||
self._add_tx_to_local_history(tx_hash)
|
||||
# save
|
||||
self.transactions[tx_hash] = tx
|
||||
return True
|
||||
|
||||
def remove_transaction(self, tx_hash):
|
||||
def remove_from_spent_outpoints():
|
||||
# undo spends in spent_outpoints
|
||||
if tx is not None: # if we have the tx, this branch is faster
|
||||
for txin in tx.inputs():
|
||||
if txin['type'] == 'coinbase':
|
||||
continue
|
||||
prevout_hash = txin['prevout_hash']
|
||||
prevout_n = txin['prevout_n']
|
||||
self.spent_outpoints[prevout_hash].pop(prevout_n, None)
|
||||
if not self.spent_outpoints[prevout_hash]:
|
||||
self.spent_outpoints.pop(prevout_hash)
|
||||
else: # expensive but always works
|
||||
for prevout_hash, d in list(self.spent_outpoints.items()):
|
||||
for prevout_n, spending_txid in d.items():
|
||||
if spending_txid == tx_hash:
|
||||
self.spent_outpoints[prevout_hash].pop(prevout_n, None)
|
||||
if not self.spent_outpoints[prevout_hash]:
|
||||
self.spent_outpoints.pop(prevout_hash)
|
||||
# Remove this tx itself; if nothing spends from it.
|
||||
# It is not so clear what to do if other txns spend from it, but it will be
|
||||
# removed when those other txns are removed.
|
||||
if not self.spent_outpoints[tx_hash]:
|
||||
self.spent_outpoints.pop(tx_hash)
|
||||
|
||||
with self.transaction_lock:
|
||||
self.print_error("removing tx from history", tx_hash)
|
||||
tx = self.transactions.pop(tx_hash, None)
|
||||
remove_from_spent_outpoints()
|
||||
self._remove_tx_from_local_history(tx_hash)
|
||||
self.txi.pop(tx_hash, None)
|
||||
self.txo.pop(tx_hash, None)
|
||||
|
||||
def get_depending_transactions(self, tx_hash):
|
||||
"""Returns all (grand-)children of tx_hash in this wallet."""
|
||||
children = set()
|
||||
for other_hash in self.spent_outpoints[tx_hash].values():
|
||||
children.add(other_hash)
|
||||
children |= self.get_depending_transactions(other_hash)
|
||||
return children
|
||||
|
||||
def receive_tx_callback(self, tx_hash, tx, tx_height):
|
||||
self.add_unverified_tx(tx_hash, tx_height)
|
||||
self.add_transaction(tx_hash, tx, allow_unrelated=True)
|
||||
|
||||
def receive_history_callback(self, addr, hist, tx_fees):
|
||||
with self.lock:
|
||||
old_hist = self.get_address_history(addr)
|
||||
for tx_hash, height in old_hist:
|
||||
if (tx_hash, height) not in hist:
|
||||
# make tx local
|
||||
self.unverified_tx.pop(tx_hash, None)
|
||||
self.verified_tx.pop(tx_hash, None)
|
||||
if self.verifier:
|
||||
self.verifier.remove_spv_proof_for_tx(tx_hash)
|
||||
self.history[addr] = hist
|
||||
|
||||
for tx_hash, tx_height in hist:
|
||||
# add it in case it was previously unconfirmed
|
||||
self.add_unverified_tx(tx_hash, tx_height)
|
||||
# if addr is new, we have to recompute txi and txo
|
||||
tx = self.transactions.get(tx_hash)
|
||||
if tx is None:
|
||||
continue
|
||||
self.add_transaction(tx_hash, tx, allow_unrelated=True)
|
||||
|
||||
# Store fees
|
||||
self.tx_fees.update(tx_fees)
|
||||
|
||||
@profiler
|
||||
def load_transactions(self):
|
||||
# load txi, txo, tx_fees
|
||||
# bookkeeping data of is_mine inputs of transactions
|
||||
self.txi = self.storage.get('txi', {}) # txid -> address -> (prev_outpoint, value)
|
||||
for txid, d in list(self.txi.items()):
|
||||
for addr, lst in d.items():
|
||||
self.txi[txid][addr] = set([tuple(x) for x in lst])
|
||||
# bookkeeping data of is_mine outputs of transactions
|
||||
self.txo = self.storage.get('txo', {}) # txid -> address -> (output_index, value, is_coinbase)
|
||||
self.tx_fees = self.storage.get('tx_fees', {})
|
||||
tx_list = self.storage.get('transactions', {})
|
||||
# load transactions
|
||||
self.transactions = {}
|
||||
for tx_hash, raw in tx_list.items():
|
||||
tx = Transaction(raw)
|
||||
self.transactions[tx_hash] = tx
|
||||
if self.txi.get(tx_hash) is None and self.txo.get(tx_hash) is None:
|
||||
self.print_error("removing unreferenced tx", tx_hash)
|
||||
self.transactions.pop(tx_hash)
|
||||
# load spent_outpoints
|
||||
_spent_outpoints = self.storage.get('spent_outpoints', {})
|
||||
self.spent_outpoints = defaultdict(dict)
|
||||
for prevout_hash, d in _spent_outpoints.items():
|
||||
for prevout_n_str, spending_txid in d.items():
|
||||
prevout_n = int(prevout_n_str)
|
||||
if spending_txid not in self.transactions:
|
||||
continue # only care about txns we have
|
||||
self.spent_outpoints[prevout_hash][prevout_n] = spending_txid
|
||||
|
||||
@profiler
|
||||
def load_local_history(self):
|
||||
self._history_local = {} # address -> set(txid)
|
||||
self._address_history_changed_events = defaultdict(asyncio.Event) # address -> Event
|
||||
for txid in itertools.chain(self.txi, self.txo):
|
||||
self._add_tx_to_local_history(txid)
|
||||
|
||||
@profiler
|
||||
def check_history(self):
|
||||
save = False
|
||||
hist_addrs_mine = list(filter(lambda k: self.is_mine(k), self.history.keys()))
|
||||
hist_addrs_not_mine = list(filter(lambda k: not self.is_mine(k), self.history.keys()))
|
||||
for addr in hist_addrs_not_mine:
|
||||
self.history.pop(addr)
|
||||
save = True
|
||||
for addr in hist_addrs_mine:
|
||||
hist = self.history[addr]
|
||||
for tx_hash, tx_height in hist:
|
||||
if self.txi.get(tx_hash) or self.txo.get(tx_hash):
|
||||
continue
|
||||
tx = self.transactions.get(tx_hash)
|
||||
if tx is not None:
|
||||
self.add_transaction(tx_hash, tx, allow_unrelated=True)
|
||||
save = True
|
||||
if save:
|
||||
self.save_transactions()
|
||||
|
||||
def remove_local_transactions_we_dont_have(self):
|
||||
txid_set = set(self.txi) | set(self.txo)
|
||||
for txid in txid_set:
|
||||
tx_height = self.get_tx_height(txid).height
|
||||
if tx_height == TX_HEIGHT_LOCAL and txid not in self.transactions:
|
||||
self.remove_transaction(txid)
|
||||
|
||||
@profiler
|
||||
def save_transactions(self, write=False):
|
||||
with self.transaction_lock:
|
||||
tx = {}
|
||||
for k,v in self.transactions.items():
|
||||
tx[k] = str(v)
|
||||
self.storage.put('transactions', tx)
|
||||
self.storage.put('txi', self.txi)
|
||||
self.storage.put('txo', self.txo)
|
||||
self.storage.put('tx_fees', self.tx_fees)
|
||||
self.storage.put('addr_history', self.history)
|
||||
self.storage.put('spent_outpoints', self.spent_outpoints)
|
||||
if write:
|
||||
self.storage.write()
|
||||
|
||||
def save_verified_tx(self, write=False):
|
||||
with self.lock:
|
||||
verified_tx_to_save = {}
|
||||
for txid, tx_info in self.verified_tx.items():
|
||||
verified_tx_to_save[txid] = (tx_info.height, tx_info.timestamp,
|
||||
tx_info.txpos, tx_info.header_hash, tx_info.flodata)
|
||||
self.storage.put('verified_tx3', verified_tx_to_save)
|
||||
if write:
|
||||
self.storage.write()
|
||||
|
||||
def clear_history(self):
|
||||
with self.lock:
|
||||
with self.transaction_lock:
|
||||
self.txi = {}
|
||||
self.txo = {}
|
||||
self.tx_fees = {}
|
||||
self.spent_outpoints = defaultdict(dict)
|
||||
self.history = {}
|
||||
self.verified_tx = {}
|
||||
self.transactions = {} # type: Dict[str, Transaction]
|
||||
self.save_transactions()
|
||||
|
||||
def get_txpos(self, tx_hash):
|
||||
"""Returns (height, txpos) tuple, even if the tx is unverified."""
|
||||
with self.lock:
|
||||
if tx_hash in self.verified_tx:
|
||||
info = self.verified_tx[tx_hash]
|
||||
return info.height, info.txpos
|
||||
elif tx_hash in self.unverified_tx:
|
||||
height = self.unverified_tx[tx_hash]
|
||||
return (height, 0) if height > 0 else ((1e9 - height), 0)
|
||||
else:
|
||||
return (1e9+1, 0)
|
||||
|
||||
def with_local_height_cached(func):
|
||||
# get local height only once, as it's relatively expensive.
|
||||
# take care that nested calls work as expected
|
||||
def f(self, *args, **kwargs):
|
||||
orig_val = getattr(self.threadlocal_cache, 'local_height', None)
|
||||
self.threadlocal_cache.local_height = orig_val or self.get_local_height()
|
||||
try:
|
||||
return func(self, *args, **kwargs)
|
||||
finally:
|
||||
self.threadlocal_cache.local_height = orig_val
|
||||
return f
|
||||
|
||||
@with_local_height_cached
|
||||
def get_history(self, domain=None):
|
||||
# get domain
|
||||
if domain is None:
|
||||
domain = self.history.keys()
|
||||
domain = set(domain)
|
||||
# 1. Get the history of each address in the domain, maintain the
|
||||
# delta of a tx as the sum of its deltas on domain addresses
|
||||
tx_deltas = defaultdict(int)
|
||||
for addr in domain:
|
||||
h = self.get_address_history(addr)
|
||||
for tx_hash, height in h:
|
||||
delta = self.get_tx_delta(tx_hash, addr)
|
||||
if delta is None or tx_deltas[tx_hash] is None:
|
||||
tx_deltas[tx_hash] = None
|
||||
else:
|
||||
tx_deltas[tx_hash] += delta
|
||||
# 2. create sorted history
|
||||
history = []
|
||||
for tx_hash in tx_deltas:
|
||||
delta = tx_deltas[tx_hash]
|
||||
tx_mined_status = self.get_tx_height(tx_hash)
|
||||
history.append((tx_hash, tx_mined_status, delta))
|
||||
history.sort(key = lambda x: self.get_txpos(x[0]), reverse=True)
|
||||
# 3. add balance
|
||||
c, u, x = self.get_balance(domain)
|
||||
balance = c + u + x
|
||||
h2 = []
|
||||
for tx_hash, tx_mined_status, delta in history:
|
||||
h2.append((tx_hash, tx_mined_status, delta, balance))
|
||||
if balance is None or delta is None:
|
||||
balance = None
|
||||
else:
|
||||
balance -= delta
|
||||
h2.reverse()
|
||||
# fixme: this may happen if history is incomplete
|
||||
if balance not in [None, 0]:
|
||||
self.print_error("Error: history not synchronized")
|
||||
return []
|
||||
|
||||
return h2
|
||||
|
||||
def _add_tx_to_local_history(self, txid):
|
||||
with self.transaction_lock:
|
||||
for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])):
|
||||
cur_hist = self._history_local.get(addr, set())
|
||||
cur_hist.add(txid)
|
||||
self._history_local[addr] = cur_hist
|
||||
self._mark_address_history_changed(addr)
|
||||
|
||||
def _remove_tx_from_local_history(self, txid):
|
||||
with self.transaction_lock:
|
||||
for addr in itertools.chain(self.txi.get(txid, []), self.txo.get(txid, [])):
|
||||
cur_hist = self._history_local.get(addr, set())
|
||||
try:
|
||||
cur_hist.remove(txid)
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
self._history_local[addr] = cur_hist
|
||||
|
||||
def _mark_address_history_changed(self, addr: str) -> None:
|
||||
# history for this address changed, wake up coroutines:
|
||||
self._address_history_changed_events[addr].set()
|
||||
# clear event immediately so that coroutines can wait() for the next change:
|
||||
self._address_history_changed_events[addr].clear()
|
||||
|
||||
async def wait_for_address_history_to_change(self, addr: str) -> None:
|
||||
"""Wait until the server tells us about a new transaction related to addr.
|
||||
|
||||
Unconfirmed and confirmed transactions are not distinguished, and so e.g. SPV
|
||||
is not taken into account.
|
||||
"""
|
||||
assert self.is_mine(addr), "address needs to be is_mine to be watched"
|
||||
await self._address_history_changed_events[addr].wait()
|
||||
|
||||
def add_unverified_tx(self, tx_hash, tx_height):
|
||||
if tx_hash in self.verified_tx:
|
||||
if tx_height in (TX_HEIGHT_UNCONFIRMED, TX_HEIGHT_UNCONF_PARENT):
|
||||
with self.lock:
|
||||
self.verified_tx.pop(tx_hash)
|
||||
if self.verifier:
|
||||
self.verifier.remove_spv_proof_for_tx(tx_hash)
|
||||
else:
|
||||
with self.lock:
|
||||
# tx will be verified only if height > 0
|
||||
self.unverified_tx[tx_hash] = tx_height
|
||||
|
||||
def remove_unverified_tx(self, tx_hash, tx_height):
|
||||
with self.lock:
|
||||
new_height = self.unverified_tx.get(tx_hash)
|
||||
if new_height == tx_height:
|
||||
self.unverified_tx.pop(tx_hash, None)
|
||||
|
||||
def add_verified_tx(self, tx_hash: str, info: TxMinedInfo):
|
||||
# Remove from the unverified map and add to the verified map
|
||||
with self.lock:
|
||||
self.unverified_tx.pop(tx_hash, None)
|
||||
self.verified_tx[tx_hash] = info
|
||||
tx_mined_status = self.get_tx_height(tx_hash)
|
||||
self.network.trigger_callback('verified', self, tx_hash, tx_mined_status)
|
||||
|
||||
def get_unverified_txs(self):
|
||||
'''Returns a map from tx hash to transaction height'''
|
||||
with self.lock:
|
||||
return dict(self.unverified_tx) # copy
|
||||
|
||||
def undo_verifications(self, blockchain, height):
|
||||
'''Used by the verifier when a reorg has happened'''
|
||||
txs = set()
|
||||
with self.lock:
|
||||
for tx_hash, info in list(self.verified_tx.items()):
|
||||
tx_height = info.height
|
||||
if tx_height >= height:
|
||||
header = blockchain.read_header(tx_height)
|
||||
if not header or hash_header(header) != info.header_hash:
|
||||
self.verified_tx.pop(tx_hash, None)
|
||||
# NOTE: we should add these txns to self.unverified_tx,
|
||||
# but with what height?
|
||||
# If on the new fork after the reorg, the txn is at the
|
||||
# same height, we will not get a status update for the
|
||||
# address. If the txn is not mined or at a diff height,
|
||||
# we should get a status update. Unless we put tx into
|
||||
# unverified_tx, it will turn into local. So we put it
|
||||
# into unverified_tx with the old height, and if we get
|
||||
# a status update, that will overwrite it.
|
||||
self.unverified_tx[tx_hash] = tx_height
|
||||
txs.add(tx_hash)
|
||||
return txs
|
||||
|
||||
def get_local_height(self):
|
||||
""" return last known height if we are offline """
|
||||
cached_local_height = getattr(self.threadlocal_cache, 'local_height', None)
|
||||
if cached_local_height is not None:
|
||||
return cached_local_height
|
||||
return self.network.get_local_height() if self.network else self.storage.get('stored_height', 0)
|
||||
|
||||
def get_tx_height(self, tx_hash: str) -> TxMinedInfo:
|
||||
with self.lock:
|
||||
if tx_hash in self.verified_tx:
|
||||
info = self.verified_tx[tx_hash]
|
||||
conf = max(self.get_local_height() - info.height + 1, 0)
|
||||
return info._replace(conf=conf)
|
||||
elif tx_hash in self.unverified_tx:
|
||||
height = self.unverified_tx[tx_hash]
|
||||
return TxMinedInfo(height=height, conf=0)
|
||||
else:
|
||||
# local transaction
|
||||
return TxMinedInfo(height=TX_HEIGHT_LOCAL, conf=0)
|
||||
|
||||
def get_flodata(self, tx_hash: str):
|
||||
""" Given a transaction, returns flodata """
|
||||
with self.lock:
|
||||
if tx_hash in self.verified_tx:
|
||||
info = self.verified_tx[tx_hash]
|
||||
flodata = info[5]
|
||||
return flodata
|
||||
elif tx_hash in self.unverified_tx:
|
||||
tx = self.transactions.get(tx_hash)
|
||||
flodata = tx.flodata[5:]
|
||||
return flodata
|
||||
else:
|
||||
# local transaction
|
||||
tx = self.transactions.get(tx_hash)
|
||||
flodata = tx.flodata[5:]
|
||||
return flodata
|
||||
|
||||
def set_up_to_date(self, up_to_date):
|
||||
with self.lock:
|
||||
self.up_to_date = up_to_date
|
||||
if self.network:
|
||||
self.network.notify('status')
|
||||
if up_to_date:
|
||||
self.save_transactions(write=True)
|
||||
# if the verifier is also up to date, persist that too;
|
||||
# otherwise it will persist its results when it finishes
|
||||
if self.verifier and self.verifier.is_up_to_date():
|
||||
self.save_verified_tx(write=True)
|
||||
|
||||
def is_up_to_date(self):
|
||||
with self.lock: return self.up_to_date
|
||||
|
||||
@with_transaction_lock
|
||||
def get_tx_delta(self, tx_hash, address):
|
||||
"""effect of tx on address"""
|
||||
delta = 0
|
||||
# substract the value of coins sent from address
|
||||
d = self.txi.get(tx_hash, {}).get(address, [])
|
||||
for n, v in d:
|
||||
delta -= v
|
||||
# add the value of the coins received at address
|
||||
d = self.txo.get(tx_hash, {}).get(address, [])
|
||||
for n, v, cb in d:
|
||||
delta += v
|
||||
return delta
|
||||
|
||||
@with_transaction_lock
|
||||
def get_tx_value(self, txid):
|
||||
"""effect of tx on the entire domain"""
|
||||
delta = 0
|
||||
for addr, d in self.txi.get(txid, {}).items():
|
||||
for n, v in d:
|
||||
delta -= v
|
||||
for addr, d in self.txo.get(txid, {}).items():
|
||||
for n, v, cb in d:
|
||||
delta += v
|
||||
return delta
|
||||
|
||||
def get_wallet_delta(self, tx: Transaction):
|
||||
""" effect of tx on wallet """
|
||||
is_relevant = False # "related to wallet?"
|
||||
is_mine = False
|
||||
is_pruned = False
|
||||
is_partial = False
|
||||
v_in = v_out = v_out_mine = 0
|
||||
for txin in tx.inputs():
|
||||
addr = self.get_txin_address(txin)
|
||||
if self.is_mine(addr):
|
||||
is_mine = True
|
||||
is_relevant = True
|
||||
d = self.txo.get(txin['prevout_hash'], {}).get(addr, [])
|
||||
for n, v, cb in d:
|
||||
if n == txin['prevout_n']:
|
||||
value = v
|
||||
break
|
||||
else:
|
||||
value = None
|
||||
if value is None:
|
||||
is_pruned = True
|
||||
else:
|
||||
v_in += value
|
||||
else:
|
||||
is_partial = True
|
||||
if not is_mine:
|
||||
is_partial = False
|
||||
for o in tx.outputs():
|
||||
v_out += o.value
|
||||
if self.is_mine(o.address):
|
||||
v_out_mine += o.value
|
||||
is_relevant = True
|
||||
if is_pruned:
|
||||
# some inputs are mine:
|
||||
fee = None
|
||||
if is_mine:
|
||||
v = v_out_mine - v_out
|
||||
else:
|
||||
# no input is mine
|
||||
v = v_out_mine
|
||||
else:
|
||||
v = v_out_mine - v_in
|
||||
if is_partial:
|
||||
# some inputs are mine, but not all
|
||||
fee = None
|
||||
else:
|
||||
# all inputs are mine
|
||||
fee = v_in - v_out
|
||||
if not is_mine:
|
||||
fee = None
|
||||
return is_relevant, is_mine, v, fee
|
||||
|
||||
def get_tx_fee(self, tx: Transaction) -> Optional[int]:
|
||||
if not tx:
|
||||
return None
|
||||
if hasattr(tx, '_cached_fee'):
|
||||
return tx._cached_fee
|
||||
with self.lock, self.transaction_lock:
|
||||
is_relevant, is_mine, v, fee = self.get_wallet_delta(tx)
|
||||
if fee is None:
|
||||
txid = tx.txid()
|
||||
fee = self.tx_fees.get(txid)
|
||||
# only cache non-None, as None can still change while syncing
|
||||
if fee is not None:
|
||||
tx._cached_fee = fee
|
||||
return fee
|
||||
|
||||
def get_addr_io(self, address):
|
||||
with self.lock, self.transaction_lock:
|
||||
h = self.get_address_history(address)
|
||||
received = {}
|
||||
sent = {}
|
||||
for tx_hash, height in h:
|
||||
l = self.txo.get(tx_hash, {}).get(address, [])
|
||||
for n, v, is_cb in l:
|
||||
received[tx_hash + ':%d'%n] = (height, v, is_cb)
|
||||
for tx_hash, height in h:
|
||||
l = self.txi.get(tx_hash, {}).get(address, [])
|
||||
for txi, v in l:
|
||||
sent[txi] = height
|
||||
return received, sent
|
||||
|
||||
def get_addr_utxo(self, address):
|
||||
coins, spent = self.get_addr_io(address)
|
||||
for txi in spent:
|
||||
coins.pop(txi)
|
||||
out = {}
|
||||
for txo, v in coins.items():
|
||||
tx_height, value, is_cb = v
|
||||
prevout_hash, prevout_n = txo.split(':')
|
||||
x = {
|
||||
'address':address,
|
||||
'value':value,
|
||||
'prevout_n':int(prevout_n),
|
||||
'prevout_hash':prevout_hash,
|
||||
'height':tx_height,
|
||||
'coinbase':is_cb
|
||||
}
|
||||
out[txo] = x
|
||||
return out
|
||||
|
||||
# return the total amount ever received by an address
|
||||
def get_addr_received(self, address):
|
||||
received, sent = self.get_addr_io(address)
|
||||
return sum([v for height, v, is_cb in received.values()])
|
||||
|
||||
@with_local_height_cached
|
||||
def get_addr_balance(self, address):
|
||||
"""Return the balance of a FLO address:
|
||||
confirmed and matured, unconfirmed, unmatured
|
||||
"""
|
||||
received, sent = self.get_addr_io(address)
|
||||
c = u = x = 0
|
||||
local_height = self.get_local_height()
|
||||
for txo, (tx_height, v, is_cb) in received.items():
|
||||
if is_cb and tx_height + COINBASE_MATURITY > local_height:
|
||||
x += v
|
||||
elif tx_height > 0:
|
||||
c += v
|
||||
else:
|
||||
u += v
|
||||
if txo in sent:
|
||||
if sent[txo] > 0:
|
||||
c -= v
|
||||
else:
|
||||
u -= v
|
||||
return c, u, x
|
||||
|
||||
@with_local_height_cached
|
||||
def get_utxos(self, domain=None, excluded=None, mature=False, confirmed_only=False, nonlocal_only=False):
|
||||
coins = []
|
||||
if domain is None:
|
||||
domain = self.get_addresses()
|
||||
domain = set(domain)
|
||||
if excluded:
|
||||
domain = set(domain) - excluded
|
||||
for addr in domain:
|
||||
utxos = self.get_addr_utxo(addr)
|
||||
for x in utxos.values():
|
||||
if confirmed_only and x['height'] <= 0:
|
||||
continue
|
||||
if nonlocal_only and x['height'] == TX_HEIGHT_LOCAL:
|
||||
continue
|
||||
if mature and x['coinbase'] and x['height'] + COINBASE_MATURITY > self.get_local_height():
|
||||
continue
|
||||
coins.append(x)
|
||||
continue
|
||||
return coins
|
||||
|
||||
def get_balance(self, domain=None):
|
||||
if domain is None:
|
||||
domain = self.get_addresses()
|
||||
domain = set(domain)
|
||||
cc = uu = xx = 0
|
||||
for addr in domain:
|
||||
c, u, x = self.get_addr_balance(addr)
|
||||
cc += c
|
||||
uu += u
|
||||
xx += x
|
||||
return cc, uu, xx
|
||||
|
||||
def is_used(self, address):
|
||||
h = self.history.get(address,[])
|
||||
return len(h) != 0
|
||||
|
||||
def is_empty(self, address):
|
||||
c, u, x = self.get_addr_balance(address)
|
||||
return c+u+x == 0
|
||||
|
||||
def synchronize(self):
|
||||
pass
|
||||
@ -19,6 +19,7 @@
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import asyncio
|
||||
import json
|
||||
import locale
|
||||
import traceback
|
||||
@ -26,13 +27,13 @@ import subprocess
|
||||
import sys
|
||||
import os
|
||||
|
||||
import requests
|
||||
|
||||
from electrum import ELECTRUM_VERSION, constants
|
||||
from electrum.i18n import _
|
||||
from .version import ELECTRUM_VERSION
|
||||
from . import constants
|
||||
from .i18n import _
|
||||
from .util import make_aiohttp_session
|
||||
|
||||
|
||||
class BaseCrashReporter(object):
|
||||
class BaseCrashReporter:
|
||||
report_server = "https://crashhub.electrum.org"
|
||||
config_key = "show_crash_reporter"
|
||||
issue_template = """<h2>Traceback</h2>
|
||||
@ -59,16 +60,22 @@ class BaseCrashReporter(object):
|
||||
def __init__(self, exctype, value, tb):
|
||||
self.exc_args = (exctype, value, tb)
|
||||
|
||||
def send_report(self, endpoint="/crash"):
|
||||
def send_report(self, asyncio_loop, proxy, endpoint="/crash"):
|
||||
if constants.net.GENESIS[-4:] not in ["4943", "e26f"] and ".electrum.org" in BaseCrashReporter.report_server:
|
||||
# Gah! Some kind of altcoin wants to send us crash reports.
|
||||
raise Exception(_("Missing report URL."))
|
||||
report = self.get_traceback_info()
|
||||
report.update(self.get_additional_info())
|
||||
report = json.dumps(report)
|
||||
response = requests.post(BaseCrashReporter.report_server + endpoint, data=report)
|
||||
coro = self.do_post(proxy, BaseCrashReporter.report_server + endpoint, data=report)
|
||||
response = asyncio.run_coroutine_threadsafe(coro, asyncio_loop).result(5)
|
||||
return response
|
||||
|
||||
async def do_post(self, proxy, url, data):
|
||||
async with make_aiohttp_session(proxy) as session:
|
||||
async with session.post(url, data=data) as resp:
|
||||
return await resp.text()
|
||||
|
||||
def get_traceback_info(self):
|
||||
exc_string = str(self.exc_args[1])
|
||||
stack = traceback.extract_tb(self.exc_args[2])
|
||||
@ -124,4 +131,4 @@ class BaseCrashReporter(object):
|
||||
raise NotImplementedError
|
||||
|
||||
def get_os_version(self):
|
||||
raise NotImplementedError
|
||||
raise NotImplementedError
|
||||
@ -27,14 +27,24 @@ import os
|
||||
import sys
|
||||
import traceback
|
||||
from functools import partial
|
||||
from typing import List, TYPE_CHECKING, Tuple, NamedTuple, Any
|
||||
|
||||
from . import bitcoin
|
||||
from . import keystore
|
||||
from .bip32 import is_bip32_derivation, xpub_type
|
||||
from .keystore import bip44_derivation, purpose48_derivation
|
||||
from .wallet import Imported_Wallet, Standard_Wallet, Multisig_Wallet, wallet_types, Wallet
|
||||
from .storage import STO_EV_USER_PW, STO_EV_XPUB_PW, get_derivation_used_for_hw_device_encryption
|
||||
from .wallet import (Imported_Wallet, Standard_Wallet, Multisig_Wallet,
|
||||
wallet_types, Wallet, Abstract_Wallet)
|
||||
from .storage import (WalletStorage, STO_EV_USER_PW, STO_EV_XPUB_PW,
|
||||
get_derivation_used_for_hw_device_encryption)
|
||||
from .i18n import _
|
||||
from .util import UserCancelled, InvalidPassword
|
||||
from .util import UserCancelled, InvalidPassword, WalletFileException
|
||||
from .simple_config import SimpleConfig
|
||||
from .plugin import Plugins
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .plugin import DeviceInfo
|
||||
|
||||
|
||||
# hardware device setup purpose
|
||||
HWD_SETUP_NEW_WALLET, HWD_SETUP_DECRYPT_WALLET = range(0, 2)
|
||||
@ -46,15 +56,21 @@ class ScriptTypeNotSupported(Exception): pass
|
||||
class GoBack(Exception): pass
|
||||
|
||||
|
||||
class WizardStackItem(NamedTuple):
|
||||
action: Any
|
||||
args: Any
|
||||
storage_data: dict
|
||||
|
||||
|
||||
class BaseWizard(object):
|
||||
|
||||
def __init__(self, config, plugins, storage):
|
||||
def __init__(self, config: SimpleConfig, plugins: Plugins, storage: WalletStorage):
|
||||
super(BaseWizard, self).__init__()
|
||||
self.config = config
|
||||
self.plugins = plugins
|
||||
self.storage = storage
|
||||
self.wallet = None
|
||||
self.stack = []
|
||||
self.wallet = None # type: Abstract_Wallet
|
||||
self._stack = [] # type: List[WizardStackItem]
|
||||
self.plugin = None
|
||||
self.keystores = []
|
||||
self.is_kivy = config.get('gui') == 'kivy'
|
||||
@ -66,7 +82,8 @@ class BaseWizard(object):
|
||||
def run(self, *args):
|
||||
action = args[0]
|
||||
args = args[1:]
|
||||
self.stack.append((action, args))
|
||||
storage_data = self.storage.get_all_data()
|
||||
self._stack.append(WizardStackItem(action, args, storage_data))
|
||||
if not action:
|
||||
return
|
||||
if type(action) is tuple:
|
||||
@ -81,19 +98,25 @@ class BaseWizard(object):
|
||||
raise Exception("unknown action", action)
|
||||
|
||||
def can_go_back(self):
|
||||
return len(self.stack)>1
|
||||
return len(self._stack) > 1
|
||||
|
||||
def go_back(self):
|
||||
if not self.can_go_back():
|
||||
return
|
||||
self.stack.pop()
|
||||
action, args = self.stack.pop()
|
||||
self.run(action, *args)
|
||||
# pop 'current' frame
|
||||
self._stack.pop()
|
||||
# pop 'previous' frame
|
||||
stack_item = self._stack.pop()
|
||||
# try to undo side effects since we last entered 'previous' frame
|
||||
# FIXME only self.storage is properly restored
|
||||
self.storage.overwrite_all_data(stack_item.storage_data)
|
||||
# rerun 'previous' frame
|
||||
self.run(stack_item.action, *stack_item.args)
|
||||
|
||||
def reset_stack(self):
|
||||
self._stack = []
|
||||
|
||||
def new(self):
|
||||
if not self.storage.file_writable():
|
||||
self.show_error(_("Wallet file not writable"))
|
||||
return
|
||||
name = os.path.basename(self.storage.path)
|
||||
title = _("Create") + ' ' + name
|
||||
message = '\n'.join([
|
||||
@ -101,18 +124,27 @@ class BaseWizard(object):
|
||||
])
|
||||
wallet_kinds = [
|
||||
('standard', _("Standard wallet")),
|
||||
('2fa', _("Wallet with two-factor authentication")),
|
||||
('multisig', _("Multi-signature wallet")),
|
||||
('imported', _("Import Bitcoin addresses or private keys")),
|
||||
('imported', _("Import FLO addresses or private keys")),
|
||||
]
|
||||
choices = [pair for pair in wallet_kinds if pair[0] in wallet_types]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.on_wallet_type)
|
||||
|
||||
def upgrade_storage(self):
|
||||
exc = None
|
||||
def on_finished():
|
||||
self.wallet = Wallet(self.storage)
|
||||
self.terminate()
|
||||
self.waiting_dialog(partial(self.storage.upgrade), _('Upgrading wallet format...'), on_finished=on_finished)
|
||||
if exc is None:
|
||||
self.wallet = Wallet(self.storage)
|
||||
self.terminate()
|
||||
else:
|
||||
raise exc
|
||||
def do_upgrade():
|
||||
nonlocal exc
|
||||
try:
|
||||
self.storage.upgrade()
|
||||
except Exception as e:
|
||||
exc = e
|
||||
self.waiting_dialog(do_upgrade, _('Upgrading wallet format...'), on_finished=on_finished)
|
||||
|
||||
def load_2fa(self):
|
||||
self.storage.put('wallet_type', '2fa')
|
||||
@ -134,8 +166,8 @@ class BaseWizard(object):
|
||||
|
||||
def choose_multisig(self):
|
||||
def on_multisig(m, n):
|
||||
self.multisig_type = "%dof%d"%(m, n)
|
||||
self.storage.put('wallet_type', self.multisig_type)
|
||||
multisig_type = "%dof%d" % (m, n)
|
||||
self.storage.put('wallet_type', multisig_type)
|
||||
self.n = n
|
||||
self.run('choose_keystore')
|
||||
self.multisig_dialog(run_next=on_multisig)
|
||||
@ -166,8 +198,8 @@ class BaseWizard(object):
|
||||
|
||||
def import_addresses_or_keys(self):
|
||||
v = lambda x: keystore.is_address_list(x) or keystore.is_private_key_list(x)
|
||||
title = _("Import Bitcoin Addresses")
|
||||
message = _("Enter a list of Bitcoin addresses (this will create a watching-only wallet), or a list of private keys.")
|
||||
title = _("Import FLO Addresses")
|
||||
message = _("Enter a list of FLO addresses (this will create a watching-only wallet), or a list of private keys.")
|
||||
self.add_xpub_dialog(title=title, message=message, run_next=self.on_import,
|
||||
is_valid=v, allow_multi=True, show_wif_help=True)
|
||||
|
||||
@ -176,17 +208,23 @@ class BaseWizard(object):
|
||||
# will be reflected on self.storage
|
||||
if keystore.is_address_list(text):
|
||||
w = Imported_Wallet(self.storage)
|
||||
for x in text.split():
|
||||
w.import_address(x)
|
||||
addresses = text.split()
|
||||
good_inputs, bad_inputs = w.import_addresses(addresses, write_to_disk=False)
|
||||
elif keystore.is_private_key_list(text):
|
||||
k = keystore.Imported_KeyStore({})
|
||||
self.storage.put('keystore', k.dump())
|
||||
w = Imported_Wallet(self.storage)
|
||||
for x in keystore.get_private_keys(text):
|
||||
w.import_private_key(x, None)
|
||||
keys = keystore.get_private_keys(text)
|
||||
good_inputs, bad_inputs = w.import_private_keys(keys, None, write_to_disk=False)
|
||||
self.keystores.append(w.keystore)
|
||||
else:
|
||||
return self.terminate()
|
||||
if bad_inputs:
|
||||
msg = "\n".join(f"{key[:10]}... ({msg})" for key, msg in bad_inputs[:10])
|
||||
if len(bad_inputs) > 10: msg += '\n...'
|
||||
self.show_error(_("The following inputs could not be imported")
|
||||
+ f' ({len(bad_inputs)}):\n' + msg)
|
||||
# FIXME what if len(good_inputs) == 0 ?
|
||||
return self.run('create_wallet')
|
||||
|
||||
def restore_from_key(self):
|
||||
@ -209,33 +247,48 @@ class BaseWizard(object):
|
||||
def choose_hw_device(self, purpose=HWD_SETUP_NEW_WALLET):
|
||||
title = _('Hardware Keystore')
|
||||
# check available plugins
|
||||
support = self.plugins.get_hardware_support()
|
||||
if not support:
|
||||
msg = '\n'.join([
|
||||
_('No hardware wallet support found on your system.'),
|
||||
_('Please install the relevant libraries (eg python-trezor for Trezor).'),
|
||||
])
|
||||
self.confirm_dialog(title=title, message=msg, run_next= lambda x: self.choose_hw_device(purpose))
|
||||
return
|
||||
# scan devices
|
||||
devices = []
|
||||
supported_plugins = self.plugins.get_hardware_support()
|
||||
devices = [] # type: List[Tuple[str, DeviceInfo]]
|
||||
devmgr = self.plugins.device_manager
|
||||
debug_msg = ''
|
||||
|
||||
def failed_getting_device_infos(name, e):
|
||||
nonlocal debug_msg
|
||||
devmgr.print_error(f'error getting device infos for {name}: {e}')
|
||||
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
|
||||
debug_msg += f' {name}: (error getting device infos)\n{indented_error_msg}\n'
|
||||
|
||||
# scan devices
|
||||
try:
|
||||
scanned_devices = devmgr.scan_devices()
|
||||
except BaseException as e:
|
||||
devmgr.print_error('error scanning devices: {}'.format(e))
|
||||
devmgr.print_error('error scanning devices: {}'.format(repr(e)))
|
||||
debug_msg = ' {}:\n {}'.format(_('Error scanning devices'), e)
|
||||
else:
|
||||
debug_msg = ''
|
||||
for name, description, plugin in support:
|
||||
for splugin in supported_plugins:
|
||||
name, plugin = splugin.name, splugin.plugin
|
||||
# plugin init errored?
|
||||
if not plugin:
|
||||
e = splugin.exception
|
||||
indented_error_msg = ' '.join([''] + str(e).splitlines(keepends=True))
|
||||
debug_msg += f' {name}: (error during plugin init)\n'
|
||||
debug_msg += ' {}\n'.format(_('You might have an incompatible library.'))
|
||||
debug_msg += f'{indented_error_msg}\n'
|
||||
continue
|
||||
# see if plugin recognizes 'scanned_devices'
|
||||
try:
|
||||
# FIXME: side-effect: unpaired_device_info sets client.handler
|
||||
u = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices)
|
||||
device_infos = devmgr.unpaired_device_infos(None, plugin, devices=scanned_devices,
|
||||
include_failing_clients=True)
|
||||
except BaseException as e:
|
||||
devmgr.print_error('error getting device infos for {}: {}'.format(name, e))
|
||||
debug_msg += ' {}:\n {}\n'.format(plugin.name, e)
|
||||
traceback.print_exc()
|
||||
failed_getting_device_infos(name, e)
|
||||
continue
|
||||
devices += list(map(lambda x: (name, x), u))
|
||||
device_infos_failing = list(filter(lambda di: di.exception is not None, device_infos))
|
||||
for di in device_infos_failing:
|
||||
failed_getting_device_infos(name, di.exception)
|
||||
device_infos_working = list(filter(lambda di: di.exception is None, device_infos))
|
||||
devices += list(map(lambda x: (name, x), device_infos_working))
|
||||
if not debug_msg:
|
||||
debug_msg = ' {}'.format(_('No exceptions encountered.'))
|
||||
if not devices:
|
||||
@ -255,7 +308,9 @@ class BaseWizard(object):
|
||||
for name, info in devices:
|
||||
state = _("initialized") if info.initialized else _("wiped")
|
||||
label = info.label or _("An unnamed {}").format(name)
|
||||
descr = "%s [%s, %s]" % (label, name, state)
|
||||
try: transport_str = info.device.transport_ui_string[:20]
|
||||
except: transport_str = 'unknown transport'
|
||||
descr = f"{label} [{name}, {state}, {transport_str}]"
|
||||
choices.append(((name, info), descr))
|
||||
msg = _('Select a device') + ':'
|
||||
self.choice_dialog(title=title, message=msg, choices=choices, run_next= lambda *args: self.on_device(*args, purpose=purpose))
|
||||
@ -311,12 +366,14 @@ class BaseWizard(object):
|
||||
# There is no general standard for HD multisig.
|
||||
# For legacy, this is partially compatible with BIP45; assumes index=0
|
||||
# For segwit, a custom path is used, as there is no standard at all.
|
||||
default_choice_idx = 2
|
||||
choices = [
|
||||
('standard', 'legacy multisig (p2sh)', "m/45'/0"),
|
||||
('p2wsh-p2sh', 'p2sh-segwit multisig (p2wsh-p2sh)', purpose48_derivation(0, xtype='p2wsh-p2sh')),
|
||||
('p2wsh', 'native segwit multisig (p2wsh)', purpose48_derivation(0, xtype='p2wsh')),
|
||||
]
|
||||
else:
|
||||
default_choice_idx = 2
|
||||
choices = [
|
||||
('standard', 'legacy (p2pkh)', bip44_derivation(0, bip43_purpose=44)),
|
||||
('p2wpkh-p2sh', 'p2sh-segwit (p2wpkh-p2sh)', bip44_derivation(0, bip43_purpose=49)),
|
||||
@ -326,7 +383,8 @@ class BaseWizard(object):
|
||||
try:
|
||||
self.choice_and_line_dialog(
|
||||
run_next=f, title=_('Script type and Derivation path'), message1=message1,
|
||||
message2=message2, choices=choices, test_text=bitcoin.is_bip32_derivation)
|
||||
message2=message2, choices=choices, test_text=is_bip32_derivation,
|
||||
default_choice_idx=default_choice_idx)
|
||||
return
|
||||
except ScriptTypeNotSupported as e:
|
||||
self.show_error(e)
|
||||
@ -339,6 +397,7 @@ class BaseWizard(object):
|
||||
except ScriptTypeNotSupported:
|
||||
raise # this is handled in derivation_dialog
|
||||
except BaseException as e:
|
||||
traceback.print_exc(file=sys.stderr)
|
||||
self.show_error(e)
|
||||
return
|
||||
d = {
|
||||
@ -351,7 +410,7 @@ class BaseWizard(object):
|
||||
k = hardware_keystore(d)
|
||||
self.on_keystore(k)
|
||||
|
||||
def passphrase_dialog(self, run_next):
|
||||
def passphrase_dialog(self, run_next, is_restoring=False):
|
||||
title = _('Seed extension')
|
||||
message = '\n'.join([
|
||||
_('You may extend your seed with custom words.'),
|
||||
@ -361,7 +420,10 @@ class BaseWizard(object):
|
||||
_('Note that this is NOT your encryption password.'),
|
||||
_('If you do not know what this is, leave this field empty.'),
|
||||
])
|
||||
self.line_dialog(title=title, message=message, warning=warning, default='', test=lambda x:True, run_next=run_next)
|
||||
warn_issue4566 = is_restoring and self.seed_type == 'bip39'
|
||||
self.line_dialog(title=title, message=message, warning=warning,
|
||||
default='', test=lambda x:True, run_next=run_next,
|
||||
warn_issue4566=warn_issue4566)
|
||||
|
||||
def restore_from_seed(self):
|
||||
self.opt_bip39 = True
|
||||
@ -374,13 +436,13 @@ class BaseWizard(object):
|
||||
self.seed_type = 'bip39' if is_bip39 else bitcoin.seed_type(seed)
|
||||
if self.seed_type == 'bip39':
|
||||
f = lambda passphrase: self.on_restore_bip39(seed, passphrase)
|
||||
self.passphrase_dialog(run_next=f) if is_ext else f('')
|
||||
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
|
||||
elif self.seed_type in ['standard', 'segwit']:
|
||||
f = lambda passphrase: self.run('create_keystore', seed, passphrase)
|
||||
self.passphrase_dialog(run_next=f) if is_ext else f('')
|
||||
self.passphrase_dialog(run_next=f, is_restoring=True) if is_ext else f('')
|
||||
elif self.seed_type == 'old':
|
||||
self.run('create_keystore', seed, '')
|
||||
elif self.seed_type == '2fa':
|
||||
elif bitcoin.is_any_2fa_seed_type(self.seed_type):
|
||||
self.load_2fa()
|
||||
self.run('on_restore_seed', seed, is_ext)
|
||||
else:
|
||||
@ -402,7 +464,6 @@ class BaseWizard(object):
|
||||
def on_keystore(self, k):
|
||||
has_xpub = isinstance(k, keystore.Xpub)
|
||||
if has_xpub:
|
||||
from .bitcoin import xpub_type
|
||||
t1 = xpub_type(k.xpub)
|
||||
if self.wallet_type == 'standard':
|
||||
if has_xpub and t1 not in ['standard', 'p2wpkh', 'p2wpkh-p2sh']:
|
||||
@ -430,7 +491,7 @@ class BaseWizard(object):
|
||||
self.keystores.append(k)
|
||||
if len(self.keystores) == 1:
|
||||
xpub = k.get_master_public_key()
|
||||
self.stack = []
|
||||
self.reset_stack()
|
||||
self.run('show_xpub_and_add_cosigners', xpub)
|
||||
elif len(self.keystores) < self.n:
|
||||
self.run('choose_keystore')
|
||||
@ -474,6 +535,7 @@ class BaseWizard(object):
|
||||
|
||||
def on_password(self, password, *, encrypt_storage,
|
||||
storage_enc_version=STO_EV_USER_PW, encrypt_keystore):
|
||||
assert not self.storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
|
||||
self.storage.set_keystore_encryption(bool(password) and encrypt_keystore)
|
||||
if encrypt_storage:
|
||||
self.storage.set_password(password, enc_version=storage_enc_version)
|
||||
@ -503,18 +565,16 @@ class BaseWizard(object):
|
||||
def show_xpub_and_add_cosigners(self, xpub):
|
||||
self.show_xpub_dialog(xpub=xpub, run_next=lambda x: self.run('choose_keystore'))
|
||||
|
||||
def choose_seed_type(self):
|
||||
def choose_seed_type(self, message=None, choices=None):
|
||||
title = _('Choose Seed type')
|
||||
message = ' '.join([
|
||||
_("The type of addresses used by your wallet will depend on your seed."),
|
||||
_("Segwit wallets use bech32 addresses, defined in BIP173."),
|
||||
_("Please note that websites and other wallets may not support these addresses yet."),
|
||||
_("Thus, you might want to keep using a non-segwit wallet in order to be able to receive bitcoins during the transition period.")
|
||||
])
|
||||
choices = [
|
||||
('create_standard_seed', _('Standard')),
|
||||
('create_segwit_seed', _('Segwit')),
|
||||
]
|
||||
if message is None:
|
||||
message = ' '.join([
|
||||
_("The type of addresses used by your wallet will depend on your seed.")
|
||||
])
|
||||
if choices is None:
|
||||
choices = [
|
||||
('create_standard_seed', _('Legacy')),
|
||||
]
|
||||
self.choice_dialog(title=title, message=message, choices=choices, run_next=self.run)
|
||||
|
||||
def create_segwit_seed(self): self.create_seed('segwit')
|
||||
269
electrum/bip32.py
Normal file
@ -0,0 +1,269 @@
|
||||
# Copyright (C) 2018 The Electrum developers
|
||||
# Distributed under the MIT software license, see the accompanying
|
||||
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
|
||||
|
||||
import hashlib
|
||||
from typing import List
|
||||
|
||||
from .util import bfh, bh2u, BitcoinException, print_error
|
||||
from . import constants
|
||||
from . import ecc
|
||||
from .crypto import hash_160, hmac_oneshot
|
||||
from .bitcoin import rev_hex, int_to_hex, EncodeBase58Check, DecodeBase58Check
|
||||
|
||||
|
||||
BIP32_PRIME = 0x80000000
|
||||
|
||||
|
||||
def protect_against_invalid_ecpoint(func):
|
||||
def func_wrapper(*args):
|
||||
n = args[-1]
|
||||
while True:
|
||||
is_prime = n & BIP32_PRIME
|
||||
try:
|
||||
return func(*args[:-1], n=n)
|
||||
except ecc.InvalidECPointException:
|
||||
print_error('bip32 protect_against_invalid_ecpoint: skipping index')
|
||||
n += 1
|
||||
is_prime2 = n & BIP32_PRIME
|
||||
if is_prime != is_prime2: raise OverflowError()
|
||||
return func_wrapper
|
||||
|
||||
|
||||
# Child private key derivation function (from master private key)
|
||||
# k = master private key (32 bytes)
|
||||
# c = master chain code (extra entropy for key derivation) (32 bytes)
|
||||
# n = the index of the key we want to derive. (only 32 bits will be used)
|
||||
# If n is hardened (i.e. the 32nd bit is set), the resulting private key's
|
||||
# corresponding public key can NOT be determined without the master private key.
|
||||
# However, if n is not hardened, the resulting private key's corresponding
|
||||
# public key can be determined without the master private key.
|
||||
@protect_against_invalid_ecpoint
|
||||
def CKD_priv(k, c, n):
|
||||
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
|
||||
is_prime = n & BIP32_PRIME
|
||||
return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime)
|
||||
|
||||
|
||||
def _CKD_priv(k, c, s, is_prime):
|
||||
try:
|
||||
keypair = ecc.ECPrivkey(k)
|
||||
except ecc.InvalidECPointException as e:
|
||||
raise BitcoinException('Impossible xprv (not within curve order)') from e
|
||||
cK = keypair.get_public_key_bytes(compressed=True)
|
||||
data = bytes([0]) + k + s if is_prime else cK + s
|
||||
I = hmac_oneshot(c, data, hashlib.sha512)
|
||||
I_left = ecc.string_to_number(I[0:32])
|
||||
k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER
|
||||
if I_left >= ecc.CURVE_ORDER or k_n == 0:
|
||||
raise ecc.InvalidECPointException()
|
||||
k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER)
|
||||
c_n = I[32:]
|
||||
return k_n, c_n
|
||||
|
||||
# Child public key derivation function (from public key only)
|
||||
# K = master public key
|
||||
# c = master chain code
|
||||
# n = index of key we want to derive
|
||||
# This function allows us to find the nth public key, as long as n is
|
||||
# not hardened. If n is hardened, we need the master private key to find it.
|
||||
@protect_against_invalid_ecpoint
|
||||
def CKD_pub(cK, c, n):
|
||||
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
|
||||
if n & BIP32_PRIME: raise Exception()
|
||||
return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4))))
|
||||
|
||||
# helper function, callable with arbitrary string.
|
||||
# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing)
|
||||
def _CKD_pub(cK, c, s):
|
||||
I = hmac_oneshot(c, cK + s, hashlib.sha512)
|
||||
pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK)
|
||||
if pubkey.is_at_infinity():
|
||||
raise ecc.InvalidECPointException()
|
||||
cK_n = pubkey.get_public_key_bytes(compressed=True)
|
||||
c_n = I[32:]
|
||||
return cK_n, c_n
|
||||
|
||||
|
||||
def xprv_header(xtype, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return bfh("%08x" % net.XPRV_HEADERS[xtype])
|
||||
|
||||
|
||||
def xpub_header(xtype, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return bfh("%08x" % net.XPUB_HEADERS[xtype])
|
||||
|
||||
|
||||
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
|
||||
child_number=b'\x00'*4, *, net=None):
|
||||
if not ecc.is_secret_within_curve_range(k):
|
||||
raise BitcoinException('Impossible xprv (not within curve order)')
|
||||
xprv = xprv_header(xtype, net=net) \
|
||||
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
|
||||
return EncodeBase58Check(xprv)
|
||||
|
||||
|
||||
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
|
||||
child_number=b'\x00'*4, *, net=None):
|
||||
xpub = xpub_header(xtype, net=net) \
|
||||
+ bytes([depth]) + fingerprint + child_number + c + cK
|
||||
return EncodeBase58Check(xpub)
|
||||
|
||||
|
||||
class InvalidMasterKeyVersionBytes(BitcoinException): pass
|
||||
|
||||
|
||||
def deserialize_xkey(xkey, prv, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
xkey = DecodeBase58Check(xkey)
|
||||
if len(xkey) != 78:
|
||||
raise BitcoinException('Invalid length for extended key: {}'
|
||||
.format(len(xkey)))
|
||||
depth = xkey[4]
|
||||
fingerprint = xkey[5:9]
|
||||
child_number = xkey[9:13]
|
||||
c = xkey[13:13+32]
|
||||
header = int.from_bytes(xkey[0:4], byteorder='big')
|
||||
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
|
||||
if header not in headers.values():
|
||||
raise InvalidMasterKeyVersionBytes('Invalid extended key format: {}'
|
||||
.format(hex(header)))
|
||||
xtype = list(headers.keys())[list(headers.values()).index(header)]
|
||||
n = 33 if prv else 32
|
||||
K_or_k = xkey[13+n:]
|
||||
if prv and not ecc.is_secret_within_curve_range(K_or_k):
|
||||
raise BitcoinException('Impossible xprv (not within curve order)')
|
||||
return xtype, depth, fingerprint, child_number, c, K_or_k
|
||||
|
||||
|
||||
def deserialize_xpub(xkey, *, net=None):
|
||||
return deserialize_xkey(xkey, False, net=net)
|
||||
|
||||
def deserialize_xprv(xkey, *, net=None):
|
||||
return deserialize_xkey(xkey, True, net=net)
|
||||
|
||||
def xpub_type(x):
|
||||
return deserialize_xpub(x)[0]
|
||||
|
||||
|
||||
def is_xpub(text):
|
||||
try:
|
||||
deserialize_xpub(text)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def is_xprv(text):
|
||||
try:
|
||||
deserialize_xprv(text)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def xpub_from_xprv(xprv):
|
||||
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
|
||||
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
|
||||
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
|
||||
|
||||
|
||||
def bip32_root(seed, xtype):
|
||||
I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512)
|
||||
master_k = I[0:32]
|
||||
master_c = I[32:]
|
||||
# create xprv first, as that will check if master_k is within curve order
|
||||
xprv = serialize_xprv(xtype, master_c, master_k)
|
||||
cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True)
|
||||
xpub = serialize_xpub(xtype, master_c, cK)
|
||||
return xprv, xpub
|
||||
|
||||
|
||||
def xpub_from_pubkey(xtype, cK):
|
||||
if cK[0] not in (0x02, 0x03):
|
||||
raise ValueError('Unexpected first byte: {}'.format(cK[0]))
|
||||
return serialize_xpub(xtype, b'\x00'*32, cK)
|
||||
|
||||
|
||||
def bip32_derivation(s: str) -> int:
|
||||
if not s.startswith('m/'):
|
||||
raise ValueError('invalid bip32 derivation path: {}'.format(s))
|
||||
s = s[2:]
|
||||
for n in s.split('/'):
|
||||
if n == '': continue
|
||||
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
|
||||
yield i
|
||||
|
||||
def convert_bip32_path_to_list_of_uint32(n: str) -> List[int]:
|
||||
"""Convert bip32 path to list of uint32 integers with prime flags
|
||||
m/0/-1/1' -> [0, 0x80000001, 0x80000001]
|
||||
|
||||
based on code in trezorlib
|
||||
"""
|
||||
path = []
|
||||
for x in n.split('/')[1:]:
|
||||
if x == '': continue
|
||||
prime = 0
|
||||
if x.endswith("'"):
|
||||
x = x.replace('\'', '')
|
||||
prime = BIP32_PRIME
|
||||
if x.startswith('-'):
|
||||
prime = BIP32_PRIME
|
||||
path.append(abs(int(x)) | prime)
|
||||
return path
|
||||
|
||||
def is_bip32_derivation(x: str) -> bool:
|
||||
try:
|
||||
[ i for i in bip32_derivation(x)]
|
||||
return True
|
||||
except :
|
||||
return False
|
||||
|
||||
def bip32_private_derivation(xprv, branch, sequence):
|
||||
if not sequence.startswith(branch):
|
||||
raise ValueError('incompatible branch ({}) and sequence ({})'
|
||||
.format(branch, sequence))
|
||||
if branch == sequence:
|
||||
return xprv, xpub_from_xprv(xprv)
|
||||
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
|
||||
sequence = sequence[len(branch):]
|
||||
for n in sequence.split('/'):
|
||||
if n == '': continue
|
||||
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
|
||||
parent_k = k
|
||||
k, c = CKD_priv(k, c, i)
|
||||
depth += 1
|
||||
parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True)
|
||||
fingerprint = hash_160(parent_cK)[0:4]
|
||||
child_number = bfh("%08X"%i)
|
||||
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
|
||||
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
|
||||
xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number)
|
||||
return xprv, xpub
|
||||
|
||||
|
||||
def bip32_public_derivation(xpub, branch, sequence):
|
||||
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
|
||||
if not sequence.startswith(branch):
|
||||
raise ValueError('incompatible branch ({}) and sequence ({})'
|
||||
.format(branch, sequence))
|
||||
sequence = sequence[len(branch):]
|
||||
for n in sequence.split('/'):
|
||||
if n == '': continue
|
||||
i = int(n)
|
||||
parent_cK = cK
|
||||
cK, c = CKD_pub(cK, c, i)
|
||||
depth += 1
|
||||
fingerprint = hash_160(parent_cK)[0:4]
|
||||
child_number = bfh("%08X"%i)
|
||||
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
|
||||
|
||||
|
||||
def bip32_private_key(sequence, k, chain):
|
||||
for i in sequence:
|
||||
k, chain = CKD_priv(k, chain, i)
|
||||
return k
|
||||
@ -24,14 +24,17 @@
|
||||
# SOFTWARE.
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import List, Tuple, TYPE_CHECKING, Optional, Union
|
||||
|
||||
from .util import bfh, bh2u, BitcoinException, print_error, assert_bytes, to_bytes, inv_dict
|
||||
from .util import bfh, bh2u, BitcoinException, assert_bytes, to_bytes, inv_dict
|
||||
from . import version
|
||||
from . import segwit_addr
|
||||
from . import constants
|
||||
from . import ecc
|
||||
from .crypto import Hash, sha256, hash_160, hmac_oneshot
|
||||
from .crypto import sha256d, sha256, hash_160, hmac_oneshot
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .network import Network
|
||||
|
||||
|
||||
################################## transactions
|
||||
@ -46,7 +49,7 @@ TYPE_PUBKEY = 1
|
||||
TYPE_SCRIPT = 2
|
||||
|
||||
|
||||
def rev_hex(s):
|
||||
def rev_hex(s: str) -> str:
|
||||
return bh2u(bfh(s)[::-1])
|
||||
|
||||
|
||||
@ -57,7 +60,7 @@ def int_to_hex(i: int, length: int=1) -> str:
|
||||
if not isinstance(i, int):
|
||||
raise TypeError('{} instead of int'.format(i))
|
||||
range_size = pow(256, length)
|
||||
if i < -range_size/2 or i >= range_size:
|
||||
if i < -(range_size//2) or i >= range_size:
|
||||
raise OverflowError('cannot convert int {} to hex ({} bytes)'.format(i, length))
|
||||
if i < 0:
|
||||
# two's complement
|
||||
@ -147,19 +150,37 @@ def add_number_to_script(i: int) -> bytes:
|
||||
return bfh(push_script(script_num_to_hex(i)))
|
||||
|
||||
|
||||
hash_encode = lambda x: bh2u(x[::-1])
|
||||
hash_decode = lambda x: bfh(x)[::-1]
|
||||
hmac_sha_512 = lambda x, y: hmac_oneshot(x, y, hashlib.sha512)
|
||||
def relayfee(network: 'Network'=None) -> int:
|
||||
from .simple_config import FEERATE_DEFAULT_RELAY
|
||||
MAX_RELAY_FEE = 50000
|
||||
f = network.relay_fee if network and network.relay_fee else FEERATE_DEFAULT_RELAY
|
||||
return min(f, MAX_RELAY_FEE)
|
||||
|
||||
|
||||
def is_new_seed(x, prefix=version.SEED_PREFIX):
|
||||
def dust_threshold(network: 'Network'=None) -> int:
|
||||
# Change <= dust threshold is added to the tx fee
|
||||
return 182 * 3 * relayfee(network) // 1000
|
||||
|
||||
|
||||
def hash_encode(x: bytes) -> str:
|
||||
return bh2u(x[::-1])
|
||||
|
||||
|
||||
def hash_decode(x: str) -> bytes:
|
||||
return bfh(x)[::-1]
|
||||
|
||||
|
||||
################################## electrum seeds
|
||||
|
||||
|
||||
def is_new_seed(x: str, prefix=version.SEED_PREFIX) -> bool:
|
||||
from . import mnemonic
|
||||
x = mnemonic.normalize_text(x)
|
||||
s = bh2u(hmac_sha_512(b"Seed version", x.encode('utf8')))
|
||||
s = bh2u(hmac_oneshot(b"Seed version", x.encode('utf8'), hashlib.sha512))
|
||||
return s.startswith(prefix)
|
||||
|
||||
|
||||
def is_old_seed(seed):
|
||||
def is_old_seed(seed: str) -> bool:
|
||||
from . import old_mnemonic, mnemonic
|
||||
seed = mnemonic.normalize_text(seed)
|
||||
words = seed.split()
|
||||
@ -177,7 +198,7 @@ def is_old_seed(seed):
|
||||
return is_hex or (uses_electrum_words and (len(words) == 12 or len(words) == 24))
|
||||
|
||||
|
||||
def seed_type(x):
|
||||
def seed_type(x: str) -> str:
|
||||
if is_old_seed(x):
|
||||
return 'old'
|
||||
elif is_new_seed(x):
|
||||
@ -186,119 +207,130 @@ def seed_type(x):
|
||||
return 'segwit'
|
||||
elif is_new_seed(x, version.SEED_PREFIX_2FA):
|
||||
return '2fa'
|
||||
elif is_new_seed(x, version.SEED_PREFIX_2FA_SW):
|
||||
return '2fa_segwit'
|
||||
return ''
|
||||
|
||||
is_seed = lambda x: bool(seed_type(x))
|
||||
|
||||
def is_seed(x: str) -> bool:
|
||||
return bool(seed_type(x))
|
||||
|
||||
|
||||
def is_any_2fa_seed_type(seed_type):
|
||||
return seed_type in ['2fa', '2fa_segwit']
|
||||
|
||||
|
||||
############ functions from pywallet #####################
|
||||
|
||||
def hash160_to_b58_address(h160: bytes, addrtype):
|
||||
s = bytes([addrtype])
|
||||
s += h160
|
||||
return base_encode(s+Hash(s)[0:4], base=58)
|
||||
def hash160_to_b58_address(h160: bytes, addrtype: int) -> str:
|
||||
s = bytes([addrtype]) + h160
|
||||
s = s + sha256d(s)[0:4]
|
||||
return base_encode(s, base=58)
|
||||
|
||||
|
||||
def b58_address_to_hash160(addr):
|
||||
def b58_address_to_hash160(addr: str) -> Tuple[int, bytes]:
|
||||
addr = to_bytes(addr, 'ascii')
|
||||
_bytes = base_decode(addr, 25, base=58)
|
||||
return _bytes[0], _bytes[1:21]
|
||||
|
||||
|
||||
def hash160_to_p2pkh(h160, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
def hash160_to_p2pkh(h160: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash160_to_b58_address(h160, net.ADDRTYPE_P2PKH)
|
||||
|
||||
def hash160_to_p2sh(h160, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
def hash160_to_p2sh(h160: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash160_to_b58_address(h160, net.ADDRTYPE_P2SH)
|
||||
|
||||
def public_key_to_p2pkh(public_key: bytes) -> str:
|
||||
return hash160_to_p2pkh(hash_160(public_key))
|
||||
def public_key_to_p2pkh(public_key: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash160_to_p2pkh(hash_160(public_key), net=net)
|
||||
|
||||
def hash_to_segwit_addr(h, witver, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
def hash_to_segwit_addr(h: bytes, witver: int, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return segwit_addr.encode(net.SEGWIT_HRP, witver, h)
|
||||
|
||||
def public_key_to_p2wpkh(public_key):
|
||||
return hash_to_segwit_addr(hash_160(public_key), witver=0)
|
||||
def public_key_to_p2wpkh(public_key: bytes, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash_to_segwit_addr(hash_160(public_key), witver=0, net=net)
|
||||
|
||||
def script_to_p2wsh(script):
|
||||
return hash_to_segwit_addr(sha256(bfh(script)), witver=0)
|
||||
def script_to_p2wsh(script: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
return hash_to_segwit_addr(sha256(bfh(script)), witver=0, net=net)
|
||||
|
||||
def p2wpkh_nested_script(pubkey):
|
||||
def p2wpkh_nested_script(pubkey: str) -> str:
|
||||
pkh = bh2u(hash_160(bfh(pubkey)))
|
||||
return '00' + push_script(pkh)
|
||||
|
||||
def p2wsh_nested_script(witness_script):
|
||||
def p2wsh_nested_script(witness_script: str) -> str:
|
||||
wsh = bh2u(sha256(bfh(witness_script)))
|
||||
return '00' + push_script(wsh)
|
||||
|
||||
def pubkey_to_address(txin_type, pubkey):
|
||||
def pubkey_to_address(txin_type: str, pubkey: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
if txin_type == 'p2pkh':
|
||||
return public_key_to_p2pkh(bfh(pubkey))
|
||||
return public_key_to_p2pkh(bfh(pubkey), net=net)
|
||||
elif txin_type == 'p2wpkh':
|
||||
return public_key_to_p2wpkh(bfh(pubkey))
|
||||
return public_key_to_p2wpkh(bfh(pubkey), net=net)
|
||||
elif txin_type == 'p2wpkh-p2sh':
|
||||
scriptSig = p2wpkh_nested_script(pubkey)
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)))
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
|
||||
else:
|
||||
raise NotImplementedError(txin_type)
|
||||
|
||||
def redeem_script_to_address(txin_type, redeem_script):
|
||||
def redeem_script_to_address(txin_type: str, redeem_script: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
if txin_type == 'p2sh':
|
||||
return hash160_to_p2sh(hash_160(bfh(redeem_script)))
|
||||
return hash160_to_p2sh(hash_160(bfh(redeem_script)), net=net)
|
||||
elif txin_type == 'p2wsh':
|
||||
return script_to_p2wsh(redeem_script)
|
||||
return script_to_p2wsh(redeem_script, net=net)
|
||||
elif txin_type == 'p2wsh-p2sh':
|
||||
scriptSig = p2wsh_nested_script(redeem_script)
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)))
|
||||
return hash160_to_p2sh(hash_160(bfh(scriptSig)), net=net)
|
||||
else:
|
||||
raise NotImplementedError(txin_type)
|
||||
|
||||
|
||||
def script_to_address(script, *, net=None):
|
||||
def script_to_address(script: str, *, net=None) -> str:
|
||||
from .transaction import get_address_from_output_script
|
||||
t, addr = get_address_from_output_script(bfh(script), net=net)
|
||||
assert t == TYPE_ADDRESS
|
||||
return addr
|
||||
|
||||
def address_to_script(addr, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
def address_to_script(addr: str, *, net=None) -> str:
|
||||
if net is None: net = constants.net
|
||||
if not is_address(addr, net=net):
|
||||
raise BitcoinException(f"invalid bitcoin address: {addr}")
|
||||
witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr)
|
||||
if witprog is not None:
|
||||
if not (0 <= witver <= 16):
|
||||
raise BitcoinException('impossible witness version: {}'.format(witver))
|
||||
raise BitcoinException(f'impossible witness version: {witver}')
|
||||
OP_n = witver + 0x50 if witver > 0 else 0
|
||||
script = bh2u(bytes([OP_n]))
|
||||
script += push_script(bh2u(bytes(witprog)))
|
||||
return script
|
||||
addrtype, hash_160 = b58_address_to_hash160(addr)
|
||||
addrtype, hash_160_ = b58_address_to_hash160(addr)
|
||||
if addrtype == net.ADDRTYPE_P2PKH:
|
||||
script = '76a9' # op_dup, op_hash_160
|
||||
script += push_script(bh2u(hash_160))
|
||||
script += push_script(bh2u(hash_160_))
|
||||
script += '88ac' # op_equalverify, op_checksig
|
||||
elif addrtype == net.ADDRTYPE_P2SH:
|
||||
script = 'a9' # op_hash_160
|
||||
script += push_script(bh2u(hash_160))
|
||||
script += push_script(bh2u(hash_160_))
|
||||
script += '87' # op_equal
|
||||
else:
|
||||
raise BitcoinException('unknown address type: {}'.format(addrtype))
|
||||
raise BitcoinException(f'unknown address type: {addrtype}')
|
||||
return script
|
||||
|
||||
def address_to_scripthash(addr):
|
||||
def address_to_scripthash(addr: str) -> str:
|
||||
script = address_to_script(addr)
|
||||
return script_to_scripthash(script)
|
||||
|
||||
def script_to_scripthash(script):
|
||||
h = sha256(bytes.fromhex(script))[0:32]
|
||||
def script_to_scripthash(script: str) -> str:
|
||||
h = sha256(bfh(script))[0:32]
|
||||
return bh2u(bytes(reversed(h)))
|
||||
|
||||
def public_key_to_p2pk_script(pubkey):
|
||||
def public_key_to_p2pk_script(pubkey: str) -> str:
|
||||
script = push_script(pubkey)
|
||||
script += 'ac' # op_checksig
|
||||
return script
|
||||
@ -340,7 +372,7 @@ def base_encode(v: bytes, base: int) -> str:
|
||||
return result.decode('ascii')
|
||||
|
||||
|
||||
def base_decode(v, length, base):
|
||||
def base_decode(v: Union[bytes, str], length: Optional[int], base: int) -> Optional[bytes]:
|
||||
""" decode v into a string of len bytes."""
|
||||
# assert_bytes(v)
|
||||
v = to_bytes(v, 'ascii')
|
||||
@ -378,21 +410,20 @@ class InvalidChecksum(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def EncodeBase58Check(vchIn):
|
||||
hash = Hash(vchIn)
|
||||
def EncodeBase58Check(vchIn: bytes) -> str:
|
||||
hash = sha256d(vchIn)
|
||||
return base_encode(vchIn + hash[0:4], base=58)
|
||||
|
||||
|
||||
def DecodeBase58Check(psz):
|
||||
def DecodeBase58Check(psz: Union[bytes, str]) -> bytes:
|
||||
vchRet = base_decode(psz, None, base=58)
|
||||
key = vchRet[0:-4]
|
||||
csum = vchRet[-4:]
|
||||
hash = Hash(key)
|
||||
cs32 = hash[0:4]
|
||||
if cs32 != csum:
|
||||
raise InvalidChecksum('expected {}, actual {}'.format(bh2u(cs32), bh2u(csum)))
|
||||
payload = vchRet[0:-4]
|
||||
csum_found = vchRet[-4:]
|
||||
csum_calculated = sha256d(payload)[0:4]
|
||||
if csum_calculated != csum_found:
|
||||
raise InvalidChecksum(f'calculated {bh2u(csum_calculated)}, found {bh2u(csum_found)}')
|
||||
else:
|
||||
return key
|
||||
return payload
|
||||
|
||||
|
||||
# backwards compat
|
||||
@ -409,12 +440,6 @@ WIF_SCRIPT_TYPES = {
|
||||
WIF_SCRIPT_TYPES_INV = inv_dict(WIF_SCRIPT_TYPES)
|
||||
|
||||
|
||||
PURPOSE48_SCRIPT_TYPES = {
|
||||
'p2wsh-p2sh': 1, # specifically multisig
|
||||
'p2wsh': 2, # specifically multisig
|
||||
}
|
||||
PURPOSE48_SCRIPT_TYPES_INV = inv_dict(PURPOSE48_SCRIPT_TYPES)
|
||||
|
||||
|
||||
def serialize_privkey(secret: bytes, compressed: bool, txin_type: str,
|
||||
internal_use: bool=False) -> str:
|
||||
@ -433,7 +458,7 @@ def serialize_privkey(secret: bytes, compressed: bool, txin_type: str,
|
||||
return '{}:{}'.format(txin_type, base58_wif)
|
||||
|
||||
|
||||
def deserialize_privkey(key: str) -> (str, bytes, bool):
|
||||
def deserialize_privkey(key: str) -> Tuple[str, bytes, bool]:
|
||||
if is_minikey(key):
|
||||
return 'p2pkh', minikey_to_private_key(key), False
|
||||
|
||||
@ -470,36 +495,40 @@ def deserialize_privkey(key: str) -> (str, bytes, bool):
|
||||
return txin_type, secret_bytes, compressed
|
||||
|
||||
|
||||
def is_compressed(sec):
|
||||
def is_compressed_privkey(sec: str) -> bool:
|
||||
return deserialize_privkey(sec)[2]
|
||||
|
||||
|
||||
def address_from_private_key(sec):
|
||||
def address_from_private_key(sec: str) -> str:
|
||||
txin_type, privkey, compressed = deserialize_privkey(sec)
|
||||
public_key = ecc.ECPrivkey(privkey).get_public_key_hex(compressed=compressed)
|
||||
return pubkey_to_address(txin_type, public_key)
|
||||
|
||||
def is_segwit_address(addr):
|
||||
def is_segwit_address(addr: str, *, net=None) -> bool:
|
||||
if net is None: net = constants.net
|
||||
try:
|
||||
witver, witprog = segwit_addr.decode(constants.net.SEGWIT_HRP, addr)
|
||||
witver, witprog = segwit_addr.decode(net.SEGWIT_HRP, addr)
|
||||
except Exception as e:
|
||||
return False
|
||||
return witprog is not None
|
||||
|
||||
def is_b58_address(addr):
|
||||
def is_b58_address(addr: str, *, net=None) -> bool:
|
||||
if net is None: net = constants.net
|
||||
try:
|
||||
addrtype, h = b58_address_to_hash160(addr)
|
||||
except Exception as e:
|
||||
return False
|
||||
if addrtype not in [constants.net.ADDRTYPE_P2PKH, constants.net.ADDRTYPE_P2SH]:
|
||||
if addrtype not in [net.ADDRTYPE_P2PKH, net.ADDRTYPE_P2SH]:
|
||||
return False
|
||||
return addr == hash160_to_b58_address(h, addrtype)
|
||||
|
||||
def is_address(addr):
|
||||
return is_segwit_address(addr) or is_b58_address(addr)
|
||||
def is_address(addr: str, *, net=None) -> bool:
|
||||
if net is None: net = constants.net
|
||||
return is_segwit_address(addr, net=net) \
|
||||
or is_b58_address(addr, net=net)
|
||||
|
||||
|
||||
def is_private_key(key):
|
||||
def is_private_key(key: str) -> bool:
|
||||
try:
|
||||
k = deserialize_privkey(key)
|
||||
return k is not False
|
||||
@ -509,7 +538,7 @@ def is_private_key(key):
|
||||
|
||||
########### end pywallet functions #######################
|
||||
|
||||
def is_minikey(text):
|
||||
def is_minikey(text: str) -> bool:
|
||||
# Minikeys are typically 22 or 30 characters, but this routine
|
||||
# permits any length of 20 or more provided the minikey is valid.
|
||||
# A valid minikey must begin with an 'S', be in base58, and when
|
||||
@ -519,243 +548,5 @@ def is_minikey(text):
|
||||
and all(ord(c) in __b58chars for c in text)
|
||||
and sha256(text + '?')[0] == 0x00)
|
||||
|
||||
def minikey_to_private_key(text):
|
||||
def minikey_to_private_key(text: str) -> bytes:
|
||||
return sha256(text)
|
||||
|
||||
|
||||
###################################### BIP32 ##############################
|
||||
|
||||
BIP32_PRIME = 0x80000000
|
||||
|
||||
|
||||
def protect_against_invalid_ecpoint(func):
|
||||
def func_wrapper(*args):
|
||||
n = args[-1]
|
||||
while True:
|
||||
is_prime = n & BIP32_PRIME
|
||||
try:
|
||||
return func(*args[:-1], n=n)
|
||||
except ecc.InvalidECPointException:
|
||||
print_error('bip32 protect_against_invalid_ecpoint: skipping index')
|
||||
n += 1
|
||||
is_prime2 = n & BIP32_PRIME
|
||||
if is_prime != is_prime2: raise OverflowError()
|
||||
return func_wrapper
|
||||
|
||||
|
||||
# Child private key derivation function (from master private key)
|
||||
# k = master private key (32 bytes)
|
||||
# c = master chain code (extra entropy for key derivation) (32 bytes)
|
||||
# n = the index of the key we want to derive. (only 32 bits will be used)
|
||||
# If n is hardened (i.e. the 32nd bit is set), the resulting private key's
|
||||
# corresponding public key can NOT be determined without the master private key.
|
||||
# However, if n is not hardened, the resulting private key's corresponding
|
||||
# public key can be determined without the master private key.
|
||||
@protect_against_invalid_ecpoint
|
||||
def CKD_priv(k, c, n):
|
||||
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
|
||||
is_prime = n & BIP32_PRIME
|
||||
return _CKD_priv(k, c, bfh(rev_hex(int_to_hex(n,4))), is_prime)
|
||||
|
||||
|
||||
def _CKD_priv(k, c, s, is_prime):
|
||||
try:
|
||||
keypair = ecc.ECPrivkey(k)
|
||||
except ecc.InvalidECPointException as e:
|
||||
raise BitcoinException('Impossible xprv (not within curve order)') from e
|
||||
cK = keypair.get_public_key_bytes(compressed=True)
|
||||
data = bytes([0]) + k + s if is_prime else cK + s
|
||||
I = hmac_oneshot(c, data, hashlib.sha512)
|
||||
I_left = ecc.string_to_number(I[0:32])
|
||||
k_n = (I_left + ecc.string_to_number(k)) % ecc.CURVE_ORDER
|
||||
if I_left >= ecc.CURVE_ORDER or k_n == 0:
|
||||
raise ecc.InvalidECPointException()
|
||||
k_n = ecc.number_to_string(k_n, ecc.CURVE_ORDER)
|
||||
c_n = I[32:]
|
||||
return k_n, c_n
|
||||
|
||||
# Child public key derivation function (from public key only)
|
||||
# K = master public key
|
||||
# c = master chain code
|
||||
# n = index of key we want to derive
|
||||
# This function allows us to find the nth public key, as long as n is
|
||||
# not hardened. If n is hardened, we need the master private key to find it.
|
||||
@protect_against_invalid_ecpoint
|
||||
def CKD_pub(cK, c, n):
|
||||
if n < 0: raise ValueError('the bip32 index needs to be non-negative')
|
||||
if n & BIP32_PRIME: raise Exception()
|
||||
return _CKD_pub(cK, c, bfh(rev_hex(int_to_hex(n,4))))
|
||||
|
||||
# helper function, callable with arbitrary string.
|
||||
# note: 's' does not need to fit into 32 bits here! (c.f. trustedcoin billing)
|
||||
def _CKD_pub(cK, c, s):
|
||||
I = hmac_oneshot(c, cK + s, hashlib.sha512)
|
||||
pubkey = ecc.ECPrivkey(I[0:32]) + ecc.ECPubkey(cK)
|
||||
if pubkey.is_at_infinity():
|
||||
raise ecc.InvalidECPointException()
|
||||
cK_n = pubkey.get_public_key_bytes(compressed=True)
|
||||
c_n = I[32:]
|
||||
return cK_n, c_n
|
||||
|
||||
|
||||
def xprv_header(xtype, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return bfh("%08x" % net.XPRV_HEADERS[xtype])
|
||||
|
||||
|
||||
def xpub_header(xtype, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
return bfh("%08x" % net.XPUB_HEADERS[xtype])
|
||||
|
||||
|
||||
def serialize_xprv(xtype, c, k, depth=0, fingerprint=b'\x00'*4,
|
||||
child_number=b'\x00'*4, *, net=None):
|
||||
if not ecc.is_secret_within_curve_range(k):
|
||||
raise BitcoinException('Impossible xprv (not within curve order)')
|
||||
xprv = xprv_header(xtype, net=net) \
|
||||
+ bytes([depth]) + fingerprint + child_number + c + bytes([0]) + k
|
||||
return EncodeBase58Check(xprv)
|
||||
|
||||
|
||||
def serialize_xpub(xtype, c, cK, depth=0, fingerprint=b'\x00'*4,
|
||||
child_number=b'\x00'*4, *, net=None):
|
||||
xpub = xpub_header(xtype, net=net) \
|
||||
+ bytes([depth]) + fingerprint + child_number + c + cK
|
||||
return EncodeBase58Check(xpub)
|
||||
|
||||
|
||||
def deserialize_xkey(xkey, prv, *, net=None):
|
||||
if net is None:
|
||||
net = constants.net
|
||||
xkey = DecodeBase58Check(xkey)
|
||||
if len(xkey) != 78:
|
||||
raise BitcoinException('Invalid length for extended key: {}'
|
||||
.format(len(xkey)))
|
||||
depth = xkey[4]
|
||||
fingerprint = xkey[5:9]
|
||||
child_number = xkey[9:13]
|
||||
c = xkey[13:13+32]
|
||||
header = int('0x' + bh2u(xkey[0:4]), 16)
|
||||
headers = net.XPRV_HEADERS if prv else net.XPUB_HEADERS
|
||||
if header not in headers.values():
|
||||
raise BitcoinException('Invalid extended key format: {}'
|
||||
.format(hex(header)))
|
||||
xtype = list(headers.keys())[list(headers.values()).index(header)]
|
||||
n = 33 if prv else 32
|
||||
K_or_k = xkey[13+n:]
|
||||
if prv and not ecc.is_secret_within_curve_range(K_or_k):
|
||||
raise BitcoinException('Impossible xprv (not within curve order)')
|
||||
return xtype, depth, fingerprint, child_number, c, K_or_k
|
||||
|
||||
|
||||
def deserialize_xpub(xkey, *, net=None):
|
||||
return deserialize_xkey(xkey, False, net=net)
|
||||
|
||||
def deserialize_xprv(xkey, *, net=None):
|
||||
return deserialize_xkey(xkey, True, net=net)
|
||||
|
||||
def xpub_type(x):
|
||||
return deserialize_xpub(x)[0]
|
||||
|
||||
|
||||
def is_xpub(text):
|
||||
try:
|
||||
deserialize_xpub(text)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def is_xprv(text):
|
||||
try:
|
||||
deserialize_xprv(text)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def xpub_from_xprv(xprv):
|
||||
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
|
||||
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
|
||||
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
|
||||
|
||||
|
||||
def bip32_root(seed, xtype):
|
||||
I = hmac_oneshot(b"Bitcoin seed", seed, hashlib.sha512)
|
||||
master_k = I[0:32]
|
||||
master_c = I[32:]
|
||||
# create xprv first, as that will check if master_k is within curve order
|
||||
xprv = serialize_xprv(xtype, master_c, master_k)
|
||||
cK = ecc.ECPrivkey(master_k).get_public_key_bytes(compressed=True)
|
||||
xpub = serialize_xpub(xtype, master_c, cK)
|
||||
return xprv, xpub
|
||||
|
||||
|
||||
def xpub_from_pubkey(xtype, cK):
|
||||
if cK[0] not in (0x02, 0x03):
|
||||
raise ValueError('Unexpected first byte: {}'.format(cK[0]))
|
||||
return serialize_xpub(xtype, b'\x00'*32, cK)
|
||||
|
||||
|
||||
def bip32_derivation(s):
|
||||
if not s.startswith('m/'):
|
||||
raise ValueError('invalid bip32 derivation path: {}'.format(s))
|
||||
s = s[2:]
|
||||
for n in s.split('/'):
|
||||
if n == '': continue
|
||||
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
|
||||
yield i
|
||||
|
||||
def is_bip32_derivation(x):
|
||||
try:
|
||||
[ i for i in bip32_derivation(x)]
|
||||
return True
|
||||
except :
|
||||
return False
|
||||
|
||||
def bip32_private_derivation(xprv, branch, sequence):
|
||||
if not sequence.startswith(branch):
|
||||
raise ValueError('incompatible branch ({}) and sequence ({})'
|
||||
.format(branch, sequence))
|
||||
if branch == sequence:
|
||||
return xprv, xpub_from_xprv(xprv)
|
||||
xtype, depth, fingerprint, child_number, c, k = deserialize_xprv(xprv)
|
||||
sequence = sequence[len(branch):]
|
||||
for n in sequence.split('/'):
|
||||
if n == '': continue
|
||||
i = int(n[:-1]) + BIP32_PRIME if n[-1] == "'" else int(n)
|
||||
parent_k = k
|
||||
k, c = CKD_priv(k, c, i)
|
||||
depth += 1
|
||||
parent_cK = ecc.ECPrivkey(parent_k).get_public_key_bytes(compressed=True)
|
||||
fingerprint = hash_160(parent_cK)[0:4]
|
||||
child_number = bfh("%08X"%i)
|
||||
cK = ecc.ECPrivkey(k).get_public_key_bytes(compressed=True)
|
||||
xpub = serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
|
||||
xprv = serialize_xprv(xtype, c, k, depth, fingerprint, child_number)
|
||||
return xprv, xpub
|
||||
|
||||
|
||||
def bip32_public_derivation(xpub, branch, sequence):
|
||||
xtype, depth, fingerprint, child_number, c, cK = deserialize_xpub(xpub)
|
||||
if not sequence.startswith(branch):
|
||||
raise ValueError('incompatible branch ({}) and sequence ({})'
|
||||
.format(branch, sequence))
|
||||
sequence = sequence[len(branch):]
|
||||
for n in sequence.split('/'):
|
||||
if n == '': continue
|
||||
i = int(n)
|
||||
parent_cK = cK
|
||||
cK, c = CKD_pub(cK, c, i)
|
||||
depth += 1
|
||||
fingerprint = hash_160(parent_cK)[0:4]
|
||||
child_number = bfh("%08X"%i)
|
||||
return serialize_xpub(xtype, c, cK, depth, fingerprint, child_number)
|
||||
|
||||
|
||||
def bip32_private_key(sequence, k, chain):
|
||||
for i in sequence:
|
||||
k, chain = CKD_priv(k, chain, i)
|
||||
return k
|
||||
732
electrum/blockchain.py
Normal file
@ -0,0 +1,732 @@
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2012 thomasv@ecdsa.org
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import os
|
||||
import threading
|
||||
from typing import Optional, Dict
|
||||
|
||||
from . import util
|
||||
from .bitcoin import hash_encode, int_to_hex, rev_hex
|
||||
from .crypto import sha256d
|
||||
from . import constants
|
||||
from .util import bfh, bh2u
|
||||
from .simple_config import SimpleConfig
|
||||
|
||||
try:
|
||||
import pylibscrypt
|
||||
getPoWHash = lambda x: pylibscrypt.scrypt(password=x, salt=x, N=1024, r=1, p=1, olen=32)
|
||||
except ImportError:
|
||||
util.print_msg("Warning: package pylibscrypt not available")
|
||||
|
||||
|
||||
HEADER_SIZE = 80 # bytes
|
||||
MAX_TARGET = 0x00000fffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
|
||||
|
||||
|
||||
class MissingHeader(Exception):
|
||||
pass
|
||||
|
||||
class InvalidHeader(Exception):
|
||||
pass
|
||||
|
||||
def serialize_header(header_dict: dict) -> str:
|
||||
s = int_to_hex(header_dict['version'], 4) \
|
||||
+ rev_hex(header_dict['prev_block_hash']) \
|
||||
+ rev_hex(header_dict['merkle_root']) \
|
||||
+ int_to_hex(int(header_dict['timestamp']), 4) \
|
||||
+ int_to_hex(int(header_dict['bits']), 4) \
|
||||
+ int_to_hex(int(header_dict['nonce']), 4)
|
||||
return s
|
||||
|
||||
def deserialize_header(s: bytes, height: int) -> dict:
|
||||
if not s:
|
||||
raise InvalidHeader('Invalid header: {}'.format(s))
|
||||
if len(s) != HEADER_SIZE:
|
||||
raise InvalidHeader('Invalid header length: {}'.format(len(s)))
|
||||
hex_to_int = lambda s: int.from_bytes(s, byteorder='little')
|
||||
h = {}
|
||||
h['version'] = hex_to_int(s[0:4])
|
||||
h['prev_block_hash'] = hash_encode(s[4:36])
|
||||
h['merkle_root'] = hash_encode(s[36:68])
|
||||
h['timestamp'] = hex_to_int(s[68:72])
|
||||
h['bits'] = hex_to_int(s[72:76])
|
||||
h['nonce'] = hex_to_int(s[76:80])
|
||||
h['block_height'] = height
|
||||
return h
|
||||
|
||||
def hash_header(header: dict) -> str:
|
||||
if header is None:
|
||||
return '0' * 64
|
||||
if header.get('prev_block_hash') is None:
|
||||
header['prev_block_hash'] = '00'*32
|
||||
return hash_raw_header(serialize_header(header))
|
||||
|
||||
|
||||
def hash_raw_header(header: str) -> str:
|
||||
return hash_encode(sha256d(bfh(header)))
|
||||
|
||||
|
||||
# key: blockhash hex at forkpoint
|
||||
# the chain at some key is the best chain that includes the given hash
|
||||
blockchains = {} # type: Dict[str, Blockchain]
|
||||
blockchains_lock = threading.RLock()
|
||||
|
||||
|
||||
def read_blockchains(config: 'SimpleConfig'):
|
||||
best_chain = Blockchain(config=config,
|
||||
forkpoint=0,
|
||||
parent=None,
|
||||
forkpoint_hash=constants.net.GENESIS,
|
||||
prev_hash=None)
|
||||
blockchains[constants.net.GENESIS] = best_chain
|
||||
# consistency checks
|
||||
if best_chain.height() > constants.net.max_checkpoint():
|
||||
header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1)
|
||||
if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False):
|
||||
util.print_error("[blockchain] deleting best chain. cannot connect header after last cp to last cp.")
|
||||
os.unlink(best_chain.path())
|
||||
best_chain.update_size()
|
||||
# forks
|
||||
fdir = os.path.join(util.get_headers_dir(config), 'forks')
|
||||
util.make_dir(fdir)
|
||||
# files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}
|
||||
l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir))
|
||||
l = sorted(l, key=lambda x: int(x.split('_')[1])) # sort by forkpoint
|
||||
|
||||
def delete_chain(filename, reason):
|
||||
util.print_error(f"[blockchain] deleting chain {filename}: {reason}")
|
||||
os.unlink(os.path.join(fdir, filename))
|
||||
|
||||
def instantiate_chain(filename):
|
||||
__, forkpoint, prev_hash, first_hash = filename.split('_')
|
||||
forkpoint = int(forkpoint)
|
||||
prev_hash = (64-len(prev_hash)) * "0" + prev_hash # left-pad with zeroes
|
||||
first_hash = (64-len(first_hash)) * "0" + first_hash
|
||||
# forks below the max checkpoint are not allowed
|
||||
if forkpoint <= constants.net.max_checkpoint():
|
||||
delete_chain(filename, "deleting fork below max checkpoint")
|
||||
return
|
||||
# find parent (sorting by forkpoint guarantees it's already instantiated)
|
||||
for parent in blockchains.values():
|
||||
if parent.check_hash(forkpoint - 1, prev_hash):
|
||||
break
|
||||
else:
|
||||
delete_chain(filename, "cannot find parent for chain")
|
||||
return
|
||||
b = Blockchain(config=config,
|
||||
forkpoint=forkpoint,
|
||||
parent=parent,
|
||||
forkpoint_hash=first_hash,
|
||||
prev_hash=prev_hash)
|
||||
# consistency checks
|
||||
h = b.read_header(b.forkpoint)
|
||||
if first_hash != hash_header(h):
|
||||
delete_chain(filename, "incorrect first hash for chain")
|
||||
return
|
||||
if not b.parent.can_connect(h, check_height=False):
|
||||
delete_chain(filename, "cannot connect chain to parent")
|
||||
return
|
||||
chain_id = b.get_id()
|
||||
assert first_hash == chain_id, (first_hash, chain_id)
|
||||
blockchains[chain_id] = b
|
||||
|
||||
for filename in l:
|
||||
instantiate_chain(filename)
|
||||
|
||||
|
||||
def pow_hash_header(header):
|
||||
return hash_encode(getPoWHash(bfh(serialize_header(header))))
|
||||
|
||||
|
||||
def get_best_chain() -> 'Blockchain':
|
||||
return blockchains[constants.net.GENESIS]
|
||||
|
||||
# block hash -> chain work; up to and including that block
|
||||
_CHAINWORK_CACHE = {
|
||||
"0000000000000000000000000000000000000000000000000000000000000000": 0, # virtual block at height -1
|
||||
} # type: Dict[str, int]
|
||||
|
||||
|
||||
class Blockchain(util.PrintError):
|
||||
"""
|
||||
Manages blockchain headers and their verification
|
||||
"""
|
||||
|
||||
def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'],
|
||||
forkpoint_hash: str, prev_hash: Optional[str]):
|
||||
assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash
|
||||
assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash
|
||||
# assert (parent is None) == (forkpoint == 0)
|
||||
if 0 < forkpoint <= constants.net.max_checkpoint():
|
||||
raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}")
|
||||
self.config = config
|
||||
self.forkpoint = forkpoint # height of first header
|
||||
self.parent = parent
|
||||
self._forkpoint_hash = forkpoint_hash # blockhash at forkpoint. "first hash"
|
||||
self._prev_hash = prev_hash # blockhash immediately before forkpoint
|
||||
self.lock = threading.RLock()
|
||||
self.update_size()
|
||||
|
||||
def with_lock(func):
|
||||
def func_wrapper(self, *args, **kwargs):
|
||||
with self.lock:
|
||||
return func(self, *args, **kwargs)
|
||||
return func_wrapper
|
||||
|
||||
@property
|
||||
def checkpoints(self):
|
||||
return constants.net.CHECKPOINTS
|
||||
|
||||
def get_max_child(self) -> Optional[int]:
|
||||
with blockchains_lock: chains = list(blockchains.values())
|
||||
children = list(filter(lambda y: y.parent==self, chains))
|
||||
return max([x.forkpoint for x in children]) if children else None
|
||||
|
||||
def get_max_forkpoint(self) -> int:
|
||||
"""Returns the max height where there is a fork
|
||||
related to this chain.
|
||||
"""
|
||||
mc = self.get_max_child()
|
||||
return mc if mc is not None else self.forkpoint
|
||||
|
||||
@with_lock
|
||||
def get_branch_size(self) -> int:
|
||||
return self.height() - self.get_max_forkpoint() + 1
|
||||
|
||||
def get_name(self) -> str:
|
||||
return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10]
|
||||
|
||||
def check_header(self, header: dict) -> bool:
|
||||
header_hash = hash_header(header)
|
||||
height = header.get('block_height')
|
||||
return self.check_hash(height, header_hash)
|
||||
|
||||
def check_hash(self, height: int, header_hash: str) -> bool:
|
||||
"""Returns whether the hash of the block at given height
|
||||
is the given hash.
|
||||
"""
|
||||
assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash # hex
|
||||
try:
|
||||
return header_hash == self.get_hash(height)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def fork(parent, header: dict) -> 'Blockchain':
|
||||
if not parent.can_connect(header, check_height=False):
|
||||
raise Exception("forking header does not connect to parent chain")
|
||||
forkpoint = header.get('block_height')
|
||||
self = Blockchain(config=parent.config,
|
||||
forkpoint=forkpoint,
|
||||
parent=parent,
|
||||
forkpoint_hash=hash_header(header),
|
||||
prev_hash=parent.get_hash(forkpoint-1))
|
||||
open(self.path(), 'w+').close()
|
||||
self.save_header(header)
|
||||
# put into global dict. note that in some cases
|
||||
# save_header might have already put it there but that's OK
|
||||
chain_id = self.get_id()
|
||||
with blockchains_lock:
|
||||
blockchains[chain_id] = self
|
||||
return self
|
||||
|
||||
@with_lock
|
||||
def height(self) -> int:
|
||||
return self.forkpoint + self.size() - 1
|
||||
|
||||
@with_lock
|
||||
def size(self) -> int:
|
||||
return self._size
|
||||
|
||||
@with_lock
|
||||
def update_size(self) -> None:
|
||||
p = self.path()
|
||||
self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0
|
||||
|
||||
#def pow_hash_header(header):
|
||||
# return hash_encode(getPoWHash(bfh(serialize_header(header))))
|
||||
|
||||
@classmethod
|
||||
def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None:
|
||||
_hash = hash_header(header)
|
||||
_powhash = pow_hash_header(header)
|
||||
if expected_header_hash and expected_header_hash != _hash:
|
||||
raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash))
|
||||
if prev_hash != header.get('prev_block_hash'):
|
||||
raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
|
||||
if constants.net.TESTNET:
|
||||
return
|
||||
bits = cls.target_to_bits(target)
|
||||
bits = target
|
||||
if bits != header.get('bits'):
|
||||
raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits')))
|
||||
block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big')
|
||||
target_val = cls.bits_to_target(bits)
|
||||
if int('0x' + _powhash, 16) > target_val:
|
||||
raise Exception("insufficient proof of work: %s vs target %s" % (int('0x' + _hash, 16), target_val))
|
||||
|
||||
def verify_chunk(self, index: int, data: bytes) -> None:
|
||||
num = len(data) // HEADER_SIZE
|
||||
current_header = index * 2016
|
||||
print('chunk ' + str(index))
|
||||
prev_hash = self.get_hash(current_header - 1)
|
||||
headerLast = None
|
||||
headerFirst = None
|
||||
capture = None
|
||||
lst = []
|
||||
for i in range(num):
|
||||
averaging_interval = self.AveragingInterval(current_header)
|
||||
difficulty_interval = self.DifficultyAdjustmentInterval(current_header)
|
||||
if current_header < 426000:
|
||||
target = self.get_target(current_header - 1, headerLast, headerFirst)
|
||||
try:
|
||||
expected_header_hash = self.get_hash(current_header)
|
||||
except MissingHeader:
|
||||
expected_header_hash = None
|
||||
raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE]
|
||||
header = deserialize_header(raw_header, current_header)
|
||||
self.verify_header(header, prev_hash, target, expected_header_hash)
|
||||
prev_hash = hash_header(header)
|
||||
headerLast = header
|
||||
if current_header == 0:
|
||||
headerFirst = header
|
||||
elif (current_header + averaging_interval + 1) % difficulty_interval == 0:
|
||||
capture = header
|
||||
if current_header != 0 and current_header % difficulty_interval == 0:
|
||||
headerFirst = capture
|
||||
if current_header >= 425993:
|
||||
lst.append(headerLast)
|
||||
current_header = current_header + 1
|
||||
else:
|
||||
if len(lst)>6:
|
||||
headerFirst = lst[0]
|
||||
target = self.get_target(current_header - 1, headerLast, headerFirst)
|
||||
try:
|
||||
expected_header_hash = self.get_hash(current_header)
|
||||
except MissingHeader:
|
||||
expected_header_hash = None
|
||||
raw_header = data[i * HEADER_SIZE: (i + 1) * HEADER_SIZE]
|
||||
header = deserialize_header(raw_header, current_header)
|
||||
self.verify_header(header, prev_hash, target, expected_header_hash)
|
||||
prev_hash = hash_header(header)
|
||||
headerLast = header
|
||||
lst.append(header)
|
||||
if len(lst)>7:
|
||||
lst.pop(0)
|
||||
current_header = current_header + 1
|
||||
|
||||
@with_lock
|
||||
def path(self):
|
||||
d = util.get_headers_dir(self.config)
|
||||
if self.parent is None:
|
||||
filename = 'blockchain_headers'
|
||||
else:
|
||||
assert self.forkpoint > 0, self.forkpoint
|
||||
prev_hash = self._prev_hash.lstrip('0')
|
||||
first_hash = self._forkpoint_hash.lstrip('0')
|
||||
basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}'
|
||||
filename = os.path.join('forks', basename)
|
||||
return os.path.join(d, filename)
|
||||
|
||||
@with_lock
|
||||
def save_chunk(self, index: int, chunk: bytes):
|
||||
assert index >= 0, index
|
||||
chunk_within_checkpoint_region = index < len(self.checkpoints)
|
||||
# chunks in checkpoint region are the responsibility of the 'main chain'
|
||||
if chunk_within_checkpoint_region and self.parent is not None:
|
||||
main_chain = get_best_chain()
|
||||
main_chain.save_chunk(index, chunk)
|
||||
return
|
||||
|
||||
delta_height = (index * 2016 - self.forkpoint)
|
||||
delta_bytes = delta_height * HEADER_SIZE
|
||||
# if this chunk contains our forkpoint, only save the part after forkpoint
|
||||
# (the part before is the responsibility of the parent)
|
||||
if delta_bytes < 0:
|
||||
chunk = chunk[-delta_bytes:]
|
||||
delta_bytes = 0
|
||||
truncate = not chunk_within_checkpoint_region
|
||||
self.write(chunk, delta_bytes, truncate)
|
||||
self.swap_with_parent()
|
||||
|
||||
def swap_with_parent(self) -> None:
|
||||
parent_lock = self.parent.lock if self.parent is not None else threading.Lock()
|
||||
with parent_lock, self.lock, blockchains_lock: # this order should not deadlock
|
||||
# do the swap; possibly multiple ones
|
||||
cnt = 0
|
||||
while self._swap_with_parent():
|
||||
cnt += 1
|
||||
if cnt > len(blockchains): # make sure we are making progress
|
||||
raise Exception(f'swapping fork with parent too many times: {cnt}')
|
||||
|
||||
def _swap_with_parent(self) -> bool:
|
||||
"""Check if this chain became stronger than its parent, and swap
|
||||
the underlying files if so. The Blockchain instances will keep
|
||||
'containing' the same headers, but their ids change and so
|
||||
they will be stored in different files."""
|
||||
if self.parent is None:
|
||||
return False
|
||||
if self.parent.get_chainwork() >= self.get_chainwork():
|
||||
return False
|
||||
self.print_error("swap", self.forkpoint, self.parent.forkpoint)
|
||||
parent_branch_size = self.parent.height() - self.forkpoint + 1
|
||||
forkpoint = self.forkpoint # type: Optional[int]
|
||||
parent = self.parent # type: Optional[Blockchain]
|
||||
child_old_id = self.get_id()
|
||||
parent_old_id = parent.get_id()
|
||||
# swap files
|
||||
# child takes parent's name
|
||||
# parent's new name will be something new (not child's old name)
|
||||
self.assert_headers_file_available(self.path())
|
||||
child_old_name = self.path()
|
||||
with open(self.path(), 'rb') as f:
|
||||
my_data = f.read()
|
||||
self.assert_headers_file_available(parent.path())
|
||||
with open(parent.path(), 'rb') as f:
|
||||
f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE)
|
||||
parent_data = f.read(parent_branch_size*HEADER_SIZE)
|
||||
self.write(parent_data, 0)
|
||||
parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE)
|
||||
# swap parameters
|
||||
self.parent, parent.parent = parent.parent, self # type: Optional[Blockchain], Optional[Blockchain]
|
||||
self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint
|
||||
self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE]))
|
||||
self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash
|
||||
# parent's new name
|
||||
os.replace(child_old_name, parent.path())
|
||||
self.update_size()
|
||||
parent.update_size()
|
||||
# update pointers
|
||||
blockchains.pop(child_old_id, None)
|
||||
blockchains.pop(parent_old_id, None)
|
||||
blockchains[self.get_id()] = self
|
||||
blockchains[parent.get_id()] = parent
|
||||
return True
|
||||
|
||||
def get_id(self) -> str:
|
||||
return self._forkpoint_hash
|
||||
|
||||
def assert_headers_file_available(self, path):
|
||||
if os.path.exists(path):
|
||||
return
|
||||
elif not os.path.exists(util.get_headers_dir(self.config)):
|
||||
raise FileNotFoundError('Electrum headers_dir does not exist. Was it deleted while running?')
|
||||
else:
|
||||
raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))
|
||||
|
||||
@with_lock
|
||||
def write(self, data: bytes, offset: int, truncate: bool=True) -> None:
|
||||
filename = self.path()
|
||||
self.assert_headers_file_available(filename)
|
||||
with open(filename, 'rb+') as f:
|
||||
if truncate and offset != self._size * HEADER_SIZE:
|
||||
f.seek(offset)
|
||||
f.truncate()
|
||||
f.seek(offset)
|
||||
f.write(data)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
self.update_size()
|
||||
|
||||
@with_lock
|
||||
def save_header(self, header: dict) -> None:
|
||||
delta = header.get('block_height') - self.forkpoint
|
||||
data = bfh(serialize_header(header))
|
||||
# headers are only _appended_ to the end:
|
||||
assert delta == self.size(), (delta, self.size())
|
||||
assert len(data) == HEADER_SIZE
|
||||
self.write(data, delta*HEADER_SIZE)
|
||||
self.swap_with_parent()
|
||||
|
||||
@with_lock
|
||||
def read_header(self, height: int) -> Optional[dict]:
|
||||
if height < 0:
|
||||
return
|
||||
if height < self.forkpoint:
|
||||
return self.parent.read_header(height)
|
||||
if height > self.height():
|
||||
return
|
||||
delta = height - self.forkpoint
|
||||
name = self.path()
|
||||
self.assert_headers_file_available(name)
|
||||
with open(name, 'rb') as f:
|
||||
f.seek(delta * HEADER_SIZE)
|
||||
h = f.read(HEADER_SIZE)
|
||||
if len(h) < HEADER_SIZE:
|
||||
raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))
|
||||
if h == bytes([0])*HEADER_SIZE:
|
||||
return None
|
||||
return deserialize_header(h, height)
|
||||
|
||||
def header_at_tip(self) -> Optional[dict]:
|
||||
"""Return latest header."""
|
||||
height = self.height()
|
||||
return self.read_header(height)
|
||||
|
||||
def get_hash(self, height: int) -> str:
|
||||
def is_height_checkpoint():
|
||||
within_cp_range = height <= constants.net.max_checkpoint()
|
||||
at_chunk_boundary = (height+1) % 2016 == 0
|
||||
return within_cp_range and at_chunk_boundary
|
||||
|
||||
if height == -1:
|
||||
return '0000000000000000000000000000000000000000000000000000000000000000'
|
||||
elif height == 0:
|
||||
return constants.net.GENESIS
|
||||
elif is_height_checkpoint():
|
||||
index = height // 2016
|
||||
h, t = self.checkpoints[index]
|
||||
return h
|
||||
else:
|
||||
header = self.read_header(height)
|
||||
if header is None:
|
||||
raise MissingHeader(height)
|
||||
return hash_header(header)
|
||||
|
||||
def get_target(self, index: int, headerLast: dict=None, headerFirst: dict=None) -> int:
|
||||
# compute target from chunk x, used in chunk x+1
|
||||
if constants.net.TESTNET:
|
||||
return 0
|
||||
# The range is first 90 blocks because FLO's block time was 90 blocks when it started
|
||||
if -1 <= index <= 88:
|
||||
return 0x1e0ffff0
|
||||
if index < len(self.checkpoints):
|
||||
h, t = self.checkpoints[index]
|
||||
return t
|
||||
# new target
|
||||
if headerLast is None:
|
||||
headerLast = self.read_header(index)
|
||||
height = headerLast["block_height"]
|
||||
# check if the height passes is in range for retargeting
|
||||
if (height + 1) % self.DifficultyAdjustmentInterval(height + 1) != 0:
|
||||
return int(headerLast["bits"])
|
||||
if headerFirst is None:
|
||||
averagingInterval = self.AveragingInterval(height + 1)
|
||||
blockstogoback = averagingInterval - 1
|
||||
# print("Blocks to go back = " + str(blockstogoback))
|
||||
if (height + 1) != averagingInterval:
|
||||
blockstogoback = averagingInterval
|
||||
firstHeight = height - blockstogoback
|
||||
headerFirst = self.read_header(int(firstHeight))
|
||||
|
||||
firstBlockTime = headerFirst["timestamp"]
|
||||
nMinActualTimespan = int(self.MinActualTimespan(int(headerLast["block_height"]) + 1))
|
||||
|
||||
nMaxActualTimespan = int(self.MaxActualTimespan(int(headerLast["block_height"]) + 1))
|
||||
# Limit adjustment step
|
||||
nActualTimespan = headerLast["timestamp"] - firstBlockTime
|
||||
if nActualTimespan < nMinActualTimespan:
|
||||
nActualTimespan = nMinActualTimespan
|
||||
if nActualTimespan > nMaxActualTimespan:
|
||||
nActualTimespan = nMaxActualTimespan
|
||||
# Retarget
|
||||
bnNewBits = int(headerLast["bits"])
|
||||
bnNew = self.bits_to_target(bnNewBits)
|
||||
bnOld = bnNew
|
||||
# FLO: intermediate uint256 can overflow by 1 bit
|
||||
# const arith_uint256 bnPowLimit = UintToArith256(params.powLimit);
|
||||
fShift = bnNew > MAX_TARGET - 1
|
||||
|
||||
if (fShift):
|
||||
bnNew = bnNew >> 1
|
||||
bnNew = bnNew * nActualTimespan
|
||||
bnNew = bnNew / self.TargetTimespan(headerLast["block_height"] + 1)
|
||||
if fShift:
|
||||
bnNew = bnNew << 1
|
||||
if bnNew > MAX_TARGET:
|
||||
bnNew = MAX_TARGET
|
||||
bnNew = self.target_to_bits(int(bnNew))
|
||||
return bnNew
|
||||
|
||||
@classmethod
|
||||
def bits_to_target(cls, bits: int) -> int:
|
||||
bitsN = (bits >> 24) & 0xff
|
||||
if not (0x03 <= bitsN <= 0x1e):
|
||||
raise Exception("First part of bits should be in [0x03, 0x1e]")
|
||||
bitsBase = bits & 0xffffff
|
||||
if not (0x8000 <= bitsBase <= 0x7fffff):
|
||||
raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
|
||||
return bitsBase << (8 * (bitsN - 3))
|
||||
|
||||
@classmethod
|
||||
def target_to_bits(cls, target: int) -> int:
|
||||
c = ("%064x" % target)[2:]
|
||||
while c[:2] == '00' and len(c) > 6:
|
||||
c = c[2:]
|
||||
bitsN, bitsBase = len(c) // 2, int.from_bytes(bfh(c[:6]), byteorder='big')
|
||||
if bitsBase >= 0x800000:
|
||||
bitsN += 1
|
||||
bitsBase >>= 8
|
||||
return bitsN << 24 | bitsBase
|
||||
|
||||
def chainwork_of_header_at_height(self, height: int) -> int:
|
||||
"""work done by single header at given height"""
|
||||
chunk_idx = height // 2016 - 1
|
||||
target = self.get_target(chunk_idx)
|
||||
work = ((2 ** 256 - target - 1) // (target + 1)) + 1
|
||||
return work
|
||||
|
||||
@with_lock
|
||||
def get_chainwork(self, height=None) -> int:
|
||||
if height is None:
|
||||
height = max(0, self.height())
|
||||
if constants.net.TESTNET:
|
||||
# On testnet/regtest, difficulty works somewhat different.
|
||||
# It's out of scope to properly implement that.
|
||||
return height
|
||||
last_retarget = height // 2016 * 2016 - 1
|
||||
cached_height = last_retarget
|
||||
while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None:
|
||||
if cached_height <= -1:
|
||||
break
|
||||
cached_height -= 2016
|
||||
assert cached_height >= -1, cached_height
|
||||
running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)]
|
||||
while cached_height < last_retarget:
|
||||
cached_height += 2016
|
||||
work_in_single_header = self.chainwork_of_header_at_height(cached_height)
|
||||
work_in_chunk = 2016 * work_in_single_header
|
||||
running_total += work_in_chunk
|
||||
_CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total
|
||||
cached_height += 2016
|
||||
work_in_single_header = self.chainwork_of_header_at_height(cached_height)
|
||||
work_in_last_partial_chunk = (height % 2016 + 1) * work_in_single_header
|
||||
return running_total + work_in_last_partial_chunk
|
||||
|
||||
def can_connect(self, header: dict, check_height: bool=True) -> bool:
|
||||
if header is None:
|
||||
return False
|
||||
height = header['block_height']
|
||||
if check_height and self.height() != height - 1:
|
||||
#self.print_error("cannot connect at height", height)
|
||||
return False
|
||||
if height == 0:
|
||||
return hash_header(header) == constants.net.GENESIS
|
||||
try:
|
||||
prev_hash = self.get_hash(height - 1)
|
||||
except:
|
||||
return False
|
||||
if prev_hash != header.get('prev_block_hash'):
|
||||
return False
|
||||
try:
|
||||
target = self.get_target(height - 1)
|
||||
except MissingHeader:
|
||||
return False
|
||||
try:
|
||||
self.verify_header(header, prev_hash, target)
|
||||
except BaseException as e:
|
||||
return False
|
||||
return True
|
||||
|
||||
def connect_chunk(self, idx: int, hexdata: str) -> bool:
|
||||
assert idx >= 0, idx
|
||||
try:
|
||||
data = bfh(hexdata)
|
||||
self.verify_chunk(idx, data)
|
||||
#self.print_error("validated chunk %d" % idx)
|
||||
self.save_chunk(idx, data)
|
||||
return True
|
||||
except BaseException as e:
|
||||
self.print_error(f'verify_chunk idx {idx} failed: {repr(e)}')
|
||||
return False
|
||||
|
||||
def get_checkpoints(self):
|
||||
# for each chunk, store the hash of the last block and the target after the chunk
|
||||
cp = []
|
||||
n = self.height() // 2016
|
||||
for index in range(n):
|
||||
h = self.get_hash((index+1) * 2016 -1)
|
||||
target = self.get_target(index)
|
||||
cp.append((h, target))
|
||||
return cp
|
||||
|
||||
def AveragingInterval(self, height):
|
||||
# V1
|
||||
if height < constants.net.nHeight_Difficulty_Version2:
|
||||
return constants.net.nAveragingInterval_Version1
|
||||
# V2
|
||||
elif height < constants.net.nHeight_Difficulty_Version3:
|
||||
return constants.net.nAveragingInterval_Version2
|
||||
# V3
|
||||
else:
|
||||
return constants.net.nAveragingInterval_Version3
|
||||
|
||||
def MinActualTimespan(self, height):
|
||||
averagingTargetTimespan = self.AveragingInterval(height) * constants.net.nPowTargetSpacing
|
||||
# V1
|
||||
if height < constants.net.nHeight_Difficulty_Version2:
|
||||
return int(averagingTargetTimespan * (100 - constants.net.nMaxAdjustUp_Version1) / 100)
|
||||
# V2
|
||||
elif height < constants.net.nHeight_Difficulty_Version3:
|
||||
return int(averagingTargetTimespan * (100 - constants.net.nMaxAdjustUp_Version2) / 100)
|
||||
# V3
|
||||
else:
|
||||
return int(averagingTargetTimespan * (100 - constants.net.nMaxAdjustUp_Version3) / 100)
|
||||
|
||||
def MaxActualTimespan(self, height):
|
||||
averagingTargetTimespan = self.AveragingInterval(height) * constants.net.nPowTargetSpacing
|
||||
# V1
|
||||
if height < constants.net.nHeight_Difficulty_Version2:
|
||||
return int(averagingTargetTimespan * (100 + constants.net.nMaxAdjustDown_Version1) / 100)
|
||||
# V2
|
||||
elif height < constants.net.nHeight_Difficulty_Version3:
|
||||
return int(averagingTargetTimespan * (100 + constants.net.nMaxAdjustDown_Version2) / 100)
|
||||
# V3
|
||||
else:
|
||||
return int(averagingTargetTimespan * (100 + constants.net.nMaxAdjustDown_Version3) / 100)
|
||||
|
||||
def TargetTimespan(self, height):
|
||||
# V1
|
||||
if height < constants.net.nHeight_Difficulty_Version2:
|
||||
return constants.net.nTargetTimespan_Version1
|
||||
# V2
|
||||
if height < constants.net.nHeight_Difficulty_Version3:
|
||||
return constants.net.nAveragingInterval_Version2 * constants.net.nPowTargetSpacing
|
||||
# V3
|
||||
return constants.net.nAveragingInterval_Version3 * constants.net.nPowTargetSpacing
|
||||
|
||||
def DifficultyAdjustmentInterval(self, height):
|
||||
# V1
|
||||
if height < constants.net.nHeight_Difficulty_Version2:
|
||||
return constants.net.nInterval_Version1
|
||||
# V2
|
||||
if height < constants.net.nHeight_Difficulty_Version3:
|
||||
return constants.net.nInterval_Version2
|
||||
# V3
|
||||
return constants.net.nInterval_Version3
|
||||
|
||||
|
||||
def check_header(header: dict) -> Optional[Blockchain]:
|
||||
if type(header) is not dict:
|
||||
return None
|
||||
with blockchains_lock: chains = list(blockchains.values())
|
||||
for b in chains:
|
||||
if b.check_header(header):
|
||||
return b
|
||||
return None
|
||||
|
||||
|
||||
def can_connect(header: dict) -> Optional[Blockchain]:
|
||||
with blockchains_lock: chains = list(blockchains.values())
|
||||
for b in chains:
|
||||
if b.can_connect(header):
|
||||
return b
|
||||
return None
|
||||
3
electrum/checkpoints.json
Normal file
@ -0,0 +1,3 @@
|
||||
[
|
||||
|
||||
]
|
||||
3
electrum/checkpoints_testnet.json
Normal file
@ -0,0 +1,3 @@
|
||||
[
|
||||
|
||||
]
|
||||
@ -22,11 +22,12 @@
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
from collections import defaultdict, namedtuple
|
||||
from collections import defaultdict
|
||||
from math import floor, log10
|
||||
from typing import NamedTuple, List
|
||||
|
||||
from .bitcoin import sha256, COIN, TYPE_ADDRESS, is_address
|
||||
from .transaction import Transaction
|
||||
from .transaction import Transaction, TxOutput
|
||||
from .util import NotEnoughFunds, PrintError
|
||||
|
||||
|
||||
@ -68,13 +69,14 @@ class PRNG:
|
||||
x[i], x[j] = x[j], x[i]
|
||||
|
||||
|
||||
Bucket = namedtuple('Bucket',
|
||||
['desc',
|
||||
'weight', # as in BIP-141
|
||||
'value', # in satoshis
|
||||
'coins', # UTXOs
|
||||
'min_height', # min block height where a coin was confirmed
|
||||
'witness']) # whether any coin uses segwit
|
||||
class Bucket(NamedTuple):
|
||||
desc: str
|
||||
weight: int # as in BIP-141
|
||||
value: int # in satoshis
|
||||
coins: List[dict] # UTXOs
|
||||
min_height: int # min block height where a coin was confirmed
|
||||
witness: bool # whether any coin uses segwit
|
||||
|
||||
|
||||
def strip_unneeded(bkts, sufficient_funds):
|
||||
'''Remove buckets that are unnecessary in achieving the spend amount'''
|
||||
@ -82,8 +84,8 @@ def strip_unneeded(bkts, sufficient_funds):
|
||||
for i in range(len(bkts)):
|
||||
if not sufficient_funds(bkts[i + 1:]):
|
||||
return bkts[i:]
|
||||
# Shouldn't get here
|
||||
return bkts
|
||||
# none of the buckets are needed
|
||||
return []
|
||||
|
||||
class CoinChooserBase(PrintError):
|
||||
|
||||
@ -117,7 +119,7 @@ class CoinChooserBase(PrintError):
|
||||
|
||||
def change_amounts(self, tx, count, fee_estimator, dust_threshold):
|
||||
# Break change up if bigger than max_change
|
||||
output_amounts = [o[2] for o in tx.outputs()]
|
||||
output_amounts = [o.value for o in tx.outputs()]
|
||||
# Don't split change of less than 0.02 BTC
|
||||
max_change = max(max(output_amounts) * 1.25, 0.02 * COIN)
|
||||
|
||||
@ -178,14 +180,14 @@ class CoinChooserBase(PrintError):
|
||||
# size of the change output, add it to the transaction.
|
||||
dust = sum(amount for amount in amounts if amount < dust_threshold)
|
||||
amounts = [amount for amount in amounts if amount >= dust_threshold]
|
||||
change = [(TYPE_ADDRESS, addr, amount)
|
||||
change = [TxOutput(TYPE_ADDRESS, addr, amount)
|
||||
for addr, amount in zip(change_addrs, amounts)]
|
||||
self.print_error('change:', change)
|
||||
if dust:
|
||||
self.print_error('not keeping dust', dust)
|
||||
return change
|
||||
|
||||
def make_tx(self, coins, outputs, change_addrs, fee_estimator,
|
||||
def make_tx(self, coins, inputs, outputs, change_addrs, fee_estimator,
|
||||
dust_threshold):
|
||||
"""Select unspent coins to spend to pay outputs. If the change is
|
||||
greater than dust_threshold (after adding the change output to
|
||||
@ -200,11 +202,14 @@ class CoinChooserBase(PrintError):
|
||||
self.p = PRNG(''.join(sorted(utxos)))
|
||||
|
||||
# Copy the outputs so when adding change we don't modify "outputs"
|
||||
tx = Transaction.from_io([], outputs[:])
|
||||
tx = Transaction.from_io(inputs[:], outputs[:])
|
||||
input_value = tx.input_value()
|
||||
|
||||
# Weight of the transaction with no inputs and no change
|
||||
# Note: this will use legacy tx serialization as the need for "segwit"
|
||||
# would be detected from inputs. The only side effect should be that the
|
||||
# marker and flag are excluded, which is compensated in get_tx_weight()
|
||||
# FIXME calculation will be off by this (2 wu) in case of RBF batching
|
||||
base_weight = tx.estimated_weight()
|
||||
spent_amount = tx.output_value()
|
||||
|
||||
@ -228,7 +233,7 @@ class CoinChooserBase(PrintError):
|
||||
def sufficient_funds(buckets):
|
||||
'''Given a list of buckets, return True if it has enough
|
||||
value to pay for the transaction'''
|
||||
total_input = sum(bucket.value for bucket in buckets)
|
||||
total_input = input_value + sum(bucket.value for bucket in buckets)
|
||||
total_weight = get_tx_weight(buckets)
|
||||
return total_input >= spent_amount + fee_estimator_w(total_weight)
|
||||
|
||||
@ -354,9 +359,9 @@ class CoinChooserPrivacy(CoinChooserRandom):
|
||||
return [coin['address'] for coin in coins]
|
||||
|
||||
def penalty_func(self, tx):
|
||||
min_change = min(o[2] for o in tx.outputs()) * 0.75
|
||||
max_change = max(o[2] for o in tx.outputs()) * 1.33
|
||||
spent_amount = sum(o[2] for o in tx.outputs())
|
||||
min_change = min(o.value for o in tx.outputs()) * 0.75
|
||||
max_change = max(o.value for o in tx.outputs()) * 1.33
|
||||
spent_amount = sum(o.value for o in tx.outputs())
|
||||
|
||||
def penalty(buckets):
|
||||
badness = len(buckets) - 1
|
||||
@ -32,15 +32,26 @@ import ast
|
||||
import base64
|
||||
from functools import wraps
|
||||
from decimal import Decimal
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from .import util, ecc
|
||||
from .util import bfh, bh2u, format_satoshis, json_decode, print_error, json_encode
|
||||
from .import bitcoin
|
||||
from . import bitcoin
|
||||
from .bitcoin import is_address, hash_160, COIN, TYPE_ADDRESS
|
||||
from . import bip32
|
||||
from .i18n import _
|
||||
from .transaction import Transaction, multisig_script
|
||||
from .transaction import Transaction, multisig_script, TxOutput
|
||||
from .paymentrequest import PR_PAID, PR_UNPAID, PR_UNKNOWN, PR_EXPIRED
|
||||
from .plugins import run_hook
|
||||
from .synchronizer import Notifier
|
||||
from .storage import WalletStorage
|
||||
from . import keystore
|
||||
from .wallet import Wallet, Imported_Wallet, Abstract_Wallet
|
||||
from .mnemonic import Mnemonic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from .network import Network
|
||||
from .simple_config import SimpleConfig
|
||||
|
||||
|
||||
known_commands = {}
|
||||
|
||||
@ -91,7 +102,8 @@ def command(s):
|
||||
|
||||
class Commands:
|
||||
|
||||
def __init__(self, config, wallet, network, callback = None):
|
||||
def __init__(self, config: 'SimpleConfig', wallet: Abstract_Wallet,
|
||||
network: Optional['Network'], callback=None):
|
||||
self.config = config
|
||||
self.wallet = wallet
|
||||
self.network = network
|
||||
@ -123,17 +135,80 @@ class Commands:
|
||||
return ' '.join(sorted(known_commands.keys()))
|
||||
|
||||
@command('')
|
||||
def create(self, segwit=False):
|
||||
def create(self, passphrase=None, password=None, encrypt_file=True, segwit=False):
|
||||
"""Create a new wallet"""
|
||||
raise Exception('Not a JSON-RPC command')
|
||||
storage = WalletStorage(self.config.get_wallet_path())
|
||||
if storage.file_exists():
|
||||
raise Exception("Remove the existing wallet first!")
|
||||
|
||||
@command('wn')
|
||||
def restore(self, text):
|
||||
seed_type = 'segwit' if segwit else 'standard'
|
||||
seed = Mnemonic('en').make_seed(seed_type)
|
||||
k = keystore.from_seed(seed, passphrase)
|
||||
storage.put('keystore', k.dump())
|
||||
storage.put('wallet_type', 'standard')
|
||||
wallet = Wallet(storage)
|
||||
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
|
||||
wallet.synchronize()
|
||||
msg = "Please keep your seed in a safe place; if you lose it, you will not be able to restore your wallet."
|
||||
|
||||
wallet.storage.write()
|
||||
return {'seed': seed, 'path': wallet.storage.path, 'msg': msg}
|
||||
|
||||
@command('')
|
||||
def restore(self, text, passphrase=None, password=None, encrypt_file=True):
|
||||
"""Restore a wallet from text. Text can be a seed phrase, a master
|
||||
public key, a master private key, a list of bitcoin addresses
|
||||
or bitcoin private keys. If you want to be prompted for your
|
||||
seed, type '?' or ':' (concealed) """
|
||||
raise Exception('Not a JSON-RPC command')
|
||||
storage = WalletStorage(self.config.get_wallet_path())
|
||||
if storage.file_exists():
|
||||
raise Exception("Remove the existing wallet first!")
|
||||
|
||||
text = text.strip()
|
||||
if keystore.is_address_list(text):
|
||||
wallet = Imported_Wallet(storage)
|
||||
addresses = text.split()
|
||||
good_inputs, bad_inputs = wallet.import_addresses(addresses, write_to_disk=False)
|
||||
# FIXME tell user about bad_inputs
|
||||
if not good_inputs:
|
||||
raise Exception("None of the given addresses can be imported")
|
||||
elif keystore.is_private_key_list(text, allow_spaces_inside_key=False):
|
||||
k = keystore.Imported_KeyStore({})
|
||||
storage.put('keystore', k.dump())
|
||||
wallet = Imported_Wallet(storage)
|
||||
keys = keystore.get_private_keys(text)
|
||||
good_inputs, bad_inputs = wallet.import_private_keys(keys, None, write_to_disk=False)
|
||||
# FIXME tell user about bad_inputs
|
||||
if not good_inputs:
|
||||
raise Exception("None of the given privkeys can be imported")
|
||||
else:
|
||||
if keystore.is_seed(text):
|
||||
k = keystore.from_seed(text, passphrase)
|
||||
elif keystore.is_master_key(text):
|
||||
k = keystore.from_master_key(text)
|
||||
else:
|
||||
raise Exception("Seed or key not recognized")
|
||||
storage.put('keystore', k.dump())
|
||||
storage.put('wallet_type', 'standard')
|
||||
wallet = Wallet(storage)
|
||||
|
||||
assert not storage.file_exists(), "file was created too soon! plaintext keys might have been written to disk"
|
||||
wallet.update_password(old_pw=None, new_pw=password, encrypt_storage=encrypt_file)
|
||||
wallet.synchronize()
|
||||
|
||||
if self.network:
|
||||
wallet.start_network(self.network)
|
||||
print_error("Recovering wallet...")
|
||||
wallet.wait_until_synchronized()
|
||||
wallet.stop_threads()
|
||||
# note: we don't wait for SPV
|
||||
msg = "Recovery successful" if wallet.is_found() else "Found no history for this wallet"
|
||||
else:
|
||||
msg = ("This wallet was restored offline. It may contain more addresses than displayed. "
|
||||
"Start a daemon (not offline) to sync history.")
|
||||
|
||||
wallet.storage.write()
|
||||
return {'path': wallet.storage.path, 'msg': msg}
|
||||
|
||||
@command('wp')
|
||||
def password(self, password=None, new_password=None):
|
||||
@ -145,6 +220,11 @@ class Commands:
|
||||
self.wallet.storage.write()
|
||||
return {'password':self.wallet.has_password()}
|
||||
|
||||
@command('w')
|
||||
def get(self, key):
|
||||
"""Return item from wallet storage"""
|
||||
return self.wallet.storage.get(key)
|
||||
|
||||
@command('')
|
||||
def getconfig(self, key):
|
||||
"""Return a configuration variable. """
|
||||
@ -181,13 +261,13 @@ class Commands:
|
||||
walletless server query, results are not checked by SPV.
|
||||
"""
|
||||
sh = bitcoin.address_to_scripthash(address)
|
||||
return self.network.get_history_for_scripthash(sh)
|
||||
return self.network.run_from_another_thread(self.network.get_history_for_scripthash(sh))
|
||||
|
||||
@command('w')
|
||||
def listunspent(self):
|
||||
"""List unspent outputs. Returns the list of unspent transaction
|
||||
outputs in your wallet."""
|
||||
l = copy.deepcopy(self.wallet.get_utxos(exclude_frozen=False))
|
||||
l = copy.deepcopy(self.wallet.get_utxos())
|
||||
for i in l:
|
||||
v = i["value"]
|
||||
i["value"] = str(Decimal(v)/COIN) if v is not None else None
|
||||
@ -199,7 +279,7 @@ class Commands:
|
||||
is a walletless server query, results are not checked by SPV.
|
||||
"""
|
||||
sh = bitcoin.address_to_scripthash(address)
|
||||
return self.network.listunspent_for_scripthash(sh)
|
||||
return self.network.run_from_another_thread(self.network.listunspent_for_scripthash(sh))
|
||||
|
||||
@command('')
|
||||
def serialize(self, jsontx):
|
||||
@ -226,7 +306,7 @@ class Commands:
|
||||
txin['signatures'] = [None]
|
||||
txin['num_sig'] = 1
|
||||
|
||||
outputs = [(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
|
||||
outputs = [TxOutput(TYPE_ADDRESS, x['address'], int(x['value'])) for x in outputs]
|
||||
tx = Transaction.from_io(inputs, outputs, locktime=locktime)
|
||||
tx.sign(keypairs)
|
||||
return tx.as_dict()
|
||||
@ -249,13 +329,14 @@ class Commands:
|
||||
def deserialize(self, tx):
|
||||
"""Deserialize a serialized transaction"""
|
||||
tx = Transaction(tx)
|
||||
return tx.deserialize()
|
||||
return tx.deserialize(force_full_parse=True)
|
||||
|
||||
@command('n')
|
||||
def broadcast(self, tx):
|
||||
"""Broadcast a transaction to the network. """
|
||||
tx = Transaction(tx)
|
||||
return self.network.broadcast_transaction(tx)
|
||||
self.network.run_from_another_thread(self.network.broadcast_transaction(tx))
|
||||
return tx.txid()
|
||||
|
||||
@command('')
|
||||
def createmultisig(self, num, pubkeys):
|
||||
@ -322,7 +403,7 @@ class Commands:
|
||||
server query, results are not checked by SPV.
|
||||
"""
|
||||
sh = bitcoin.address_to_scripthash(address)
|
||||
out = self.network.get_balance_for_scripthash(sh)
|
||||
out = self.network.run_from_another_thread(self.network.get_balance_for_scripthash(sh))
|
||||
out["confirmed"] = str(Decimal(out["confirmed"])/COIN)
|
||||
out["unconfirmed"] = str(Decimal(out["unconfirmed"])/COIN)
|
||||
return out
|
||||
@ -331,7 +412,7 @@ class Commands:
|
||||
def getmerkle(self, txid, height):
|
||||
"""Get Merkle branch of a transaction included in a block. Electrum
|
||||
uses this to verify transactions (Simple Payment Verification)."""
|
||||
return self.network.get_merkle_for_transaction(txid, int(height))
|
||||
return self.network.run_from_another_thread(self.network.get_merkle_for_transaction(txid, int(height)))
|
||||
|
||||
@command('n')
|
||||
def getservers(self):
|
||||
@ -354,6 +435,16 @@ class Commands:
|
||||
"""Get master private key. Return your wallet\'s master private key"""
|
||||
return str(self.wallet.keystore.get_master_private_key(password))
|
||||
|
||||
@command('')
|
||||
def convert_xkey(self, xkey, xtype):
|
||||
"""Convert xtype of a master key. e.g. xpub -> ypub"""
|
||||
is_xprv = bip32.is_xprv(xkey)
|
||||
if not bip32.is_xpub(xkey) and not is_xprv:
|
||||
raise Exception('xkey should be a master public/private key')
|
||||
_, depth, fingerprint, child_number, c, cK = bip32.deserialize_xkey(xkey, is_xprv)
|
||||
serialize = bip32.serialize_xprv if is_xprv else bip32.serialize_xpub
|
||||
return serialize(xtype, c, cK, depth, fingerprint, child_number)
|
||||
|
||||
@command('wp')
|
||||
def getseed(self, password=None):
|
||||
"""Get seed phrase. Print the generation seed of your wallet."""
|
||||
@ -368,7 +459,7 @@ class Commands:
|
||||
try:
|
||||
addr = self.wallet.import_private_key(privkey, password)
|
||||
out = "Keypair imported: " + addr
|
||||
except BaseException as e:
|
||||
except Exception as e:
|
||||
out = "Error: " + str(e)
|
||||
return out
|
||||
|
||||
@ -415,11 +506,11 @@ class Commands:
|
||||
for address, amount in outputs:
|
||||
address = self._resolver(address)
|
||||
amount = satoshis(amount)
|
||||
final_outputs.append((TYPE_ADDRESS, address, amount))
|
||||
final_outputs.append(TxOutput(TYPE_ADDRESS, address, amount))
|
||||
|
||||
coins = self.wallet.get_spendable_coins(domain, self.config)
|
||||
tx = self.wallet.make_unsigned_transaction(coins, final_outputs, self.config, fee, change_addr)
|
||||
if locktime != None:
|
||||
if locktime != None:
|
||||
tx.locktime = locktime
|
||||
if rbf is None:
|
||||
rbf = self.config.get('use_rbf', True)
|
||||
@ -446,9 +537,15 @@ class Commands:
|
||||
return tx.as_dict()
|
||||
|
||||
@command('w')
|
||||
def history(self, year=None, show_addresses=False, show_fiat=False):
|
||||
def history(self, year=None, show_addresses=False, show_fiat=False, show_fees=False,
|
||||
from_height=None, to_height=None):
|
||||
"""Wallet history. Returns the transaction history of your wallet."""
|
||||
kwargs = {'show_addresses': show_addresses}
|
||||
kwargs = {
|
||||
'show_addresses': show_addresses,
|
||||
'show_fees': show_fees,
|
||||
'from_height': from_height,
|
||||
'to_height': to_height,
|
||||
}
|
||||
if year:
|
||||
import time
|
||||
start_date = datetime.datetime(year, 1, 1)
|
||||
@ -517,7 +614,7 @@ class Commands:
|
||||
if self.wallet and txid in self.wallet.transactions:
|
||||
tx = self.wallet.transactions[txid]
|
||||
else:
|
||||
raw = self.network.get_transaction(txid)
|
||||
raw = self.network.run_from_another_thread(self.network.get_transaction(txid))
|
||||
if raw:
|
||||
tx = Transaction(raw)
|
||||
else:
|
||||
@ -635,20 +732,11 @@ class Commands:
|
||||
self.wallet.remove_payment_request(k, self.config)
|
||||
|
||||
@command('n')
|
||||
def notify(self, address, URL):
|
||||
def notify(self, address: str, URL: str):
|
||||
"""Watch an address. Every time the address changes, a http POST is sent to the URL."""
|
||||
def callback(x):
|
||||
import urllib.request
|
||||
headers = {'content-type':'application/json'}
|
||||
data = {'address':address, 'status':x.get('result')}
|
||||
serialized_data = util.to_bytes(json.dumps(data))
|
||||
try:
|
||||
req = urllib.request.Request(URL, serialized_data, headers)
|
||||
response_stream = urllib.request.urlopen(req, timeout=5)
|
||||
util.print_error('Got Response for %s' % address)
|
||||
except BaseException as e:
|
||||
util.print_error(str(e))
|
||||
self.network.subscribe_to_addresses([address], callback)
|
||||
if not hasattr(self, "_notifier"):
|
||||
self._notifier = Notifier(self.network)
|
||||
self.network.run_from_another_thread(self._notifier.start_watching_queue.put((address, URL)))
|
||||
return True
|
||||
|
||||
@command('wn')
|
||||
@ -680,6 +768,16 @@ class Commands:
|
||||
# for the python console
|
||||
return sorted(known_commands.keys())
|
||||
|
||||
|
||||
def eval_bool(x: str) -> bool:
|
||||
if x == 'false': return False
|
||||
if x == 'true': return True
|
||||
try:
|
||||
return bool(ast.literal_eval(x))
|
||||
except:
|
||||
return bool(x)
|
||||
|
||||
|
||||
param_descriptions = {
|
||||
'privkey': 'Private key. Type \'?\' to get a prompt.',
|
||||
'destination': 'Bitcoin address, contact or alias',
|
||||
@ -702,6 +800,7 @@ param_descriptions = {
|
||||
command_options = {
|
||||
'password': ("-W", "Password"),
|
||||
'new_password':(None, "New Password"),
|
||||
'encrypt_file':(None, "Whether the file on disk should be encrypted with the provided password"),
|
||||
'receiving': (None, "Show only receiving addresses"),
|
||||
'change': (None, "Show only change addresses"),
|
||||
'frozen': (None, "Show only frozen addresses"),
|
||||
@ -717,6 +816,7 @@ command_options = {
|
||||
'nbits': (None, "Number of bits of entropy"),
|
||||
'segwit': (None, "Create segwit seed"),
|
||||
'language': ("-L", "Default language for wordlist"),
|
||||
'passphrase': (None, "Seed extension"),
|
||||
'privkey': (None, "Private key. Set to '?' to get a prompt."),
|
||||
'unsigned': ("-u", "Do not sign transaction"),
|
||||
'rbf': (None, "Replace-by-fee transaction"),
|
||||
@ -731,9 +831,12 @@ command_options = {
|
||||
'paid': (None, "Show only paid requests."),
|
||||
'show_addresses': (None, "Show input and output addresses"),
|
||||
'show_fiat': (None, "Show fiat value of transactions"),
|
||||
'show_fees': (None, "Show miner fees paid by transactions"),
|
||||
'year': (None, "Show history for a given year"),
|
||||
'fee_method': (None, "Fee estimation method to use"),
|
||||
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position")
|
||||
'fee_level': (None, "Float between 0.0 and 1.0, representing fee slider position"),
|
||||
'from_height': (None, "Only show transactions that confirmed after given block height"),
|
||||
'to_height': (None, "Only show transactions that confirmed before given block height"),
|
||||
}
|
||||
|
||||
|
||||
@ -745,6 +848,8 @@ arg_types = {
|
||||
'nbits': int,
|
||||
'imax': int,
|
||||
'year': int,
|
||||
'from_height': int,
|
||||
'to_height': int,
|
||||
'tx': tx_from_str,
|
||||
'pubkeys': json_loads,
|
||||
'jsontx': json_loads,
|
||||
@ -755,6 +860,7 @@ arg_types = {
|
||||
'locktime': int,
|
||||
'fee_method': str,
|
||||
'fee_level': json_loads,
|
||||
'encrypt_file': eval_bool,
|
||||
}
|
||||
|
||||
config_variables = {
|
||||
@ -826,10 +932,14 @@ def add_network_options(parser):
|
||||
parser.add_argument("-1", "--oneserver", action="store_true", dest="oneserver", default=None, help="connect to one server only")
|
||||
parser.add_argument("-s", "--server", dest="server", default=None, help="set server host:port:protocol, where protocol is either t (tcp) or s (ssl)")
|
||||
parser.add_argument("-p", "--proxy", dest="proxy", default=None, help="set proxy [type:]host[:port], where type is socks4,socks5 or http")
|
||||
parser.add_argument("--noonion", action="store_true", dest="noonion", default=None, help="do not try to connect to onion servers")
|
||||
parser.add_argument("--skipmerklecheck", action="store_true", dest="skipmerklecheck", default=False, help="Tolerate invalid merkle proofs from server")
|
||||
|
||||
def add_global_options(parser):
|
||||
group = parser.add_argument_group('global options')
|
||||
group.add_argument("-v", "--verbose", action="store_true", dest="verbose", default=False, help="Show debugging information")
|
||||
# const is for when no argument is given to verbosity
|
||||
# default is for when the flag is missing
|
||||
group.add_argument("-v", dest="verbosity", help="Set verbosity filter", default='', const='*', nargs='?')
|
||||
group.add_argument("-D", "--dir", dest="electrum_path", help="electrum directory")
|
||||
group.add_argument("-P", "--portable", action="store_true", dest="portable", default=False, help="Use local 'electrum_data' directory")
|
||||
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
|
||||
@ -850,6 +960,7 @@ def get_parser():
|
||||
parser_gui.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
|
||||
parser_gui.add_argument("-m", action="store_true", dest="hide_gui", default=False, help="hide GUI on startup")
|
||||
parser_gui.add_argument("-L", "--lang", dest="language", default=None, help="default language used in GUI")
|
||||
parser_gui.add_argument("--daemon", action="store_true", dest="daemon", default=False, help="keep daemon running after GUI is closed")
|
||||
add_network_options(parser_gui)
|
||||
add_global_options(parser_gui)
|
||||
# daemon
|
||||
@ -863,12 +974,10 @@ def get_parser():
|
||||
cmd = known_commands[cmdname]
|
||||
p = subparsers.add_parser(cmdname, help=cmd.help, description=cmd.description)
|
||||
add_global_options(p)
|
||||
if cmdname == 'restore':
|
||||
p.add_argument("-o", "--offline", action="store_true", dest="offline", default=False, help="Run offline")
|
||||
for optname, default in zip(cmd.options, cmd.defaults):
|
||||
a, help = command_options[optname]
|
||||
b = '--' + optname
|
||||
action = "store_true" if type(default) is bool else 'store'
|
||||
action = "store_true" if default is False else 'store'
|
||||
args = (a, b) if a else (b,)
|
||||
if action == 'store':
|
||||
_type = arg_types.get(optname, str)
|
||||
@ -37,62 +37,114 @@ def read_json(filename, default):
|
||||
return r
|
||||
|
||||
|
||||
class BitcoinMainnet:
|
||||
class AbstractNet:
|
||||
|
||||
@classmethod
|
||||
def max_checkpoint(cls) -> int:
|
||||
return max(0, len(cls.CHECKPOINTS) * 2016 - 1)
|
||||
|
||||
|
||||
class BitcoinMainnet(AbstractNet):
|
||||
|
||||
TESTNET = False
|
||||
WIF_PREFIX = 0x80
|
||||
ADDRTYPE_P2PKH = 0
|
||||
ADDRTYPE_P2SH = 5
|
||||
SEGWIT_HRP = "bc"
|
||||
GENESIS = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
|
||||
WIF_PREFIX = 0xa3
|
||||
ADDRTYPE_P2PKH = 35
|
||||
ADDRTYPE_P2SH = 94
|
||||
SEGWIT_HRP = "flo"
|
||||
GENESIS = "09c7781c9df90708e278c35d38ea5c9041d7ecfcdd1c56ba67274b7cff3e1cea"
|
||||
DEFAULT_PORTS = {'t': '50001', 's': '50002'}
|
||||
DEFAULT_SERVERS = read_json('servers.json', {})
|
||||
CHECKPOINTS = read_json('checkpoints.json', [])
|
||||
|
||||
XPRV_HEADERS = {
|
||||
'standard': 0x0488ade4, # xprv
|
||||
'standard': 0x01343c31, # xprv
|
||||
'p2wpkh-p2sh': 0x049d7878, # yprv
|
||||
'p2wsh-p2sh': 0x0295b005, # Yprv
|
||||
'p2wpkh': 0x04b2430c, # zprv
|
||||
'p2wsh': 0x02aa7a99, # Zprv
|
||||
}
|
||||
XPUB_HEADERS = {
|
||||
'standard': 0x0488b21e, # xpub
|
||||
'standard': 0x0134406b, # xpub
|
||||
'p2wpkh-p2sh': 0x049d7cb2, # ypub
|
||||
'p2wsh-p2sh': 0x0295b43f, # Ypub
|
||||
'p2wpkh': 0x04b24746, # zpub
|
||||
'p2wsh': 0x02aa7ed3, # Zpub
|
||||
}
|
||||
BIP44_COIN_TYPE = 0
|
||||
BIP44_COIN_TYPE = 216
|
||||
# FLO Network constants
|
||||
fPowAllowMinDifficultyBlocks = False
|
||||
fPowNoRetargeting = False
|
||||
nRuleChangeActivationThreshold = 6048 # 75% of 8064
|
||||
nMinerConfirmationWindow = 8064
|
||||
# Difficulty adjustments
|
||||
nPowTargetSpacing = 40 # 40s block time
|
||||
# V1
|
||||
nTargetTimespan_Version1 = 60 * 60
|
||||
nInterval_Version1 = nTargetTimespan_Version1 / nPowTargetSpacing
|
||||
nMaxAdjustUp_Version1 = 75
|
||||
nMaxAdjustDown_Version1 = 300
|
||||
nAveragingInterval_Version1 = nInterval_Version1
|
||||
# V2
|
||||
nHeight_Difficulty_Version2 = 208440
|
||||
nInterval_Version2 = 15
|
||||
nMaxAdjustDown_Version2 = 300
|
||||
nMaxAdjustUp_Version2 = 75
|
||||
nAveragingInterval_Version2 = nInterval_Version2
|
||||
# V3
|
||||
nHeight_Difficulty_Version3 = 426000
|
||||
nInterval_Version3 = 1
|
||||
nMaxAdjustDown_Version3 = 3
|
||||
nMaxAdjustUp_Version3 = 2
|
||||
nAveragingInterval_Version3 = 6
|
||||
|
||||
|
||||
class BitcoinTestnet:
|
||||
class BitcoinTestnet(AbstractNet):
|
||||
|
||||
TESTNET = True
|
||||
WIF_PREFIX = 0xef
|
||||
ADDRTYPE_P2PKH = 111
|
||||
ADDRTYPE_P2SH = 196
|
||||
SEGWIT_HRP = "tb"
|
||||
GENESIS = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
|
||||
ADDRTYPE_P2PKH = 115
|
||||
ADDRTYPE_P2SH = 58
|
||||
SEGWIT_HRP = "tflo"
|
||||
GENESIS = "9b7bc86236c34b5e3a39367c036b7fe8807a966c22a7a1f0da2a198a27e03731"
|
||||
DEFAULT_PORTS = {'t': '51001', 's': '51002'}
|
||||
DEFAULT_SERVERS = read_json('servers_testnet.json', {})
|
||||
CHECKPOINTS = read_json('checkpoints_testnet.json', [])
|
||||
|
||||
XPRV_HEADERS = {
|
||||
'standard': 0x04358394, # tprv
|
||||
'standard': 0x01343c23, # tprv
|
||||
'p2wpkh-p2sh': 0x044a4e28, # uprv
|
||||
'p2wsh-p2sh': 0x024285b5, # Uprv
|
||||
'p2wpkh': 0x045f18bc, # vprv
|
||||
'p2wsh': 0x02575048, # Vprv
|
||||
}
|
||||
XPUB_HEADERS = {
|
||||
'standard': 0x043587cf, # tpub
|
||||
'standard': 0x013440e2, # tpub
|
||||
'p2wpkh-p2sh': 0x044a5262, # upub
|
||||
'p2wsh-p2sh': 0x024289ef, # Upub
|
||||
'p2wpkh': 0x045f1cf6, # vpub
|
||||
'p2wsh': 0x02575483, # Vpub
|
||||
}
|
||||
BIP44_COIN_TYPE = 1
|
||||
#Difficulty adjustments
|
||||
nPowTargetSpacing = 40 # 40 block time
|
||||
# V1
|
||||
nTargetTimespan_Version1 = 60 * 60
|
||||
nInterval_Version1 = nTargetTimespan_Version1 / nPowTargetSpacing;
|
||||
nMaxAdjustUp_Version1 = 75
|
||||
nMaxAdjustDown_Version1 = 300
|
||||
nAveragingInterval_Version1 = nInterval_Version1
|
||||
# V2
|
||||
nHeight_Difficulty_Version2 = 50000
|
||||
nInterval_Version2 = 15
|
||||
nMaxAdjustDown_Version2 = 300
|
||||
nMaxAdjustUp_Version2 = 75
|
||||
nAveragingInterval_Version2 = nInterval_Version2
|
||||
# V3
|
||||
nHeight_Difficulty_Version3 = 60000
|
||||
nInterval_Version3 = 1
|
||||
nMaxAdjustDown_Version3 = 3
|
||||
nMaxAdjustUp_Version3 = 2
|
||||
nAveragingInterval_Version3 = 6
|
||||
|
||||
|
||||
class BitcoinRegtest(BitcoinTestnet):
|
||||
@ -21,11 +21,9 @@
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import re
|
||||
|
||||
import dns
|
||||
from dns.exception import DNSException
|
||||
import json
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
from . import bitcoin
|
||||
from . import dnssec
|
||||
@ -67,8 +65,9 @@ class Contacts(dict):
|
||||
|
||||
def pop(self, key):
|
||||
if key in self.keys():
|
||||
dict.pop(self, key)
|
||||
res = dict.pop(self, key)
|
||||
self.save()
|
||||
return res
|
||||
|
||||
def resolve(self, k):
|
||||
if bitcoin.is_address(k):
|
||||
@ -100,7 +99,7 @@ class Contacts(dict):
|
||||
try:
|
||||
records, validated = dnssec.query(url, dns.rdatatype.TXT)
|
||||
except DNSException as e:
|
||||
print_error('Error resolving openalias: ', str(e))
|
||||
print_error(f'Error resolving openalias: {repr(e)}')
|
||||
return None
|
||||
prefix = 'btc'
|
||||
for record in records:
|
||||
216
electrum/crypto.py
Normal file
@ -0,0 +1,216 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Electrum - lightweight Bitcoin client
|
||||
# Copyright (C) 2018 The Electrum developers
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person
|
||||
# obtaining a copy of this software and associated documentation files
|
||||
# (the "Software"), to deal in the Software without restriction,
|
||||
# including without limitation the rights to use, copy, modify, merge,
|
||||
# publish, distribute, sublicense, and/or sell copies of the Software,
|
||||
# and to permit persons to whom the Software is furnished to do so,
|
||||
# subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be
|
||||
# included in all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
|
||||
import base64
|
||||
import os
|
||||
import hashlib
|
||||
import hmac
|
||||
from typing import Union
|
||||
|
||||
import pyaes
|
||||
|
||||
from .util import assert_bytes, InvalidPassword, to_bytes, to_string, WalletFileException
|
||||
from .i18n import _
|
||||
|
||||
|
||||
try:
|
||||
from Cryptodome.Cipher import AES
|
||||
except:
|
||||
AES = None
|
||||
|
||||
|
||||
class InvalidPadding(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def append_PKCS7_padding(data: bytes) -> bytes:
|
||||
assert_bytes(data)
|
||||
padlen = 16 - (len(data) % 16)
|
||||
return data + bytes([padlen]) * padlen
|
||||
|
||||
|
||||
def strip_PKCS7_padding(data: bytes) -> bytes:
|
||||
assert_bytes(data)
|
||||
if len(data) % 16 != 0 or len(data) == 0:
|
||||
raise InvalidPadding("invalid length")
|
||||
padlen = data[-1]
|
||||
if not (0 < padlen <= 16):
|
||||
raise InvalidPadding("invalid padding byte (out of range)")
|
||||
for i in data[-padlen:]:
|
||||
if i != padlen:
|
||||
raise InvalidPadding("invalid padding byte (inconsistent)")
|
||||
return data[0:-padlen]
|
||||
|
||||
|
||||
def aes_encrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
|
||||
assert_bytes(key, iv, data)
|
||||
data = append_PKCS7_padding(data)
|
||||
if AES:
|
||||
e = AES.new(key, AES.MODE_CBC, iv).encrypt(data)
|
||||
else:
|
||||
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
|
||||
aes = pyaes.Encrypter(aes_cbc, padding=pyaes.PADDING_NONE)
|
||||
e = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer
|
||||
return e
|
||||
|
||||
|
||||
def aes_decrypt_with_iv(key: bytes, iv: bytes, data: bytes) -> bytes:
|
||||
assert_bytes(key, iv, data)
|
||||
if AES:
|
||||
cipher = AES.new(key, AES.MODE_CBC, iv)
|
||||
data = cipher.decrypt(data)
|
||||
else:
|
||||
aes_cbc = pyaes.AESModeOfOperationCBC(key, iv=iv)
|
||||
aes = pyaes.Decrypter(aes_cbc, padding=pyaes.PADDING_NONE)
|
||||
data = aes.feed(data) + aes.feed() # empty aes.feed() flushes buffer
|
||||
try:
|
||||
return strip_PKCS7_padding(data)
|
||||
except InvalidPadding:
|
||||
raise InvalidPassword()
|
||||
|
||||
|
||||
def EncodeAES_base64(secret: bytes, msg: bytes) -> bytes:
|
||||
"""Returns base64 encoded ciphertext."""
|
||||
e = EncodeAES_bytes(secret, msg)
|
||||
return base64.b64encode(e)
|
||||
|
||||
|
||||
def EncodeAES_bytes(secret: bytes, msg: bytes) -> bytes:
|
||||
assert_bytes(msg)
|
||||
iv = bytes(os.urandom(16))
|
||||
ct = aes_encrypt_with_iv(secret, iv, msg)
|
||||
return iv + ct
|
||||
|
||||
|
||||
def DecodeAES_base64(secret: bytes, ciphertext_b64: Union[bytes, str]) -> bytes:
|
||||
ciphertext = bytes(base64.b64decode(ciphertext_b64))
|
||||
return DecodeAES_bytes(secret, ciphertext)
|
||||
|
||||
|
||||
def DecodeAES_bytes(secret: bytes, ciphertext: bytes) -> bytes:
|
||||
assert_bytes(ciphertext)
|
||||
iv, e = ciphertext[:16], ciphertext[16:]
|
||||
s = aes_decrypt_with_iv(secret, iv, e)
|
||||
return s
|
||||
|
||||
|
||||
PW_HASH_VERSION_LATEST = 1
|
||||
KNOWN_PW_HASH_VERSIONS = (1, 2, )
|
||||
SUPPORTED_PW_HASH_VERSIONS = (1, )
|
||||
assert PW_HASH_VERSION_LATEST in KNOWN_PW_HASH_VERSIONS
|
||||
assert PW_HASH_VERSION_LATEST in SUPPORTED_PW_HASH_VERSIONS
|
||||
|
||||
|
||||
class UnexpectedPasswordHashVersion(InvalidPassword, WalletFileException):
|
||||
def __init__(self, version):
|
||||
self.version = version
|
||||
|
||||
def __str__(self):
|
||||
return "{unexpected}: {version}\n{instruction}".format(
|
||||
unexpected=_("Unexpected password hash version"),
|
||||
version=self.version,
|
||||
instruction=_('You are most likely using an outdated version of Electrum. Please update.'))
|
||||
|
||||
|
||||
class UnsupportedPasswordHashVersion(InvalidPassword, WalletFileException):
|
||||
def __init__(self, version):
|
||||
self.version = version
|
||||
|
||||
def __str__(self):
|
||||
return "{unsupported}: {version}\n{instruction}".format(
|
||||
unsupported=_("Unsupported password hash version"),
|
||||
version=self.version,
|
||||
instruction=f"To open this wallet, try 'git checkout password_v{self.version}'.\n"
|
||||
"Alternatively, restore from seed.")
|
||||
|
||||
|
||||
def _hash_password(password: Union[bytes, str], *, version: int) -> bytes:
|
||||
pw = to_bytes(password, 'utf8')
|
||||
if version not in SUPPORTED_PW_HASH_VERSIONS:
|
||||
raise UnsupportedPasswordHashVersion(version)
|
||||
if version == 1:
|
||||
return sha256d(pw)
|
||||
else:
|
||||
assert version not in KNOWN_PW_HASH_VERSIONS
|
||||
raise UnexpectedPasswordHashVersion(version)
|
||||
|
||||
|
||||
def pw_encode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
|
||||
if not password:
|
||||
return data
|
||||
if version not in KNOWN_PW_HASH_VERSIONS:
|
||||
raise UnexpectedPasswordHashVersion(version)
|
||||
# derive key from password
|
||||
secret = _hash_password(password, version=version)
|
||||
# encrypt given data
|
||||
ciphertext = EncodeAES_bytes(secret, to_bytes(data, "utf8"))
|
||||
ciphertext_b64 = base64.b64encode(ciphertext)
|
||||
return ciphertext_b64.decode('utf8')
|
||||
|
||||
|
||||
def pw_decode(data: str, password: Union[bytes, str, None], *, version: int) -> str:
|
||||
if password is None:
|
||||
return data
|
||||
if version not in KNOWN_PW_HASH_VERSIONS:
|
||||
raise UnexpectedPasswordHashVersion(version)
|
||||
data_bytes = bytes(base64.b64decode(data))
|
||||
# derive key from password
|
||||
secret = _hash_password(password, version=version)
|
||||
# decrypt given data
|
||||
try:
|
||||
d = to_string(DecodeAES_bytes(secret, data_bytes), "utf8")
|
||||
except Exception as e:
|
||||
raise InvalidPassword() from e
|
||||
return d
|
||||
|
||||
|
||||
def sha256(x: Union[bytes, str]) -> bytes:
|
||||
x = to_bytes(x, 'utf8')
|
||||
return bytes(hashlib.sha256(x).digest())
|
||||
|
||||
|
||||
def sha256d(x: Union[bytes, str]) -> bytes:
|
||||
x = to_bytes(x, 'utf8')
|
||||
out = bytes(sha256(sha256(x)))
|
||||
return out
|
||||
|
||||
|
||||
def hash_160(x: bytes) -> bytes:
|
||||
try:
|
||||
md = hashlib.new('ripemd160')
|
||||
md.update(sha256(x))
|
||||
return md.digest()
|
||||
except BaseException:
|
||||
from . import ripemd
|
||||
md = ripemd.new(sha256(x))
|
||||
return md.digest()
|
||||
|
||||
|
||||
def hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes:
|
||||
if hasattr(hmac, 'digest'):
|
||||
# requires python 3.7+; faster
|
||||
return hmac.digest(key, msg, digest)
|
||||
else:
|
||||
return hmac.new(key, msg, digest).digest()
|
||||
44
electrum/currencies.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"CoinMarketcap": [
|
||||
"AED",
|
||||
"ALL",
|
||||
"ARS",
|
||||
"AUD",
|
||||
"BHD",
|
||||
"BOB",
|
||||
"BRL",
|
||||
"KHR",
|
||||
"CAD",
|
||||
"CLP",
|
||||
"CNY",
|
||||
"COP",
|
||||
"CUP",
|
||||
"CZK",
|
||||
"EGP",
|
||||
"EUR",
|
||||
"HKD",
|
||||
"HUF",
|
||||
"ISK",
|
||||
"INR",
|
||||
"IDR",
|
||||
"IQD",
|
||||
"ILS",
|
||||
"JPY",
|
||||
"LBP",
|
||||
"MYR",
|
||||
"MXN",
|
||||
"NPR",
|
||||
"NZD",
|
||||
"NGN",
|
||||
"NOK",
|
||||
"GBP",
|
||||
"QAR",
|
||||
"RUB",
|
||||
"SGD",
|
||||
"SEK",
|
||||
"CHF",
|
||||
"THB",
|
||||
"USD",
|
||||
"VND"
|
||||
]
|
||||
}
|
||||
@ -22,29 +22,31 @@
|
||||
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
# SOFTWARE.
|
||||
import asyncio
|
||||
import ast
|
||||
import os
|
||||
import time
|
||||
import traceback
|
||||
import sys
|
||||
import threading
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
# from jsonrpc import JSONRPCResponseManager
|
||||
import jsonrpclib
|
||||
from .jsonrpc import VerifyingJSONRPCServer
|
||||
|
||||
from .jsonrpc import VerifyingJSONRPCServer
|
||||
from .version import ELECTRUM_VERSION
|
||||
from .network import Network
|
||||
from .util import json_decode, DaemonThread
|
||||
from .util import print_error, to_string
|
||||
from .wallet import Wallet
|
||||
from .util import (json_decode, DaemonThread, print_error, to_string,
|
||||
create_and_start_event_loop, profiler, standardize_path)
|
||||
from .wallet import Wallet, Abstract_Wallet
|
||||
from .storage import WalletStorage
|
||||
from .commands import known_commands, Commands
|
||||
from .simple_config import SimpleConfig
|
||||
from .exchange_rate import FxThread
|
||||
from .plugins import run_hook
|
||||
from .plugin import run_hook
|
||||
|
||||
|
||||
def get_lockfile(config):
|
||||
def get_lockfile(config: SimpleConfig):
|
||||
return os.path.join(config.path, 'daemon')
|
||||
|
||||
|
||||
@ -52,7 +54,7 @@ def remove_lockfile(lockfile):
|
||||
os.unlink(lockfile)
|
||||
|
||||
|
||||
def get_fd_or_server(config):
|
||||
def get_fd_or_server(config: SimpleConfig):
|
||||
'''Tries to create the lockfile, using O_EXCL to
|
||||
prevent races. If it succeeds it returns the FD.
|
||||
Otherwise try and connect to the server specified in the lockfile.
|
||||
@ -71,7 +73,7 @@ def get_fd_or_server(config):
|
||||
remove_lockfile(lockfile)
|
||||
|
||||
|
||||
def get_server(config):
|
||||
def get_server(config: SimpleConfig) -> Optional[jsonrpclib.Server]:
|
||||
lockfile = get_lockfile(config)
|
||||
while True:
|
||||
create_time = None
|
||||
@ -90,14 +92,14 @@ def get_server(config):
|
||||
server.ping()
|
||||
return server
|
||||
except Exception as e:
|
||||
print_error("[get_server]", e)
|
||||
print_error(f"failed to connect to JSON-RPC server: {e}")
|
||||
if not create_time or create_time < time.time() - 1.0:
|
||||
return None
|
||||
# Sleep a bit and try again; it might have just been started
|
||||
time.sleep(1.0)
|
||||
|
||||
|
||||
def get_rpc_credentials(config):
|
||||
def get_rpc_credentials(config: SimpleConfig) -> Tuple[str, str]:
|
||||
rpc_user = config.get('rpcuser', None)
|
||||
rpc_password = config.get('rpcpassword', None)
|
||||
if rpc_user is None or rpc_password is None:
|
||||
@ -119,26 +121,33 @@ def get_rpc_credentials(config):
|
||||
|
||||
class Daemon(DaemonThread):
|
||||
|
||||
def __init__(self, config, fd, is_gui):
|
||||
@profiler
|
||||
def __init__(self, config: SimpleConfig, fd=None, *, listen_jsonrpc=True):
|
||||
DaemonThread.__init__(self)
|
||||
self.config = config
|
||||
if fd is None and listen_jsonrpc:
|
||||
fd, server = get_fd_or_server(config)
|
||||
if fd is None: raise Exception('failed to lock daemon; already running?')
|
||||
self.asyncio_loop, self._stop_loop, self._loop_thread = create_and_start_event_loop()
|
||||
if config.get('offline'):
|
||||
self.network = None
|
||||
else:
|
||||
self.network = Network(config)
|
||||
self.network.start()
|
||||
self.network._loop_thread = self._loop_thread
|
||||
self.fx = FxThread(config, self.network)
|
||||
if self.network:
|
||||
self.network.add_jobs([self.fx])
|
||||
self.network.start([self.fx.run])
|
||||
self.gui = None
|
||||
self.wallets = {}
|
||||
self.wallets = {} # type: Dict[str, Abstract_Wallet]
|
||||
# Setup JSONRPC server
|
||||
self.init_server(config, fd, is_gui)
|
||||
self.server = None
|
||||
if listen_jsonrpc:
|
||||
self.init_server(config, fd)
|
||||
self.start()
|
||||
|
||||
def init_server(self, config, fd, is_gui):
|
||||
def init_server(self, config: SimpleConfig, fd):
|
||||
host = config.get('rpchost', '127.0.0.1')
|
||||
port = config.get('rpcport', 0)
|
||||
|
||||
rpc_user, rpc_password = get_rpc_credentials(config)
|
||||
try:
|
||||
server = VerifyingJSONRPCServer((host, port), logRequests=False,
|
||||
@ -153,19 +162,18 @@ class Daemon(DaemonThread):
|
||||
self.server = server
|
||||
server.timeout = 0.1
|
||||
server.register_function(self.ping, 'ping')
|
||||
if is_gui:
|
||||
server.register_function(self.run_gui, 'gui')
|
||||
else:
|
||||
server.register_function(self.run_daemon, 'daemon')
|
||||
self.cmd_runner = Commands(self.config, None, self.network)
|
||||
for cmdname in known_commands:
|
||||
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
|
||||
server.register_function(self.run_cmdline, 'run_cmdline')
|
||||
server.register_function(self.run_gui, 'gui')
|
||||
server.register_function(self.run_daemon, 'daemon')
|
||||
self.cmd_runner = Commands(self.config, None, self.network)
|
||||
for cmdname in known_commands:
|
||||
server.register_function(getattr(self.cmd_runner, cmdname), cmdname)
|
||||
server.register_function(self.run_cmdline, 'run_cmdline')
|
||||
|
||||
def ping(self):
|
||||
return True
|
||||
|
||||
def run_daemon(self, config_options):
|
||||
asyncio.set_event_loop(self.asyncio_loop)
|
||||
config = SimpleConfig(config_options)
|
||||
sub = config.get('subcommand')
|
||||
assert sub in [None, 'start', 'stop', 'status', 'load_wallet', 'close_wallet']
|
||||
@ -187,18 +195,18 @@ class Daemon(DaemonThread):
|
||||
response = False
|
||||
elif sub == 'status':
|
||||
if self.network:
|
||||
p = self.network.get_parameters()
|
||||
net_params = self.network.get_parameters()
|
||||
current_wallet = self.cmd_runner.wallet
|
||||
current_wallet_path = current_wallet.storage.path \
|
||||
if current_wallet else None
|
||||
response = {
|
||||
'path': self.network.config.path,
|
||||
'server': p[0],
|
||||
'server': net_params.host,
|
||||
'blockchain_height': self.network.get_local_height(),
|
||||
'server_height': self.network.get_server_height(),
|
||||
'spv_nodes': len(self.network.get_interfaces()),
|
||||
'connected': self.network.is_connected(),
|
||||
'auto_connect': p[4],
|
||||
'auto_connect': net_params.auto_connect,
|
||||
'version': ELECTRUM_VERSION,
|
||||
'wallets': {k: w.is_up_to_date()
|
||||
for k, w in self.wallets.items()},
|
||||
@ -215,18 +223,19 @@ class Daemon(DaemonThread):
|
||||
def run_gui(self, config_options):
|
||||
config = SimpleConfig(config_options)
|
||||
if self.gui:
|
||||
#if hasattr(self.gui, 'new_window'):
|
||||
# path = config.get_wallet_path()
|
||||
# self.gui.new_window(path, config.get('url'))
|
||||
# response = "ok"
|
||||
#else:
|
||||
# response = "error: current GUI does not support multiple windows"
|
||||
response = "error: Electrum GUI already running"
|
||||
if hasattr(self.gui, 'new_window'):
|
||||
config.open_last_wallet()
|
||||
path = config.get_wallet_path()
|
||||
self.gui.new_window(path, config.get('url'))
|
||||
response = "ok"
|
||||
else:
|
||||
response = "error: current GUI does not support multiple windows"
|
||||
else:
|
||||
response = "Error: Electrum is running in daemon mode. Please stop the daemon first."
|
||||
return response
|
||||
|
||||
def load_wallet(self, path, password):
|
||||
def load_wallet(self, path, password) -> Optional[Abstract_Wallet]:
|
||||
path = standardize_path(path)
|
||||
# wizard will be launched if we return
|
||||
if path in self.wallets:
|
||||
wallet = self.wallets[path]
|
||||
@ -243,22 +252,31 @@ class Daemon(DaemonThread):
|
||||
if storage.get_action():
|
||||
return
|
||||
wallet = Wallet(storage)
|
||||
wallet.start_threads(self.network)
|
||||
wallet.start_network(self.network)
|
||||
self.wallets[path] = wallet
|
||||
return wallet
|
||||
|
||||
def add_wallet(self, wallet):
|
||||
def add_wallet(self, wallet: Abstract_Wallet):
|
||||
path = wallet.storage.path
|
||||
self.wallets[path] = wallet
|
||||
|
||||
def get_wallet(self, path):
|
||||
return self.wallets.get(path)
|
||||
|
||||
def delete_wallet(self, path):
|
||||
self.stop_wallet(path)
|
||||
if os.path.exists(path):
|
||||
os.unlink(path)
|
||||
return True
|
||||
return False
|
||||
|
||||
def stop_wallet(self, path):
|
||||
wallet = self.wallets.pop(path)
|
||||
wallet = self.wallets.pop(path, None)
|
||||
if not wallet: return
|
||||
wallet.stop_threads()
|
||||
|
||||
def run_cmdline(self, config_options):
|
||||
asyncio.set_event_loop(self.asyncio_loop)
|
||||
password = config_options.get('password')
|
||||
new_password = config_options.get('new_password')
|
||||
config = SimpleConfig(config_options)
|
||||
@ -284,30 +302,39 @@ class Daemon(DaemonThread):
|
||||
kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x))
|
||||
cmd_runner = Commands(config, wallet, self.network)
|
||||
func = getattr(cmd_runner, cmd.name)
|
||||
result = func(*args, **kwargs)
|
||||
try:
|
||||
result = func(*args, **kwargs)
|
||||
except TypeError as e:
|
||||
raise Exception("Wrapping TypeError to prevent JSONRPC-Pelix from hiding traceback") from e
|
||||
return result
|
||||
|
||||
def run(self):
|
||||
while self.is_running():
|
||||
self.server.handle_request() if self.server else time.sleep(0.1)
|
||||
# stop network/wallets
|
||||
for k, wallet in self.wallets.items():
|
||||
wallet.stop_threads()
|
||||
if self.network:
|
||||
self.print_error("shutting down network")
|
||||
self.network.stop()
|
||||
self.network.join()
|
||||
# stop event loop
|
||||
self.asyncio_loop.call_soon_threadsafe(self._stop_loop.set_result, 1)
|
||||
self._loop_thread.join(timeout=1)
|
||||
self.on_stop()
|
||||
|
||||
def stop(self):
|
||||
if self.gui:
|
||||
self.gui.stop()
|
||||
self.print_error("stopping, removing lockfile")
|
||||
remove_lockfile(get_lockfile(self.config))
|
||||
DaemonThread.stop(self)
|
||||
|
||||
def init_gui(self, config, plugins):
|
||||
threading.current_thread().setName('GUI')
|
||||
gui_name = config.get('gui', 'qt')
|
||||
if gui_name in ['lite', 'classic']:
|
||||
gui_name = 'qt'
|
||||
gui = __import__('electrum_gui.' + gui_name, fromlist=['electrum_gui'])
|
||||
gui = __import__('electrum.gui.' + gui_name, fromlist=['electrum'])
|
||||
self.gui = gui.ElectrumGui(config, self, plugins)
|
||||
try:
|
||||
self.gui.main()
|
||||
@ -110,11 +110,9 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
||||
if rrsig.algorithm == ECDSAP256SHA256:
|
||||
curve = ecdsa.curves.NIST256p
|
||||
key_len = 32
|
||||
digest_len = 32
|
||||
elif rrsig.algorithm == ECDSAP384SHA384:
|
||||
curve = ecdsa.curves.NIST384p
|
||||
key_len = 48
|
||||
digest_len = 48
|
||||
else:
|
||||
# shouldn't happen
|
||||
raise ValidationFailure('unknown ECDSA curve')
|
||||
@ -141,7 +139,7 @@ def python_validate_rrsig(rrset, rrsig, keys, origin=None, now=None):
|
||||
rrnamebuf = rrname.to_digestable(origin)
|
||||
rrfixed = struct.pack('!HHI', rdataset.rdtype, rdataset.rdclass,
|
||||
rrsig.original_ttl)
|
||||
rrlist = sorted(rdataset);
|
||||
rrlist = sorted(rdataset)
|
||||
for rr in rrlist:
|
||||
hash.update(rrnamebuf)
|
||||
hash.update(rrfixed)
|
||||
@ -24,10 +24,8 @@
|
||||
# SOFTWARE.
|
||||
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
from typing import Union
|
||||
|
||||
from typing import Union, Tuple
|
||||
|
||||
import ecdsa
|
||||
from ecdsa.ecdsa import curve_secp256k1, generator_secp256k1
|
||||
@ -36,8 +34,10 @@ from ecdsa.ellipticcurve import Point
|
||||
from ecdsa.util import string_to_number, number_to_string
|
||||
|
||||
from .util import bfh, bh2u, assert_bytes, print_error, to_bytes, InvalidPassword, profiler
|
||||
from .crypto import (Hash, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot)
|
||||
from .crypto import (sha256d, aes_encrypt_with_iv, aes_decrypt_with_iv, hmac_oneshot)
|
||||
from .ecc_fast import do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1
|
||||
from . import msqr
|
||||
from . import constants
|
||||
|
||||
|
||||
do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1()
|
||||
@ -53,31 +53,31 @@ def point_at_infinity():
|
||||
return ECPubkey(None)
|
||||
|
||||
|
||||
def sig_string_from_der_sig(der_sig, order=CURVE_ORDER):
|
||||
def sig_string_from_der_sig(der_sig: bytes, order=CURVE_ORDER) -> bytes:
|
||||
r, s = ecdsa.util.sigdecode_der(der_sig, order)
|
||||
return ecdsa.util.sigencode_string(r, s, order)
|
||||
|
||||
|
||||
def der_sig_from_sig_string(sig_string, order=CURVE_ORDER):
|
||||
def der_sig_from_sig_string(sig_string: bytes, order=CURVE_ORDER) -> bytes:
|
||||
r, s = ecdsa.util.sigdecode_string(sig_string, order)
|
||||
return ecdsa.util.sigencode_der_canonize(r, s, order)
|
||||
|
||||
|
||||
def der_sig_from_r_and_s(r, s, order=CURVE_ORDER):
|
||||
def der_sig_from_r_and_s(r: int, s: int, order=CURVE_ORDER) -> bytes:
|
||||
return ecdsa.util.sigencode_der_canonize(r, s, order)
|
||||
|
||||
|
||||
def get_r_and_s_from_der_sig(der_sig, order=CURVE_ORDER):
|
||||
def get_r_and_s_from_der_sig(der_sig: bytes, order=CURVE_ORDER) -> Tuple[int, int]:
|
||||
r, s = ecdsa.util.sigdecode_der(der_sig, order)
|
||||
return r, s
|
||||
|
||||
|
||||
def get_r_and_s_from_sig_string(sig_string, order=CURVE_ORDER):
|
||||
def get_r_and_s_from_sig_string(sig_string: bytes, order=CURVE_ORDER) -> Tuple[int, int]:
|
||||
r, s = ecdsa.util.sigdecode_string(sig_string, order)
|
||||
return r, s
|
||||
|
||||
|
||||
def sig_string_from_r_and_s(r, s, order=CURVE_ORDER):
|
||||
def sig_string_from_r_and_s(r: int, s: int, order=CURVE_ORDER) -> bytes:
|
||||
return ecdsa.util.sigencode_string_canonize(r, s, order)
|
||||
|
||||
|
||||
@ -94,23 +94,22 @@ def point_to_ser(P, compressed=True) -> bytes:
|
||||
return bfh('04'+('%064x' % x)+('%064x' % y))
|
||||
|
||||
|
||||
def get_y_coord_from_x(x, odd=True):
|
||||
def get_y_coord_from_x(x: int, odd: bool=True) -> int:
|
||||
curve = curve_secp256k1
|
||||
_p = curve.p()
|
||||
_a = curve.a()
|
||||
_b = curve.b()
|
||||
for offset in range(128):
|
||||
Mx = x + offset
|
||||
My2 = pow(Mx, 3, _p) + _a * pow(Mx, 2, _p) + _b % _p
|
||||
My = pow(My2, (_p + 1) // 4, _p)
|
||||
if curve.contains_point(Mx, My):
|
||||
if odd == bool(My & 1):
|
||||
return My
|
||||
return _p - My
|
||||
raise Exception('ECC_YfromX: No Y found')
|
||||
x = x % _p
|
||||
y2 = (pow(x, 3, _p) + _a * x + _b) % _p
|
||||
y = msqr.modular_sqrt(y2, _p)
|
||||
if curve.contains_point(x, y):
|
||||
if odd == bool(y & 1):
|
||||
return y
|
||||
return _p - y
|
||||
raise InvalidECPointException()
|
||||
|
||||
|
||||
def ser_to_point(ser: bytes) -> (int, int):
|
||||
def ser_to_point(ser: bytes) -> Tuple[int, int]:
|
||||
if ser[0] not in (0x02, 0x03, 0x04):
|
||||
raise ValueError('Unexpected first byte: {}'.format(ser[0]))
|
||||
if ser[0] == 0x04:
|
||||
@ -227,7 +226,7 @@ class ECPubkey(object):
|
||||
def get_public_key_hex(self, compressed=True):
|
||||
return bh2u(self.get_public_key_bytes(compressed))
|
||||
|
||||
def point(self) -> (int, int):
|
||||
def point(self) -> Tuple[int, int]:
|
||||
return self._pubkey.point.x(), self._pubkey.point.y()
|
||||
|
||||
def __mul__(self, other: int):
|
||||
@ -254,7 +253,7 @@ class ECPubkey(object):
|
||||
|
||||
def verify_message_for_address(self, sig65: bytes, message: bytes) -> None:
|
||||
assert_bytes(message)
|
||||
h = Hash(msg_magic(message))
|
||||
h = sha256d(msg_magic(message))
|
||||
public_key, compressed = self.from_signature65(sig65, h)
|
||||
# check public key
|
||||
if public_key != self:
|
||||
@ -296,6 +295,14 @@ class ECPubkey(object):
|
||||
def is_at_infinity(self):
|
||||
return self == point_at_infinity()
|
||||
|
||||
@classmethod
|
||||
def is_pubkey_bytes(cls, b: bytes):
|
||||
try:
|
||||
ECPubkey(b)
|
||||
return True
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
def msg_magic(message: bytes) -> bytes:
|
||||
from .bitcoin import var_int
|
||||
@ -303,16 +310,17 @@ def msg_magic(message: bytes) -> bytes:
|
||||
return b"\x18Bitcoin Signed Message:\n" + length + message
|
||||
|
||||
|
||||
def verify_message_with_address(address: str, sig65: bytes, message: bytes):
|
||||
def verify_message_with_address(address: str, sig65: bytes, message: bytes, *, net=None):
|
||||
from .bitcoin import pubkey_to_address
|
||||
assert_bytes(sig65, message)
|
||||
if net is None: net = constants.net
|
||||
try:
|
||||
h = Hash(msg_magic(message))
|
||||
h = sha256d(msg_magic(message))
|
||||
public_key, compressed = ECPubkey.from_signature65(sig65, h)
|
||||
# check public key using the address
|
||||
pubkey_hex = public_key.get_public_key_hex(compressed)
|
||||
for txin_type in ['p2pkh','p2wpkh','p2wpkh-p2sh']:
|
||||
addr = pubkey_to_address(txin_type, pubkey_hex)
|
||||
addr = pubkey_to_address(txin_type, pubkey_hex, net=net)
|
||||
if address == addr:
|
||||
break
|
||||
else:
|
||||
@ -321,7 +329,7 @@ def verify_message_with_address(address: str, sig65: bytes, message: bytes):
|
||||
public_key.verify_message_hash(sig65[1:], h)
|
||||
return True
|
||||
except Exception as e:
|
||||
print_error("Verification error: {0}".format(e))
|
||||
print_error(f"Verification error: {repr(e)}")
|
||||
return False
|
||||
|
||||
|
||||
@ -397,14 +405,14 @@ class ECPrivkey(ECPubkey):
|
||||
raise Exception("error: cannot sign message. no recid fits..")
|
||||
|
||||
message = to_bytes(message, 'utf8')
|
||||
msg_hash = Hash(msg_magic(message))
|
||||
msg_hash = sha256d(msg_magic(message))
|
||||
sig_string = self.sign(msg_hash,
|
||||
sigencode=sig_string_from_r_and_s,
|
||||
sigdecode=get_r_and_s_from_sig_string)
|
||||
sig65, recid = bruteforce_recid(sig_string)
|
||||
return sig65
|
||||
|
||||
def decrypt_message(self, encrypted, magic=b'BIE1'):
|
||||
def decrypt_message(self, encrypted: Tuple[str, bytes], magic: bytes=b'BIE1') -> bytes:
|
||||
encrypted = base64.b64decode(encrypted)
|
||||
if len(encrypted) < 85:
|
||||
raise Exception('invalid ciphertext: length')
|
||||
@ -429,6 +437,6 @@ class ECPrivkey(ECPubkey):
|
||||
return aes_decrypt_with_iv(key_e, iv, ciphertext)
|
||||
|
||||
|
||||
def construct_sig65(sig_string, recid, is_compressed):
|
||||
def construct_sig65(sig_string: bytes, recid: int, is_compressed: bool) -> bytes:
|
||||
comp = 4 if is_compressed else 0
|
||||
return bytes([27 + recid + comp]) + sig_string
|
||||
@ -72,6 +72,9 @@ def load_library():
|
||||
secp256k1.secp256k1_ecdsa_signature_parse_compact.argtypes = [c_void_p, c_char_p, c_char_p]
|
||||
secp256k1.secp256k1_ecdsa_signature_parse_compact.restype = c_int
|
||||
|
||||
secp256k1.secp256k1_ecdsa_signature_normalize.argtypes = [c_void_p, c_char_p, c_char_p]
|
||||
secp256k1.secp256k1_ecdsa_signature_normalize.restype = c_int
|
||||
|
||||
secp256k1.secp256k1_ecdsa_signature_serialize_compact.argtypes = [c_void_p, c_char_p, c_char_p]
|
||||
secp256k1.secp256k1_ecdsa_signature_serialize_compact.restype = c_int
|
||||
|
||||
@ -181,7 +184,9 @@ def _prepare_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1():
|
||||
|
||||
def do_monkey_patching_of_python_ecdsa_internals_with_libsecp256k1():
|
||||
if not _libsecp256k1:
|
||||
print_error('[ecc] warning: libsecp256k1 library not available, falling back to python-ecdsa')
|
||||
# FIXME print_error will always print as 'verbosity' is not yet initialised
|
||||
print_error('[ecc] info: libsecp256k1 library not available, falling back to python-ecdsa. '
|
||||
'This means signing operations will be slower.')
|
||||
return
|
||||
if not _patched_functions.prepared_to_patch:
|
||||
raise Exception("can't patch python-ecdsa without preparations")
|
||||
1
electrum/electrum
Symbolic link
@ -0,0 +1 @@
|
||||
../run_electrum
|
||||
@ -1,18 +1,23 @@
|
||||
import asyncio
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
import requests
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
from threading import Thread
|
||||
import time
|
||||
import csv
|
||||
import decimal
|
||||
from decimal import Decimal
|
||||
import concurrent.futures
|
||||
import traceback
|
||||
from typing import Sequence
|
||||
|
||||
from .bitcoin import COIN
|
||||
from .i18n import _
|
||||
from .util import PrintError, ThreadJob, make_dir
|
||||
from .util import (PrintError, ThreadJob, make_dir, log_exceptions,
|
||||
make_aiohttp_session, resource_path)
|
||||
from .network import Network
|
||||
from .simple_config import SimpleConfig
|
||||
|
||||
|
||||
# See https://en.wikipedia.org/wiki/ISO_4217
|
||||
@ -32,34 +37,42 @@ class ExchangeBase(PrintError):
|
||||
self.on_quotes = on_quotes
|
||||
self.on_history = on_history
|
||||
|
||||
def get_json(self, site, get_string):
|
||||
async def get_raw(self, site, get_string):
|
||||
# APIs must have https
|
||||
url = ''.join(['https://', site, get_string])
|
||||
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'}, timeout=10)
|
||||
return response.json()
|
||||
async with make_aiohttp_session(Network.get_instance().proxy) as session:
|
||||
async with session.get(url) as response:
|
||||
return await response.text()
|
||||
|
||||
def get_csv(self, site, get_string):
|
||||
async def get_json(self, site, get_string):
|
||||
# APIs must have https
|
||||
url = ''.join(['https://', site, get_string])
|
||||
response = requests.request('GET', url, headers={'User-Agent' : 'Electrum'})
|
||||
reader = csv.DictReader(response.content.decode().split('\n'))
|
||||
async with make_aiohttp_session(Network.get_instance().proxy) as session:
|
||||
async with session.get(url) as response:
|
||||
# set content_type to None to disable checking MIME type
|
||||
return await response.json(content_type=None)
|
||||
|
||||
async def get_csv(self, site, get_string):
|
||||
raw = await self.get_raw(site, get_string)
|
||||
reader = csv.DictReader(raw.split('\n'))
|
||||
return list(reader)
|
||||
|
||||
def name(self):
|
||||
return self.__class__.__name__
|
||||
|
||||
def update_safe(self, ccy):
|
||||
@log_exceptions
|
||||
async def update_safe(self, ccy):
|
||||
try:
|
||||
self.print_error("getting fx quotes for", ccy)
|
||||
self.quotes = self.get_rates(ccy)
|
||||
self.quotes = await self.get_rates(ccy)
|
||||
self.print_error("received fx quotes")
|
||||
except BaseException as e:
|
||||
self.print_error("failed fx quotes:", e)
|
||||
self.print_error("failed fx quotes:", repr(e))
|
||||
self.quotes = {}
|
||||
self.on_quotes()
|
||||
|
||||
def update(self, ccy):
|
||||
t = Thread(target=self.update_safe, args=(ccy,))
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
asyncio.get_event_loop().create_task(self.update_safe(ccy))
|
||||
|
||||
def read_historical_rates(self, ccy, cache_dir):
|
||||
filename = os.path.join(cache_dir, self.name() + '_'+ ccy)
|
||||
@ -78,13 +91,15 @@ class ExchangeBase(PrintError):
|
||||
self.on_history()
|
||||
return h
|
||||
|
||||
def get_historical_rates_safe(self, ccy, cache_dir):
|
||||
@log_exceptions
|
||||
async def get_historical_rates_safe(self, ccy, cache_dir):
|
||||
try:
|
||||
self.print_error("requesting fx history for", ccy)
|
||||
h = self.request_history(ccy)
|
||||
h = await self.request_history(ccy)
|
||||
self.print_error("received fx history for", ccy)
|
||||
except BaseException as e:
|
||||
self.print_error("failed fx history:", e)
|
||||
#traceback.print_exc()
|
||||
return
|
||||
filename = os.path.join(cache_dir, self.name() + '_' + ccy)
|
||||
with open(filename, 'w', encoding='utf-8') as f:
|
||||
@ -100,9 +115,7 @@ class ExchangeBase(PrintError):
|
||||
if h is None:
|
||||
h = self.read_historical_rates(ccy, cache_dir)
|
||||
if h is None or h['timestamp'] < time.time() - 24*3600:
|
||||
t = Thread(target=self.get_historical_rates_safe, args=(ccy, cache_dir))
|
||||
t.setDaemon(True)
|
||||
t.start()
|
||||
asyncio.get_event_loop().create_task(self.get_historical_rates_safe(ccy, cache_dir))
|
||||
|
||||
def history_ccys(self):
|
||||
return []
|
||||
@ -116,8 +129,8 @@ class ExchangeBase(PrintError):
|
||||
|
||||
class BitcoinAverage(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('apiv2.bitcoinaverage.com', '/indices/global/ticker/short')
|
||||
return dict([(r.replace("BTC", ""), Decimal(json[r]['last']))
|
||||
for r in json if r != 'timestamp'])
|
||||
|
||||
@ -126,8 +139,8 @@ class BitcoinAverage(ExchangeBase):
|
||||
'MXN', 'NOK', 'NZD', 'PLN', 'RON', 'RUB', 'SEK', 'SGD', 'USD',
|
||||
'ZAR']
|
||||
|
||||
def request_history(self, ccy):
|
||||
history = self.get_csv('apiv2.bitcoinaverage.com',
|
||||
async def request_history(self, ccy):
|
||||
history = await self.get_csv('apiv2.bitcoinaverage.com',
|
||||
"/indices/global/history/BTC%s?period=alltime&format=csv" % ccy)
|
||||
return dict([(h['DateTime'][:10], h['Average'])
|
||||
for h in history])
|
||||
@ -135,8 +148,8 @@ class BitcoinAverage(ExchangeBase):
|
||||
|
||||
class Bitcointoyou(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('bitcointoyou.com', "/API/ticker.aspx")
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('bitcointoyou.com', "/API/ticker.aspx")
|
||||
return {'BRL': Decimal(json['ticker']['last'])}
|
||||
|
||||
def history_ccys(self):
|
||||
@ -145,8 +158,8 @@ class Bitcointoyou(ExchangeBase):
|
||||
|
||||
class BitcoinVenezuela(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.bitcoinvenezuela.com', '/')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.bitcoinvenezuela.com', '/')
|
||||
rates = [(r, json['BTC'][r]) for r in json['BTC']
|
||||
if json['BTC'][r] is not None] # Giving NULL for LTC
|
||||
return dict(rates)
|
||||
@ -154,99 +167,99 @@ class BitcoinVenezuela(ExchangeBase):
|
||||
def history_ccys(self):
|
||||
return ['ARS', 'EUR', 'USD', 'VEF']
|
||||
|
||||
def request_history(self, ccy):
|
||||
return self.get_json('api.bitcoinvenezuela.com',
|
||||
"/historical/index.php?coin=BTC")[ccy +'_BTC']
|
||||
async def request_history(self, ccy):
|
||||
json = await self.get_json('api.bitcoinvenezuela.com',
|
||||
"/historical/index.php?coin=BTC")
|
||||
return json[ccy +'_BTC']
|
||||
|
||||
|
||||
class Bitbank(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('public.bitbank.cc', '/btc_jpy/ticker')
|
||||
return {'JPY': Decimal(json['data']['last'])}
|
||||
|
||||
|
||||
class BitFlyer(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('bitflyer.jp', '/api/echo/price')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('bitflyer.jp', '/api/echo/price')
|
||||
return {'JPY': Decimal(json['mid'])}
|
||||
|
||||
|
||||
class Bitmarket(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('www.bitmarket.pl', '/json/BTCPLN/ticker.json')
|
||||
return {'PLN': Decimal(json['last'])}
|
||||
|
||||
|
||||
class BitPay(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('bitpay.com', '/api/rates')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('bitpay.com', '/api/rates')
|
||||
return dict([(r['code'], Decimal(r['rate'])) for r in json])
|
||||
|
||||
|
||||
class Bitso(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.bitso.com', '/v2/ticker')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.bitso.com', '/v2/ticker')
|
||||
return {'MXN': Decimal(json['last'])}
|
||||
|
||||
|
||||
class BitStamp(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('www.bitstamp.net', '/api/ticker/')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('www.bitstamp.net', '/api/ticker/')
|
||||
return {'USD': Decimal(json['last'])}
|
||||
|
||||
|
||||
class Bitvalor(ExchangeBase):
|
||||
|
||||
def get_rates(self,ccy):
|
||||
json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
async def get_rates(self,ccy):
|
||||
json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
return {'BRL': Decimal(json['ticker_1h']['total']['last'])}
|
||||
|
||||
|
||||
class BlockchainInfo(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('blockchain.info', '/ticker')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('blockchain.info', '/ticker')
|
||||
return dict([(r, Decimal(json[r]['15m'])) for r in json])
|
||||
|
||||
|
||||
class BTCChina(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('data.btcchina.com', '/data/ticker')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('data.btcchina.com', '/data/ticker')
|
||||
return {'CNY': Decimal(json['ticker']['last'])}
|
||||
|
||||
|
||||
class BTCParalelo(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('btcparalelo.com', '/api/price')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('btcparalelo.com', '/api/price')
|
||||
return {'VEF': Decimal(json['price'])}
|
||||
|
||||
|
||||
class Coinbase(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('coinbase.com',
|
||||
'/api/v1/currencies/exchange_rates')
|
||||
return dict([(r[7:].upper(), Decimal(json[r]))
|
||||
for r in json if r.startswith('btc_to_')])
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.coinbase.com',
|
||||
'/v2/exchange-rates?currency=BTC')
|
||||
return {ccy: Decimal(rate) for (ccy, rate) in json["data"]["rates"].items()}
|
||||
|
||||
|
||||
class CoinDesk(ExchangeBase):
|
||||
|
||||
def get_currencies(self):
|
||||
dicts = self.get_json('api.coindesk.com',
|
||||
async def get_currencies(self):
|
||||
dicts = await self.get_json('api.coindesk.com',
|
||||
'/v1/bpi/supported-currencies.json')
|
||||
return [d['currency'] for d in dicts]
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.coindesk.com',
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.coindesk.com',
|
||||
'/v1/bpi/currentprice/%s.json' % ccy)
|
||||
result = {ccy: Decimal(json['bpi'][ccy]['rate_float'])}
|
||||
return result
|
||||
@ -257,35 +270,43 @@ class CoinDesk(ExchangeBase):
|
||||
def history_ccys(self):
|
||||
return self.history_starts().keys()
|
||||
|
||||
def request_history(self, ccy):
|
||||
async def request_history(self, ccy):
|
||||
start = self.history_starts()[ccy]
|
||||
end = datetime.today().strftime('%Y-%m-%d')
|
||||
# Note ?currency and ?index don't work as documented. Sigh.
|
||||
query = ('/v1/bpi/historical/close.json?start=%s&end=%s'
|
||||
% (start, end))
|
||||
json = self.get_json('api.coindesk.com', query)
|
||||
json = await self.get_json('api.coindesk.com', query)
|
||||
return json['bpi']
|
||||
|
||||
|
||||
class CoinMarketcap(ExchangeBase):
|
||||
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('pro-api.coinmarketcap.com','/v1/cryptocurrency/quotes/latest?symbol=FLO&CMC_PRO_API_KEY=194ee8a1-5a58-4f3e-ba07-a1d6bc633210&convert=%s' % ccy)
|
||||
result = {ccy: Decimal(json['data']['FLO']['quote'][ccy]['price'])}
|
||||
return result
|
||||
|
||||
|
||||
class Coinsecure(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.coinsecure.in', '/v0/noauth/newticker')
|
||||
return {'INR': Decimal(json['lastprice'] / 100.0 )}
|
||||
|
||||
|
||||
class Foxbit(ExchangeBase):
|
||||
|
||||
def get_rates(self,ccy):
|
||||
json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
async def get_rates(self,ccy):
|
||||
json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
return {'BRL': Decimal(json['ticker_1h']['exchanges']['FOX']['last'])}
|
||||
|
||||
|
||||
class itBit(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
async def get_rates(self, ccy):
|
||||
ccys = ['USD', 'EUR', 'SGD']
|
||||
json = self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
|
||||
json = await self.get_json('api.itbit.com', '/v1/markets/XBT%s/ticker' % ccy)
|
||||
result = dict.fromkeys(ccys)
|
||||
if ccy in ccys:
|
||||
result[ccy] = Decimal(json['lastPrice'])
|
||||
@ -294,10 +315,10 @@ class itBit(ExchangeBase):
|
||||
|
||||
class Kraken(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
async def get_rates(self, ccy):
|
||||
ccys = ['EUR', 'USD', 'CAD', 'GBP', 'JPY']
|
||||
pairs = ['XBT%s' % c for c in ccys]
|
||||
json = self.get_json('api.kraken.com',
|
||||
json = await self.get_json('api.kraken.com',
|
||||
'/0/public/Ticker?pair=%s' % ','.join(pairs))
|
||||
return dict((k[-3:], Decimal(float(v['c'][0])))
|
||||
for k, v in json['result'].items())
|
||||
@ -305,45 +326,45 @@ class Kraken(ExchangeBase):
|
||||
|
||||
class LocalBitcoins(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('localbitcoins.com',
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('localbitcoins.com',
|
||||
'/bitcoinaverage/ticker-all-currencies/')
|
||||
return dict([(r, Decimal(json[r]['rates']['last'])) for r in json])
|
||||
|
||||
|
||||
class MercadoBitcoin(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
return {'BRL': Decimal(json['ticker_1h']['exchanges']['MBT']['last'])}
|
||||
|
||||
|
||||
class NegocieCoins(ExchangeBase):
|
||||
|
||||
def get_rates(self,ccy):
|
||||
json = self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
async def get_rates(self,ccy):
|
||||
json = await self.get_json('api.bitvalor.com', '/v1/ticker.json')
|
||||
return {'BRL': Decimal(json['ticker_1h']['exchanges']['NEG']['last'])}
|
||||
|
||||
class TheRockTrading(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.therocktrading.com',
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.therocktrading.com',
|
||||
'/v1/funds/BTCEUR/ticker')
|
||||
return {'EUR': Decimal(json['last'])}
|
||||
|
||||
class Unocoin(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('www.unocoin.com', 'trade?buy')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('www.unocoin.com', 'trade?buy')
|
||||
return {'INR': Decimal(json)}
|
||||
|
||||
|
||||
class WEX(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json_eur = self.get_json('wex.nz', '/api/3/ticker/btc_eur')
|
||||
json_rub = self.get_json('wex.nz', '/api/3/ticker/btc_rur')
|
||||
json_usd = self.get_json('wex.nz', '/api/3/ticker/btc_usd')
|
||||
async def get_rates(self, ccy):
|
||||
json_eur = await self.get_json('wex.nz', '/api/3/ticker/btc_eur')
|
||||
json_rub = await self.get_json('wex.nz', '/api/3/ticker/btc_rur')
|
||||
json_usd = await self.get_json('wex.nz', '/api/3/ticker/btc_usd')
|
||||
return {'EUR': Decimal(json_eur['btc_eur']['last']),
|
||||
'RUB': Decimal(json_rub['btc_rur']['last']),
|
||||
'USD': Decimal(json_usd['btc_usd']['last'])}
|
||||
@ -351,15 +372,15 @@ class WEX(ExchangeBase):
|
||||
|
||||
class Winkdex(ExchangeBase):
|
||||
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('winkdex.com', '/api/v0/price')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('winkdex.com', '/api/v0/price')
|
||||
return {'USD': Decimal(json['price'] / 100.0)}
|
||||
|
||||
def history_ccys(self):
|
||||
return ['USD']
|
||||
|
||||
def request_history(self, ccy):
|
||||
json = self.get_json('winkdex.com',
|
||||
async def request_history(self, ccy):
|
||||
json = await self.get_json('winkdex.com',
|
||||
"/api/v0/series?start_time=1342915200")
|
||||
history = json['series'][0]['results']
|
||||
return dict([(h['timestamp'][:10], h['price'] / 100.0)
|
||||
@ -367,8 +388,8 @@ class Winkdex(ExchangeBase):
|
||||
|
||||
|
||||
class Zaif(ExchangeBase):
|
||||
def get_rates(self, ccy):
|
||||
json = self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
|
||||
async def get_rates(self, ccy):
|
||||
json = await self.get_json('api.zaif.jp', '/api/1/last_price/btc_jpy')
|
||||
return {'JPY': Decimal(json['last_price'])}
|
||||
|
||||
|
||||
@ -381,8 +402,7 @@ def dictinvert(d):
|
||||
return inv
|
||||
|
||||
def get_exchanges_and_currencies():
|
||||
import os, json
|
||||
path = os.path.join(os.path.dirname(__file__), 'currencies.json')
|
||||
path = resource_path('currencies.json')
|
||||
try:
|
||||
with open(path, 'r', encoding='utf-8') as f:
|
||||
return json.loads(f.read())
|
||||
@ -423,48 +443,67 @@ def get_exchanges_by_ccy(history=True):
|
||||
|
||||
class FxThread(ThreadJob):
|
||||
|
||||
def __init__(self, config, network):
|
||||
def __init__(self, config: SimpleConfig, network: Network):
|
||||
self.config = config
|
||||
self.network = network
|
||||
if self.network:
|
||||
self.network.register_callback(self.set_proxy, ['proxy_set'])
|
||||
self.ccy = self.get_currency()
|
||||
self.history_used_spot = False
|
||||
self.ccy_combo = None
|
||||
self.hist_checkbox = None
|
||||
self.cache_dir = os.path.join(config.path, 'cache')
|
||||
self._trigger = asyncio.Event()
|
||||
self._trigger.set()
|
||||
self.set_exchange(self.config_exchange())
|
||||
make_dir(self.cache_dir)
|
||||
|
||||
def get_currencies(self, h):
|
||||
d = get_exchanges_by_ccy(h)
|
||||
def set_proxy(self, trigger_name, *args):
|
||||
self._trigger.set()
|
||||
|
||||
@staticmethod
|
||||
def get_currencies(history: bool) -> Sequence[str]:
|
||||
d = get_exchanges_by_ccy(history)
|
||||
return sorted(d.keys())
|
||||
|
||||
def get_exchanges_by_ccy(self, ccy, h):
|
||||
d = get_exchanges_by_ccy(h)
|
||||
@staticmethod
|
||||
def get_exchanges_by_ccy(ccy: str, history: bool) -> Sequence[str]:
|
||||
d = get_exchanges_by_ccy(history)
|
||||
return d.get(ccy, [])
|
||||
|
||||
@staticmethod
|
||||
def remove_thousands_separator(text):
|
||||
return text.replace(',', '') # FIXME use THOUSAND_SEPARATOR in util
|
||||
|
||||
def ccy_amount_str(self, amount, commas):
|
||||
prec = CCY_PRECISIONS.get(self.ccy, 2)
|
||||
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec))
|
||||
fmt_str = "{:%s.%df}" % ("," if commas else "", max(0, prec)) # FIXME use util.THOUSAND_SEPARATOR and util.DECIMAL_POINT
|
||||
try:
|
||||
rounded_amount = round(amount, prec)
|
||||
except decimal.InvalidOperation:
|
||||
rounded_amount = amount
|
||||
return fmt_str.format(rounded_amount)
|
||||
|
||||
def run(self):
|
||||
# This runs from the plugins thread which catches exceptions
|
||||
if self.is_enabled():
|
||||
if self.timeout ==0 and self.show_history():
|
||||
self.exchange.get_historical_rates(self.ccy, self.cache_dir)
|
||||
if self.timeout <= time.time():
|
||||
self.timeout = time.time() + 150
|
||||
async def run(self):
|
||||
while True:
|
||||
try:
|
||||
await asyncio.wait_for(self._trigger.wait(), 150)
|
||||
except concurrent.futures.TimeoutError:
|
||||
pass
|
||||
else:
|
||||
self._trigger.clear()
|
||||
if self.is_enabled():
|
||||
if self.show_history():
|
||||
self.exchange.get_historical_rates(self.ccy, self.cache_dir)
|
||||
if self.is_enabled():
|
||||
self.exchange.update(self.ccy)
|
||||
|
||||
def is_enabled(self):
|
||||
return bool(self.config.get('use_exchange_rate'))
|
||||
|
||||
def set_enabled(self, b):
|
||||
return self.config.set_key('use_exchange_rate', bool(b))
|
||||
self.config.set_key('use_exchange_rate', bool(b))
|
||||
self.trigger_update()
|
||||
|
||||
def get_history_config(self):
|
||||
return bool(self.config.get('history_rates'))
|
||||
@ -489,7 +528,7 @@ class FxThread(ThreadJob):
|
||||
return self.config.get("currency", "EUR")
|
||||
|
||||
def config_exchange(self):
|
||||
return self.config.get('use_exchange', 'BitcoinAverage')
|
||||
return self.config.get('use_exchange', 'CoinMarketcap')
|
||||
|
||||
def show_history(self):
|
||||
return self.is_enabled() and self.get_history_config() and self.ccy in self.exchange.history_ccys()
|
||||
@ -497,9 +536,13 @@ class FxThread(ThreadJob):
|
||||
def set_currency(self, ccy):
|
||||
self.ccy = ccy
|
||||
self.config.set_key('currency', ccy, True)
|
||||
self.timeout = 0 # Because self.ccy changes
|
||||
self.trigger_update()
|
||||
self.on_quotes()
|
||||
|
||||
def trigger_update(self):
|
||||
if self.network:
|
||||
self.network.asyncio_loop.call_soon_threadsafe(self._trigger.set)
|
||||
|
||||
def set_exchange(self, name):
|
||||
class_ = globals().get(name, BitcoinAverage)
|
||||
self.print_error("using exchange", name)
|
||||
@ -508,7 +551,7 @@ class FxThread(ThreadJob):
|
||||
self.exchange = class_(self.on_quotes, self.on_history)
|
||||
# A new exchange means new fx quotes, initially empty. Force
|
||||
# a quote refresh
|
||||
self.timeout = 0
|
||||
self.trigger_update()
|
||||
self.exchange.read_historical_rates(self.ccy, self.cache_dir)
|
||||
|
||||
def on_quotes(self):
|
||||
@ -519,8 +562,8 @@ class FxThread(ThreadJob):
|
||||
if self.network:
|
||||
self.network.trigger_callback('on_history')
|
||||
|
||||
def exchange_rate(self):
|
||||
'''Returns None, or the exchange rate as a Decimal'''
|
||||
def exchange_rate(self) -> Decimal:
|
||||
"""Returns the exchange rate as a Decimal"""
|
||||
rate = self.exchange.quotes.get(self.ccy)
|
||||
if rate is None:
|
||||
return Decimal('NaN')
|
||||
@ -570,4 +613,4 @@ class FxThread(ThreadJob):
|
||||
def timestamp_rate(self, timestamp):
|
||||
from .util import timestamp_to_datetime
|
||||
date = timestamp_to_datetime(timestamp)
|
||||
return self.history_rate(date)
|
||||
return self.history_rate(date)
|
||||
@ -1,5 +1,5 @@
|
||||
# To create a new GUI, please add its code to this directory.
|
||||
# Three objects are passed to the ElectrumGui: config, daemon and plugins
|
||||
# The Wallet object is instanciated by the GUI
|
||||
# The Wallet object is instantiated by the GUI
|
||||
|
||||
# Notifications about network events are sent to the GUI by using network.register_callback()
|
||||
BIN
electrum/gui/icons/camera_dark.png
Normal file
|
After Width: | Height: | Size: 687 B |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
BIN
electrum/gui/icons/clock1.png
Normal file
|
After Width: | Height: | Size: 7.4 KiB |
BIN
electrum/gui/icons/clock2.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
electrum/gui/icons/clock3.png
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
electrum/gui/icons/clock4.png
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
electrum/gui/icons/clock5.pdn
Normal file
BIN
electrum/gui/icons/clock5.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
BIN
electrum/gui/icons/coldcard.png
Normal file
|
After Width: | Height: | Size: 528 B |
BIN
electrum/gui/icons/coldcard_unpaired.png
Normal file
|
After Width: | Height: | Size: 788 B |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
BIN
electrum/gui/icons/electrum.icns
Normal file
|
After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
BIN
electrum/gui/icons/electrum.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
electrum/gui/icons/electrum_launcher.png
Normal file
|
After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
BIN
electrum/gui/icons/electrum_presplash.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
electrum/gui/icons/electrumb.png
Normal file
|
After Width: | Height: | Size: 144 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |