From 8da3e2bf9f438f3d2418df1c36199453bc71293e Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 13:56:44 +0100 Subject: [PATCH 01/13] Setup project --- .dockerignore | 12 + .env.sample | 7 + .env.test | 9 + .gitattributes | 1 + .github/CODEOWNERS | 3 + .github/ISSUE_TEMPLATE/bug_report.md | 30 ++ .github/ISSUE_TEMPLATE/feature_request.md | 34 ++ .github/PULL_REQUEST_TEMPLATE.md | 10 + .github/dependabot.yml | 25 ++ .github/release.yml | 15 + .github/workflows/cla.yml | 36 ++ .github/workflows/python.yml | 121 ++++++ .gitignore | 127 +++++++ .pre-commit-config.yaml | 28 ++ README.md | 35 ++ config/__init__.py | 3 + config/celery_app.py | 41 ++ config/gunicorn.py | 8 + config/settings/__init__.py | 0 config/settings/base.py | 352 ++++++++++++++++++ config/settings/local.py | 49 +++ config/settings/production.py | 32 ++ config/settings/test.py | 42 +++ config/urls.py | 86 +++++ config/wsgi.py | 42 +++ docker-compose.yml | 60 +++ docker/nginx/nginx.conf | 68 ++++ docker/web/Dockerfile | 38 ++ docker/web/celery/scheduler/run.sh | 18 + docker/web/celery/worker/run.sh | 37 ++ docker/web/run_web.sh | 12 + gunicorn.conf.py | 24 ++ gunicorn_custom_workers.py | 27 ++ manage.py | 30 ++ requirements-dev.txt | 9 + requirements-test.txt | 12 + requirements.txt | 22 ++ run_tests.sh | 14 + safe_locking_service/__init__.py | 5 + .../locking_events/__init__.py | 0 safe_locking_service/locking_events/admin.py | 3 + safe_locking_service/locking_events/apps.py | 7 + .../locking_events/management/__init__.py | 0 .../management/commands/__init__.py | 0 .../locking_events/migrations/__init__.py | 0 safe_locking_service/locking_events/models.py | 3 + safe_locking_service/locking_events/tasks.py | 1 + safe_locking_service/locking_events/tests.py | 3 + safe_locking_service/locking_events/urls.py | 9 + safe_locking_service/locking_events/views.py | 25 ++ safe_locking_service/static/.gitignore | 0 safe_locking_service/static/safe/favicon.png | Bin 0 -> 53183 bytes safe_locking_service/static/safe/logo.png | Bin 0 -> 3367 bytes safe_locking_service/static/safe/logo.svg | 1 + .../static/safe/safe_contract_logo.png | Bin 0 -> 755 bytes .../templates/drf-yasg/swagger-ui.html | 25 ++ safe_locking_service/utils/__init__.py | 0 safe_locking_service/utils/exceptions.py | 39 ++ safe_locking_service/utils/loggers.py | 60 +++ setup.cfg | 60 +++ 60 files changed, 1760 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.sample create mode 100644 .env.test create mode 100644 .gitattributes create mode 100644 .github/CODEOWNERS create mode 100644 .github/ISSUE_TEMPLATE/bug_report.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.md create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/dependabot.yml create mode 100644 .github/release.yml create mode 100644 .github/workflows/cla.yml create mode 100644 .github/workflows/python.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 README.md create mode 100644 config/__init__.py create mode 100644 config/celery_app.py create mode 100644 config/gunicorn.py create mode 100644 config/settings/__init__.py create mode 100644 config/settings/base.py create mode 100644 config/settings/local.py create mode 100644 config/settings/production.py create mode 100644 config/settings/test.py create mode 100644 config/urls.py create mode 100644 config/wsgi.py create mode 100644 docker-compose.yml create mode 100644 docker/nginx/nginx.conf create mode 100644 docker/web/Dockerfile create mode 100755 docker/web/celery/scheduler/run.sh create mode 100755 docker/web/celery/worker/run.sh create mode 100755 docker/web/run_web.sh create mode 100644 gunicorn.conf.py create mode 100644 gunicorn_custom_workers.py create mode 100644 manage.py create mode 100644 requirements-dev.txt create mode 100644 requirements-test.txt create mode 100644 requirements.txt create mode 100755 run_tests.sh create mode 100644 safe_locking_service/__init__.py create mode 100644 safe_locking_service/locking_events/__init__.py create mode 100644 safe_locking_service/locking_events/admin.py create mode 100644 safe_locking_service/locking_events/apps.py create mode 100644 safe_locking_service/locking_events/management/__init__.py create mode 100644 safe_locking_service/locking_events/management/commands/__init__.py create mode 100644 safe_locking_service/locking_events/migrations/__init__.py create mode 100644 safe_locking_service/locking_events/models.py create mode 100644 safe_locking_service/locking_events/tasks.py create mode 100644 safe_locking_service/locking_events/tests.py create mode 100644 safe_locking_service/locking_events/urls.py create mode 100644 safe_locking_service/locking_events/views.py create mode 100644 safe_locking_service/static/.gitignore create mode 100644 safe_locking_service/static/safe/favicon.png create mode 100755 safe_locking_service/static/safe/logo.png create mode 100644 safe_locking_service/static/safe/logo.svg create mode 100644 safe_locking_service/static/safe/safe_contract_logo.png create mode 100644 safe_locking_service/templates/drf-yasg/swagger-ui.html create mode 100644 safe_locking_service/utils/__init__.py create mode 100644 safe_locking_service/utils/exceptions.py create mode 100644 safe_locking_service/utils/loggers.py create mode 100644 setup.cfg diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0a407ba --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.cache +.dockerignore +.gitignore +.git +.github +.env +.pylintrc +__pycache__ +*.pyc +*.egg-info +.idea/ +.vscode diff --git a/.env.sample b/.env.sample new file mode 100644 index 0000000..86879c9 --- /dev/null +++ b/.env.sample @@ -0,0 +1,7 @@ +PYTHONPATH=/app/ +DEBUG=0 +DJANGO_SETTINGS_MODULE=config.settings.test +DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y +ETH_HASH_BACKEND=pysha3 + +DATABASE_URL=psql://postgres:postgres@db:5432/postgres diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..fabd26c --- /dev/null +++ b/.env.test @@ -0,0 +1,9 @@ +PYTHONPATH=/app/ +DEBUG=0 +DJANGO_SETTINGS_MODULE=config.settings.test +DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y +ETH_HASH_BACKEND=pysha3 + +DATABASE_URL=psql://postgres:postgres@db:5432/postgres +# Only required for testing +ETHEREUM_MAINNET_NODE=https://ethereum.publicnode.com diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..26d7482 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +# These owners will be the default owners for everything in +# the repo. Unless a later match takes precedence. +* @safe-global/core-api diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..5504f1c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Do POST on '...' + - Provide `json` you are submitting to the service (if it applies) +2. Then GET on '....' +3. Links to issues in other repos (if possible) + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Environment (please complete the following information):** + - Staging or production? + - Which chain? + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..67a2881 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,34 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement + +--- + +# What is needed? +A clear and concise description of what you want to happen. + +# Background +More information about the feature needed + +# Related issues +Paste here the related links for the issues on the clients/safe project if applicable. Please provide at least one of the following: +- Links to epics in your repository +- Images taken from mocks +- Gitbook or any form of written documentation links, etc. Any of these alternatives will help us contextualise your request. + +# Endpoint +If applicable, description on the endpoint and the result you expect: + +## URL +`GET /api/v1/safes/
/creation/` + +## Response +``` +{ + created: "W(8Dwg89Vib7{nDUHzulYO1O~3S=lV_6N{6dX z^Y@}37gUe4>Y~>-|J%5TK=ZAq8U_Sj4jQ8OOTFio^YqsBoj+poweS-O7Xn?EcRjT( zXo$`5H)Cg;dg;Uzdew9fY`WE5EAVDeORW`c+Fk0eJ^O{x!H;8)*jGKAH>_#%?qgoJ zJF(I~`HXzj0PPuQtngPKcdgAS=ut$vHyY=?ySkN@qhcEocp3NK>l-Q$6 zZT WoFQelH7i6Muc z7>(~~wmow9sm#BJx5@ahQfK1kztVrzsr>8v$e2{ykAMAKBdK{~y}|=QjZEXi+x8rE zx{*z|>s8kZlnSQ3Bj8`9a*h_9iCNCSUt@RQ34T#|k}~=^mLq%`ngBLhZK^bnIE* z)rmeCTC!V?XO1~|)>!}G#jr-L;*P$0HEyW<>%RlqRZf$81%?J$=(gyU^R7|XA{vmr z^U9 Dm)bcGs2t=GcRny?cGtgeW(*ix ztJ=akma$Tl>}>T=}>60eW^i-L+R~ZIr!<8K^VKV~S3- z&^qq2z!5bf^s-x2A0BM3_0qS>iZNBU-B=Q{H013Bk0C>R^-~(ZYFy>uQ=^e976e(e zDl$4+b@kFGRqThIaXsy{P`A^Ua)ZDLareDq+a _W$ lqbGUJu&=gW{|i}Q(<-L&&h0x~aY}9;7Tb13v*7{PT1DOu)!IGM z(@y7&W3SEQQ_s%+5a<@v<-6Y#!fze9pE0=5Nnf|=P+4TR`R7fXTZVNw*)KW8A)`h_ zo3U>Kd;7#^1}y3tdG_9`^lQE19CgrNrRt!X(+r=DNDere)O`MeC)>A-`SHfRSLar5 zlj-XXt`=+6c=o)R$vqZT|M7=`S$aP@R<2y>yO4q2)9M^;9rEX-x*7cohv~O#I#f$$ zXmH{B)Z{PYECQEnH96=XnbWP+iP)w!wU=H9)Ou6-RP|}K?C;CkcU+owa(GtEn5Y}J zHDnWd1twQYZk=owVs0=rdR%Cb!^zt6 jFBC39MqY+A~e~6fMEH_MkEhaPa;lw>xuLk)WAFch&EiR?j z_31B{v}tp0^bIuCx}<%kN{?@9#;*Ot^-kMY3$o|dGK)&Fx{w+0MTgDbRvy~m*JE*i zNACCd7!>`%>EYRs{XUgsdXD}*s#*ON7HASGJChT5`&xg4D(H7M7Weps|DO0?n`BN8 zNq<@R{>#{K+n1P@|A2uXTfV$}F780!kjjHAN60(2^ZcpjD&JNCrq(N5f3?XR#Vy0W z4Qr66^=9K_i#FLe4P>2r=vR_^*I24M*p9oGJzM6a^L)}!f4A3qLG{{Ko~XSpC?;sy z-*K~hK5w)C?8m2VQfh8RlQ#b;@~AJyd~v}nu1lw@_KlZZv6%N!D`=v<&SA^u`ZeF^ zzLA}>M}x9~t@NMW|1F>}y?V4Idc}Ro^Scs~=F@rMxP})EA8(Dz3920wo?~38z{omD zCn@;xfUwz(eX9jeOsyF-BcRZ#dh`TyZU6Z%7QCX{VY>ZvBRXa8$g1YwO>1eT-@l#g z)w^kBYn?aR`L@5FjPo$a`17l(zns%MKXbP2q3&p;r~NWO9@xL?Si8NUl?;44WxsoT z=?tRu9V#ci9cZufTi__|b*=X39hSX6`(noG%X2F~K9lvxJ tvsx-rgfC`OdQJ-^Fue*X$sFveOxG7r~)E0gW!YpVl2(?N{fE_huZCAGoV+EPuW| zcwXm6Ut-qFUM_fbzUohh=6lWiw#kJt4lQQ)?)b2U-xrN7hekU*?r?Ja^BozXL9^p# zgeCaC4{<5@#`IX ~0_R16uB_{j5{ !FzvDAcz}O3NL>&s6@|8rvLIa|6?5))4Y$yvIQSa1X}pf zRJQP=iP6GBQ@=%prlW!jO(TK}&Egjqnmt-lXr8s?gL&c74;Hix6!O8cF!Y0ER#<`6 zBT)SE0_%ts1=gd&^KJZA=Gz!WytkLF$+MNM&9jq5fYu35`~MQcKl;Ds378qTh+l!- z2+yvdP~g7=?CGFFU?*S)nG>*+3cG-xLJO=2*a_GP*a_GRfjxayzWmbae0eDFcLjc< z^;iq04euS4^p7V0FF&M?eY%LhF3!Mo;7$PL0Xt#u3D~J5c5}tPOV}-oV<%uIU?*TF zV5hZtwt3k1gor%*>A+t%IL}NL`QA~s>8-PD(>tesiKgNopIc-sn + 1LHTfN|e-O`nF@9GzcAR^hdF8%Gz7zJIu@m;5fSs`SsoUSWjs$i+#=ZlzGspd3 zt-&14^Ii+M!$A3rUBb_}rE|}?D}ucgUay8-;&*)CvF`$Q`!ca(?}5KAZ3h8+KCp-F z$aSy9*mvc4$ad${`$ua~47(|CM}Y*~6~X=)a_(Pa$KEq`0(Qc_6R<}CyD4Lj0qxE9 z{H!%71^ZqRyNF-&%>NTPSCZFDa!%s)CH7t9cgiy_?K@+~oma fPG)K zmux?%qAy?-_&nBu&%9#aRoE*c=azjmr;s8G3I&A~S<-UQijP(l{?VFNf>wQmd@r=2 z)rC0kptXgzwC)4WJ81m}oOjU10tebu;7GqS Ebj`zSHsS{j_*5x*DLS4wD*FHFH6pW=LJ1Br?KbEXv|qN8hh59#wlq0 zSqqwQhRKp9f+n4@5@>Rq6-|k=rl|ti&~K-0XxeExO$W_5ZA-y7?P*hvGi`at_g#Vg zt*i3f?^MISN5oFRPQV@p?0N@*|4>%_a@Rn@&iW1BGoQ~F@E7Ct jx+B;j2&kl*gY9L0ek+ztokDl zWQh2WX8M%51`>We{XnUuVJ}b4Eycbs$TTPKu0~W--;fLq|2r};FeJH)5iPyuNKx-q z`_9 UkY-IF5!$13^1MPgTlyd^V6VH5c?B2ZZbXderz+H4aqv3eQ zkEiU^o5spDP{Z$n_YYymtMNOPepAw(gH0pYvI=|d{{a7-la93Oy}0k9oKy081;68c zFOJ=ZfSF=5d>@|7XygKtomSwNuumg_=LMI^>(z8S$(}84(}WgU(%fej `^?8SIJ^E+|hvl`&s`(WQi>^SoT+;r+qqu@IaItcjBrZ*LQHi|W<3+zv%`(7bA zr{TwdP7nO^QY^{oe{uf7OKKY#(x}z86qAQNe~0t_wjS-v1+U8mpU?3whF!Sx1b#;< z?2QTg{v_^Ale!>TJmW7i68twf^V9zgcEP6kU`5Mvtf<+bI#jcUp}+%}5B_UnIRgCr z|2WsLL*qBu(~5+;v@+3^RwcU8>Z@+F>5c~-%*1^c_FWTuthn!toq(Ipq&J<;`0-R= zuVV!42@-ylP4oHo+=@bLntsianhmT&P7RI7(MLs2c&{R3yjPJk-V4+KPc?FBP={=s zj6_+gSOa4dL$Yx+BDu4%l3cv&(8whYbTCcC4n8OBJMO%2?j`Iv^Aw-nEa8vzW=0?h ze^+47lg|9VwdYoTu%WPQ8(Q=f^w@@iAIoVmXvt$+TAFN2%aZLV1QeQVPhpSjY55}u zTJgw{!a*w^I?*c7>W9v>=79^Xedt2VE<00)Z!I+BX)T-ybz=R%%Fc+w&w9{_Y{o9| zI}PmEZ(wgqz@2w4&9CdZbU*lL1>a@pr?9KbInj6Z3Ey1-e0QKV1@PVF! Z(+;5b*DYJ@2hUrB_~hga^ R}tSG+jFZm{8ysy zPV!yBcc<{*ly1`~ZCU7dyK_Bg+e>$9*v^!ApUcv#82?=0Kb6y{81^Q_=N|hmVW$ge zEiPSr-GZ_EfzqWjuP)=ul5_CaYxu83f3@JVhAm&3?@sBrRQfSQ-&N_@*_IdVc!f=~ z2l%(Y@F3szpTb|8=0&^FY2b%`Bk;R3;CD*w>3;NQT64xuz@C2TbxS|s9tA2a*QO~Z z=ZgDm75;iPKSsrTckH{;uw~1z=h&8wN(KIQW}n!95&xNNP3+)zguAZ5-kh=1<<~6> zue|nW>=EMLOTMd;K3fgH<>&hDM1Qqv-(kmZ&Ijcw?mO=M_V@7LK&RgUy;joe)$O^x zIi3`qTCYO*=SREK+3Y5QoKx_61$N=i3)r#m1niXXs#OH`J)ZZzqP{!!S(fd$EX{YP z@>^~KcDCccS!6{$Cs+v7Yl0>98gEJcrdiRs&a2z9z`rBa^ON|E>(r(N zQSKC<-BdX9yzhMH{}lNhUBtOp!%kOUwPNg#DvJHz_$`^&FS~0`o~`Q$XV{>oA*oYM z{A^)sM3bW(XwO^8ju&m3{W)H=vx4{+MtRU5St?#HVdp!~*e{Fw4(zSz+N;)#JxkqZ z``L1i`8-Gwqbl8J3tqpuz=?j2v{C9YrSb5ZHEU7ZL1>SZscF+3$o8gPsb18m{ip7~ zAuR&_b6MiPE99KO?^M|RdEXg3U4Pkzv8%-JCISIdj4ob`uep*eY%+bQ`4Xp+1MM? zhPe9lXQrRJoWq^xeHZUMV^0*Z(~XyH1?- m3m(|eLeQcIa$5cx zzDwvg>ahw*&TW bUDIad~Ty7jW1GBzvOvKqX;cx>{6HH|%CLEfE>$ W};+NJ`udT7*+bhkc zW1n9=|2nkd8GN o^6^rcdbb7 zR<>`C?GY>3Wecv^LB0#INu|F&3iwB@mzTT$%>UZ;x1_yqyo&qo*nd@?Jy))rQ( X jmt$#C>P%bT_plV~?+hJ;%I$u7JO6 z- ea6~Q;7(`SmtNd=r-_~Q zdclVQy j^*6EJ2u$=Pvc)C;8%;&z@Cv}RJ-26 zo@M $a@lu>b0CuDI{)vn}0sr^2qZX%xDhXw#G_=M?;|J*i=*`>CBGLOxiG z67sK#^ZH`B7;8S*Rv3TqRR`JzonDAdDr_2cdrs1ARWe?P)2R6^8 @8$dMG;JETWkos1=U!pc2>Q*nmlCgU2VT$E+tZ!Y4ia{H@Vs;S zgXdisyAiNoD%++JZCUo;ET{l}D+eQ5e6?hZO5L7g?8ReL>T*t<*Pnv#vY77 ^1{5bitt)%$Sg58%!p#GOBs4H*yJ?kMP37JT(t@Y`j mN{Ip*~OcAR;}PLG~<4NXezBm-`yuJB>xA?Hx-%&V{qc_`3p!=Bqwy;hK4 z&~wy%kNjTJ=~yrFZf!!djyurW2X!gpfh(;~a-|JPZnO~;dEcEj-S?p1@7JTvpsn{j zY1=(7+761k=S|Uf>(kD=4QLl=_Z=VFbEhF4ebPwKZ;JV^irI6e%Q=bP@xCjur*zFr zPU+eeBm-_HBVbQZ&pA~0-6?Z1;J2J`z=k|p)DiR?KI; hl^!sC=L zjX<)Md2-pRd>a|CPydwfZX ~j4yA1vd*&Ncx(>7SjKN9YIG2fkJ&vD*eala+|>(?Z?;LMlrqyI Wip&ifDCOfJZ=eyGa3Q~B=7$T?KXc;@wM zlj;h0Ub+MS2rKwUiO)& DhfxRKH$13AA#d1w0>`I$Pk$c86J_2&i1@YPceE)GK z`2KG=)R3;H3w}!_cFCSAL(WOq>DjaHu^>Z`jH$F2p8@!;xURqt{+m?EcPHi9s$(yn zbI38OtuNfEQ71F#2etm=+^1UP;9`PrTWLzy)7q4Yy*RJuea9VV?5Wsuzw)s2dT?C@ zaLYF3I&y8nNceB^KPTsKFYLLXy-wuT$b{II=X@ozPm&S-P5I IVbXZbvdWx^}yaOAJ|7e#(C#+&s5%v@q^DZ=>dCKMPpRTJQV1)k;p|@deW7a zoN%S3$K7ZdDCD?1g&y~yuwx#y{8&9&0SZ6nNh^ w M4M|7S4QBY5A7r -MVKvK7oZ zRNAuY_8jLRm9*!|j!~6q)4-OMY#N0 Eqr0?z%xfmsE* !oD)P%UVpN*>mdc#NisWLbY}sP=9Q*5;*TYw@;k#p7R LKjIQ2{Hc{c@p-hG#_miNTIqn^e?s3`$ 9DZF0HLn$>EqZGeonR4#Qqm-_}u<2Ytf*s3xJ==8uS1;_lY=4%3UzYgDUk2=T zA?Kze*E9jSXL }4 zd}P3G1ngbG>q8NzxqRkL({#jX3QO}_enQR_!lp@wjK2h49}53nSIBuIkPP?*>^K9$ zQ)5#9FZ_Ug7jo*4W%vlS1;8&u-n9&O-U$ABKVTn)a~}cUT|DA6kKn7%0`|fL6|XPk zTnzB~N8t7G(CgO&`zZM9{ea&HPZ{vbB<$e(GMt0|1SwG<8Uzqg5&w5Hi3<7Ik|>j( zHHnh>sVDq1;UDskGMV_#@d4qV;{$o(?`@=~@NxVH$>Q(zq-UJ?vreG+)RLa!kN^Mq zQ^GAh<3#8>$>LK_dP?gkt*i8u)?L^(-Z=4ds(ljv1MHi$kJ7$M`>fb^t{C?M1%Q-& zA{_o*p9thq3;iIFS1LS_U&uWn^pR`U6!}JDK~c!LxrW?}SBSsotK#pL`H025T3%pv z4L dT|YhqHa$%k4zbZH|8D= zO2OQPo&=bCiF}`uv2W$vl{NU7yO`JZ-aZBE9*+5YA1w4%_8nk5>EFDh8XU#b1al7p zrC{zFwbke$BJ|LZ@^wm^JFmSejwAGqkmj!D&vZ(GPb&yEo(bDHtm8BNlUK31x4<66 zW9}OL5X5yaZSJU#Rpc2-@kjN!voBuV507;ha;hYs6xKZ+^KTLTR;+i2UH(}R)aI^( z_uqk@7oWR&J~;O;k#dEQH=s6mRXsD#yIMY^RKG6vYmofNl1~Y9=lb2}u&ut^@zz}j zwpE4e)z#*%i}w>jx$1Kl&b(U9mipYKb0fyQRdeU~mnxT2HFx#}moaxKUpW_Zp9s51 z7k1$%>&>P5ay)t6dG2}A+*Nf5Qa-lWH->9VHR`EROD)w?tJhJlL!O3IL(TQmk*J;C zln+A8N)@*ea}y+g2HW$nYw}{U>rI4Sufw`M*9({S!dzuMe+94MGlO+E*60NRKB?IU z0X4x3GR$f6H8YxU$($yFCS9_i$)G70Eomy~w~JOZ?E-3tKr=4b(9H943K9r;7YW#l zG{oG5+GkZ>g^*LCsC~uUIj0~OHraQgUE(e1ro}Pv{1wy!Pn>sEtz)Tt5_6xIZcZ(R z8I!pk{JHY7$sB)If)Ee1#OJz Z;A% z1n=Xq2g*84&AF?1&9WlYq5o+n)L{#83B-~ |4M=P1<9 zO11JzpS4&gu2C -I2j_7jIAuT_iZk9*2I#}#uI^n$YZq^KJx z`-;(W@X2X6EXcZUZ6R+(z2=~z@A>@m8RFdhCFfjd^$o kUG?CKppZMU?Na+JegVyFcqjJ zo-GHMQOo{j I->C>UFYMbE$@8 zl4Ab)n0s7FolKNZ<>_SM(8)M|cIA6Ht$b@st8 to(cL21%`En6p3uWb{1v8n51=tCyzWT yhrGh${IF~jea@Y zQpou&9@{rT-q6xR?m}D$wb@dAu9(X%=D59v&w%GX7IXh7of(xbFZNdwbsBY@OxZ_H z(ML^@$EWNmq|(XM^hxF1SvOGUlRbX6Ds%oJhdhLs7N2?edz5+joU4SIqmS@+@Z6(# z?q%^wp1(>LR_SEr>64pKN5*v}TwAdn^#z<`9 mPck zo)C{?A1&t;vyW0Scd2%Q=YFk(UZch*+5Y=1eG=zp{uMhKw#u5m54EPDp*A!u%!Y!F zp$04yHDJYfe2)> kpU*lO_hI2$hMqr_K7T&{%MW@g zYnC{#Q>xYCd{#vqi07VP9-rj37i>T98L2jjb9Tj=B-G`o<+E@t*4UkLs%>0Srtul+ zJJA}w1ynhzoR2?Xf}HegC~A*b9w_i~?0)SDZ=vQ$oI7H;YPCj+I;#rJeffK9TKH70 zt|&=PA Y>sw?_W zaV^h;9hg7*Q55GVe17^(un}{6xqe42pF0+{JEt?nx*efrCqt~+QRg)YulzOEo$GXv zyUlZdCHcvU^GTsL=N@WP+)LIba7~p@M-y6?h8jdg-X7 *CJ_vXl3 znZDgws72tqE9mgbn2tgx1CQ6_lbZJx`ALf0bIwf?^Ugc++^>~ocdz2w63pMWtoe5` zq4f&;Pw*Kaub$-->%+sB+0w9JTN)m0M?Zq*>~*1iX{e=8%pEqAvj2xDpH%hWrSM5* zjg*vErHDE5+@r)ZFV+UB^GTsL zPCHN^yek7fn+#8iL9N`rH{P^Ay*?dGZ$O9BedtJ9Lpqk`ODA5V55()ns`@~wW}-|! zsmd`B>m-DH@i@d8^*K)(bDzZLMqMY9a%I;+mmj#$lI(DYt l#suugs`*e{+xpwdp5PTRhwLv!r$kYTwU_I`kE(V_$3P)X$nGEU8PUo;9WO>4H8f z=wzkyNzI%fMXviKj+5fNFu7vxDW&=NHX^T`ZIESmk 3$;A316`$n$ zs;aS2 |A})~@yVS$|NF=ZM{goNKNVf? z;{T54KQq*WuD@xmrW+`7BUF4+E&s0@VctR4GusF`P>g|JSa%usQpdWRVD9m%c!EYB zU7==;`6TBwZ+zrNuD)W;D$nDe(!4dEbmNUGcUPTH@|g#Zl w>fIj(L(#h2AZpn6qPn&jU)uW;FZAtEAOcpjqCCC!LE03&fjHz{JYdU<@ zk8ZqH<(sSLTMKzuVr>P_{W)^ZTJYSTmXdRbbw_TkqwII|?Z(`{gHD#q=SC3=5p(IK zzBepeVY}{0_oOum?i6;`jaHm>7kWIcI#Z8UpYf!%abC0zv_8(8Hk__c8&5Z&O{aZm z^Qnfk<&-Z)orX{0ZVMrIR;v5foV$|O@R`ZQyuW*b^TK&^W#!IselTJQj6nx;pNP5V zsq(?a-gFxNN!WjfbC3@UJLEX>VPnysO3a0YF09CdJ*Sofdl@x8T<4L9Tz}4Yzm9bm z?yFS0tidO{h&k@hy5(W+6H}k{&?#^3(pkc~3;oj3J6RXJW+Ki^u9(v+#6r+RRO-PX z)!|Duxtcnel-DKdWUAUMHQQC3yCR2ArIU%ghWRA;Bd`0!=g)fRR%GrH2CO^sGm)Q| z (37t&nX(sk|Dya)|9jvOq3+D}s_Mez1ucl9` z^O|_9eT&D>Id8D+x(I2mWj%$wNaRLh{v7)=LHsNTwrh$a79!eEihgYB>n`L1YUpI; z>yr|nMBY;h&P@>Hk_pQuaqj;v_YLbV |xvh^PwIIqtewre=-zZ9wWfK-cH!pB#p zKFRi |6c~?9q!FG*<{g;OO`ccZk z)UdnZYf_yVu|Edp{t tkxg4aaBcD)As?-lgPd=;;0lMg=m3O3X==#x=6^W!l8rZ_kHn7@!S2E9_5KleFv zr)16oL*iGQOs2z6{xJR!(}`p A$Fri z#yarFyTPYD8h+Ki@T(>vev=CyO%ePAMX*(KVXG#=*4qoceKd4-H`EB~K<5&2mN;*T zgog7fOti7av=+(K#1H^nFXZKJ=b6;9d+}(}MGb zB)(tT9Ms0;IW$7-@*Kw3$Y+vrQq*G~l7E10RQ9_gXH2j&IR8Y--vI7&&{rCvr-E#E zcFBAb&d-o?OfYs^P*Q1QFIDvD5BsR-&5z!#oJ+#~Ebhg>zCh7~pZjzwe21*33i@@9 zdlJ55plwW!r;zUgc_A!Aaop0_Sa)R(8slD^Z%#9k%xTJXbNcPN1x>qdNi(ilQQ$Re z3c6-PvlG#8ClNhI676W^Q%529gU9B$8powr&w;E;g5RkT^L)-V0I%2BZ;UOhIggF= zrp!rMN@9 saGwLew%|Ny$Qfb$Ib_Un z_QkyB#mDA2X$V 3{ TAS<;H)m?u!$Rm)A@u1$o&xKlh`l{V+>pl}jj@G(q8x8iADjKh zlK*ujdg!gi_g<_kv=hGTA`-b;{5==kH;UsUQF%D$d2T}Q+wbPfg}vgv;F!0$wGpxX zC)5%u;`FGaDuh16WABx4^PWrj5b9$~yp{b(?7!oFbsW> d++JgRw%6I_<=RQ^1;ctA`_)hv&tvC`XI<=RCgdq{Y!fjaj*|#I z3pp+*;I^ko;9iJ+Wscs)LOjX{F+sj>vko~4e))s&%a2~}AjDn;A0V&4m62d?bDu%A zu~~oQv5R=mHDn6+1 z{JO@Tc 0EX8n(KSLl8`c2V)Mm%g#4*27K6) @>&69* yLOE$7C%LL!P_eMW~*wQ(F=$B-Ti@>8OQs$mnqkea34>$^Taq=2Oc{~{j75=c;0=)mVjOI!SOu4{>@VG zld??Np6yD(e>l_BC`bA&3O!nn*QI^0y$SU!Lj5Agy(D{+>lOK|b3aM;CGyx}oJ)vT z@Ys8$JI?ov&)fWahz%7Rzt1#F^t@HcGwx^4XZ_GSv3B?f;#X{U@z@- EVC#yu;P?l{MG7Ty#4Rx!Wl`^&xOx{bCF }zB45o ze1C}?aRBa>!|{PWldR~w1# 5W2<^KIyL z91Wj5_epX5L@#qm4)gfm{OmvpY05h;oOSjiN&W!Xp=q$8o3Jl{eZ0W04ZU$O`=>Sa zM#KaZK4~6bIuCp%$|CN)Gx%pmN_;KeGd}C=OHmt}{f3KEpY+sbKLn4j=pkW&n8Z2u z&r1GH$uG;j%sT~G3jOM>p<7rx7z@2yxqoYMa&UnTwc3krripd1;#ub&UP`~;Irz*h zBp(RZh_H{FZ4`{%2xBLe&>P`X-Tt&5t-0(@YcJNLhzp*y;er=!I$xhQpGS_tpFR|I z&X;!l(TMh6Lhpf;) r=dWZN zNwAYpmjM5g1#*EFLsw7Z_%?qpiKx4YW3x|& x(h21&w6(nk|0xZA GP(r%lC6G@gjV7&MPyTe#e{w z?^x2DzoD=Fh5V}r_Ov+{d>ipF+~57MH%)mj=a7*uKD&bL9R6%D?Af+s>X4;VZ6Vj# z2KmO;&b7(AjfwC*9x=%E;2a5@!R0u2y0FhW {4|#<|VFx#7=7v!2hNWqpfvj#;;@X#5d;kKA!NO*>{wGmoOze2|Us z{V5IGn9;Ud-l7hb-i$w+!g@026onO93A!=Yj<7AjxqVCUJ!2gITM+9k%)`pgm^NRm zPv^m(Acy(0`2J?tlKGajFvW@%CEHL0blTus_T*Gw%q #66cleO$Y&?$;%sMy#k&c<^PPV;p#9Ky&%bIR=(&(V z{C&|WtXr~fXX7gNT${4rPOzE66YJ6{@b<%R8lX>dBSC+;nBGFr*EqifcPa&XN;H2z z$STC*0>wTP`*b^d9cH#eA4`9q2jHkg=U1>oVYv zqLoh^g|%j#hWE_aq_#kY&}ZBCw4w9QS_&~B))(;E4BX+)@Ga>=KCfWE3ik%y_{5bO zqjwU=DeWNh>_N7Ub?EB=drElLT9CQ;Ec4zK;Cs4a49 G`N;ACvcVL*c?InG4DLM{hy1j2(6L$H z;a*FBziiL4J_Gz`1$dt+^IiTte-`$k-~$6s)rB9TGx%CG_*n{gSt0mXA=Wwt>lKZ8 ScgAOQSwCUE#WLgHzWyJ}Mdthf literal 0 HcmV?d00001 diff --git a/safe_locking_service/static/safe/logo.png b/safe_locking_service/static/safe/logo.png new file mode 100755 index 0000000000000000000000000000000000000000..5cbcc554e9b81ded351ac655756a8c0230c3638d GIT binary patch literal 3367 zcmZ{ncQD)w*T;WEj~cl;AxaQpl_-lSL3BctMBP}ORX2L?M7NPBOL8M&$!a0m3fbIP zEkSgm?jl%gb)vlPyfg32JI_3G=A7@GbLKPOKhK;rbJGV*3_J_~05BQo>sVZ>+`maj zb6Ev6QtK~;%3aGu3jpe~ubx7vFMB~3eG3x+2p0o@*hBz0xg5o=0RS8X0PD^G0DcJo zoc<+k57jOgG*68l=l~c0Mmgqv{^f+;U;l9s08pp>n-sntrT_A@!3HL}v@7(ibdu6= z_jd{aaCON*N9&Qt+=n8k=MFaTZ!z=W)$fz?F4^?~G<>Q;I`y6bc~9F}K7M@Fsf?E7 zd-U~&5T8KKj%I BA~G6#6D!t3FdOGa ZF^6;ifOZ0hv;fFY$8M-nkd{Cqv2#M~tMD`o7> zcIc>NV(!cx$PB*up{*H=OEG6AuP!LCl;fE;?5g%HY5b~#Kq82-=XktX71np>mq*W` zwEBJA$)vwr{I<^%SVST}=+mD>V!=$!P@-DAkHB)sXve*r8C|StLRLRf<$gxX3t>KC z*3g6TnTw0q+4jA`(Z?@mKH^pgU=bfjSP+j=;O49~wM}UmCs=HP4bl_qDoRp|*zomZ z!fo|C-ExWVV%;@Jds;gyzyTKb2~yDhs16Dj(8}b(@NhA<$8K;#%rZ8qs8|@FLd$Ch zL?sqH%ls*i@=L~6U)E>(ux$EYR@@yZ^0M F2(+@iJq#3sDe$f!?D?jP+EH5tj z?O#&CRPiTv5EcfLOx_|!yDN`uY{vv_sXw~Y*|D7{wRX~#NDr#FosDl?nQ96YVPg35 zY%$zol9tdg?F=7)VaszP#(Q5gX9q_};Pz|kdb-WxDDKg`qlT{beZDE@&pIu-d0YDr zfum}Up(+`&evq6Qx3RYD6}Lv2AG)WuHaymaHxW#tZV=3Q2AC~DVoohIxx{06O##N< z0G1=e8`OE-D9sbe4i8g0jDb`cKp9%{M5U$jdqP6mSzyGqJ-Wok>g4fKn{#d~porTS zg|NicAa$FtddV^@w#O$dEeDvgW$YUs(>cB;gt6}%`KrzJ`}r}0ZK`vB^$^V)l2hXC zQ?$yc@B9>uVcy=VPU(iMU2*MW5fb?Niz;-7l5%!=ptoybdWrka8CFUBJcM3?XD#yw zZ}h8xjrmBL{&4b4FS|lk?|qts@&$Lj^D)gumW~~U2cP`1H4Q!G8!=nwTp+9Dhn~nn zOCs4mFx^IG!Ou}Etht#HP2PD^eo;JsjV7vKbsKT94YH_(HEx4Sx#KP*Y^0QYL}+>z|O0|-Q3kV!cI!u@7KwEr-M`a z>7xO2 69B+te7RUW&QIXe&i`Q;#_K{d{{=ShhC&*+r1;>nt*50NQ?}c**51U za+7_@TxfgO?WNmkc)vmZK1M0-DzTTjA%EuFZt+o?&{BpKSR5!1SSGo;6cOrX-M7!= zuYZo_r>1xl`ui?A>QB;FW5IsO=oT;Z5nL1MSM0WacG%VCD7sOR@>JZC47I27aZFV2 zI^1=>pYKkKA3&^&XJ2Vs`gt;M4<%gvu~b+)u9xWXMiK3Pb|uD`g!v>&kgNbVvAx}O zYwu7ZXW6}+{-m1ZmFi-xXXo=4#2i=?kQK?i3ZQS`TUPi!*Ba{t=q>A0c5unrM$*T) zPzP4W*%svWo*mXL?%w$3_HO#vZLp}uq0>C}eXp?S)Wc( ?XGTl#8hfS+HXU|ERD`e5SAYcg5S1;R79GY6n5U{gy{aR%DrK zaS=JQC-1E(tXiinRY(hv7=&UM LkV;Hk&BVNFvNy-Y53)((|+9~StF~#SjGMdTbJDP}JMhU29 zVjus!HHCEUgTPUX%}zzeFoz88y_ln$h4(J*e%UZ0erH$dj6_WR;G=Twy}G~I(_=eW zKi)s`_ML%c^^>_5!t=I)tuN;FTDUy0&`4VkATUMke&dE@YevO$t`e08E!$$y@n*tw z?D>xiS`y0;*a$0f&-re0UX7_k<+CK~ofrK|E6f;ukmy%K=W7MCIE`SX#XS<8%;#aj zHXP}f7D;9<|E)Y?!%953MdlyBS5?ai7L@j_hU9i5(4Ln=zKO|j#2=12se|7OCWZx= z5@;l^F_Bx%`A>7>R@prUqltCA7*_E|`Kx_TYXsezcLNGl$dk@-$8lw;@N;)xh*K=2 zvSvWh7utXShTC!+i-JyNFK;c1FiVarpAb3q2os8xw)E&XE=4#n>7v4@h2$DanmE#N z<6L) byV#8Kz%nMHbvzgDf$&D6aqEGte-kMV zDOrS=*M#l_88z65B%Z31zQU#eT@OrCbo?CV6BaryNrf%GedJ8{LZmI67eZigWK=ag zwPtHAP%X)iG|JyL+2=tJ+$JA|CFG4#P~u~DU~b4G@q{ENte{8_7e;ly`{eVqU7=6~ zUXjvoAajK7Js7)T&m7%1{7Y1+4cbP-(fY_Bc;B}6FX{*E-0U%7O6-Ru^!VbJ99NtR zb$J7NgvlSZ*Y>`bhcnlbMk@ZWe;PIZ`$=Q*;?FVk$zAPXOl3~nKMdU%k0cj<$LdOZ z`gfbvXe;QNDrA_?$^Y~kA}({?kYF!rsoDjiOx}TH61*@vFewOotXIi6bAP3oHugx? zywJWgC!vh0^jW00L;!+wB9CCm?2<>b=!ll@D>fXqhOtX $eb> Dz`#^ktO;brX~?^s!lB0>C_Iwwq!_%dynJvK2E0=p5#I F_Kf>$_lNNG`DJxRP;Rbogq{6}QpXUDO`Lte7t0pYuQ}^urqq)>=bb(< zpp!~Xq;M>-_o%ZVvCQe+Q6c-0nG=7Nz-THMZf~L?{}M-P&8#vzQ<<7^uSSI Ylwb)(C2MpSwa`q05hYm6?+=!#an-EEqE&;N^P5ReSf2#Y06q_H~Y()rR#cr9u zcx}#uyk0#l6Q?4%$@aSOm02AqBE~ghgR{Dl6#!pVW3CA~KKEiOkb_ru?ZwJ<2WQK9 zhq(Hq9!uBSscV@&IUAn*!}ABR8CKULRKmos>y{y0J2ii|G?*duy7GE@#!7QuCaytg zLfp08zCRSMez1#j*f((PwlztjwNYj`uUvj_u9?c|rqKe>NKq zjvxcPoXn49_=HMw!ndYac}=%7US-#MZ7>lCJUKb?Oalqb^n+J|9FVrW&t<{%U%vOl z88=g3yX^S1))YCFoa5oX9gCDM-eg^dBDwdT&e=m0Ls9XiEpAC-XqIm|)iS{b=%3C# zNyvQf;Z#6sk4_Q4H~Joi>nuhWz$B1d+?sk)7a_~jw9~?q)YD=UdaoTR6?44lH%o>j zS~-$9x=fd%=WzFr=dWe$_V1T
XP?Sf|T>|3&=Me1a>lzdc d@%8^tM&a)Nnkas-#a}Xjfv%|zTH87Pe*o=+VKV># literal 0 HcmV?d00001 diff --git a/safe_locking_service/static/safe/logo.svg b/safe_locking_service/static/safe/logo.svg new file mode 100644 index 0000000..df57691 --- /dev/null +++ b/safe_locking_service/static/safe/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/safe_locking_service/static/safe/safe_contract_logo.png b/safe_locking_service/static/safe/safe_contract_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..14eb5bb0204a16d5146c21ddc61d958a0e55fe7e GIT binary patch literal 755 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvE8Azrw%`pX1HUT~%uK)l47ZMhF^yrbVpKo?f z_K_n;IyyTP6%{{x_)t| %z1i2w*M|Ois;S80 z=mH1-<-bpADQ1;_YWn&`!8}i^6Xt)E=RV&5yS4btB~Kp<*J*KTa}+(y1=A%C`$w zyi{M%%gB?!z$|fqfz7}n_&}O%f~nktBNYwiaso@1xA=1hT19 OecB+1?ysV9Pk%FjwM&p~A|Dm@*RupFp`Ixo)#3HLUzl(Jm&- z`0t0xoUVqzi^u VB@DZN0-Alr(`UdSL5Os@F#^2QRjU(8}FZvc(gNImG_@K*K1 z27!idku@jX7^X!;#ipq+ be|d!=mpR^3xq?CV%6y +{% endblock %} + +{% block main_styles %} + + + +{% endblock %} diff --git a/safe_locking_service/utils/__init__.py b/safe_locking_service/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/safe_locking_service/utils/exceptions.py b/safe_locking_service/utils/exceptions.py new file mode 100644 index 0000000..06b8875 --- /dev/null +++ b/safe_locking_service/utils/exceptions.py @@ -0,0 +1,39 @@ +import logging + +from rest_framework import status +from rest_framework.response import Response +from rest_framework.views import exception_handler + +logger = logging.getLogger(__name__) + + +def custom_exception_handler(exc, context): + if isinstance(exc, NodeConnectionException): + response = Response(status=status.HTTP_503_SERVICE_UNAVAILABLE) + + if str(exc): + exception_str = "{}: {}".format(exc.__class__.__name__, exc) + else: + exception_str = exc.__class__.__name__ + response.data = { + "exception": "Problem connecting to Ethereum network", + "trace": exception_str, + } + + logger.warning( + "%s - Exception: %s - Data received %s", + context["request"].build_absolute_uri(), + exception_str, + context["request"].data, + exc_info=exc, + ) + else: + # Call REST framework's default exception handler, + # to get the standard error response. + response = exception_handler(exc, context) + + return response + + +class NodeConnectionException(IOError): + pass diff --git a/safe_locking_service/utils/loggers.py b/safe_locking_service/utils/loggers.py new file mode 100644 index 0000000..11b9e11 --- /dev/null +++ b/safe_locking_service/utils/loggers.py @@ -0,0 +1,60 @@ +import logging +import time + +from django.http import HttpRequest + +from gunicorn import glogging + + +def get_milliseconds_now(): + return int(time.time() * 1000) + + +class IgnoreCheckUrl(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + message = record.getMessage() + return not ("GET /check/" in message and "200" in message) + + +class IgnoreSucceededNone(logging.Filter): + """ + Ignore Celery messages + They are usually emitted when a redis lock is active + """ + + def filter(self, rec: logging.LogRecord): + message = rec.getMessage() + return not ("Task" in message and "succeeded" in message and "None" in message) + + +class CustomGunicornLogger(glogging.Logger): + def setup(self, cfg): + super().setup(cfg) + + # Add filters to Gunicorn logger + logger = logging.getLogger("gunicorn.access") + logger.addFilter(IgnoreCheckUrl()) + + +class LoggingMiddleware: + def __init__(self, get_response): + self.get_response = get_response + self.logger = logging.getLogger("LoggingMiddleware") + + def __call__(self, request: HttpRequest): + milliseconds = get_milliseconds_now() + response = self.get_response(request) + if request.resolver_match: + route = ( + request.resolver_match.route if request.resolver_match else request.path + ) + delta = get_milliseconds_now() - milliseconds + self.logger.info( + "MT::%s::%s::%s::%d::%s", + request.method, + route, + delta, + response.status_code, + request.path, + ) + return response diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2292422 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,60 @@ +[flake8] +max-line-length = 88 +select = C,E,F,W,B,B950 +extend-ignore = E203,E501,F841,W503 +exclude = .tox,.git,*/static/CACHE/*,docs,node_modules,venv + +[pycodestyle] +max-line-length = 120 +exclude = .tox,.git,*/static/CACHE/*,docs,node_modules,venv + +[isort] +profile = black +default_section = THIRDPARTY +known_first_party = safe_locking_service +known_gnosis = py_eth_sig_utils,gnosis +known_django = django +sections = FUTURE,STDLIB,DJANGO,THIRDPARTY,GNOSIS,FIRSTPARTY,LOCALFOLDER + +[tool:pytest] +env = + DJANGO_SETTINGS_MODULE=config.settings.test + DJANGO_DOT_ENV_FILE=.env.test + +[mypy] +python_version = 3.11 +check_untyped_defs = True +ignore_missing_imports = True +warn_unused_ignores = True +warn_redundant_casts = True +warn_unused_configs = True +plugins = mypy_django_plugin.main + +[mypy.plugins.django-stubs] +django_settings_module = config.settings.test + +[mypy-*.migrations.*] +# Django migrations should not produce any errors: +ignore_errors = True + +[coverage:report] +exclude_lines = +# Have to re-enable the standard pragma + pragma: no cover + + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: + if settings.DEBUG + + # Ignore pass lines + pass + +[coverage:run] +include = safe_locking_service/* +omit = + *__init__.py* + *tests* + */migrations/* From d31c1188dec2d329009405f57571eccf2a1e5dbd Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:22:50 +0100 Subject: [PATCH 02/13] Add missing environment variables and containers --- .github/workflows/python.yml | 62 ++++++++++++++++++++++++++++++------ 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 68e2fc1..725bb77 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -2,7 +2,7 @@ name: Python CI on: push: branches: - - main + - master - develop pull_request: release: @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] steps: - uses: actions/checkout@v4 @@ -30,7 +30,29 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.11"] + python-version: ["3.12"] + services: + postgres: + image: postgres:14 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 + rabbitmq: + image: rabbitmq:alpine + options: >- + --health-cmd "rabbitmqctl await_startup" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - "5672:5672" steps: - name: Setup and run ganache run: | @@ -44,20 +66,22 @@ jobs: cache-dependency-path: 'requirements*.txt' - name: Install dependencies run: | - pip install wheel + pip install wheel setuptools pip install -r requirements-test.txt env: PIP_USE_MIRRORS: true - name: Run tests and coverage run: | python manage.py check - # python manage.py makemigrations --check --dry-run + python manage.py makemigrations --check --dry-run coverage run --source=$SOURCE_FOLDER -m pytest -rxXs --reruns 3 env: SOURCE_FOLDER: safe_locking_service + CELERY_BROKER_URL: redis://localhost:6379/0 + DATABASE_URL: psql://postgres:postgres@localhost/postgres DJANGO_SETTINGS_MODULE: config.settings.test ETHEREUM_MAINNET_NODE: ${{ secrets.ETHEREUM_MAINNET_NODE }} - ETHEREUM_NODES_URLS: http://localhost:8545 + ETHEREUM_NODE_URL: http://localhost:8545 ETH_HASH_BACKEND: pysha3 - name: Coveralls uses: coverallsapp/github-action@v2 @@ -66,7 +90,7 @@ jobs: needs: - linting - test-app - if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop' || (github.event_name == 'release' && github.event.action == 'released') + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || (github.event_name == 'release' && github.event.action == 'released') steps: - uses: actions/checkout@v4 - uses: docker/setup-qemu-action@v3 @@ -78,8 +102,8 @@ jobs: with: username: ${{ secrets.DOCKER_USER }} password: ${{ secrets.DOCKER_PASSWORD }} - - name: Deploy main - if: github.ref == 'refs/heads/main' + - name: Deploy Master + if: github.ref == 'refs/heads/master' uses: docker/build-push-action@v5 with: context: . @@ -119,3 +143,23 @@ jobs: linux/arm64 cache-from: type=gha cache-to: type=gha,mode=max + autodeploy: + runs-on: ubuntu-latest + needs: [docker-deploy] + if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' + steps: + - uses: actions/checkout@v4 + - name: Deploy Staging + if: github.ref == 'refs/heads/master' + run: bash scripts/autodeploy.sh + env: + AUTODEPLOY_URL: ${{ secrets.AUTODEPLOY_URL }} + AUTODEPLOY_TOKEN: ${{ secrets.AUTODEPLOY_TOKEN }} + TARGET_ENV: "staging" + - name: Deploy Develop + if: github.ref == 'refs/heads/develop' + run: bash scripts/autodeploy.sh + env: + AUTODEPLOY_URL: ${{ secrets.AUTODEPLOY_URL }} + AUTODEPLOY_TOKEN: ${{ secrets.AUTODEPLOY_TOKEN }} + TARGET_ENV: "develop" \ No newline at end of file From 86d82644e9b04a63f34cfcb70fc68e41e80bc505 Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:30:53 +0100 Subject: [PATCH 03/13] Add docker-compose for local tests --- docker-compose.dev.yml | 8 ++++++++ run_tests.sh | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) create mode 100644 docker-compose.dev.yml diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..cd9d932 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,8 @@ +version: '3.5' + +services: + ganache: + image: trufflesuite/ganache:latest + command: --defaultBalanceEther 10000 --gasLimit 10000000 -a 10 --chain.chainId 1337 --chain.networkId 1337 -d --host 0.0.0.0 + ports: + - "8545:8545" \ No newline at end of file diff --git a/run_tests.sh b/run_tests.sh index f2cda5e..e6d6ff9 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -4,9 +4,9 @@ set -euo pipefail export DJANGO_SETTINGS_MODULE=config.settings.test export DJANGO_DOT_ENV_FILE=.env.test -docker compose -f docker-compose.yml -f docker-compose.dev.yml build --force-rm db redis ganache rabbitmq -docker compose -f docker-compose.yml -f docker-compose.dev.yml up --no-start db redis ganache rabbitmq -docker compose -f docker-compose.yml -f docker-compose.dev.yml start db redis ganache rabbitmq +docker compose -f docker-compose.yml -f docker-compose.dev.yml build --force-rm db ganache rabbitmq +docker compose -f docker-compose.yml -f docker-compose.dev.yml up --no-start db ganache rabbitmq +docker compose -f docker-compose.yml -f docker-compose.dev.yml start db ganache rabbitmq sleep 10 From 343087336186b42a1b40720e9d1ddb2dd454ed1a Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:40:16 +0100 Subject: [PATCH 04/13] Add test about view --- .env.test | 2 +- safe_locking_service/locking_events/tests.py | 3 --- safe_locking_service/locking_events/tests/__init__.py | 0 .../locking_events/tests/test_views.py | 11 +++++++++++ 4 files changed, 12 insertions(+), 4 deletions(-) delete mode 100644 safe_locking_service/locking_events/tests.py create mode 100644 safe_locking_service/locking_events/tests/__init__.py create mode 100644 safe_locking_service/locking_events/tests/test_views.py diff --git a/.env.test b/.env.test index fabd26c..ce38ce6 100644 --- a/.env.test +++ b/.env.test @@ -4,6 +4,6 @@ DJANGO_SETTINGS_MODULE=config.settings.test DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y ETH_HASH_BACKEND=pysha3 -DATABASE_URL=psql://postgres:postgres@db:5432/postgres +DATABASE_URL=psql://postgres:postgres@localhost:5432/postgres # Only required for testing ETHEREUM_MAINNET_NODE=https://ethereum.publicnode.com diff --git a/safe_locking_service/locking_events/tests.py b/safe_locking_service/locking_events/tests.py deleted file mode 100644 index a79ca8b..0000000 --- a/safe_locking_service/locking_events/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -# from django.test import TestCase - -# Create your tests here. diff --git a/safe_locking_service/locking_events/tests/__init__.py b/safe_locking_service/locking_events/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/safe_locking_service/locking_events/tests/test_views.py b/safe_locking_service/locking_events/tests/test_views.py new file mode 100644 index 0000000..2be463a --- /dev/null +++ b/safe_locking_service/locking_events/tests/test_views.py @@ -0,0 +1,11 @@ +from django.test import TestCase +from django.urls import reverse + +from rest_framework import status + + +class TestQueueService(TestCase): + def test_about_view(self): + url = reverse("v1:locking_events:about") + response = self.client.get(url, format="json") + self.assertEqual(response.status_code, status.HTTP_200_OK) From 9f8d6b0d5de63f49cca47be933e60c50b80e721f Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:47:08 +0100 Subject: [PATCH 05/13] Update dependabot reviewer --- .github/dependabot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 2a5f193..75b5422 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -6,7 +6,7 @@ updates: interval: weekly day: monday reviewers: - - "safe-global/safe-services" + - "safe-global/core-api" - package-ecosystem: docker directory: "/docker/web" @@ -14,7 +14,7 @@ updates: interval: weekly day: monday reviewers: - - "safe-global/safe-services" + - "safe-global/core-api" - package-ecosystem: github-actions directory: "/" @@ -22,4 +22,4 @@ updates: interval: weekly day: monday reviewers: - - "safe-global/safe-services" + - "safe-global/core-api" From b40a810a298375ee77d2928ea2f977ffaf2a80d4 Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:51:47 +0100 Subject: [PATCH 06/13] Add about endpoint doc --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 99f54b4..3f7cf72 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ [](https://github.com/safe-global/safe-locking-service/actions/workflows/python.yml) [](https://coveralls.io/github/safe-global/safe-locking-service?branch=main) [](https://github.com/pre-commit/pre-commit) - - + + [](https://hub.docker.com/r/safeglobal/safe-locking-service) # Safe Locking Service @@ -17,7 +17,7 @@ cp .env.sample .env Configure environment variables on `.env`: - `DJANGO_SECRET_KEY`: **IMPORTANT: Update it with a secure generated string**. -- `ETHEREUM_NODES_URLS`: RPC node url. +- `ETHEREUM_NODE_URL`: RPC node url. ## Execution @@ -29,7 +29,7 @@ docker compose up Then go to http://localhost:8000 to see the service documentation. ## Endpoints -To be defined +- /v1/about ## Contributors [See contributors](https://github.com/safe-global/safe-locking-service/graphs/contributors) From a40acfd62a9e7d5fce413204630244e19327a031 Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:07:40 +0100 Subject: [PATCH 07/13] Add `Setup for development` section --- .env.docker | 7 +++++++ .env.sample | 2 +- README.md | 17 +++++++++++++++++ docker/web/Dockerfile | 2 +- 4 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 .env.docker diff --git a/.env.docker b/.env.docker new file mode 100644 index 0000000..86879c9 --- /dev/null +++ b/.env.docker @@ -0,0 +1,7 @@ +PYTHONPATH=/app/ +DEBUG=0 +DJANGO_SETTINGS_MODULE=config.settings.test +DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y +ETH_HASH_BACKEND=pysha3 + +DATABASE_URL=psql://postgres:postgres@db:5432/postgres diff --git a/.env.sample b/.env.sample index 86879c9..e7f2597 100644 --- a/.env.sample +++ b/.env.sample @@ -4,4 +4,4 @@ DJANGO_SETTINGS_MODULE=config.settings.test DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y ETH_HASH_BACKEND=pysha3 -DATABASE_URL=psql://postgres:postgres@db:5432/postgres +DATABASE_URL=psql://postgres:postgres@localhost:5432/postgres diff --git a/README.md b/README.md index 3f7cf72..5927999 100644 --- a/README.md +++ b/README.md @@ -31,5 +31,22 @@ Then go to http://localhost:8000 to see the service documentation. ## Endpoints - /v1/about +## Setup for development +Use a virtualenv if possible: + +```bash +python -m venv venv +``` + +Then enter the virtualenv and install the dependencies: + +```bash +source venv/bin/activate +pip install -r requirements-dev.txt +pre-commit install -f +cp .env.sample .env +./run_tests.sh +``` + ## Contributors [See contributors](https://github.com/safe-global/safe-locking-service/graphs/contributors) diff --git a/docker/web/Dockerfile b/docker/web/Dockerfile index bb10c49..35f809c 100644 --- a/docker/web/Dockerfile +++ b/docker/web/Dockerfile @@ -35,4 +35,4 @@ COPY --chown=python:python . . # Use numeric ids so kubernetes identifies the user correctly USER 999:999 -RUN DJANGO_SETTINGS_MODULE=config.settings.production DJANGO_DOT_ENV_FILE=.env.sample python manage.py collectstatic --noinput +RUN DJANGO_SETTINGS_MODULE=config.settings.production DJANGO_DOT_ENV_FILE=.env.docker python manage.py collectstatic --noinput From 9379549eaa037d3a2a63df9741355114af0b05e8 Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Thu, 22 Feb 2024 18:10:54 +0100 Subject: [PATCH 08/13] Update to python 3.12 --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 2292422..1172726 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ env = DJANGO_DOT_ENV_FILE=.env.test [mypy] -python_version = 3.11 +python_version = 3.12 check_untyped_defs = True ignore_missing_imports = True warn_unused_ignores = True From 0b0831773bb8958cf0bb7d260cfb18c4da8ed93c Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:12:55 +0100 Subject: [PATCH 09/13] Disable coverage --- .github/workflows/python.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/python.yml b/.github/workflows/python.yml index 725bb77..f51e8d9 100644 --- a/.github/workflows/python.yml +++ b/.github/workflows/python.yml @@ -70,11 +70,10 @@ jobs: pip install -r requirements-test.txt env: PIP_USE_MIRRORS: true - - name: Run tests and coverage + - name: Run tests run: | python manage.py check python manage.py makemigrations --check --dry-run - coverage run --source=$SOURCE_FOLDER -m pytest -rxXs --reruns 3 env: SOURCE_FOLDER: safe_locking_service CELERY_BROKER_URL: redis://localhost:6379/0 @@ -83,8 +82,6 @@ jobs: ETHEREUM_MAINNET_NODE: ${{ secrets.ETHEREUM_MAINNET_NODE }} ETHEREUM_NODE_URL: http://localhost:8545 ETH_HASH_BACKEND: pysha3 - - name: Coveralls - uses: coverallsapp/github-action@v2 docker-deploy: runs-on: ubuntu-latest needs: From 44b63a2a8704e2389cb0dcfbbc35193c1b9c539b Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:18:14 +0100 Subject: [PATCH 10/13] Add redis cache --- .env.docker | 2 +- .env.sample | 2 +- config/settings/base.py | 2 ++ config/settings/production.py | 21 ++++++++++++++++++++- docker-compose.yml | 7 +++++++ requirements.txt | 1 + 6 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.env.docker b/.env.docker index 86879c9..f4fa022 100644 --- a/.env.docker +++ b/.env.docker @@ -3,5 +3,5 @@ DEBUG=0 DJANGO_SETTINGS_MODULE=config.settings.test DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y ETH_HASH_BACKEND=pysha3 - +REDIS_URL=redis://redis:6379/0 DATABASE_URL=psql://postgres:postgres@db:5432/postgres diff --git a/.env.sample b/.env.sample index e7f2597..dc9913f 100644 --- a/.env.sample +++ b/.env.sample @@ -3,5 +3,5 @@ DEBUG=0 DJANGO_SETTINGS_MODULE=config.settings.test DJANGO_SECRET_KEY=t3st-s3cr3t#-!k3y ETH_HASH_BACKEND=pysha3 - +REDIS_URL=redis://localhost:6379/0 DATABASE_URL=psql://postgres:postgres@localhost:5432/postgres diff --git a/config/settings/base.py b/config/settings/base.py index 97eb158..e20e347 100644 --- a/config/settings/base.py +++ b/config/settings/base.py @@ -348,5 +348,7 @@ }, } +REDIS_URL = env("REDIS_URL", default="redis://localhost:6379/0") + # Ethereum ETHEREUM_NODE_URL = env("ETHEREUM_NODE_URL", default=None) diff --git a/config/settings/production.py b/config/settings/production.py index 699f42c..24ff0d3 100644 --- a/config/settings/production.py +++ b/config/settings/production.py @@ -1,5 +1,5 @@ from .base import * # noqa -from .base import env +from .base import REDIS_URL, env # GENERAL # ------------------------------------------------------------------------------ @@ -13,6 +13,21 @@ # DATABASES['default'] = env.db('DATABASE_URL') # noqa F405 DATABASES["default"]["ATOMIC_REQUESTS"] = False # noqa F405 +# CACHES +# ------------------------------------------------------------------------------ +CACHES = { + "default": { + "BACKEND": "django_redis.cache.RedisCache", + "LOCATION": REDIS_URL, + "OPTIONS": { + "CLIENT_CLASS": "django_redis.client.DefaultClient", + # Mimicing memcache behavior. + # http://niwinz.github.io/django-redis/latest/#_memcached_exceptions_behavior + "IGNORE_EXCEPTIONS": True, + }, + } +} + # SECURITY # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/middleware/#x-content-type-options-nosniff @@ -27,6 +42,10 @@ # Django Admin URL regex. ADMIN_URL = env("DJANGO_ADMIN_URL", default="admin/") +# CELERY +# ------------------------------------------------------------------------------ +CELERY_RESULT_BACKEND = env("CELERY_RESULT_BACKEND", default=REDIS_URL) + # Gunicorn # ------------------------------------------------------------------------------ INSTALLED_APPS += ["gunicorn"] # noqa F405 diff --git a/docker-compose.yml b/docker-compose.yml index de667af..a378317 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,13 @@ services: - nginx-shared:/nginx command: docker/web/run_web.sh + redis: + image: redis:alpine + ports: + - "6379:6379" + command: + - --appendonly yes + rabbitmq: image: rabbitmq:alpine ports: diff --git a/requirements.txt b/requirements.txt index a8c6274..80d3e5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,6 +9,7 @@ django-debug-toolbar django-debug-toolbar-force django-environ==0.11.2 django-extensions==3.2.3 +django-redis==5.4.0 djangorestframework==3.14.0 djangorestframework-camel-case==1.4.2 docutils==0.20.1 From 66b70e23021eabdcfb91efe0c9372b4f60ae1096 Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Tue, 27 Feb 2024 11:55:32 +0100 Subject: [PATCH 11/13] Add locking events model --- config/settings/test.py | 2 +- .../locking_events/migrations/0001_initial.py | 107 ++++++++++++++++++ safe_locking_service/locking_events/models.py | 80 ++++++++++++- .../locking_events/tests/factories.py | 58 ++++++++++ .../locking_events/tests/test_models.py | 61 ++++++++++ 5 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 safe_locking_service/locking_events/migrations/0001_initial.py create mode 100644 safe_locking_service/locking_events/tests/factories.py create mode 100644 safe_locking_service/locking_events/tests/test_models.py diff --git a/config/settings/test.py b/config/settings/test.py index 51b02b8..6095280 100644 --- a/config/settings/test.py +++ b/config/settings/test.py @@ -8,7 +8,7 @@ # GENERAL # ------------------------------------------------------------------------------ # https://docs.djangoproject.com/en/dev/ref/settings/#debug -DEBUG = True +DEBUG = False # https://docs.djangoproject.com/en/dev/ref/settings/#secret-key SECRET_KEY = env( "DJANGO_SECRET_KEY", diff --git a/safe_locking_service/locking_events/migrations/0001_initial.py b/safe_locking_service/locking_events/migrations/0001_initial.py new file mode 100644 index 0000000..54de15b --- /dev/null +++ b/safe_locking_service/locking_events/migrations/0001_initial.py @@ -0,0 +1,107 @@ +# Generated by Django 4.2.10 on 2024-02-27 10:44 + +import django.db.models.deletion +from django.db import migrations, models + +import gnosis.eth.django.models + + +class Migration(migrations.Migration): + initial = True + + dependencies = [] + + operations = [ + migrations.CreateModel( + name="EthereumTx", + fields=[ + ("created", models.DateTimeField(auto_now_add=True)), + ( + "tx_hash", + gnosis.eth.django.models.Keccak256Field( + primary_key=True, serialize=False + ), + ), + ("block_hash", gnosis.eth.django.models.Keccak256Field()), + ("block_number", models.PositiveIntegerField()), + ("block_timestamp", models.DateTimeField()), + ], + ), + migrations.CreateModel( + name="UnlockEvent", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), + ("amount", models.PositiveIntegerField()), + ("log_index", models.PositiveIntegerField()), + ("block_timestamp", models.DateTimeField()), + ("unlock_index", models.PositiveIntegerField()), + ( + "ethereum_tx", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="locking_events.ethereumtx", + ), + ), + ], + ), + migrations.CreateModel( + name="WithdrawnEvent", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), + ("amount", models.PositiveIntegerField()), + ("log_index", models.PositiveIntegerField()), + ("block_timestamp", models.DateTimeField()), + ( + "ethereum_tx", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="locking_events.ethereumtx", + ), + ), + ( + "unlock_index", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="locking_events.unlockevent", + ), + ), + ], + ), + migrations.CreateModel( + name="LockEvent", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ("created", models.DateTimeField(auto_now_add=True)), + ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), + ("amount", models.PositiveIntegerField()), + ("log_index", models.PositiveIntegerField()), + ("block_timestamp", models.DateTimeField()), + ( + "ethereum_tx", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="locking_events.ethereumtx", + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.AddConstraint( + model_name="withdrawnevent", + constraint=models.UniqueConstraint( + fields=("holder", "unlock_index"), name="unique_withdrawn_event_index" + ), + ), + migrations.AddConstraint( + model_name="unlockevent", + constraint=models.UniqueConstraint( + fields=("holder", "unlock_index"), name="unique_unlock_event_index" + ), + ), + ] diff --git a/safe_locking_service/locking_events/models.py b/safe_locking_service/locking_events/models.py index 0b4331b..ecf18c8 100644 --- a/safe_locking_service/locking_events/models.py +++ b/safe_locking_service/locking_events/models.py @@ -1,3 +1,79 @@ -# from django.db import models +from django.db import models -# Create your models here. +from gnosis.eth.django.models import EthereumAddressV2Field, Keccak256Field + + +class EthereumTx(models.Model): + created = models.DateTimeField(auto_now_add=True) + tx_hash = Keccak256Field(primary_key=True) + block_hash = Keccak256Field() + block_number = models.PositiveIntegerField() + block_timestamp = models.DateTimeField() + + def __str__(self): + return f"Transaction hash {self.tx_hash}" + + +class CommonEvent(models.Model): + """ + Abstract model that defines generic fields of a locking event. (Abstract model doesn't create tables) + The block timestamp is stored also in this model to improve the query performance. + """ + + id = models.AutoField(primary_key=True) + created = models.DateTimeField(auto_now_add=True) + holder = EthereumAddressV2Field() + ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE) + amount = models.PositiveIntegerField() + log_index = models.PositiveIntegerField() + block_timestamp = models.DateTimeField() + + class Meta: + abstract = True + + +class LockEvent(CommonEvent): + """ + Model to store event Locked(address indexed holder, uint96 amount) + """ + + pass + + def __str__(self): + return f"Holder {self.holder} locked {self.amount} Safe tokens" + + +class UnlockEvent(CommonEvent): + """ + Model to store event Unlocked(address indexed holder, uint32 indexed index, uint96 amount) + """ + + unlock_index = models.PositiveIntegerField() + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["holder", "unlock_index"], name="unique_unlock_event_index" + ) + ] + + def __str__(self): + return f"Holder {self.holder} unlocked {self.amount} Safe tokens" + + +class WithdrawnEvent(CommonEvent): + """ + Model to store event Withdrawn(address indexed holder, uint32 indexed index, uint96 amount) + """ + + unlock_index = models.ForeignKey(UnlockEvent, on_delete=models.CASCADE) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=["holder", "unlock_index"], name="unique_withdrawn_event_index" + ) + ] + + def __str__(self): + return f"Holder {self.holder} withdrawn {self.amount} Safe tokens" diff --git a/safe_locking_service/locking_events/tests/factories.py b/safe_locking_service/locking_events/tests/factories.py new file mode 100644 index 0000000..d593922 --- /dev/null +++ b/safe_locking_service/locking_events/tests/factories.py @@ -0,0 +1,58 @@ +from django.utils import timezone + +from eth_account import Account +from factory import LazyFunction, Sequence, SubFactory, fuzzy +from factory.django import DjangoModelFactory +from web3 import Web3 + +from safe_locking_service.locking_events.models import ( + EthereumTx, + LockEvent, + UnlockEvent, + WithdrawnEvent, +) + + +class EthereumTxFactory(DjangoModelFactory): + class Meta: + model = EthereumTx + + tx_hash = Sequence(lambda n: Web3.keccak(text=f"tx_hash-{n}").hex()) + block_hash = Sequence(lambda n: Web3.keccak(text=f"tx_hash-{n}").hex()) + block_number = Sequence(lambda n: n + 1) + block_timestamp = LazyFunction(timezone.now) + + +class LockEventFactory(DjangoModelFactory): + class Meta: + model = LockEvent + + holder = LazyFunction(lambda: Account.create().address) + ethereum_tx = SubFactory(EthereumTxFactory) + amount = fuzzy.FuzzyInteger(0, 1000) + log_index = Sequence(lambda n: n) + block_timestamp = LazyFunction(timezone.now) + + +class UnlockEventFactory(DjangoModelFactory): + class Meta: + model = UnlockEvent + + holder = LazyFunction(lambda: Account.create().address) + ethereum_tx = SubFactory(EthereumTxFactory) + unlock_index = Sequence(lambda n: n + 1) + amount = fuzzy.FuzzyInteger(0, 1000) + log_index = Sequence(lambda n: n) + block_timestamp = LazyFunction(timezone.now) + + +class WithdrawnEventFactory(DjangoModelFactory): + class Meta: + model = WithdrawnEvent + + holder = LazyFunction(lambda: Account.create().address) + ethereum_tx = SubFactory(EthereumTxFactory) + unlock_index = SubFactory(UnlockEventFactory) + amount = fuzzy.FuzzyInteger(0, 1000) + log_index = Sequence(lambda n: n) + block_timestamp = LazyFunction(timezone.now) diff --git a/safe_locking_service/locking_events/tests/test_models.py b/safe_locking_service/locking_events/tests/test_models.py new file mode 100644 index 0000000..50ea99d --- /dev/null +++ b/safe_locking_service/locking_events/tests/test_models.py @@ -0,0 +1,61 @@ +from django.db import IntegrityError +from django.test import TestCase + +from eth_account import Account + +from safe_locking_service.locking_events.models import ( + EthereumTx, + LockEvent, + UnlockEvent, + WithdrawnEvent, +) +from safe_locking_service.locking_events.tests.factories import ( + LockEventFactory, + UnlockEventFactory, + WithdrawnEventFactory, +) + + +class TestLockingModel(TestCase): + def test_create_lock_event(self): + safe_address = Account.create().address + ethereum_tx = LockEventFactory(holder=safe_address, amount=1000).ethereum_tx + self.assertEqual(EthereumTx.objects.count(), 1) + lock_event = LockEvent.objects.filter( + holder=safe_address, ethereum_tx=ethereum_tx + )[0] + self.assertEqual(lock_event.holder, safe_address) + self.assertEqual(lock_event.amount, 1000) + + def test_create_unlock_event(self): + safe_address = Account.create().address + ethereum_tx = UnlockEventFactory(holder=safe_address, amount=1000).ethereum_tx + self.assertEqual(EthereumTx.objects.count(), 1) + unlock_event = UnlockEvent.objects.filter( + holder=safe_address, ethereum_tx=ethereum_tx + )[0] + self.assertEqual(unlock_event.holder, safe_address) + self.assertEqual(unlock_event.amount, 1000) + with self.assertRaisesMessage(IntegrityError, "violates unique constraint"): + UnlockEventFactory( + holder=safe_address, amount=1000, unlock_index=unlock_event.unlock_index + ) + + def test_create_withdrawn_event(self): + safe_address = Account.create().address + ethereum_tx = WithdrawnEventFactory( + holder=safe_address, amount=1000 + ).ethereum_tx + # Expected at least two transactions, one for unlock and other for withdrawn + self.assertEqual(EthereumTx.objects.count(), 2) + withdrawn_event = WithdrawnEvent.objects.filter( + holder=safe_address, ethereum_tx=ethereum_tx + )[0] + self.assertEqual(withdrawn_event.holder, safe_address) + self.assertEqual(withdrawn_event.amount, 1000) + with self.assertRaisesMessage(IntegrityError, "violates unique constraint"): + WithdrawnEventFactory( + holder=safe_address, + amount=1000, + unlock_index=withdrawn_event.unlock_index, + ) From 7e14bd84df1479e4725e3bbd1e3f599f151d274e Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Tue, 27 Feb 2024 18:45:38 +0100 Subject: [PATCH 12/13] Add review suggestions --- requirements.txt | 2 +- .../locking_events/migrations/0001_initial.py | 30 ++++++++++--------- safe_locking_service/locking_events/models.py | 29 ++++++++---------- .../locking_events/tests/factories.py | 22 +++++++------- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/requirements.txt b/requirements.txt index 80d3e5d..8a11616 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,5 +19,5 @@ hexbytes==0.3.1 packaging>=21 psycopg2==2.9.9 requests==2.31.0 -safe-eth-py[django]==6.0.0b16 +safe-eth-py[django]==6.0.0b17 web3==6.15.1 diff --git a/safe_locking_service/locking_events/migrations/0001_initial.py b/safe_locking_service/locking_events/migrations/0001_initial.py index 54de15b..2f783ff 100644 --- a/safe_locking_service/locking_events/migrations/0001_initial.py +++ b/safe_locking_service/locking_events/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-02-27 10:44 +# Generated by Django 4.2.10 on 2024-02-27 17:41 import django.db.models.deletion from django.db import migrations, models @@ -15,7 +15,6 @@ class Migration(migrations.Migration): migrations.CreateModel( name="EthereumTx", fields=[ - ("created", models.DateTimeField(auto_now_add=True)), ( "tx_hash", gnosis.eth.django.models.Keccak256Field( @@ -31,11 +30,10 @@ class Migration(migrations.Migration): name="UnlockEvent", fields=[ ("id", models.AutoField(primary_key=True, serialize=False)), - ("created", models.DateTimeField(auto_now_add=True)), - ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), - ("amount", models.PositiveIntegerField()), + ("timestamp", models.DateTimeField()), ("log_index", models.PositiveIntegerField()), - ("block_timestamp", models.DateTimeField()), + ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), + ("amount", gnosis.eth.django.models.Uint96Field()), ("unlock_index", models.PositiveIntegerField()), ( "ethereum_tx", @@ -50,11 +48,10 @@ class Migration(migrations.Migration): name="WithdrawnEvent", fields=[ ("id", models.AutoField(primary_key=True, serialize=False)), - ("created", models.DateTimeField(auto_now_add=True)), - ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), - ("amount", models.PositiveIntegerField()), + ("timestamp", models.DateTimeField()), ("log_index", models.PositiveIntegerField()), - ("block_timestamp", models.DateTimeField()), + ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), + ("amount", gnosis.eth.django.models.Uint96Field()), ( "ethereum_tx", models.ForeignKey( @@ -75,11 +72,10 @@ class Migration(migrations.Migration): name="LockEvent", fields=[ ("id", models.AutoField(primary_key=True, serialize=False)), - ("created", models.DateTimeField(auto_now_add=True)), - ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), - ("amount", models.PositiveIntegerField()), + ("timestamp", models.DateTimeField()), ("log_index", models.PositiveIntegerField()), - ("block_timestamp", models.DateTimeField()), + ("holder", gnosis.eth.django.models.EthereumAddressV2Field()), + ("amount", gnosis.eth.django.models.Uint96Field()), ( "ethereum_tx", models.ForeignKey( @@ -104,4 +100,10 @@ class Migration(migrations.Migration): fields=("holder", "unlock_index"), name="unique_unlock_event_index" ), ), + migrations.AddConstraint( + model_name="lockevent", + constraint=models.UniqueConstraint( + fields=("ethereum_tx", "log_index"), name="unique_ethereum_tx_log_index" + ), + ), ] diff --git a/safe_locking_service/locking_events/models.py b/safe_locking_service/locking_events/models.py index ecf18c8..d745423 100644 --- a/safe_locking_service/locking_events/models.py +++ b/safe_locking_service/locking_events/models.py @@ -1,10 +1,9 @@ from django.db import models -from gnosis.eth.django.models import EthereumAddressV2Field, Keccak256Field +from gnosis.eth.django.models import EthereumAddressV2Field, Keccak256Field, Uint96Field class EthereumTx(models.Model): - created = models.DateTimeField(auto_now_add=True) tx_hash = Keccak256Field(primary_key=True) block_hash = Keccak256Field() block_number = models.PositiveIntegerField() @@ -17,19 +16,26 @@ def __str__(self): class CommonEvent(models.Model): """ Abstract model that defines generic fields of a locking event. (Abstract model doesn't create tables) - The block timestamp is stored also in this model to improve the query performance. + The timestamp is stored also in this model to improve the query performance. """ id = models.AutoField(primary_key=True) - created = models.DateTimeField(auto_now_add=True) - holder = EthereumAddressV2Field() + timestamp = models.DateTimeField() ethereum_tx = models.ForeignKey(EthereumTx, on_delete=models.CASCADE) - amount = models.PositiveIntegerField() log_index = models.PositiveIntegerField() - block_timestamp = models.DateTimeField() + holder = EthereumAddressV2Field() + amount = Uint96Field() class Meta: abstract = True + constraints = [ + models.UniqueConstraint( + fields=["ethereum_tx", "log_index"], name="unique_ethereum_tx_log_index" + ) + ] + + def __str__(self): + return f"timestamp={self.timestamp} tx-hash={self.ethereum_tx_id} log_index={self.log_index} holder={self.holder}" class LockEvent(CommonEvent): @@ -39,9 +45,6 @@ class LockEvent(CommonEvent): pass - def __str__(self): - return f"Holder {self.holder} locked {self.amount} Safe tokens" - class UnlockEvent(CommonEvent): """ @@ -57,9 +60,6 @@ class Meta: ) ] - def __str__(self): - return f"Holder {self.holder} unlocked {self.amount} Safe tokens" - class WithdrawnEvent(CommonEvent): """ @@ -74,6 +74,3 @@ class Meta: fields=["holder", "unlock_index"], name="unique_withdrawn_event_index" ) ] - - def __str__(self): - return f"Holder {self.holder} withdrawn {self.amount} Safe tokens" diff --git a/safe_locking_service/locking_events/tests/factories.py b/safe_locking_service/locking_events/tests/factories.py index d593922..150a4e8 100644 --- a/safe_locking_service/locking_events/tests/factories.py +++ b/safe_locking_service/locking_events/tests/factories.py @@ -27,32 +27,32 @@ class LockEventFactory(DjangoModelFactory): class Meta: model = LockEvent - holder = LazyFunction(lambda: Account.create().address) + timestamp = LazyFunction(timezone.now) ethereum_tx = SubFactory(EthereumTxFactory) - amount = fuzzy.FuzzyInteger(0, 1000) log_index = Sequence(lambda n: n) - block_timestamp = LazyFunction(timezone.now) + amount = fuzzy.FuzzyInteger(0, 1000) + holder = LazyFunction(lambda: Account.create().address) class UnlockEventFactory(DjangoModelFactory): class Meta: model = UnlockEvent - holder = LazyFunction(lambda: Account.create().address) + timestamp = LazyFunction(timezone.now) ethereum_tx = SubFactory(EthereumTxFactory) - unlock_index = Sequence(lambda n: n + 1) - amount = fuzzy.FuzzyInteger(0, 1000) log_index = Sequence(lambda n: n) - block_timestamp = LazyFunction(timezone.now) + holder = LazyFunction(lambda: Account.create().address) + amount = fuzzy.FuzzyInteger(0, 1000) + unlock_index = Sequence(lambda n: n + 1) class WithdrawnEventFactory(DjangoModelFactory): class Meta: model = WithdrawnEvent - holder = LazyFunction(lambda: Account.create().address) + timestamp = LazyFunction(timezone.now) ethereum_tx = SubFactory(EthereumTxFactory) - unlock_index = SubFactory(UnlockEventFactory) - amount = fuzzy.FuzzyInteger(0, 1000) log_index = Sequence(lambda n: n) - block_timestamp = LazyFunction(timezone.now) + holder = LazyFunction(lambda: Account.create().address) + amount = fuzzy.FuzzyInteger(0, 1000) + unlock_index = SubFactory(UnlockEventFactory) From e443461d9d1bffb85d025df4a00dbc67452d743c Mon Sep 17 00:00:00 2001 From: moisses89 <7888669+moisses89@users.noreply.github.com> Date: Wed, 28 Feb 2024 12:09:12 +0100 Subject: [PATCH 13/13] Add __str__ function to each event --- safe_locking_service/locking_events/models.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/safe_locking_service/locking_events/models.py b/safe_locking_service/locking_events/models.py index d745423..a4f1037 100644 --- a/safe_locking_service/locking_events/models.py +++ b/safe_locking_service/locking_events/models.py @@ -35,7 +35,7 @@ class Meta: ] def __str__(self): - return f"timestamp={self.timestamp} tx-hash={self.ethereum_tx_id} log_index={self.log_index} holder={self.holder}" + return f"timestamp={self.timestamp} tx-hash={self.ethereum_tx_id} log_index={self.log_index} holder={self.holder} amount={self.amount}" class LockEvent(CommonEvent): @@ -45,6 +45,9 @@ class LockEvent(CommonEvent): pass + def __str__(self): + return "LockEvent: " + super().__str__() + class UnlockEvent(CommonEvent): """ @@ -60,6 +63,9 @@ class Meta: ) ] + def __str__(self): + return "UnlockEvent: " + super().__str__() + class WithdrawnEvent(CommonEvent): """ @@ -74,3 +80,6 @@ class Meta: fields=["holder", "unlock_index"], name="unique_withdrawn_event_index" ) ] + + def __str__(self): + return "WithdrawnEvent: " + super().__str__()