From d33735faa07d3c292e928b49a004a59cf63255bd Mon Sep 17 00:00:00 2001 From: callebtc <93376500+callebtc@users.noreply.github.com> Date: Mon, 13 Feb 2023 10:53:29 +0100 Subject: [PATCH] init --- Cashu_Chrome_extension.crx | Bin 0 -> 13894 bytes README.md | 11 + __init__.py | 47 + __pycache__/__init__.cpython-39.pyc | Bin 0 -> 1422 bytes __pycache__/crud.cpython-39.pyc | Bin 0 -> 1739 bytes __pycache__/ledger.cpython-39.pyc | Bin 0 -> 154 bytes __pycache__/migrations.cpython-39.pyc | Bin 0 -> 855 bytes __pycache__/mint.cpython-39.pyc | Bin 0 -> 5375 bytes __pycache__/mint_helper.cpython-39.pyc | Bin 0 -> 4348 bytes __pycache__/models.cpython-39.pyc | Bin 0 -> 5191 bytes __pycache__/tasks.cpython-39.pyc | Bin 0 -> 1062 bytes __pycache__/views.cpython-39.pyc | Bin 0 -> 5620 bytes __pycache__/views_api.cpython-39.pyc | Bin 0 -> 10866 bytes config.json | 7 + crud.py | 51 + extension.pem | 28 + extension/128.png | Bin 0 -> 9443 bytes extension/16.png | Bin 0 -> 828 bytes extension/32.png | Bin 0 -> 2020 bytes extension/embedder.html | 24 + extension/manifest.json | 19 + migrations.py | 33 + models.py | 147 ++ static/image/cashu.png | Bin 0 -> 23602 bytes static/js/base64.js | 37 + static/js/dhke.js | 31 + static/js/noble-secp256k1.js | 1178 ++++++++++ static/js/utils.js | 23 + tasks.py | 31 + templates/cashu/_api_docs.html | 80 + templates/cashu/_cashu.html | 28 + templates/cashu/index.html | 367 +++ templates/cashu/mint.html | 92 + templates/cashu/wallet.html | 2992 ++++++++++++++++++++++++ templates/cashu/wallet2.html | 2622 +++++++++++++++++++++ views.py | 253 ++ views_api.py | 440 ++++ 37 files changed, 8541 insertions(+) create mode 100644 Cashu_Chrome_extension.crx create mode 100644 README.md create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-39.pyc create mode 100644 __pycache__/crud.cpython-39.pyc create mode 100644 __pycache__/ledger.cpython-39.pyc create mode 100644 __pycache__/migrations.cpython-39.pyc create mode 100644 __pycache__/mint.cpython-39.pyc create mode 100644 __pycache__/mint_helper.cpython-39.pyc create mode 100644 __pycache__/models.cpython-39.pyc create mode 100644 __pycache__/tasks.cpython-39.pyc create mode 100644 __pycache__/views.cpython-39.pyc create mode 100644 __pycache__/views_api.cpython-39.pyc create mode 100644 config.json create mode 100644 crud.py create mode 100644 extension.pem create mode 100644 extension/128.png create mode 100644 extension/16.png create mode 100644 extension/32.png create mode 100644 extension/embedder.html create mode 100644 extension/manifest.json create mode 100644 migrations.py create mode 100644 models.py create mode 100644 static/image/cashu.png create mode 100644 static/js/base64.js create mode 100644 static/js/dhke.js create mode 100644 static/js/noble-secp256k1.js create mode 100644 static/js/utils.js create mode 100644 tasks.py create mode 100644 templates/cashu/_api_docs.html create mode 100644 templates/cashu/_cashu.html create mode 100644 templates/cashu/index.html create mode 100644 templates/cashu/mint.html create mode 100644 templates/cashu/wallet.html create mode 100644 templates/cashu/wallet2.html create mode 100644 views.py create mode 100644 views_api.py diff --git a/Cashu_Chrome_extension.crx b/Cashu_Chrome_extension.crx new file mode 100644 index 0000000000000000000000000000000000000000..1c3595cf6691eb46c83177596fbd1504d7339898 GIT binary patch literal 13894 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�XJy&eAy(D%M@_FmYu z!A59x%C5;)x9BFvz72*XBWgCJ_IwVZ8M=7&V@g5WfF6Cd89E;bm!!GJYdFt`Y(iR& zINeo-J^A87$MObUjng$D6a^U7?9M5oS>R(rc5kqXFyBGv=qiWh_8*@*+QU@c;lS89 zooBF@$kef;gs#GwoBOJCG0v{soLkQ@7_sumHN1TvS%(2y2!W>Iv(OO}dw}~&^h=T} zpW(o_`Qo*FaUrpyFw?GRpQ9C$ld}VoA)`s;x`d$|Y#E7^s~bA98mshe1rKr-8OpkT4#J5H0a*OBafGOfXe9y^jA*7=OmT1jb0G>M|Bh^!o~2Z2~RSwc&a znh}vOtoR*iIaNG3gp~fbX7nY0S5FtAZ2uG@>@M~U31Lqge)FGmm^-l26L8P>mnbgK zh2)-%%lC!nyR^oF0!Ow*FcrbYa)maMD-;HJDhH{%#tW+oCn~UUo4P8pw{hE%-WgG0 zkCEDbOYQO@8!GN;eDHE5gsYwt*5d9fCHeO6w{b3aE=XtSMdYhlx5ExT3~QYeo1Pqi z;LA6h`3IB4OB>63UeNN$c9NJ}+e^>c4S=>R0x*XF%~p|(TC(O2C+DweO*kREc*k`? zOKg15%uYykM^5Z!7|T8s&O;MJQI-$ZY5#lmCcwh!O8!98gxljon^|B%r1PN*Es>%#ZY~x`#$K*N=$j z&4|55O0J`k3lobS0jFW}U`YZ0F@-!fu%RWc4n{?@C#xlo8f=3PGe54R?7U1(fMcUl z8_GJNLcdJ?03Fzn87QZECI4uiV;=hlfC7q;FTb~eOvAw$ ze=S85{_bkmu7nFfa-Wuf3RI*<>HqV~L8CaJZs~yM9}YUvVz%(24wwA^JM7&$gf_$~ zL8ha7ag0}KQpk-Be@+kyu*6|D-sKGXv5M+{+JEA!4XhYpMln8hE+-6$lZacsZ^XeE6_H8df8#y7`I%jR|n zhHQ^diP=>?W*>TACgu5LeGBFZqy}<$T9<3`f==YfQHSlJi4?kJOfW+QRD*@OK%B%V zlCn%|07XSk9Juy>L>+u^^+u{M;BbvrjUnJ~Ld86kjVdG$sMf7Xxt<$JIlc1HCo;^@ z#M*GU6A38v{G^;4_QoS7NJ>c*|srn!F0)4;2bWqYL2* zQh`Qw*qegN;!BS{U}72M zBFoNVKmM6wPOJ)Vk}}CV#Y^D&1)1;TF8pRzumLw;RJ|F_!<}K)fvU0Y3kS-W#C}Ag zkgUO(&^M~?pMJHu<6Z`&#ny(gyaXZ`3VHiaz${1&3`iR)Rph)xqtiPA0<;a<0Bxwl zI<9!ukB}w#V$M$U`tU>7YovkSBk=w8kbJWt_+4f*qT`TDMh(atvC`^tWQf{HXmq); zZ@Eh-lD3G zn&0<(IQ>gBvQcWPioCJ2#*V){$QHlIU1Yx11E$AGIGvWJG)5j?zsmTh+Vbm8%#Q%J z*5P*C63VoQQrMT@&)%R!;F0FU0m{;D9EL=xO$%b|si@ z18gG;B?PZ+o?ww+c56&HkX#Rq6CB+R7IzVYGGvyT8gSpi>0N#FRSYoQo*r(GL}fI+ zuZ4Xj1e6f`&cs|na2#>78)`CqXV@NnMc1Omk#herd(%6y{*1g*4pu&=AKW4)!}LWF z#twSyBdaynrDnxM7@CJ9OJHOUypv4WOrFWRD+WEoK2{Dz$637CBIjmOTh+*TPAD z(mR9~n zleE7pKD<^wK7i$Dgg<@8CWRVdUhyo%HR+L#lpk5pgc#??-sE5_C`YYd3|WCJC`z*j?n%T zA@w~|nj)aGnm%*({oLM5m?ObGq(jb$WU?0>U6RVfGHNC#Cr8)rtB z`^QwRF$tDDWj{5mI^j1uo|C#0$?}I=APyK^pTLv)+few|LM)1M$gd7->aCrs-sn17 zbPz)Z0(|BS5KndXKU7-ikj9i$b@@vfBAk}>Q#RM+jv+`1Ht$H^EeufQm$c@_9e+ZS zS#s82R{uL*26(?$uR$TSnhNj~1LRnKGaR#ZI$EfU8t@ewFQ9ztq9`%Hb^;J2< zUiBNJM?&`RB5KIx^)nx)EZ1Q_nR1U!nVmaZ!j_#8jNw7|rDiAp6ULKDW3kF#u=FBH%|o zYmkK=ziH|x+eTKB{Ss+hRPvx%*?1C5coG75F~eHZ181QcsRCX*aksKW{13EI{BOs< zAzh7?Wn%PJqFJNHCdvR$hj{|U{LR91A1Sv=ZisJ4QGbb#cLPn~2QF@-0ga(@{tZiy z0Fiaq(?{M!#dCQ}xdg|DIVDMC@If;O&qDbu_ld&@Ft2zh8SD`kjY5kzAn8A<)JbT) zO7LW9MfMRBHHd~fzUCtsNofU1WFr_&nwq@OjbOiDnFrJ{+4aoo+n^BL5~7;FCH3|c z$Vg>zhac@5E-GIW(zFc4x5s!sEO=pmI+j7ZGh-Tr%`x2q0X4g-EKDqpuYVpL#|1y^ zkGsv0usc(%uWLc(%`)lRrm~bCsrxkdPMc>f3!F zP=Q|{fm>kUx2V(}PFGEk2Nu3NWw+&b0H3gFket6gKC1sMj@`l?+p&ha>uU;fhZ)Sh zQfqx#0dco1%>r{hCwGv&5?UQJhnQ>WDWR$hRL@WAtaM)DHAH>x)MOghwOKD@N6#G5 zHMlcxI4Tf$)9?+H#h*$K70}7wfRZ*rZa5Z5AKj2i(f+7+{258LxCtXnMqL0>7)zHG2kubYPF%pgt6c_%fmPj@ysSC zl`9CZvxi6mFOC~xb-mX2bSYbBs)~{drCm$Uj6^5G5KcJkOhtPvR&ihL$~J#avq>2# ztr7pZC#DW$N8o^#5Ed20V=HR-`~~d37P@PctD<(n!yV#ddec1**(aXs`&xy`rLdCq z*~s|>S}KPdFjD^}jU7H)^Nr+cJO%qM?bKKs+hk`u%Wg*aF`Af8)30ji+uV+`x<`Oj zeWzu!X4|ZMX}r2xOKRTXYngEdZ5oxi$5o;7Y3xrNI)lIrn6{~D5apO7f2w$Mgp(1-xYP!+5zj6 zAW(z!v(xvJ@~Z~%LHQ^cd60@f1V?5R^vbq8O8e_hoK*PI$P?#}OvDe^6SRx{c@x3t zffbS@=KcEeV49D$rSb(X1{fWzM3c|l^O~Xcmy~-{OCPnxa`2yx$)j~cBJ~4N$ zIG~pmBTY>F2uM+LQQvaxX;B6-V5!)ED5eEtKN-dtKpTAnSzX?n- z>oXg}t3lR8A(BaI3bt~lp!A{vXybnUs0R92Tz#5IROGhlz7L|w=X|2l9GU)@{>-L(4tq3b(+7_b|Uf#%H zM(?(yuG%lVaOLvJQcyV8Nw(916fnx)%jsHF%IZ0973X@MNBIbTeAn#iIX^o`K0kMS zI!N!zGR?J&khaGYYuzE{jC!!q%Lwg~=)S}n=aoS!l~pKEv_@62kp1-0)7WXwxag)< znNsx`9K4I&e57&aZVLqtvlc`YG8AOWm09;4zj{TKAY zroiK!Wa-wGB!3lg9s38;K;|_=``!JWwkk3 z@Z}a+hI)q*!;dvvaTe8tzvS=O~W}L<0{07CLLp}p4J^R*7 zKc@B3{|Dmp-@usaqpTz!lKDQccmE`) zf3Q&;{|ji;Klmx+|AIylUB4TB6#m!HDA4pj&?qj&>Uce7RU`btN%21}CS}uKWAHu~ z3v0@Tv@z?kOISx^e^#+(|C1qdx2-vLW=un4b|5O!4_7{2G9o#@$Q1ycuK!N7~TFf{)_E=;3MFgyI=gqTT0zrL%SUk&J>rAi zy~I~(sGm$Z{qqcK7t5Fko@FR)ns@>TOj~DGSUbYe(o_7f^maVKqB_2WatvRh-Pd(0 zFN3_`RaBtiOeBP%o%2L-RLPg~BWruNVV<-@QdX4kGAwaZ_u&?J?Tt&`fhAs*we0F~ zPd1|7erR3(y9Va@s5%K)c3LPdz5&CmO2kh2;wccR2Fy2dL3KZt(@)9={W|c#Am980$ z6ie0@U@g_+Y&m{3R@Epv?MpvHCE3>^wXssmq?%%8jy;p24ltuAWL9=@y4fgmm7ikK zbE{b#*}*ZjxLlS5pKzqH=8Qf)S?Zl_XDq&4CA@hDx||LNl+EXz=~k4))5!tYB?)w6 zlV7wq*S*!6yjH1e#0-(71*NVBU5Y$C$!n~-WQSS5Q5Y=tdInstPRXjrq00uh)o&c$ zSEf2$DCWwJ837poj6gzK*fu8GL30(8hLz_^wP50m4*;v0DIgLI1+q_R;WYQ`X$7FS}51Y z68;@%0N|hWkA-4m{a;up;tw_x|Cfbw{`T$Wqwv4BP|%+LSSU@&Qw)qNZe!K1hm ztCCk0-*m3|bVD&yqqGhoTZ;^Z@{Fd#sk-#3lDR>vV^w)M-PDfyF>zsd4Ph~HW?l3P z#PATkJ6|I#%+zC4p_nug+$0V*t3=NyUyI2=-u`IwE4InsY!;7qC!RUy9ocba>s!is zNq3(b6Mb9Nm;AKoZYL@za^6|2i*vM^N|K)L@l1oHUryKYcP15IDR_uJdiSc6Xlh3@JGzA^vEI6!)hG;u9n85)(6T|eop&P92G!lOf^qalW$hqNZ&#QMo&tC=IqczJ z{iAm&nkK=%F+S`Y2%Xpip4gM$lJ|A%z0;=T5IpeuT){Zb2(UA;IAramlfS#=-=(9q z3lPfs6{t}OhEcf;lVe%fF!lGw^3mJ%jRp#l!Ah;$$ZZvD!$OF*rMK8^n$cRU%&R&v zR%i)&o)hVD#!J&+4I3mJha?~s)em{se8YMyX#FEe-wdor8Z;%DESz5}U&{Jp{aMV? z;o%F6y-+OtcUSLz5cZ`ZZhu>6QE&)!dRq%jp#2>(QBdE26bW7zIoQ2KOg4LB;pB{q z6MW>%XdqJ>fKIOi>NDbwqVHc<`OKnnM*md)Md^b^XPhInQcqXsb_>^EtrhXGpcbz8 zie40`sqGc{_Qt|7wG`T7aj`Pm=;8GEM@pv=tCfZ88MOq2=Cj<3f;@H<) z0vGjH!b2<8@NJX29MXi#83~W2$cundLgzD;;kRe?;Y75Hl}L~Nw~NwY)XcK2u$EB^ z*KW8AkxQ<<{rv>VYq5lbq64rNZieTgcv=6CJpH|W_*zoa&kogl7#=##M#YRD%jDSZ zdafSp+EF7mlFaf14nrSnooH#73OwKSvHTVGw)@FkFXpv*an;By$B6nmHF;;hV984g zvLWx7VF^b$-vnwgcemvQks8|1{6U7p+N8%Y3b*e=z7?eF*BX+3VA2;}#p4*}6r(gs zm&r$i_M1am{j@n@X!C2Z%VSYI{KXdZ-D1^;6 z>+mp%fnF5B`k5q^1s8kDl_^KRW5%LSSHi~Hjkezmlknvr7`!+}96 zmRZ61Dk5ox6q3qwV4;9*;3CqMfKV`CDJI24of;^t?}pN_5TD6|YDSC-oanY9&}C@V zUBD^*R)ICsBNtp1lkVVd^xKV)Eogr#=Wh^yWgkjMu3S)b_SQ#>U*EqKA=IOS%9gbJ z&$Mv|#w8~FQ{WfyoTt{^06F!KF#0q-(Ge1CvXFRkC1fq!Q-O+tts#>FnS&>_NLX-Q zt!+w5GMYRK_1p+`&WBpUWKGrPo6}RoqY77r)E$H|xZK}j&kGr+;v?`Uz_KB&`hDNA zk7;$JpesYH#vEVVeR1Ev=XQ5w@~1m}F0_jak%g^`;IWer=GtQmU$TkTLHs$ z-;PEV^>6CStRthwVubIGy+B-)Sy#ym-LQ~dasZ5$_F&D-nY<*{Q4kqU##v=P_jK$L z0p`chcIICo<7T_H+lem|v5h*G#JY&EIw*i{Up@3F>Q_i`UP7?Xz|tL=RNVL1H@-6N z&5g9ZiBM=mhDAINLDF42PZ55ax7#-$&661&PC`ZLfQ`8&;@kyWnD`b>o}VM}pQqKYDQlJ|$OAYEI9lE`d?=9nt!m z;6_*kC+IWtoBoLQNh8DY&FFg7;RB~KJPT%~{XF-H47K|hw*Q~m3k(4D!;?z=n#)}O zahLz!p43+(J1cWDXBS3mXM4N<$&=EH9fu2IMh^a{x7H$|g1GpvKzNa3bpSb3y%oyk zYG&@+LQd-9qnpJ%gvnGPT73gK6@)|telQg9EUE)6*~Ddm8vE+i}YW4Ck^1=9^8M^{7Snlzmm5io zwlESt_pdN*^M>$vlW$yO-s0`woCwnni+6dC z_pb@xYA zDt!q0AwPn+ha6AP@X;IS7o81p;hdxkP>tjBXV0^_5el4*i3v;`ejQ7p!S%liOmG=! z@fsI>Ba&POMldB(E>szR7zQSsF|iP8X=ZU6u^{CTpiQ6*ZHQNfg@K;USM71B$FKqU zS4T3vjEx>cfJk+mrTV>nTNaO%2u-Gzv{fEu)zP6-xOre^g%jj?;if?%Almy@@JJ}y ze|kLm=7OC+fA-z+ixYNw@;%*|%yXe8B2>bht9St&oQfr!sdF(6=F+U{pN;MNIix21 zvT(H!oOYo_8w|BE`@fY}RY+@PHA7N(xf0X(RpFW-(nX^x5*i9E!1o^l&QtibWuyV3 zMfl2&f}E{B_&*%(z}CtV^)*u4pi1%A@%Tau1%QR)C^q9N!>3Y2FqZLX?f(~9#^N+K zjE(YT(W#2P8{~(!I9nycY5?*L1_2#zmSddf)eKn@r=}RZ3?R!iQ-Dg$A?G5C!;&oc zH^ex`y~4K`-+8sl;|OrUXG5>32YQ)?aaMF9VJyK? z!(^7_#Rd%0vd{$jN999awQI#52pFnsWBvt8?Z7e|u^hAnQsxs_&=ymKecZw75m>_` zbpVR`B3;A^$fztBMSU8;kvvw8^|sdDsN&(&UJ+#*EE?4aM*LFMtlwGtGa-R_TGj1h zYrSa8)>YkISf`usYJ+>h%B-|su8l0-BUut#wQUXu>Z3li&ly;Sqm828I$+BNbQ#>g zG_!XMx?rrFW>KsD`3|O{wur?`plqp_?a-od54DnPC3ODdB;#`_zO>`iUw~+m7KV2% g{Tcxr$y@htx85Qi_6flR_Q_k%8#un(gI(YG2ZB3?&Hw-a literal 0 HcmV?d00001 diff --git a/__pycache__/crud.cpython-39.pyc b/__pycache__/crud.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1cb40c0f574ff520414c930723342660cfa57f3 GIT binary patch literal 1739 zcmah}&2Jk;6rVS{>)kj`NJ0>#hiZ^enk^4z2?c%#kjgBWT(A@e|5xd;;gk`DC&BEk1?48lP;F zaQYScLPE#5Yl+e(Sl`vjAd6DtZDnY@0y_(a^6i&kynB(_8HSz-VrzVra5MH)94Mg+ zb1zEM(sX&GBU6z=4`mj+_KNd{xR7y_Us2Q}RGORuhn(azcb-^rP+SxbrIJH8ctKYY zt%n4N_H%L!W$(z%UG?A*Vs;GQK<;8JPV4Z8Dm!nht#)%`ldZNk*ID81VxieW!t1Q` zw9c}qFX~Ju@lzqy&Oquq>&hs$5Y$;e+Eo6a=;MLvCMx2(Ks^c++|ft>q<>YX zC!q%N9b6HDoWQ@>vg@KzG%QmJX52>Of*D=7V89XNAm=shl7!^&3w#G{WCmuQwhrgp z&9&yeO?I0-*jWGkwUmGHu({D>31@fMO6ZjE23?`!ok1pKfLDdI0LwUXuvZ{cs#%

UuN|?yy`hqS-)0lbmQcrgADLJvhSZKc$@ZKs%Pxv*ZOF zfp$+l?&RLnOX(nvQnif6#lo;+s)@oP(@_=+<0vhS`vj}bIH}04CJbK~%;0-?rh#Y1#dN7_ z4qVg`kf}|U6$xrkc7#@Q-$bb>ZaRB0C79f$r5X_*-=(m!g2qcUke%b4X7N-^! z>nA7cr)QSv=j0`2mK5veq^76lrReKH`TD69C8>GEnfZA@g^9%(rTRIkDe0+2dIgoY TIBatBQ%ZAE?LY>924V&POdlob literal 0 HcmV?d00001 diff --git a/__pycache__/migrations.cpython-39.pyc b/__pycache__/migrations.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6c8ed961179f1d1b4f0448d8870897f391afaa48 GIT binary patch literal 855 zcmb_ay-ve05O&fcPz4h^@_+=9pn(~LXiE?kB}FBbfTeOBQ!EmviXBl9BP;I!BLfeC zcm|fPJOUdFoD(P%MNFJ@=ll8Yd}kk-nJFP4^ZAB8nh3pU<0pX+Z34?_5I*vaYjli! z^8i@|YJlsDv|a#y0+#b2uFx%@?~FR2caV|Ug>O5pQM{ZhVW7I;ijx3SB2N>nNH<{R zG(cP^oN5|YYdICq!Cs}>a58K;%UB#iG>rS$a}GVc->UCbT1R--Ia(S6oRc76Dg(Jq z54-I~V;mt!$o`5ePS02@Powy=%$BoVX*WE)Ac>L-oMWm^b)6wj*3OYd@s#v8Bc)MwT$#kZ~h^Q5F1Xd OhmoHIY(wY20;3N_qru<+ literal 0 HcmV?d00001 diff --git a/__pycache__/mint.cpython-39.pyc b/__pycache__/mint.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..98b7973945c6fe23b08c71db31c46fa43bba9f7e GIT binary patch literal 5375 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 literal 0 HcmV?d00001 diff --git a/__pycache__/views_api.cpython-39.pyc b/__pycache__/views_api.cpython-39.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff37aa0c7873e02617c8c28717b8fcb3f01468df GIT binary patch literal 10866 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 Cashu: + + await db.execute( + """ + INSERT INTO cashu.cashu (id, wallet, name, tickershort, fraction, maxsats, coins, keyset_id) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + cashu_id, + wallet_id, + data.name, + data.tickershort, + data.fraction, + data.maxsats, + data.coins, + keyset_id, + ), + ) + + cashu = await get_cashu(cashu_id) + assert cashu, "Newly created cashu couldn't be retrieved" + return cashu + + +async def get_cashu(cashu_id) -> Optional[Cashu]: + row = await db.fetchone("SELECT * FROM cashu.cashu WHERE id = ?", (cashu_id,)) + return Cashu(**row) if row else None + + +async def get_cashus(wallet_ids: Union[str, List[str]]) -> List[Cashu]: + if isinstance(wallet_ids, str): + wallet_ids = [wallet_ids] + + q = ",".join(["?"] * len(wallet_ids)) + rows = await db.fetchall( + f"SELECT * FROM cashu.cashu WHERE wallet IN ({q})", (*wallet_ids,) + ) + + return [Cashu(**row) for row in rows] + + +async def delete_cashu(cashu_id) -> None: + await db.execute("DELETE FROM cashu.cashu WHERE id = ?", (cashu_id,)) diff --git a/extension.pem b/extension.pem new file mode 100644 index 000000000..13b6d6e95 --- /dev/null +++ b/extension.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCqdYhs1zok/3Es +uSycCn0YKGXaXO9Vbhb1Aol1vzYc0UkQseDrGCiEF/0D5Vol6n3aqsZabKfQC46e +ylEs+HWUYStsDApkSsyrlT+38nNYFtbCGNzf3lFqJxaOKrelCmhiYc3vzJc4yHpp +OaWtuWDlx3bWnJSMi8VF6UkqAtZrlLTrx45Wd/X2lPh5V3hHbwQy+BJ0kGOJuNWO +iMg4PUtaCVa8QQQSSdbXzLeMnkDXJRgGr6tEaFbZWCmQBOYIrmYcXTa4xSg+SaLa +fBqGGY5CFa2mnGarhQ9I6/Or7KES4CgyIewuJSC4VZQ9WNFTeWRJmwF59LQIB1B+ +Su61Ds5vAgMBAAECggEAUTUJN4EZzEJ9smbNn9FHY1jrnSolQzfdbAA3aIcdyoDS +JEf59UZfFv8upMqBNgsOxjwRn0agc/btjSypm5kKuxPH0gOyd7FC7IOABaxV0yOe +UH3zzBtoAgB5OGWVc+UPcb9SkumPKZgo6TRzf94PEakDLp8ODh8QbowDNSaA7GL2 +FXvSgcxtLFv24/wMj7MPtUJDPPzhzRafe097RAJKi9ig33GpiohVg7HWxBEt/m2q +WPJqV1izIYVuyUV1NN2qV/7KP35vqGu3sMyuKjmz33+7G5OUzxoixFp1hx1iQwNM +QmzrNjKgGKPOvI169K73LtT6GfcAcVyvudPioJi2oQKBgQDlvEg79bXuV72OMkDI +LINTk0Pncs5fJMhsPrbhvAbi8C9qH3j/e3uh4/f/RZVy8PZFe2YsmJo6cE60t445 +oejGpj0L9gB+ptWQa+6tJ0vO2DFrGz7siBDQDK08bBfE0FRAYYH1oy81oZihAwh/ +ypUWgxAAkFIDG1b8IYozUwlgaQKBgQC98mWG1ypNFDKU5ffFmxzCermXhe4sIOny +E9Na51lMuzp1HVOHqYio27KGWB9NhFnhCRdzq2csJrdL5GqNBzF3mVc7Wp1QA9Iz +GAN1BWYsObtffmePI73UOtdOf3vWRWt9xFDw07CuPUJoZLxCO8DE2R8TfBEZpMVx +L99xZ4ldFwKBgCCCZk5l1Nrne6F1pwksk2DuQ/uqmV2JXPtEXKjQ7MCMcIk57uto +U/RyyBNNi3p6cr8eWTUSWzDN17VnDC9HpCMIPzdEFonVnf8T5vCF0QpyOXwInPzn +vElUoecc/LgTKvj5U2SpNPMH7QtL3JYI2tQWs9sAkTpzUfnX3TCqHXeRAoGAGeEJ +phpwn/nNfm3qmRiuDW0tFhDgR1DgruO2mx8EEqk7qLMrK0OU4/Sy2QzujuAmYrub +X14N+3z24jcnsTk2qyXQNbz9y+YJYH64Edx/dC0Vc0hEkcL73Qt7RC2oUKQ6pfL2 +WClvW/3qq1B2OcvsUqWGT2BK8CR75hTwq4HAvFcCgYB0Cuv/cHo0iLzvCX2LwIiq +N38udO9oYa2VV/Ny4lf/oWFyLuL2syNJnW6fhqZk3ACVbIxQ4t85IJ+kKSKgSBXV +9p3kAo93/sP+Qd3yWK/2kc8fwQXPQWhEynmS0c2LmfmA6h+JlPUG+HHhXwEMcplc +iuvTCsY3KzPtBZgm8hFIKg== +-----END PRIVATE KEY----- diff --git a/extension/128.png b/extension/128.png new file mode 100644 index 0000000000000000000000000000000000000000..35c4fdf0ee6dfcab7d4e4e3da2a574385cbe457c GIT binary patch literal 9443 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 VEKDO8UWG{j1sPT83Q6OD{{cX-`nUi9 literal 0 HcmV?d00001 diff --git a/extension/16.png b/extension/16.png new file mode 100644 index 0000000000000000000000000000000000000000..3bb890c50b8295ffe86e412555b7d3016a37a528 GIT binary patch literal 828 zcmV-C1H=4@P)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D0?|oCK~y+Tt&m+X zRAC&(&qdR8W2n(>jT<$kL0C(Lx-eZdMrw>0HB)Ld)0#r|<5JXwv=US7hLcj5yyEV4khUyrml7xJr!ox1%7+f_#yKXhq{h7Gg>g zv~^?w4j($i<~Ee5iL1K+-VNzgqB zEN(i4LtB0xOf6E3ci&s9@q>E|Q&R^DV@5#-bD%>RR3zJ>im<}t2>M!umE2GpnrIr9 z=L*<6Z(^qZ0j7JauryYW`fEdce-poeOkXu72V1c;JBk;@EOG)Y&_p_+6te?@?)d{YT^BJ&$`k9*UnhaNSqtM+ z6^7e%nChv*oT&!Cr`}=7rAM=(7uv8H;&B9>qEumwmr}$nt0QfsRm-Pnldq*PHs)ib zy#y{p1)QCwm}SZ^tbb3W`?+P0Bk1cWMNFJVJVAPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2YE?EK~z{r?N@71 zROcBsKkARBX{P;CewaAcm|#R?VVAu?Y-gHIshYOw(6N}%rq#x2>r_olT$a7F%SAy9 zS3yKYFkBSy4v2+a_I_a(7MAN0K#i9|8k06NX_9&RerL~uJH(7;@-xrOGiUks?0KH= zy`7I`-{MiAllBX>l76{X+J9Lq_0Fm#p8qjwrJh-~rjz>j8)Sn?{O#Wy|N3}4&K;)~jaq6l zJ8^9LDOA7SfM#{y61E!5D0|fox*uwZ3x<>-6udCWjPzv*t?T&5JsK23*A_#H|D(cR zcyY0K4@@W0;b_@~yPDKbU4lYvERy>*TPW*23AGgQ=dXrMy;&>)v_i1 z%)m|mNsL;e4XDIUC=wmW5;$O#jxjS9Ugpc86uVww#iI}eP`Tp}x~enCKn`rD^%%NX zj9D@;-CK=m6Omnc&X|v4+R+48YY!Q4lL03uu1$5JV6ztoUg}4Fniu&g6HrQ=8NA$5 zsKkzdF=dzx45DIt5odtq-S(~?!_we|4zYn^FKwQ?^gWaS-dIifKKY;43z(IfZP3@TXQ(EcbL}f+B~T{q-GmR{b97X)=i8W3iqhk@3<-c-o5afu|mG?o-JA`&P)36Y$)Mc*Oqjr^phU zsN#pX%4c=J3h+YlIOgZ)kw^1Hk?5r81X&uIdD%s%#IB%T=I0EsB4jmit>zVYTXSKh zE*QRi77GSiqG-?7eGokKs ze0ZY?on^gvV~q#dLKm`=NSq3e?KqoED0%US0j9;vEKDZz}1Ml(wV8%(AnxH-^*Pd~YZ&pw;R z?640P3VXS_P*6%(I@q&04Y70SSwSPVt!ClT&>+fA@}nrd9*r6~dXDd-F3^yP9L@ww zg}GkCnHcLTTSy11g=x!i{C9pD|Cyh_y_-Flay6rFkA+JKs|ogOmS#<=YZWgW4I0Ic za2i+?hG6)47upKb>5V9--5a<_L>Q@wN!mS^3TA>jWAbVVv6NVb_smuJ;O05h?6hM4 z`bhYlHePNiR1(*g{M2zSSJ{bvKzuEf*$UGqDppb;^n>^N{QnV%abzM4c*2*6mPA^`9puB9q*kUn6w>h?n_lC?qx!JIk>XE*q`; z*N>s()k<6{%ixY?Y%;Qfd=Qx_)5+a9o3oov%Ry*GLm#T7Rw4iRhl5;fPNs^T4Y!Sl zE6l=|&){-pIxdx`b7zI#<9|SJZRQeM8h(d&2 z*C?HUF4cj<8MPcsww*wEW*r^JqgjzzkK^0VpyW4Zs03rpdjwZv_{V*H=%nsV6pb#e z#P@|U`37=CGteYWK$S>sMP#4#qG=fDESa4c{6Y~w(IsCy`Ah!i-yAez-%9$AXPZvy zE!Rpsy&8#Ugppn_J!GzoeU~E{ewx4iTRa4oE&D&4m$L7DiiU0g0000 + + + + + + + + diff --git a/extension/manifest.json b/extension/manifest.json new file mode 100644 index 000000000..a4fa9a41e --- /dev/null +++ b/extension/manifest.json @@ -0,0 +1,19 @@ +{ + "description": "Cashu ecash wallet", + "icons": {"128": "128.png", "32": "32.png", "16": "16.png"}, + "manifest_version": 3, + "name": "Cashu", + "version": "0.1", + "action": { + "default_icon": "32.png", + "default_popup": "embedder.html", + "default_title": "Cashu" + }, + "permissions": [ + "unlimitedStorage", + "storage" + ], + "content_security_policy": { + "extension_pages": "default-src 'self'" + } +} \ No newline at end of file diff --git a/migrations.py b/migrations.py new file mode 100644 index 000000000..b54c41087 --- /dev/null +++ b/migrations.py @@ -0,0 +1,33 @@ +async def m001_initial(db): + """ + Initial cashu table. + """ + await db.execute( + """ + CREATE TABLE cashu.cashu ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + name TEXT NOT NULL, + tickershort TEXT DEFAULT 'sats', + fraction BOOL, + maxsats INT, + coins INT, + keyset_id TEXT NOT NULL, + issued_sat INT + ); + """ + ) + + """ + Initial cashus table. + """ + await db.execute( + """ + CREATE TABLE cashu.pegs ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + inout BOOL NOT NULL, + amount INT + ); + """ + ) diff --git a/models.py b/models.py new file mode 100644 index 000000000..84f28c2bc --- /dev/null +++ b/models.py @@ -0,0 +1,147 @@ +from sqlite3 import Row +from typing import List + +from fastapi import Query +from pydantic import BaseModel + + +class Cashu(BaseModel): + id: str = Query(None) + name: str = Query(None) + wallet: str = Query(None) + tickershort: str = Query(None) + fraction: bool = Query(None) + maxsats: int = Query(0) + coins: int = Query(0) + keyset_id: str = Query(None) + + @classmethod + def from_row(cls, row: Row): + return cls(**dict(row)) + + +class Pegs(BaseModel): + id: str + wallet: str + inout: str + amount: str + + @classmethod + def from_row(cls, row: Row): + return cls(**dict(row)) + + +class PayLnurlWData(BaseModel): + lnurl: str + + +class Promises(BaseModel): + id: str + amount: int + B_b: str + C_b: str + cashu_id: str + + +class Proof(BaseModel): + amount: int + secret: str + C: str + reserved: bool = False # whether this proof is reserved for sending + send_id: str = "" # unique ID of send attempt + time_created: str = "" + time_reserved: str = "" + + @classmethod + def from_row(cls, row: Row): + return cls( + amount=row[0], + C=row[1], + secret=row[2], + reserved=row[3] or False, + send_id=row[4] or "", + time_created=row[5] or "", + time_reserved=row[6] or "", + ) + + @classmethod + def from_dict(cls, d: dict): + assert "secret" in d, "no secret in proof" + assert "amount" in d, "no amount in proof" + return cls( + amount=d.get("amount"), + C=d.get("C"), + secret=d.get("secret"), + reserved=d.get("reserved") or False, + send_id=d.get("send_id") or "", + time_created=d.get("time_created") or "", + time_reserved=d.get("time_reserved") or "", + ) + + def to_dict(self): + return dict(amount=self.amount, secret=self.secret, C=self.C) + + def __getitem__(self, key): + return self.__getattribute__(key) + + def __setitem__(self, key, val): + self.__setattr__(key, val) + + +class Proofs(BaseModel): + """TODO: Use this model""" + + proofs: List[Proof] + + +class Invoice(BaseModel): + amount: int + pr: str + hash: str + issued: bool = False + + @classmethod + def from_row(cls, row: Row): + return cls( + amount=int(row[0]), + pr=str(row[1]), + hash=str(row[2]), + issued=bool(row[3]), + ) + + +class BlindedMessage(BaseModel): + amount: int + B_: str + + +class BlindedSignature(BaseModel): + amount: int + C_: str + + @classmethod + def from_dict(cls, d: dict): + return cls( + amount=d["amount"], + C_=d["C_"], + ) + + +class MintPayloads(BaseModel): + blinded_messages: List[BlindedMessage] = [] + + +class SplitPayload(BaseModel): + proofs: List[Proof] + amount: int + output_data: MintPayloads + + +class CheckPayload(BaseModel): + proofs: List[Proof] + + +class MeltPayload(BaseModel): + proofs: List[Proof] + amount: int + invoice: str diff --git a/static/image/cashu.png b/static/image/cashu.png new file mode 100644 index 0000000000000000000000000000000000000000..e90611fc061c20d0c1c14bd2317c745181c4a96c GIT binary patch literal 23602 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 literal 0 HcmV?d00001 diff --git a/static/js/base64.js b/static/js/base64.js new file mode 100644 index 000000000..b150882f7 --- /dev/null +++ b/static/js/base64.js @@ -0,0 +1,37 @@ +function unescapeBase64Url(str) { + return (str + '==='.slice((str.length + 3) % 4)) + .replace(/-/g, '+') + .replace(/_/g, '/') +} + +function escapeBase64Url(str) { + return str.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '') +} + +const uint8ToBase64 = (function (exports) { + 'use strict' + + var fromCharCode = String.fromCharCode + var encode = function encode(uint8array) { + var output = [] + + for (var i = 0, length = uint8array.length; i < length; i++) { + output.push(fromCharCode(uint8array[i])) + } + + return btoa(output.join('')) + } + + var asCharCode = function asCharCode(c) { + return c.charCodeAt(0) + } + + var decode = function decode(chars) { + return Uint8Array.from(atob(chars), asCharCode) + } + + exports.decode = decode + exports.encode = encode + + return exports +})({}) diff --git a/static/js/dhke.js b/static/js/dhke.js new file mode 100644 index 000000000..cebef240c --- /dev/null +++ b/static/js/dhke.js @@ -0,0 +1,31 @@ +async function hashToCurve(secretMessage) { + let point + while (!point) { + const hash = await nobleSecp256k1.utils.sha256(secretMessage) + const hashHex = nobleSecp256k1.utils.bytesToHex(hash) + const pointX = '02' + hashHex + try { + point = nobleSecp256k1.Point.fromHex(pointX) + } catch (error) { + secretMessage = await nobleSecp256k1.utils.sha256(secretMessage) + } + } + return point +} + +async function step1Alice(secretMessage) { + secretMessage = uint8ToBase64.encode(secretMessage) + secretMessage = new TextEncoder().encode(secretMessage) + const Y = await hashToCurve(secretMessage) + const r_bytes = nobleSecp256k1.utils.randomPrivateKey() + const r = bytesToNumber(r_bytes) + const P = nobleSecp256k1.Point.fromPrivateKey(r) + const B_ = Y.add(P) + return {B_: B_.toHex(true), r: nobleSecp256k1.utils.bytesToHex(r_bytes)} +} + +function step3Alice(C_, r, A) { + const rInt = bytesToNumber(r) + const C = C_.subtract(A.multiply(rInt)) + return C +} diff --git a/static/js/noble-secp256k1.js b/static/js/noble-secp256k1.js new file mode 100644 index 000000000..6a6bd4417 --- /dev/null +++ b/static/js/noble-secp256k1.js @@ -0,0 +1,1178 @@ +;(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' + ? factory(exports) + : typeof define === 'function' && define.amd + ? define(['exports'], factory) + : ((global = + typeof globalThis !== 'undefined' ? globalThis : global || self), + factory((global.nobleSecp256k1 = {}))) +})(this, function (exports) { + 'use strict' + + const _nodeResolve_empty = {} + + const nodeCrypto = /*#__PURE__*/ Object.freeze({ + __proto__: null, + default: _nodeResolve_empty + }) + + /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ + const _0n = BigInt(0) + const _1n = BigInt(1) + const _2n = BigInt(2) + const _3n = BigInt(3) + const _8n = BigInt(8) + const POW_2_256 = _2n ** BigInt(256) + const CURVE = { + a: _0n, + b: BigInt(7), + P: POW_2_256 - _2n ** BigInt(32) - BigInt(977), + n: POW_2_256 - BigInt('432420386565659656852420866394968145599'), + h: _1n, + Gx: BigInt( + '55066263022277343669578718895168534326250603453777594175500187360389116729240' + ), + Gy: BigInt( + '32670510020758816978083085130507043184471273380659243275938904335757337482424' + ), + beta: BigInt( + '0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee' + ) + } + function weistrass(x) { + const {a, b} = CURVE + const x2 = mod(x * x) + const x3 = mod(x2 * x) + return mod(x3 + a * x + b) + } + const USE_ENDOMORPHISM = CURVE.a === _0n + class JacobianPoint { + constructor(x, y, z) { + this.x = x + this.y = y + this.z = z + } + static fromAffine(p) { + if (!(p instanceof Point)) { + throw new TypeError('JacobianPoint#fromAffine: expected Point') + } + return new JacobianPoint(p.x, p.y, _1n) + } + static toAffineBatch(points) { + const toInv = invertBatch(points.map(p => p.z)) + return points.map((p, i) => p.toAffine(toInv[i])) + } + static normalizeZ(points) { + return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine) + } + equals(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected') + const {x: X1, y: Y1, z: Z1} = this + const {x: X2, y: Y2, z: Z2} = other + const Z1Z1 = mod(Z1 ** _2n) + const Z2Z2 = mod(Z2 ** _2n) + const U1 = mod(X1 * Z2Z2) + const U2 = mod(X2 * Z1Z1) + const S1 = mod(mod(Y1 * Z2) * Z2Z2) + const S2 = mod(mod(Y2 * Z1) * Z1Z1) + return U1 === U2 && S1 === S2 + } + negate() { + return new JacobianPoint(this.x, mod(-this.y), this.z) + } + double() { + const {x: X1, y: Y1, z: Z1} = this + const A = mod(X1 ** _2n) + const B = mod(Y1 ** _2n) + const C = mod(B ** _2n) + const D = mod(_2n * (mod((X1 + B) ** _2n) - A - C)) + const E = mod(_3n * A) + const F = mod(E ** _2n) + const X3 = mod(F - _2n * D) + const Y3 = mod(E * (D - X3) - _8n * C) + const Z3 = mod(_2n * Y1 * Z1) + return new JacobianPoint(X3, Y3, Z3) + } + add(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected') + const {x: X1, y: Y1, z: Z1} = this + const {x: X2, y: Y2, z: Z2} = other + if (X2 === _0n || Y2 === _0n) return this + if (X1 === _0n || Y1 === _0n) return other + const Z1Z1 = mod(Z1 ** _2n) + const Z2Z2 = mod(Z2 ** _2n) + const U1 = mod(X1 * Z2Z2) + const U2 = mod(X2 * Z1Z1) + const S1 = mod(mod(Y1 * Z2) * Z2Z2) + const S2 = mod(mod(Y2 * Z1) * Z1Z1) + const H = mod(U2 - U1) + const r = mod(S2 - S1) + if (H === _0n) { + if (r === _0n) { + return this.double() + } else { + return JacobianPoint.ZERO + } + } + const HH = mod(H ** _2n) + const HHH = mod(H * HH) + const V = mod(U1 * HH) + const X3 = mod(r ** _2n - HHH - _2n * V) + const Y3 = mod(r * (V - X3) - S1 * HHH) + const Z3 = mod(Z1 * Z2 * H) + return new JacobianPoint(X3, Y3, Z3) + } + subtract(other) { + return this.add(other.negate()) + } + multiplyUnsafe(scalar) { + const P0 = JacobianPoint.ZERO + if (typeof scalar === 'bigint' && scalar === _0n) return P0 + let n = normalizeScalar(scalar) + if (n === _1n) return this + if (!USE_ENDOMORPHISM) { + let p = P0 + let d = this + while (n > _0n) { + if (n & _1n) p = p.add(d) + d = d.double() + n >>= _1n + } + return p + } + let {k1neg, k1, k2neg, k2} = splitScalarEndo(n) + let k1p = P0 + let k2p = P0 + let d = this + while (k1 > _0n || k2 > _0n) { + if (k1 & _1n) k1p = k1p.add(d) + if (k2 & _1n) k2p = k2p.add(d) + d = d.double() + k1 >>= _1n + k2 >>= _1n + } + if (k1neg) k1p = k1p.negate() + if (k2neg) k2p = k2p.negate() + k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z) + return k1p.add(k2p) + } + precomputeWindow(W) { + const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1 + const points = [] + let p = this + let base = p + for (let window = 0; window < windows; window++) { + base = p + points.push(base) + for (let i = 1; i < 2 ** (W - 1); i++) { + base = base.add(p) + points.push(base) + } + p = base.double() + } + return points + } + wNAF(n, affinePoint) { + if (!affinePoint && this.equals(JacobianPoint.BASE)) + affinePoint = Point.BASE + const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1 + if (256 % W) { + throw new Error( + 'Point#wNAF: Invalid precomputation window, must be power of 2' + ) + } + let precomputes = affinePoint && pointPrecomputes.get(affinePoint) + if (!precomputes) { + precomputes = this.precomputeWindow(W) + if (affinePoint && W !== 1) { + precomputes = JacobianPoint.normalizeZ(precomputes) + pointPrecomputes.set(affinePoint, precomputes) + } + } + let p = JacobianPoint.ZERO + let f = JacobianPoint.ZERO + const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W) + const windowSize = 2 ** (W - 1) + const mask = BigInt(2 ** W - 1) + const maxNumber = 2 ** W + const shiftBy = BigInt(W) + for (let window = 0; window < windows; window++) { + const offset = window * windowSize + let wbits = Number(n & mask) + n >>= shiftBy + if (wbits > windowSize) { + wbits -= maxNumber + n += _1n + } + if (wbits === 0) { + let pr = precomputes[offset] + if (window % 2) pr = pr.negate() + f = f.add(pr) + } else { + let cached = precomputes[offset + Math.abs(wbits) - 1] + if (wbits < 0) cached = cached.negate() + p = p.add(cached) + } + } + return {p, f} + } + multiply(scalar, affinePoint) { + let n = normalizeScalar(scalar) + let point + let fake + if (USE_ENDOMORPHISM) { + const {k1neg, k1, k2neg, k2} = splitScalarEndo(n) + let {p: k1p, f: f1p} = this.wNAF(k1, affinePoint) + let {p: k2p, f: f2p} = this.wNAF(k2, affinePoint) + if (k1neg) k1p = k1p.negate() + if (k2neg) k2p = k2p.negate() + k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z) + point = k1p.add(k2p) + fake = f1p.add(f2p) + } else { + const {p, f} = this.wNAF(n, affinePoint) + point = p + fake = f + } + return JacobianPoint.normalizeZ([point, fake])[0] + } + toAffine(invZ = invert(this.z)) { + const {x, y, z} = this + const iz1 = invZ + const iz2 = mod(iz1 * iz1) + const iz3 = mod(iz2 * iz1) + const ax = mod(x * iz2) + const ay = mod(y * iz3) + const zz = mod(z * iz1) + if (zz !== _1n) throw new Error('invZ was invalid') + return new Point(ax, ay) + } + } + JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n) + JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n) + const pointPrecomputes = new WeakMap() + class Point { + constructor(x, y) { + this.x = x + this.y = y + } + _setWindowSize(windowSize) { + this._WINDOW_SIZE = windowSize + pointPrecomputes.delete(this) + } + static fromCompressedHex(bytes) { + const isShort = bytes.length === 32 + const x = bytesToNumber(isShort ? bytes : bytes.subarray(1)) + if (!isValidFieldElement(x)) throw new Error('Point is not on curve') + const y2 = weistrass(x) + let y = sqrtMod(y2) + const isYOdd = (y & _1n) === _1n + if (isShort) { + if (isYOdd) y = mod(-y) + } else { + const isFirstByteOdd = (bytes[0] & 1) === 1 + if (isFirstByteOdd !== isYOdd) y = mod(-y) + } + const point = new Point(x, y) + point.assertValidity() + return point + } + static fromUncompressedHex(bytes) { + const x = bytesToNumber(bytes.subarray(1, 33)) + const y = bytesToNumber(bytes.subarray(33, 65)) + const point = new Point(x, y) + point.assertValidity() + return point + } + static fromHex(hex) { + const bytes = ensureBytes(hex) + const len = bytes.length + const header = bytes[0] + if (len === 32 || (len === 33 && (header === 0x02 || header === 0x03))) { + return this.fromCompressedHex(bytes) + } + if (len === 65 && header === 0x04) return this.fromUncompressedHex(bytes) + throw new Error( + `Point.fromHex: received invalid point. Expected 32-33 compressed bytes or 65 uncompressed bytes, not ${len}` + ) + } + static fromPrivateKey(privateKey) { + return Point.BASE.multiply(normalizePrivateKey(privateKey)) + } + static fromSignature(msgHash, signature, recovery) { + msgHash = ensureBytes(msgHash) + const h = truncateHash(msgHash) + const {r, s} = normalizeSignature(signature) + if (recovery !== 0 && recovery !== 1) { + throw new Error('Cannot recover signature: invalid recovery bit') + } + const prefix = recovery & 1 ? '03' : '02' + const R = Point.fromHex(prefix + numTo32bStr(r)) + const {n} = CURVE + const rinv = invert(r, n) + const u1 = mod(-h * rinv, n) + const u2 = mod(s * rinv, n) + const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2) + if (!Q) throw new Error('Cannot recover signature: point at infinify') + Q.assertValidity() + return Q + } + toRawBytes(isCompressed = false) { + return hexToBytes(this.toHex(isCompressed)) + } + toHex(isCompressed = false) { + const x = numTo32bStr(this.x) + if (isCompressed) { + const prefix = this.y & _1n ? '03' : '02' + return `${prefix}${x}` + } else { + return `04${x}${numTo32bStr(this.y)}` + } + } + toHexX() { + return this.toHex(true).slice(2) + } + toRawX() { + return this.toRawBytes(true).slice(1) + } + assertValidity() { + const msg = 'Point is not on elliptic curve' + const {x, y} = this + if (!isValidFieldElement(x) || !isValidFieldElement(y)) + throw new Error(msg) + const left = mod(y * y) + const right = weistrass(x) + if (mod(left - right) !== _0n) throw new Error(msg) + } + equals(other) { + return this.x === other.x && this.y === other.y + } + negate() { + return new Point(this.x, mod(-this.y)) + } + double() { + return JacobianPoint.fromAffine(this).double().toAffine() + } + add(other) { + return JacobianPoint.fromAffine(this) + .add(JacobianPoint.fromAffine(other)) + .toAffine() + } + subtract(other) { + return this.add(other.negate()) + } + multiply(scalar) { + return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine() + } + multiplyAndAddUnsafe(Q, a, b) { + const P = JacobianPoint.fromAffine(this) + const aP = + a === _0n || a === _1n || this !== Point.BASE + ? P.multiplyUnsafe(a) + : P.multiply(a) + const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b) + const sum = aP.add(bQ) + return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine() + } + } + Point.BASE = new Point(CURVE.Gx, CURVE.Gy) + Point.ZERO = new Point(_0n, _0n) + function sliceDER(s) { + return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s + } + function parseDERInt(data) { + if (data.length < 2 || data[0] !== 0x02) { + throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`) + } + const len = data[1] + const res = data.subarray(2, len + 2) + if (!len || res.length !== len) { + throw new Error(`Invalid signature integer: wrong length`) + } + if (res[0] === 0x00 && res[1] <= 0x7f) { + throw new Error('Invalid signature integer: trailing length') + } + return {data: bytesToNumber(res), left: data.subarray(len + 2)} + } + function parseDERSignature(data) { + if (data.length < 2 || data[0] != 0x30) { + throw new Error(`Invalid signature tag: ${bytesToHex(data)}`) + } + if (data[1] !== data.length - 2) { + throw new Error('Invalid signature: incorrect length') + } + const {data: r, left: sBytes} = parseDERInt(data.subarray(2)) + const {data: s, left: rBytesLeft} = parseDERInt(sBytes) + if (rBytesLeft.length) { + throw new Error( + `Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}` + ) + } + return {r, s} + } + class Signature { + constructor(r, s) { + this.r = r + this.s = s + this.assertValidity() + } + static fromCompact(hex) { + const arr = isUint8a(hex) + const name = 'Signature.fromCompact' + if (typeof hex !== 'string' && !arr) + throw new TypeError(`${name}: Expected string or Uint8Array`) + const str = arr ? bytesToHex(hex) : hex + if (str.length !== 128) throw new Error(`${name}: Expected 64-byte hex`) + return new Signature( + hexToNumber(str.slice(0, 64)), + hexToNumber(str.slice(64, 128)) + ) + } + static fromDER(hex) { + const arr = isUint8a(hex) + if (typeof hex !== 'string' && !arr) + throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`) + const {r, s} = parseDERSignature(arr ? hex : hexToBytes(hex)) + return new Signature(r, s) + } + static fromHex(hex) { + return this.fromDER(hex) + } + assertValidity() { + const {r, s} = this + if (!isWithinCurveOrder(r)) + throw new Error('Invalid Signature: r must be 0 < r < n') + if (!isWithinCurveOrder(s)) + throw new Error('Invalid Signature: s must be 0 < s < n') + } + hasHighS() { + const HALF = CURVE.n >> _1n + return this.s > HALF + } + normalizeS() { + return this.hasHighS() ? new Signature(this.r, CURVE.n - this.s) : this + } + toDERRawBytes(isCompressed = false) { + return hexToBytes(this.toDERHex(isCompressed)) + } + toDERHex(isCompressed = false) { + const sHex = sliceDER(numberToHexUnpadded(this.s)) + if (isCompressed) return sHex + const rHex = sliceDER(numberToHexUnpadded(this.r)) + const rLen = numberToHexUnpadded(rHex.length / 2) + const sLen = numberToHexUnpadded(sHex.length / 2) + const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4) + return `30${length}02${rLen}${rHex}02${sLen}${sHex}` + } + toRawBytes() { + return this.toDERRawBytes() + } + toHex() { + return this.toDERHex() + } + toCompactRawBytes() { + return hexToBytes(this.toCompactHex()) + } + toCompactHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s) + } + } + function concatBytes(...arrays) { + if (!arrays.every(isUint8a)) throw new Error('Uint8Array list expected') + if (arrays.length === 1) return arrays[0] + const length = arrays.reduce((a, arr) => a + arr.length, 0) + const result = new Uint8Array(length) + for (let i = 0, pad = 0; i < arrays.length; i++) { + const arr = arrays[i] + result.set(arr, pad) + pad += arr.length + } + return result + } + function isUint8a(bytes) { + return bytes instanceof Uint8Array + } + const hexes = Array.from({length: 256}, (v, i) => + i.toString(16).padStart(2, '0') + ) + function bytesToHex(uint8a) { + if (!(uint8a instanceof Uint8Array)) throw new Error('Expected Uint8Array') + let hex = '' + for (let i = 0; i < uint8a.length; i++) { + hex += hexes[uint8a[i]] + } + return hex + } + function numTo32bStr(num) { + if (num > POW_2_256) throw new Error('Expected number < 2^256') + return num.toString(16).padStart(64, '0') + } + function numTo32b(num) { + return hexToBytes(numTo32bStr(num)) + } + function numberToHexUnpadded(num) { + const hex = num.toString(16) + return hex.length & 1 ? `0${hex}` : hex + } + function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToNumber: expected string, got ' + typeof hex) + } + return BigInt(`0x${hex}`) + } + function hexToBytes(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToBytes: expected string, got ' + typeof hex) + } + if (hex.length % 2) + throw new Error('hexToBytes: received invalid unpadded hex' + hex.length) + const array = new Uint8Array(hex.length / 2) + for (let i = 0; i < array.length; i++) { + const j = i * 2 + const hexByte = hex.slice(j, j + 2) + const byte = Number.parseInt(hexByte, 16) + if (Number.isNaN(byte) || byte < 0) + throw new Error('Invalid byte sequence') + array[i] = byte + } + return array + } + function bytesToNumber(bytes) { + return hexToNumber(bytesToHex(bytes)) + } + function ensureBytes(hex) { + return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex) + } + function normalizeScalar(num) { + if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) + return BigInt(num) + if (typeof num === 'bigint' && isWithinCurveOrder(num)) return num + throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n') + } + function mod(a, b = CURVE.P) { + const result = a % b + return result >= _0n ? result : b + result + } + function pow2(x, power) { + const {P} = CURVE + let res = x + while (power-- > _0n) { + res *= res + res %= P + } + return res + } + function sqrtMod(x) { + const {P} = CURVE + const _6n = BigInt(6) + const _11n = BigInt(11) + const _22n = BigInt(22) + const _23n = BigInt(23) + const _44n = BigInt(44) + const _88n = BigInt(88) + const b2 = (x * x * x) % P + const b3 = (b2 * b2 * x) % P + const b6 = (pow2(b3, _3n) * b3) % P + const b9 = (pow2(b6, _3n) * b3) % P + const b11 = (pow2(b9, _2n) * b2) % P + const b22 = (pow2(b11, _11n) * b11) % P + const b44 = (pow2(b22, _22n) * b22) % P + const b88 = (pow2(b44, _44n) * b44) % P + const b176 = (pow2(b88, _88n) * b88) % P + const b220 = (pow2(b176, _44n) * b44) % P + const b223 = (pow2(b220, _3n) * b3) % P + const t1 = (pow2(b223, _23n) * b22) % P + const t2 = (pow2(t1, _6n) * b2) % P + return pow2(t2, _2n) + } + function invert(number, modulo = CURVE.P) { + if (number === _0n || modulo <= _0n) { + throw new Error( + `invert: expected positive integers, got n=${number} mod=${modulo}` + ) + } + let a = mod(number, modulo) + let b = modulo + let x = _0n, + u = _1n + while (a !== _0n) { + const q = b / a + const r = b % a + const m = x - u * q + ;(b = a), (a = r), (x = u), (u = m) + } + const gcd = b + if (gcd !== _1n) throw new Error('invert: does not exist') + return mod(x, modulo) + } + function invertBatch(nums, p = CURVE.P) { + const scratch = new Array(nums.length) + const lastMultiplied = nums.reduce((acc, num, i) => { + if (num === _0n) return acc + scratch[i] = acc + return mod(acc * num, p) + }, _1n) + const inverted = invert(lastMultiplied, p) + nums.reduceRight((acc, num, i) => { + if (num === _0n) return acc + scratch[i] = mod(acc * scratch[i], p) + return mod(acc * num, p) + }, inverted) + return scratch + } + const divNearest = (a, b) => (a + b / _2n) / b + const POW_2_128 = _2n ** BigInt(128) + function splitScalarEndo(k) { + const {n} = CURVE + const a1 = BigInt('0x3086d221a7d46bcde86c90e49284eb15') + const b1 = -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3') + const a2 = BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8') + const b2 = a1 + const c1 = divNearest(b2 * k, n) + const c2 = divNearest(-b1 * k, n) + let k1 = mod(k - c1 * a1 - c2 * a2, n) + let k2 = mod(-c1 * b1 - c2 * b2, n) + const k1neg = k1 > POW_2_128 + const k2neg = k2 > POW_2_128 + if (k1neg) k1 = n - k1 + if (k2neg) k2 = n - k2 + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error('splitScalarEndo: Endomorphism failed, k=' + k) + } + return {k1neg, k1, k2neg, k2} + } + function truncateHash(hash) { + const {n} = CURVE + const byteLength = hash.length + const delta = byteLength * 8 - 256 + let h = bytesToNumber(hash) + if (delta > 0) h = h >> BigInt(delta) + if (h >= n) h -= n + return h + } + class HmacDrbg { + constructor() { + this.v = new Uint8Array(32).fill(1) + this.k = new Uint8Array(32).fill(0) + this.counter = 0 + } + hmac(...values) { + return utils.hmacSha256(this.k, ...values) + } + hmacSync(...values) { + if (typeof utils.hmacSha256Sync !== 'function') + throw new Error('utils.hmacSha256Sync is undefined, you need to set it') + const res = utils.hmacSha256Sync(this.k, ...values) + if (res instanceof Promise) + throw new Error('To use sync sign(), ensure utils.hmacSha256 is sync') + return res + } + incr() { + if (this.counter >= 1000) { + throw new Error('Tried 1,000 k values for sign(), all were invalid') + } + this.counter += 1 + } + async reseed(seed = new Uint8Array()) { + this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed) + this.v = await this.hmac(this.v) + if (seed.length === 0) return + this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed) + this.v = await this.hmac(this.v) + } + reseedSync(seed = new Uint8Array()) { + this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed) + this.v = this.hmacSync(this.v) + if (seed.length === 0) return + this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed) + this.v = this.hmacSync(this.v) + } + async generate() { + this.incr() + this.v = await this.hmac(this.v) + return this.v + } + generateSync() { + this.incr() + this.v = this.hmacSync(this.v) + return this.v + } + } + function isWithinCurveOrder(num) { + return _0n < num && num < CURVE.n + } + function isValidFieldElement(num) { + return _0n < num && num < CURVE.P + } + function kmdToSig(kBytes, m, d) { + const k = bytesToNumber(kBytes) + if (!isWithinCurveOrder(k)) return + const {n} = CURVE + const q = Point.BASE.multiply(k) + const r = mod(q.x, n) + if (r === _0n) return + const s = mod(invert(k, n) * mod(m + d * r, n), n) + if (s === _0n) return + const sig = new Signature(r, s) + const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n) + return {sig, recovery} + } + function normalizePrivateKey(key) { + let num + if (typeof key === 'bigint') { + num = key + } else if ( + typeof key === 'number' && + Number.isSafeInteger(key) && + key > 0 + ) { + num = BigInt(key) + } else if (typeof key === 'string') { + if (key.length !== 64) throw new Error('Expected 32 bytes of private key') + num = hexToNumber(key) + } else if (isUint8a(key)) { + if (key.length !== 32) throw new Error('Expected 32 bytes of private key') + num = bytesToNumber(key) + } else { + throw new TypeError('Expected valid private key') + } + if (!isWithinCurveOrder(num)) + throw new Error('Expected private key: 0 < key < n') + return num + } + function normalizePublicKey(publicKey) { + if (publicKey instanceof Point) { + publicKey.assertValidity() + return publicKey + } else { + return Point.fromHex(publicKey) + } + } + function normalizeSignature(signature) { + if (signature instanceof Signature) { + signature.assertValidity() + return signature + } + try { + return Signature.fromDER(signature) + } catch (error) { + return Signature.fromCompact(signature) + } + } + function getPublicKey(privateKey, isCompressed = false) { + return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed) + } + function recoverPublicKey( + msgHash, + signature, + recovery, + isCompressed = false + ) { + return Point.fromSignature(msgHash, signature, recovery).toRawBytes( + isCompressed + ) + } + function isPub(item) { + const arr = isUint8a(item) + const str = typeof item === 'string' + const len = (arr || str) && item.length + if (arr) return len === 33 || len === 65 + if (str) return len === 66 || len === 130 + if (item instanceof Point) return true + return false + } + function getSharedSecret(privateA, publicB, isCompressed = false) { + if (isPub(privateA)) + throw new TypeError('getSharedSecret: first arg must be private key') + if (!isPub(publicB)) + throw new TypeError('getSharedSecret: second arg must be public key') + const b = normalizePublicKey(publicB) + b.assertValidity() + return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed) + } + function bits2int(bytes) { + const slice = bytes.length > 32 ? bytes.slice(0, 32) : bytes + return bytesToNumber(slice) + } + function bits2octets(bytes) { + const z1 = bits2int(bytes) + const z2 = mod(z1, CURVE.n) + return int2octets(z2 < _0n ? z1 : z2) + } + function int2octets(num) { + if (typeof num !== 'bigint') throw new Error('Expected bigint') + const hex = numTo32bStr(num) + return hexToBytes(hex) + } + function initSigArgs(msgHash, privateKey, extraEntropy) { + if (msgHash == null) + throw new Error(`sign: expected valid message hash, not "${msgHash}"`) + const h1 = ensureBytes(msgHash) + const d = normalizePrivateKey(privateKey) + const seedArgs = [int2octets(d), bits2octets(h1)] + if (extraEntropy != null) { + if (extraEntropy === true) extraEntropy = utils.randomBytes(32) + const e = ensureBytes(extraEntropy) + if (e.length !== 32) + throw new Error('sign: Expected 32 bytes of extra data') + seedArgs.push(e) + } + const seed = concatBytes(...seedArgs) + const m = bits2int(h1) + return {seed, m, d} + } + function finalizeSig(recSig, opts) { + let {sig, recovery} = recSig + const {canonical, der, recovered} = Object.assign( + {canonical: true, der: true}, + opts + ) + if (canonical && sig.hasHighS()) { + sig = sig.normalizeS() + recovery ^= 1 + } + const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes() + return recovered ? [hashed, recovery] : hashed + } + async function sign(msgHash, privKey, opts = {}) { + const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy) + let sig + const drbg = new HmacDrbg() + await drbg.reseed(seed) + while (!(sig = kmdToSig(await drbg.generate(), m, d))) await drbg.reseed() + return finalizeSig(sig, opts) + } + function signSync(msgHash, privKey, opts = {}) { + const {seed, m, d} = initSigArgs(msgHash, privKey, opts.extraEntropy) + let sig + const drbg = new HmacDrbg() + drbg.reseedSync(seed) + while (!(sig = kmdToSig(drbg.generateSync(), m, d))) drbg.reseedSync() + return finalizeSig(sig, opts) + } + const vopts = {strict: true} + function verify(signature, msgHash, publicKey, opts = vopts) { + let sig + try { + sig = normalizeSignature(signature) + msgHash = ensureBytes(msgHash) + } catch (error) { + return false + } + const {r, s} = sig + if (opts.strict && sig.hasHighS()) return false + const h = truncateHash(msgHash) + let P + try { + P = normalizePublicKey(publicKey) + } catch (error) { + return false + } + const {n} = CURVE + const sinv = invert(s, n) + const u1 = mod(h * sinv, n) + const u2 = mod(r * sinv, n) + const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2) + if (!R) return false + const v = mod(R.x, n) + return v === r + } + function finalizeSchnorrChallenge(ch) { + return mod(bytesToNumber(ch), CURVE.n) + } + function hasEvenY(point) { + return (point.y & _1n) === _0n + } + class SchnorrSignature { + constructor(r, s) { + this.r = r + this.s = s + this.assertValidity() + } + static fromHex(hex) { + const bytes = ensureBytes(hex) + if (bytes.length !== 64) + throw new TypeError( + `SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}` + ) + const r = bytesToNumber(bytes.subarray(0, 32)) + const s = bytesToNumber(bytes.subarray(32, 64)) + return new SchnorrSignature(r, s) + } + assertValidity() { + const {r, s} = this + if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) + throw new Error('Invalid signature') + } + toHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s) + } + toRawBytes() { + return hexToBytes(this.toHex()) + } + } + function schnorrGetPublicKey(privateKey) { + return Point.fromPrivateKey(privateKey).toRawX() + } + function initSchnorrSigArgs(message, privateKey, auxRand) { + if (message == null) + throw new TypeError(`sign: Expected valid message, not "${message}"`) + const m = ensureBytes(message) + const d0 = normalizePrivateKey(privateKey) + const rand = ensureBytes(auxRand) + if (rand.length !== 32) + throw new TypeError('sign: Expected 32 bytes of aux randomness') + const P = Point.fromPrivateKey(d0) + const px = P.toRawX() + const d = hasEvenY(P) ? d0 : CURVE.n - d0 + return {m, P, px, d, rand} + } + function initSchnorrNonce(d, t0h) { + return numTo32b(d ^ bytesToNumber(t0h)) + } + function finalizeSchnorrNonce(k0h) { + const k0 = mod(bytesToNumber(k0h), CURVE.n) + if (k0 === _0n) + throw new Error('sign: Creation of signature failed. k is zero') + const R = Point.fromPrivateKey(k0) + const rx = R.toRawX() + const k = hasEvenY(R) ? k0 : CURVE.n - k0 + return {R, rx, k} + } + function finalizeSchnorrSig(R, k, e, d) { + return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes() + } + async function schnorrSign( + message, + privateKey, + auxRand = utils.randomBytes() + ) { + const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand) + const t = initSchnorrNonce(d, await utils.taggedHash(TAGS.aux, rand)) + const {R, rx, k} = finalizeSchnorrNonce( + await utils.taggedHash(TAGS.nonce, t, px, m) + ) + const e = finalizeSchnorrChallenge( + await utils.taggedHash(TAGS.challenge, rx, px, m) + ) + const sig = finalizeSchnorrSig(R, k, e, d) + const isValid = await schnorrVerify(sig, m, px) + if (!isValid) throw new Error('sign: Invalid signature produced') + return sig + } + function schnorrSignSync(message, privateKey, auxRand = utils.randomBytes()) { + const {m, px, d, rand} = initSchnorrSigArgs(message, privateKey, auxRand) + const t = initSchnorrNonce(d, utils.taggedHashSync(TAGS.aux, rand)) + const {R, rx, k} = finalizeSchnorrNonce( + utils.taggedHashSync(TAGS.nonce, t, px, m) + ) + const e = finalizeSchnorrChallenge( + utils.taggedHashSync(TAGS.challenge, rx, px, m) + ) + const sig = finalizeSchnorrSig(R, k, e, d) + const isValid = schnorrVerifySync(sig, m, px) + if (!isValid) throw new Error('sign: Invalid signature produced') + return sig + } + function initSchnorrVerify(signature, message, publicKey) { + const raw = signature instanceof SchnorrSignature + const sig = raw ? signature : SchnorrSignature.fromHex(signature) + if (raw) sig.assertValidity() + return { + ...sig, + m: ensureBytes(message), + P: normalizePublicKey(publicKey) + } + } + function finalizeSchnorrVerify(r, P, s, e) { + const R = Point.BASE.multiplyAndAddUnsafe( + P, + normalizePrivateKey(s), + mod(-e, CURVE.n) + ) + if (!R || !hasEvenY(R) || R.x !== r) return false + return true + } + async function schnorrVerify(signature, message, publicKey) { + try { + const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey) + const e = finalizeSchnorrChallenge( + await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m) + ) + return finalizeSchnorrVerify(r, P, s, e) + } catch (error) { + return false + } + } + function schnorrVerifySync(signature, message, publicKey) { + try { + const {r, s, m, P} = initSchnorrVerify(signature, message, publicKey) + const e = finalizeSchnorrChallenge( + utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m) + ) + return finalizeSchnorrVerify(r, P, s, e) + } catch (error) { + return false + } + } + const schnorr = { + Signature: SchnorrSignature, + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + signSync: schnorrSignSync, + verifySync: schnorrVerifySync + } + Point.BASE._setWindowSize(8) + const crypto = { + node: nodeCrypto, + web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined + } + const TAGS = { + challenge: 'BIP0340/challenge', + aux: 'BIP0340/aux', + nonce: 'BIP0340/nonce' + } + const TAGGED_HASH_PREFIXES = {} + const utils = { + isValidPrivateKey(privateKey) { + try { + normalizePrivateKey(privateKey) + return true + } catch (error) { + return false + } + }, + privateAdd: (privateKey, tweak) => { + const p = normalizePrivateKey(privateKey) + const t = normalizePrivateKey(tweak) + return numTo32b(mod(p + t, CURVE.n)) + }, + privateNegate: privateKey => { + const p = normalizePrivateKey(privateKey) + return numTo32b(CURVE.n - p) + }, + pointAddScalar: (p, tweak, isCompressed) => { + const P = Point.fromHex(p) + const t = normalizePrivateKey(tweak) + const Q = Point.BASE.multiplyAndAddUnsafe(P, t, _1n) + if (!Q) throw new Error('Tweaked point at infinity') + return Q.toRawBytes(isCompressed) + }, + pointMultiply: (p, tweak, isCompressed) => { + const P = Point.fromHex(p) + const t = bytesToNumber(ensureBytes(tweak)) + return P.multiply(t).toRawBytes(isCompressed) + }, + hashToPrivateKey: hash => { + hash = ensureBytes(hash) + if (hash.length < 40 || hash.length > 1024) + throw new Error('Expected 40-1024 bytes of private key as per FIPS 186') + const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n + return numTo32b(num) + }, + randomBytes: (bytesLength = 32) => { + if (crypto.web) { + return crypto.web.getRandomValues(new Uint8Array(bytesLength)) + } else if (crypto.node) { + const {randomBytes} = crypto.node + return Uint8Array.from(randomBytes(bytesLength)) + } else { + throw new Error("The environment doesn't have randomBytes function") + } + }, + randomPrivateKey: () => { + return utils.hashToPrivateKey(utils.randomBytes(40)) + }, + bytesToHex, + hexToBytes, + concatBytes, + mod, + invert, + sha256: async (...messages) => { + if (crypto.web) { + const buffer = await crypto.web.subtle.digest( + 'SHA-256', + concatBytes(...messages) + ) + return new Uint8Array(buffer) + } else if (crypto.node) { + const {createHash} = crypto.node + const hash = createHash('sha256') + messages.forEach(m => hash.update(m)) + return Uint8Array.from(hash.digest()) + } else { + throw new Error("The environment doesn't have sha256 function") + } + }, + hmacSha256: async (key, ...messages) => { + if (crypto.web) { + const ckey = await crypto.web.subtle.importKey( + 'raw', + key, + {name: 'HMAC', hash: {name: 'SHA-256'}}, + false, + ['sign'] + ) + const message = concatBytes(...messages) + const buffer = await crypto.web.subtle.sign('HMAC', ckey, message) + return new Uint8Array(buffer) + } else if (crypto.node) { + const {createHmac} = crypto.node + const hash = createHmac('sha256', key) + messages.forEach(m => hash.update(m)) + return Uint8Array.from(hash.digest()) + } else { + throw new Error("The environment doesn't have hmac-sha256 function") + } + }, + sha256Sync: undefined, + hmacSha256Sync: undefined, + taggedHash: async (tag, ...messages) => { + let tagP = TAGGED_HASH_PREFIXES[tag] + if (tagP === undefined) { + const tagH = await utils.sha256( + Uint8Array.from(tag, c => c.charCodeAt(0)) + ) + tagP = concatBytes(tagH, tagH) + TAGGED_HASH_PREFIXES[tag] = tagP + } + return utils.sha256(tagP, ...messages) + }, + taggedHashSync: (tag, ...messages) => { + if (typeof utils.sha256Sync !== 'function') + throw new Error('utils.sha256Sync is undefined, you need to set it') + let tagP = TAGGED_HASH_PREFIXES[tag] + if (tagP === undefined) { + const tagH = utils.sha256Sync( + Uint8Array.from(tag, c => c.charCodeAt(0)) + ) + tagP = concatBytes(tagH, tagH) + TAGGED_HASH_PREFIXES[tag] = tagP + } + return utils.sha256Sync(tagP, ...messages) + }, + precompute(windowSize = 8, point = Point.BASE) { + const cached = point === Point.BASE ? point : new Point(point.x, point.y) + cached._setWindowSize(windowSize) + cached.multiply(_3n) + return cached + } + } + + exports.CURVE = CURVE + exports.Point = Point + exports.Signature = Signature + exports.getPublicKey = getPublicKey + exports.getSharedSecret = getSharedSecret + exports.recoverPublicKey = recoverPublicKey + exports.schnorr = schnorr + exports.sign = sign + exports.signSync = signSync + exports.utils = utils + exports.verify = verify + + Object.defineProperty(exports, '__esModule', {value: true}) +}) diff --git a/static/js/utils.js b/static/js/utils.js new file mode 100644 index 000000000..cf852b58b --- /dev/null +++ b/static/js/utils.js @@ -0,0 +1,23 @@ +function splitAmount(value) { + const chunks = [] + for (let i = 0; i < 32; i++) { + const mask = 1 << i + if ((value & mask) !== 0) chunks.push(Math.pow(2, i)) + } + return chunks +} + +function bytesToNumber(bytes) { + return hexToNumber(nobleSecp256k1.utils.bytesToHex(bytes)) +} + +function bigIntStringify(key, value) { + return typeof value === 'bigint' ? value.toString() : value +} + +function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToNumber: expected string, got ' + typeof hex) + } + return BigInt(`0x${hex}`) +} diff --git a/tasks.py b/tasks.py new file mode 100644 index 000000000..982d3ac18 --- /dev/null +++ b/tasks.py @@ -0,0 +1,31 @@ +import asyncio + +from cashu.core.migrations import migrate_databases +from cashu.mint import migrations + +from lnbits.core.models import Payment +from lnbits.tasks import register_invoice_listener + +from . import db, ledger + + +async def startup_cashu_mint(): + await migrate_databases(db, migrations) + await ledger.load_used_proofs() + await ledger.init_keysets(autosave=False) + + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + if payment.extra.get("tag") != "cashu": + return + + return diff --git a/templates/cashu/_api_docs.html b/templates/cashu/_api_docs.html new file mode 100644 index 000000000..f7bb19f60 --- /dev/null +++ b/templates/cashu/_api_docs.html @@ -0,0 +1,80 @@ + + + + diff --git a/templates/cashu/_cashu.html b/templates/cashu/_cashu.html new file mode 100644 index 000000000..370e9eb4a --- /dev/null +++ b/templates/cashu/_cashu.html @@ -0,0 +1,28 @@ + + + +

Create Cashu ecash mints and wallets.

+ Created by + arcbtc, + vlad, + calle. + + + diff --git a/templates/cashu/index.html b/templates/cashu/index.html new file mode 100644 index 000000000..2599669cf --- /dev/null +++ b/templates/cashu/index.html @@ -0,0 +1,367 @@ +{% extends "base.html" %} {% from "macros.jinja" import window_vars with context +%} {% block page %} +
+
+ + + Cashu mint and wallet +

+

+ Here you can create multiple cashu mints that you can share. Each mint + can service many users but all ecash tokens of a mint are only valid + inside that mint and not across different mints. To exchange funds + between mints, use Lightning payments. +

+ Important +

+

+ If you are the operator of this LNbits instance, make sure to set + CASHU_PRIVATE_KEY="randomkey" in your configuration file. Do not + create mints before setting the key and do not change the key once + set. +

+
+
+ + + +
+
+
Mints
+
+
+ Export to CSV +
+
+ + {% raw %} + + + + {% endraw %} + + New Mint +
+
+
+ +
+ + +
{{SITE_TITLE}} Cashu extension
+
+ + + + {% include "cashu/_api_docs.html" %} + + {% include "cashu/_cashu.html" %} + + +
+
+ + + + + + + +
+ Create Mint + + Cancel +
+
+
+
+
+{% endblock %} {% block scripts %} {{ window_vars(user) }} + +{% endblock %} diff --git a/templates/cashu/mint.html b/templates/cashu/mint.html new file mode 100644 index 000000000..a6959ec24 --- /dev/null +++ b/templates/cashu/mint.html @@ -0,0 +1,92 @@ +{% extends "public.html" %} {% block page %} +
+
+ + +
+ +

{{ mint_name }}

+ + click to open wallet +
+
+
+ + +
Read the following carefully!
+

+ This is a + Cashu + mint. Cashu is an ecash system for Bitcoin. +

+

+ Open this page in your native browser
+ Before you continue to the wallet, make sure to open this page in your + device's native browser application (Safari for iOS, Chrome for + Android). Do not use Cashu in an embedded browser that opens when you + click a link in a messenger. +

+

+ Add wallet to home screen
+ You can add Cashu to your home screen as a progressive web app (PWA). + After opening the wallet in your browser (click the link above), on + Android (Chrome), click the menu at the upper right. On iOS (Safari), + click the share button. Now press the Add to Home screen button. +

+

+ Backup your wallet
+ Ecash is a bearer asset. That means losing access to your wallet will + make you lose your funds. The wallet stores ecash tokens on your + device's database. If you lose the link or delete your your data + without backing up, you will lose your tokens. Press the Backup button + in the wallet to download a copy of your tokens. +

+

+ This service is in BETA
+ Cashu is still experimental and in active development. There are + likely bugs in this implementation so please use this with caution. We + hold no responsibility for people losing access to funds. Use at your + own risk! +

+
+
+
+
+ +{% endblock %} {% block scripts %} + + + +{% endblock %} diff --git a/templates/cashu/wallet.html b/templates/cashu/wallet.html new file mode 100644 index 000000000..9638f347e --- /dev/null +++ b/templates/cashu/wallet.html @@ -0,0 +1,2992 @@ +{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu wallet {% +endraw %} {% endblock %} {% block footer %}{% endblock %} {% block +page_container %} + + +
+
+ + +
+
+
+

+
+ {% raw %} {{ getBalance() }} + {{tickerShort}} {% endraw %} +
+

+
+
+ + +
+
+
+ + + +
+
+ Get Ecash +
+
+
+ + Send Ecash +
+
+ + + + + + + + + + + + +
+ + + + Mints + You can connect your wallet to multiple Cashu mints. + Enter a mint URL and select the mint your want to + use. + + + + +
+ {% raw %} + + + + + + + + + {{mint.url}} + + + + + + + + + + + {% endraw %} + +
+
+
+
+
+ + + + + + + +
+
+
+ + + + Multimint Swaps + Swap funds from one mint to another via Lightning. + Warning: this feature is still experimental and could + behave in unexpected ways! + + + + + + + + + + + Swap + + +
+
+ + + + + {% raw %} + + {% endraw %} + + + + + + + + {% raw %} + + {% endraw %} + + + + + + + + {% raw %} + + {% endraw %} + + +
+
+
+ +
+
+
+ Create invoice + +
+ +
+
+ Warning + Download wallet backup + InstallInstall Cashu +
+
+
+ Pay invoice + +
+
+
+
+ + + +
+ + + + + + + + +
+ + + +
+ {% raw %} +
+ Pay {{ payInvoiceData.invoice.fsat }}{% endraw %} + {{LNBITS_DENOMINATION}} {% raw %} +
+ +

+ Description: {{ + payInvoiceData.invoice.description }}
+ Expire date: {{ payInvoiceData.invoice.expireDate + }}
+ Hash: {{ payInvoiceData.invoice.hash }} +

+ {% endraw %} +
+ + Cancel +
+
+ Not enough funds! + Cancel +
+
+ +
+ {% raw %} + +

+ {{ payInvoiceData.domain }} is requesting {{ + payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} {% + endraw %} {{LNBITS_DENOMINATION}} {% raw %} + +

+

+ {{ payInvoiceData.lnurlpay.targetUser || + payInvoiceData.domain }} + is requesting
+ between + {{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }} + and + {{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} + {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} + +

+ +
+

+ {{ payInvoiceData.lnurlpay.description }} +

+

+ +

+
+
+
+ {% endraw %} + +
+
+ +
+
+
+ Send + Cancel +
+
+
+
+ + + +
+ Enter + + + Close +
+
+
+ + + +
+ + Cancel + +
+
+
+
+
+ + + +
+ +
+
+ Cancel +
+
+
+ + + + + + + + Cashu + wallet + + +

Please take a moment to read the following information.

+ +

+ Open this wallet on your device's native browser + Cashu stores your ecash on your device locally. For the best + experience, use this wallet with your device's native web browser + (for example Safari for iOS, Chrome for Android). +

+

+ Add to home screen. + Add Cashu to your home screen as a progressive web app (PWA). On + Android Chrome, click the hamburger menu at the upper right. On + iOS Safari, click the share button. Now press the Add to Home + screen button. +

+

+ This software is in BETA! We hold no + responsibility for people losing access to funds. Use at your own + risk! Ecash is a bearer asset, meaning losing access to this + wallet will mean you will lose the funds. This wallet stores ecash + tokens in its database. If you lose the link or delete your your + data without backing up, you will lose your tokens. Press the + Backup button to download a copy of your tokens. +

+
+ Install Cashu + Copy URL + Continue +
+
+
+
+ + + +
Warning
+

+ Bookmark this page and backup your tokens! + Ecash is a bearer asset, meaning losing access to this wallet will + mean you will lose the funds. This wallet stores ecash tokens in its + database. If you lose the link or delete your your data without + backing up, you will lose your tokens. Press the Backup button to + download a copy of your tokens. +

+

+ Add to home screen. + You can add Cashu to your home screen as a progressive web app + (PWA). On Android Chrome, click the hamburger menu at the upper + right. On iOS Safari, click the share button. Now press the Add to + Home screen button. +

+

+ This software is in BETA! We hold no responsibility + for people losing access to funds. Use at your own risk! +

+
+ Copy wallet URL + I understand +
+
+
+ + + +
+
+
+ Create a Lightning invoice +
+
+ + +
+ +
+ Copy invoice + Create Invoice + Close +
+
+
+ + + +
+
+
+ How much would you like to send? +
+
+ + +
+
+
+ + + + + + +
+ +
+
+ Send Tokens + +
+ Copy token + Copy link +
+ + Close +
+
+
+ + + +
+
+
+ Receive Cashu tokens +
+
+ +
+ +
+ Receive + + Close +
+
+
+ + +
Do you trust this mint?
+

+ A Cashu mint does not know about your financial activity but it + controls your funds. Make sure that you trust the operator of this + mint. +

+ +
+ Add mint + Cancel +
+
+
+
+
+
+{% endblock %} {% block styles %} + +{% endblock %} {% block scripts %} + + + + + +{% endblock %} diff --git a/templates/cashu/wallet2.html b/templates/cashu/wallet2.html new file mode 100644 index 000000000..f628a76b0 --- /dev/null +++ b/templates/cashu/wallet2.html @@ -0,0 +1,2622 @@ +{% extends "public.html" %} {% block toolbar_title %} {% raw %} Cashu {% endraw +%} - {{mint_name}} {% endblock %} {% block footer %}{% endblock %} {% block +page_container %} + + +
+
+ + +
+
+
+ Create invoice + +
+
+

+
+ {% raw %} {{ getBalance() }} {{tickerShort}} {% endraw %} +
+

+
+
+ Pay invoice + +
+
+
+
+
+
+

+
+ {% raw %} {{ getBalance() }} + {{tickerShort}} {% endraw %} +
+

+
+
+
+
+
+ + + +
+
+ Get Ecash +
+
+
+ + Pay Ecash +
+
+ + + + + + + + + + + + +
+ + + + Cashu mints + {{item}} + You have to connect your wallet to one or more + mints. + + + + Your mints +
+ {% raw %} + + + + + {{mint.url}} + + {{mint.balance}} {{tickerShort}} + + + + + + {% endraw %} + +
+
+
+
+
+ + + + + + + +
+
+
+ + + + + {% raw %} + + {% endraw %} + + + + + + + + {% raw %} + + {% endraw %} + + + + + + + + {% raw %} + + {% endraw %} + + +
+
+
+ +
+ + Warning + BackupDownload wallet backup +
+
+ +
+ + + + + + + + +
+ + + +
+
+ {% raw %} {{ + parseFloat(String(payInvoiceData.invoice.fsat).replaceAll(",", + "")) / 100 }} {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} +
+
+ {{ payInvoiceData.invoice.fsat }}{% endraw %} + {{LNBITS_DENOMINATION}} {% raw %} +
+ +

+ Description: {{ + payInvoiceData.invoice.description }}
+ Expire date: {{ payInvoiceData.invoice.expireDate + }}
+ Hash: {{ payInvoiceData.invoice.hash }} +

+ {% endraw %} +
+ Pay + Cancel +
+
+ Not enough funds! + Cancel +
+
+
+ {% raw %} + +

+ Authenticate with {{ payInvoiceData.lnurlauth.domain }}? +

+ +

+ For every website and for every LNbits wallet, a new keypair + will be deterministically generated so your identity can't be + tied to your LNbits wallet or linked across websites. No other + data will be shared with {{ payInvoiceData.lnurlauth.domain }}. +

+

+ Your public key for + {{ payInvoiceData.lnurlauth.domain }} is: +

+

+ + {{ payInvoiceData.lnurlauth.pubkey }} + +

+
+ Login + Cancel +
+
+ {% endraw %} +
+
+ {% raw %} + +

+ {{ payInvoiceData.lnurlpay.domain }} is requesting {{ + payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} + {{LNBITS_DENOMINATION}} + +
+ and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment +
+

+

+ {{ payInvoiceData.lnurlpay.targetUser || + payInvoiceData.lnurlpay.domain }} + is requesting
+ between + {{ payInvoiceData.lnurlpay.minSendable | msatoshiFormat }} + and + {{ payInvoiceData.lnurlpay.maxSendable | msatoshiFormat }} + {% endraw %} {{LNBITS_DENOMINATION}} {% raw %} + +
+ and a {{payInvoiceData.lnurlpay.commentAllowed}}-char comment +
+

+ +
+

+ {{ payInvoiceData.lnurlpay.description }} +

+

+ +

+
+
+
+ {% endraw %} + + {% raw %} +
+
+ +
+
+
+ Send {{LNBITS_DENOMINATION}} + Cancel +
+
+ {% endraw %} +
+
+ + + +
+ Enter + + + Close +
+
+
+ + + +
+ + Cancel + +
+
+
+
+
+ + + +
+ +
+
+ Cancel +
+
+
+ + + +
Warning
+

+ Bookmark this page and backup your tokens! + Ecash is a bearer asset, meaning losing access to this wallet will + mean you will lose the funds. This wallet stores ecash tokens in its + database. If you lose the link or delete your your data without + backing up, you will lose your tokens. Press the Backup button to + download a copy of your tokens. +

+

+ Add to home screen. + You can add Cashu to your home screen as a progressive web app + (PWA). On Android Chrome, click the hamburger menu at the upper + right. On iOS Safari, click the share button. Now press the Add to + Home screen button. +

+

+ This service is in BETA! We hold no responsibility + for people losing access to funds. Use at your own risk! +

+
+ Copy wallet URL + I understand +
+
+
+ + + +
+
+
+ Create a Lightning invoice +
+
+ + +
+ +
+ Copy invoice + Create Invoice + Close +
+
+
+ + + +
+
+
+ How much would you like to send? +
+
+ + +
+
+
+ + + + + + +
+ +
+
+ Send Tokens + +
+ Copy token + Copy link +
+ + Close +
+
+
+ + + +
+
+
+ Receive Cashu tokens +
+
+ +
+ +
+ Receive + + + Close +
+
+
+
+
+
+{% endblock %} {% block styles %} + +{% endblock %} {% block scripts %} + + + + + +{% endblock %} diff --git a/views.py b/views.py new file mode 100644 index 000000000..09d314c36 --- /dev/null +++ b/views.py @@ -0,0 +1,253 @@ +from http import HTTPStatus +from typing import Optional + +from fastapi import Depends, Request +from fastapi.templating import Jinja2Templates +from starlette.exceptions import HTTPException +from starlette.responses import HTMLResponse + +from lnbits.core.models import User +from lnbits.decorators import check_user_exists + +from . import cashu_ext, cashu_renderer +from .crud import get_cashu + +templates = Jinja2Templates(directory="templates") + + +@cashu_ext.get("/", response_class=HTMLResponse) +async def index( + request: Request, + user: User = Depends(check_user_exists), +): + return cashu_renderer().TemplateResponse( + "cashu/index.html", {"request": request, "user": user.dict()} + ) + + +@cashu_ext.get("/wallet") +async def wallet(request: Request, mint_id: Optional[str] = None): + if mint_id is not None: + cashu = await get_cashu(mint_id) + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + manifest_url = f"/cashu/manifest/{mint_id}.webmanifest" + mint_name = cashu.name + else: + manifest_url = "/cashu/cashu.webmanifest" + mint_name = "Cashu mint" + + return cashu_renderer().TemplateResponse( + "cashu/wallet.html", + { + "request": request, + "web_manifest": manifest_url, + "mint_name": mint_name, + }, + ) + + +@cashu_ext.get("/mint/{mintID}") +async def cashu(request: Request, mintID): + cashu = await get_cashu(mintID) + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + return cashu_renderer().TemplateResponse( + "cashu/mint.html", + {"request": request, "mint_id": mintID}, + ) + + +@cashu_ext.get("/manifest/{cashu_id}.webmanifest") +async def manifest_lnbits(cashu_id: str): + cashu = await get_cashu(cashu_id) + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + return get_manifest(cashu_id, cashu.name) + + +@cashu_ext.get("/cashu.webmanifest") +async def manifest(): + return get_manifest() + + +def get_manifest(mint_id: Optional[str] = None, mint_name: Optional[str] = None): + manifest_name = "Cashu" + if mint_name: + manifest_name += " - " + mint_name + manifest_url = "/cashu/wallet" + if mint_id: + manifest_url += "?mint_id=" + mint_id + + return { + "short_name": "Cashu", + "name": manifest_name, + "icons": [ + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png", + "type": "image/png", + "sizes": "512x512", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png", + "type": "image/png", + "sizes": "96x96", + }, + ], + "id": manifest_url, + "start_url": manifest_url, + "background_color": "#1F2234", + "description": "Cashu ecash wallet", + "display": "standalone", + "scope": "/cashu/", + "theme_color": "#1F2234", + "protocol_handlers": [ + {"protocol": "web+cashu", "url": "&recv_token=%s"}, + {"protocol": "web+lightning", "url": "&lightning=%s"}, + ], + "shortcuts": [ + { + "name": manifest_name, + "short_name": "Cashu", + "description": manifest_name, + "url": manifest_url, + "icons": [ + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png", + "sizes": "512x512", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png", + "sizes": "192x192", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png", + "sizes": "144x144", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/96x96.png", + "sizes": "96x96", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png", + "sizes": "72x72", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/48x48.png", + "sizes": "48x48", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/16x16.png", + "sizes": "16x16", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/20x20.png", + "sizes": "20x20", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/29x29.png", + "sizes": "29x29", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/32x32.png", + "sizes": "32x32", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/40x40.png", + "sizes": "40x40", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/50x50.png", + "sizes": "50x50", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/57x57.png", + "sizes": "57x57", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/58x58.png", + "sizes": "58x58", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/60x60.png", + "sizes": "60x60", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/64x64.png", + "sizes": "64x64", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/72x72.png", + "sizes": "72x72", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/76x76.png", + "sizes": "76x76", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/80x80.png", + "sizes": "80x80", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/87x87.png", + "sizes": "87x87", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/100x100.png", + "sizes": "100x100", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/114x114.png", + "sizes": "114x114", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/120x120.png", + "sizes": "120x120", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/128x128.png", + "sizes": "128x128", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/144x144.png", + "sizes": "144x144", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/152x152.png", + "sizes": "152x152", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/167x167.png", + "sizes": "167x167", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/180x180.png", + "sizes": "180x180", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/192x192.png", + "sizes": "192x192", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/256x256.png", + "sizes": "256x256", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/512x512.png", + "sizes": "512x512", + }, + { + "src": "https://github.com/cashubtc/cashu-ui/raw/main/ui/icons/circle/1024x1024.png", + "sizes": "1024x1024", + }, + ], + } + ], + } diff --git a/views_api.py b/views_api.py new file mode 100644 index 000000000..682412a4e --- /dev/null +++ b/views_api.py @@ -0,0 +1,440 @@ +import math +from http import HTTPStatus +from typing import Dict, Union + +# -------- cashu imports +from cashu.core.base import ( + CheckFeesRequest, + CheckFeesResponse, + CheckSpendableRequest, + CheckSpendableResponse, + GetMeltResponse, + GetMintResponse, + Invoice, + PostMeltRequest, + PostMintRequest, + PostMintResponse, + PostSplitRequest, + PostSplitResponse, +) +from fastapi import Depends, Query +from loguru import logger +from starlette.exceptions import HTTPException + +from lnbits import bolt11 +from lnbits.core.crud import check_internal, get_user +from lnbits.core.services import ( + check_transaction_status, + create_invoice, + fee_reserve, + pay_invoice, +) +from lnbits.decorators import WalletTypeInfo, get_key_type, require_admin_key +from lnbits.helpers import urlsafe_short_hash +from lnbits.wallets.base import PaymentStatus + +from . import cashu_ext, ledger +from .crud import create_cashu, delete_cashu, get_cashu, get_cashus +from .models import Cashu + +# --------- extension imports + +# WARNING: Do not set this to False in production! This will create +# tokens for free otherwise. This is for testing purposes only! + +LIGHTNING = True + +if not LIGHTNING: + logger.warning( + "Cashu: LIGHTNING is set False! That means that I will create ecash for free!" + ) + +######################################## +############### LNBITS MINTS ########### +######################################## + + +@cashu_ext.get("/api/v1/mints", status_code=HTTPStatus.OK) +async def api_cashus( + all_wallets: bool = Query(False), wallet: WalletTypeInfo = Depends(get_key_type) +): + """ + Get all mints of this wallet. + """ + wallet_ids = [wallet.wallet.id] + if all_wallets: + user = await get_user(wallet.wallet.user) + if user: + wallet_ids = user.wallet_ids + + return [cashu.dict() for cashu in await get_cashus(wallet_ids)] + + +@cashu_ext.post("/api/v1/mints", status_code=HTTPStatus.CREATED) +async def api_cashu_create( + data: Cashu, + wallet: WalletTypeInfo = Depends(get_key_type), +): + """ + Create a new mint for this wallet. + """ + cashu_id = urlsafe_short_hash() + # generate a new keyset in cashu + keyset = await ledger.load_keyset(cashu_id) + + cashu = await create_cashu( + cashu_id=cashu_id, keyset_id=keyset.id, wallet_id=wallet.wallet.id, data=data + ) + logger.debug(cashu) + return cashu.dict() + + +@cashu_ext.delete("/api/v1/mints/{cashu_id}") +async def api_cashu_delete( + cashu_id: str, wallet: WalletTypeInfo = Depends(require_admin_key) +): + """ + Delete an existing cashu mint. + """ + cashu = await get_cashu(cashu_id) + + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Cashu mint does not exist." + ) + + if cashu.wallet != wallet.wallet.id: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="Not your Cashu mint." + ) + + await delete_cashu(cashu_id) + raise HTTPException(status_code=HTTPStatus.NO_CONTENT) + + +####################################### +########### CASHU ENDPOINTS ########### +####################################### + + +@cashu_ext.get("/api/v1/{cashu_id}/keys", status_code=HTTPStatus.OK) +async def keys(cashu_id: str = Query(None)) -> dict[int, str]: + """Get the public keys of the mint""" + cashu: Union[Cashu, None] = await get_cashu(cashu_id) + + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + return ledger.get_keyset(keyset_id=cashu.keyset_id) + + +@cashu_ext.get("/api/v1/{cashu_id}/keys/{idBase64Urlsafe}") +async def keyset_keys( + cashu_id: str = Query(None), idBase64Urlsafe: str = Query(None) +) -> dict[int, str]: + """ + Get the public keys of the mint of a specificy keyset id. + The id is encoded in base64_urlsafe and needs to be converted back to + normal base64 before it can be processed. + """ + + cashu: Union[Cashu, None] = await get_cashu(cashu_id) + + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + id = idBase64Urlsafe.replace("-", "+").replace("_", "/") + keyset = ledger.get_keyset(keyset_id=id) + return keyset + + +@cashu_ext.get("/api/v1/{cashu_id}/keysets", status_code=HTTPStatus.OK) +async def keysets(cashu_id: str = Query(None)) -> dict[str, list[str]]: + """Get the public keys of the mint""" + cashu: Union[Cashu, None] = await get_cashu(cashu_id) + + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + return {"keysets": [cashu.keyset_id]} + + +@cashu_ext.get("/api/v1/{cashu_id}/mint") +async def request_mint(cashu_id: str = Query(None), amount: int = 0) -> GetMintResponse: + """ + Request minting of new tokens. The mint responds with a Lightning invoice. + This endpoint can be used for a Lightning invoice UX flow. + + Call `POST /mint` after paying the invoice. + """ + cashu: Union[Cashu, None] = await get_cashu(cashu_id) + + if not cashu: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + # create an invoice that the wallet needs to pay + try: + payment_hash, payment_request = await create_invoice( + wallet_id=cashu.wallet, + amount=amount, + memo=f"{cashu.name}", + extra={"tag": "cashu"}, + ) + invoice = Invoice( + amount=amount, pr=payment_request, hash=payment_hash, issued=False + ) + # await store_lightning_invoice(cashu_id, invoice) + await ledger.crud.store_lightning_invoice(invoice=invoice, db=ledger.db) + except Exception as e: + logger.error(e) + raise HTTPException(status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail=str(e)) + + print(f"Lightning invoice: {payment_request}") + resp = GetMintResponse(pr=payment_request, hash=payment_hash) + # return {"pr": payment_request, "hash": payment_hash} + return resp + + +@cashu_ext.post("/api/v1/{cashu_id}/mint") +async def mint( + data: PostMintRequest, + cashu_id: str = Query(None), + payment_hash: str = Query(None), +) -> PostMintResponse: + """ + Requests the minting of tokens belonging to a paid payment request. + Call this endpoint after `GET /mint`. + """ + cashu: Union[Cashu, None] = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + + keyset = ledger.keysets.keysets[cashu.keyset_id] + + if LIGHTNING: + invoice: Invoice = await ledger.crud.get_lightning_invoice( + db=ledger.db, hash=payment_hash + ) + if invoice is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, + detail="Mint does not know this invoice.", + ) + if invoice.issued: + raise HTTPException( + status_code=HTTPStatus.PAYMENT_REQUIRED, + detail="Tokens already issued for this invoice.", + ) + + # set this invoice as issued + await ledger.crud.update_lightning_invoice( + db=ledger.db, hash=payment_hash, issued=True + ) + + status: PaymentStatus = await check_transaction_status( + cashu.wallet, payment_hash + ) + + try: + total_requested = sum([bm.amount for bm in data.outputs]) + if total_requested > invoice.amount: + raise HTTPException( + status_code=HTTPStatus.PAYMENT_REQUIRED, + detail=f"Requested amount too high: {total_requested}. Invoice amount: {invoice.amount}", + ) + + if not status.paid: + raise HTTPException( + status_code=HTTPStatus.PAYMENT_REQUIRED, detail="Invoice not paid." + ) + + promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset) + return PostMintResponse(promises=promises) + except (Exception, HTTPException) as e: + logger.debug(f"Cashu: /melt {str(e) or getattr(e, 'detail')}") + # unset issued flag because something went wrong + await ledger.crud.update_lightning_invoice( + db=ledger.db, hash=payment_hash, issued=False + ) + raise HTTPException( + status_code=getattr(e, "status_code") + or HTTPStatus.INTERNAL_SERVER_ERROR, + detail=str(e) or getattr(e, "detail"), + ) + else: + # only used for testing when LIGHTNING=false + promises = await ledger._generate_promises(B_s=data.outputs, keyset=keyset) + return PostMintResponse(promises=promises) + + +@cashu_ext.post("/api/v1/{cashu_id}/melt") +async def melt_coins( + payload: PostMeltRequest, cashu_id: str = Query(None) +) -> GetMeltResponse: + """Invalidates proofs and pays a Lightning invoice.""" + cashu: Union[None, Cashu] = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + proofs = payload.proofs + invoice = payload.pr + + # !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID + # THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID + # TOKENS + assert all([p.id == cashu.keyset_id for p in proofs]), HTTPException( + status_code=HTTPStatus.METHOD_NOT_ALLOWED, + detail="Error: Tokens are from another mint.", + ) + + # set proofs as pending + await ledger._set_proofs_pending(proofs) + + try: + await ledger._verify_proofs(proofs) + + total_provided = sum([p["amount"] for p in proofs]) + invoice_obj = bolt11.decode(invoice) + amount = math.ceil(invoice_obj.amount_msat / 1000) + + internal_checking_id = await check_internal(invoice_obj.payment_hash) + + if not internal_checking_id: + fees_msat = fee_reserve(invoice_obj.amount_msat) + else: + fees_msat = 0 + assert total_provided >= amount + math.ceil(fees_msat / 1000), Exception( + f"Provided proofs ({total_provided} sats) not enough for Lightning payment ({amount + fees_msat} sats)." + ) + logger.debug(f"Cashu: Initiating payment of {total_provided} sats") + try: + await pay_invoice( + wallet_id=cashu.wallet, + payment_request=invoice, + description="Pay cashu invoice", + extra={"tag": "cashu", "cashu_name": cashu.name}, + ) + except Exception as e: + logger.debug(f"Cashu error paying invoice {invoice_obj.payment_hash}: {e}") + raise e + finally: + logger.debug( + f"Cashu: Wallet {cashu.wallet} checking PaymentStatus of {invoice_obj.payment_hash}" + ) + status: PaymentStatus = await check_transaction_status( + cashu.wallet, invoice_obj.payment_hash + ) + if status.paid is True: + logger.debug( + f"Cashu: Payment successful, invalidating proofs for {invoice_obj.payment_hash}" + ) + await ledger._invalidate_proofs(proofs) + else: + logger.debug(f"Cashu: Payment failed for {invoice_obj.payment_hash}") + except Exception as e: + logger.debug(f"Cashu: Exception: {str(e)}") + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=f"Cashu: {str(e)}", + ) + finally: + logger.debug("Cashu: Unset pending") + # delete proofs from pending list + await ledger._unset_proofs_pending(proofs) + + return GetMeltResponse(paid=status.paid, preimage=status.preimage) + + +@cashu_ext.post("/api/v1/{cashu_id}/check") +async def check_spendable( + payload: CheckSpendableRequest, cashu_id: str = Query(None) +) -> Dict[int, bool]: + """Check whether a secret has been spent already or not.""" + cashu: Union[None, Cashu] = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + spendableList = await ledger.check_spendable(payload.proofs) + return CheckSpendableResponse(spendable=spendableList) + + +@cashu_ext.post("/api/v1/{cashu_id}/checkfees") +async def check_fees( + payload: CheckFeesRequest, cashu_id: str = Query(None) +) -> CheckFeesResponse: + """ + Responds with the fees necessary to pay a Lightning invoice. + Used by wallets for figuring out the fees they need to supply. + This is can be useful for checking whether an invoice is internal (Cashu-to-Cashu). + """ + cashu: Union[None, Cashu] = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + invoice_obj = bolt11.decode(payload.pr) + internal_checking_id = await check_internal(invoice_obj.payment_hash) + + if not internal_checking_id: + fees_msat = fee_reserve(invoice_obj.amount_msat) + else: + fees_msat = 0 + return CheckFeesResponse(fee=math.ceil(fees_msat / 1000)) + + +@cashu_ext.post("/api/v1/{cashu_id}/split") +async def split( + payload: PostSplitRequest, cashu_id: str = Query(None) +) -> PostSplitResponse: + """ + Requetst a set of tokens with amount "total" to be split into two + newly minted sets with amount "split" and "total-split". + """ + cashu: Union[None, Cashu] = await get_cashu(cashu_id) + if cashu is None: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="Mint does not exist." + ) + proofs = payload.proofs + + # !!!!!!! MAKE SURE THAT PROOFS ARE ONLY FROM THIS CASHU KEYSET ID + # THIS IS NECESSARY BECAUSE THE CASHU BACKEND WILL ACCEPT ANY VALID + # TOKENS + if not all([p.id == cashu.keyset_id for p in proofs]): + raise HTTPException( + status_code=HTTPStatus.METHOD_NOT_ALLOWED, + detail="Error: Tokens are from another mint.", + ) + + amount = payload.amount + outputs = payload.outputs + assert outputs, Exception("no outputs provided.") + split_return = None + try: + keyset = ledger.keysets.keysets[cashu.keyset_id] + split_return = await ledger.split(proofs, amount, outputs, keyset) + except Exception as exc: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail=str(exc), + ) + if not split_return: + raise HTTPException( + status_code=HTTPStatus.BAD_REQUEST, + detail="there was an error with the split", + ) + frst_promises, scnd_promises = split_return + resp = PostSplitResponse(fst=frst_promises, snd=scnd_promises) + return resp