From 04cfdc2bf3b15bc9d9bdc5b0137d0d729c0ccadf Mon Sep 17 00:00:00 2001 From: Taylor Helsper Date: Thu, 29 Sep 2022 23:06:45 -0500 Subject: [PATCH 01/29] Update index.html --- lnbits/core/templates/core/index.html | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index f769b44f6..1319fa1f0 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -171,6 +171,19 @@ +
+
+ + + +
+
+   +
+
From fb58f1ed5e04895582ac79e889a74e2d9a07dd57 Mon Sep 17 00:00:00 2001 From: Taylor Helsper Date: Thu, 29 Sep 2022 23:42:38 -0500 Subject: [PATCH 02/29] Update myNode images --- lnbits/static/images/mynode.png | Bin 3757 -> 13994 bytes lnbits/static/images/mynodel.png | Bin 14352 -> 9272 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/lnbits/static/images/mynode.png b/lnbits/static/images/mynode.png index cf25bc586797b750a64ddba848b2499a0698a5b2..390446b8ee906da9e01142b477a59506269ce5e7 100644 GIT binary patch literal 13994 zcmeHtWmKF?)@~Er-2*i4(73xh!8Oo82*D+|1Pj4E1lQp176_7{x07>b z=FGiwzx&Nv_xm@!R?}To&wlo_&iJ8sU^>tw{ z2ek=w zs(%i%`|k3`9U16q75OGp3N1(E_TIB=PLerjgz2%R-oEX=uD-4PB&!4KQsQBE+Jc?0 ze%K+Qy}|x&sbDPL$4Tv?$~K22@X3w)+U@nJHm!RsW5kwkJadqB#)U+2*QYbG6rp!- z@Hz^;nM&@SoGQ_4n-^bs7MvF13`-2`B$C`?n>`NKwGVBjb!p57^*>aK8{OQV+io7d zybyXAki6Wze_bC$g}vRhxxagL_L}taRC{9=dv!qjr+Vvkht1{6@~POV)oX>SijP6; z2mUdqjOtmjv!5!?mw%Z3_}J#W`RR6@xM^XOQ`r7IEm+y@7|}=IE?Rp= zxf#1pak$mJL31yap*|l$P9C#bs*Mz9r1UgF!X5Rr{mQ9Hy5z)rZJ30Ft<~t}#w++? z`| z8Y5#ujKp*!r=pozBj;Ip4YBJ<2j@2L!=8M4WL4q-+XBugHk^RS zR?s+%^hXa`)4k`Kf%*4>>le7UIPnkFtIHjJ>m7l)MDA@|vc%Fg>Y*{#mHed);}38T zt8GoM|Q-L{(7j+lvKB|;b_@#GR3kT{wZa7 zLm&#Blr1G6141yjal}-ytx>IfWVS289B$bZ!8OX{Ij5<>872+FzMPcpeDJ`)eXG&q zgt;Ap)70oux+95)c)6dgqCOW;eW{A&GgR{&X~eE;8+>%!PZ`frnE&12Ly@fERXsyn zXm1OP{y_!AY&WCbgvN#KkaPG|or4ZU617=|w8y}oF;*>NAkgmnM=#dl_k=YTbFTPl z67m_dhow0B5)MTf`R_J8<3IFNVtM8`9R6gZU<-WygU))5_&G*7mljD=wb#8Pr0znP zZTe%fOd1e$^6aT%T?~M_yY4HBEG3qtugWn1M-Xb|sT=XKAmf%IZGN?1J z#qMorv*cKI%AMKJdc6y9rr*G#N^VtZN0B{qnS)vo@i@ug)A6CUaqb%j#)`xJN1|q9 zJ+pLvTIyAuqqJPH?CyiXj%C#hDCaCQk0KoB$f|hxqB}j=D$?OXS&guQ6E%kjT^(KW z6uCjfhqPCPb> z^+s}CFD$qP2e*glj8~zbSy?>?S7hMc)o~9qDz@XAxVg;3v$t z6bJ;_A}#L|>GksLSu1rIO`@#KKc_1tJVj+!Pr2B{(=~TCis$1`TLy>7Ktta)>dK7& zIDpVU(02nf_$6gnxDwxH@a7)S=;P{hp5F%wIhX;|JE$9zhQE>i+_J(KESej=9Q)+NJgtC5M?w?MM}EP?pWMfyt|TA>?YGvoZpD4hQW_ihCr;fXn$ z1Pu#W>6Z`x1$P?Cd%!khpy2j zDbl*cA28)bS!GZf^0iG~h0Cb9?+9Yc=LQBDqcp@r;7u0R2=XY^0vUZS);3Ka=JM$a z*eWvN^-y=vFhftR8l3FHRM)@uz81U6tW8!2t|{xp=`i}ycbZamt_I~k zH4ld1#_J9Y5$lM4W1r7Op1?iIramY!ks3niC7H2V)b~%JP*Ol-HTQ$OIrhMQW-{OE}o#4zopE@ z17cM^0e8WfZ#_#nmAWT5Aiq*LAd{{`0+n)_6H;dyXox3NSuuoQV%$wrvk*1oXzuhX z7K4zLbX%+}@>yeY6p6o|7B$*lbvm^ZQ+5_|^`fF8@nk28=<%i|B| zjw=l+{LE!jBO}8|9#2sxs-VW!OuGm8bOE7>WusfgxXYNCGFW^5H-)XjvbH_OzTy@N zUv?}c*((+Ds$RGiOTUM*Q3w=m*T40$X49OA<+I_3yVdPZpwGbhOx8f5ru19l-~qrli8jeZd5CSTnagkQj*8&IJ9Z2jz5 z*b55J5-QZs6=Q?Yywy1&bE31ahX5Vq##2Q|i7%O*<^Cxxi*n(50O#3hQrMalC|@0N zrgKblKRl*}7MbZ|DQh%Zuqs8b`;A%1@qlW247o)T`7&NrsgEupdvL11 z?>%_DB&XGUUI{TYty}njJSX9yj#)UGd$%`+M&x52yB!a-3)3Ap3DGVs%O%FxPu0cn zK^eRgao8D);ERt|@FPt#ov{8`w>5*-PMKntyc?CpmM;z5yKuKCpiGB!(|>P4sS zuWmMJF6?y22J9me`g05zUmf|ZTj+6%g!?UbG2DqU(gYMk8FCgr?4abwGSz}krYI;3 z&)z2Sr>1PZMjve zx`sW6pTiz`p-a%exMIFeg1@xgiGyz^8J6Yh9mA;NmV{VP2*s?ytQ?9sc#Wh?m)5LD z7rCoWj!$^&E*NUyqd|^~K*$aMVdBZx{?G$?2+$j2BovK?8B+)_Z5|sXh#iKOrPP?M z6SPyA`XsxLFe}gKUyq+=^`aMS?5-ohc>SB8Nug)L98bj-j?T4 z@Ye(`Ma)W<=0{+C z?IJ*WU&ob_m zBzGmZrOR{zQs1O8M>>=DMD?8xQ@m_aK%);6omP%{#Tl2m^aDv0QZj0qd-O>ecgp28 ze=uwN3DFIYrECnGiq$Z@N`8%1=Gu!`;9Mgy&Wrk_9+&pCvuEfl!55)!e(O_36byy6 z!TyAVujB^>v@)4V%u}tM5E!ButUUeH-TVB$QI3aP$sN%Z)*w8c32m1%h(Q%;r7Mgd zuE}GoWdkB@hcaOgfJwyyOz6IjBdYXH8~9;~vBoDcMsv?#(&4;{z}AmDr=NaOse$)| zqo4#KQPoj{!Z`Cfm(y{VT(zoN z$+GkNS`1!jy4(Xr321~rSCBr5$t|T$fb6bwLh(|2Bs-~>vt6jQkDwQP=)ejlT(XuT z>9^3EkCo;43_=P+oX~8=R^(psl|}kjF1Y7#DrUQQOoYYs`$4O-pCoLw&lDL*w#}y^ zYRA8?4DL2D!n++OjK{K!31$B*;YB0cW@7_t5&7!Vr6Y$o;7YHtSD>hXwO;<@Jzho7 z*}SOIw_6Q+XTM-y4m2w&98-W_cNmjaHL%}GTTzOgk(YAu&%?G*T#UC7Hs1i;rP_3h z6oi#LNY7V)*)kmbpk)#u~kJ4Qf0(me#T$ zP3}_bVl=WR0nXm<4#GF2*!IeAvlVkc`o4I4WlIqU$8g$7*wmV$9-PAf*lB$FGIxsv zF0z+NRiIT8tB8EKZV6+jcDhh!gl_A=b-3|mP`M%rgVNV2r2kx%a!PG%uI zileAyE6MhG#g%DBq+BK_50tFrc&X|XqQ<; zF?PQb5vhFr`jmE7m-wKDg_x-D30Usvz;{ryKBA7c;oXwWDdLi!KL^odA1$1|!E1GW znb~7`H3*StMC+Y79wNU4I-Q)XF5i+TTO`e_lYht8!Q>buG}Bnm052=mL}H?RjhYK- zPIg54a4yc}cX<^g6c-*_)c&LIIhkZczP$&7I%)QJGRjIc-q%Mud5S z{ruvFJOU2otR2d0-baR9jMV6(Uw^D{!4M>)3%u<--9VnHu_XMOBS z%saQGW4s*6w+G^5JjBBd5D83ha#eokWFVpscB9W}- z8YNaJJ(KK2Ty-ECKS$a>bY@|KYT1G3wcooz0h7z#_U@OaV@k?!N3~%TZvcEEqsa86 zlfz8GH;;N_DL`1~C-%?zk{sxO#w*n6l%zFvW|A@Q7rNwOijYz&;%+B7-p3zy}iP zEdc5#8oE7)JNUKcB;}cMW2>gmrQ&YLYm{Q2^}egWFWt|3iAAI!?!^A&2u*yt>L~)! z6OvLfQdc7a^~sgPrM688bJx#Q_RsX{^@R_-ymND|w+N62sXWUo*)bev4hQ1D4=p6Z zMo)FJirQ1FY9@uE<&6f>{CqFl8|$GaGsT1uq}D zvJa_bX);uE-TqKA(Q(Za0pBH;A~EEGw&~g1;u~4C;guOU9kyna1ksGkK0!af`@FXb z{L>HE%Z2Na*EqKACOv-c*q)* zczk$t1sXuZOinuB4CH)#hgB9}kFw$D>||s#m1Jc8_T(D&ygKV$l9+O@CP75U%dlYE zB7Fj;wgXH~ z#CF?|*+o#N>e}G#^a!g~kIk;Ck3I?zeJvaz@|QB$F&z`hI}2#HAPw9nsowc0G3H3{ zI?tnQsJr!j=K`@v3X4F6#bx6zRV-DnSE_aL52aR$qGa%g!;;T}cW1m!hOr;w+^6)R zYi`5Ll>G4hFBRYKDe`zGxO`1(AwXEt##6?(xMavIZN^P=a}&b3lxxa-taU2aaaO3nAF)}-YJ8TH_#SVem^|uY-^T=k z;CPEP<3OLDeFXrpbM0VH`VG`ng)N<-9AGPF3kZh~)CKlS001B=;o}0fbbxpOEg&{_ zPGU5tpE_uOc2;6E`h032H5VC(t(~Hu8${brUB}YT!BWVIMnW7-)JGUb0EKvffj&@2 zCwE~VF`D1J!m#hZx;bfpzabtDVl)P7nm`$6HwciIgO>xuF6U$C#Z4oQ1{8I(vKH2o zmH(3h))J$!_3&^J=H&GD_U7>B;c#}d;p7q$65<4Lb8>UD!w~H5zD^!sA9g2q+FulZ zaL7X3E#2%~JnWpEfWJ7w7S5g?Vl*_cap0fiuu)4nA6Vy~4t}+N)4O|EaVo(Yys-Je z0G!+)kRUsVo1I&T^RM=>Q8l%{TRXY`sUl2IP9LxfCl?2Z6AJw&3wIAWuYdUamlp0i zupeAFwIJ@!o^F;9IWLHl2kl>-x;T2e|JA3bJLFf_@3hjy-7y7SAE6cy*Ts++ze`BmHIU$Y^C@c_n7&F&D;XUlE|FJ;-G@oB1|3@G& zcYo*qC-mR+`W?$}U4>`4z9Qm9wRtmGJMcTs*vBJ|2D$JFg&!hn?4o z%aYxK*Ghn$pPyTZpU+AFHpu-KDkUd(53rLZWYad8U>^74R$1O)yU0>WU+Kje1@dqJ#zF9A%)KZY!A!A>?1*mnD8t^KXv?qB4w1xS#en+w9t&d<#a z{#6PV>=u>~A$Bf7UVd0z!19j^{Lk#}&ek5@U^j@A4J-#>>kO-{-|Gxy`Xl|A{~7LW z3;C5>AlP0D5`t}kI@~N4e!&=7K#TQn|wjLVZPXDF*e*yf1 zLDkL@;^gl9Uxofx$RD!&MGkh&tw-ord!2ijv|H$=kDe!NB|C3$+-{eC3$FUpY1iSh3h8?Qmj^X8JZyo#?(OwoU=afVSRqQXQaU~h$0iOw1lxo`Hr4`fM*?N=q?-}& zQ&A$6n^A`3aL{z(Lh4!MO3@l_G&Rel?Hpv#iKHjARLav9kiTganZy#xNus=wq-m1u zrYDxBL&bTe#{af$bTr^TdBobr%i5bCp-_BPB$%6L^*&%X>pIJsAAtDZc**I9=BbSP zCXv@<9W_CduQqUIl8lj&%@N??3f@O9k%1(`0ay`0v@e&BfOo2*Zn$w$yL|{K^_8vC zBH@FSu4u=IvkW-8{;!*WC$ww`00cx5>NyH+Jug+FoS}Z)=W|k$Y{s*GouRiRlg2XX2`1vz9ODnNN<2t$Dr+4I&X~|_A`23yG15=PbkVJ8ZBP+ zH~GD0%6K>S^;6S9$`dbV)?9BitSI{YenZ!WA2x)}eyQ;!J;JdbV)dVUZW@^t`nH6; z@ky8X{0pLEZ*mS#YKOiwwW9#&G#*+X<{R}EjE#s`&(FE53UUrmE8+?@er6s}PPROM zfssBZ@Ys%n$Rr7ecGl8##G~57rceJYVz1VjL*>x6<9?yq-AD-~x;JpXy*6laY`*hm z?FXwYZRXtVSR&?5J|J)`&pmY&uaJGsVrv^Q(8h!sfTPh9$}HNcmUcXp&3xKkJ-5Rx zViZIP(0uy+=F_rx>>!GTyU=FV%MAx?uLSIU?RX73*uQ()-WOKYdr;#v_;;vG>Avr*sA*HxB5`)PZU<7xgJttxXVU~HpDK1!U%^?MH>o_rUXuA@ml^8U z!JO)6-(5o)iP)YUXn`Y`2k$X0udJ0Onv@GD;7TkUT zNuy_*Yoh)b_A-BmQ{p~Y}EY^8RaQU#PCsHwq{BwILCB& z_ec}71pUc2M)`(Nj`>|ay!3GUcA6+o{b<|tSxoql6$cMH@rl5!J@?92jMGP3ssau%kZ&6EJV-I9|Go$Iy^NJfbJ&b@?}(ve>;^!~=dtpBCkVi|xvyV$68 ztaOo}O8lkU^VpDFKC|Tz^hO>rR1}V7HH1s8=TxCz3gESA!<*FuoZjawfj@~=>XJgQ zQbIb@%eiL0eOVLl^_4D7x9s6}?BQ=tXj)#BaQY-5!Ql{CMMO0jL`d)Zv@YlhPIe-S zsi!CS)pY5F2>FaZHGqzNLW}Y8%d97w^EcBnpnxkHgI?WR_@zd_Ddo@Eo!vr`Z71QP;G%IZ}E zY8KC@iT$z`rZFY`TBOps*DdRX%yQBN2AjSNM!%)Q0s1LzIVy3F5mk6x_cYFhf@-f{ zb!hsv8>ud7@qTUpaQ|@U*sk35BcAetal`?s(;f5zAi1h=G!LI4x3fLUR@li^>72|V zXKY(O%iA-A>e}YhgU~Hva!Vh9Z_p9AFaG%mtNm@e(TR8^8G9>!p_yLJ_7^ukUQrfd zLjD1SF3DPfEA8%f#q z11-DaKB{!WmpH9ww-jpMqldn=K%u^+k`X&t!G^&K1Iv9hUo4K1x9&50JGT%^$KXT! z72J>{=V))X7ts$k(YOZPeYhSQM0;>c*?AowH%01p)l{JCF(Ha?Fvv}m#&TSO3h(+o z{pJ$JWz^a2M|6YARbGCfcy1rGVFo9@KS}XSr7$!7NAa;~<&?^W4-4Jq(&o}^-S;I# zKy>REarp(6;ADIp$$=ZyYv{M{>jqgH1Z`|M1j;jS@-LaKke*v~DD8j(qA5Lv@;4}( zDl=JaW5mDfw`i0#t_Zkp4(CTQPreuSrEo|^y8;P)&$>;WmBYI`N05DQ`fb1Gwiq2+ zek8I_Z1hDXeh*QSQA9`}m=Me1_!eJJo+CU!x7rsD?RC2mOJ|b0*oWa#^W$ZkxF%cj za~+$_DGQu(GA=s1xmuBXqOGXpl>ZRF;x-Sn;S9wr9=0NP>o_Vgth5``Hhn8iF9cUwxc3sA zYW12P1G&Y$Dzk=hKmhwO!ySCJ*{vTX(%QGH$%dph3OX7=&PP6gnuhcv?{HX%jtvx1nP{)`X4zF=O=ux z=NBK_@0(sAn`O-nyj!&}E-7kyuNR}vbCdn-!^elt=05G&xjyc_egktk%(DFERauo$ zF;wRu6$d&^5f7Ja>NR+!0f$48v%u^}tZ9NB-R%{j#P45n7u>1V;N*EOeaDY$a+}H` z0}kG9B~hY>a%G08oKjhn;J0*SN?g4i=)0Bp=!FyQ`?5`~<9+v*=;zsQ@i&6jHKTOK zqu2@A+HVPCK8luA9Zz!&T~I8CMc!tF$QXHl?z*0yqqFu^iQkC3u2rE)8vi2vexilH z-^T05(AY|#jz}r`<@4rxe*j<&dJ-dG(56*Y@@^n=Y32h@`(#n%!H1u*m-?u|XXxnF zX}TAxdfj_lGCo2KN0@ ziSM2icL&d2^FOow*`6*T(McXN<~QV0trbHZ#IkHK#j7yA*7iJbs&>kGj$VU>ygq`j zSzTgSE?!q<`%SQs7IMwr(RI-*A@jStZ`t8F!XHa!8tcSZQ#(jaQJUt=->Jj|>VE8n z`wV2gG7KhfnccZA8=5fQ59xZ{tsPfb~p;5CFp z?s1C+NM@3IrP?o_?vy7+UybwsbTS@Zip&Tw1Sx}8jr{djZlNvI-K0A|ei|*tPKv0J zKi{jPD%e{{t*;cb$y0X=V&Gkr>lf(jlis}JvAHuW(d_)(p~mve`s>x0tNKKkV=O{a zGVZfTZpb3XLY<3&bS){)h48E>=X2{L(!OE0^Mtlfk;9+|I*X?^sh6gkKQ*Qn<@$~* z-Sz~rym8#fm|0f3iRG_S8Y{=Cx%f9f)fy^(mkpKWYtj)Q&Z(I86~>5& zrhAF`wm#~Ub)Lh$8Oj!jWR)5jJ^vee_omB>vk6Cr++MG0Yv!>qIP=~u^}c88l}dZU ztI?9(?90?RNRn|wlN4}g>6^ILKh4{ee27nan#hkv@Zl%kETe^^ zU||SGull(z$zoE^@kMiXJ)s^UBvd5+=z2rt(hg3S>vKg%50BkU$3r0;V5i4nNpQAR kLm@=^*N446fv86~L;$^^>Eb#I?7#|8l2ezhk~RC{+yBhJfF|=ynLSTn{PFuMl3@TV3hRM*SEZmRXQd4UcyAxS?|S7*CnM+y#b^mxTdUL8_vsYLbHwH`f7`$8~*wFW7CUx2H5;Vljl~7jHQ}VdytvjH z%>kPp7|13xH%7}k^fkEm^w|0E=53sMO`jo%Pj|+_x=EV%DG>AL!f4SO^h`$6!6D+J z3iJ+rKs|!-58Tpgx9IOG_JfQZ>*Cpq93XCuabFyELq-N*U%O|ZYx{I^BR}-j{hiRS zcWK0sC1nnFt=kFrm?s>qf{heQ(yv&XGZ7xSzNRHE;@qduSGiv8=x_r z{}155|DVKIl;pZW2YgocBM+)oW%6 z(KLNhFd*VLpyR-7;$XNsK4OEeLvWbiYf>KB@?F0&m7DLT@-Q^o;Lor^F717r7VR34 zSx*@XHZOzU#K8wL9Q14Sjw5_>_A!`=-SV`{1<^G;$B{Elr55&L@`4&FOsmh1t=C#6 zLkedbtI?=tuWE9WK1@LsSX^4;AQV34JEA zS0s4M3_$l)iyZ~x6tlH4uO8r!ZMPwK2#BFX=`i?(&5z}5+}HZPd&D5bS5z(&*HkE!SIDBtuyZ{O9eAsakRjmdPT-yyQubQ8n=Ob|7RMoFk6Z znz0BT@3Vl%Y>fAY%F*8u^JSQ_VjPJ-M(VJ%vaw0Kk4~kM%AUuN{ix;Z9ZLshsuEwo z{8>fM)+~UofcM&@aM8Yw=oC*zdmeNqC-pu2+E-PbAZ$|E9zC67(pZ_jF`?Xy1-7Bf zFMawkwelN%&@@-KK%dqODEAotl#D*AsHPHnN2uJPgr+eM9Hi2lP@l83ls)+yDjaJB zk?v*1XKdhAx)kNp*k3U5>~haY(^fJS5^<{I;aioU1#e`;BwRxIKaeQaMzz@(-U1J*+(7FUirZLRUDR#|x3Em**J>*^Z*VVVM}taJ!BK zrx1Qh8WtD;##S*czWwdxsvpgdK7t{OG&OT`Kf{!;cN>XCT;6~siK4|r6LOAr2Cgx8KgX4Y3(o6KRJSqnOVuGtg69HrUbDv1_cFY$Yv z4wQ<*7rXzoOt38$B$?iFs6lM%PQSVl@h-{GdCN%9{p=ZTdxnv>Z4w8^_o0U^P4Z#j z#I{~{z8s~ZJ?qxPSnZIA7%f2vBN@EZs~ef8A&X|aKIi&T`BkOf${`{3WZOgfzV_rp zLgCA@KnLf;0I8mK-fHAI^dZv&O2gvu31IpN3qvCw>RO3q8b#z2O3X#M<+4Q&cuEDx z3TZysGLj^*1I>Xws5r77EGyoP33cc~T6o!=yfrq|eoQ!W3WgNec85DrOM*h^SDYlw zk1nZPUt|kLbK;`2)9LUp+-cTdoWw^EcvElGo%G+-H%#?NgK=k*n)JEBPBA<{q~-S{ z%;e>%#)fC}ZH7RX*o$8cvg^LBP!E`H3;58y@G#zbfo`DQEt9rf>BSxhJ0?t$zKJZn zBE6|~jZTh0dN2`F%hM_a!ncE*sE_U^-o&oWef#)u=ewuo{9w64&w1^jLZ@=gj;o2` zX(v4q4?y2%tF|8G`m~_P=hJ=w1rZ$q2ZQUlXDjOVsTS48wm%>HhhlfQ%=XqpQhn(L zug09VS#nt(e~>l#vopAt@I&Ah>bpK|$z^!nvQjO#_OppjoeCF@B3F0glds8QMp$U>84S=EOGZQl;OZ3tXWjA}0g#&{!=~;%nK>ZzT(A5Uk06|T< z@b}9R)C-FN=c1HngSvf7I`@)TaJuxVN;cNkD;jOBOx&yt?p(zHtV3z{97QhEnWXoG zlo^b!Xjb8f(+yzWjc{TpgskPwv&~J)?M%pxNmo!gQ+YD_$n%9~g5vf&<+Pl=ujYgBvPwXQE8ntJcL0XzR^Oj`nz7>+ zx$hJFeRsf)TH&(4H?5cG+mu(n!4f^nYZ{XA7^{jnFs#12GC#gv&uFKr?ottA{PlBQ~P^({`e;k z%pZ|cJ^*A^liOq{KvygLZuqCv`py>)j;vo2YPOq~Tc!^}!S#W5l-3uYu=f=PGz`Ay z+`qg(9DK6kO-wZW#K|8z-JkMKt}J@f=y?eArJ$6e-NF?$=OxD=%@NGynaE9}riXa# zU>OK94LiW1?aBr$)Y&5i3R>;a;Nk_P)YPL?C@_pXl6NFX1kN#s%ZD_?7bzy{Z3%w+ z@HFN4$n*ItO%)1cwmN}IpB5i?f&W7gZ5$OlM_#g$N8F&mpUC5lD;&_lLEq9BnwmLV z4>inqFAg&ceAR{=QRIR|AGcHYimL9eKJnS9&e{YGF`P_{wIvY2xv{Zc%op@0moN$e zz?U1N=ib9~O8UdSotv*l+=91fu52|!lfY6tv-|lVt>#HgtNi=>!jY#Oy${PrPyC|k z+x0I$;5mxWf5D}NY%7br2{BSG^M7-a2CFcCtD@Fj5ISR*pv((SVFXmKntt@G+P;A) z;bv{JLZdXCDrM_gi~r3DPnODD>aMW{#E5=NVLz>w501XmFUd%fdC%^@qACkdVVTRY z+H(~=3-i5V{Lev%21cvUPkwG9h!oJijwdYSn@fBFfxoc8r1!(0258xkmrp#DHfO`T zpCSHKn=r4j6|4MUROQ7Pp}JL`)clEJj(=Xi&j&i_m->!TY6#JnZJ1Nlprm6!Sl+LH zU&HO4yNhl_^WZHQ|EG+jZNh=|~P+i(O({*ey#oZ7B04e)s z0gf~w?gZ8PkssShWka3V!UsGUe?(0?Nl;$9V~;R78Cznv;gOE)Z}jw=gAk>*@IR#kZj5rRDT=z<}S(i{|xW;ucStwH>RA5xz3 z8(k5zHr+BbKJ!{;nJP)1ie8a*Z&nycWiPR0Rn@(8Nafg`f8D47B(svII}F*2Ihpq} z>|vLiHWD_ur4Wb&L}@gW)xIeUQQvF9Yey^Z zi)=OA9V}WYj4U#wUI6W=-pQ+whWlJiNT^|ZJv8e0)jhlPw0!GSOw*c~3fI)5jSSq` zt(TqYo4%-#H~LZrxK$G>6Q>)`vLJtv)VFG!6D%w8@TEUt968@TcvG))lUePQv0sR* zyORq!y!MDxpn~McNg;+lJ^=&fFL#s5KAQE`RuNIYtHis!aK^D)D}-7|z5)apHo`WLu@L`eVu diff --git a/lnbits/static/images/mynodel.png b/lnbits/static/images/mynodel.png index b8afb9ffcd0dbe9c3c07cbd1e007b1067d92caa9..344b54b6db53406258f7f1207b78f72b9fb1cbf3 100644 GIT binary patch delta 5190 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 delta 10444 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 Date: Fri, 30 Sep 2022 10:19:17 -0500 Subject: [PATCH 03/29] Fix formatting --- lnbits/core/templates/core/index.html | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lnbits/core/templates/core/index.html b/lnbits/core/templates/core/index.html index 1319fa1f0..68a7b7ede 100644 --- a/lnbits/core/templates/core/index.html +++ b/lnbits/core/templates/core/index.html @@ -180,9 +180,7 @@ > -
-   -
+
 
From 3aa4d09103ac94b1e3703086f1528da1020f53e3 Mon Sep 17 00:00:00 2001 From: Indra <86242283+VajraOfIndra@users.noreply.github.com> Date: Sun, 2 Oct 2022 16:45:16 +0100 Subject: [PATCH 04/29] Adding missing theme options in conf file Missing bitcoin theme in comment. Missing flamingo theme in list of values for LNBITS_THEME_OPTIONS. --- .env.example | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index 93b823250..7d6de35f4 100644 --- a/.env.example +++ b/.env.example @@ -37,8 +37,8 @@ LNBITS_RESERVE_FEE_PERCENT=1.0 LNBITS_SITE_TITLE="LNbits" LNBITS_SITE_TAGLINE="free and open-source lightning wallet" LNBITS_SITE_DESCRIPTION="Some description about your service, will display if title is not 'LNbits'" -# Choose from mint, flamingo, freedom, salvador, autumn, monochrome, classic -LNBITS_THEME_OPTIONS="classic, bitcoin, freedom, mint, autumn, monochrome, salvador" +# Choose from bitcoin, mint, flamingo, freedom, salvador, autumn, monochrome, classic +LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" # Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet @@ -91,4 +91,4 @@ LNBITS_DENOMINATION=sats # EclairWallet ECLAIR_URL=http://127.0.0.1:8283 -ECLAIR_PASS=eclairpw \ No newline at end of file +ECLAIR_PASS=eclairpw From b6755abc8e9911c34b96d3039140b2b64ff5634f Mon Sep 17 00:00:00 2001 From: HODLmeTight <35168804+TrezorHannes@users.noreply.github.com> Date: Wed, 5 Oct 2022 09:16:54 +0200 Subject: [PATCH 05/29] updated poetry install notes - referenced option for python3.10 and newer - poetry install with --only main, instead of --no-dev (deprecated) --- docs/guide/installation.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 87679ed5b..5e9fdb28a 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -26,8 +26,8 @@ sudo apt install python3.9 python3.9-distutils curl -sSL https://install.python-poetry.org | python3 - export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal -poetry env use python3.9 -poetry install --no-dev +poetry env use python3.9 # you can exchange with python3.10 or newer versions. Identify your version with python3 --version and specify here +poetry install --only main poetry run python build.py mkdir data From 79319faa2146da17cd22467d9dd4762c42540f67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Thu, 6 Oct 2022 10:17:21 +0200 Subject: [PATCH 06/29] make workflows only run on pull_request --- .github/workflows/codeql.yml | 2 -- .github/workflows/formatting.yml | 2 -- .github/workflows/mypy.yml | 2 +- .github/workflows/regtest.yml | 2 +- .github/workflows/tests.yml | 2 +- 5 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 876c8b8af..bbe959837 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,8 +1,6 @@ name: codeql on: - push: - branches: [main, ] pull_request: branches: [main] schedule: diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index e3d0fd35d..21c7fb38e 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,8 +1,6 @@ name: formatting on: - push: - branches: [ main ] pull_request: branches: [ main ] diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index d80da678a..96b574d2d 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,6 +1,6 @@ name: mypy -on: [push, pull_request] +on: [pull_request] jobs: check: diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index 2d7aae6b3..f0adb3ac5 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -1,6 +1,6 @@ name: regtest -on: [push, pull_request] +on: [pull_request] jobs: LndRestWallet: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 5d368fbb1..c7b6e44bb 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: tests -on: [push, pull_request] +on: [pull_request] jobs: venv-sqlite: From 63e4f7d59c0505654d47b8793738211966b9b776 Mon Sep 17 00:00:00 2001 From: HODLmeTight <35168804+TrezorHannes@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:25:14 +0200 Subject: [PATCH 07/29] changed comment places and small adjustments - #comments in own lines - cleaned up some ominous settings - added --debug option for running the server --- docs/guide/installation.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 5e9fdb28a..37104fde2 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -18,21 +18,25 @@ If you have problems installing LNbits using these instructions, please have a l git clone https://github.com/lnbits/lnbits-legend.git cd lnbits-legend/ -# for making sure python 3.9 is installed, skip if installed +# for making sure python 3.9 is installed, skip if installed. To check your installed version: python3 --version sudo apt update sudo apt install software-properties-common sudo add-apt-repository ppa:deadsnakes/ppa sudo apt install python3.9 python3.9-distutils curl -sSL https://install.python-poetry.org | python3 - -export PATH="/home/ubuntu/.local/bin:$PATH" # or whatever is suggested in the poetry install notes printed to terminal -poetry env use python3.9 # you can exchange with python3.10 or newer versions. Identify your version with python3 --version and specify here +# Once the above poetry install is completed, use the installation path printed to terminal and replace in the following command +export PATH="/home/user/.local/bin:$PATH" +# Next command, you can exchange with python3.10 or newer versions. +# Identify your version with python3 --version and specify in the next line +# command is only needed when your default python is not ^3.9 or ^3.10 +poetry env use python3.9 poetry install --only main -poetry run python build.py mkdir data cp .env.example .env -nano .env # set funding source +# set funding source amongst other options +nano .env ``` #### Running the server @@ -40,6 +44,7 @@ nano .env # set funding source ```sh poetry run lnbits # To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0' +# add --debug for slightly more verbose output ``` ## Option 2: Nix From 36b60ad318fffa87d8ec55753538525531d76dec Mon Sep 17 00:00:00 2001 From: HODLmeTight <35168804+TrezorHannes@users.noreply.github.com> Date: Thu, 6 Oct 2022 14:50:33 +0200 Subject: [PATCH 08/29] added debug option for .env --- docs/guide/installation.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 37104fde2..6b95f93b1 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -44,7 +44,8 @@ nano .env ```sh poetry run lnbits # To change port/host pass 'poetry run lnbits --port 9000 --host 0.0.0.0' -# add --debug for slightly more verbose output +# adding --debug in the start-up command above to help your troubleshooting and generate a more verbose output +# Note that you have to add the line DEBUG=true in your .env file, too. ``` ## Option 2: Nix From 27bae6a0c6cce731256095465905c92c1547c1e2 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 14:16:21 +0100 Subject: [PATCH 09/29] adds atm check, so withdraw isnt cancelled on false call by wallets --- lnbits/extensions/lnurldevice/lnurl.py | 29 +++++++++++++++----------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index df0cd4b8f..ec7f3b486 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -184,22 +184,27 @@ async def lnurl_callback( raise HTTPException( status_code=HTTPStatus.FORBIDDEN, detail="lnurldevice not found." ) - if pr: - if lnurldevicepayment.id != k1: - return {"status": "ERROR", "reason": "Bad K1"} - if lnurldevicepayment.payhash != "payment_hash": - return {"status": "ERROR", "reason": f"Payment already claimed"} + if device.device == "atm": + if not pr: + raise HTTPException( + status_code=HTTPStatus.FORBIDDEN, detail="No payment request" + ) + else: + if lnurldevicepayment.id != k1: + return {"status": "ERROR", "reason": "Bad K1"} + if lnurldevicepayment.payhash != "payment_hash": + return {"status": "ERROR", "reason": f"Payment already claimed"} lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=lnurldevicepayment.payload ) - await pay_invoice( - wallet_id=device.wallet, - payment_request=pr, - max_sat=lnurldevicepayment.sats / 1000, - extra={"tag": "withdraw"}, - ) - return {"status": "OK"} + await pay_invoice( + wallet_id=device.wallet, + payment_request=pr, + max_sat=lnurldevicepayment.sats / 1000, + extra={"tag": "withdraw"}, + ) + return {"status": "OK"} payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, From 8e8bf08ea587aa9013a966a807600812fee3c780 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:21:09 +0100 Subject: [PATCH 10/29] fix issue with splitting to multiple wallets-queue --- lnbits/extensions/splitpayments/tasks.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/splitpayments/tasks.py b/lnbits/extensions/splitpayments/tasks.py index b7cf1750c..cfc6c226f 100644 --- a/lnbits/extensions/splitpayments/tasks.py +++ b/lnbits/extensions/splitpayments/tasks.py @@ -28,6 +28,10 @@ async def on_invoice_paid(payment: Payment) -> None: # now we make some special internal transfers (from no one to the receiver) targets = await get_targets(payment.wallet_id) + + if not targets: + return + transfers = [ (target.wallet, int(target.percent * payment.amount / 100)) for target in targets @@ -41,9 +45,6 @@ async def on_invoice_paid(payment: Payment) -> None: ) return - if not targets: - return - # mark the original payment with one extra key, "splitted" # (this prevents us from doing this process again and it's informative) # and reduce it by the amount we're going to send to the producer @@ -76,5 +77,5 @@ async def on_invoice_paid(payment: Payment) -> None: ) # manually send this for now - await internal_invoice_queue.put(internal_checking_id) + await internal_invoice_queue.put(internal_checking_id) return From 11ec82e33962462880d08a9dc2ab6e828762edbd Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:21:53 +0100 Subject: [PATCH 11/29] floor amount when not whole sat --- lnbits/extensions/scrub/tasks.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py index 320d34da0..62adc5e5e 100644 --- a/lnbits/extensions/scrub/tasks.py +++ b/lnbits/extensions/scrub/tasks.py @@ -1,6 +1,7 @@ import asyncio import json from http import HTTPStatus +from math import floor from urllib.parse import urlparse import httpx @@ -26,7 +27,7 @@ async def wait_for_paid_invoices(): async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) - if "scrubed" == payment.extra.get("tag"): + if payment.extra.get("tag") == "scrubed": # already scrubbed return @@ -42,12 +43,13 @@ async def on_invoice_paid(payment: Payment) -> None: # I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267 domain = urlparse(data["callback"]).netloc - + rounded_amount = floor(payment.amount / 1000) * 1000 + async with httpx.AsyncClient() as client: try: r = await client.get( data["callback"], - params={"amount": payment.amount}, + params={"amount": rounded_amount}, timeout=40, ) if r.is_error: @@ -66,7 +68,8 @@ async def on_invoice_paid(payment: Payment) -> None: ) invoice = bolt11.decode(params["pr"]) - if invoice.amount_msat != payment.amount: + + if invoice.amount_msat != rounded_amount: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, detail=f"{domain} returned an invalid invoice. Expected {payment.amount} msat, got {invoice.amount_msat}.", From 7316ef7dbb67a9327597e2e97aa7ef54f7a08ac0 Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:33:20 +0100 Subject: [PATCH 12/29] add disclaimer about whole (integer) sats --- lnbits/extensions/scrub/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lnbits/extensions/scrub/README.md b/lnbits/extensions/scrub/README.md index 680c5e6db..3b8d0b2d7 100644 --- a/lnbits/extensions/scrub/README.md +++ b/lnbits/extensions/scrub/README.md @@ -4,6 +4,8 @@ SCRUB is a small but handy extension that allows a user to take advantage of all the functionalities inside **LNbits** and upon a payment received to your LNbits wallet, automatically forward it to your desired wallet via LNURL or LNAddress! +Only whole values, integers, are Scrubbed, amounts will be rounded down (example: 6.3 will be 6)! The decimals, if existing, will be kept in your wallet! + [**Wallets supporting LNURL**](https://github.com/fiatjaf/awesome-lnurl#wallets) ## Usage From 92a756b9bb92078bb937f4a22c9b35b033f44fed Mon Sep 17 00:00:00 2001 From: Tiago vasconcelos Date: Thu, 6 Oct 2022 15:34:37 +0100 Subject: [PATCH 13/29] make format --- lnbits/extensions/scrub/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/scrub/tasks.py b/lnbits/extensions/scrub/tasks.py index 62adc5e5e..852f3860c 100644 --- a/lnbits/extensions/scrub/tasks.py +++ b/lnbits/extensions/scrub/tasks.py @@ -44,7 +44,7 @@ async def on_invoice_paid(payment: Payment) -> None: # I REALLY HATE THIS DUPLICATION OF CODE!! CORE/VIEWS/API.PY, LINE 267 domain = urlparse(data["callback"]).netloc rounded_amount = floor(payment.amount / 1000) * 1000 - + async with httpx.AsyncClient() as client: try: r = await client.get( @@ -68,7 +68,7 @@ async def on_invoice_paid(payment: Payment) -> None: ) invoice = bolt11.decode(params["pr"]) - + if invoice.amount_msat != rounded_amount: raise HTTPException( status_code=HTTPStatus.BAD_REQUEST, From 3222ae198d4f4bda826f1a63f861d5e90d0df4ac Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 17:10:15 +0100 Subject: [PATCH 14/29] Adding bitcoinswitch to lnurldevices --- lnbits/extensions/lnurldevice/__init__.py | 7 +++- lnbits/extensions/lnurldevice/lnurl.py | 40 ++++++++++++++++-- lnbits/extensions/lnurldevice/migrations.py | 6 +++ lnbits/extensions/lnurldevice/models.py | 2 + lnbits/extensions/lnurldevice/tasks.py | 39 ++++++++++++++++++ lnbits/extensions/lnurldevice/views.py | 45 +++++++++++++++++++++ 6 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 lnbits/extensions/lnurldevice/tasks.py diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index 54849c957..dc4456b4d 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -2,6 +2,7 @@ from fastapi import APIRouter from lnbits.db import Database from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_lnurldevice") @@ -11,7 +12,11 @@ lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice def lnurldevice_renderer(): return template_renderer(["lnbits/extensions/lnurldevice/templates"]) - +from .tasks import wait_for_paid_invoices from .lnurl import * # noqa from .views import * # noqa from .views_api import * # noqa + +def lnurldevice_start(): + loop = asyncio.get_event_loop() + loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index ec7f3b486..a2bc0dd4f 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -102,7 +102,22 @@ async def lnurl_v1_params( if device.device == "atm": if paymentcheck: return {"status": "ERROR", "reason": f"Payment already claimed"} - + if device.device == "switch": + lnurldevicepayment = await create_lnurldevicepayment( + deviceid=device.id, + sats=device.profit, + ) + if not lnurldevicepayment: + return {"status": "ERROR", "reason": "Could not create payment."} + return { + "tag": "payRequest", + "callback": request.url_for( + "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id + ), + "minSendable": device.profit * 1000, + "maxSendable": device.profit * 1000, + "metadata": await device.lnurlpay_metadata(), + } if len(p) % 4 > 0: p += "=" * (4 - (len(p) % 4)) @@ -205,6 +220,27 @@ async def lnurl_callback( extra={"tag": "withdraw"}, ) return {"status": "OK"} + if device.device == "switch": + payment_hash, payment_request = await create_invoice( + wallet_id=device.wallet, + amount=lnurldevicepayment.sats / 1000, + memo=device.title, + unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), + extra={"tag": "Switch", "id": device.paymentid, "time": device.amount}, + ) + lnurldevicepayment = await update_lnurldevicepayment( + lnurldevicepayment_id=paymentid, payhash=payment_hash + ) + + return { + "pr": payment_request, + "successAction": { + "tag": "url", + "description": "Check the attached link", + "url": request.url_for("lnurldevice.displaypin", paymentid=paymentid), + }, + "routes": [], + } payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, @@ -226,5 +262,3 @@ async def lnurl_callback( }, "routes": [], } - - return resp.dict() diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index c7899282c..65913b023 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -79,3 +79,9 @@ async def m002_redux(db): ) except: return + +async def m003_redux(db): + """ + Add 'meta' for storing various metadata about the wallet + """ + await db.execute("ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;") \ No newline at end of file diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index fef0aec1e..cf6171419 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -17,6 +17,7 @@ class createLnurldevice(BaseModel): currency: str device: str profit: float + amount: int class lnurldevices(BaseModel): @@ -27,6 +28,7 @@ class lnurldevices(BaseModel): currency: str device: str profit: float + amount: int timestamp: str def from_row(cls, row: Row) -> "lnurldevices": diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py new file mode 100644 index 000000000..2337c0ad1 --- /dev/null +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -0,0 +1,39 @@ +import asyncio +import json +from http import HTTPStatus +from urllib.parse import urlparse + +import httpx +from fastapi import HTTPException + +from lnbits import bolt11 +from lnbits.core.models import Payment +from lnbits.core.services import pay_invoice +from lnbits.helpers import get_current_extension_name +from lnbits.tasks import register_invoice_listener + +from .crud import get_lnurldevice +from .views import updater + +async def wait_for_paid_invoices(): + invoice_queue = asyncio.Queue() + register_invoice_listener(invoice_queue, get_current_extension_name()) + + while True: + payment = await invoice_queue.get() + await on_invoice_paid(payment) + + +async def on_invoice_paid(payment: Payment) -> None: + # (avoid loops) + if "switch" == payment.extra.get("tag"): + lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id")) + if not lnurldevicepayment: + return + if lnurldevicepayment.payhash == "used": + return + lnurldevicepayment = await update_lnurldevicepayment( + lnurldevicepayment_id=paymentid, payhash="used" + ) + return await updater(lnurldevicepayment.deviceid) + return \ No newline at end of file diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 3389e17c7..4a28a5c1a 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -51,3 +51,48 @@ async def displaypin(request: Request, paymentid: str = Query(None)): "lnurldevice/error.html", {"request": request, "pin": "filler", "not_paid": True}, ) + + +##################WEBSOCKET ROUTES######################## + + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket, lnurldevice_id: str): + await websocket.accept() + websocket.id = lnurldevice_id + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, lnurldevice_id: str): + for connection in self.active_connections: + if connection.id == lnurldevice_id: + await connection.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@lnurldevice_ext.websocket("/ws/{lnurldevice_id}", name="lnurldevice.lnurldevice_by_id") +async def websocket_endpoint(websocket: WebSocket, lnurldevice_id: str): + await manager.connect(websocket, lnurldevice_id) + try: + while True: + data = await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) + + +async def updater(lnurldevice_id): + lnurldevice = await get_lnurldevice(lnurldevice_id) + if not lnurldevice: + return + await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) \ No newline at end of file From 05c8f02cb7cc714457e885c2c9002f6cc97c3074 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 17:41:06 +0100 Subject: [PATCH 15/29] Added code sample for websocket stuff --- docs/devs/websockets.md | 87 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/devs/websockets.md diff --git a/docs/devs/websockets.md b/docs/devs/websockets.md new file mode 100644 index 000000000..34cf940de --- /dev/null +++ b/docs/devs/websockets.md @@ -0,0 +1,87 @@ +--- +layout: default +parent: For developers +title: Websockets +nav_order: 2 +--- + + +Websockets +================= + +`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from teh copilot extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension): + + +```sh +from fastapi import Request, WebSocket, WebSocketDisconnect + +class ConnectionManager: + def __init__(self): + self.active_connections: List[WebSocket] = [] + + async def connect(self, websocket: WebSocket, extension_id: str): + await websocket.accept() + websocket.id = extension_id + self.active_connections.append(websocket) + + def disconnect(self, websocket: WebSocket): + self.active_connections.remove(websocket) + + async def send_personal_message(self, message: str, extension_id: str): + for connection in self.active_connections: + if connection.id == extension_id: + await connection.send_text(message) + + async def broadcast(self, message: str): + for connection in self.active_connections: + await connection.send_text(message) + + +manager = ConnectionManager() + + +@extension_ext.websocket("/ws/{extension_id}", name="extension.websocket_by_id") +async def websocket_endpoint(websocket: WebSocket, extension_id: str): + await manager.connect(websocket, extension_id) + try: + while True: + data = await websocket.receive_text() + except WebSocketDisconnect: + manager.disconnect(websocket) + + +async def updater(extension_id, data): + extension = await get_extension(extension_id) + if not extension: + return + await manager.send_personal_message(f"{data}", extension_id) +``` + +Example vue-js function for listening to the websocket: + +``` +initWs: async function () { + if (location.protocol !== 'http:') { + localUrl = + 'wss://' + + document.domain + + ':' + + location.port + + '/extension/ws/' + + self.extension.id + } else { + localUrl = + 'ws://' + + document.domain + + ':' + + location.port + + '/extension/ws/' + + self.extension.id + } + this.ws = new WebSocket(localUrl) + this.ws.addEventListener('message', async ({data}) => { + const res = JSON.parse(data.toString()) + console.log(res) + }) +}, +``` \ No newline at end of file From 6d55445a1bb941ed2b13b290cb4d8994ef0a52ba Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:44:04 +0100 Subject: [PATCH 16/29] Update websockets.md --- docs/devs/websockets.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/devs/websockets.md b/docs/devs/websockets.md index 34cf940de..0638e4f28 100644 --- a/docs/devs/websockets.md +++ b/docs/devs/websockets.md @@ -9,7 +9,7 @@ nav_order: 2 Websockets ================= -`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from teh copilot extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension): +`websockets` are a great way to add a two way instant data channel between server and client. This example was taken from the `copilot` extension, we create a websocket endpoint which can be restricted by `id`, then can feed it data to broadcast to any client on the socket using the `updater(extension_id, data)` function (`extension` has been used in place of an extension name, wreplace to your own extension): ```sh @@ -84,4 +84,4 @@ initWs: async function () { console.log(res) }) }, -``` \ No newline at end of file +``` From b46c06012d1e4e280eb9a8d3459276489a935b62 Mon Sep 17 00:00:00 2001 From: Arc <33088785+arcbtc@users.noreply.github.com> Date: Thu, 6 Oct 2022 17:51:36 +0100 Subject: [PATCH 17/29] Revert "make gh workflows only run on pull_request" --- .github/workflows/codeql.yml | 2 ++ .github/workflows/formatting.yml | 2 ++ .github/workflows/mypy.yml | 2 +- .github/workflows/regtest.yml | 2 +- .github/workflows/tests.yml | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index bbe959837..876c8b8af 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -1,6 +1,8 @@ name: codeql on: + push: + branches: [main, ] pull_request: branches: [main] schedule: diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 21c7fb38e..e3d0fd35d 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -1,6 +1,8 @@ name: formatting on: + push: + branches: [ main ] pull_request: branches: [ main ] diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml index 96b574d2d..d80da678a 100644 --- a/.github/workflows/mypy.yml +++ b/.github/workflows/mypy.yml @@ -1,6 +1,6 @@ name: mypy -on: [pull_request] +on: [push, pull_request] jobs: check: diff --git a/.github/workflows/regtest.yml b/.github/workflows/regtest.yml index f0adb3ac5..2d7aae6b3 100644 --- a/.github/workflows/regtest.yml +++ b/.github/workflows/regtest.yml @@ -1,6 +1,6 @@ name: regtest -on: [pull_request] +on: [push, pull_request] jobs: LndRestWallet: diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index c7b6e44bb..5d368fbb1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,6 +1,6 @@ name: tests -on: [pull_request] +on: [push, pull_request] jobs: venv-sqlite: From ff98b87eca0ab2309ccc1b1be33969aaeb96b035 Mon Sep 17 00:00:00 2001 From: ben Date: Thu, 6 Oct 2022 22:19:30 +0100 Subject: [PATCH 18/29] Added a switch --- lnbits/extensions/lnurldevice/__init__.py | 1 + lnbits/extensions/lnurldevice/models.py | 2 +- .../templates/lnurldevice/_api_docs.html | 8 +-- .../templates/lnurldevice/index.html | 66 ++++++++++++++++++- lnbits/extensions/lnurldevice/views.py | 32 ++++++++- 5 files changed, 102 insertions(+), 7 deletions(-) diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index dc4456b4d..5452e3562 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -1,3 +1,4 @@ +import asyncio from fastapi import APIRouter from lnbits.db import Database diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index cf6171419..0714c656d 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -36,7 +36,7 @@ class lnurldevices(BaseModel): def lnurl(self, req: Request) -> Lnurl: url = req.url_for( - "lnurldevice.lnurl_response", device_id=self.id, _external=True + "lnurldevice.lnurl_v1_params", device_id=self.id ) return lnurl_encode(url) diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index 7f9afa27b..7497714e1 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -1,10 +1,10 @@

- Register LNURLDevice devices to receive payments in your LNbits wallet.
- Build your own here - https://github.com/arcbtc/bitcoinpos + Such as the LNPoS + https://lnbits.github.io/lnpos
Created by, Ben Arc + LNURLDevice Settings + + + bitcoinSwitch embeddable QRCode + +

LNURLDevice device string
+
{% raw + %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{% endraw + %} Click to copy URL + + Click to copy URL - +
@@ -191,6 +221,7 @@ label="Type of device" > +
+ + +
{ diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 4a28a5c1a..a64de84b8 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -1,4 +1,6 @@ from http import HTTPStatus +import pyqrcode +from io import BytesIO from fastapi import Request from fastapi.param_functions import Query @@ -14,6 +16,9 @@ from lnbits.decorators import check_user_exists from . import lnurldevice_ext, lnurldevice_renderer from .crud import get_lnurldevice, get_lnurldevicepayment +from fastapi import Request, WebSocket, WebSocketDisconnect + +from starlette.responses import HTMLResponse, StreamingResponse templates = Jinja2Templates(directory="templates") @@ -52,6 +57,30 @@ async def displaypin(request: Request, paymentid: str = Query(None)): {"request": request, "pin": "filler", "not_paid": True}, ) +@lnurldevice_ext.get("/img/{lnurldevice_id}", response_class=StreamingResponse) +async def img(request: Request, lnurldevice_id): + lnurldevice = await get_lnurldevice(lnurldevice_id) + if not lnurldevice: + raise HTTPException( + status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." + ) + qr = pyqrcode.create(lnurldevice.lnurl(request)) + stream = BytesIO() + qr.svg(stream, scale=3) + stream.seek(0) + + async def _generator(stream: BytesIO): + yield stream.getvalue() + + return StreamingResponse( + _generator(stream), + headers={ + "Content-Type": "image/svg+xml", + "Cache-Control": "no-cache, no-store, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, + ) ##################WEBSOCKET ROUTES######################## @@ -95,4 +124,5 @@ async def updater(lnurldevice_id): lnurldevice = await get_lnurldevice(lnurldevice_id) if not lnurldevice: return - await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) \ No newline at end of file + await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) + From 3bf38884ed8cb30ef9b5c428fdaea1acbedf1020 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?dni=20=E2=9A=A1?= Date: Fri, 7 Oct 2022 08:30:07 +0200 Subject: [PATCH 19/29] log successful connection to backend with logger.success() --- lnbits/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/app.py b/lnbits/app.py index 514825380..8b9cf7985 100644 --- a/lnbits/app.py +++ b/lnbits/app.py @@ -126,7 +126,7 @@ def check_funding_source(app: FastAPI) -> None: logger.info("Retrying connection to backend in 5 seconds...") await asyncio.sleep(5) signal.signal(signal.SIGINT, original_sigint_handler) - logger.info( + logger.success( f"✔️ Backend {WALLET.__class__.__name__} connected and with a balance of {balance} msat." ) From 52dc528a8a7557da685da624e22058e737e49a16 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 11:29:06 +0100 Subject: [PATCH 20/29] Added qrdialogue --- .../templates/lnurldevice/index.html | 61 +++++++++++++++---- lnbits/extensions/lnurldevice/views.py | 19 +----- lnbits/extensions/lnurldevice/views_api.py | 13 +++- 3 files changed, 61 insertions(+), 32 deletions(-) diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index 528e5afe6..b55c83f76 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -94,18 +94,17 @@ - bitcoinSwitch embeddable QRCode - + v-if="props.row.device == 'switch'" + :disable="protocol == 'http:'" + flat + unelevated + dense + size="xs" + icon="visibility" + :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" + @click="openQrCodeDialog(props.row.id)" + > LNURLs only work over HTTPS view LNURL + + + + + + {% raw %} + +

+ ID: {{ qrCodeDialog.data.id }}
+ +

+ {% endraw %} +
+ Copy LNURL + Close +
+
+
{% endblock %} {% block scripts %} {{ window_vars(user) }} @@ -306,6 +333,7 @@ mixins: [windowMixin], data: function () { return { + protocol: window.location.protocol, location: window.location.hostname, wslocation: window.location.hostname, filter: '', @@ -404,6 +432,14 @@ } }, methods: { + openQrCodeDialog: function (lnurldevice_id) { + var lnurldevice = _.findWhere(this.lnurldeviceLinks, {id: lnurldevice_id}) + console.log(lnurldevice) + this.qrCodeDialog.data = _.clone(lnurldevice) + this.qrCodeDialog.data.url = + window.location.protocol + '//' + window.location.host + this.qrCodeDialog.show = true + }, cancellnurldevice: function (data) { var self = this self.formDialoglnurldevice.show = false @@ -460,6 +496,7 @@ .then(function (response) { if (response.data) { self.lnurldeviceLinks = response.data.map(maplnurldevice) + console.log(response.data) } }) .catch(function (error) { diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index a64de84b8..26f581d36 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -64,23 +64,8 @@ async def img(request: Request, lnurldevice_id): raise HTTPException( status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." ) - qr = pyqrcode.create(lnurldevice.lnurl(request)) - stream = BytesIO() - qr.svg(stream, scale=3) - stream.seek(0) - - async def _generator(stream: BytesIO): - yield stream.getvalue() - - return StreamingResponse( - _generator(stream), - headers={ - "Content-Type": "image/svg+xml", - "Cache-Control": "no-cache, no-store, must-revalidate", - "Pragma": "no-cache", - "Expires": "0", - }, - ) + return lnurldevice.lnurl(request) + ##################WEBSOCKET ROUTES######################## diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py index d152d210c..4a4c2b905 100644 --- a/lnbits/extensions/lnurldevice/views_api.py +++ b/lnbits/extensions/lnurldevice/views_api.py @@ -45,14 +45,21 @@ async def api_lnurldevice_create_or_update( @lnurldevice_ext.get("/api/v1/lnurlpos") -async def api_lnurldevices_retrieve(wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_lnurldevices_retrieve(req: Request, wallet: WalletTypeInfo = Depends(get_key_type)): wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids try: return [ - {**lnurldevice.dict()} for lnurldevice in await get_lnurldevices(wallet_ids) + {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + for lnurldevice in await get_lnurldevices(wallet_ids) ] except: - return "" + try: + return [ + {**lnurldevice.dict()} + for lnurldevice in await get_lnurldevices(wallet_ids) + ] + except: + return "" @lnurldevice_ext.get("/api/v1/lnurlpos/{lnurldevice_id}") From 7711387d6472171539493c2f830869edef1e49fd Mon Sep 17 00:00:00 2001 From: Vlad Stan Date: Fri, 7 Oct 2022 15:23:57 +0300 Subject: [PATCH 21/29] fix: use bigint for amounts in postgres (#1030) --- lnbits/db.py | 6 ++++++ lnbits/extensions/boltcards/migrations.py | 8 ++++---- lnbits/extensions/boltz/migrations.py | 12 ++++++------ lnbits/extensions/invoices/migrations.py | 2 +- lnbits/extensions/lnurldevice/migrations.py | 2 +- lnbits/extensions/lnurlpayout/migrations.py | 4 ++-- lnbits/extensions/streamalerts/migrations.py | 2 +- lnbits/extensions/tipjar/migrations.py | 4 ++-- 8 files changed, 23 insertions(+), 17 deletions(-) diff --git a/lnbits/db.py b/lnbits/db.py index 669817842..f52b03914 100644 --- a/lnbits/db.py +++ b/lnbits/db.py @@ -52,6 +52,12 @@ class Compat: return "" return "" + @property + def big_int(self) -> str: + if self.type in {POSTGRES}: + return "BIGINT" + return "INT" + class Connection(Compat): def __init__(self, conn: AsyncConnection, txn, typ, name, schema): diff --git a/lnbits/extensions/boltcards/migrations.py b/lnbits/extensions/boltcards/migrations.py index 081260139..9609e0c37 100644 --- a/lnbits/extensions/boltcards/migrations.py +++ b/lnbits/extensions/boltcards/migrations.py @@ -29,7 +29,7 @@ async def m001_initial(db): ) await db.execute( - """ + f""" CREATE TABLE boltcards.hits ( id TEXT PRIMARY KEY UNIQUE, card_id TEXT NOT NULL, @@ -38,7 +38,7 @@ async def m001_initial(db): useragent TEXT, old_ctr INT NOT NULL DEFAULT 0, new_ctr INT NOT NULL DEFAULT 0, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ @@ -47,11 +47,11 @@ async def m001_initial(db): ) await db.execute( - """ + f""" CREATE TABLE boltcards.refunds ( id TEXT PRIMARY KEY UNIQUE, hit_id TEXT NOT NULL, - refund_amount INT NOT NULL, + refund_amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ diff --git a/lnbits/extensions/boltz/migrations.py b/lnbits/extensions/boltz/migrations.py index e4026dd04..925322ecd 100644 --- a/lnbits/extensions/boltz/migrations.py +++ b/lnbits/extensions/boltz/migrations.py @@ -1,16 +1,16 @@ async def m001_initial(db): await db.execute( - """ + f""" CREATE TABLE boltz.submarineswap ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, payment_hash TEXT NOT NULL, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, status TEXT NOT NULL, boltz_id TEXT NOT NULL, refund_address TEXT NOT NULL, refund_privkey TEXT NOT NULL, - expected_amount INT NOT NULL, + expected_amount {db.big_int} NOT NULL, timeout_block_height INT NOT NULL, address TEXT NOT NULL, bip21 TEXT NOT NULL, @@ -22,12 +22,12 @@ async def m001_initial(db): """ ) await db.execute( - """ + f""" CREATE TABLE boltz.reverse_submarineswap ( id TEXT PRIMARY KEY, wallet TEXT NOT NULL, onchain_address TEXT NOT NULL, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, instant_settlement BOOLEAN NOT NULL, status TEXT NOT NULL, boltz_id TEXT NOT NULL, @@ -37,7 +37,7 @@ async def m001_initial(db): claim_privkey TEXT NOT NULL, lockup_address TEXT NOT NULL, invoice TEXT NOT NULL, - onchain_amount INT NOT NULL, + onchain_amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT """ + db.timestamp_now + """ diff --git a/lnbits/extensions/invoices/migrations.py b/lnbits/extensions/invoices/migrations.py index c47a954ae..74a0fdbad 100644 --- a/lnbits/extensions/invoices/migrations.py +++ b/lnbits/extensions/invoices/migrations.py @@ -45,7 +45,7 @@ async def m001_initial_invoices(db): id TEXT PRIMARY KEY, invoice_id TEXT NOT NULL, - amount INT NOT NULL, + amount {db.big_int} NOT NULL, time TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index c7899282c..da62cb7e6 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -29,7 +29,7 @@ async def m001_initial(db): payhash TEXT, payload TEXT NOT NULL, pin INT, - sats INT, + sats {db.big_int}, timestamp TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} ); """ diff --git a/lnbits/extensions/lnurlpayout/migrations.py b/lnbits/extensions/lnurlpayout/migrations.py index 6af047910..7a45e4953 100644 --- a/lnbits/extensions/lnurlpayout/migrations.py +++ b/lnbits/extensions/lnurlpayout/migrations.py @@ -3,14 +3,14 @@ async def m001_initial(db): Initial lnurlpayouts table. """ await db.execute( - """ + f""" CREATE TABLE lnurlpayout.lnurlpayouts ( id TEXT PRIMARY KEY, title TEXT NOT NULL, wallet TEXT NOT NULL, admin_key TEXT NOT NULL, lnurlpay TEXT NOT NULL, - threshold INT NOT NULL + threshold {db.big_int} NOT NULL ); """ ) diff --git a/lnbits/extensions/streamalerts/migrations.py b/lnbits/extensions/streamalerts/migrations.py index 1b0cea375..7d50e8f11 100644 --- a/lnbits/extensions/streamalerts/migrations.py +++ b/lnbits/extensions/streamalerts/migrations.py @@ -25,7 +25,7 @@ async def m001_initial(db): name TEXT NOT NULL, message TEXT NOT NULL, cur_code TEXT NOT NULL, - sats INT NOT NULL, + sats {db.big_int} NOT NULL, amount FLOAT NOT NULL, service INTEGER NOT NULL, posted BOOLEAN NOT NULL, diff --git a/lnbits/extensions/tipjar/migrations.py b/lnbits/extensions/tipjar/migrations.py index 6b58fbca2..d8f6da3f0 100644 --- a/lnbits/extensions/tipjar/migrations.py +++ b/lnbits/extensions/tipjar/migrations.py @@ -19,8 +19,8 @@ async def m001_initial(db): wallet TEXT NOT NULL, name TEXT NOT NULL, message TEXT NOT NULL, - sats INT NOT NULL, - tipjar INT NOT NULL, + sats {db.big_int} NOT NULL, + tipjar {db.big_int} NOT NULL, FOREIGN KEY(tipjar) REFERENCES {db.references_schema}TipJars(id) ); """ From 8d4337679f93161966310ccf56d30eb4fd576317 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 13:46:40 +0100 Subject: [PATCH 22/29] Websocket/listener working --- lnbits/extensions/lnurldevice/crud.py | 6 +++-- lnbits/extensions/lnurldevice/lnurl.py | 26 +++++++++++-------- lnbits/extensions/lnurldevice/tasks.py | 8 +++--- .../templates/lnurldevice/index.html | 3 ++- lnbits/extensions/lnurldevice/views_api.py | 9 ++++--- 5 files changed, 30 insertions(+), 22 deletions(-) diff --git a/lnbits/extensions/lnurldevice/crud.py b/lnbits/extensions/lnurldevice/crud.py index 451665212..4c25e4cb4 100644 --- a/lnbits/extensions/lnurldevice/crud.py +++ b/lnbits/extensions/lnurldevice/crud.py @@ -22,9 +22,10 @@ async def create_lnurldevice( wallet, currency, device, - profit + profit, + amount ) - VALUES (?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, ( lnurldevice_id, @@ -34,6 +35,7 @@ async def create_lnurldevice( data.currency, data.device, data.profit, + data.amount, ), ) return await get_lnurldevice(lnurldevice_id) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index a2bc0dd4f..3823005ea 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -103,9 +103,19 @@ async def lnurl_v1_params( if paymentcheck: return {"status": "ERROR", "reason": f"Payment already claimed"} if device.device == "switch": + + price_msat = ( + await fiat_amount_as_satoshis(float(device.profit), device.currency) + if device.currency != "sat" + else amount_in_cent + ) * 1000 + lnurldevicepayment = await create_lnurldevicepayment( deviceid=device.id, - sats=device.profit, + payload="bla", + sats=price_msat, + pin=1, + payhash="bla", ) if not lnurldevicepayment: return {"status": "ERROR", "reason": "Could not create payment."} @@ -114,8 +124,8 @@ async def lnurl_v1_params( "callback": request.url_for( "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id ), - "minSendable": device.profit * 1000, - "maxSendable": device.profit * 1000, + "minSendable": price_msat, + "maxSendable": price_msat, "metadata": await device.lnurlpay_metadata(), } if len(p) % 4 > 0: @@ -224,21 +234,15 @@ async def lnurl_callback( payment_hash, payment_request = await create_invoice( wallet_id=device.wallet, amount=lnurldevicepayment.sats / 1000, - memo=device.title, + memo=device.title + "-" + lnurldevicepayment.id, unhashed_description=(await device.lnurlpay_metadata()).encode("utf-8"), - extra={"tag": "Switch", "id": device.paymentid, "time": device.amount}, + extra={"tag": "Switch", "id": paymentid, "time": device.amount}, ) lnurldevicepayment = await update_lnurldevicepayment( lnurldevicepayment_id=paymentid, payhash=payment_hash ) - return { "pr": payment_request, - "successAction": { - "tag": "url", - "description": "Check the attached link", - "url": request.url_for("lnurldevice.displaypin", paymentid=paymentid), - }, "routes": [], } diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 2337c0ad1..4bf0d3b90 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -12,8 +12,9 @@ from lnbits.core.services import pay_invoice from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import get_lnurldevice +from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment from .views import updater +from loguru import logger async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() @@ -23,17 +24,16 @@ async def wait_for_paid_invoices(): payment = await invoice_queue.get() await on_invoice_paid(payment) - async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) - if "switch" == payment.extra.get("tag"): + if "Switch" == payment.extra.get("tag"): lnurldevicepayment = await get_lnurldevicepayment(payment.extra.get("id")) if not lnurldevicepayment: return if lnurldevicepayment.payhash == "used": return lnurldevicepayment = await update_lnurldevicepayment( - lnurldevicepayment_id=paymentid, payhash="used" + lnurldevicepayment_id=payment.extra.get("id"), payhash="used" ) return await updater(lnurldevicepayment.deviceid) return \ No newline at end of file diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index b55c83f76..ccb0a6e94 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -240,6 +240,7 @@ fill-mask="0" reverse-fill-mask :step="'0.01'" + value="0.00" > Date: Fri, 7 Oct 2022 23:18:57 +0100 Subject: [PATCH 23/29] Black --- lnbits/extensions/lnurldevice/__init__.py | 2 ++ lnbits/extensions/lnurldevice/lnurl.py | 2 +- lnbits/extensions/lnurldevice/migrations.py | 5 ++++- lnbits/extensions/lnurldevice/models.py | 4 +--- lnbits/extensions/lnurldevice/tasks.py | 10 ++++++---- lnbits/extensions/lnurldevice/views.py | 4 ++-- lnbits/extensions/lnurldevice/views_api.py | 12 +++++++----- 7 files changed, 23 insertions(+), 16 deletions(-) diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index 5452e3562..d987e6cc1 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -13,11 +13,13 @@ lnurldevice_ext: APIRouter = APIRouter(prefix="/lnurldevice", tags=["lnurldevice def lnurldevice_renderer(): return template_renderer(["lnbits/extensions/lnurldevice/templates"]) + from .tasks import wait_for_paid_invoices from .lnurl import * # noqa from .views import * # noqa from .views_api import * # noqa + def lnurldevice_start(): loop = asyncio.get_event_loop() loop.create_task(catch_everything_and_restart(wait_for_paid_invoices)) diff --git a/lnbits/extensions/lnurldevice/lnurl.py b/lnbits/extensions/lnurldevice/lnurl.py index 3823005ea..79892b78a 100644 --- a/lnbits/extensions/lnurldevice/lnurl.py +++ b/lnbits/extensions/lnurldevice/lnurl.py @@ -122,7 +122,7 @@ async def lnurl_v1_params( return { "tag": "payRequest", "callback": request.url_for( - "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id + "lnurldevice.lnurl_callback", paymentid=lnurldevicepayment.id ), "minSendable": price_msat, "maxSendable": price_msat, diff --git a/lnbits/extensions/lnurldevice/migrations.py b/lnbits/extensions/lnurldevice/migrations.py index 65913b023..fa56b3d13 100644 --- a/lnbits/extensions/lnurldevice/migrations.py +++ b/lnbits/extensions/lnurldevice/migrations.py @@ -80,8 +80,11 @@ async def m002_redux(db): except: return + async def m003_redux(db): """ Add 'meta' for storing various metadata about the wallet """ - await db.execute("ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;") \ No newline at end of file + await db.execute( + "ALTER TABLE lnurldevice.lnurldevices ADD COLUMN amount INT DEFAULT 0;" + ) diff --git a/lnbits/extensions/lnurldevice/models.py b/lnbits/extensions/lnurldevice/models.py index 0714c656d..01bcc2ba6 100644 --- a/lnbits/extensions/lnurldevice/models.py +++ b/lnbits/extensions/lnurldevice/models.py @@ -35,9 +35,7 @@ class lnurldevices(BaseModel): return cls(**dict(row)) def lnurl(self, req: Request) -> Lnurl: - url = req.url_for( - "lnurldevice.lnurl_v1_params", device_id=self.id - ) + url = req.url_for("lnurldevice.lnurl_v1_params", device_id=self.id) return lnurl_encode(url) async def lnurlpay_metadata(self) -> LnurlPayMetadata: diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 4bf0d3b90..2f048138a 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -16,6 +16,7 @@ from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepay from .views import updater from loguru import logger + async def wait_for_paid_invoices(): invoice_queue = asyncio.Queue() register_invoice_listener(invoice_queue, get_current_extension_name()) @@ -23,7 +24,8 @@ async def wait_for_paid_invoices(): while True: payment = await invoice_queue.get() await on_invoice_paid(payment) - + + async def on_invoice_paid(payment: Payment) -> None: # (avoid loops) if "Switch" == payment.extra.get("tag"): @@ -33,7 +35,7 @@ async def on_invoice_paid(payment: Payment) -> None: if lnurldevicepayment.payhash == "used": return lnurldevicepayment = await update_lnurldevicepayment( - lnurldevicepayment_id=payment.extra.get("id"), payhash="used" - ) + lnurldevicepayment_id=payment.extra.get("id"), payhash="used" + ) return await updater(lnurldevicepayment.deviceid) - return \ No newline at end of file + return diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 26f581d36..752d2c81b 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -57,6 +57,7 @@ async def displaypin(request: Request, paymentid: str = Query(None)): {"request": request, "pin": "filler", "not_paid": True}, ) + @lnurldevice_ext.get("/img/{lnurldevice_id}", response_class=StreamingResponse) async def img(request: Request, lnurldevice_id): lnurldevice = await get_lnurldevice(lnurldevice_id) @@ -65,7 +66,7 @@ async def img(request: Request, lnurldevice_id): status_code=HTTPStatus.NOT_FOUND, detail="LNURLDevice does not exist." ) return lnurldevice.lnurl(request) - + ##################WEBSOCKET ROUTES######################## @@ -110,4 +111,3 @@ async def updater(lnurldevice_id): if not lnurldevice: return await manager.send_personal_message(f"{lnurldevice.amount}", lnurldevice_id) - diff --git a/lnbits/extensions/lnurldevice/views_api.py b/lnbits/extensions/lnurldevice/views_api.py index 65625cbb9..c034f66ed 100644 --- a/lnbits/extensions/lnurldevice/views_api.py +++ b/lnbits/extensions/lnurldevice/views_api.py @@ -46,18 +46,20 @@ async def api_lnurldevice_create_or_update( @lnurldevice_ext.get("/api/v1/lnurlpos") -async def api_lnurldevices_retrieve(req: Request, wallet: WalletTypeInfo = Depends(get_key_type)): +async def api_lnurldevices_retrieve( + req: Request, wallet: WalletTypeInfo = Depends(get_key_type) +): wallet_ids = (await get_user(wallet.wallet.user)).wallet_ids try: return [ - {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} - for lnurldevice in await get_lnurldevices(wallet_ids) + {**lnurldevice.dict(), **{"lnurl": lnurldevice.lnurl(req)}} + for lnurldevice in await get_lnurldevices(wallet_ids) ] except: try: return [ - {**lnurldevice.dict()} - for lnurldevice in await get_lnurldevices(wallet_ids) + {**lnurldevice.dict()} + for lnurldevice in await get_lnurldevices(wallet_ids) ] except: return "" From d4a5fe27768a8d05417e7ed8d6340b2420291a5f Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 23:48:42 +0100 Subject: [PATCH 24/29] isort --- lnbits/extensions/lnurldevice/tasks.py | 4 ++-- lnbits/extensions/lnurldevice/views.py | 9 +++------ 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index 2f048138a..db8d87f03 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -12,9 +12,9 @@ from lnbits.core.services import pay_invoice from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment +from .crud import (get_lnurldevice, get_lnurldevicepayment, + update_lnurldevicepayment) from .views import updater -from loguru import logger async def wait_for_paid_invoices(): diff --git a/lnbits/extensions/lnurldevice/views.py b/lnbits/extensions/lnurldevice/views.py index 752d2c81b..5c6eba24b 100644 --- a/lnbits/extensions/lnurldevice/views.py +++ b/lnbits/extensions/lnurldevice/views.py @@ -1,13 +1,13 @@ from http import HTTPStatus -import pyqrcode from io import BytesIO -from fastapi import Request +import pyqrcode +from fastapi import Request, WebSocket, WebSocketDisconnect from fastapi.param_functions import Query from fastapi.params import Depends from fastapi.templating import Jinja2Templates from starlette.exceptions import HTTPException -from starlette.responses import HTMLResponse +from starlette.responses import HTMLResponse, StreamingResponse from lnbits.core.crud import update_payment_status from lnbits.core.models import User @@ -16,9 +16,6 @@ from lnbits.decorators import check_user_exists from . import lnurldevice_ext, lnurldevice_renderer from .crud import get_lnurldevice, get_lnurldevicepayment -from fastapi import Request, WebSocket, WebSocketDisconnect - -from starlette.responses import HTMLResponse, StreamingResponse templates = Jinja2Templates(directory="templates") From e3bd38fc6b11cbc9c1382fbac5fdfc005a0142db Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 23:51:28 +0100 Subject: [PATCH 25/29] back --- lnbits/extensions/lnurldevice/tasks.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lnbits/extensions/lnurldevice/tasks.py b/lnbits/extensions/lnurldevice/tasks.py index db8d87f03..c8f3db04f 100644 --- a/lnbits/extensions/lnurldevice/tasks.py +++ b/lnbits/extensions/lnurldevice/tasks.py @@ -12,8 +12,7 @@ from lnbits.core.services import pay_invoice from lnbits.helpers import get_current_extension_name from lnbits.tasks import register_invoice_listener -from .crud import (get_lnurldevice, get_lnurldevicepayment, - update_lnurldevicepayment) +from .crud import get_lnurldevice, get_lnurldevicepayment, update_lnurldevicepayment from .views import updater From 34effe3ae5690ad3b68d0a1af34ccc2d9d9b6e21 Mon Sep 17 00:00:00 2001 From: ben Date: Fri, 7 Oct 2022 23:57:28 +0100 Subject: [PATCH 26/29] isort --- lnbits/extensions/lnurldevice/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index d987e6cc1..c27c9e17d 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -2,8 +2,8 @@ import asyncio from fastapi import APIRouter from lnbits.db import Database -from lnbits.helpers import template_renderer from lnbits.tasks import catch_everything_and_restart +from lnbits.helpers import template_renderer db = Database("ext_lnurldevice") From fbc20b3579169b44227cc8ba8e20cc78be1b65f5 Mon Sep 17 00:00:00 2001 From: ben Date: Sat, 8 Oct 2022 01:28:46 +0100 Subject: [PATCH 27/29] added credits --- .../templates/lnurldevice/_api_docs.html | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index 7497714e1..b239e9812 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -2,12 +2,21 @@

For LNURL based Points of Sale, ATMs, and relay devices
- Such as the LNPoS + Use with:
+ LNPoS https://lnbits.github.io/lnpos
+ bitcoinSwitch + https://github.com/lnbits/bitcoinSwitch
+ FOSSA + https://github.com/lnbits/fossa
- Created by, Ben ArcBen Arc, BC, Vlad Stan

From 3bf3c91007ae4f589f06176a405061b92878e5ff Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:12:06 +0200 Subject: [PATCH 28/29] Add backend: ln.tips (LightningTipBot) wallet (#921) * add ln.tips * add wallet * revert lnbits * make format * .env.example * listener fix * reconnect fixed * make format * make format --- .env.example | 7 +- lnbits/extensions/lnurldevice/__init__.py | 5 +- .../templates/lnurldevice/_api_docs.html | 22 +-- .../templates/lnurldevice/index.html | 142 ++++++++------- lnbits/wallets/__init__.py | 2 + lnbits/wallets/lntips.py | 170 ++++++++++++++++++ 6 files changed, 263 insertions(+), 85 deletions(-) create mode 100644 lnbits/wallets/lntips.py diff --git a/.env.example b/.env.example index 7d6de35f4..987c6ca69 100644 --- a/.env.example +++ b/.env.example @@ -41,7 +41,7 @@ LNBITS_SITE_DESCRIPTION="Some description about your service, will display if ti LNBITS_THEME_OPTIONS="classic, bitcoin, flamingo, freedom, mint, autumn, monochrome, salvador" # LNBITS_CUSTOM_LOGO="https://lnbits.com/assets/images/logo/logo.svg" -# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet +# Choose from LNPayWallet, OpenNodeWallet, LntxbotWallet, ClicheWallet, LnTipsWallet # LndRestWallet, CoreLightningWallet, LNbitsWallet, SparkWallet, FakeWallet, EclairWallet LNBITS_BACKEND_WALLET_CLASS=VoidWallet # VoidWallet is just a fallback that works without any actual Lightning capabilities, @@ -92,3 +92,8 @@ LNBITS_DENOMINATION=sats # EclairWallet ECLAIR_URL=http://127.0.0.1:8283 ECLAIR_PASS=eclairpw + +# LnTipsWallet +# Enter /api in LightningTipBot to get your key +LNTIPS_API_KEY=LNTIPS_ADMIN_KEY +LNTIPS_API_ENDPOINT=https://ln.tips diff --git a/lnbits/extensions/lnurldevice/__init__.py b/lnbits/extensions/lnurldevice/__init__.py index c27c9e17d..d2010c449 100644 --- a/lnbits/extensions/lnurldevice/__init__.py +++ b/lnbits/extensions/lnurldevice/__init__.py @@ -1,9 +1,10 @@ import asyncio + from fastapi import APIRouter from lnbits.db import Database -from lnbits.tasks import catch_everything_and_restart from lnbits.helpers import template_renderer +from lnbits.tasks import catch_everything_and_restart db = Database("ext_lnurldevice") @@ -14,8 +15,8 @@ def lnurldevice_renderer(): return template_renderer(["lnbits/extensions/lnurldevice/templates"]) -from .tasks import wait_for_paid_invoices from .lnurl import * # noqa +from .tasks import wait_for_paid_invoices from .views import * # noqa from .views_api import * # noqa diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html index b239e9812..f93d44d80 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/_api_docs.html @@ -3,20 +3,22 @@

For LNURL based Points of Sale, ATMs, and relay devices
Use with:
- LNPoS - https://lnbits.github.io/lnpos + https://lnbits.github.io/lnpos
- bitcoinSwitch - https://github.com/lnbits/bitcoinSwitch + https://github.com/lnbits/bitcoinSwitch
- FOSSA - https://github.com/lnbits/fossa + https://github.com/lnbits/fossa
- Created by, Ben Arc, BC, Vlad StanBen Arc, + BC, + Vlad Stan

diff --git a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html index ccb0a6e94..028dd94b4 100644 --- a/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html +++ b/lnbits/extensions/lnurldevice/templates/lnurldevice/index.html @@ -94,17 +94,19 @@ LNURLs only work over HTTPS view LNURL + v-if="props.row.device == 'switch'" + :disable="protocol == 'http:'" + flat + unelevated + dense + size="xs" + icon="visibility" + :color="($q.dark.isActive) ? 'grey-7' : 'grey-5'" + @click="openQrCodeDialog(props.row.id)" + > + LNURLs only work over HTTPS view LNURL
LNURLDevice device string
- {% raw - %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{% endraw - %} Click to copy URL - - {% raw %}{{wslocation}}/lnurldevice/ws/{{settingsDialog.data.id}}{% + endraw %} Click to copy URL + + {% raw - %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}}, - {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw - %} Click to copy URL - -
+ >{% raw + %}{{location}}/lnurldevice/api/v1/lnurl/{{settingsDialog.data.id}}, + {{settingsDialog.data.key}}, {{settingsDialog.data.currency}}{% endraw + %} Click to copy URL + +
@@ -229,29 +230,28 @@ label="Profit margin (% added to invoices/deducted from faucets)" >
- - -
+ + +

ID: {{ qrCodeDialog.data.id }}
-

{% endraw %}
@@ -434,13 +433,15 @@ }, methods: { openQrCodeDialog: function (lnurldevice_id) { - var lnurldevice = _.findWhere(this.lnurldeviceLinks, {id: lnurldevice_id}) - console.log(lnurldevice) - this.qrCodeDialog.data = _.clone(lnurldevice) - this.qrCodeDialog.data.url = - window.location.protocol + '//' + window.location.host - this.qrCodeDialog.show = true - }, + var lnurldevice = _.findWhere(this.lnurldeviceLinks, { + id: lnurldevice_id + }) + console.log(lnurldevice) + this.qrCodeDialog.data = _.clone(lnurldevice) + this.qrCodeDialog.data.url = + window.location.protocol + '//' + window.location.host + this.qrCodeDialog.show = true + }, cancellnurldevice: function (data) { var self = this self.formDialoglnurldevice.show = false @@ -617,10 +618,7 @@ '//', window.location.host ].join('') - self.wslocation = [ - 'ws://', - window.location.host - ].join('') + self.wslocation = ['ws://', window.location.host].join('') LNbits.api .request('GET', '/api/v1/currencies') .then(response => { diff --git a/lnbits/wallets/__init__.py b/lnbits/wallets/__init__.py index 41949652d..fa533566f 100644 --- a/lnbits/wallets/__init__.py +++ b/lnbits/wallets/__init__.py @@ -1,5 +1,6 @@ # flake8: noqa + from .cliche import ClicheWallet from .cln import CoreLightningWallet # legacy .env support from .cln import CoreLightningWallet as CLightningWallet @@ -9,6 +10,7 @@ from .lnbits import LNbitsWallet from .lndgrpc import LndWallet from .lndrest import LndRestWallet from .lnpay import LNPayWallet +from .lntips import LnTipsWallet from .lntxbot import LntxbotWallet from .opennode import OpenNodeWallet from .spark import SparkWallet diff --git a/lnbits/wallets/lntips.py b/lnbits/wallets/lntips.py new file mode 100644 index 000000000..54220c85a --- /dev/null +++ b/lnbits/wallets/lntips.py @@ -0,0 +1,170 @@ +import asyncio +import hashlib +import json +import time +from os import getenv +from typing import AsyncGenerator, Dict, Optional + +import httpx +from loguru import logger + +from .base import ( + InvoiceResponse, + PaymentResponse, + PaymentStatus, + StatusResponse, + Wallet, +) + + +class LnTipsWallet(Wallet): + def __init__(self): + endpoint = getenv("LNTIPS_API_ENDPOINT") + self.endpoint = endpoint[:-1] if endpoint.endswith("/") else endpoint + + key = ( + getenv("LNTIPS_API_KEY") + or getenv("LNTIPS_ADMIN_KEY") + or getenv("LNTIPS_INVOICE_KEY") + ) + self.auth = {"Authorization": f"Basic {key}"} + + async def status(self) -> StatusResponse: + async with httpx.AsyncClient() as client: + r = await client.get( + f"{self.endpoint}/api/v1/balance", headers=self.auth, timeout=40 + ) + try: + data = r.json() + except: + return StatusResponse( + f"Failed to connect to {self.endpoint}, got: '{r.text[:200]}...'", 0 + ) + + if data.get("error"): + return StatusResponse(data["error"], 0) + + return StatusResponse(None, data["balance"] * 1000) + + async def create_invoice( + self, + amount: int, + memo: Optional[str] = None, + description_hash: Optional[bytes] = None, + unhashed_description: Optional[bytes] = None, + **kwargs, + ) -> InvoiceResponse: + data: Dict = {"amount": amount} + if description_hash: + data["description_hash"] = description_hash.hex() + elif unhashed_description: + data["description_hash"] = hashlib.sha256(unhashed_description).hexdigest() + else: + data["memo"] = memo or "" + + async with httpx.AsyncClient() as client: + r = await client.post( + f"{self.endpoint}/api/v1/createinvoice", + headers=self.auth, + json=data, + timeout=40, + ) + + if r.is_error: + try: + data = r.json() + error_message = data["message"] + except: + error_message = r.text + pass + + return InvoiceResponse(False, None, None, error_message) + + data = r.json() + return InvoiceResponse( + True, data["payment_hash"], data["payment_request"], None + ) + + async def pay_invoice(self, bolt11: str, fee_limit_msat: int) -> PaymentResponse: + async with httpx.AsyncClient() as client: + r = await client.post( + f"{self.endpoint}/api/v1/payinvoice", + headers=self.auth, + json={"pay_req": bolt11}, + timeout=None, + ) + if r.is_error: + return PaymentResponse(False, None, 0, None, r.text) + + if "error" in r.json(): + try: + data = r.json() + error_message = data["error"] + except: + error_message = r.text + pass + return PaymentResponse(False, None, 0, None, error_message) + + data = r.json()["details"] + checking_id = data["payment_hash"] + fee_msat = -data["fee"] + preimage = data["preimage"] + return PaymentResponse(True, checking_id, fee_msat, preimage, None) + + async def get_invoice_status(self, checking_id: str) -> PaymentStatus: + async with httpx.AsyncClient() as client: + r = await client.post( + f"{self.endpoint}/api/v1/invoicestatus/{checking_id}", + headers=self.auth, + ) + + if r.is_error or len(r.text) == 0: + return PaymentStatus(None) + + data = r.json() + return PaymentStatus(data["paid"]) + + async def get_payment_status(self, checking_id: str) -> PaymentStatus: + async with httpx.AsyncClient() as client: + r = await client.post( + url=f"{self.endpoint}/api/v1/paymentstatus/{checking_id}", + headers=self.auth, + ) + + if r.is_error: + return PaymentStatus(None) + data = r.json() + + paid_to_status = {False: None, True: True} + return PaymentStatus(paid_to_status[data.get("paid")]) + + async def paid_invoices_stream(self) -> AsyncGenerator[str, None]: + last_connected = None + while True: + url = f"{self.endpoint}/api/v1/invoicestream" + try: + async with httpx.AsyncClient(timeout=None, headers=self.auth) as client: + last_connected = time.time() + async with client.stream("GET", url) as r: + async for line in r.aiter_lines(): + try: + prefix = "data: " + if not line.startswith(prefix): + continue + data = line[len(prefix) :] # sse parsing + inv = json.loads(data) + if not inv.get("payment_hash"): + continue + except: + continue + yield inv["payment_hash"] + except Exception as e: + pass + + # do not sleep if the connection was active for more than 10s + # since the backend is expected to drop the connection after 90s + if last_connected is None or time.time() - last_connected < 10: + logger.error( + f"lost connection to {self.endpoint}/api/v1/invoicestream, retrying in 5 seconds" + ) + await asyncio.sleep(5) From d1302e4868f0fe12cf6bdf7d25e82045981077c2 Mon Sep 17 00:00:00 2001 From: calle <93376500+callebtc@users.noreply.github.com> Date: Tue, 11 Oct 2022 08:52:39 +0200 Subject: [PATCH 29/29] show progress (#987) --- lnbits/core/crud.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lnbits/core/crud.py b/lnbits/core/crud.py index cbed62922..bb1ca0c1c 100644 --- a/lnbits/core/crud.py +++ b/lnbits/core/crud.py @@ -333,7 +333,7 @@ async def delete_expired_invoices( """ ) logger.debug(f"Checking expiry of {len(rows)} invoices") - for (payment_request,) in rows: + for i, (payment_request,) in enumerate(rows): try: invoice = bolt11.decode(payment_request) except: @@ -343,7 +343,7 @@ async def delete_expired_invoices( if expiration_date > datetime.datetime.utcnow(): continue logger.debug( - f"Deleting expired invoice: {invoice.payment_hash} (expired: {expiration_date})" + f"Deleting expired invoice {i}/{len(rows)}: {invoice.payment_hash} (expired: {expiration_date})" ) await (conn or db).execute( """