From d025f6a526973faac70b7b2076ddbaed337edc08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20S=C5=82omnicki?= Date: Fri, 15 Mar 2024 11:33:16 +0100 Subject: [PATCH 001/107] fix: remove remaining mention of contract-deployer in nginx (Pepesza) --- localenv/control-plane/nginx.conf | 16 ++++++++-------- localenv/multideployer/entrypoint.sh | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/localenv/control-plane/nginx.conf b/localenv/control-plane/nginx.conf index 110bc33e99..fb6f391392 100644 --- a/localenv/control-plane/nginx.conf +++ b/localenv/control-plane/nginx.conf @@ -11,7 +11,7 @@ upstream graph { } upstream contracts { - server contracts-deployer:8546; + server multideployer:8022; } server { @@ -26,7 +26,7 @@ server { add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; - } + } root /usr/share/nginx/html; try_files $uri /index.html; @@ -46,9 +46,9 @@ server { proxy_ssl_session_reuse off; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; - proxy_redirect off; + proxy_redirect off; } -} +} server { listen 80; @@ -61,10 +61,10 @@ server { proxy_ssl_session_reuse off; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; - proxy_redirect off; + proxy_redirect off; } -} +} server { listen 80; @@ -77,7 +77,7 @@ server { proxy_ssl_session_reuse off; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; - proxy_redirect off; + proxy_redirect off; } } @@ -92,6 +92,6 @@ server { proxy_ssl_session_reuse off; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; - proxy_redirect off; + proxy_redirect off; } } diff --git a/localenv/multideployer/entrypoint.sh b/localenv/multideployer/entrypoint.sh index 8ebb11592b..59ab1d93e1 100755 --- a/localenv/multideployer/entrypoint.sh +++ b/localenv/multideployer/entrypoint.sh @@ -3,7 +3,7 @@ set -ueo pipefail export LOCAL_RPC_URL=${RPC_URL} -export PORT=${PORT:-8546} +export PORT=${PORT:-8022} wait_for_rpc(){ curl --retry-connrefused --retry 10 --retry-delay 1 \ From 3f750dfea3d2be3b48441b94bcd41e73e4001873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Mon, 18 Mar 2024 09:50:39 +0100 Subject: [PATCH 002/107] chore: prune multideployer --- localenv/multideployer/Dockerfile | 1 - localenv/multideployer/server.py | 2 +- localenv/multideployer/wait_for_graph_rpc.sh | 13 ------------- localenv/multideployer/wait_for_subgraph.sh | 1 + 4 files changed, 2 insertions(+), 15 deletions(-) delete mode 100755 localenv/multideployer/wait_for_graph_rpc.sh diff --git a/localenv/multideployer/Dockerfile b/localenv/multideployer/Dockerfile index 758ab79406..93ee3407ec 100644 --- a/localenv/multideployer/Dockerfile +++ b/localenv/multideployer/Dockerfile @@ -10,7 +10,6 @@ COPY --from=hardhat /app/ /hardhat/ COPY --chmod=+x entrypoint.sh . COPY --chmod=+x wait_for_subgraph.sh . -COPY --chmod=+x wait_for_graph_rpc.sh . COPY server.py /app/server.py ENTRYPOINT ["./entrypoint.sh"] diff --git a/localenv/multideployer/server.py b/localenv/multideployer/server.py index c85a7b0120..3de01a0fe0 100644 --- a/localenv/multideployer/server.py +++ b/localenv/multideployer/server.py @@ -144,7 +144,7 @@ def do_GET(self): port = 8022 print(f"Multideployer listening on http://{host}:{port}") print( - f"Run GET with appropriate timeout value against http://{host}:{port}/?name=NAMEOFYOURSUBGRAPH" + f"Run GET with appropriate timeout value against http://{host}:{port}/?name=NAME_OF_YOUR_SUBGRAPH" ) server = HTTPServer((host, port), WebRequestHandler) server.serve_forever() diff --git a/localenv/multideployer/wait_for_graph_rpc.sh b/localenv/multideployer/wait_for_graph_rpc.sh deleted file mode 100755 index e4db928018..0000000000 --- a/localenv/multideployer/wait_for_graph_rpc.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/usr/bin/env bash - -set -ueo pipefail - -SUBGRAPH_ENDPOINT=${1-"http://127.0.0.1:8000/subgraphs/name/octant"} - -# Please note that curl will report success after graph is up. -# This command does not check for subgraph status! -curl --retry-connrefused --retry 20 --retry-delay 1 \ - -s -X POST $SUBGRAPH_ENDPOINT \ - -H "Content-Type: application/json" \ - --data '{"query":"{\n epoches {\n id\n }\n}","variables":null,"extensions":{"headers":null}}' -echo "Done waiting for graph RPC" diff --git a/localenv/multideployer/wait_for_subgraph.sh b/localenv/multideployer/wait_for_subgraph.sh index c877735752..11126755e0 100755 --- a/localenv/multideployer/wait_for_subgraph.sh +++ b/localenv/multideployer/wait_for_subgraph.sh @@ -9,6 +9,7 @@ function retry { while : ; do set -x curl \ + -s \ -X POST "$SUBGRAPH_ENDPOINT" \ -H "Content-Type: application/json" \ --data-raw '{"query":"{\n epoches {\n id\n }\n}","variables":null,"extensions":{"headers":null}}' \ From dd8af14aab1e4e7801f4c0d21ecd9f3cc95de81d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Mon, 18 Mar 2024 09:51:35 +0100 Subject: [PATCH 003/107] chore: explicitly specify amd64 platform --- localenv/docker-compose.yaml | 1 + localenv/localenv.yaml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/localenv/docker-compose.yaml b/localenv/docker-compose.yaml index b2ddce237e..21c02e12c9 100644 --- a/localenv/docker-compose.yaml +++ b/localenv/docker-compose.yaml @@ -55,6 +55,7 @@ services: multideployer: image: octant/multideployer:latest + platform: linux/amd64 networks: - octant depends_on: diff --git a/localenv/localenv.yaml b/localenv/localenv.yaml index 1097a46710..4ac1bc433c 100644 --- a/localenv/localenv.yaml +++ b/localenv/localenv.yaml @@ -28,6 +28,7 @@ services: backend: image: octant/backend:latest + platform: linux/amd64 ports: - '5000:5000' environment: @@ -52,6 +53,7 @@ services: epochs-snapshotter: image: 'octant/snapshotter:latest' + platform: linux/amd64 depends_on: - backend networks: @@ -60,6 +62,7 @@ services: control-plane: image: octant/control-plane:latest + platform: linux/amd64 ports: - "8080:80" networks: From b744bd0ecb8562718a63c3e02e6d8a34f953503e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Mon, 18 Mar 2024 09:51:59 +0100 Subject: [PATCH 004/107] chore: move anvil to base compose yaml --- localenv/docker-compose.yaml | 9 +++++++++ localenv/localenv.yaml | 11 ----------- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/localenv/docker-compose.yaml b/localenv/docker-compose.yaml index 21c02e12c9..877e9e53b9 100644 --- a/localenv/docker-compose.yaml +++ b/localenv/docker-compose.yaml @@ -3,6 +3,15 @@ version: "3.8" services: + anvil: + image: octant/anvil:latest + platform: linux/amd64 + command: '--block-time "${BLOCK_TIME:-5}"' + ports: + - "8545:8545" + networks: + - octant + graph-node: image: graphprotocol/graph-node:v0.26.0 platform: linux/amd64 diff --git a/localenv/localenv.yaml b/localenv/localenv.yaml index 4ac1bc433c..00157d0eee 100644 --- a/localenv/localenv.yaml +++ b/localenv/localenv.yaml @@ -3,17 +3,6 @@ version: "3.8" services: - - anvil: - image: octant/anvil:latest - platform: linux/amd64 - command: '--block-time "${BLOCK_TIME:-5}"' - ports: - - "8545:8545" - networks: - - octant - - backend-postgres: image: 'postgres:13' ports: From fa6185d6ad4daf4133e8d7906471b8d05d12eb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Mon, 18 Mar 2024 09:52:51 +0100 Subject: [PATCH 005/107] chore: prolong boot time for multideployer dependants --- localenv/backend/entrypoint.sh | 2 +- localenv/snapshotter/entrypoint.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/localenv/backend/entrypoint.sh b/localenv/backend/entrypoint.sh index ea27d3ff4d..7b67a5f51b 100644 --- a/localenv/backend/entrypoint.sh +++ b/localenv/backend/entrypoint.sh @@ -3,7 +3,7 @@ set -ueo pipefail wait_for_contracts(){ - curl --retry-connrefused --retry 10 --retry-delay 5 \ + curl --retry-connrefused --retry 10 --retry-delay 5 --max-time 300 \ -s -X GET "${CONTRACTS_DEPLOYER_URL}" } diff --git a/localenv/snapshotter/entrypoint.sh b/localenv/snapshotter/entrypoint.sh index 7452c141b9..69cadff4df 100644 --- a/localenv/snapshotter/entrypoint.sh +++ b/localenv/snapshotter/entrypoint.sh @@ -33,7 +33,7 @@ trigger_snapshot(){ } log Waiting for backend -curl --retry-connrefused --retry 20 --retry-delay 5 -s \ +curl --retry-connrefused --retry 120 --retry-delay 5 -s \ -X 'GET' "${BACKEND_URL}/info/healthcheck" \ -H 'Accept: application/json' From 64d8935830fb972659f3fdde97909a434f780610 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Mon, 18 Mar 2024 09:53:50 +0100 Subject: [PATCH 006/107] fix: restore fetching contracts data within control-plane --- localenv/control-plane/assets/index.html | 2 +- localenv/control-plane/nginx.conf | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/localenv/control-plane/assets/index.html b/localenv/control-plane/assets/index.html index 8c1ad51054..3af609bc30 100644 --- a/localenv/control-plane/assets/index.html +++ b/localenv/control-plane/assets/index.html @@ -5,7 +5,7 @@ Octant development @@ -25,13 +30,6 @@

Service

URL

- - - - - - - BACKEND backend.octant.localhost:8080 @@ -52,4 +50,4 @@

LOCAL CONTRACTS:

- \ No newline at end of file + From 2f7abdfb9db2483fcc42d18752163e2bcf3d8942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20S=C5=82omnicki?= Date: Wed, 20 Mar 2024 15:43:02 +0100 Subject: [PATCH 024/107] [LocalEnv] nginx config fixes --- localenv/control-plane/nginx.conf | 87 +++++++++++++++++++------------ 1 file changed, 53 insertions(+), 34 deletions(-) diff --git a/localenv/control-plane/nginx.conf b/localenv/control-plane/nginx.conf index d4e554395f..d1eca1acef 100644 --- a/localenv/control-plane/nginx.conf +++ b/localenv/control-plane/nginx.conf @@ -15,35 +15,61 @@ upstream deployer { } server { - listen 80; + listen 80; server_name octant.localhost localhost; location / { - - if ($request_method = 'OPTIONS') { # browser CORS preflight - add_header 'Access-Control-Allow-Origin' 'http://octant.localhost:8080'; - add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; - add_header 'Access-Control-Max-Age' 1728000; - add_header 'Content-Type' 'text/plain; charset=utf-8'; - add_header 'Content-Length' 0; - return 204; - } + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + if ($request_method = 'POST') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + } + if ($request_method = 'GET') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + } root /usr/share/nginx/html; try_files $uri /index.html; } location /deployment { - if ($request_method = 'GET') { # add CORS headers - add_header 'Access-Control-Allow-Origin' 'http://octant.localhost:8080' always; - add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always; - add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; - } + if ($request_method = 'OPTIONS') { + add_header 'Access-Control-Allow-Origin' '*'; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; + add_header 'Access-Control-Max-Age' 1728000; + add_header 'Content-Type' 'text/plain; charset=utf-8'; + add_header 'Content-Length' 0; + return 204; + } + if ($request_method = 'POST') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + } + if ($request_method = 'GET') { + add_header 'Access-Control-Allow-Origin' '*' always; + add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always; + add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always; + add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always; + } proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-NginX-Proxy true; proxy_pass http://deployer$request_uri; - proxy_ssl_session_reuse off; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; proxy_redirect off; @@ -51,30 +77,25 @@ server { } server { - listen 80; - server_name rpc.octant.localhost; - location / { + listen 80; + server_name backend.octant.localhost; + location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-NginX-Proxy true; - proxy_pass http://anvil$request_uri; - proxy_ssl_session_reuse off; + proxy_pass http://backend$request_uri; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; proxy_redirect off; } - } server { - listen 80; - server_name backend.octant.localhost; - location / { + listen 80; + server_name rpc.octant.localhost; + location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-NginX-Proxy true; - proxy_pass http://backend$request_uri; - proxy_ssl_session_reuse off; + proxy_pass http://anvil$request_uri; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; proxy_redirect off; @@ -82,14 +103,12 @@ server { } server { - listen 80; + listen 80; server_name graph.octant.localhost; - location / { + location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-NginX-Proxy true; proxy_pass http://graph$request_uri; - proxy_ssl_session_reuse off; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; proxy_redirect off; From 0be04af1ae9d246c81f4cb7ca0fc241eae2edbe3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 20 Mar 2024 15:47:01 +0100 Subject: [PATCH 025/107] oct-1309: cr fixes --- client/cypress/e2e/earn.cy.ts | 16 +++++----------- client/cypress/utils/e2e.ts | 15 +++++++++++++++ 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index 9fbdc58f89..8f1e1fafac 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -1,9 +1,6 @@ -import axios from 'axios'; - -import { visitWithLoader, mockCoinPricesServer } from 'cypress/utils/e2e'; +import { visitWithLoader, mockCoinPricesServer, moveEpoch } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import env from 'src/env'; import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import Chainable = Cypress.Chainable; @@ -188,7 +185,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); }); - it('Wallet connected: Lock 1000 GLM + move epoch', () => { + it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { connectWallet(); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') @@ -211,13 +208,10 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes ); cy.get('[data-test=GlmLockNotification--success]').should('be.visible'); cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.wait(5000); + // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). + cy.wait(2000); cy.window().then(async win => { - await win.mutateAsyncMoveEpoch(); - cy.wait(5000); - await axios.post(`${env.serverEndpoint}snapshots/pending`); - cy.wait(5000); - cy.reload(); + await moveEpoch(win); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { timeout: 60000, }) diff --git a/client/cypress/utils/e2e.ts b/client/cypress/utils/e2e.ts index 1495efd423..a673ac807a 100644 --- a/client/cypress/utils/e2e.ts +++ b/client/cypress/utils/e2e.ts @@ -1,4 +1,7 @@ +import axios from 'axios'; + import { navigationTabs } from 'src/constants/navigationTabs/navigationTabs'; +import env from 'src/env'; import Chainable = Cypress.Chainable; @@ -46,3 +49,15 @@ export const connectWallet = ( cy.switchToMetamaskNotification(); return cy.acceptMetamaskAccess(); }; + +export const moveEpoch = async (cypressWindow: Cypress.AUTWindow): Promise => { + await cypressWindow.mutateAsyncMoveEpoch(); + // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). + cy.wait(2000); + // Manually taking a pending snapshot after the epoch shift ensures that the snapshot is taken. Passing epoch multiple times without manually triggering pending snapshot in a short period of time may cause the e2e environment to fail. + await axios.post(`${env.serverEndpoint}snapshots/pending`); + // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). + cy.wait(2000); + // reload is needed to get updated data in the app + cy.reload(); +}; From 8c3ac79536218ab7cf24db4ef64b2554e7b08731 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 20 Mar 2024 16:03:44 +0100 Subject: [PATCH 026/107] oct-1309: removed additional cy.wait --- client/cypress/e2e/earn.cy.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index 8f1e1fafac..ae988ce896 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -208,8 +208,6 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes ); cy.get('[data-test=GlmLockNotification--success]').should('be.visible'); cy.get('[data-test=GlmLockTabs__Button]').click(); - // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). - cy.wait(2000); cy.window().then(async win => { await moveEpoch(win); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { From e05a758adb1ebf66eb81d25d232c873e61bf07bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20S=C5=82omnicki?= Date: Wed, 20 Mar 2024 19:15:26 +0100 Subject: [PATCH 027/107] [LocalEnv] Fix client Windows incompatibility --- client/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/package.json b/client/package.json index 851d4602c3..bb0efcc107 100644 --- a/client/package.json +++ b/client/package.json @@ -11,7 +11,7 @@ "build": "vite build", "build:staging": "vite build --mode staging", "codegen": "graphql-codegen --config codegen.ts", - "types:json": "yarn run json2ts -i './src/resources/schema/' -o 'src/types/gen' --cwd './src/resources/schema'", + "types:json": "yarn run json2ts -i src/resources/schema -o src/types/gen --cwd src/resources/schema", "dev": "yarn install && vite", "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", "eslint:fix": "yarn prettier && yarn eslint --fix", From 0bb1ee3aaec4960d23881751da51e43894c590e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 21 Mar 2024 22:55:50 +0100 Subject: [PATCH 028/107] [FIX] CY projects: .find instead of .get project name (#92) --- client/cypress/e2e/projects.cy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts index b19b864cb4..9d096bcdf2 100644 --- a/client/cypress/e2e/projects.cy.ts +++ b/client/cypress/e2e/projects.cy.ts @@ -20,7 +20,7 @@ function checkProjectItemElements(index, name, isPatronMode = false): Chainable< cy.get('[data-test^=ProjectsView__ProjectsListItem]') .eq(index) .should('be.visible') - .get('[data-test=ProjectsListItem__name]') + .find('[data-test=ProjectsListItem__name]') .should('be.visible') .contains(name); cy.get('[data-test^=ProjectsView__ProjectsListItem') From 02901db8183eb655fb4d6bc8e5fa93b0bbe3fac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 26 Mar 2024 10:51:58 +0100 Subject: [PATCH 029/107] OCT-1337 Setup cloud Sentry for the client (#38) --- client/.gitignore | 4 +- client/package.json | 6 + .../AllocationSummary/AllocationSummary.tsx | 2 +- client/src/env.ts | 5 +- client/src/index.tsx | 2 + client/src/sentry.ts | 18 + client/src/types/env.ts | 1 + client/vite.config.js | 13 + client/yarn.lock | 699 +++++++++++++++--- 9 files changed, 664 insertions(+), 86 deletions(-) create mode 100644 client/src/sentry.ts diff --git a/client/.gitignore b/client/.gitignore index e6a11d6d1b..55338a3611 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -4,4 +4,6 @@ typechain-types /client/bundleAnalysis.html /cypress/downloads/** /cypress/videos/** -/cypress/screenshots/** \ No newline at end of file +/cypress/screenshots/** +# Sentry Config File +.env.sentry-build-plugin diff --git a/client/package.json b/client/package.json index 851d4602c3..9b961f2eaa 100644 --- a/client/package.json +++ b/client/package.json @@ -10,6 +10,7 @@ "prebuild": "yarn types:json", "build": "vite build", "build:staging": "vite build --mode staging", + "postbuild": "yarn sourcemaps:remove", "codegen": "graphql-codegen --config codegen.ts", "types:json": "yarn run json2ts -i './src/resources/schema/' -o 'src/types/gen' --cwd './src/resources/schema'", "dev": "yarn install && vite", @@ -27,10 +28,15 @@ "start": "yarn build --base=/ && yarn preview --base=/ --host=0.0.0.0", "test": "TZ=UTC jest", "type-check": "tsc --noEmit true", + "sourcemaps:remove": "rimraf dist/**/*.map", "synpress:open": "synpress open --configFile synpress.config.ts", "synpress:run": "synpress run --configFile synpress.config.ts" }, "dependencies": { + "@ethersproject/constants": "^5.7.0", + "@ethersproject/providers": "^5.7.2", + "@sentry/react": "^7.101.1", + "@sentry/vite-plugin": "^2.14.1", "@tanstack/react-query": "^5.18.0", "@wagmi/connectors": "^3.1.5", "@web3modal/ethereum": "2.7.1", diff --git a/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx b/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx index b75d129e2b..ba1a8cb588 100644 --- a/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx +++ b/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx @@ -111,7 +111,7 @@ const AllocationSummary: FC = ({ )} - {(personalAllocation === 0n) !== true && ( + {personalAllocation !== 0n && ( { }), ); } + if (isProduction && process.env.VITE_SENTRY_AUTH_TOKEN) { + plugins.push( + sentryVitePlugin({ + authToken: process.env.VITE_SENTRY_AUTH_TOKEN, + org: 'golem-foundation', + project: 'octant-client-production', + }), + ); + } return { base, + build: { + sourcemap: true, + }, css: { modules: { generateScopedName: localIdentName, diff --git a/client/yarn.lock b/client/yarn.lock index d2ef3c2fe6..68e34b8693 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -17,7 +17,7 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.9.4.tgz#aae21cb858bbb0411949d5b7b3051f4209043f62" integrity sha512-UK0bHA7hh9cR39V+4gl2/NnBBjoXIxkuWAPCaY4X7fbH4L/azIi7ilWOCjMUYfpJgraLUAqkRi2BqrjME8Rynw== -"@ampproject/remapping@^2.2.0": +"@ampproject/remapping@^2.1.0", "@ampproject/remapping@^2.2.0": version "2.2.1" resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.1.tgz#99e8e11851128b8702cd57c33684f1d0f260b630" integrity sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg== @@ -63,7 +63,7 @@ "@babel/highlight" "^7.22.13" chalk "^2.4.2" -"@babel/code-frame@^7.23.5": +"@babel/code-frame@^7.16.7", "@babel/code-frame@^7.23.5": version "7.23.5" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.5.tgz#9009b69a8c602293476ad598ff53e4562e15c244" integrity sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA== @@ -81,6 +81,27 @@ resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.23.5.tgz#ffb878728bb6bdcb6f4510aa51b1be9afb8cfd98" integrity sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw== +"@babel/core@7.18.5": + version "7.18.5" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.18.5.tgz#c597fa680e58d571c28dda9827669c78cdd7f000" + integrity sha512-MGY8vg3DxMnctw0LdvSEojOsumc70g0t18gNyUdAZqB1Rpd1Bqo/svHGvt+UJ6JcGX+DIekGFDxxIWofBxLCnQ== + dependencies: + "@ampproject/remapping" "^2.1.0" + "@babel/code-frame" "^7.16.7" + "@babel/generator" "^7.18.2" + "@babel/helper-compilation-targets" "^7.18.2" + "@babel/helper-module-transforms" "^7.18.0" + "@babel/helpers" "^7.18.2" + "@babel/parser" "^7.18.5" + "@babel/template" "^7.16.7" + "@babel/traverse" "^7.18.5" + "@babel/types" "^7.18.4" + convert-source-map "^1.7.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.1" + semver "^6.3.0" + "@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.14.0", "@babel/core@^7.22.9": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.22.15.tgz#15d4fd03f478a459015a4b94cfbb3bd42c48d2f4" @@ -163,7 +184,7 @@ "@jridgewell/trace-mapping" "^0.3.17" jsesc "^2.5.1" -"@babel/generator@^7.23.6": +"@babel/generator@^7.18.2", "@babel/generator@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.6.tgz#9e1fca4811c77a10580d17d26b57b036133f3c2e" integrity sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw== @@ -180,6 +201,17 @@ dependencies: "@babel/types" "^7.22.5" +"@babel/helper-compilation-targets@^7.18.2", "@babel/helper-compilation-targets@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" + integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== + dependencies: + "@babel/compat-data" "^7.23.5" + "@babel/helper-validator-option" "^7.23.5" + browserslist "^4.22.2" + lru-cache "^5.1.1" + semver "^6.3.1" + "@babel/helper-compilation-targets@^7.20.7", "@babel/helper-compilation-targets@^7.22.15", "@babel/helper-compilation-targets@^7.22.5": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.15.tgz#0698fc44551a26cf29f18d4662d5bf545a6cfc52" @@ -191,17 +223,6 @@ lru-cache "^5.1.1" semver "^6.3.1" -"@babel/helper-compilation-targets@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz#4d79069b16cbcf1461289eccfbbd81501ae39991" - integrity sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ== - dependencies: - "@babel/compat-data" "^7.23.5" - "@babel/helper-validator-option" "^7.23.5" - browserslist "^4.22.2" - lru-cache "^5.1.1" - semver "^6.3.1" - "@babel/helper-create-class-features-plugin@^7.18.6": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.15.tgz#97a61b385e57fe458496fad19f8e63b63c867de4" @@ -264,6 +285,17 @@ dependencies: "@babel/types" "^7.22.15" +"@babel/helper-module-transforms@^7.18.0", "@babel/helper-module-transforms@^7.23.3": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" + integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== + dependencies: + "@babel/helper-environment-visitor" "^7.22.20" + "@babel/helper-module-imports" "^7.22.15" + "@babel/helper-simple-access" "^7.22.5" + "@babel/helper-split-export-declaration" "^7.22.6" + "@babel/helper-validator-identifier" "^7.22.20" + "@babel/helper-module-transforms@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.22.15.tgz#40ad2f6950f143900e9c1c72363c0b431a606082" @@ -275,17 +307,6 @@ "@babel/helper-split-export-declaration" "^7.22.6" "@babel/helper-validator-identifier" "^7.22.15" -"@babel/helper-module-transforms@^7.23.3": - version "7.23.3" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.23.3.tgz#d7d12c3c5d30af5b3c0fcab2a6d5217773e2d0f1" - integrity sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ== - dependencies: - "@babel/helper-environment-visitor" "^7.22.20" - "@babel/helper-module-imports" "^7.22.15" - "@babel/helper-simple-access" "^7.22.5" - "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/helper-validator-identifier" "^7.22.20" - "@babel/helper-optimise-call-expression@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz#f21531a9ccbff644fdd156b4077c16ff0c3f609e" @@ -358,6 +379,15 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz#907a3fbd4523426285365d1206c423c4c5520307" integrity sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw== +"@babel/helpers@^7.18.2", "@babel/helpers@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" + integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== + dependencies: + "@babel/template" "^7.23.9" + "@babel/traverse" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/helpers@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.22.15.tgz#f09c3df31e86e3ea0b7ff7556d85cdebd47ea6f1" @@ -376,15 +406,6 @@ "@babel/traverse" "^7.23.7" "@babel/types" "^7.23.6" -"@babel/helpers@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.23.9.tgz#c3e20bbe7f7a7e10cb9b178384b4affdf5995c7d" - integrity sha512-87ICKgU5t5SzOT7sBMfCOZQ2rHjRU+Pcb9BoILMYz600W6DkVRLFBPwQ18gwUVvggqXivaUakpnxWQGbpywbBQ== - dependencies: - "@babel/template" "^7.23.9" - "@babel/traverse" "^7.23.9" - "@babel/types" "^7.23.9" - "@babel/highlight@^7.22.13": version "7.22.13" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.13.tgz#9cda839e5d3be9ca9e8c26b6dd69e7548f0cbf16" @@ -408,16 +429,16 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.16.tgz#180aead7f247305cce6551bea2720934e2fa2c95" integrity sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA== +"@babel/parser@^7.18.5", "@babel/parser@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" + integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== + "@babel/parser@^7.23.6": version "7.23.6" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.6.tgz#ba1c9e512bda72a47e285ae42aff9d2a635a9e3b" integrity sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ== -"@babel/parser@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.9.tgz#7b903b6149b0f8fa7ad564af646c4c38a77fc44b" - integrity sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA== - "@babel/plugin-proposal-class-properties@^7.0.0": version "7.18.6" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz#b110f59741895f7ec21a6fff696ec46265c446a3" @@ -786,6 +807,15 @@ dependencies: regenerator-runtime "^0.14.0" +"@babel/template@^7.16.7", "@babel/template@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" + integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== + dependencies: + "@babel/code-frame" "^7.23.5" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" + "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.22.15", "@babel/template@^7.22.5", "@babel/template@^7.3.3": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38" @@ -795,15 +825,6 @@ "@babel/parser" "^7.22.15" "@babel/types" "^7.22.15" -"@babel/template@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.23.9.tgz#f881d0487cba2828d3259dcb9ef5005a9731011a" - integrity sha512-+xrD2BWLpvHKNmX2QbpdpsBaWnRxahMwJjO+KZk2JOElj5nSmKezyS1B4u+QbHMTX69t4ukm6hh9lsYQ7GHCKA== - dependencies: - "@babel/code-frame" "^7.23.5" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" - "@babel/traverse@^7.14.0", "@babel/traverse@^7.16.8", "@babel/traverse@^7.22.15": version "7.22.15" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.22.15.tgz#75be4d2d6e216e880e93017f4e2389aeb77ef2d9" @@ -820,10 +841,10 @@ debug "^4.1.0" globals "^11.1.0" -"@babel/traverse@^7.23.7": - version "7.23.7" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" - integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== +"@babel/traverse@^7.18.5", "@babel/traverse@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" + integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -831,15 +852,15 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.6" - "@babel/types" "^7.23.6" + "@babel/parser" "^7.23.9" + "@babel/types" "^7.23.9" debug "^4.3.1" globals "^11.1.0" -"@babel/traverse@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.9.tgz#2f9d6aead6b564669394c5ce0f9302bb65b9d950" - integrity sha512-I/4UJ9vs90OkBtY6iiiTORVMyIhJ4kAVmsKo9KFc8UOxMeUfi2hvtIBsET5u9GizXE6/GFSuKCTNfgCswuEjRg== +"@babel/traverse@^7.23.7": + version "7.23.7" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.7.tgz#9a7bf285c928cb99b5ead19c3b1ce5b310c9c305" + integrity sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg== dependencies: "@babel/code-frame" "^7.23.5" "@babel/generator" "^7.23.6" @@ -847,8 +868,8 @@ "@babel/helper-function-name" "^7.23.0" "@babel/helper-hoist-variables" "^7.22.5" "@babel/helper-split-export-declaration" "^7.22.6" - "@babel/parser" "^7.23.9" - "@babel/types" "^7.23.9" + "@babel/parser" "^7.23.6" + "@babel/types" "^7.23.6" debug "^4.3.1" globals "^11.1.0" @@ -861,19 +882,19 @@ "@babel/helper-validator-identifier" "^7.22.15" to-fast-properties "^2.0.0" -"@babel/types@^7.23.0", "@babel/types@^7.23.6": - version "7.23.6" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" - integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== +"@babel/types@^7.18.4", "@babel/types@^7.23.9": + version "7.23.9" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" + integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== dependencies: "@babel/helper-string-parser" "^7.23.4" "@babel/helper-validator-identifier" "^7.22.20" to-fast-properties "^2.0.0" -"@babel/types@^7.23.9": - version "7.23.9" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.9.tgz#1dd7b59a9a2b5c87f8b41e52770b5ecbf492e002" - integrity sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q== +"@babel/types@^7.23.0", "@babel/types@^7.23.6": + version "7.23.6" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.6.tgz#be33fdb151e1f5a56877d704492c240fc71c7ccd" + integrity sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg== dependencies: "@babel/helper-string-parser" "^7.23.4" "@babel/helper-validator-identifier" "^7.22.20" @@ -1156,6 +1177,219 @@ resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== +"@ethersproject/abstract-provider@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef" + integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + +"@ethersproject/abstract-signer@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2" + integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/address@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37" + integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + +"@ethersproject/base64@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c" + integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + +"@ethersproject/basex@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b" + integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + +"@ethersproject/bignumber@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2" + integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + bn.js "^5.2.1" + +"@ethersproject/bytes@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d" + integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/constants@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e" + integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA== + dependencies: + "@ethersproject/bignumber" "^5.7.0" + +"@ethersproject/hash@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7" + integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g== + dependencies: + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + +"@ethersproject/keccak256@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a" + integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + js-sha3 "0.8.0" + +"@ethersproject/logger@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892" + integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig== + +"@ethersproject/networks@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6" + integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/properties@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30" + integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw== + dependencies: + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/providers@^5.7.2": + version "5.7.2" + resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb" + integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg== + dependencies: + "@ethersproject/abstract-provider" "^5.7.0" + "@ethersproject/abstract-signer" "^5.7.0" + "@ethersproject/address" "^5.7.0" + "@ethersproject/base64" "^5.7.0" + "@ethersproject/basex" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/hash" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/networks" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/random" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/sha2" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@ethersproject/transactions" "^5.7.0" + "@ethersproject/web" "^5.7.0" + bech32 "1.1.4" + ws "7.4.6" + +"@ethersproject/random@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c" + integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/rlp@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304" + integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/sha2@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb" + integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + hash.js "1.1.7" + +"@ethersproject/signing-key@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3" + integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + bn.js "^5.2.1" + elliptic "6.5.4" + hash.js "1.1.7" + +"@ethersproject/strings@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2" + integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg== + dependencies: + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + +"@ethersproject/transactions@^5.7.0": + version "5.7.0" + resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b" + integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ== + dependencies: + "@ethersproject/address" "^5.7.0" + "@ethersproject/bignumber" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/constants" "^5.7.0" + "@ethersproject/keccak256" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/rlp" "^5.7.0" + "@ethersproject/signing-key" "^5.7.0" + +"@ethersproject/web@^5.7.0": + version "5.7.1" + resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae" + integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w== + dependencies: + "@ethersproject/base64" "^5.7.0" + "@ethersproject/bytes" "^5.7.0" + "@ethersproject/logger" "^5.7.0" + "@ethersproject/properties" "^5.7.0" + "@ethersproject/strings" "^5.7.0" + "@foundry-rs/easy-foundryup@^0.1.3": version "0.1.3" resolved "https://registry.yarnpkg.com/@foundry-rs/easy-foundryup/-/easy-foundryup-0.1.3.tgz#f5281aff6b3a98f277bdafcb4330e71f80979ed4" @@ -1884,7 +2118,7 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13", "@jridgewell/sourcemap-codec@^1.4.14": version "1.4.15" resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== @@ -2148,6 +2382,181 @@ "@noble/hashes" "~1.3.0" "@scure/base" "~1.1.0" +"@sentry-internal/feedback@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.101.1.tgz#d7e27ebcc46bd7306cfaa89b591293a394d75672" + integrity sha512-fOKDMVvLX+FuJHJszKBvRg1m7+fd4hchqRnZ9DDfitT6P5Ppl0gbEt/LStqu8Wq5M0tna+hpdwHlVEt7gZVKzw== + dependencies: + "@sentry/core" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry-internal/replay-canvas@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.101.1.tgz#6856431a6360dd456c693d0510410dcd46f28150" + integrity sha512-09l6nD+lxWvwkpXLlIZuzj/z79Llbo6mcH33TJvxrUTjAqSGF/i3Pd5bTLWro9atippOyQgIV/yTGG4Bc5FhyQ== + dependencies: + "@sentry/core" "7.101.1" + "@sentry/replay" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry-internal/tracing@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.101.1.tgz#9504e29c3c2c3ef5f652777e487b596cf8f78e1a" + integrity sha512-ihjWG8x4x0ozx6t+EHoXLKbsPrgzYLCpeBLWyS+M6n3hn6cmHM76c8nZw3ldhUQi5UYL3LFC/JZ50b4oSxtlrg== + dependencies: + "@sentry/core" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry/babel-plugin-component-annotate@2.14.1": + version "2.14.1" + resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-2.14.1.tgz#3eb759809e051d341071b9e1d5a2e8cf35b67c56" + integrity sha512-NHVOr6m0vOoh1UNSZr+OpWQERjjQM7lO48WN/N/MzobIIxc2pymw2KAq3lNJ1SnVAy1t9RNP8u+g6aEFEMGZ/w== + +"@sentry/browser@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.101.1.tgz#bab1257de499ed0e928bb4264a2e64d16cecccb5" + integrity sha512-+rIFoWPdO29AHVYsAwq8QEl2Ihv17Xh9Bt2aPFvLTGDA0caHjnx98g2jSOvLIOah6HI7Nwp3Njg2zBEzDtHkNw== + dependencies: + "@sentry-internal/feedback" "7.101.1" + "@sentry-internal/replay-canvas" "7.101.1" + "@sentry-internal/tracing" "7.101.1" + "@sentry/core" "7.101.1" + "@sentry/replay" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry/bundler-plugin-core@2.14.1": + version "2.14.1" + resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.14.1.tgz#419fcd2e792f8b4fc6bb6eb735d09c2325ba1697" + integrity sha512-JbYkeQQ+FTy4KjuJmnjjRGKv1LOSH+Q9cbcMHkr+vNrwAbdxkQ7WURGEKUCFTciIekToMCOiFk+g3FQlRmzLPg== + dependencies: + "@babel/core" "7.18.5" + "@sentry/babel-plugin-component-annotate" "2.14.1" + "@sentry/cli" "^2.22.3" + "@sentry/node" "^7.60.0" + "@sentry/utils" "^7.60.0" + dotenv "^16.3.1" + find-up "5.0.0" + glob "9.3.2" + magic-string "0.27.0" + unplugin "1.0.1" + +"@sentry/cli-darwin@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.28.6.tgz#83f9127de77e2a2d25eb143d90720b3e9042adc1" + integrity sha512-KRf0VvTltHQ5gA7CdbUkaIp222LAk/f1+KqpDzO6nB/jC/tL4sfiy6YyM4uiH6IbVEudB8WpHCECiatmyAqMBA== + +"@sentry/cli-linux-arm64@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm64/-/cli-linux-arm64-2.28.6.tgz#6bb660e5d8145270e287a9a21201d2f9576b0634" + integrity sha512-caMDt37FI752n4/3pVltDjlrRlPFCOxK4PHvoZGQ3KFMsai0ZhE/0CLBUMQqfZf0M0r8KB2x7wqLm7xSELjefQ== + +"@sentry/cli-linux-arm@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-arm/-/cli-linux-arm-2.28.6.tgz#73d466004ac445d9258e83a7b3d4e0ee6604e0bd" + integrity sha512-ANG7U47yEHD1g3JrfhpT4/MclEvmDZhctWgSP5gVw5X4AlcI87E6dTqccnLgvZjiIAQTaJJAZuSHVVF3Jk403w== + +"@sentry/cli-linux-i686@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-i686/-/cli-linux-i686-2.28.6.tgz#f7175ca639ee05cf12d808f7fc31d59d6e2ee3b9" + integrity sha512-Tj1+GMc6lFsDRquOqaGKXFpW9QbmNK4TSfynkWKiJxdTEn5jSMlXXfr0r9OQrxu3dCCqEHkhEyU63NYVpgxIPw== + +"@sentry/cli-linux-x64@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-linux-x64/-/cli-linux-x64-2.28.6.tgz#df0af8d6c8c8c880eb7345c715a4dfa509544a40" + integrity sha512-Dt/Xz784w/z3tEObfyJEMmRIzn0D5qoK53H9kZ6e0yNvJOSKNCSOq5cQk4n1/qeG0K/6SU9dirmvHwFUiVNyYg== + +"@sentry/cli-win32-i686@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-i686/-/cli-win32-i686-2.28.6.tgz#0df19912d1823b6ec034b4c4c714c7601211c926" + integrity sha512-zkpWtvY3kt+ogVaAbfFr2MEkgMMHJNJUnNMO8Ixce9gh38sybIkDkZNFnVPBXMClJV0APa4QH0EwumYBFZUMuQ== + +"@sentry/cli-win32-x64@2.28.6": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli-win32-x64/-/cli-win32-x64-2.28.6.tgz#2344a206be3b555ec6540740f93a181199962804" + integrity sha512-TG2YzZ9JMeNFzbicdr5fbtsusVGACbrEfHmPgzWGDeLUP90mZxiMTjkXsE1X/5jQEQjB2+fyfXloba/Ugo51hA== + +"@sentry/cli@^2.22.3": + version "2.28.6" + resolved "https://registry.yarnpkg.com/@sentry/cli/-/cli-2.28.6.tgz#645f31b9e742e7bf7668c8f867149359e79b8123" + integrity sha512-o2Ngz7xXuhwHxMi+4BFgZ4qjkX0tdZeOSIZkFAGnTbRhQe5T8bxq6CcQRLdPhqMgqvDn7XuJ3YlFtD3ZjHvD7g== + dependencies: + https-proxy-agent "^5.0.0" + node-fetch "^2.6.7" + progress "^2.0.3" + proxy-from-env "^1.1.0" + which "^2.0.2" + optionalDependencies: + "@sentry/cli-darwin" "2.28.6" + "@sentry/cli-linux-arm" "2.28.6" + "@sentry/cli-linux-arm64" "2.28.6" + "@sentry/cli-linux-i686" "2.28.6" + "@sentry/cli-linux-x64" "2.28.6" + "@sentry/cli-win32-i686" "2.28.6" + "@sentry/cli-win32-x64" "2.28.6" + +"@sentry/core@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.101.1.tgz#929841b7734129803b6dcd4d16bf0d3f53af4657" + integrity sha512-XSmXXeYT1d4O14eDF3OXPJFUgaN2qYEeIGUztqPX9nBs9/ij8y/kZOayFqlIMnfGvjOUM+63sy/2xDBOpFn6ug== + dependencies: + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry/node@^7.60.0": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-7.101.1.tgz#d652503002be921be5ca042a3ef7457d309cdfa9" + integrity sha512-iXSxUT6Zbt/KUY0+fRcW5II6Tgp2zdTfhBW+fQuDt/UUZt7Ypvb+6n4U2oom3LJfttmD7mdjQuT4+vsNImDjTQ== + dependencies: + "@sentry-internal/tracing" "7.101.1" + "@sentry/core" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry/react@^7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/react/-/react-7.101.1.tgz#678781d477bb4ff5b24444508168dd899472c1b2" + integrity sha512-CwaBXntX2e3XHZQZVuv/tcfm5H+UHcS6aVChGfUiBHIBi2JpAqdnLdQIFGTkE8BSnKyolKgIsnvIU3BQ//QTig== + dependencies: + "@sentry/browser" "7.101.1" + "@sentry/core" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + hoist-non-react-statics "^3.3.2" + +"@sentry/replay@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.101.1.tgz#129ca5ac70690e78468b037a71a9b756bc3d965f" + integrity sha512-l4jmj2Rf/myzk3TA83PdMiomassG8okdBh1b2Hp1+ycBRVZFDmsR81gKPvnefSXwGwGNGKEmp6Q2bdGzekpp3Q== + dependencies: + "@sentry-internal/tracing" "7.101.1" + "@sentry/core" "7.101.1" + "@sentry/types" "7.101.1" + "@sentry/utils" "7.101.1" + +"@sentry/types@7.101.1": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.101.1.tgz#7f936022d9b373f85ebf357634bf03a9e433a3d0" + integrity sha512-bwtkQvrCZ6JGc7vqX7TEAKBgkbQFORt84FFS3JQQb8G3efTt9fZd2ReY4buteKQdlALl8h1QWVngTLmI+kyUuw== + +"@sentry/utils@7.101.1", "@sentry/utils@^7.60.0": + version "7.101.1" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.101.1.tgz#97399b1e6a63a15e8f9fec5112ac4834239f1db6" + integrity sha512-Nrg0nrEI3nrOCd9SLJ/WGzxS5KMQE4cryLOvrDcHJRWpsSyGBF1hLLerk84Nsw/0myMsn7zTYU+xoq7idNsX5A== + dependencies: + "@sentry/types" "7.101.1" + +"@sentry/vite-plugin@^2.14.1": + version "2.14.1" + resolved "https://registry.yarnpkg.com/@sentry/vite-plugin/-/vite-plugin-2.14.1.tgz#daa9e353990ba141e18f66cd0f80017eb001cfe9" + integrity sha512-leNq+hWaKRhp0e+U1LYVbKBUAN4P+RXhG6GyMLvjKZL0LTsbLni05XeWg3Xy364Xoawcj6vgEN2lVvlpUvUnEQ== + dependencies: + "@sentry/bundler-plugin-core" "2.14.1" + unplugin "1.0.1" + "@sideway/address@^4.1.5": version "4.1.5" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.5.tgz#4bc149a0076623ced99ca8208ba780d65a99b9d5" @@ -4301,6 +4710,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +bech32@1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9" + integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ== + big.js@^5.2.2: version "5.2.2" resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" @@ -4362,6 +4776,11 @@ bluebird@^3.7.2: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== +bn.js@^4.11.9: + version "4.12.0" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88" + integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA== + bn.js@^5.1.1, bn.js@^5.2.0, bn.js@^5.2.1: version "5.2.1" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70" @@ -4429,6 +4848,11 @@ braces@^3.0.2, braces@~3.0.2: dependencies: fill-range "^7.0.1" +brorand@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" + integrity sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w== + browserslist@^4.21.9: version "4.21.10" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.10.tgz#dbbac576628c13d3b2231332cb2ec5a46e015bb0" @@ -5820,6 +6244,19 @@ electron-to-chromium@^1.4.601: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.640.tgz#76290a36fa4b5f1f4cadaf1fc582478ebb3ac246" integrity sha512-z/6oZ/Muqk4BaE7P69bXhUhpJbUM9ZJeka43ZwxsDshKtePns4mhBlh8bU5+yrnOnz3fhG82XLzGUXazOmsWnA== +elliptic@6.5.4: + version "6.5.4" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb" + integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + email-addresses@^3.0.1: version "3.1.0" resolved "https://registry.yarnpkg.com/email-addresses/-/email-addresses-3.1.0.tgz#cabf7e085cbdb63008a70319a74e6136188812fb" @@ -6935,6 +7372,14 @@ find-config@^1.0.0: dependencies: user-home "^2.0.0" +find-up@5.0.0, find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + find-up@6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/find-up/-/find-up-6.3.0.tgz#2abab3d3280b2dc7ac10199ef324c4e002c8c790" @@ -6951,14 +7396,6 @@ find-up@^4.0.0, find-up@^4.1.0: locate-path "^5.0.0" path-exists "^4.0.0" -find-up@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" - integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== - dependencies: - locate-path "^6.0.0" - path-exists "^4.0.0" - flat-cache@^3.0.4: version "3.1.0" resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.1.0.tgz#0e54ab4a1a60fe87e2946b6b00657f1c99e1af3f" @@ -7283,6 +7720,16 @@ glob-promise@^4.2.2: dependencies: "@types/glob" "^7.1.3" +glob@9.3.2: + version "9.3.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-9.3.2.tgz#8528522e003819e63d11c979b30896e0eaf52eda" + integrity sha512-BTv/JhKXFEHsErMte/AnfiSv8yYOLLiyH2lTg8vn02O21zWFgHPTfxtgn1QRe7NRgggUhC8hacR2Re94svHqeA== + dependencies: + fs.realpath "^1.0.0" + minimatch "^7.4.1" + minipass "^4.2.4" + path-scurry "^1.6.1" + glob@^7.0.3, glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" @@ -7504,7 +7951,7 @@ has@^1.0.3: dependencies: function-bind "^1.1.1" -hash.js@^1.1.7: +hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42" integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA== @@ -7545,7 +7992,16 @@ hey-listen@^1.0.8: resolved "https://registry.yarnpkg.com/hey-listen/-/hey-listen-1.0.8.tgz#8e59561ff724908de1aa924ed6ecc84a56a9aa68" integrity sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q== -hoist-non-react-statics@^3.3.0: +hmac-drbg@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1" + integrity sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg== + dependencies: + hash.js "^1.0.3" + minimalistic-assert "^1.0.0" + minimalistic-crypto-utils "^1.0.1" + +hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.2: version "3.3.2" resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45" integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw== @@ -7730,7 +8186,7 @@ http-signature@~1.3.6: jsprim "^2.0.2" sshpk "^1.14.1" -https-proxy-agent@^5.0.1: +https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== @@ -8890,6 +9346,11 @@ jose@^4.11.4: resolved "https://registry.yarnpkg.com/jose/-/jose-4.14.6.tgz#94dca1d04a0ad8c6bff0998cdb51220d473cc3af" integrity sha512-EqJPEUlZD0/CSUMubKtMaYUOtWe91tZXTWMJZoKSbLk+KtdhNdcvppH8lA9XwVu2V4Ailvsj0GBZJ2ZwDjfesQ== +js-sha3@0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840" + integrity sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q== + "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" @@ -9047,7 +9508,7 @@ json5@^1.0.1, json5@^1.0.2: dependencies: minimist "^1.2.0" -json5@^2.2.3: +json5@^2.2.1, json5@^2.2.3: version "2.2.3" resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== @@ -9404,6 +9865,11 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +"lru-cache@^9.1.1 || ^10.0.0": + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + lru-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/lru-queue/-/lru-queue-0.1.0.tgz#2738bd9f0d3cf4f84490c5736c48699ac632cda3" @@ -9416,6 +9882,13 @@ lz-string@^1.4.4, lz-string@^1.5.0: resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.5.0.tgz#c1ab50f77887b712621201ba9fd4e3a6ed099941" integrity sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== +magic-string@0.27.0: + version "0.27.0" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" + integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.13" + make-dir@^1.0.0: version "1.3.0" resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.3.0.tgz#79c1033b80515bd6d24ec9933e860ca75ee27f0c" @@ -9570,6 +10043,11 @@ minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7" integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A== +minimalistic-crypto-utils@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" + integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== + minimatch@9.0.3: version "9.0.3" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" @@ -9591,11 +10069,28 @@ minimatch@^4.2.3: dependencies: brace-expansion "^1.1.7" +minimatch@^7.4.1: + version "7.4.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-7.4.6.tgz#845d6f254d8f4a5e4fd6baf44d5f10c8448365fb" + integrity sha512-sBz8G/YjVniEz6lKPNpKxXwazJe4c19fEfV2GDMX6AjFz+MX9uDWIZW8XreVhkFW3fkIdTv/gxWr/Kks5FFAVw== + dependencies: + brace-expansion "^2.0.1" + minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== +minipass@^4.2.4: + version "4.2.8" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.2.8.tgz#f0010f64393ecfc1d1ccb5f582bcaf45f48e1a3a" + integrity sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0": + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -9693,7 +10188,7 @@ node-addon-api@^2.0.0: resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-2.0.2.tgz#432cfa82962ce494b132e9d72a15b29f71ff5d32" integrity sha512-Ntyt4AIXyaLIuMHF6IOoTakB3K+RWxwtsHNRxllEoA6vPwP9o4866g6YWDLUdnucilZhmkxiHwHr11gAENw+QA== -node-fetch@^2.6.1, node-fetch@^2.6.12: +node-fetch@^2.6.1, node-fetch@^2.6.12, node-fetch@^2.6.7: version "2.7.0" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.7.0.tgz#d0f0fa6e3e2dc1d27efcd8ad99d550bda94d187d" integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A== @@ -10222,6 +10717,14 @@ path-root@^0.1.1: dependencies: path-root-regex "^0.1.0" +path-scurry@^1.6.1: + version "1.10.1" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.1.tgz#9ba6bf5aa8500fe9fd67df4f0d9483b2b0bfc698" + integrity sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ== + dependencies: + lru-cache "^9.1.1 || ^10.0.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-to-regexp@0.1.7: version "0.1.7" resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" @@ -10471,6 +10974,11 @@ process@^0.11.10: resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== +progress@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + promise@^7.1.1: version "7.3.1" resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" @@ -12258,6 +12766,16 @@ unpipe@1.0.0, unpipe@~1.0.0: resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== +unplugin@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" + integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA== + dependencies: + acorn "^8.8.1" + chokidar "^3.5.3" + webpack-sources "^3.2.3" + webpack-virtual-modules "^0.5.0" + untildify@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" @@ -12624,6 +13142,16 @@ webpack-merge@^5.4.0: flat "^5.0.2" wildcard "^2.0.0" +webpack-sources@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" + integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== + +webpack-virtual-modules@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" + integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== + websocket-driver@>=0.5.1, websocket-driver@^0.7.4: version "0.7.4" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" @@ -12721,7 +13249,7 @@ which-typed-array@^1.1.10, which-typed-array@^1.1.11, which-typed-array@^1.1.2, gopd "^1.0.1" has-tostringtag "^1.0.0" -which@^2.0.1: +which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== @@ -12774,6 +13302,11 @@ write-file-atomic@^4.0.2: imurmurhash "^0.1.4" signal-exit "^3.0.7" +ws@7.4.6: + version "7.4.6" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.6.tgz#5654ca8ecdeee47c33a9a4bf6d28e2be2980377c" + integrity sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A== + ws@8.13.0: version "8.13.0" resolved "https://registry.yarnpkg.com/ws/-/ws-8.13.0.tgz#9a9fb92f93cf41512a0735c8f4dd09b8a1211cd0" From 12ba2d9998e5763aa855365fc9267dee4e0b9c5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marek=20S=C5=82omnicki?= Date: Tue, 26 Mar 2024 11:10:47 +0100 Subject: [PATCH 030/107] Add .gitattributes to fix EOL issue on Windows --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..dfdb8b771c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.sh text eol=lf From 2c2ce93424061b235d769186598e6cde8b154724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Tue, 26 Mar 2024 22:06:24 +0100 Subject: [PATCH 031/107] oct-1482: settings info links --- client/cypress/e2e/onboarding.cy.ts | 4 +- client/cypress/e2e/patronMode.cy.ts | 60 +++--- client/cypress/e2e/settings.cy.ts | 50 ++--- .../SettingsCryptoMainValueBox.tsx | 25 +++ .../SettingsCryptoMainValueBox/index.tsx | 2 + .../SettingsCurrencyBox.module.scss | 21 ++ .../SettingsCurrencyBox.tsx | 49 +++++ .../Settings/SettingsCurrencyBox/index.tsx | 2 + .../SettingsLinkBoxes.module.scss | 44 +++++ .../SettingsLinkBoxes/SettingsLinkBoxes.tsx | 49 +++++ .../Settings/SettingsLinkBoxes/index.tsx | 2 + .../SettingsLinksInfoBox.module.scss | 37 ++++ .../SettingsLinksInfoBox.tsx | 56 ++++++ .../Settings/SettingsLinksInfoBox/index.tsx | 2 + .../SettingsMainInfoBox.module.scss | 64 ++++++ .../SettingsMainInfoBox.tsx | 51 +++++ .../Settings/SettingsMainInfoBox/index.tsx | 2 + .../SettingsPatronMode/SettingsPatronMode.tsx | 2 +- .../SettingsPatronModeBox.module.scss | 11 ++ .../SettingsPatronModeBox.tsx | 51 +++++ .../Settings/SettingsPatronModeBox/index.tsx | 2 + .../SettingsShowOnboardingBox.tsx | 26 +++ .../SettingsShowOnboardingBox/index.tsx | 2 + .../SettingsShowTipsBox.tsx | 25 +++ .../Settings/SettingsShowTipsBox/index.tsx | 2 + .../SettingsToggleBox.module.scss | 17 ++ .../SettingsToggleBox/SettingsToggleBox.tsx | 40 ++++ .../Settings/SettingsToggleBox/index.tsx | 2 + client/src/locales/en/translation.json | 9 +- .../SettingsView/SettingsView.module.scss | 92 +++++++-- .../src/views/SettingsView/SettingsView.tsx | 185 +++--------------- 31 files changed, 749 insertions(+), 237 deletions(-) create mode 100644 client/src/components/Settings/SettingsCryptoMainValueBox/SettingsCryptoMainValueBox.tsx create mode 100644 client/src/components/Settings/SettingsCryptoMainValueBox/index.tsx create mode 100644 client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.module.scss create mode 100644 client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.tsx create mode 100644 client/src/components/Settings/SettingsCurrencyBox/index.tsx create mode 100644 client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.module.scss create mode 100644 client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.tsx create mode 100644 client/src/components/Settings/SettingsLinkBoxes/index.tsx create mode 100644 client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.module.scss create mode 100644 client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.tsx create mode 100644 client/src/components/Settings/SettingsLinksInfoBox/index.tsx create mode 100644 client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.module.scss create mode 100644 client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx create mode 100644 client/src/components/Settings/SettingsMainInfoBox/index.tsx create mode 100644 client/src/components/Settings/SettingsPatronModeBox/SettingsPatronModeBox.module.scss create mode 100644 client/src/components/Settings/SettingsPatronModeBox/SettingsPatronModeBox.tsx create mode 100644 client/src/components/Settings/SettingsPatronModeBox/index.tsx create mode 100644 client/src/components/Settings/SettingsShowOnboardingBox/SettingsShowOnboardingBox.tsx create mode 100644 client/src/components/Settings/SettingsShowOnboardingBox/index.tsx create mode 100644 client/src/components/Settings/SettingsShowTipsBox/SettingsShowTipsBox.tsx create mode 100644 client/src/components/Settings/SettingsShowTipsBox/index.tsx create mode 100644 client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.module.scss create mode 100644 client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.tsx create mode 100644 client/src/components/Settings/SettingsToggleBox/index.tsx diff --git a/client/cypress/e2e/onboarding.cy.ts b/client/cypress/e2e/onboarding.cy.ts index fea5414626..e528f9e87f 100644 --- a/client/cypress/e2e/onboarding.cy.ts +++ b/client/cypress/e2e/onboarding.cy.ts @@ -252,7 +252,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { cy.get('[data-test=ModalOnboarding__Button]').click(); navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').check().should('be.checked'); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); cy.reload(); cy.get('[data-test=ModalOnboarding]').should('be.visible'); }); @@ -260,7 +260,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => it('renders only once when "Always show Allocate onboarding" option is not checked', () => { cy.get('[data-test=ModalOnboarding__Button]').click(); navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('not.be.checked'); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); cy.reload(); cy.get('[data-test=ModalOnboarding]').should('not.exist'); }); diff --git a/client/cypress/e2e/patronMode.cy.ts b/client/cypress/e2e/patronMode.cy.ts index 369160c656..b5f3455c47 100644 --- a/client/cypress/e2e/patronMode.cy.ts +++ b/client/cypress/e2e/patronMode.cy.ts @@ -27,14 +27,14 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Patron mode toggle is not checked', () => { - cy.get('[data-test=InputToggle__PatronMode]').should('not.be.checked'); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').should('not.be.checked'); }); if (isDesktop) { it('Patron mode tooltip is visible on hover and has correct text', () => { - cy.get('[data-test=Tooltip__patronMode]').trigger('mouseover'); - cy.get('[data-test=Tooltip__patronMode__content]').should('be.visible'); - cy.get('[data-test=Tooltip__patronMode__content]') + cy.get('[data-test=SettingsPatronModeBox__Tooltip]').trigger('mouseover'); + cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]').should('be.visible'); + cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]') .invoke('text') .should( 'eq', @@ -44,43 +44,43 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes } it('Checking patron mode opens patron mode modal', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=ModalPatronMode]').should('be.visible'); }); it('Patron mode modal last paragraph has correct text', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); - cy.get('[data-test=PatronMode__fourthParagraph]') + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); + cy.get('[data-test=SettingsPatronMode__fourthParagraph]') .invoke('text') .should('eq', 'Slide the switch below all the way to the right to enable patron mode.'); }); it('Slider is visible', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider]').should('be.visible'); }); it('Slider has correct label', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider__label]') .invoke('text') .should('eq', 'Slide right to confirm'); }); it('Slider button is visible ', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider__button]').should('be.visible'); }); it('Slider button has right arrow inside', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider__button__arrow]') .should('be.visible') .should('have.css', 'transform', 'none'); }); it('Slider button is on the left side', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderLeftDistance = sliderEl[0].getBoundingClientRect().left; const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); @@ -94,7 +94,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Slider button returns to the starting point if user drops it below or equal 50% of slider width', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderDimensions = sliderEl[0].getBoundingClientRect(); @@ -138,7 +138,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Slider elements change color while moving slider button', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderDimensions = sliderEl[0].getBoundingClientRect(); @@ -279,7 +279,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Slider button goes to the end of track if user drops it above 50% of slider width + animation after signature', () => { - cy.get('[data-test=InputToggle__PatronMode]').check(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderDimensions = sliderEl[0].getBoundingClientRect(); @@ -401,14 +401,14 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Patron mode toggle is checked', () => { - cy.get('[data-test=InputToggle__PatronMode]').should('be.checked'); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').should('be.checked'); }); if (isDesktop) { it('Patron mode tooltip is visible on hover and has correct text', () => { - cy.get('[data-test=Tooltip__patronMode]').trigger('mouseover'); - cy.get('[data-test=Tooltip__patronMode__content]').should('be.visible'); - cy.get('[data-test=Tooltip__patronMode__content]') + cy.get('[data-test=SettingsPatronModeBox__Tooltip]').trigger('mouseover'); + cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]').should('be.visible'); + cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]') .invoke('text') .should( 'eq', @@ -418,43 +418,43 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes } it('Unchecking patron mode opens patron mode modal', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=ModalPatronMode]').should('be.visible'); }); it('Patron mode modal last paragraph has correct text', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); - cy.get('[data-test=PatronMode__fourthParagraph]') + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); + cy.get('[data-test=SettingsPatronMode__fourthParagraph]') .invoke('text') .should('eq', 'Slide the switch below all the way to the left to disable patron mode.'); }); it('Slider is visible', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider]').should('be.visible'); }); it('Slider has correct label', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider__label]') .invoke('text') .should('eq', 'Slide left to confirm'); }); it('Slider button is visible', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider__button]').should('be.visible'); }); it('Slider button has left arrow inside', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider__button__arrow]') .should('be.visible') .should('have.css', 'transform', 'matrix(-1, 0, 0, -1, 0, 0)'); }); it('Slider button is on the right side', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderRightDistance = sliderEl[0].getBoundingClientRect().right; const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); @@ -468,7 +468,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Slider button returns to the starting point if user drops it below or equal 50% of slider width', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderDimensions = sliderEl[0].getBoundingClientRect(); @@ -512,7 +512,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Slider elements change color while moving slider button', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderDimensions = sliderEl[0].getBoundingClientRect(); @@ -655,7 +655,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); it('Slider button goes to the end of track if user drops it above 50% of slider width + animation after signature', () => { - cy.get('[data-test=InputToggle__PatronMode]').uncheck(); + cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); cy.get('[data-test=PatronModeSlider]').then(sliderEl => { const sliderDimensions = sliderEl[0].getBoundingClientRect(); diff --git a/client/cypress/e2e/settings.cy.ts b/client/cypress/e2e/settings.cy.ts index 127c276477..087b77329e 100644 --- a/client/cypress/e2e/settings.cy.ts +++ b/client/cypress/e2e/settings.cy.ts @@ -21,47 +21,47 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('"Always show Allocate onboarding" option toggle works', () => { - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').check(); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('be.checked'); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check(); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE)).eq('true'); }); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').click(); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('not.be.checked'); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').click(); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE)).eq('false'); }); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').click(); - cy.get('[data-test=InputToggle__AlwaysShowOnboarding]').should('be.checked'); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').click(); + cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE)).eq('true'); }); }); it('"Use crypto as main value display" option is checked by default', () => { - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').should('be.checked'); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('true'); }); }); it('"Use crypto as main value display" option toggle works', () => { - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').check(); - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').should('be.checked'); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').check(); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('true'); }); - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').click(); - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').should('not.be.checked'); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').click(); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('not.be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('false'); }); - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').click(); - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').should('be.checked'); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').click(); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('true'); }); @@ -84,7 +84,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('"Use crypto as main value display" option changes DoubleValue sections order', () => { - cy.get('[data-test=InputToggle__UseCryptoAsMainValueDisplay]').uncheck(); + cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').uncheck(); navigateWithCheck(ROOT_ROUTES.earn.absolute); const cryptoValue = getValueCryptoToDisplay({ @@ -111,7 +111,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => const nextDisplayCurrencyToUppercase = i < DISPLAY_CURRENCIES.length - 1 ? DISPLAY_CURRENCIES[i + 1].toUpperCase() : undefined; - cy.get('[data-test=SettingsView__InputSelect--currency__SingleValue]').contains( + cy.get('[data-test=SettingsCurrencyBox__InputSelect--currency__SingleValue]').contains( displayCurrencyToUppercase, ); navigateWithCheck(ROOT_ROUTES.earn.absolute); @@ -127,35 +127,35 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => } navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsView__InputSelect--currency]').click(); + cy.get('[data-test=SettingsCurrencyBox__InputSelect--currency]').click(); cy.get( - `[data-test=SettingsView__InputSelect--currency__Option--${nextDisplayCurrencyToUppercase}]`, + `[data-test=SettingsCurrencyBox__InputSelect--currency__Option--${nextDisplayCurrencyToUppercase}]`, ).click(); } }); it('"Always show Octant tips" option toggle works', () => { - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').check(); - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').should('be.checked'); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').check(); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(ARE_OCTANT_TIPS_ALWAYS_VISIBLE)).eq('true'); }); - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').click(); - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').should('not.be.checked'); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').click(); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').should('not.be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(ARE_OCTANT_TIPS_ALWAYS_VISIBLE)).eq('false'); }); - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').click(); - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').should('be.checked'); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').click(); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').should('be.checked'); cy.getAllLocalStorage().then(() => { expect(localStorage.getItem(ARE_OCTANT_TIPS_ALWAYS_VISIBLE)).eq('true'); }); }); it('"Always show Octant tips" works (checked)', () => { - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').check(); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').check(); navigateWithCheck(ROOT_ROUTES.earn.absolute); cy.get('[data-test=EarnView__TipTile--connectWallet]').should('exist'); @@ -171,7 +171,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => }); it('"Always show Octant tips" works (unchecked)', () => { - cy.get('[data-test=AlwaysShowOctantTips__InputCheckbox]').uncheck(); + cy.get('[data-test=SettingsShowTipsBox__InputToggle]').uncheck(); navigateWithCheck(ROOT_ROUTES.earn.absolute); cy.get('[data-test=EarnView__TipTile--connectWallet]').should('exist'); diff --git a/client/src/components/Settings/SettingsCryptoMainValueBox/SettingsCryptoMainValueBox.tsx b/client/src/components/Settings/SettingsCryptoMainValueBox/SettingsCryptoMainValueBox.tsx new file mode 100644 index 0000000000..ef5cb5c79b --- /dev/null +++ b/client/src/components/Settings/SettingsCryptoMainValueBox/SettingsCryptoMainValueBox.tsx @@ -0,0 +1,25 @@ +import React, { ReactNode, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SettingsToggleBox from 'components/Settings/SettingsToggleBox'; +import useSettingsStore from 'store/settings/store'; + +const SettingsCryptoMainValueBox = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { setIsCryptoMainValueDisplay, isCryptoMainValueDisplay } = useSettingsStore(state => ({ + isCryptoMainValueDisplay: state.data.isCryptoMainValueDisplay, + setIsCryptoMainValueDisplay: state.setIsCryptoMainValueDisplay, + })); + + return ( + setIsCryptoMainValueDisplay(isChecked)} + > + {t('cryptoMainValueDisplay')} + + ); +}; + +export default memo(SettingsCryptoMainValueBox); diff --git a/client/src/components/Settings/SettingsCryptoMainValueBox/index.tsx b/client/src/components/Settings/SettingsCryptoMainValueBox/index.tsx new file mode 100644 index 0000000000..4121f39b77 --- /dev/null +++ b/client/src/components/Settings/SettingsCryptoMainValueBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsCryptoMainValueBox'; diff --git a/client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.module.scss b/client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.module.scss new file mode 100644 index 0000000000..011be12bf2 --- /dev/null +++ b/client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.module.scss @@ -0,0 +1,21 @@ +.root { + height: 6.4rem; + font-size: $font-size-12; + margin: 1.6rem auto 0; + + @media #{$desktop-up} { + font-size: $font-size-14; + } + + .spacer { + height: 2.4rem; + width: 0.1rem; + background: $color-octant-grey1; + margin-right: 4rem; + } + + .currencySelectorWrapper { + display: flex; + font-size: $font-size-12; + } +} diff --git a/client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.tsx b/client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.tsx new file mode 100644 index 0000000000..012ad1539d --- /dev/null +++ b/client/src/components/Settings/SettingsCurrencyBox/SettingsCurrencyBox.tsx @@ -0,0 +1,49 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import BoxRounded from 'components/ui/BoxRounded'; +import InputSelect from 'components/ui/InputSelect'; +import useSettingsStore from 'store/settings/store'; +import { SettingsData } from 'store/settings/types'; +import { Options } from 'views/SettingsView/types'; + +import styles from './SettingsCurrencyBox.module.scss'; + +const options: Options = [ + { label: 'USD', value: 'usd' }, + { label: 'AUD', value: 'aud' }, + { label: 'EUR', value: 'eur' }, + { label: 'JPY', value: 'jpy' }, + { label: 'CNY', value: 'cny' }, + { label: 'GBP', value: 'gbp' }, +]; + +const SettingsCurrencyBox = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { setDisplayCurrency, displayCurrency } = useSettingsStore(state => ({ + displayCurrency: state.data.displayCurrency, + setDisplayCurrency: state.setDisplayCurrency, + })); + + return ( + + {t('chooseDisplayCurrency')} +
+
+ setDisplayCurrency(option!.value as SettingsData['displayCurrency'])} + options={options} + selectedOption={options.find(({ value }) => value === displayCurrency)} + /> +
+ + ); +}; + +export default SettingsCurrencyBox; diff --git a/client/src/components/Settings/SettingsCurrencyBox/index.tsx b/client/src/components/Settings/SettingsCurrencyBox/index.tsx new file mode 100644 index 0000000000..f978ffc565 --- /dev/null +++ b/client/src/components/Settings/SettingsCurrencyBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsCurrencyBox'; diff --git a/client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.module.scss b/client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.module.scss new file mode 100644 index 0000000000..7b2a53f78d --- /dev/null +++ b/client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.module.scss @@ -0,0 +1,44 @@ +.root { + width: 100%; + display: flex; + gap: 1.5rem; + margin-top: 1.6rem; + + .boxChildrenWrapper { + height: 100%; + } + + .box { + font-size: $font-size-12; + line-height: 2rem; + font-weight: $font-weight-bold; + display: flex; + align-items: center; + justify-content: center; + height: 6.4rem; + } + + .buttonLink { + display: block; + font-size: $font-size-12; + line-height: 2rem; + font-weight: $font-weight-semibold; + min-height: 2rem; + width: 100%; + height: 100%; + display: flex; + align-items: center; + + .buttonLinkText { + margin-left: 0.4rem; + } + + .buttonLinkArrowSvg { + margin-left: 0.4rem; + } + + @media #{$desktop-up} { + font-size: $font-size-14; + } + } +} diff --git a/client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.tsx b/client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.tsx new file mode 100644 index 0000000000..85f65cedca --- /dev/null +++ b/client/src/components/Settings/SettingsLinkBoxes/SettingsLinkBoxes.tsx @@ -0,0 +1,49 @@ +import React, { ReactNode, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import BoxRounded from 'components/ui/BoxRounded'; +import Button from 'components/ui/Button'; +import Svg from 'components/ui/Svg'; +import { DISCORD_LINK, OCTANT_BUILD_LINK, OCTANT_DOCS } from 'constants/urls'; +import { arrowTopRight } from 'svg/misc'; + +import styles from './SettingsLinkBoxes.module.scss'; + +const SettingsLinkBoxes = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + + const mobileLinks = [ + { + href: OCTANT_BUILD_LINK, + label: t('website'), // 'Website', + }, + { + href: OCTANT_DOCS, + label: t('docs'), // 'Docs', + }, + { + href: DISCORD_LINK, + label: t('discord'), + }, + ]; + + return ( +
+ {mobileLinks.map(({ href, label }) => ( + + + + ))} +
+ ); +}; + +export default memo(SettingsLinkBoxes); diff --git a/client/src/components/Settings/SettingsLinkBoxes/index.tsx b/client/src/components/Settings/SettingsLinkBoxes/index.tsx new file mode 100644 index 0000000000..957a823fa9 --- /dev/null +++ b/client/src/components/Settings/SettingsLinkBoxes/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsLinkBoxes'; diff --git a/client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.module.scss b/client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.module.scss new file mode 100644 index 0000000000..b9e65b0b62 --- /dev/null +++ b/client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.module.scss @@ -0,0 +1,37 @@ +.root { + padding: 4.4rem 2.2rem 0.5rem; + + @media #{$desktop-up} { + padding: 4.5rem 2.2rem 2.5rem; + } + + .buttonLink { + display: block; + font-size: $font-size-12; + line-height: 2rem; + font-weight: $font-weight-semibold; + min-height: 2rem; + + .buttonLinkText { + margin-left: 0.4rem; + } + + .buttonLinkArrowSvg { + margin-left: 0.4rem; + path { + fill: $color-octant-green; + } + } + + @media #{$desktop-up} { + font-size: $font-size-14; + } + } + + .octantInfo { + color: $color-octant-grey5; + font-size: $font-size-14; + font-weight: $font-weight-semibold; + line-height: 2rem; + } +} diff --git a/client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.tsx b/client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.tsx new file mode 100644 index 0000000000..8c2420aa44 --- /dev/null +++ b/client/src/components/Settings/SettingsLinksInfoBox/SettingsLinksInfoBox.tsx @@ -0,0 +1,56 @@ +import React, { ReactNode, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import BoxRounded from 'components/ui/BoxRounded'; +import Button from 'components/ui/Button'; +import Svg from 'components/ui/Svg'; +import { DISCORD_LINK, OCTANT_BUILD_LINK, OCTANT_DOCS, TERMS_OF_USE } from 'constants/urls'; +import { arrowRight } from 'svg/misc'; + +import styles from './SettingsLinksInfoBox.module.scss'; + +const SettingsLinksInfoBox = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + + const desktopLinks = [ + { + href: OCTANT_BUILD_LINK, + label: t('octantBuild'), // 'Octant.build', + }, + { + href: OCTANT_DOCS, + label: t('userDocs'), // 'User Docs', + }, + { + href: DISCORD_LINK, + label: t('discordCommunity'), // 'Discord Community', + }, + { + href: TERMS_OF_USE, + label: t('termsAndConditions'), + }, + ]; + + return ( + +
{t('octantInfo')}
+
+ {desktopLinks.map(({ label, href }) => ( + + ))} +
+
+ ); +}; + +export default memo(SettingsLinksInfoBox); diff --git a/client/src/components/Settings/SettingsLinksInfoBox/index.tsx b/client/src/components/Settings/SettingsLinksInfoBox/index.tsx new file mode 100644 index 0000000000..37989d0cb4 --- /dev/null +++ b/client/src/components/Settings/SettingsLinksInfoBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsLinksInfoBox'; diff --git a/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.module.scss b/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.module.scss new file mode 100644 index 0000000000..abcd1314e1 --- /dev/null +++ b/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.module.scss @@ -0,0 +1,64 @@ +.root { + padding: 4.4rem 2.2rem 0.5rem; + + @media #{$desktop-up} { + padding: 4.5rem 2.2rem 2.5rem; + } + + .infoTitle { + margin-bottom: 0.8rem; + } + + .infoEpoch { + padding: 0.4rem 0.8rem; + background: $color-octant-grey1; + border-radius: $border-radius-04; + font-size: $font-size-12; + font-weight: $font-weight-bold; + line-height: 1.6rem; + + @media #{$desktop-up} { + font-size: $font-size-14; + } + } + + .infoContainer { + display: flex; + flex-direction: column; + justify-content: center; + min-height: 9.9rem; + + .info { + line-height: 1.8rem; + color: $color-octant-grey5; + } + + @media #{$desktop-up} { + justify-content: flex-end; + min-height: 8rem; + margin-top: 1.6rem; + + .info { + line-height: 2rem; + font-size: $font-size-14; + font-weight: $font-weight-semibold; + } + + .golemFoundationProject { + color: $color-octant-green; + } + } + + .buttonLink { + display: block; + font-size: $font-size-12; + line-height: 2rem; + font-weight: $font-weight-semibold; + min-height: 2rem; + + @media #{$desktop-up} { + font-size: $font-size-14; + } + } + } +} diff --git a/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx b/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx new file mode 100644 index 0000000000..f170fc8b02 --- /dev/null +++ b/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx @@ -0,0 +1,51 @@ +import cx from 'classnames'; +import React, { ReactNode, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import BoxRounded from 'components/ui/BoxRounded'; +import Button from 'components/ui/Button'; +import Svg from 'components/ui/Svg'; +import { TERMS_OF_USE } from 'constants/urls'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import { octantWordmark } from 'svg/logo'; + +import styles from './SettingsMainInfoBox.module.scss'; + +const SettingsMainInfoBox = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { isDesktop } = useMediaQuery(); + const { data: currentEpoch } = useCurrentEpoch(); + + return ( + +
+ +
{t('epoch', { epoch: currentEpoch })}
+
+
+
+ {t('golemFoundationProject')} +
+
{t('poweredByCoinGeckoApi')}
+ {!isDesktop && ( + + )} +
+
+ ); +}; + +export default memo(SettingsMainInfoBox); diff --git a/client/src/components/Settings/SettingsMainInfoBox/index.tsx b/client/src/components/Settings/SettingsMainInfoBox/index.tsx new file mode 100644 index 0000000000..2f9f8326a4 --- /dev/null +++ b/client/src/components/Settings/SettingsMainInfoBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsMainInfoBox'; diff --git a/client/src/components/Settings/SettingsPatronMode/SettingsPatronMode.tsx b/client/src/components/Settings/SettingsPatronMode/SettingsPatronMode.tsx index 5a9a8f29d6..450f2c813b 100644 --- a/client/src/components/Settings/SettingsPatronMode/SettingsPatronMode.tsx +++ b/client/src/components/Settings/SettingsPatronMode/SettingsPatronMode.tsx @@ -43,7 +43,7 @@ const SettingsPatronMode: FC = ({ onPatronModeStatusCha i18nKey={`${keyPrefix}.thirdParagraph`} /> - + { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const [isPatronModeModalOpen, setIsPatronModeModalOpen] = useState(false); + const { data: isPatronModeEnabled } = useIsPatronMode(); + + return ( + <> + setIsPatronModeModalOpen(true)} + > +
+ {t('enablePatronMode')} + + + +
+
+ setIsPatronModeModalOpen(false), + }} + /> + + ); +}; + +export default SettingsPatronModeBox; diff --git a/client/src/components/Settings/SettingsPatronModeBox/index.tsx b/client/src/components/Settings/SettingsPatronModeBox/index.tsx new file mode 100644 index 0000000000..a9240e259b --- /dev/null +++ b/client/src/components/Settings/SettingsPatronModeBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsPatronModeBox'; diff --git a/client/src/components/Settings/SettingsShowOnboardingBox/SettingsShowOnboardingBox.tsx b/client/src/components/Settings/SettingsShowOnboardingBox/SettingsShowOnboardingBox.tsx new file mode 100644 index 0000000000..7975b7caf5 --- /dev/null +++ b/client/src/components/Settings/SettingsShowOnboardingBox/SettingsShowOnboardingBox.tsx @@ -0,0 +1,26 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SettingsToggleBox from 'components/Settings/SettingsToggleBox'; +import useSettingsStore from 'store/settings/store'; + +const SettingsShowOnboardingBox = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { setIsAllocateOnboardingAlwaysVisible, isAllocateOnboardingAlwaysVisible } = + useSettingsStore(state => ({ + isAllocateOnboardingAlwaysVisible: state.data.isAllocateOnboardingAlwaysVisible, + setIsAllocateOnboardingAlwaysVisible: state.setIsAllocateOnboardingAlwaysVisible, + })); + + return ( + setIsAllocateOnboardingAlwaysVisible(event.target.checked)} + > + {t('alwaysShowOnboarding')} + + ); +}; + +export default SettingsShowOnboardingBox; diff --git a/client/src/components/Settings/SettingsShowOnboardingBox/index.tsx b/client/src/components/Settings/SettingsShowOnboardingBox/index.tsx new file mode 100644 index 0000000000..ac857c8503 --- /dev/null +++ b/client/src/components/Settings/SettingsShowOnboardingBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsShowOnboardingBox'; diff --git a/client/src/components/Settings/SettingsShowTipsBox/SettingsShowTipsBox.tsx b/client/src/components/Settings/SettingsShowTipsBox/SettingsShowTipsBox.tsx new file mode 100644 index 0000000000..57cfdb1c68 --- /dev/null +++ b/client/src/components/Settings/SettingsShowTipsBox/SettingsShowTipsBox.tsx @@ -0,0 +1,25 @@ +import React, { ReactNode } from 'react'; +import { useTranslation } from 'react-i18next'; + +import SettingsToggleBox from 'components/Settings/SettingsToggleBox'; +import useSettingsStore from 'store/settings/store'; + +const SettingsShowTipsBox = (): ReactNode => { + const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { setAreOctantTipsAlwaysVisible, areOctantTipsAlwaysVisible } = useSettingsStore(state => ({ + areOctantTipsAlwaysVisible: state.data.areOctantTipsAlwaysVisible, + setAreOctantTipsAlwaysVisible: state.setAreOctantTipsAlwaysVisible, + })); + + return ( + setAreOctantTipsAlwaysVisible(event.target.checked)} + > + {t('alwaysShowOctantTips')} + + ); +}; + +export default SettingsShowTipsBox; diff --git a/client/src/components/Settings/SettingsShowTipsBox/index.tsx b/client/src/components/Settings/SettingsShowTipsBox/index.tsx new file mode 100644 index 0000000000..55c60a1397 --- /dev/null +++ b/client/src/components/Settings/SettingsShowTipsBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsShowTipsBox'; diff --git a/client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.module.scss b/client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.module.scss new file mode 100644 index 0000000000..ae242dbfc7 --- /dev/null +++ b/client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.module.scss @@ -0,0 +1,17 @@ +.box { + height: 6.4rem; + font-size: $font-size-12; + + &:not(:first-child) { + margin: 1.6rem auto 0; + } + + @media #{$desktop-up} { + font-size: $font-size-14; + } +} + +.inputToggle { + // Prevents inputs from shrinking when label text is long. + flex-shrink: 0; +} diff --git a/client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.tsx b/client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.tsx new file mode 100644 index 0000000000..9b2ca0ef06 --- /dev/null +++ b/client/src/components/Settings/SettingsToggleBox/SettingsToggleBox.tsx @@ -0,0 +1,40 @@ +import React, { FC, ReactNode } from 'react'; + +import BoxRounded from 'components/ui/BoxRounded'; +import InputToggle from 'components/ui/InputToggle'; + +import styles from './SettingsToggleBox.module.scss'; + +type SettingsToggleBoxProps = { + children: ReactNode; + dataTest?: string; + isChecked?: boolean; + onChange: (e: React.ChangeEvent) => void; +}; + +const SettingsToggleBox: FC = ({ + children, + isChecked, + onChange, + dataTest = 'SettingsToggleBox', +}) => { + return ( + + {children} + + + ); +}; + +export default SettingsToggleBox; diff --git a/client/src/components/Settings/SettingsToggleBox/index.tsx b/client/src/components/Settings/SettingsToggleBox/index.tsx new file mode 100644 index 0000000000..4a0ef46ba6 --- /dev/null +++ b/client/src/components/Settings/SettingsToggleBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './SettingsToggleBox'; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 75e9768954..5d3fb397e2 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -463,7 +463,14 @@ "alwaysShowOnboarding": "Always show onboarding", "alwaysShowOctantTips": "Always show tips", "enablePatronMode": "Enable patron mode", - "patronModeTooltip": "Patron mode is for token holders who want to support Octant. It disables allocation to yourself or projects. All rewards go directly to the matching fund with no action required by the patron." + "patronModeTooltip": "Patron mode is for token holders who want to support Octant. It disables allocation to yourself or projects. All rewards go directly to the matching fund with no action required by the patron.", + "octantBuild": "Octant.build", + "userDocs": "User Docs", + "discordCommunity": "Discord Community", + "website": "Website", + "discord": "Discord", + "docs": "Docs", + "octantInfo": "Octant is a platform for experiments in decentralized governance that reward participation. Learn more below." }, "syncStatus": { "information": "We're synchronizing things to prepare the
next epoch, so the app will be unavailable
for a little while. Please check back soon." diff --git a/client/src/views/SettingsView/SettingsView.module.scss b/client/src/views/SettingsView/SettingsView.module.scss index 265f6675a8..53c0cd7fdb 100644 --- a/client/src/views/SettingsView/SettingsView.module.scss +++ b/client/src/views/SettingsView/SettingsView.module.scss @@ -63,28 +63,25 @@ justify-content: center; min-height: 9.9rem; + .info { + line-height: 1.8rem; + color: $color-octant-grey5; + } + @media #{$desktop-up} { + justify-content: flex-end; min-height: 8rem; margin-top: 1.6rem; - } - - .info, - .link { - font-size: $font-size-12; - line-height: 2rem; - font-weight: $font-weight-semibold; - @media #{$desktop-up} { + .info { + line-height: 2rem; font-size: $font-size-14; + font-weight: $font-weight-semibold; } - } - .link { - min-height: 2rem; - } - - .info { - color: $color-octant-grey5; + .golemFoundationProject { + color: $color-octant-green; + } } } @@ -99,3 +96,68 @@ justify-content: center; margin-left: 1.2rem; } + +.infoBoxesWrapper { + display: flex; + gap: 1.6rem; + width: 100%; +} + +.arrowRight path { + fill: $color-octant-green; +} + +.link { + display: block; + font-size: $font-size-12; + line-height: 2rem; + font-weight: $font-weight-semibold; + min-height: 2rem; + + .linkText { + margin-left: 0.4rem; + } + + @media #{$desktop-up} { + font-size: $font-size-14; + } +} + +.octantInfo { + color: $color-octant-grey5; + font-size: $font-size-14; + font-weight: $font-weight-semibold; + line-height: 2rem; +} + +.linkBoxesWrapper { + width: 100%; + display: flex; + gap: 1.5rem; + margin-top: 1.6rem; + + .linkBoxChildrenWrapper { + height: 100%; + } + + .link { + width: 100%; + height: 100%; + display: flex; + align-items: center; + } + + .linkBox { + font-size: $font-size-12; + line-height: 2rem; + font-weight: $font-weight-bold; + display: flex; + align-items: center; + justify-content: center; + height: 6.4rem; + } +} + +.arrowTopRight { + margin-left: 0.4rem; +} diff --git a/client/src/views/SettingsView/SettingsView.tsx b/client/src/views/SettingsView/SettingsView.tsx index 08e61435be..6304bc22d0 100644 --- a/client/src/views/SettingsView/SettingsView.tsx +++ b/client/src/views/SettingsView/SettingsView.tsx @@ -1,187 +1,46 @@ -import React, { Fragment, ReactElement, useState } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, { ReactElement } from 'react'; import { useAccount } from 'wagmi'; -import ModalSettingsPatronMode from 'components/Settings/ModalSettingsPatronMode'; +import SettingsCryptoMainValueBox from 'components/Settings/SettingsCryptoMainValueBox'; +import SettingsCurrencyBox from 'components/Settings/SettingsCurrencyBox'; +import SettingsLinkBoxes from 'components/Settings/SettingsLinkBoxes'; +import SettingsLinksInfoBox from 'components/Settings/SettingsLinksInfoBox'; +import SettingsMainInfoBox from 'components/Settings/SettingsMainInfoBox'; +import SettingsPatronModeBox from 'components/Settings/SettingsPatronModeBox'; +import SettingsShowOnboardingBox from 'components/Settings/SettingsShowOnboardingBox'; +import SettingsShowTipsBox from 'components/Settings/SettingsShowTipsBox'; import Layout from 'components/shared/Layout'; -import BoxRounded from 'components/ui/BoxRounded'; -import Button from 'components/ui/Button'; -import InputSelect from 'components/ui/InputSelect'; -import InputToggle from 'components/ui/InputToggle'; -import Svg from 'components/ui/Svg'; -import Tooltip from 'components/ui/Tooltip'; -import { TERMS_OF_USE } from 'constants/urls'; import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; import useMediaQuery from 'hooks/helpers/useMediaQuery'; -import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -import useIsPatronMode from 'hooks/queries/useIsPatronMode'; -import useSettingsStore from 'store/settings/store'; -import { SettingsData } from 'store/settings/types'; -import { octantWordmark } from 'svg/logo'; -import { questionMark } from 'svg/misc'; import styles from './SettingsView.module.scss'; -import { Options } from './types'; - -const options: Options = [ - { label: 'USD', value: 'usd' }, - { label: 'AUD', value: 'aud' }, - { label: 'EUR', value: 'eur' }, - { label: 'JPY', value: 'jpy' }, - { label: 'CNY', value: 'cny' }, - { label: 'GBP', value: 'gbp' }, -]; const SettingsView = (): ReactElement => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); const { isDesktop } = useMediaQuery(); - const { data: currentEpoch } = useCurrentEpoch(); const { isConnected } = useAccount(); - const { - setDisplayCurrency, - setIsAllocateOnboardingAlwaysVisible, - setIsCryptoMainValueDisplay, - setAreOctantTipsAlwaysVisible, - displayCurrency, - isAllocateOnboardingAlwaysVisible, - isCryptoMainValueDisplay, - areOctantTipsAlwaysVisible, - } = useSettingsStore(state => ({ - areOctantTipsAlwaysVisible: state.data.areOctantTipsAlwaysVisible, - displayCurrency: state.data.displayCurrency, - isAllocateOnboardingAlwaysVisible: state.data.isAllocateOnboardingAlwaysVisible, - isCryptoMainValueDisplay: state.data.isCryptoMainValueDisplay, - setAreOctantTipsAlwaysVisible: state.setAreOctantTipsAlwaysVisible, - setDisplayCurrency: state.setDisplayCurrency, - setIsAllocateOnboardingAlwaysVisible: state.setIsAllocateOnboardingAlwaysVisible, - setIsCryptoMainValueDisplay: state.setIsCryptoMainValueDisplay, - })); - - const [isPatronModeModalOpen, setIsPatronModeModalOpen] = useState(false); - const { data: isPatronModeEnabled } = useIsPatronMode(); const isProjectAdminMode = useIsProjectAdminMode(); return ( {!isProjectAdminMode && ( - - -
{t('epoch', { epoch: currentEpoch })}
-
-
{t('golemFoundationProject')}
-
{t('poweredByCoinGeckoApi')}
- -
-
- )} - - {t('chooseDisplayCurrency')} -
-
- - setDisplayCurrency(option!.value as SettingsData['displayCurrency']) - } - options={options} - selectedOption={options.find(({ value }) => value === displayCurrency)} - /> -
- - - {t('cryptoMainValueDisplay')} - setIsCryptoMainValueDisplay(isChecked)} - /> - - {isConnected && !isProjectAdminMode && ( - -
- {t('enablePatronMode')} - - - + <> +
+ + {isDesktop && }
- setIsPatronModeModalOpen(true)} - /> - + {!isDesktop && } + )} + + + {isConnected && !isProjectAdminMode && } {!isProjectAdminMode && ( - - - {t('alwaysShowOctantTips')} - setAreOctantTipsAlwaysVisible(event.target.checked)} - /> - - - {t('alwaysShowOnboarding')} - setIsAllocateOnboardingAlwaysVisible(event.target.checked)} - /> - - + <> + + + )} - setIsPatronModeModalOpen(false), - }} - /> ); }; From 21988071dbdc17f212dbac02bdedbfbb051a141d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Mon, 18 Mar 2024 17:47:21 +0100 Subject: [PATCH 032/107] chore: refactor epoch state validatios --- backend/app/context/epoch_state.py | 18 ++++++++++++------ backend/tests/conftest.py | 8 +++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/backend/app/context/epoch_state.py b/backend/app/context/epoch_state.py index 944428bcaf..5d5133253e 100644 --- a/backend/app/context/epoch_state.py +++ b/backend/app/context/epoch_state.py @@ -65,21 +65,27 @@ def validate_epoch_num(epoch_num: int): def validate_epoch_state(epoch_num: int, epoch_state: EpochState): - pending_snapshot = database.pending_epoch_snapshot.get_by_epoch(epoch_num) - finalized_snapshot = database.finalized_epoch_snapshot.get_by_epoch(epoch_num) + has_pending_snapshot = _has_pending_epoch_snapshot(epoch_num) + has_finalized_snapshot = _has_finalized_epoch_snapshot(epoch_num) if epoch_state == EpochState.PRE_PENDING: - if pending_snapshot is not None: + if has_pending_snapshot: raise InvalidEpoch() if epoch_state == EpochState.PENDING: - if pending_snapshot is None: + if not has_pending_snapshot: raise InvalidEpoch() if epoch_state == EpochState.FINALIZING: - if pending_snapshot is None or finalized_snapshot is not None: + if has_finalized_snapshot or not has_pending_snapshot: raise InvalidEpoch() if epoch_state == EpochState.FINALIZED: - if pending_snapshot is None or finalized_snapshot is None: + if not (has_pending_snapshot and has_finalized_snapshot): raise InvalidEpoch() + +def _has_pending_epoch_snapshot(epoch_num: int): + return database.pending_epoch_snapshot.get_by_epoch(epoch_num) is not None + +def _has_finalized_epoch_snapshot(epoch_num: int): + return database.finalized_epoch_snapshot.get_by_epoch(epoch_num) is not None diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index f7a8140f0b..a04a5a8172 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -495,7 +495,13 @@ def patch_has_pending_epoch_snapshot(monkeypatch): ( monkeypatch.setattr( "app.legacy.core.allocations.has_pending_epoch_snapshot", - MOCK_HAS_PENDING_SNAPSHOT, + MOCK_HAS_PENDING_SNAPSHOT + ) + ) + ( + monkeypatch.setattr( + "app.context.epoch_state._has_pending_epoch_snapshot", + MOCK_HAS_PENDING_SNAPSHOT ) ) MOCK_HAS_PENDING_SNAPSHOT.return_value = True From 70ac22147d8a131d22ea50cf994cb9ab02bce110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 12 Mar 2024 16:56:00 +0100 Subject: [PATCH 033/107] feature: rework next allocation nonce --- .../infrastructure/database/allocations.py | 9 ++ backend/app/infrastructure/database/models.py | 5 - .../app/infrastructure/routes/allocations.py | 8 +- backend/app/legacy/controllers/allocations.py | 11 +- backend/app/legacy/core/allocations.py | 14 +-- .../app/modules/modules_factory/current.py | 3 + .../modules/user/allocations/controller.py | 8 ++ backend/app/modules/user/allocations/core.py | 7 +- .../user/allocations/service/history.py | 11 ++ ...89a0fae4ae_drop_allocation_nonce_column.py | 36 ++++++ backend/tests/legacy/test_allocations.py | 6 +- backend/tests/legacy/test_rewards.py | 5 + .../allocations/test_history_allocations.py | 104 ++++++++++++++++++ 13 files changed, 204 insertions(+), 23 deletions(-) create mode 100644 backend/app/modules/user/allocations/service/history.py create mode 100644 backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py create mode 100644 backend/tests/modules/user/allocations/test_history_allocations.py diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 3ff5cbfcab..0ff5e7cde8 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -213,3 +213,12 @@ def get_allocation_request_by_user_and_epoch( raise UserNotFound(user_address) return AllocationRequest.query.filter_by(user_id=user.id, epoch=epoch).first() + + +def get_user_last_allocation_request(user_address: str) -> AllocationRequest | None: + return ( + AllocationRequest.query.join(User, User.id == AllocationRequest.user_id) + .filter(User.address == user_address) + .order_by(AllocationRequest.nonce.desc()) + .first() + ) diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index d70c2e9ca7..85f770987d 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -20,11 +20,6 @@ class User(BaseModel): id = Column(db.Integer, primary_key=True) address = Column(db.String(42), unique=True, nullable=False) - allocation_nonce = Column( - db.Integer, - nullable=True, - comment="Allocations signing nonce, last used value. Range [0..inf)", - ) def get_effective_deposit(self, epoch: int) -> Optional[int]: effective_deposit = None diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 4f0d613882..31d5fec1aa 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -7,7 +7,11 @@ from app.legacy.core.allocations import AllocationRequest from app.extensions import api from app.infrastructure import OctantResource -from app.modules.user.allocations.controller import get_donors, simulate_allocation +from app.modules.user.allocations.controller import ( + get_donors, + simulate_allocation, + get_user_next_nonce, +) ns = Namespace("allocations", description="Octant allocations") api.add_namespace(ns) @@ -329,7 +333,7 @@ class AllocationNonce(OctantResource): @ns.marshal_with(allocation_nonce_model) @ns.response(200, "User allocations nonce successfully retrieved") def get(self, user_address: str): - return {"allocationNonce": allocations.get_allocation_nonce(user_address)} + return {"allocationNonce": get_user_next_nonce(user_address)} @ns.route("/donors/") diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index e35dca0e4e..2c50643281 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -1,3 +1,4 @@ +from typing_extensions import deprecated from dataclasses import dataclass from typing import List, Dict from typing import Optional @@ -7,6 +8,7 @@ from app import exceptions from app.extensions import db, epochs from app.infrastructure import database +from app.modules.user.allocations import controller as new_controller from app.legacy.core.allocations import ( AllocationRequest, recover_user_address, @@ -15,7 +17,6 @@ add_allocations_to_db, revoke_previous_allocation, store_allocation_request, - next_allocation_nonce, ) from app.legacy.core.common import AccountFunds from app.legacy.core.epochs import epoch_snapshots @@ -33,7 +34,7 @@ def allocate( ) -> str: user_address = recover_user_address(request) user = database.user.get_by_address(user_address) - next_nonce = next_allocation_nonce(user) + next_nonce = new_controller.get_user_next_nonce(user_address) _make_allocation( request.payload, user_address, request.override_existing_allocations, next_nonce @@ -110,11 +111,7 @@ def get_sum_by_epoch(epoch: int | None = None) -> int: return database.allocations.get_alloc_sum_by_epoch(epoch) -def get_allocation_nonce(user_address: str) -> int: - user = database.user.get_by_address(user_address) - return next_allocation_nonce(user) - - +@deprecated("ALLOCATIONS REWORK") def revoke_previous_user_allocation(user_address: str): pending_epoch = epochs.get_pending_epoch() diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py index c19efccaaf..60bee2fb45 100644 --- a/backend/app/legacy/core/allocations.py +++ b/backend/app/legacy/core/allocations.py @@ -7,7 +7,6 @@ from app import exceptions from app.extensions import proposals from app.infrastructure import database -from app.infrastructure.database.models import User from app.legacy.core.epochs.epoch_snapshots import has_pending_epoch_snapshot from app.legacy.core.user.budget import get_budget from app.legacy.core.user.patron_mode import get_patron_mode_status @@ -124,9 +123,10 @@ def revoke_previous_allocation(user_address: str, epoch: int): database.allocations.soft_delete_all_by_epoch_and_user_id(epoch, user.id) -def next_allocation_nonce(user: User | None) -> int: - if user is None: - return 0 - if user.allocation_nonce is None: - return 0 - return user.allocation_nonce + 1 +def has_user_allocated_rewards(user_address: str, epoch: int) -> List[str]: + allocation_signature = ( + database.allocations.get_allocation_request_by_user_and_epoch( + user_address, epoch + ) + ) + return allocation_signature is not None diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index 2173ca3ec7..cedad86d1d 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -14,6 +14,7 @@ from app.modules.snapshots.pending.service.simulated import SimulatedPendingSnapshots from app.modules.staking.proceeds.service.estimated import EstimatedStakingProceeds from app.modules.user.allocations.service.saved import SavedUserAllocations +from app.modules.user.allocations.service.history import UserAllocationsHistory from app.modules.user.deposits.service.calculated import CalculatedUserDeposits from app.modules.user.events_generator.service.db_and_graph import ( DbAndGraphEventsGenerator, @@ -29,6 +30,7 @@ class CurrentUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protoco class CurrentServices(Model): + user_allocations_history_service: UserAllocationsHistory user_deposits_service: CurrentUserDeposits octant_rewards_service: OctantRewards history_service: HistoryService @@ -70,6 +72,7 @@ def create(chain_id: int) -> "CurrentServices": patron_donations=patron_donations, ) return CurrentServices( + user_allocations_history_service=UserAllocationsHistory(), user_deposits_service=user_deposits, octant_rewards_service=CalculatedOctantRewards( staking_proceeds=EstimatedStakingProceeds(), diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index d78d80ebbd..340de7253b 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -6,6 +6,14 @@ from app.modules.dto import AllocationDTO from app.modules.registry import get_services from app.modules.user.allocations.service.pending import PendingUserAllocations +from app.modules.user.allocations.service.history import UserAllocationsHistory + + +def get_user_next_nonce(user_address: str) -> int: + service: UserAllocationsHistory = get_services( + EpochState.CURRENT + ).user_allocations_history_service + return service.get_next_user_nonce(user_address) def get_donors(epoch_num: int) -> List[str]: diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index a5e9f281ca..990258758a 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -1,11 +1,16 @@ -from typing import List +from typing import List, Optional from app.engine.projects import ProjectSettings +from app.infrastructure.database.models import AllocationRequest from app.modules.common.leverage import calculate_leverage from app.modules.common.project_rewards import get_projects_rewards from app.modules.dto import AllocationDTO +def next_allocation_nonce(prev_allocation_request: Optional[AllocationRequest]) -> int: + return 0 if prev_allocation_request is None else prev_allocation_request.nonce + 1 + + def simulate_allocation( projects_settings: ProjectSettings, all_allocations_before: List[AllocationDTO], diff --git a/backend/app/modules/user/allocations/service/history.py b/backend/app/modules/user/allocations/service/history.py new file mode 100644 index 0000000000..5345625beb --- /dev/null +++ b/backend/app/modules/user/allocations/service/history.py @@ -0,0 +1,11 @@ +from app.pydantic import Model + +from app.infrastructure import database + + +class UserAllocationsHistory(Model): + def get_next_user_nonce(self, user_address: str) -> int: + allocation_request = database.allocations.get_user_last_allocation_request( + user_address + ) + return 0 if allocation_request is None else allocation_request.nonce + 1 diff --git a/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py b/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py new file mode 100644 index 0000000000..9b69c33329 --- /dev/null +++ b/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py @@ -0,0 +1,36 @@ +"""drop allocation nonce column from user table + +Revision ID: 1f89a0fae4ae +Revises: 7bb6835486a5 +Create Date: 2024-03-12 18:00:32.503807 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "1f89a0fae4ae" +down_revision = "7bb6835486a5" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_column("allocation_nonce") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "allocation_nonce", sa.INTEGER(), autoincrement=False, nullable=True + ) + ) + + # ### end Alembic commands ### diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py index 8a9d945273..de70bbd73a 100644 --- a/backend/tests/legacy/test_allocations.py +++ b/backend/tests/legacy/test_allocations.py @@ -8,7 +8,6 @@ from app.legacy.controllers.allocations import ( get_all_by_user_and_epoch, get_all_by_proposal_and_epoch, - get_allocation_nonce, get_all_by_epoch, get_sum_by_epoch, allocate, @@ -31,6 +30,11 @@ from tests.helpers import create_epoch_event +from app.modules.user.allocations import controller as new_controller +def get_allocation_nonce(user_address): + return new_controller.get_user_next_nonce(user_address) + + @pytest.fixture(scope="function") def get_all_by_epoch_expected_result(user_accounts, proposal_accounts): return [ diff --git a/backend/tests/legacy/test_rewards.py b/backend/tests/legacy/test_rewards.py index d933de02fa..4db2275ca7 100644 --- a/backend/tests/legacy/test_rewards.py +++ b/backend/tests/legacy/test_rewards.py @@ -18,6 +18,11 @@ ) +from app.modules.user.allocations import controller as new_controller +def get_allocation_nonce(user_address): + return new_controller.get_user_next_nonce(user_address) + + @pytest.fixture(autouse=True) def before( proposal_accounts, diff --git a/backend/tests/modules/user/allocations/test_history_allocations.py b/backend/tests/modules/user/allocations/test_history_allocations.py new file mode 100644 index 0000000000..6fc0f83f9e --- /dev/null +++ b/backend/tests/modules/user/allocations/test_history_allocations.py @@ -0,0 +1,104 @@ +import pytest + +from app.infrastructure import database +from app.modules.dto import UserAllocationRequestPayload, UserAllocationPayload +from app.modules.user.allocations.service.history import UserAllocationsHistory + + +@pytest.fixture() +def service(app): + return UserAllocationsHistory() + + +def _mock_request(nonce): + fake_signature = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + return UserAllocationRequestPayload( + payload=UserAllocationPayload([], nonce), signature=fake_signature + ) + + +def test_user_nonce_for_non_existent_user_is_0(service, alice): + assert database.user.get_by_address(alice.address) is None + assert service.get_next_user_nonce(alice.address) == 0 + + +def test_user_nonce_for_new_user_is_0(service, mock_users_db): + alice, _, _ = mock_users_db + + assert service.get_next_user_nonce(alice.address) == 0 + + +def test_user_nonce_changes_increases_at_each_allocation_request( + service, mock_users_db +): + alice, _, _ = mock_users_db + + database.allocations.store_allocation_request(alice.address, 0, _mock_request(0)) + new_nonce = service.get_next_user_nonce(alice.address) + + assert new_nonce == 1 + + database.allocations.store_allocation_request( + alice.address, 0, _mock_request(new_nonce) + ) + new_nonce = service.get_next_user_nonce(alice.address) + + assert new_nonce == 2 + + +def test_user_nonce_changes_increases_at_each_allocation_request_for_each_user( + service, mock_users_db +): + alice, bob, carol = mock_users_db + + for i in range(0, 5): + database.allocations.store_allocation_request( + alice.address, 0, _mock_request(i) + ) + next_user_nonce = service.get_next_user_nonce(alice.address) + assert next_user_nonce == i + 1 + + # for other users, nonces do not change + assert service.get_next_user_nonce(bob.address) == 0 + assert service.get_next_user_nonce(carol.address) == 0 + + for i in range(0, 4): + database.allocations.store_allocation_request(bob.address, 0, _mock_request(i)) + next_user_nonce = service.get_next_user_nonce(bob.address) + assert next_user_nonce == i + 1 + + # for other users, nonces do not change + assert service.get_next_user_nonce(alice.address) == 5 + assert service.get_next_user_nonce(carol.address) == 0 + + for i in range(0, 3): + database.allocations.store_allocation_request( + carol.address, 0, _mock_request(i) + ) + next_user_nonce = service.get_next_user_nonce(carol.address) + assert next_user_nonce == i + 1 + + # for other users, nonces do not change + assert service.get_next_user_nonce(alice.address) == 5 + assert service.get_next_user_nonce(bob.address) == 4 + + +def test_user_nonce_is_continuous_despite_epoch_changes(service, mock_users_db): + alice, _, _ = mock_users_db + + database.allocations.store_allocation_request(alice.address, 1, _mock_request(0)) + new_nonce = service.get_next_user_nonce(alice.address) + assert new_nonce == 1 + + database.allocations.store_allocation_request( + alice.address, 2, _mock_request(new_nonce) + ) + new_nonce = service.get_next_user_nonce(alice.address) + assert new_nonce == 2 + + database.allocations.store_allocation_request( + alice.address, 10, _mock_request(new_nonce) + ) + new_nonce = service.get_next_user_nonce(alice.address) + assert new_nonce == 3 From 2e10bf77f302fd440728d006f1da3540abdf2fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 12 Mar 2024 18:57:16 +0100 Subject: [PATCH 034/107] feature: create get allocs protocol --- backend/app/modules/modules_factory/finalized.py | 9 ++++++++- backend/app/modules/modules_factory/pending.py | 5 ++++- backend/app/modules/modules_factory/protocols.py | 6 ++++++ backend/app/modules/user/allocations/service/saved.py | 1 - 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/backend/app/modules/modules_factory/finalized.py b/backend/app/modules/modules_factory/finalized.py index 44d20f75bb..1a38827c9f 100644 --- a/backend/app/modules/modules_factory/finalized.py +++ b/backend/app/modules/modules_factory/finalized.py @@ -3,6 +3,7 @@ from app.modules.modules_factory.protocols import ( OctantRewards, DonorsAddresses, + GetUserAllocationsProtocol, UserPatronMode, UserRewards, UserEffectiveDeposits, @@ -29,10 +30,16 @@ class FinalizedUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Proto pass +class FinalizedUserAllocationsProtocol( + DonorsAddresses, GetUserAllocationsProtocol, Protocol +): + pass + + class FinalizedServices(Model): user_deposits_service: FinalizedUserDeposits octant_rewards_service: FinalizedOctantRewardsProtocol - user_allocations_service: DonorsAddresses + user_allocations_service: FinalizedUserAllocationsProtocol user_patron_mode_service: UserPatronMode user_budgets_service: UserBudgets user_rewards_service: UserRewards diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 34e48cb5f6..ca99479184 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -13,6 +13,7 @@ EstimatedProjectRewardsService, OctantRewards, DonorsAddresses, + GetUserAllocationsProtocol, ) from app.modules.octant_rewards.service.pending import PendingOctantRewards from app.modules.snapshots.finalized.service.simulated import ( @@ -36,7 +37,9 @@ class PendingUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protoco pass -class PendingUserAllocationsProtocol(DonorsAddresses, SimulateAllocation, Protocol): +class PendingUserAllocationsProtocol( + DonorsAddresses, GetUserAllocationsProtocol, SimulateAllocation, Protocol +): pass diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index 369f16d6f4..3a0e7e98cb 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -51,6 +51,12 @@ def get_all_donors_addresses(self, context: Context) -> List[str]: ... +@runtime_checkable +class GetUserAllocationsProtocol(Protocol): + def get_all_allocations(self, context: Context) -> int: + ... + + @runtime_checkable class SimulateAllocation(Protocol): def simulate_allocation( diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index d6e8f80d79..aaff615b85 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -6,7 +6,6 @@ from app.modules.dto import AccountFundsDTO, AllocationItem from app.pydantic import Model - class SavedUserAllocations(Model): def get_all_donors_addresses(self, context: Context) -> List[str]: return database.allocations.get_users_with_allocations( From 9e7608a98ef3c89659142e0ae2d54d3d45709bfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 13 Mar 2024 08:37:36 +0100 Subject: [PATCH 035/107] refactor: rename AllocationDTO to AllocationItem --- .../app/engine/projects/rewards/__init__.py | 4 +- .../projects/rewards/allocations/__init__.py | 4 +- backend/app/modules/dto.py | 11 ++++- .../projects/rewards/test_default_rewards.py | 44 +++++++++---------- 4 files changed, 35 insertions(+), 28 deletions(-) diff --git a/backend/app/engine/projects/rewards/__init__.py b/backend/app/engine/projects/rewards/__init__.py index b81e1739e5..062ef3e327 100644 --- a/backend/app/engine/projects/rewards/__init__.py +++ b/backend/app/engine/projects/rewards/__init__.py @@ -4,7 +4,7 @@ from dataclass_wizard import JSONWizard -from app.engine.projects.rewards.allocations import AllocationPayload +from app.engine.projects.rewards.allocations import AllocationItem @dataclass @@ -26,7 +26,7 @@ def __iter__(self): @dataclass class ProjectRewardsPayload: matched_rewards: int = None - allocations: List[AllocationPayload] = None + allocations: List[AllocationItem] = None projects: List[str] = None diff --git a/backend/app/engine/projects/rewards/allocations/__init__.py b/backend/app/engine/projects/rewards/allocations/__init__.py index f61fed39f4..117501c6f8 100644 --- a/backend/app/engine/projects/rewards/allocations/__init__.py +++ b/backend/app/engine/projects/rewards/allocations/__init__.py @@ -4,14 +4,14 @@ @dataclass(frozen=True) -class AllocationPayload: +class AllocationItem: proposal_address: str amount: int @dataclass class ProjectAllocationsPayload: - allocations: List[AllocationPayload] = None + allocations: List[AllocationItem] = None @dataclass diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 74b272ffdc..9d1b87e996 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -5,8 +5,8 @@ from dataclass_wizard import JSONWizard -from app.engine.projects.rewards import AllocationPayload from app.modules.common.time import Timestamp +from app.engine.projects.rewards import AllocationItem from app.engine.user.effective_deposit import UserDeposit from app.modules.snapshots.pending import UserBudgetInfo @@ -65,10 +65,17 @@ class PendingSnapshotDTO(JSONWizard): @dataclass(frozen=True) -class AllocationDTO(AllocationPayload, JSONWizard): +class AllocationDTO(AllocationItem, JSONWizard): user_address: Optional[str] = None +@dataclass(frozen=True) +class ProposalDonationDTO(JSONWizard): + donor: str + amount: int + proposal: str + + class WithdrawalStatus(StrEnum): PENDING = "pending" AVAILABLE = "available" diff --git a/backend/tests/engine/projects/rewards/test_default_rewards.py b/backend/tests/engine/projects/rewards/test_default_rewards.py index 7ac5b545f9..39172c75dc 100644 --- a/backend/tests/engine/projects/rewards/test_default_rewards.py +++ b/backend/tests/engine/projects/rewards/test_default_rewards.py @@ -5,7 +5,7 @@ ProjectRewardsPayload, ProjectRewardsResult, ProjectRewardDTO, - AllocationPayload, + AllocationItem, ) from tests.helpers.context import get_project_details @@ -24,7 +24,7 @@ def test_compute_rewards_for_none_allocations(): def test_compute_rewards_for_allocations_to_one_project(): projects = get_project_details().projects - allocations = [AllocationPayload(projects[0], 100_000000000)] + allocations = [AllocationItem(projects[0], 100_000000000)] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects) uut = DefaultProjectRewards() @@ -47,10 +47,10 @@ def test_compute_rewards_for_allocations_to_one_project(): def test_compute_rewards_for_allocations_to_multiple_project(): projects = get_project_details().projects allocations = [ - AllocationPayload(projects[0], 100_000000000), - AllocationPayload(projects[0], 100_000000000), - AllocationPayload(projects[1], 200_000000000), - AllocationPayload(projects[2], 500_000000000), + AllocationItem(projects[0], 100_000000000), + AllocationItem(projects[0], 100_000000000), + AllocationItem(projects[1], 200_000000000), + AllocationItem(projects[2], 500_000000000), ] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects) uut = DefaultProjectRewards() @@ -82,9 +82,9 @@ def test_compute_rewards_for_allocations_to_multiple_project(): def test_total_matched_rewards_are_distributed(): projects = get_project_details().projects allocations = [ - AllocationPayload(projects[0], 200_000000000), - AllocationPayload(projects[1], 200_000000000), - AllocationPayload(projects[2], 500_000000000), + AllocationItem(projects[0], 200_000000000), + AllocationItem(projects[1], 200_000000000), + AllocationItem(projects[2], 500_000000000), ] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects) uut = DefaultProjectRewards() @@ -100,9 +100,9 @@ def test_compute_rewards_when_one_project_is_below_threshold(): projects = get_project_details().projects allocations = [ - AllocationPayload(projects[0], 69_000000000), - AllocationPayload(projects[1], 200_000000000), - AllocationPayload(projects[2], 500_000000000), + AllocationItem(projects[0], 69_000000000), + AllocationItem(projects[1], 200_000000000), + AllocationItem(projects[2], 500_000000000), ] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects[:5]) uut = DefaultProjectRewards() @@ -129,9 +129,9 @@ def test_compute_rewards_when_one_project_is_at_threshold(): projects = get_project_details().projects allocations = [ - AllocationPayload(projects[0], 100_000000000), - AllocationPayload(projects[1], 400_000000000), - AllocationPayload(projects[2], 500_000000000), + AllocationItem(projects[0], 100_000000000), + AllocationItem(projects[1], 400_000000000), + AllocationItem(projects[2], 500_000000000), ] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects[:5]) uut = DefaultProjectRewards() @@ -158,9 +158,9 @@ def test_compute_rewards_when_multiple_projects_are_below_threshold(): projects = get_project_details().projects allocations = [ - AllocationPayload(projects[0], 30_000000000), - AllocationPayload(projects[1], 30_000000000), - AllocationPayload(projects[2], 500_000000000), + AllocationItem(projects[0], 30_000000000), + AllocationItem(projects[1], 30_000000000), + AllocationItem(projects[2], 500_000000000), ] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects[:5]) uut = DefaultProjectRewards() @@ -185,10 +185,10 @@ def test_total_allocated_is_computed(): projects = get_project_details().projects allocations = [ - AllocationPayload(projects[0], 300_000000000), - AllocationPayload(projects[0], 300_000000000), - AllocationPayload(projects[1], 200_000000000), - AllocationPayload(projects[2], 500_000000000), + AllocationItem(projects[0], 300_000000000), + AllocationItem(projects[0], 300_000000000), + AllocationItem(projects[1], 200_000000000), + AllocationItem(projects[2], 500_000000000), ] payload = ProjectRewardsPayload(MATCHED_REWARDS, allocations, projects[:5]) uut = DefaultProjectRewards() From 9facc9115f918c64a2e0a5fb3c68e6db89d902b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 12 Mar 2024 19:49:11 +0100 Subject: [PATCH 036/107] feature: rework get all donations by epoch --- .../app/infrastructure/routes/allocations.py | 16 +- backend/app/legacy/controllers/allocations.py | 1 + backend/app/modules/dto.py | 5 + .../app/modules/modules_factory/protocols.py | 2 +- .../modules/user/allocations/controller.py | 10 +- .../modules/user/allocations/service/saved.py | 15 ++ backend/tests/legacy/test_allocations.py | 49 +----- backend/tests/legacy/test_rewards.py | 4 +- .../allocations/test_saved_allocations.py | 141 ++++++++++++++++-- 9 files changed, 174 insertions(+), 69 deletions(-) diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 31d5fec1aa..908eda3c6e 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -7,11 +7,7 @@ from app.legacy.core.allocations import AllocationRequest from app.extensions import api from app.infrastructure import OctantResource -from app.modules.user.allocations.controller import ( - get_donors, - simulate_allocation, - get_user_next_nonce, -) +from app.modules.user.allocations import controller ns = Namespace("allocations", description="Octant allocations") api.add_namespace(ns) @@ -230,7 +226,9 @@ class AllocationLeverage(OctantResource): @ns.response(200, "User leverage successfully estimated") def post(self, user_address: str): app.logger.debug("Estimating user leverage") - leverage, threshold, matched = simulate_allocation(ns.payload, user_address) + leverage, threshold, matched = controller.simulate_allocation( + ns.payload, user_address + ) app.logger.debug(f"Estimated leverage: {leverage}") app.logger.debug(f"Estimated threshold: {threshold}") @@ -251,7 +249,7 @@ class EpochAllocations(OctantResource): @ns.response(200, "Epoch allocations successfully retrieved") def get(self, epoch: int): app.logger.debug(f"Getting latest allocations in epoch {epoch}") - allocs = allocations.get_all_by_epoch(epoch, include_zeroes=True) + allocs = controller.get_all_allocations(epoch) app.logger.debug(f"Allocations for epoch {epoch}: {allocs}") return {"allocations": allocs} @@ -333,7 +331,7 @@ class AllocationNonce(OctantResource): @ns.marshal_with(allocation_nonce_model) @ns.response(200, "User allocations nonce successfully retrieved") def get(self, user_address: str): - return {"allocationNonce": get_user_next_nonce(user_address)} + return {"allocationNonce": controller.get_user_next_nonce(user_address)} @ns.route("/donors/") @@ -348,7 +346,7 @@ class Donors(OctantResource): @ns.response(200, "Donors addresses retrieved") def get(self, epoch: int): app.logger.debug(f"Getting donors addresses for epoch {epoch}") - donors = get_donors(epoch) + donors = controller.get_donors(epoch) app.logger.debug(f"Donors addresses: {donors}") return {"donors": donors} diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index 2c50643281..a98f97aa12 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -91,6 +91,7 @@ def get_all_by_proposal_and_epoch( ] +@deprecated("ALLOCATIONS REWORK") def get_all_by_epoch( epoch: int, include_zeroes: bool = False ) -> List[EpochAllocationRecord]: diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 9d1b87e996..367825fd8f 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -68,6 +68,11 @@ class PendingSnapshotDTO(JSONWizard): class AllocationDTO(AllocationItem, JSONWizard): user_address: Optional[str] = None +@dataclass(frozen=True) +class ProposalDonationDTO(JSONWizard): + donor: str + amount: int + proposal: str @dataclass(frozen=True) class ProposalDonationDTO(JSONWizard): diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index 3a0e7e98cb..e1ef29cf0a 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -53,7 +53,7 @@ def get_all_donors_addresses(self, context: Context) -> List[str]: @runtime_checkable class GetUserAllocationsProtocol(Protocol): - def get_all_allocations(self, context: Context) -> int: + def get_all_allocations(self, context: Context) -> List[AllocationDTO]: ... diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index 340de7253b..a023bf16af 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -3,7 +3,7 @@ from app.context.epoch_state import EpochState from app.context.manager import epoch_context, state_context from app.exceptions import NotImplementedForGivenEpochState -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationDTO, ProposalDonationDTO from app.modules.registry import get_services from app.modules.user.allocations.service.pending import PendingUserAllocations from app.modules.user.allocations.service.history import UserAllocationsHistory @@ -16,6 +16,14 @@ def get_user_next_nonce(user_address: str) -> int: return service.get_next_user_nonce(user_address) +def get_all_allocations(epoch_num: int) -> List[ProposalDonationDTO]: + context = epoch_context(epoch_num) + if context.epoch_state > EpochState.PENDING: + raise NotImplementedForGivenEpochState() + service = get_services(context.epoch_state).user_allocations_service + return service.get_all_allocations(context) + + def get_donors(epoch_num: int) -> List[str]: context = epoch_context(epoch_num) if context.epoch_state > EpochState.PENDING: diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index aaff615b85..79371b4bce 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -46,3 +46,18 @@ def get_user_allocations_by_timestamp( user_address, from_timestamp.datetime(), limit ) ] + def get_all_allocations(self, context: Context) -> List[ProposalDonationDTO]: + allocations = database.allocations.get_all(context.epoch_details.epoch_num) + return [ + ProposalDonationDTO( + donor=alloc.user_address, + amount=alloc.amount, + proposal=alloc.proposal_address, + ) + for alloc in allocations + ] + + def get_last_user_allocation( + self, context: Context, user_address: str + ) -> Tuple[List[AllocationDTO], bool]: + pass diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py index de70bbd73a..646ed09e90 100644 --- a/backend/tests/legacy/test_allocations.py +++ b/backend/tests/legacy/test_allocations.py @@ -8,7 +8,6 @@ from app.legacy.controllers.allocations import ( get_all_by_user_and_epoch, get_all_by_proposal_and_epoch, - get_all_by_epoch, get_sum_by_epoch, allocate, ) @@ -31,9 +30,15 @@ from app.modules.user.allocations import controller as new_controller + + def get_allocation_nonce(user_address): return new_controller.get_user_next_nonce(user_address) - + + +def get_all_by_epoch(epoch, include_zeroes=False): + return new_controller.get_all_allocations(epoch) + @pytest.fixture(scope="function") def get_all_by_epoch_expected_result(user_accounts, proposal_accounts): @@ -342,46 +347,6 @@ def test_get_by_user_and_epoch(mock_allocations_db, user_accounts, proposal_acco assert result[2].amount == str(300 * 10**18) -def test_get_all_by_epoch( - mock_pending_epoch_snapshot_db, - mock_allocations_db, - get_all_by_epoch_expected_result, -): - result = get_all_by_epoch(MOCKED_PENDING_EPOCH_NO) - - assert len(result) == len(get_all_by_epoch_expected_result) - for i in result: - assert dataclasses.asdict(i) in get_all_by_epoch_expected_result - - -def test_get_by_epoch_fails_for_current_or_future_epoch( - mock_allocations_db, user_accounts, proposal_accounts -): - with pytest.raises(exceptions.EpochAllocationPeriodNotStartedYet): - get_all_by_epoch(MOCKED_PENDING_EPOCH_NO + 1) - - -def test_get_all_by_epoch_with_allocation_amount_equal_0( - mock_pending_epoch_snapshot_db, - mock_allocations_db, - user_accounts, - proposal_accounts, - get_all_by_epoch_expected_result, -): - user = database.user.get_or_add_user(user_accounts[2].address) - db.session.commit() - user_allocations = [ - Allocation(proposal_accounts[1].address, 0), - ] - database.allocations.add_all(MOCKED_PENDING_EPOCH_NO, user.id, 0, user_allocations) - - result = get_all_by_epoch(MOCKED_PENDING_EPOCH_NO) - - assert len(result) == len(get_all_by_epoch_expected_result) - for i in result: - assert dataclasses.asdict(i) in get_all_by_epoch_expected_result - - def test_get_by_proposal_and_epoch( mock_allocations_db, user_accounts, proposal_accounts ): diff --git a/backend/tests/legacy/test_rewards.py b/backend/tests/legacy/test_rewards.py index 4db2275ca7..8c23b9b4cf 100644 --- a/backend/tests/legacy/test_rewards.py +++ b/backend/tests/legacy/test_rewards.py @@ -19,9 +19,11 @@ from app.modules.user.allocations import controller as new_controller + + def get_allocation_nonce(user_address): return new_controller.get_user_next_nonce(user_address) - + @pytest.fixture(autouse=True) def before( diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index 0d06ae4622..41c4cac701 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -4,8 +4,10 @@ from app.extensions import db from app.infrastructure import database from app.modules.common.time import from_timestamp_s -from app.modules.dto import AllocationDTO, AllocationItem +from app.modules.dto import AllocationDTO, AllocationItem, ProposalDonationDTO +from app.modules.user.allocations.controller import revoke_previous_allocation from app.modules.user.allocations.service.saved import SavedUserAllocations + from tests.helpers.context import get_context @@ -14,7 +16,12 @@ def before(app): pass -def test_get_all_donors_addresses(mock_users_db, proposal_accounts): +@pytest.fixture() +def service(): + return SavedUserAllocations() + + +def test_get_all_donors_addresses(service, mock_users_db, proposal_accounts): user1, user2, user3 = mock_users_db allocation = [ @@ -29,8 +36,6 @@ def test_get_all_donors_addresses(mock_users_db, proposal_accounts): context_epoch_1 = get_context(1) context_epoch_2 = get_context(2) - service = SavedUserAllocations() - result_epoch_1 = service.get_all_donors_addresses(context_epoch_1) result_epoch_2 = service.get_all_donors_addresses(context_epoch_2) @@ -38,7 +43,7 @@ def test_get_all_donors_addresses(mock_users_db, proposal_accounts): assert result_epoch_2 == [user3.address] -def test_return_only_not_removed_allocations(mock_users_db, proposal_accounts): +def test_return_only_not_removed_allocations(service, mock_users_db, proposal_accounts): user1, user2, _ = mock_users_db allocation = [ @@ -52,14 +57,12 @@ def test_return_only_not_removed_allocations(mock_users_db, proposal_accounts): context = get_context(1) - service = SavedUserAllocations() - result = service.get_all_donors_addresses(context) assert result == [user1.address] -def test_get_user_allocation_sum(context, mock_users_db, proposal_accounts): +def test_get_user_allocation_sum(service, context, mock_users_db, proposal_accounts): user1, user2, _ = mock_users_db allocation = [ AllocationDTO(proposal_accounts[0].address, 100), @@ -69,31 +72,26 @@ def test_get_user_allocation_sum(context, mock_users_db, proposal_accounts): database.allocations.add_all(1, user2.id, 0, allocation) db.session.commit() - service = SavedUserAllocations() - result = service.get_user_allocation_sum(context, user1.address) assert result == 300 -def test_has_user_allocated_rewards(context, mock_users_db, proposal_accounts): +def test_has_user_allocated_rewards(service, context, mock_users_db, proposal_accounts): user1, _, _ = mock_users_db database.allocations.add_allocation_request(user1.address, 1, 0, "0x00", False) db.session.commit() - service = SavedUserAllocations() - result = service.has_user_allocated_rewards(context, user1.address) assert result is True def test_has_user_allocated_rewards_returns_false( - context, mock_users_db, proposal_accounts + service, context, mock_users_db, proposal_accounts ): user1, _, _ = mock_users_db - service = SavedUserAllocations() result = service.has_user_allocated_rewards(context, user1.address) @@ -148,3 +146,116 @@ def test_user_allocations_by_timestamp(context, mock_users_db, proposal_accounts timestamp=from_timestamp_s(1710720000), ) ] +def test_get_all_allocations_returns_empty_list_when_no_allocations( + service, context, mock_users_db +): + user1, _, _ = mock_users_db + + assert service.get_all_allocations(context) == [] + + +def test_get_all_allocations_returns_list_of_allocations( + service, context, mock_users_db, proposal_accounts +): + user1, user2, _ = mock_users_db + allocation = [ + AllocationDTO(proposal_accounts[0].address, 100), + AllocationDTO(proposal_accounts[1].address, 200), + ] + database.allocations.add_all( + context.epoch_details.epoch_num, user1.id, 0, allocation + ) + database.allocations.add_all( + context.epoch_details.epoch_num, user2.id, 0, allocation + ) + + expected_results = [] + for a in allocation: + expected_results.append( + ProposalDonationDTO(user1.address, a.amount, a.proposal_address) + ) + expected_results.append( + ProposalDonationDTO(user2.address, a.amount, a.proposal_address) + ) + + result = service.get_all_allocations(context) + + assert len(result) == 4 + for i in result: + assert i in expected_results + + +def test_get_all_allocations_does_not_include_revoked_allocations_in_returned_list( + service, + context, + mock_users_db, + proposal_accounts, +): + user1, user2, _ = mock_users_db + allocation = [ + AllocationDTO(proposal_accounts[0].address, 100), + AllocationDTO(proposal_accounts[1].address, 200), + ] + database.allocations.add_all( + context.epoch_details.epoch_num, user1.id, 0, allocation + ) + database.allocations.add_all( + context.epoch_details.epoch_num, user2.id, 0, allocation + ) + + expected_results = [] + for a in allocation: + expected_results.append( + ProposalDonationDTO(user2.address, a.amount, a.proposal_address) + ) + + database.allocations.soft_delete_all_by_epoch_and_user_id( + context.epoch_details.epoch_num, user1.id + ) + + result = service.get_all_allocations(context) + + assert len(result) == 2 + for i in result: + assert i in expected_results + + +def test_get_all_allocations_does_not_return_allocations_from_previous_and_future_epochs( + service, + context, + mock_users_db, + proposal_accounts, +): + user1, _, _ = mock_users_db + allocation = [ + AllocationDTO(proposal_accounts[0].address, 100), + AllocationDTO(proposal_accounts[1].address, 200), + ] + database.allocations.add_all( + context.epoch_details.epoch_num - 1, user1.id, 0, allocation + ) + database.allocations.add_all( + context.epoch_details.epoch_num + 1, user1.id, 1, allocation + ) + + assert service.get_all_allocations(context) == [] + + +def test_get_all_with_allocation_amount_equal_0( + service, + context, + mock_users_db, + proposal_accounts, +): + user1, _, _ = mock_users_db + allocation = [ + AllocationDTO(proposal_accounts[0].address, 0), + ] + database.allocations.add_all( + context.epoch_details.epoch_num, user1.id, 0, allocation + ) + + expected_result = [ + ProposalDonationDTO(user1.address, 0, proposal_accounts[0].address) + ] + assert service.get_all_allocations(context) == expected_result From 041143e0abad6bd0f330dc1c71d2755498be7fff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 12 Mar 2024 21:19:08 +0100 Subject: [PATCH 037/107] feature: rework user last allocation within epoch --- backend/app/context/epoch_state.py | 2 + .../infrastructure/database/allocations.py | 60 +++-- .../app/infrastructure/routes/allocations.py | 2 +- backend/app/legacy/controllers/allocations.py | 19 +- backend/app/modules/dto.py | 2 + .../app/modules/modules_factory/protocols.py | 8 +- .../modules/user/allocations/controller.py | 14 +- .../modules/user/allocations/service/saved.py | 21 +- backend/tests/conftest.py | 4 +- .../allocations/test_saved_allocations.py | 236 ++++++++++-------- 10 files changed, 213 insertions(+), 155 deletions(-) diff --git a/backend/app/context/epoch_state.py b/backend/app/context/epoch_state.py index 5d5133253e..9ebd823de0 100644 --- a/backend/app/context/epoch_state.py +++ b/backend/app/context/epoch_state.py @@ -84,8 +84,10 @@ def validate_epoch_state(epoch_num: int, epoch_state: EpochState): if not (has_pending_snapshot and has_finalized_snapshot): raise InvalidEpoch() + def _has_pending_epoch_snapshot(epoch_num: int): return database.pending_epoch_snapshot.get_by_epoch(epoch_num) is not None + def _has_finalized_epoch_snapshot(epoch_num: int): return database.finalized_epoch_snapshot.get_by_epoch(epoch_num) is not None diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 0ff5e7cde8..184859c63d 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -10,7 +10,12 @@ from app.extensions import db from app.infrastructure.database.models import Allocation, User, AllocationRequest from app.infrastructure.database.user import get_by_address -from app.modules.dto import AllocationDTO, AccountFundsDTO +from app.modules.dto import ( + AllocationItem, + AllocationDTO, + AccountFundsDTO, + UserAllocationRequestPayload, +) @deprecated("Use `get_all` function") @@ -23,13 +28,12 @@ def get_all_by_epoch(epoch: int, with_deleted=False) -> List[Allocation]: return query.all() -def get_all(epoch: int, with_deleted=False) -> List[AllocationDTO]: - query: Query = Allocation.query.filter_by(epoch=epoch) - - if not with_deleted: - query = query.filter(Allocation.deleted_at.is_(None)) - - allocations = query.all() +def get_all(epoch: int) -> List[AllocationDTO]: + allocations = ( + Allocation.query.filter_by(epoch=epoch) + .filter(Allocation.deleted_at.is_(None)) + .all() + ) return [ AllocationDTO( @@ -60,19 +64,23 @@ def get_user_allocations_history( def get_all_by_user_addr_and_epoch( - user_address: str, epoch: int, with_deleted=False -) -> List[Allocation]: - user: User = get_by_address(user_address) - - if user is None: - return [] - - query: Query = Allocation.query.filter_by(user_id=user.id, epoch=epoch) - - if not with_deleted: - query = query.filter(Allocation.deleted_at.is_(None)) + user_address: str, epoch: int +) -> List[AllocationItem]: + allocations: List[Allocation] = ( + Allocation.query.join(User, User.id == Allocation.user_id) + .filter(User.address == user_address) + .filter(Allocation.epoch == epoch) + .filter(Allocation.deleted_at.is_(None)) + .all() + ) - return query.all() + return [ + AllocationItem( + proposal_address=alloc.proposal_address, + amount=int(alloc.amount), + ) + for alloc in allocations + ] def get_all_by_proposal_addr_and_epoch( @@ -208,11 +216,13 @@ def soft_delete_all_by_epoch_and_user_id(epoch: int, user_id: int): def get_allocation_request_by_user_and_epoch( user_address: str, epoch: int ) -> AllocationRequest | None: - user: User = get_by_address(user_address) - if user is None: - raise UserNotFound(user_address) - - return AllocationRequest.query.filter_by(user_id=user.id, epoch=epoch).first() + return ( + AllocationRequest.query.join(User, User.id == AllocationRequest.user_id) + .filter(User.address == user_address) + .filter(AllocationRequest.epoch == epoch) + .order_by(AllocationRequest.nonce.desc()) + .first() + ) def get_user_last_allocation_request(user_address: str) -> AllocationRequest | None: diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 908eda3c6e..ca090792a0 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -271,7 +271,7 @@ def get(self, user_address: str, epoch: int): ( allocs, is_manually_edited, - ) = allocations.get_last_request_by_user_and_epoch(user_address, epoch) + ) = controller.get_last_user_allocation(user_address, epoch) user_allocations = [dataclasses.asdict(w) for w in allocs] app.logger.debug( diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index a98f97aa12..51ea07b515 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -51,6 +51,7 @@ def allocate( return user_address +@deprecated("ALLOCATIONS REWORK") def get_all_by_user_and_epoch( user_address: str, epoch: int | None = None ) -> List[AccountFunds]: @@ -58,24 +59,6 @@ def get_all_by_user_and_epoch( return [AccountFunds(a.proposal_address, a.amount) for a in allocations] -def get_last_request_by_user_and_epoch( - user_address: str, epoch: int | None = None -) -> (List[AccountFunds], Optional[bool]): - allocations = _get_user_allocations_for_epoch(user_address, epoch) - - is_manually_edited = None - if len(allocations) != 0: - allocation_nonce = allocations[0].nonce - alloc_request = database.allocations.get_allocation_request_by_user_nonce( - user_address, allocation_nonce - ) - is_manually_edited = alloc_request.is_manually_edited - - return [ - AccountFunds(a.proposal_address, a.amount) for a in allocations - ], is_manually_edited - - def get_all_by_proposal_and_epoch( proposal_address: str, epoch: int = None ) -> List[AccountFunds]: diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 367825fd8f..5091560eb8 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -68,12 +68,14 @@ class PendingSnapshotDTO(JSONWizard): class AllocationDTO(AllocationItem, JSONWizard): user_address: Optional[str] = None + @dataclass(frozen=True) class ProposalDonationDTO(JSONWizard): donor: str amount: int proposal: str + @dataclass(frozen=True) class ProposalDonationDTO(JSONWizard): donor: str diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index e1ef29cf0a..eaa3bcedc0 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -1,10 +1,11 @@ -from typing import Protocol, List, Dict, Tuple, runtime_checkable +from typing import Protocol, List, Dict, Tuple, Optional, runtime_checkable from app.context.manager import Context from app.engine.projects.rewards import ProjectRewardDTO, ProjectRewardsResult from app.engine.user.effective_deposit import UserDeposit from app.modules.dto import ( OctantRewardsDTO, + AccountFundsDTO, AllocationDTO, FinalizedSnapshotDTO, PendingSnapshotDTO, @@ -56,6 +57,11 @@ class GetUserAllocationsProtocol(Protocol): def get_all_allocations(self, context: Context) -> List[AllocationDTO]: ... + def get_last_user_allocation( + self, context: Context, user_address: str + ) -> Tuple[List[AccountFundsDTO], Optional[bool]]: + ... + @runtime_checkable class SimulateAllocation(Protocol): diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index a023bf16af..36d46d6fce 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -1,9 +1,9 @@ -from typing import List, Tuple, Dict +from typing import List, Tuple, Dict, Optional from app.context.epoch_state import EpochState from app.context.manager import epoch_context, state_context from app.exceptions import NotImplementedForGivenEpochState -from app.modules.dto import AllocationDTO, ProposalDonationDTO +from app.modules.dto import AccountFundsDTO, AllocationDTO, ProposalDonationDTO from app.modules.registry import get_services from app.modules.user.allocations.service.pending import PendingUserAllocations from app.modules.user.allocations.service.history import UserAllocationsHistory @@ -24,6 +24,16 @@ def get_all_allocations(epoch_num: int) -> List[ProposalDonationDTO]: return service.get_all_allocations(context) +def get_last_user_allocation( + user_address: str, epoch_num: int +) -> Tuple[List[AccountFundsDTO], Optional[bool]]: + context = epoch_context(epoch_num) + if context.epoch_state > EpochState.PENDING: + raise NotImplementedForGivenEpochState() + service = get_services(context.epoch_state).user_allocations_service + return service.get_last_user_allocation(context, user_address) + + def get_donors(epoch_num: int) -> List[str]: context = epoch_context(epoch_num) if context.epoch_state > EpochState.PENDING: diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index 79371b4bce..3338b9027d 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -1,11 +1,12 @@ -from typing import List +from typing import List, Tuple, Optional from app.context.manager import Context from app.infrastructure import database from app.modules.common.time import Timestamp, from_datetime -from app.modules.dto import AccountFundsDTO, AllocationItem +from app.modules.dto import AllocationItem, AccountFundsDTO, ProposalDonationDTO from app.pydantic import Model + class SavedUserAllocations(Model): def get_all_donors_addresses(self, context: Context) -> List[str]: return database.allocations.get_users_with_allocations( @@ -59,5 +60,17 @@ def get_all_allocations(self, context: Context) -> List[ProposalDonationDTO]: def get_last_user_allocation( self, context: Context, user_address: str - ) -> Tuple[List[AllocationDTO], bool]: - pass + ) -> Tuple[List[AllocationItem], Optional[bool]]: + epoch_num = context.epoch_details.epoch_num + last_request = database.allocations.get_allocation_request_by_user_and_epoch( + user_address, epoch_num + ) + + if not last_request: + return [], None + + allocations = database.allocations.get_all_by_user_addr_and_epoch( + user_address, epoch_num + ) + + return allocations, last_request.is_manually_edited diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index a04a5a8172..659bce25f6 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -495,13 +495,13 @@ def patch_has_pending_epoch_snapshot(monkeypatch): ( monkeypatch.setattr( "app.legacy.core.allocations.has_pending_epoch_snapshot", - MOCK_HAS_PENDING_SNAPSHOT + MOCK_HAS_PENDING_SNAPSHOT, ) ) ( monkeypatch.setattr( "app.context.epoch_state._has_pending_epoch_snapshot", - MOCK_HAS_PENDING_SNAPSHOT + MOCK_HAS_PENDING_SNAPSHOT, ) ) MOCK_HAS_PENDING_SNAPSHOT.return_value = True diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index 41c4cac701..8c18d95eb1 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -4,7 +4,12 @@ from app.extensions import db from app.infrastructure import database from app.modules.common.time import from_timestamp_s -from app.modules.dto import AllocationDTO, AllocationItem, ProposalDonationDTO +from app.modules.dto import ( + AllocationItem, + ProposalDonationDTO, + UserAllocationRequestPayload, + UserAllocationPayload, +) from app.modules.user.allocations.controller import revoke_previous_allocation from app.modules.user.allocations.service.saved import SavedUserAllocations @@ -21,21 +26,48 @@ def service(): return SavedUserAllocations() -def test_get_all_donors_addresses(service, mock_users_db, proposal_accounts): - user1, user2, user3 = mock_users_db +@pytest.fixture() +def make_user_allocation(proposal_accounts): + def _make_user_allocation(context, user, allocations=1, nonce=0, **kwargs): + database.allocations.soft_delete_all_by_epoch_and_user_id( + context.epoch_details.epoch_num, user.id + ) - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - ] + allocation_items = [ + AllocationItem(proposal_accounts[i].address, (i + 1) * 100) + for i in range(allocations) + ] - database.allocations.add_all(1, user1.id, 0, allocation) - database.allocations.add_all(1, user2.id, 0, allocation) - database.allocations.add_all(2, user3.id, 0, allocation) - db.session.commit() + if kwargs.get("allocation_items"): + allocation_items = kwargs.get("allocation_items") + + request = UserAllocationRequestPayload( + payload=UserAllocationPayload(allocations=allocation_items, nonce=nonce), + signature="0xdeadbeef", + ) + + database.allocations.store_allocation_request( + user.address, context.epoch_details.epoch_num, request, **kwargs + ) + return allocation_items + + return _make_user_allocation + + +def _alloc_item_to_donation(item, user): + return ProposalDonationDTO(user.address, item.amount, item.proposal_address) + + +def test_get_all_donors_addresses(service, mock_users_db, make_user_allocation): + user1, user2, user3 = mock_users_db context_epoch_1 = get_context(1) context_epoch_2 = get_context(2) + make_user_allocation(context_epoch_1, user1) + make_user_allocation(context_epoch_1, user2) + make_user_allocation(context_epoch_2, user3) + result_epoch_1 = service.get_all_donors_addresses(context_epoch_1) result_epoch_2 = service.get_all_donors_addresses(context_epoch_2) @@ -43,45 +75,36 @@ def test_get_all_donors_addresses(service, mock_users_db, proposal_accounts): assert result_epoch_2 == [user3.address] -def test_return_only_not_removed_allocations(service, mock_users_db, proposal_accounts): +def test_return_only_not_removed_allocations( + service, mock_users_db, make_user_allocation +): user1, user2, _ = mock_users_db - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - ] - - database.allocations.add_all(1, user1.id, 0, allocation) - database.allocations.add_all(1, user2.id, 0, allocation) - database.allocations.soft_delete_all_by_epoch_and_user_id(1, user2.id) - db.session.commit() - context = get_context(1) + make_user_allocation(context, user1) + make_user_allocation(context, user2) + database.allocations.soft_delete_all_by_epoch_and_user_id(1, user2.id) result = service.get_all_donors_addresses(context) assert result == [user1.address] -def test_get_user_allocation_sum(service, context, mock_users_db, proposal_accounts): +def test_get_user_allocation_sum(service, context, mock_users_db, make_user_allocation): user1, user2, _ = mock_users_db - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - AllocationDTO(proposal_accounts[1].address, 200), - ] - database.allocations.add_all(1, user1.id, 0, allocation) - database.allocations.add_all(1, user2.id, 0, allocation) - db.session.commit() + make_user_allocation(context, user1, allocations=2) + make_user_allocation(context, user2, allocations=2) result = service.get_user_allocation_sum(context, user1.address) assert result == 300 -def test_has_user_allocated_rewards(service, context, mock_users_db, proposal_accounts): +def test_has_user_allocated_rewards( + service, context, mock_users_db, make_user_allocation +): user1, _, _ = mock_users_db - database.allocations.add_allocation_request(user1.address, 1, 0, "0x00", False) - - db.session.commit() + make_user_allocation(context, user1) result = service.has_user_allocated_rewards(context, user1.address) @@ -89,11 +112,13 @@ def test_has_user_allocated_rewards(service, context, mock_users_db, proposal_ac def test_has_user_allocated_rewards_returns_false( - service, context, mock_users_db, proposal_accounts + service, context, mock_users_db, make_user_allocation ): - user1, _, _ = mock_users_db + user1, user2, _ = mock_users_db - result = service.has_user_allocated_rewards(context, user1.address) + make_user_allocation(context, user1) # other user makes an allocation + + result = service.has_user_allocated_rewards(context, user2.address) assert result is False @@ -155,28 +180,15 @@ def test_get_all_allocations_returns_empty_list_when_no_allocations( def test_get_all_allocations_returns_list_of_allocations( - service, context, mock_users_db, proposal_accounts + service, context, mock_users_db, make_user_allocation ): user1, user2, _ = mock_users_db - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - AllocationDTO(proposal_accounts[1].address, 200), - ] - database.allocations.add_all( - context.epoch_details.epoch_num, user1.id, 0, allocation - ) - database.allocations.add_all( - context.epoch_details.epoch_num, user2.id, 0, allocation - ) - expected_results = [] - for a in allocation: - expected_results.append( - ProposalDonationDTO(user1.address, a.amount, a.proposal_address) - ) - expected_results.append( - ProposalDonationDTO(user2.address, a.amount, a.proposal_address) - ) + user1_allocations = make_user_allocation(context, user1, allocations=2) + user2_allocations = make_user_allocation(context, user2, allocations=2) + user1_donations = [_alloc_item_to_donation(a, user1) for a in user1_allocations] + user2_donations = [_alloc_item_to_donation(a, user2) for a in user2_allocations] + expected_results = user1_donations + user2_donations result = service.get_all_allocations(context) @@ -186,33 +198,18 @@ def test_get_all_allocations_returns_list_of_allocations( def test_get_all_allocations_does_not_include_revoked_allocations_in_returned_list( - service, - context, - mock_users_db, - proposal_accounts, + service, context, mock_users_db, make_user_allocation ): user1, user2, _ = mock_users_db - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - AllocationDTO(proposal_accounts[1].address, 200), - ] - database.allocations.add_all( - context.epoch_details.epoch_num, user1.id, 0, allocation - ) - database.allocations.add_all( - context.epoch_details.epoch_num, user2.id, 0, allocation - ) - - expected_results = [] - for a in allocation: - expected_results.append( - ProposalDonationDTO(user2.address, a.amount, a.proposal_address) - ) + make_user_allocation(context, user1, allocations=2) database.allocations.soft_delete_all_by_epoch_and_user_id( context.epoch_details.epoch_num, user1.id ) + user2_allocations = make_user_allocation(context, user2, allocations=2) + expected_results = [_alloc_item_to_donation(a, user2) for a in user2_allocations] + result = service.get_all_allocations(context) assert len(result) == 2 @@ -221,41 +218,76 @@ def test_get_all_allocations_does_not_include_revoked_allocations_in_returned_li def test_get_all_allocations_does_not_return_allocations_from_previous_and_future_epochs( - service, - context, - mock_users_db, - proposal_accounts, + service, context, mock_users_db, make_user_allocation ): user1, _, _ = mock_users_db - allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - AllocationDTO(proposal_accounts[1].address, 200), - ] - database.allocations.add_all( - context.epoch_details.epoch_num - 1, user1.id, 0, allocation - ) - database.allocations.add_all( - context.epoch_details.epoch_num + 1, user1.id, 1, allocation - ) + context_epoch_1 = get_context(1) + context_epoch_2 = get_context(2) + context_epoch_3 = get_context(3) - assert service.get_all_allocations(context) == [] + make_user_allocation(context_epoch_1, user1) + make_user_allocation(context_epoch_3, user1, nonce=1) + + assert service.get_all_allocations(context_epoch_2) == [] def test_get_all_with_allocation_amount_equal_0( - service, - context, - mock_users_db, - proposal_accounts, + service, context, mock_users_db, proposal_accounts, make_user_allocation ): user1, _, _ = mock_users_db - allocation = [ - AllocationDTO(proposal_accounts[0].address, 0), - ] - database.allocations.add_all( - context.epoch_details.epoch_num, user1.id, 0, allocation - ) + allocation_items = [AllocationItem(proposal_accounts[0].address, 0)] + make_user_allocation(context, user1, allocation_items=allocation_items) + expected_result = [_alloc_item_to_donation(a, user1) for a in allocation_items] - expected_result = [ - ProposalDonationDTO(user1.address, 0, proposal_accounts[0].address) - ] assert service.get_all_allocations(context) == expected_result + + +def test_get_last_user_allocation_when_no_allocation( + service, context, alice, make_user_allocation +): + assert service.get_last_user_allocation(context, alice.address) == ([], None) + + +def test_get_last_user_allocation_returns_the_only_allocation( + service, context, mock_users_db, make_user_allocation +): + user1, _, _ = mock_users_db + expected_result = make_user_allocation(context, user1) + + assert service.get_last_user_allocation(context, user1.address) == ( + expected_result, + None, + ) + + +def test_get_last_user_allocation_returns_the_only_the_last_allocation( + service, context, mock_users_db, make_user_allocation +): + user1, _, _ = mock_users_db + _ = make_user_allocation(context, user1) + expected_result = make_user_allocation(context, user1, allocations=10, nonce=1) + + assert service.get_last_user_allocation(context, user1.address) == ( + expected_result, + None, + ) + + +def test_get_last_user_allocation_returns_stored_metadata( + service, context, mock_users_db, make_user_allocation +): + user1, _, _ = mock_users_db + + expected_result = make_user_allocation(context, user1, is_manually_edited=False) + assert service.get_last_user_allocation(context, user1.address) == ( + expected_result, + False, + ) + + expected_result = make_user_allocation( + context, user1, nonce=1, is_manually_edited=True + ) + assert service.get_last_user_allocation(context, user1.address) == ( + expected_result, + True, + ) From 06193cb96c24f8e16166ea229799fc853ba5cf9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 12 Mar 2024 21:43:34 +0100 Subject: [PATCH 038/107] chore: remove epoch alloctions sum endpoint --- backend/app/infrastructure/events.py | 2 -- .../app/infrastructure/routes/allocations.py | 15 ------------- backend/app/legacy/controllers/allocations.py | 21 ------------------- .../app/modules/modules_factory/pending.py | 7 ++++++- backend/tests/legacy/test_allocations.py | 6 ------ 5 files changed, 6 insertions(+), 45 deletions(-) diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index 5bbe10efcf..bfd638c4aa 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -49,8 +49,6 @@ def handle_allocate(msg): threshold = get_allocation_threshold() emit("threshold", {"threshold": str(threshold)}, broadcast=True) - allocations_sum = allocations.get_sum_by_epoch() - emit("allocations_sum", {"amount": str(allocations_sum)}, broadcast=True) project_rewards = get_estimated_project_rewards().rewards emit( diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index ca090792a0..256d097952 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -284,21 +284,6 @@ def get(self, user_address: str, epoch: int): } -@ns.route("/users/sum") -@ns.doc( - description="Returns user's allocations sum", -) -class UserAllocationsSum(OctantResource): - @ns.marshal_with(user_allocations_sum_model) - @ns.response(200, "User allocations sum successfully retrieved") - def get(self): - app.logger.debug("Getting users allocations sum") - allocations_sum = allocations.get_sum_by_epoch() - app.logger.debug(f"Users allocations sum: {allocations_sum}") - - return {"amount": str(allocations_sum)} - - @ns.route("/proposal//epoch/") @ns.doc( description="Returns list of donors for given proposal in particular epoch", diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index 51ea07b515..d67b22993b 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -74,27 +74,6 @@ def get_all_by_proposal_and_epoch( ] -@deprecated("ALLOCATIONS REWORK") -def get_all_by_epoch( - epoch: int, include_zeroes: bool = False -) -> List[EpochAllocationRecord]: - if epoch > epoch_snapshots.get_last_pending_snapshot(): - raise exceptions.EpochAllocationPeriodNotStartedYet(epoch) - - allocations = database.allocations.get_all_by_epoch(epoch) - - return [ - EpochAllocationRecord(a.user.address, a.amount, a.proposal_address) - for a in allocations - if int(a.amount) != 0 or include_zeroes - ] - - -def get_sum_by_epoch(epoch: int | None = None) -> int: - epoch = epochs.get_pending_epoch() if epoch is None else epoch - return database.allocations.get_alloc_sum_by_epoch(epoch) - - @deprecated("ALLOCATIONS REWORK") def revoke_previous_user_allocation(user_address: str): pending_epoch = epochs.get_pending_epoch() diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index ca99479184..18d44d0ab8 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -13,6 +13,7 @@ EstimatedProjectRewardsService, OctantRewards, DonorsAddresses, + AllocationManipulationProtocol, GetUserAllocationsProtocol, ) from app.modules.octant_rewards.service.pending import PendingOctantRewards @@ -38,7 +39,11 @@ class PendingUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protoco class PendingUserAllocationsProtocol( - DonorsAddresses, GetUserAllocationsProtocol, SimulateAllocation, Protocol + DonorsAddresses, + AllocationManipulationProtocol, + GetUserAllocationsProtocol, + SimulateAllocation, + Protocol, ): pass diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py index 646ed09e90..ff7112b8f8 100644 --- a/backend/tests/legacy/test_allocations.py +++ b/backend/tests/legacy/test_allocations.py @@ -8,7 +8,6 @@ from app.legacy.controllers.allocations import ( get_all_by_user_and_epoch, get_all_by_proposal_and_epoch, - get_sum_by_epoch, allocate, ) from app.legacy.core.allocations import ( @@ -382,11 +381,6 @@ def test_get_by_proposal_and_epoch_with_allocation_amount_equal_0( assert result[1].amount == str(1050 * 10**18) -def test_get_sum_by_epoch(mock_allocations_db, user_accounts, proposal_accounts): - result = get_sum_by_epoch(MOCKED_PENDING_EPOCH_NO) - assert result == 1865 * 10**18 - - def test_user_exceeded_rewards_budget_in_allocations(app, proposal_accounts, tos_users): # Set some reasonable user rewards budget MOCK_GET_USER_BUDGET.return_value = 100 * 10**18 From 20788df39a01bad6c5ac3e642a99b6c367b6ad35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 12 Mar 2024 22:22:01 +0100 Subject: [PATCH 039/107] feature: rework revoke allocation --- backend/app/legacy/controllers/allocations.py | 11 ------ backend/app/legacy/controllers/user.py | 4 +- backend/app/legacy/core/allocations.py | 2 + .../app/modules/modules_factory/protocols.py | 6 +++ .../modules/user/allocations/controller.py | 20 +++++++++- .../user/allocations/service/pending.py | 14 +++++++ backend/tests/conftest.py | 8 ++++ backend/tests/legacy/test_user.py | 8 ++-- .../allocations/test_pending_allocations.py | 39 +++++++++++++++++-- 9 files changed, 92 insertions(+), 20 deletions(-) diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index d67b22993b..88350780ad 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -15,7 +15,6 @@ deserialize_payload, verify_allocations, add_allocations_to_db, - revoke_previous_allocation, store_allocation_request, ) from app.legacy.core.common import AccountFunds @@ -74,16 +73,6 @@ def get_all_by_proposal_and_epoch( ] -@deprecated("ALLOCATIONS REWORK") -def revoke_previous_user_allocation(user_address: str): - pending_epoch = epochs.get_pending_epoch() - - if pending_epoch is None: - raise exceptions.NotInDecisionWindow - - revoke_previous_allocation(user_address, pending_epoch) - - def _make_allocation( payload: Dict, user_address: str, diff --git a/backend/app/legacy/controllers/user.py b/backend/app/legacy/controllers/user.py index 26195b8766..afc08ee2d7 100644 --- a/backend/app/legacy/controllers/user.py +++ b/backend/app/legacy/controllers/user.py @@ -2,7 +2,7 @@ from app.exceptions import InvalidSignature, UserNotFound, NotInDecisionWindow from app.extensions import db -from app.legacy.controllers import allocations as allocations_controller +from app.modules.user.allocations import controller as allocations_controller from app.legacy.core.user import patron_mode as patron_mode_core from app.legacy.core.user.tos import ( has_user_agreed_to_terms_of_service, @@ -37,7 +37,7 @@ def toggle_patron_mode(user_address: str, signature: str) -> bool: patron_mode_status = patron_mode_core.toggle_patron_mode(user_address) try: - allocations_controller.revoke_previous_user_allocation(user_address) + allocations_controller.revoke_previous_allocation(user_address) except NotInDecisionWindow: app.logger.info( f"Not in allocation period. Skipped revoking previous allocation for user {user_address}" diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py index 60bee2fb45..8ce8ec9dcc 100644 --- a/backend/app/legacy/core/allocations.py +++ b/backend/app/legacy/core/allocations.py @@ -1,3 +1,4 @@ +from typing_extensions import deprecated from dataclasses import dataclass from typing import List, Dict, Tuple, Optional @@ -115,6 +116,7 @@ def verify_allocations( raise exceptions.RewardsBudgetExceeded +@deprecated("ALLOCATIONS REWORK") def revoke_previous_allocation(user_address: str, epoch: int): user = database.user.get_by_address(user_address) if user is None: diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index eaa3bcedc0..2f9dfe6241 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -63,6 +63,12 @@ def get_last_user_allocation( ... +@runtime_checkable +class AllocationManipulationProtocol(Protocol): + def revoke_previous_allocation(self, context: Context, user_address: str): + ... + + @runtime_checkable class SimulateAllocation(Protocol): def simulate_allocation( diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index 36d46d6fce..dff7ab9503 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -2,7 +2,11 @@ from app.context.epoch_state import EpochState from app.context.manager import epoch_context, state_context -from app.exceptions import NotImplementedForGivenEpochState +from app.exceptions import ( + NotImplementedForGivenEpochState, + InvalidEpoch, + NotInDecisionWindow, +) from app.modules.dto import AccountFundsDTO, AllocationDTO, ProposalDonationDTO from app.modules.registry import get_services from app.modules.user.allocations.service.pending import PendingUserAllocations @@ -59,6 +63,20 @@ def simulate_allocation( return leverage, threshold, matched +def revoke_previous_allocation(user_address: str): + context = None + + try: + context = state_context(EpochState.PENDING) + except InvalidEpoch: + raise NotInDecisionWindow + + service: PendingUserAllocations = get_services( + context.epoch_state + ).user_allocations_service + service.revoke_previous_allocation(context, user_address) + + def _deserialize_payload(payload: Dict) -> List[AllocationDTO]: return [ AllocationDTO.from_dict(allocation_data) diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 6089734753..684b31db21 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -1,6 +1,8 @@ from typing import List, Tuple, Protocol, runtime_checkable +from app import exceptions from app.context.manager import Context +from app.context.epoch_state import EpochState from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database from app.modules.dto import AllocationDTO @@ -38,3 +40,15 @@ def simulate_allocation( projects, matched_rewards, ) + + def revoke_previous_allocation(self, context: Context, user_address: str): + if context.epoch_state is not EpochState.PENDING: + raise exceptions.NotInDecisionWindow + + user = database.user.get_by_address(user_address) + if user is None: + raise exceptions.UserNotFound + + database.allocations.soft_delete_all_by_epoch_and_user_id( + context.epoch_details.epoch_num, user.id + ) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 659bce25f6..971164a82d 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -665,6 +665,14 @@ def mock_staking_proceeds(): return staking_proceeds_service_mock +@pytest.fixture(scope="function") +def mock_user_nonce(): + user_nonce_service_mock = Mock() + user_nonce_service_mock.get_next_user_nonce.return_value = 0 + + return user_nonce_service_mock + + @pytest.fixture(scope="function") def mock_events_generator(): events = [ diff --git a/backend/tests/legacy/test_user.py b/backend/tests/legacy/test_user.py index da8482fc22..d5981c22d7 100644 --- a/backend/tests/legacy/test_user.py +++ b/backend/tests/legacy/test_user.py @@ -97,7 +97,9 @@ def test_patron_mode_toggle_fails_when_use_sig_to_disable_for_enable(user_accoun toggle_patron_mode(user_accounts[0].address, toggle_true_sig) -def test_patron_mode_revokes_allocations_for_the_epoch(alice, make_allocations): +def test_patron_mode_revokes_allocations_for_the_epoch( + alice, make_allocations, mock_pending_epoch_snapshot_db +): toggle_true_sig = "52d249ca8ac8f40c01613635dac8a9b01eb50230ad1467451a058170726650b92223e80032a4bff4d25c3554e9d1347043c53b4c2dc9f1ba3f071bd3a1c8b9121b" make_allocations(alice, MOCKED_PENDING_EPOCH_NO) assert len(allocations_controller.get_all_by_user_and_epoch(alice.address)) == 3 @@ -111,7 +113,7 @@ def test_patron_mode_revokes_allocations_for_the_epoch(alice, make_allocations): def test_when_patron_mode_changes_revoked_allocations_are_not_restored( - alice, make_allocations + alice, make_allocations, mock_pending_epoch_snapshot_db ): toggle_true_sig = "52d249ca8ac8f40c01613635dac8a9b01eb50230ad1467451a058170726650b92223e80032a4bff4d25c3554e9d1347043c53b4c2dc9f1ba3f071bd3a1c8b9121b" toggle_false_sig = "979b997cb2b990f104ed4d342a364207a019649eda00497780033d154ee07c44141a6be33cecdde879b1b4238c1622660e70baddb745def53d6733e4aacaeb181b" @@ -127,7 +129,7 @@ def test_when_patron_mode_changes_revoked_allocations_are_not_restored( def test_patron_mode_does_not_revoke_allocations_from_previous_epochs( - alice, make_allocations + alice, make_allocations, mock_pending_epoch_snapshot_db ): toggle_true_sig = "52d249ca8ac8f40c01613635dac8a9b01eb50230ad1467451a058170726650b92223e80032a4bff4d25c3554e9d1347043c53b4c2dc9f1ba3f071bd3a1c8b9121b" make_allocations(alice, MOCKED_PENDING_EPOCH_NO - 1) diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index 4bc7d8e30c..eb248c696b 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -1,8 +1,11 @@ import pytest +from app import exceptions from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database +from app.context.epoch_state import EpochState from app.modules.dto import AllocationDTO +from app.modules.user.allocations.service.history import UserAllocationsHistory from app.modules.user.allocations.service.pending import PendingUserAllocations from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context @@ -13,7 +16,16 @@ def before(app): pass -def test_simulate_allocation(mock_users_db, mock_octant_rewards): +@pytest.fixture() +def service(mock_octant_rewards, mock_patron_mode, mock_user_budgets): + return PendingUserAllocations( + octant_rewards=mock_octant_rewards, + user_budgets=mock_user_budgets, + patrons_mode=mock_patron_mode, + user_nonce=UserAllocationsHistory(), + ) + +def test_simulate_allocation(service, mock_users_db): user1, _, _ = mock_users_db context = get_context() projects = context.projects_details.projects @@ -26,8 +38,6 @@ def test_simulate_allocation(mock_users_db, mock_octant_rewards): AllocationDTO(projects[1], 200_000000000), ] - service = PendingUserAllocations(octant_rewards=mock_octant_rewards) - leverage, threshold, rewards = service.simulate_allocation( context, next_allocations, user1.address ) @@ -47,3 +57,26 @@ def test_simulate_allocation(mock_users_db, mock_octant_rewards): ProjectRewardDTO(sorted_projects[8], 0, 0), ProjectRewardDTO(sorted_projects[9], 0, 0), ] + + +def test_revoke_previous_allocation(service, mock_users_db): + user1, _, _ = mock_users_db + context = get_context(epoch_state=EpochState.PENDING) + + projects = context.projects_details.projects + prev_allocation = [ + AllocationDTO(projects[0], 100_000000000), + ] + database.allocations.add_all(1, user1.id, 0, prev_allocation) + + assert service.get_user_allocation_sum(context, user1.address) == 100_000000000 + service.revoke_previous_allocation(context, user1.address) + assert service.get_user_allocation_sum(context, user1.address) == 0 + +def test_revoke_previous_allocation_fails_outside_decision_window(service, mock_users_db): + user1, _, _ = mock_users_db + + for state in [EpochState.FUTURE, EpochState.CURRENT, EpochState.PRE_PENDING, EpochState.FINALIZING, EpochState.FINALIZED]: + context = get_context(epoch_state=state) + with pytest.raises(exceptions.NotInDecisionWindow): + service.revoke_previous_allocation(context, user1.address) From 6f592206a29efadfea7ec9233662c48446104865 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 13 Mar 2024 13:17:02 +0100 Subject: [PATCH 040/107] feature: rework allocate --- .../infrastructure/database/allocations.py | 35 ++++++++ backend/app/legacy/controllers/allocations.py | 50 +---------- backend/app/legacy/core/allocations.py | 22 +---- backend/app/modules/dto.py | 13 ++- .../app/modules/modules_factory/pending.py | 8 +- .../app/modules/modules_factory/protocols.py | 6 ++ .../modules/user/allocations/controller.py | 38 +++++++- backend/app/modules/user/allocations/core.py | 88 ++++++++++++++++++- .../user/allocations/service/pending.py | 44 +++++++++- backend/tests/conftest.py | 15 +++- .../allocations/test_saved_allocations.py | 41 ++++++++- 11 files changed, 277 insertions(+), 83 deletions(-) diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 184859c63d..0c958669f0 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -153,6 +153,40 @@ def get_user_alloc_sum_by_epoch(epoch: int, user_address: str) -> int: return sum([int(a.amount) for a in allocations]) +def store_allocation_request( + user_address: int, epoch_num: int, request: UserAllocationRequestPayload, **kwargs +): + now = datetime.utcnow() + + user: User = get_by_address(user_address) + + options = {"is_manually_edited": None, **kwargs} + + new_allocations = [ + Allocation( + epoch=epoch_num, + user_id=user.id, + nonce=request.payload.nonce, + proposal_address=to_checksum_address(a.proposal_address), + amount=str(a.amount), + created_at=now, + ) + for a in request.payload.allocations + ] + + allocation_request = AllocationRequest( + user_id=user.id, + epoch=epoch_num, + nonce=request.payload.nonce, + signature=request.signature, + is_manually_edited=options["is_manually_edited"], + ) + + db.session.add(allocation_request) + db.session.add_all(new_allocations) + + +@deprecated("Alloc rework") def add_all(epoch: int, user_id: int, nonce: int, allocations): now = datetime.utcnow() @@ -170,6 +204,7 @@ def add_all(epoch: int, user_id: int, nonce: int, allocations): db.session.add_all(new_allocations) +@deprecated("Alloc rework") def add_allocation_request( user_address: str, epoch: int, diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index 88350780ad..429c8b8771 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -1,21 +1,16 @@ from typing_extensions import deprecated from dataclasses import dataclass -from typing import List, Dict +from typing import List from typing import Optional from dataclass_wizard import JSONWizard from app import exceptions -from app.extensions import db, epochs +from app.extensions import epochs from app.infrastructure import database from app.modules.user.allocations import controller as new_controller from app.legacy.core.allocations import ( AllocationRequest, - recover_user_address, - deserialize_payload, - verify_allocations, - add_allocations_to_db, - store_allocation_request, ) from app.legacy.core.common import AccountFunds from app.legacy.core.epochs import epoch_snapshots @@ -28,26 +23,11 @@ class EpochAllocationRecord(JSONWizard): proposal: str +@deprecated("ALLOCATIONS REWORK") def allocate( request: AllocationRequest, is_manually_edited: Optional[bool] = None ) -> str: - user_address = recover_user_address(request) - user = database.user.get_by_address(user_address) - next_nonce = new_controller.get_user_next_nonce(user_address) - - _make_allocation( - request.payload, user_address, request.override_existing_allocations, next_nonce - ) - user.allocation_nonce = next_nonce - - pending_epoch = epochs.get_pending_epoch() - store_allocation_request( - pending_epoch, user_address, next_nonce, request.signature, is_manually_edited - ) - - db.session.commit() - - return user_address + return new_controller.allocate(request, is_manually_edited=is_manually_edited) @deprecated("ALLOCATIONS REWORK") @@ -73,28 +53,6 @@ def get_all_by_proposal_and_epoch( ] -def _make_allocation( - payload: Dict, - user_address: str, - delete_existing_user_epoch_allocations: bool, - expected_nonce: Optional[int] = None, -): - nonce, user_allocations = deserialize_payload(payload) - epoch = epochs.get_pending_epoch() - - verify_allocations(epoch, user_address, user_allocations) - - if expected_nonce is not None and nonce != expected_nonce: - raise exceptions.WrongAllocationsNonce(nonce, expected_nonce) - - add_allocations_to_db( - epoch, - user_address, - nonce, - user_allocations, - delete_existing_user_epoch_allocations, - ) - def _get_user_allocations_for_epoch(user_address: str, epoch: int | None = None): epoch = epochs.get_pending_epoch() if epoch is None else epoch diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py index 8ce8ec9dcc..bf689aa8cf 100644 --- a/backend/app/legacy/core/allocations.py +++ b/backend/app/legacy/core/allocations.py @@ -8,10 +8,10 @@ from app import exceptions from app.extensions import proposals from app.infrastructure import database +from app.infrastructure.database.models import User from app.legacy.core.epochs.epoch_snapshots import has_pending_epoch_snapshot from app.legacy.core.user.budget import get_budget from app.legacy.core.user.patron_mode import get_patron_mode_status -from app.legacy.crypto.eip712 import recover_address, build_allocations_eip712_data @dataclass(frozen=True) @@ -21,7 +21,7 @@ class Allocation(JSONWizard): @dataclass(frozen=True) -class AllocationRequest: +class AllocationRequest(JSONWizard): payload: Dict signature: str override_existing_allocations: bool @@ -44,23 +44,6 @@ def add_allocations_to_db( database.allocations.add_all(epoch, user.id, nonce, allocations) -def store_allocation_request( - epoch: int, - user_address: str, - nonce: int, - signature: str, - is_manually_edited: Optional[bool] = None, -): - database.allocations.add_allocation_request( - user_address, epoch, nonce, signature, is_manually_edited - ) - - -def recover_user_address(request: AllocationRequest) -> str: - eip712_data = build_allocations_eip712_data(request.payload) - return recover_address(eip712_data, request.signature) - - def deserialize_payload(payload) -> Tuple[int, List[Allocation]]: allocations = [ Allocation.from_dict(allocation_data) @@ -69,6 +52,7 @@ def deserialize_payload(payload) -> Tuple[int, List[Allocation]]: return payload["nonce"], allocations +@deprecated("ALLOCATIONS REWORK") def verify_allocations( epoch: Optional[int], user_address: str, allocations: List[Allocation] ): diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 5091560eb8..42f9a27875 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -70,10 +70,15 @@ class AllocationDTO(AllocationItem, JSONWizard): @dataclass(frozen=True) -class ProposalDonationDTO(JSONWizard): - donor: str - amount: int - proposal: str +class UserAllocationPayload(JSONWizard): + allocations: List[AllocationItem] + nonce: int + + +@dataclass(frozen=True) +class UserAllocationRequestPayload(JSONWizard): + payload: UserAllocationPayload + signature: str @dataclass(frozen=True) diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 18d44d0ab8..6a6bda7d1e 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -20,6 +20,7 @@ from app.modules.snapshots.finalized.service.simulated import ( SimulatedFinalizedSnapshots, ) +from app.modules.user.allocations.service.history import UserAllocationsHistory from app.modules.user.allocations.service.pending import PendingUserAllocations from app.modules.user.budgets.service.saved import SavedUserBudgets from app.modules.user.deposits.service.saved import SavedUserDeposits @@ -63,8 +64,13 @@ class PendingServices(Model): def create() -> "PendingServices": events_based_patron_mode = EventsBasedUserPatronMode() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) - saved_user_allocations = PendingUserAllocations(octant_rewards=octant_rewards) saved_user_budgets = SavedUserBudgets() + saved_user_allocations = PendingUserAllocations( + user_nonce=UserAllocationsHistory(), + user_budgets=saved_user_budgets, + patrons_mode=events_based_patron_mode, + octant_rewards=octant_rewards, + ) user_rewards = CalculatedUserRewards( user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index 2f9dfe6241..54a163d480 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -10,6 +10,7 @@ FinalizedSnapshotDTO, PendingSnapshotDTO, WithdrawableEth, + UserAllocationRequestPayload, ) from app.modules.history.dto import UserHistoryDTO @@ -65,6 +66,11 @@ def get_last_user_allocation( @runtime_checkable class AllocationManipulationProtocol(Protocol): + def allocate( + self, context: Context, payload: UserAllocationRequestPayload, **kwargs + ): + ... + def revoke_previous_allocation(self, context: Context, user_address: str): ... diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index dff7ab9503..76b944f695 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -7,7 +7,13 @@ InvalidEpoch, NotInDecisionWindow, ) -from app.modules.dto import AccountFundsDTO, AllocationDTO, ProposalDonationDTO +from app.modules.dto import ( + AccountFundsDTO, + ProposalDonationDTO, + UserAllocationRequestPayload, + UserAllocationPayload, + AllocationItem, +) from app.modules.registry import get_services from app.modules.user.allocations.service.pending import PendingUserAllocations from app.modules.user.allocations.service.history import UserAllocationsHistory @@ -46,6 +52,16 @@ def get_donors(epoch_num: int) -> List[str]: return service.get_all_donors_addresses(context) +def allocate(payload: Dict, **kwargs): + context = state_context(EpochState.PENDING) + service: PendingUserAllocations = get_services( + context.epoch_state + ).user_allocations_service + + allocation_request = _deserialize_payload(payload) + service.allocate(context, allocation_request, **kwargs) + + def simulate_allocation( payload: Dict, user_address: str ) -> Tuple[float, int, List[Dict[str, int]]]: @@ -53,7 +69,7 @@ def simulate_allocation( service: PendingUserAllocations = get_services( context.epoch_state ).user_allocations_service - user_allocations = _deserialize_payload(payload) + user_allocations = _deserialize_items(payload) leverage, threshold, projects_rewards = service.simulate_allocation( context, user_allocations, user_address ) @@ -77,8 +93,22 @@ def revoke_previous_allocation(user_address: str): service.revoke_previous_allocation(context, user_address) -def _deserialize_payload(payload: Dict) -> List[AllocationDTO]: +def _deserialize_payload(payload: Dict) -> UserAllocationRequestPayload: + allocation_items = _deserialize_items(payload.payload) + nonce = int(payload.payload["nonce"]) + signature = payload.signature + + return UserAllocationRequestPayload( + payload=UserAllocationPayload(allocation_items, nonce), signature=signature + ) + + +def _deserialize_items(payload: Dict) -> List[AllocationItem]: + print(payload["allocations"]) return [ - AllocationDTO.from_dict(allocation_data) + AllocationItem( + proposal_address=allocation_data["proposalAddress"], + amount=int(allocation_data["amount"]), + ) for allocation_data in payload["allocations"] ] diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index 990258758a..206a9f97fc 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -1,10 +1,16 @@ from typing import List, Optional +from app import exceptions + +from app.context.manager import Context +from app.context.epoch_state import EpochState from app.engine.projects import ProjectSettings from app.infrastructure.database.models import AllocationRequest from app.modules.common.leverage import calculate_leverage from app.modules.common.project_rewards import get_projects_rewards -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationDTO, UserAllocationRequestPayload, AllocationItem + +from app.legacy.crypto.eip712 import build_allocations_eip712_data, recover_address def next_allocation_nonce(prev_allocation_request: Optional[AllocationRequest]) -> int: @@ -39,6 +45,86 @@ def simulate_allocation( ) +def recover_user_address(request: UserAllocationRequestPayload) -> str: + eip712_data = build_allocations_eip712_data(request.payload) + return recover_address(eip712_data, request.signature) + + +def verify_user_allocation_request( + context: Context, + request: UserAllocationRequestPayload, + user_address: str, + expected_nonce: int, + user_budget: int, + patrons: List[str], +): + _verify_epoch_state(context.epoch_state) + _verify_nonce(request.payload.nonce, expected_nonce) + _verify_user_not_a_patron(user_address, patrons) + _verify_allocations_not_empty(request.payload.allocations) + _verify_no_invalid_proposals( + request.payload.allocations, valid_proposals=context.projects_details.projects + ) + _verify_no_duplicates(request.payload.allocations) + _verify_no_self_allocation(request.payload.allocations, user_address) + _verify_allocations_within_budget(request.payload.allocations, user_budget) + + +def _verify_epoch_state(epoch_state: EpochState): + if epoch_state is EpochState.PRE_PENDING: + raise exceptions.MissingSnapshot + + if epoch_state is not EpochState.PENDING: + raise exceptions.NotInDecisionWindow + + +def _verify_nonce(nonce, expected_nonce): + # if expected_nonce is not None and request.payload.nonce != expected_nonce: + if nonce != expected_nonce: + raise exceptions.WrongAllocationsNonce(nonce, expected_nonce) + + +def _verify_user_not_a_patron(user_address: str, patrons: List[str]): + if user_address in patrons: + raise exceptions.NotAllowedInPatronMode(user_address) + + +def _verify_allocations_not_empty(allocations: List[AllocationItem]): + if len(allocations) == 0: + raise exceptions.EmptyAllocations() + + +def _verify_no_invalid_proposals( + allocations: List[AllocationItem], valid_proposals: List[str] +): + proposal_addresses = [a.proposal_address for a in allocations] + invalid_proposals = list(set(proposal_addresses) - set(valid_proposals)) + + if invalid_proposals: + raise exceptions.InvalidProposals(invalid_proposals) + + +def _verify_no_duplicates(allocations: List[AllocationItem]): + proposal_addresses = [allocation.proposal_address for allocation in allocations] + [proposal_addresses.remove(p) for p in set(proposal_addresses)] + + if proposal_addresses: + raise exceptions.DuplicatedProposals(proposal_addresses) + + +def _verify_no_self_allocation(allocations: List[AllocationItem], user_address: str): + for allocation in allocations: + if allocation.proposal_address == user_address: + raise exceptions.ProposalAllocateToItself + + +def _verify_allocations_within_budget(allocations: List[AllocationItem], budget: int): + proposals_sum = sum([a.amount for a in allocations]) + + if proposals_sum > budget: + raise exceptions.RewardsBudgetExceeded + + def _replace_user_allocation( all_allocations_before: List[AllocationDTO], user_allocations: List[AllocationDTO], diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 684b31db21..e29827a0da 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -1,11 +1,14 @@ from typing import List, Tuple, Protocol, runtime_checkable +from app.pydantic import Model from app import exceptions +from app.extensions import db from app.context.manager import Context from app.context.epoch_state import EpochState from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationDTO, UserAllocationRequestPayload +from app.modules.modules_factory.protocols import UserPatronMode from app.modules.user.allocations import core from app.modules.user.allocations.service.saved import SavedUserAllocations @@ -16,8 +19,45 @@ def get_matched_rewards(self, context: Context) -> int: ... -class PendingUserAllocations(SavedUserAllocations): +@runtime_checkable +class UserBudgetProtocol(Protocol): + def get_budget(self, context: Context, user_address: str) -> int: + ... + + +@runtime_checkable +class UserNonceProtocol(Protocol): + def get_next_user_nonce(self, user_address: str) -> int: + ... + + +class PendingUserAllocations(SavedUserAllocations, Model): octant_rewards: OctantRewards + user_budgets: UserBudgetProtocol + patrons_mode: UserPatronMode + user_nonce: UserNonceProtocol + + def allocate( + self, context: Context, payload: UserAllocationRequestPayload, **kwargs + ) -> str: + user_address = core.recover_user_address(payload) + + expected_nonce = self.user_nonce.get_user_next_nonce(user_address) + user_budget = self.user_budgets.get_budget(context, user_address) + patrons = self.patrons_mode.get_all_patrons_addresses(context) + + core.verify_user_allocation_request( + context, payload, expected_nonce, user_budget, patrons + ) + + self.revoke_previous_allocation(context, user_address) + db.allocations.store_allocation_request( + user_address, context.epoch_details.epoch_num, payload, **kwargs + ) + + db.session.commit() + + return user_address def simulate_allocation( self, diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 971164a82d..060ec3f27c 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -4,6 +4,7 @@ import os import time import urllib.request +from typing import List from random import randint from unittest.mock import MagicMock, Mock @@ -22,11 +23,11 @@ from app.infrastructure.contracts.erc20 import ERC20 from app.infrastructure.contracts.proposals import Proposals from app.infrastructure.contracts.vault import Vault -from app.legacy.controllers.allocations import allocate, deserialize_payload +from app.legacy.controllers.allocations import allocate from app.legacy.core.allocations import Allocation, AllocationRequest from app.legacy.crypto.account import Account as CryptoAccount from app.legacy.crypto.eip712 import build_allocations_eip712_data, sign -from app.modules.dto import AccountFundsDTO +from app.modules.dto import AccountFundsDTO, AllocationItem from app.settings import DevConfig, TestConfig from tests.helpers.constants import ( ALICE, @@ -786,8 +787,14 @@ def create_payload(proposals, amounts: list[int] | None, nonce: int = 0): return {"allocations": allocations, "nonce": nonce} -def deserialize_allocations(payload) -> list[Allocation]: - return deserialize_payload(payload)[1] +def deserialize_allocations(payload) -> List[Allocation]: + return [ + AllocationItem( + proposal_address=allocation_data["proposal_data"], + amount=int(allocation_data["amount"]), + ) + for allocation_data in payload["allocations"] + ] def _split_deposit_events(deposit_events): diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index 8c18d95eb1..4857d0b796 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -1,7 +1,6 @@ import pytest from freezegun import freeze_time -from app.extensions import db from app.infrastructure import database from app.modules.common.time import from_timestamp_s from app.modules.dto import ( @@ -10,7 +9,6 @@ UserAllocationRequestPayload, UserAllocationPayload, ) -from app.modules.user.allocations.controller import revoke_previous_allocation from app.modules.user.allocations.service.saved import SavedUserAllocations from tests.helpers.context import get_context @@ -291,3 +289,42 @@ def test_get_last_user_allocation_returns_stored_metadata( expected_result, True, ) + + +def test_get_all_allocations_by_proposal_returns_empty_list_when_no_allocations( + service, context +): + for project in context.projects_details.projects: + assert service.get_all_allocations_by_proposal(context, project) == [] + + +def test_get_all_allocations_by_proposal_returns_list_of_donations_per_project( + service, context, mock_users_db, make_user_allocation +): + user1, user2, _ = mock_users_db + project1, project2 = ( + context.projects_details.projects[0], + context.projects_details.projects[1], + ) + + user1_allocations = make_user_allocation(context, user1, allocations=2) + user2_allocations = make_user_allocation(context, user2, allocations=2) + user1_donations = [_alloc_item_to_donation(a, user1) for a in user1_allocations] + user2_donations = [_alloc_item_to_donation(a, user2) for a in user2_allocations] + expected_results = user1_donations + user2_donations + + result = service.get_all_allocations_by_proposal(context, project1) + assert len(result) == 2 + for d in result: + assert d in list(filter(lambda d: d.proposal == project1, expected_results)) + + result = service.get_all_allocations_by_proposal(context, project2) + assert len(result) == 2 + for d in result: + assert d in list(filter(lambda d: d.proposal == project2, expected_results)) + + assert result + + # other projects have no donations + for project in context.projects_details.projects[2:]: + assert service.get_all_allocations_by_proposal(context, project) == [] From 0d44f0e66adcdc04446cafa091e060561f1b5842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 19 Mar 2024 23:16:55 +0100 Subject: [PATCH 041/107] feature: rework get allocations by proposal --- .../infrastructure/database/allocations.py | 6 ++-- backend/app/infrastructure/events.py | 17 ++++----- .../app/infrastructure/routes/allocations.py | 4 +-- backend/app/legacy/controllers/allocations.py | 31 +++------------- backend/app/legacy/core/common.py | 15 -------- .../app/modules/modules_factory/protocols.py | 6 ++++ .../modules/user/allocations/controller.py | 14 ++++++++ .../modules/user/allocations/service/saved.py | 16 +++++++++ backend/tests/legacy/test_allocations.py | 36 ------------------- .../allocations/test_pending_allocations.py | 16 +++++++-- .../allocations/test_saved_allocations.py | 26 ++++++++++---- 11 files changed, 87 insertions(+), 100 deletions(-) delete mode 100644 backend/app/legacy/core/common.py diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 0c958669f0..c43cdc08eb 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -83,11 +83,11 @@ def get_all_by_user_addr_and_epoch( ] -def get_all_by_proposal_addr_and_epoch( - proposal_address: str, epoch: int, with_deleted=False +def get_all_by_project_addr_and_epoch( + project_address: str, epoch: int, with_deleted=False ) -> List[Allocation]: query: Query = Allocation.query.filter_by( - proposal_address=to_checksum_address(proposal_address), epoch=epoch + proposal_address=to_checksum_address(project_address), epoch=epoch ) if not with_deleted: diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index bfd638c4aa..a25fc5b3c0 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -8,13 +8,14 @@ from app.exceptions import OctantException from app.extensions import socketio, epochs from app.infrastructure.exception_handler import UNEXPECTED_EXCEPTION, ExceptionHandler -from app.legacy.controllers import allocations +from app.modules.dto import ProposalDonationDTO +from app.modules.user.allocations import controller + from app.legacy.controllers.allocations import allocate from app.legacy.controllers.rewards import ( get_allocation_threshold, ) from app.legacy.core.allocations import AllocationRequest -from app.legacy.core.common import AccountFunds from app.modules.project_rewards.controller import get_estimated_project_rewards @@ -56,18 +57,18 @@ def handle_allocate(msg): _serialize_project_rewards(project_rewards), broadcast=True, ) - for proposal in project_rewards: - donors = allocations.get_all_by_proposal_and_epoch(proposal.address) + for project in project_rewards: + donors = controller.get_all_donations_by_project(project.address) emit( "proposal_donors", - {"proposal": proposal.address, "donors": _serialize_donors(donors)}, + {"proposal": project.address, "donors": _serialize_donors(donors)}, broadcast=True, ) @socketio.on("proposal_donors") def handle_proposal_donors(proposal_address: str): - donors = allocations.get_all_by_proposal_and_epoch(proposal_address) + donors = controller.get_all_donations_by_project(proposal_address) emit( "proposal_donors", {"proposal": proposal_address, "donors": _serialize_donors(donors)}, @@ -94,10 +95,10 @@ def _serialize_project_rewards(project_rewards: List[ProjectRewardDTO]) -> List[ ] -def _serialize_donors(donors: List[AccountFunds]) -> List[dict]: +def _serialize_donors(donors: List[ProposalDonationDTO]) -> List[dict]: return [ { - "address": donor.address, + "address": donor.donor, "amount": str(donor.amount), } for donor in donors diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 256d097952..1a805bd29a 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -300,8 +300,8 @@ def get(self, proposal_address: str, epoch: int): f"Getting donors for proposal {proposal_address} in epoch {epoch}" ) donors = [ - dataclasses.asdict(w) - for w in allocations.get_all_by_proposal_and_epoch(proposal_address, epoch) + {"address": w.donor, "amount": str(w.amount)} + for w in controller.get_all_donations_by_project(proposal_address, epoch) ] app.logger.debug(f"Proposal donors {donors}") diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index 429c8b8771..6214983d05 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -5,15 +5,12 @@ from dataclass_wizard import JSONWizard -from app import exceptions from app.extensions import epochs from app.infrastructure import database from app.modules.user.allocations import controller as new_controller from app.legacy.core.allocations import ( AllocationRequest, ) -from app.legacy.core.common import AccountFunds -from app.legacy.core.epochs import epoch_snapshots @dataclass(frozen=True) @@ -31,29 +28,9 @@ def allocate( @deprecated("ALLOCATIONS REWORK") -def get_all_by_user_and_epoch( - user_address: str, epoch: int | None = None -) -> List[AccountFunds]: - allocations = _get_user_allocations_for_epoch(user_address, epoch) - return [AccountFunds(a.proposal_address, a.amount) for a in allocations] - - -def get_all_by_proposal_and_epoch( - proposal_address: str, epoch: int = None -) -> List[AccountFunds]: +def get_all_by_user_and_epoch(user_address: str, epoch: int | None = None): epoch = epochs.get_pending_epoch() if epoch is None else epoch - - allocations = database.allocations.get_all_by_proposal_addr_and_epoch( - proposal_address, epoch + allocations = database.allocations.get_all_by_user_addr_and_epoch( + user_address, epoch ) - return [ - AccountFunds(a.user.address, a.amount) - for a in allocations - if int(a.amount) != 0 - ] - - - -def _get_user_allocations_for_epoch(user_address: str, epoch: int | None = None): - epoch = epochs.get_pending_epoch() if epoch is None else epoch - return database.allocations.get_all_by_user_addr_and_epoch(user_address, epoch) + return [(a.proposal_address, a.amount) for a in allocations] diff --git a/backend/app/legacy/core/common.py b/backend/app/legacy/core/common.py deleted file mode 100644 index f726a48128..0000000000 --- a/backend/app/legacy/core/common.py +++ /dev/null @@ -1,15 +0,0 @@ -from dataclasses import dataclass - -from dataclass_wizard import JSONWizard - - -@dataclass(frozen=True) -class AccountFunds(JSONWizard): - address: str - amount: int - matched: int = None - - def __iter__(self): - yield self.address - yield self.amount - yield self.matched diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index 54a163d480..b0c525b72d 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -7,6 +7,7 @@ OctantRewardsDTO, AccountFundsDTO, AllocationDTO, + ProposalDonationDTO, FinalizedSnapshotDTO, PendingSnapshotDTO, WithdrawableEth, @@ -58,6 +59,11 @@ class GetUserAllocationsProtocol(Protocol): def get_all_allocations(self, context: Context) -> List[AllocationDTO]: ... + def get_allocations_by_project( + self, context: Context, project: str + ) -> List[ProposalDonationDTO]: + ... + def get_last_user_allocation( self, context: Context, user_address: str ) -> Tuple[List[AccountFundsDTO], Optional[bool]]: diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index 76b944f695..98b29320c3 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -34,6 +34,20 @@ def get_all_allocations(epoch_num: int) -> List[ProposalDonationDTO]: return service.get_all_allocations(context) +def get_all_donations_by_project( + project_address: str, epoch_num: Optional[int] = None +) -> List[ProposalDonationDTO]: + context = ( + state_context(EpochState.PENDING) + if epoch_num is None + else epoch_context(epoch_num) + ) + if context.epoch_state > EpochState.PENDING: + raise NotImplementedForGivenEpochState() + service = get_services(context.epoch_state).user_allocations_service + return service.get_allocations_by_project(context, project_address) + + def get_last_user_allocation( user_address: str, epoch_num: int ) -> Tuple[List[AccountFundsDTO], Optional[bool]]: diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index 3338b9027d..14ca6a4be8 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -47,6 +47,7 @@ def get_user_allocations_by_timestamp( user_address, from_timestamp.datetime(), limit ) ] + def get_all_allocations(self, context: Context) -> List[ProposalDonationDTO]: allocations = database.allocations.get_all(context.epoch_details.epoch_num) return [ @@ -58,6 +59,21 @@ def get_all_allocations(self, context: Context) -> List[ProposalDonationDTO]: for alloc in allocations ] + def get_allocations_by_project( + self, context: Context, project_address: str + ) -> List[ProposalDonationDTO]: + allocations = database.allocations.get_all_by_proposal_addr_and_epoch( + project_address, context.epoch_details.epoch_num + ) + + return [ + ProposalDonationDTO( + donor=a.user.address, amount=int(a.amount), proposal=proposal_address + ) + for a in allocations + if int(a.amount) != 0 + ] + def get_last_user_allocation( self, context: Context, user_address: str ) -> Tuple[List[AllocationItem], Optional[bool]]: diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py index ff7112b8f8..019a2925b8 100644 --- a/backend/tests/legacy/test_allocations.py +++ b/backend/tests/legacy/test_allocations.py @@ -7,7 +7,6 @@ from app.infrastructure import database from app.legacy.controllers.allocations import ( get_all_by_user_and_epoch, - get_all_by_proposal_and_epoch, allocate, ) from app.legacy.core.allocations import ( @@ -346,41 +345,6 @@ def test_get_by_user_and_epoch(mock_allocations_db, user_accounts, proposal_acco assert result[2].amount == str(300 * 10**18) -def test_get_by_proposal_and_epoch( - mock_allocations_db, user_accounts, proposal_accounts -): - result = get_all_by_proposal_and_epoch( - proposal_accounts[1].address, MOCKED_PENDING_EPOCH_NO - ) - - assert len(result) == 2 - assert result[0].address == user_accounts[0].address - assert result[0].amount == str(5 * 10**18) - assert result[1].address == user_accounts[1].address - assert result[1].amount == str(1050 * 10**18) - - -def test_get_by_proposal_and_epoch_with_allocation_amount_equal_0( - mock_allocations_db, user_accounts, proposal_accounts -): - user = database.user.get_or_add_user(user_accounts[2].address) - db.session.commit() - user_allocations = [ - Allocation(proposal_accounts[1].address, 0), - ] - database.allocations.add_all(MOCKED_PENDING_EPOCH_NO, user.id, 0, user_allocations) - - result = get_all_by_proposal_and_epoch( - proposal_accounts[1].address, MOCKED_PENDING_EPOCH_NO - ) - - assert len(result) == 2 - assert result[0].address == user_accounts[0].address - assert result[0].amount == str(5 * 10**18) - assert result[1].address == user_accounts[1].address - assert result[1].amount == str(1050 * 10**18) - - def test_user_exceeded_rewards_budget_in_allocations(app, proposal_accounts, tos_users): # Set some reasonable user rewards budget MOCK_GET_USER_BUDGET.return_value = 100 * 10**18 diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index eb248c696b..f24a4e964f 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -25,6 +25,7 @@ def service(mock_octant_rewards, mock_patron_mode, mock_user_budgets): user_nonce=UserAllocationsHistory(), ) + def test_simulate_allocation(service, mock_users_db): user1, _, _ = mock_users_db context = get_context() @@ -68,15 +69,24 @@ def test_revoke_previous_allocation(service, mock_users_db): AllocationDTO(projects[0], 100_000000000), ] database.allocations.add_all(1, user1.id, 0, prev_allocation) - + assert service.get_user_allocation_sum(context, user1.address) == 100_000000000 service.revoke_previous_allocation(context, user1.address) assert service.get_user_allocation_sum(context, user1.address) == 0 -def test_revoke_previous_allocation_fails_outside_decision_window(service, mock_users_db): + +def test_revoke_previous_allocation_fails_outside_decision_window( + service, mock_users_db +): user1, _, _ = mock_users_db - for state in [EpochState.FUTURE, EpochState.CURRENT, EpochState.PRE_PENDING, EpochState.FINALIZING, EpochState.FINALIZED]: + for state in [ + EpochState.FUTURE, + EpochState.CURRENT, + EpochState.PRE_PENDING, + EpochState.FINALIZING, + EpochState.FINALIZED, + ]: context = get_context(epoch_state=state) with pytest.raises(exceptions.NotInDecisionWindow): service.revoke_previous_allocation(context, user1.address) diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index 4857d0b796..d0646e24f1 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -169,6 +169,8 @@ def test_user_allocations_by_timestamp(context, mock_users_db, proposal_accounts timestamp=from_timestamp_s(1710720000), ) ] + + def test_get_all_allocations_returns_empty_list_when_no_allocations( service, context, mock_users_db ): @@ -291,14 +293,14 @@ def test_get_last_user_allocation_returns_stored_metadata( ) -def test_get_all_allocations_by_proposal_returns_empty_list_when_no_allocations( +def test_get_all_allocations_by_project_returns_empty_list_when_no_allocations( service, context ): for project in context.projects_details.projects: - assert service.get_all_allocations_by_proposal(context, project) == [] + assert service.get_all_allocations_by_project(context, project) == [] -def test_get_all_allocations_by_proposal_returns_list_of_donations_per_project( +def test_get_all_allocations_by_project_returns_list_of_donations_per_project( service, context, mock_users_db, make_user_allocation ): user1, user2, _ = mock_users_db @@ -313,12 +315,12 @@ def test_get_all_allocations_by_proposal_returns_list_of_donations_per_project( user2_donations = [_alloc_item_to_donation(a, user2) for a in user2_allocations] expected_results = user1_donations + user2_donations - result = service.get_all_allocations_by_proposal(context, project1) + result = service.get_all_allocations_by_project(context, project1) assert len(result) == 2 for d in result: assert d in list(filter(lambda d: d.proposal == project1, expected_results)) - result = service.get_all_allocations_by_proposal(context, project2) + result = service.get_all_allocations_by_project(context, project2) assert len(result) == 2 for d in result: assert d in list(filter(lambda d: d.proposal == project2, expected_results)) @@ -327,4 +329,16 @@ def test_get_all_allocations_by_proposal_returns_list_of_donations_per_project( # other projects have no donations for project in context.projects_details.projects[2:]: - assert service.get_all_allocations_by_proposal(context, project) == [] + assert service.get_all_allocations_by_project(context, project) == [] + + +def test_get_all_allocations_by_project_with_allocation_amount_equal_0( + service, context, mock_users_db, make_user_allocation +): + user1, _, _ = mock_users_db + project1 = context.projects_details.projects[0] + + allocation_items = [AllocationItem(project1, 0)] + make_user_allocation(context, user1, allocation_items=allocation_items) + + assert service.get_all_allocations_by_project(context, project1) == [] From 8c476eb0e42c6e5cf7b34e6d57c01d21ade5791a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 20 Mar 2024 08:14:33 +0100 Subject: [PATCH 042/107] refactor: extract history dtos --- backend/app/modules/dto.py | 41 ------------------ backend/app/modules/history/core.py | 2 +- backend/app/modules/history/dto.py | 43 ++++++++++++++++++- backend/app/modules/history/service/full.py | 9 +++- .../modules/user/allocations/service/saved.py | 3 +- .../user/deposits/service/calculated.py | 2 +- backend/app/modules/user/patron_mode/core.py | 2 +- .../user/patron_mode/service/events_based.py | 2 +- .../modules/withdrawals/service/finalized.py | 3 +- .../modules/history/test_history_core.py | 2 +- .../modules/history/test_history_full.py | 2 +- .../allocations/test_saved_allocations.py | 20 ++++----- .../deposits/test_calculated_user_deposits.py | 2 +- .../patron_mode/test_event_based_patrons.py | 2 +- .../withdrawals/test_withdrawals_finalized.py | 3 +- 15 files changed, 73 insertions(+), 65 deletions(-) diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 42f9a27875..595ed14b7d 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -5,7 +5,6 @@ from dataclass_wizard import JSONWizard -from app.modules.common.time import Timestamp from app.engine.projects.rewards import AllocationItem from app.engine.user.effective_deposit import UserDeposit @@ -99,43 +98,3 @@ class WithdrawableEth: amount: int proof: list[str] status: WithdrawalStatus - - -class OpType(StrEnum): - LOCK = "lock" - UNLOCK = "unlock" - ALLOCATION = "allocation" - WITHDRAWAL = "withdrawal" - PATRON_MODE_DONATION = "patron_mode_donation" - - -@dataclass(frozen=True) -class LockItem: - type: OpType - amount: int - timestamp: Timestamp - transaction_hash: str - - -@dataclass(frozen=True) -class AllocationItem: - project_address: str - epoch: int - amount: int - timestamp: Timestamp - - -@dataclass(frozen=True) -class WithdrawalItem: - type: OpType - amount: int - address: str - timestamp: Timestamp - transaction_hash: str - - -@dataclass(frozen=True) -class PatronDonationItem: - timestamp: Timestamp - epoch: int - amount: int diff --git a/backend/app/modules/history/core.py b/backend/app/modules/history/core.py index 6295681055..b29aec704d 100644 --- a/backend/app/modules/history/core.py +++ b/backend/app/modules/history/core.py @@ -1,4 +1,4 @@ -from app.modules.dto import ( +from app.modules.history.dto import ( OpType, LockItem, AllocationItem, diff --git a/backend/app/modules/history/dto.py b/backend/app/modules/history/dto.py index 43574344c9..564f6933d4 100644 --- a/backend/app/modules/history/dto.py +++ b/backend/app/modules/history/dto.py @@ -1,10 +1,51 @@ from dataclasses import dataclass +from enum import StrEnum from typing import Optional from dataclass_wizard import JSONWizard from app.modules.common.pagination import PageRecord -from app.modules.dto import OpType +from app.modules.common.time import Timestamp + + +class OpType(StrEnum): + LOCK = "lock" + UNLOCK = "unlock" + ALLOCATION = "allocation" + WITHDRAWAL = "withdrawal" + PATRON_MODE_DONATION = "patron_mode_donation" + + +@dataclass(frozen=True) +class LockItem: + type: OpType + amount: int + timestamp: Timestamp + transaction_hash: str + + +@dataclass(frozen=True) +class AllocationItem: + project_address: str + epoch: int + amount: int + timestamp: Timestamp + + +@dataclass(frozen=True) +class WithdrawalItem: + type: OpType + amount: int + address: str + timestamp: Timestamp + transaction_hash: str + + +@dataclass(frozen=True) +class PatronDonationItem: + timestamp: Timestamp + epoch: int + amount: int @dataclass(frozen=True) diff --git a/backend/app/modules/history/service/full.py b/backend/app/modules/history/service/full.py index 52fe235cd0..827add8467 100644 --- a/backend/app/modules/history/service/full.py +++ b/backend/app/modules/history/service/full.py @@ -3,9 +3,14 @@ from app.context.manager import Context from app.modules.common.pagination import Cursor, Paginator from app.modules.common.time import Timestamp -from app.modules.dto import LockItem, AllocationItem, WithdrawalItem, PatronDonationItem from app.modules.history.core import sort_history_records -from app.modules.history.dto import UserHistoryDTO +from app.modules.history.dto import ( + LockItem, + AllocationItem, + WithdrawalItem, + PatronDonationItem, + UserHistoryDTO, +) from app.pydantic import Model diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index 14ca6a4be8..46fe1048b2 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -4,6 +4,7 @@ from app.infrastructure import database from app.modules.common.time import Timestamp, from_datetime from app.modules.dto import AllocationItem, AccountFundsDTO, ProposalDonationDTO +from app.modules.history.dto import AllocationItem as HistoryAllocationItem from app.pydantic import Model @@ -37,7 +38,7 @@ def get_user_allocations_by_timestamp( self, user_address: str, from_timestamp: Timestamp, limit: int ) -> List[AllocationItem]: return [ - AllocationItem( + HistoryAllocationItem( project_address=r.proposal_address, epoch=r.epoch, amount=int(r.amount), diff --git a/backend/app/modules/user/deposits/service/calculated.py b/backend/app/modules/user/deposits/service/calculated.py index a8355f003a..3fac44847a 100644 --- a/backend/app/modules/user/deposits/service/calculated.py +++ b/backend/app/modules/user/deposits/service/calculated.py @@ -5,7 +5,7 @@ from app.infrastructure.graphql import locks, unlocks from app.modules.common.effective_deposits import calculate_effective_deposits from app.modules.common.time import Timestamp, from_timestamp_s -from app.modules.dto import LockItem, OpType +from app.modules.history.dto import LockItem, OpType from app.pydantic import Model diff --git a/backend/app/modules/user/patron_mode/core.py b/backend/app/modules/user/patron_mode/core.py index 0eff8e0eec..1205da1bd3 100644 --- a/backend/app/modules/user/patron_mode/core.py +++ b/backend/app/modules/user/patron_mode/core.py @@ -1,6 +1,6 @@ from app.context.epoch_details import EpochDetails from app.modules.common.time import Timestamp -from app.modules.dto import PatronDonationItem +from app.modules.history.dto import PatronDonationItem def filter_and_reverse_epochs( diff --git a/backend/app/modules/user/patron_mode/service/events_based.py b/backend/app/modules/user/patron_mode/service/events_based.py index bcf2aa1d63..0909f02db9 100644 --- a/backend/app/modules/user/patron_mode/service/events_based.py +++ b/backend/app/modules/user/patron_mode/service/events_based.py @@ -5,7 +5,7 @@ from app.context.manager import Context from app.infrastructure import database from app.modules.common.time import Timestamp -from app.modules.dto import PatronDonationItem +from app.modules.history.dto import PatronDonationItem from app.modules.user.patron_mode.core import ( filter_and_reverse_epochs, create_patron_donation_item, diff --git a/backend/app/modules/withdrawals/service/finalized.py b/backend/app/modules/withdrawals/service/finalized.py index 9cc35bfade..b29b9e745a 100644 --- a/backend/app/modules/withdrawals/service/finalized.py +++ b/backend/app/modules/withdrawals/service/finalized.py @@ -7,7 +7,8 @@ from app.infrastructure.graphql.merkle_roots import get_all_vault_merkle_roots from app.modules.common.merkle_tree import get_rewards_merkle_tree_for_epoch from app.modules.common.time import Timestamp, from_timestamp_s -from app.modules.dto import WithdrawableEth, WithdrawalItem, OpType +from app.modules.dto import WithdrawableEth +from app.modules.history.dto import WithdrawalItem, OpType from app.modules.withdrawals.core import create_finalized_epoch_withdrawals from app.pydantic import Model diff --git a/backend/tests/modules/history/test_history_core.py b/backend/tests/modules/history/test_history_core.py index 8b66189c69..2f7240c8ba 100644 --- a/backend/tests/modules/history/test_history_core.py +++ b/backend/tests/modules/history/test_history_core.py @@ -1,7 +1,7 @@ import pytest from app.modules.common.time import from_timestamp_s -from app.modules.dto import ( +from app.modules.history.dto import ( LockItem, OpType, PatronDonationItem, diff --git a/backend/tests/modules/history/test_history_full.py b/backend/tests/modules/history/test_history_full.py index fc8ee698cb..fa8f55a518 100644 --- a/backend/tests/modules/history/test_history_full.py +++ b/backend/tests/modules/history/test_history_full.py @@ -1,7 +1,7 @@ import pytest from app.modules.common.time import from_timestamp_s -from app.modules.dto import ( +from app.modules.history.dto import ( LockItem, OpType, AllocationItem, diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index d0646e24f1..19274d4af6 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -10,6 +10,7 @@ UserAllocationPayload, ) from app.modules.user.allocations.service.saved import SavedUserAllocations +from app.modules.history.dto import AllocationItem as HistoryAllocationItem from tests.helpers.context import get_context @@ -122,19 +123,18 @@ def test_has_user_allocated_rewards_returns_false( @freeze_time("2024-03-18 00:00:00") -def test_user_allocations_by_timestamp(context, mock_users_db, proposal_accounts): +def test_user_allocations_by_timestamp( + service, context, mock_users_db, proposal_accounts, make_user_allocation +): user1, _, _ = mock_users_db timestamp_before = from_timestamp_s(1710719999) timestamp_after = from_timestamp_s(1710720001) allocation = [ - AllocationDTO(proposal_accounts[0].address, 100), - AllocationDTO(proposal_accounts[1].address, 100), + AllocationItem(proposal_accounts[0].address, 100), + AllocationItem(proposal_accounts[1].address, 100), ] - database.allocations.add_all(1, user1.id, 0, allocation) - db.session.commit() - - service = SavedUserAllocations() + make_user_allocation(context, user1, allocation_items=allocation) result_before = service.get_user_allocations_by_timestamp( user1.address, from_timestamp=timestamp_before, limit=20 @@ -148,13 +148,13 @@ def test_user_allocations_by_timestamp(context, mock_users_db, proposal_accounts assert result_before == [] assert result_after == [ - AllocationItem( + HistoryAllocationItem( project_address=proposal_accounts[0].address, epoch=1, amount=100, timestamp=from_timestamp_s(1710720000), ), - AllocationItem( + HistoryAllocationItem( project_address=proposal_accounts[1].address, epoch=1, amount=100, @@ -162,7 +162,7 @@ def test_user_allocations_by_timestamp(context, mock_users_db, proposal_accounts ), ] assert result_after_with_limit == [ - AllocationItem( + HistoryAllocationItem( project_address=proposal_accounts[0].address, epoch=1, amount=100, diff --git a/backend/tests/modules/user/deposits/test_calculated_user_deposits.py b/backend/tests/modules/user/deposits/test_calculated_user_deposits.py index a9bafc4e80..cc36b3e694 100644 --- a/backend/tests/modules/user/deposits/test_calculated_user_deposits.py +++ b/backend/tests/modules/user/deposits/test_calculated_user_deposits.py @@ -1,6 +1,6 @@ from app.engine.user.effective_deposit import UserDeposit from app.modules.common.time import from_timestamp_s -from app.modules.dto import LockItem, OpType +from app.modules.history.dto import LockItem, OpType from app.modules.user.deposits.service.calculated import CalculatedUserDeposits from tests.conftest import USER1_ADDRESS, mock_graphql from tests.helpers.context import get_context diff --git a/backend/tests/modules/user/patron_mode/test_event_based_patrons.py b/backend/tests/modules/user/patron_mode/test_event_based_patrons.py index f127cdc224..1cfd3cacaa 100644 --- a/backend/tests/modules/user/patron_mode/test_event_based_patrons.py +++ b/backend/tests/modules/user/patron_mode/test_event_based_patrons.py @@ -2,7 +2,7 @@ from app.infrastructure import database from app.modules.common.time import from_timestamp_s -from app.modules.dto import PatronDonationItem +from app.modules.history.dto import PatronDonationItem from app.modules.user.patron_mode.service.events_based import ( EventsBasedUserPatronMode, ) diff --git a/backend/tests/modules/withdrawals/test_withdrawals_finalized.py b/backend/tests/modules/withdrawals/test_withdrawals_finalized.py index d983ed4d0a..69e5cfd719 100644 --- a/backend/tests/modules/withdrawals/test_withdrawals_finalized.py +++ b/backend/tests/modules/withdrawals/test_withdrawals_finalized.py @@ -3,7 +3,8 @@ from app import db from app.infrastructure import database from app.modules.common.time import from_timestamp_s -from app.modules.dto import WithdrawableEth, WithdrawalStatus, WithdrawalItem, OpType +from app.modules.dto import WithdrawableEth, WithdrawalStatus +from app.modules.history.dto import WithdrawalItem, OpType from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from tests.conftest import mock_graphql From 9e9d0535cb2f827ebc8927b260b63b2c3cd18a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 20 Mar 2024 08:16:18 +0100 Subject: [PATCH 043/107] test: add allocation verification tests --- backend/app/exceptions.py | 4 +- backend/app/legacy/controllers/allocations.py | 7 - backend/app/legacy/core/allocations.py | 2 +- backend/app/modules/user/allocations/core.py | 22 ++- .../user/allocations/service/history.py | 3 +- .../modules/user/allocations/service/saved.py | 4 +- backend/tests/legacy/test_allocations.py | 113 ----------- .../modules/user/allocations/test_core.py | 182 ++++++++++++++++++ .../allocations/test_saved_allocations.py | 16 +- 9 files changed, 209 insertions(+), 144 deletions(-) create mode 100644 backend/tests/modules/user/allocations/test_core.py diff --git a/backend/app/exceptions.py b/backend/app/exceptions.py index de6de44331..c90c94e519 100644 --- a/backend/app/exceptions.py +++ b/backend/app/exceptions.py @@ -63,7 +63,7 @@ def __init__(self): super().__init__(self.description, self.code) -class InvalidProposals(OctantException): +class InvalidProjects(OctantException): code = 400 description = "The following proposals are not valid: {}" @@ -71,7 +71,7 @@ def __init__(self, proposals): super().__init__(self.description.format(proposals), self.code) -class ProposalAllocateToItself(OctantException): +class ProjectAllocationToSelf(OctantException): code = 400 description = "You cannot allocate funds to your own project." diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index 6214983d05..4be22d4ed5 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -13,13 +13,6 @@ ) -@dataclass(frozen=True) -class EpochAllocationRecord(JSONWizard): - donor: str - amount: int # in wei - proposal: str - - @deprecated("ALLOCATIONS REWORK") def allocate( request: AllocationRequest, is_manually_edited: Optional[bool] = None diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py index bf689aa8cf..37b0c5f774 100644 --- a/backend/app/legacy/core/allocations.py +++ b/backend/app/legacy/core/allocations.py @@ -79,7 +79,7 @@ def verify_allocations( invalid_proposals = list(set(proposal_addresses) - set(valid_proposals)) if invalid_proposals: - raise exceptions.InvalidProposals(invalid_proposals) + raise exceptions.InvalidProjects(invalid_proposals) # Check if any allocation address has been duplicated in the payload [proposal_addresses.remove(p) for p in set(proposal_addresses)] diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index 206a9f97fc..5bee67aca8 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -57,18 +57,20 @@ def verify_user_allocation_request( expected_nonce: int, user_budget: int, patrons: List[str], -): +) -> bool: _verify_epoch_state(context.epoch_state) _verify_nonce(request.payload.nonce, expected_nonce) _verify_user_not_a_patron(user_address, patrons) _verify_allocations_not_empty(request.payload.allocations) - _verify_no_invalid_proposals( - request.payload.allocations, valid_proposals=context.projects_details.projects + _verify_no_invalid_projects( + request.payload.allocations, valid_projects=context.projects_details.projects ) _verify_no_duplicates(request.payload.allocations) _verify_no_self_allocation(request.payload.allocations, user_address) _verify_allocations_within_budget(request.payload.allocations, user_budget) + return True + def _verify_epoch_state(epoch_state: EpochState): if epoch_state is EpochState.PRE_PENDING: @@ -94,14 +96,14 @@ def _verify_allocations_not_empty(allocations: List[AllocationItem]): raise exceptions.EmptyAllocations() -def _verify_no_invalid_proposals( - allocations: List[AllocationItem], valid_proposals: List[str] +def _verify_no_invalid_projects( + allocations: List[AllocationItem], valid_projects: List[str] ): - proposal_addresses = [a.proposal_address for a in allocations] - invalid_proposals = list(set(proposal_addresses) - set(valid_proposals)) + projects_addresses = [a.proposal_address for a in allocations] + invalid_projects = list(set(projects_addresses) - set(valid_projects)) - if invalid_proposals: - raise exceptions.InvalidProposals(invalid_proposals) + if invalid_projects: + raise exceptions.InvalidProjects(invalid_projects) def _verify_no_duplicates(allocations: List[AllocationItem]): @@ -115,7 +117,7 @@ def _verify_no_duplicates(allocations: List[AllocationItem]): def _verify_no_self_allocation(allocations: List[AllocationItem], user_address: str): for allocation in allocations: if allocation.proposal_address == user_address: - raise exceptions.ProposalAllocateToItself + raise exceptions.ProjectAllocationToSelf def _verify_allocations_within_budget(allocations: List[AllocationItem], budget: int): diff --git a/backend/app/modules/user/allocations/service/history.py b/backend/app/modules/user/allocations/service/history.py index 5345625beb..d0be42565c 100644 --- a/backend/app/modules/user/allocations/service/history.py +++ b/backend/app/modules/user/allocations/service/history.py @@ -1,6 +1,7 @@ from app.pydantic import Model from app.infrastructure import database +from app.modules.user.allocations import core class UserAllocationsHistory(Model): @@ -8,4 +9,4 @@ def get_next_user_nonce(self, user_address: str) -> int: allocation_request = database.allocations.get_user_last_allocation_request( user_address ) - return 0 if allocation_request is None else allocation_request.nonce + 1 + return core.next_allocation_nonce(allocation_request) diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index 46fe1048b2..bad673090b 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -63,13 +63,13 @@ def get_all_allocations(self, context: Context) -> List[ProposalDonationDTO]: def get_allocations_by_project( self, context: Context, project_address: str ) -> List[ProposalDonationDTO]: - allocations = database.allocations.get_all_by_proposal_addr_and_epoch( + allocations = database.allocations.get_all_by_project_addr_and_epoch( project_address, context.epoch_details.epoch_num ) return [ ProposalDonationDTO( - donor=a.user.address, amount=int(a.amount), proposal=proposal_address + donor=a.user.address, amount=int(a.amount), proposal=project_address ) for a in allocations if int(a.amount) != 0 diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py index 019a2925b8..5acef56648 100644 --- a/backend/tests/legacy/test_allocations.py +++ b/backend/tests/legacy/test_allocations.py @@ -38,37 +38,6 @@ def get_all_by_epoch(epoch, include_zeroes=False): return new_controller.get_all_allocations(epoch) -@pytest.fixture(scope="function") -def get_all_by_epoch_expected_result(user_accounts, proposal_accounts): - return [ - { - "donor": user_accounts[0].address, - "proposal": proposal_accounts[0].address, - "amount": str(10 * 10**18), - }, - { - "donor": user_accounts[0].address, - "proposal": proposal_accounts[1].address, - "amount": str(5 * 10**18), - }, - { - "donor": user_accounts[0].address, - "proposal": proposal_accounts[2].address, - "amount": str(300 * 10**18), - }, - { - "donor": user_accounts[1].address, - "proposal": proposal_accounts[1].address, - "amount": str(1050 * 10**18), - }, - { - "donor": user_accounts[1].address, - "proposal": proposal_accounts[3].address, - "amount": str(500 * 10**18), - }, - ] - - @pytest.fixture(autouse=True) def before( app, @@ -249,88 +218,6 @@ def test_multiple_users_change_their_allocations(tos_users, proposal_accounts): check_allocation_threshold(updated_payload1, updated_payload2) -def test_allocation_validation_errors(proposal_accounts, user_accounts, tos_users): - # Test data - payload = create_payload(proposal_accounts[0:3], None) - signature = sign(user_accounts[0], build_allocations_eip712_data(payload)) - - # Set invalid number of proposals on purpose (two proposals while three are needed) - MOCK_PROPOSALS.get_proposal_addresses.return_value = [ - p.address for p in proposal_accounts[0:2] - ] - - # Set invalid epoch on purpose (mimicking no pending epoch) - MOCK_EPOCHS.get_pending_epoch.return_value = None - - # Call allocate method, expect exception - with pytest.raises(exceptions.NotInDecisionWindow): - allocate( - AllocationRequest(payload, signature, override_existing_allocations=True) - ) - - # Fix pending epoch - MOCK_EPOCHS.get_pending_epoch.return_value = MOCKED_PENDING_EPOCH_NO - - # Call allocate method, expect invalid proposals - with pytest.raises(exceptions.InvalidProposals): - allocate( - AllocationRequest(payload, signature, override_existing_allocations=True) - ) - - # Fix missing proposals - MOCK_PROPOSALS.get_proposal_addresses.return_value = [ - p.address for p in proposal_accounts[0:3] - ] - - # Expect no validation errors at this point - allocate(AllocationRequest(payload, signature, override_existing_allocations=True)) - - -def test_project_allocates_funds_to_itself(proposal_accounts): - # Test data - database.user.get_or_add_user(proposal_accounts[0].address) - payload = create_payload(proposal_accounts[0:3], None) - signature = sign(proposal_accounts[0], build_allocations_eip712_data(payload)) - - with pytest.raises(exceptions.ProposalAllocateToItself): - allocate( - AllocationRequest(payload, signature, override_existing_allocations=True) - ) - - -def test_allocate_by_user_in_patron_mode(tos_users, proposal_accounts): - # Test data - initial_payload = create_payload(proposal_accounts[0:3], None, 0) - initial_signature = sign( - tos_users[0], build_allocations_eip712_data(initial_payload) - ) - toggle_patron_mode(tos_users[0].address) - - # Call allocate method - with pytest.raises(exceptions.NotAllowedInPatronMode): - allocate( - AllocationRequest( - initial_payload, initial_signature, override_existing_allocations=True - ) - ) - - -def test_allocate_empty_allocations_list_should_fail(tos_users, proposal_accounts): - # Test data - initial_payload = create_payload([], None) - initial_signature = sign( - tos_users[0], build_allocations_eip712_data(initial_payload) - ) - - # Call allocate method - with pytest.raises(exceptions.EmptyAllocations): - allocate( - AllocationRequest( - initial_payload, initial_signature, override_existing_allocations=True - ) - ) - - def test_get_by_user_and_epoch(mock_allocations_db, user_accounts, proposal_accounts): result = get_all_by_user_and_epoch( user_accounts[0].address, MOCKED_PENDING_EPOCH_NO diff --git a/backend/tests/modules/user/allocations/test_core.py b/backend/tests/modules/user/allocations/test_core.py new file mode 100644 index 0000000000..ee22fd7e55 --- /dev/null +++ b/backend/tests/modules/user/allocations/test_core.py @@ -0,0 +1,182 @@ +import pytest + +from app import exceptions +from app.context.epoch_state import EpochState +from app.modules.dto import ( + UserAllocationPayload, + UserAllocationRequestPayload, + AllocationItem, +) +from app.modules.user.allocations import core + +from tests.helpers.context import get_context + + +@pytest.fixture() +def context(projects): + return get_context(epoch_state=EpochState.PENDING, projects=projects[:4]) + + +def build_allocations(allocs): + return [ + AllocationItem(proposal_address=project, amount=amount) + for project, amount in allocs + ] + + +def build_request(user, allocations=None, nonce=0): + allocations = allocations if allocations else [] + + return UserAllocationRequestPayload( + payload=UserAllocationPayload(allocations, nonce=nonce), + signature="0xdeadbeef", # signature is implicitly checked at user_address recovery + ) + + +def test_allocation_fails_outside_allocation_window(alice): + request = build_request(alice) + + for state in [ + EpochState.FUTURE, + EpochState.CURRENT, + EpochState.FINALIZING, + EpochState.FINALIZED, + ]: + context = get_context(epoch_state=state) + with pytest.raises(exceptions.NotInDecisionWindow): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [] + ) + + context = get_context(epoch_state=EpochState.PRE_PENDING) + with pytest.raises(exceptions.MissingSnapshot): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [] + ) + + +def test_allocation_fails_for_invalid_nonce(alice, context): + with pytest.raises(exceptions.WrongAllocationsNonce): + request = build_request(alice, nonce=0) + core.verify_user_allocation_request( + context, request, alice.address, 1, 10**18, [] + ) + + with pytest.raises(exceptions.WrongAllocationsNonce): + request = build_request(alice, nonce=2) + core.verify_user_allocation_request( + context, request, alice.address, 1, 10**18, [] + ) + + with pytest.raises(exceptions.WrongAllocationsNonce): + request = build_request(alice, nonce=None) + core.verify_user_allocation_request( + context, request, alice.address, 1, 10**18, [] + ) + + +def test_allocation_fails_for_a_patron(alice, bob, context): + request = build_request(alice) + with pytest.raises(exceptions.NotAllowedInPatronMode): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address, alice.address] + ) + + +def test_allocation_fails_with_empty_payload(alice, bob, context): + request = build_request(alice, allocations=[]) + with pytest.raises(exceptions.EmptyAllocations): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + + +def test_allocation_fails_with_invalid_proposals(alice, bob, context, projects): + valid_projects = context.projects_details.projects + valid_allocations = [(p, 17 * 10**16) for p in valid_projects] + + allocations = build_allocations(valid_allocations + [(projects[4], 17 * 10**16)]) + request = build_request(alice, allocations) + + with pytest.raises(exceptions.InvalidProjects): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + + +def test_allocation_fails_with_invalid_proposals(alice, bob, context): + projects = context.projects_details.projects + allocations = build_allocations( + [(p, 17 * 10**16) for p in projects] + [(projects[1], 1)] + ) + request = build_request(alice, allocations) + + with pytest.raises(exceptions.DuplicatedProposals): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + + +def test_allocation_fails_with_self_allocation(alice, bob, context): + projects = context.projects_details.projects + + allocations = build_allocations([(p, 17 * 10**16) for p in projects]) + request = build_request(alice, allocations) + + with pytest.raises(exceptions.ProjectAllocationToSelf): + core.verify_user_allocation_request( + context, request, projects[1], 0, 10**18, [bob.address] + ) + + +def test_allocation_fails_with_allocation_exceeding_budget(alice, bob, context): + projects = context.projects_details.projects + + allocations = build_allocations( + [ + (projects[0], 25 * 10**16), + (projects[1], 25 * 10**16), + (projects[2], 25 * 10**16 + 1), + (projects[3], 25 * 10**16), + ] + ) + request = build_request(alice, allocations) + + with pytest.raises(exceptions.RewardsBudgetExceeded): + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + + +def test_allocation_does_not_fail_with_allocation_equal_to_budget(alice, bob, context): + projects = context.projects_details.projects + + allocations = build_allocations( + [ + (projects[0], 25 * 10**16), + (projects[1], 25 * 10**16), + (projects[2], 25 * 10**16), + (projects[3], 25 * 10**16), + ] + ) + request = build_request(alice, allocations) + + assert core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + + +def test_allocation_does_not_fail_with_allocation_below_budget(alice, bob, context): + projects = context.projects_details.projects + allocations = build_allocations( + [ + (projects[0], 25 * 10**16), + (projects[1], 25 * 10**16), + (projects[3], 25 * 10**16), + ] + ) + request = build_request(alice, allocations) + + assert core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index 19274d4af6..bbf5368028 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -293,14 +293,14 @@ def test_get_last_user_allocation_returns_stored_metadata( ) -def test_get_all_allocations_by_project_returns_empty_list_when_no_allocations( +def test_get_allocations_by_project_returns_empty_list_when_no_allocations( service, context ): for project in context.projects_details.projects: - assert service.get_all_allocations_by_project(context, project) == [] + assert service.get_allocations_by_project(context, project) == [] -def test_get_all_allocations_by_project_returns_list_of_donations_per_project( +def test_get_allocations_by_project_returns_list_of_donations_per_project( service, context, mock_users_db, make_user_allocation ): user1, user2, _ = mock_users_db @@ -315,12 +315,12 @@ def test_get_all_allocations_by_project_returns_list_of_donations_per_project( user2_donations = [_alloc_item_to_donation(a, user2) for a in user2_allocations] expected_results = user1_donations + user2_donations - result = service.get_all_allocations_by_project(context, project1) + result = service.get_allocations_by_project(context, project1) assert len(result) == 2 for d in result: assert d in list(filter(lambda d: d.proposal == project1, expected_results)) - result = service.get_all_allocations_by_project(context, project2) + result = service.get_allocations_by_project(context, project2) assert len(result) == 2 for d in result: assert d in list(filter(lambda d: d.proposal == project2, expected_results)) @@ -329,10 +329,10 @@ def test_get_all_allocations_by_project_returns_list_of_donations_per_project( # other projects have no donations for project in context.projects_details.projects[2:]: - assert service.get_all_allocations_by_project(context, project) == [] + assert service.get_allocations_by_project(context, project) == [] -def test_get_all_allocations_by_project_with_allocation_amount_equal_0( +def test_get_allocations_by_project_with_allocation_amount_equal_0( service, context, mock_users_db, make_user_allocation ): user1, _, _ = mock_users_db @@ -341,4 +341,4 @@ def test_get_all_allocations_by_project_with_allocation_amount_equal_0( allocation_items = [AllocationItem(project1, 0)] make_user_allocation(context, user1, allocation_items=allocation_items) - assert service.get_all_allocations_by_project(context, project1) == [] + assert service.get_allocations_by_project(context, project1) == [] From 4dbc1f24596110ea9cb5e92c5589caa6dc4c464c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 20 Mar 2024 10:17:05 +0100 Subject: [PATCH 044/107] chore: fix tests --- .../infrastructure/database/allocations.py | 1 - backend/app/legacy/controllers/allocations.py | 15 --- backend/app/legacy/core/allocations.py | 1 - backend/app/legacy/crypto/eip712.py | 11 ++ .../app/modules/modules_factory/current.py | 5 +- .../app/modules/modules_factory/pending.py | 2 - .../modules/user/allocations/controller.py | 8 +- backend/app/modules/user/allocations/core.py | 5 +- .../user/allocations/service/history.py | 12 -- .../user/allocations/service/pending.py | 13 +-- .../modules/user/allocations/service/saved.py | 7 ++ backend/tests/conftest.py | 7 +- backend/tests/legacy/test_allocations.py | 21 ---- backend/tests/legacy/test_user.py | 58 ++++++---- .../modules_factory/test_modules_factory.py | 5 +- .../modules/user/allocations/test_core.py | 2 +- .../allocations/test_history_allocations.py | 104 ------------------ .../allocations/test_pending_allocations.py | 2 - .../allocations/test_saved_allocations.py | 94 ++++++++++++++++ 19 files changed, 170 insertions(+), 203 deletions(-) delete mode 100644 backend/tests/modules/user/allocations/test_history_allocations.py diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index c43cdc08eb..57ef297844 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -6,7 +6,6 @@ from sqlalchemy.orm import Query from typing_extensions import deprecated -from app.exceptions import UserNotFound from app.extensions import db from app.infrastructure.database.models import Allocation, User, AllocationRequest from app.infrastructure.database.user import get_by_address diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py index 4be22d4ed5..693535f21a 100644 --- a/backend/app/legacy/controllers/allocations.py +++ b/backend/app/legacy/controllers/allocations.py @@ -1,12 +1,6 @@ from typing_extensions import deprecated -from dataclasses import dataclass -from typing import List from typing import Optional -from dataclass_wizard import JSONWizard - -from app.extensions import epochs -from app.infrastructure import database from app.modules.user.allocations import controller as new_controller from app.legacy.core.allocations import ( AllocationRequest, @@ -18,12 +12,3 @@ def allocate( request: AllocationRequest, is_manually_edited: Optional[bool] = None ) -> str: return new_controller.allocate(request, is_manually_edited=is_manually_edited) - - -@deprecated("ALLOCATIONS REWORK") -def get_all_by_user_and_epoch(user_address: str, epoch: int | None = None): - epoch = epochs.get_pending_epoch() if epoch is None else epoch - allocations = database.allocations.get_all_by_user_addr_and_epoch( - user_address, epoch - ) - return [(a.proposal_address, a.amount) for a in allocations] diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py index 37b0c5f774..2e6c665b5e 100644 --- a/backend/app/legacy/core/allocations.py +++ b/backend/app/legacy/core/allocations.py @@ -8,7 +8,6 @@ from app import exceptions from app.extensions import proposals from app.infrastructure import database -from app.infrastructure.database.models import User from app.legacy.core.epochs.epoch_snapshots import has_pending_epoch_snapshot from app.legacy.core.user.budget import get_budget from app.legacy.core.user.patron_mode import get_patron_mode_status diff --git a/backend/app/legacy/crypto/eip712.py b/backend/app/legacy/crypto/eip712.py index b091f58a3f..ff1ccc685a 100644 --- a/backend/app/legacy/crypto/eip712.py +++ b/backend/app/legacy/crypto/eip712.py @@ -6,6 +6,7 @@ from flask import current_app as app from app.extensions import w3 +from app.modules.dto import UserAllocationPayload def build_domain(): @@ -16,6 +17,16 @@ def build_domain(): } +def build_allocations_eip712_structure(payload: UserAllocationPayload): + message = {} + message["allocations"] = [ + {"proposalAddress": a.proposal_address, "amount": a.amount} + for a in payload.allocations + ] + message["nonce"] = payload.nonce + return build_allocations_eip712_data(message) + + def build_allocations_eip712_data(message: dict) -> dict: domain = build_domain() diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index cedad86d1d..35a52938ed 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -14,7 +14,6 @@ from app.modules.snapshots.pending.service.simulated import SimulatedPendingSnapshots from app.modules.staking.proceeds.service.estimated import EstimatedStakingProceeds from app.modules.user.allocations.service.saved import SavedUserAllocations -from app.modules.user.allocations.service.history import UserAllocationsHistory from app.modules.user.deposits.service.calculated import CalculatedUserDeposits from app.modules.user.events_generator.service.db_and_graph import ( DbAndGraphEventsGenerator, @@ -30,7 +29,7 @@ class CurrentUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protoco class CurrentServices(Model): - user_allocations_history_service: UserAllocationsHistory + user_allocations_service: SavedUserAllocations user_deposits_service: CurrentUserDeposits octant_rewards_service: OctantRewards history_service: HistoryService @@ -72,7 +71,7 @@ def create(chain_id: int) -> "CurrentServices": patron_donations=patron_donations, ) return CurrentServices( - user_allocations_history_service=UserAllocationsHistory(), + user_allocations_service=user_allocations, user_deposits_service=user_deposits, octant_rewards_service=CalculatedOctantRewards( staking_proceeds=EstimatedStakingProceeds(), diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 6a6bda7d1e..76c19268f9 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -20,7 +20,6 @@ from app.modules.snapshots.finalized.service.simulated import ( SimulatedFinalizedSnapshots, ) -from app.modules.user.allocations.service.history import UserAllocationsHistory from app.modules.user.allocations.service.pending import PendingUserAllocations from app.modules.user.budgets.service.saved import SavedUserBudgets from app.modules.user.deposits.service.saved import SavedUserDeposits @@ -66,7 +65,6 @@ def create() -> "PendingServices": octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) saved_user_budgets = SavedUserBudgets() saved_user_allocations = PendingUserAllocations( - user_nonce=UserAllocationsHistory(), user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, octant_rewards=octant_rewards, diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index 98b29320c3..9ea005d571 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -16,14 +16,11 @@ ) from app.modules.registry import get_services from app.modules.user.allocations.service.pending import PendingUserAllocations -from app.modules.user.allocations.service.history import UserAllocationsHistory def get_user_next_nonce(user_address: str) -> int: - service: UserAllocationsHistory = get_services( - EpochState.CURRENT - ).user_allocations_history_service - return service.get_next_user_nonce(user_address) + service = get_services(EpochState.CURRENT).user_allocations_service + return service.get_user_next_nonce(user_address) def get_all_allocations(epoch_num: int) -> List[ProposalDonationDTO]: @@ -118,7 +115,6 @@ def _deserialize_payload(payload: Dict) -> UserAllocationRequestPayload: def _deserialize_items(payload: Dict) -> List[AllocationItem]: - print(payload["allocations"]) return [ AllocationItem( proposal_address=allocation_data["proposalAddress"], diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index 5bee67aca8..dedcd7fdfd 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -1,7 +1,6 @@ from typing import List, Optional from app import exceptions - from app.context.manager import Context from app.context.epoch_state import EpochState from app.engine.projects import ProjectSettings @@ -10,7 +9,7 @@ from app.modules.common.project_rewards import get_projects_rewards from app.modules.dto import AllocationDTO, UserAllocationRequestPayload, AllocationItem -from app.legacy.crypto.eip712 import build_allocations_eip712_data, recover_address +from app.legacy.crypto.eip712 import build_allocations_eip712_structure, recover_address def next_allocation_nonce(prev_allocation_request: Optional[AllocationRequest]) -> int: @@ -46,7 +45,7 @@ def simulate_allocation( def recover_user_address(request: UserAllocationRequestPayload) -> str: - eip712_data = build_allocations_eip712_data(request.payload) + eip712_data = build_allocations_eip712_structure(request.payload) return recover_address(eip712_data, request.signature) diff --git a/backend/app/modules/user/allocations/service/history.py b/backend/app/modules/user/allocations/service/history.py index d0be42565c..e69de29bb2 100644 --- a/backend/app/modules/user/allocations/service/history.py +++ b/backend/app/modules/user/allocations/service/history.py @@ -1,12 +0,0 @@ -from app.pydantic import Model - -from app.infrastructure import database -from app.modules.user.allocations import core - - -class UserAllocationsHistory(Model): - def get_next_user_nonce(self, user_address: str) -> int: - allocation_request = database.allocations.get_user_last_allocation_request( - user_address - ) - return core.next_allocation_nonce(allocation_request) diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index e29827a0da..6cff6ad03b 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -25,33 +25,26 @@ def get_budget(self, context: Context, user_address: str) -> int: ... -@runtime_checkable -class UserNonceProtocol(Protocol): - def get_next_user_nonce(self, user_address: str) -> int: - ... - - class PendingUserAllocations(SavedUserAllocations, Model): octant_rewards: OctantRewards user_budgets: UserBudgetProtocol patrons_mode: UserPatronMode - user_nonce: UserNonceProtocol def allocate( self, context: Context, payload: UserAllocationRequestPayload, **kwargs ) -> str: user_address = core.recover_user_address(payload) - expected_nonce = self.user_nonce.get_user_next_nonce(user_address) + expected_nonce = self.get_user_next_nonce(user_address) user_budget = self.user_budgets.get_budget(context, user_address) patrons = self.patrons_mode.get_all_patrons_addresses(context) core.verify_user_allocation_request( - context, payload, expected_nonce, user_budget, patrons + context, payload, user_address, expected_nonce, user_budget, patrons ) self.revoke_previous_allocation(context, user_address) - db.allocations.store_allocation_request( + database.allocations.store_allocation_request( user_address, context.epoch_details.epoch_num, payload, **kwargs ) diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index bad673090b..738445e841 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -5,10 +5,17 @@ from app.modules.common.time import Timestamp, from_datetime from app.modules.dto import AllocationItem, AccountFundsDTO, ProposalDonationDTO from app.modules.history.dto import AllocationItem as HistoryAllocationItem +from app.modules.user.allocations import core from app.pydantic import Model class SavedUserAllocations(Model): + def get_user_next_nonce(self, user_address: str) -> int: + allocation_request = database.allocations.get_user_last_allocation_request( + user_address + ) + return core.next_allocation_nonce(allocation_request) + def get_all_donors_addresses(self, context: Context) -> List[str]: return database.allocations.get_users_with_allocations( context.epoch_details.epoch_num diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 060ec3f27c..310d196f85 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -415,7 +415,6 @@ def mock_epoch_details(mocker, graphql_client): @pytest.fixture(scope="function") def patch_epochs(monkeypatch): - monkeypatch.setattr("app.legacy.controllers.allocations.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.controllers.snapshots.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.controllers.rewards.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.core.proposals.epochs", MOCK_EPOCHS) @@ -522,6 +521,10 @@ def patch_last_finalized_snapshot(monkeypatch): @pytest.fixture(scope="function") def patch_user_budget(monkeypatch): monkeypatch.setattr("app.legacy.core.allocations.get_budget", MOCK_GET_USER_BUDGET) + monkeypatch.setattr( + "app.modules.user.budgets.service.saved.SavedUserBudgets.get_budget", + MOCK_GET_USER_BUDGET, + ) MOCK_GET_USER_BUDGET.return_value = USER_MOCKED_BUDGET @@ -790,7 +793,7 @@ def create_payload(proposals, amounts: list[int] | None, nonce: int = 0): def deserialize_allocations(payload) -> List[Allocation]: return [ AllocationItem( - proposal_address=allocation_data["proposal_data"], + proposal_address=allocation_data["proposalAddress"], amount=int(allocation_data["amount"]), ) for allocation_data in payload["allocations"] diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py index 5acef56648..978f7ec484 100644 --- a/backend/tests/legacy/test_allocations.py +++ b/backend/tests/legacy/test_allocations.py @@ -1,19 +1,13 @@ -import dataclasses - import pytest from app import exceptions -from app.extensions import db from app.infrastructure import database from app.legacy.controllers.allocations import ( - get_all_by_user_and_epoch, allocate, ) from app.legacy.core.allocations import ( AllocationRequest, - Allocation, ) -from app.legacy.core.user.patron_mode import toggle_patron_mode from app.legacy.crypto.eip712 import sign, build_allocations_eip712_data from tests.conftest import ( create_payload, @@ -21,7 +15,6 @@ mock_graphql, MOCKED_PENDING_EPOCH_NO, MOCK_PROPOSALS, - MOCK_EPOCHS, MOCK_GET_USER_BUDGET, ) from tests.helpers import create_epoch_event @@ -218,20 +211,6 @@ def test_multiple_users_change_their_allocations(tos_users, proposal_accounts): check_allocation_threshold(updated_payload1, updated_payload2) -def test_get_by_user_and_epoch(mock_allocations_db, user_accounts, proposal_accounts): - result = get_all_by_user_and_epoch( - user_accounts[0].address, MOCKED_PENDING_EPOCH_NO - ) - - assert len(result) == 3 - assert result[0].address == proposal_accounts[0].address - assert result[0].amount == str(10 * 10**18) - assert result[1].address == proposal_accounts[1].address - assert result[1].amount == str(5 * 10**18) - assert result[2].address == proposal_accounts[2].address - assert result[2].amount == str(300 * 10**18) - - def test_user_exceeded_rewards_budget_in_allocations(app, proposal_accounts, tos_users): # Set some reasonable user rewards budget MOCK_GET_USER_BUDGET.return_value = 100 * 10**18 diff --git a/backend/tests/legacy/test_user.py b/backend/tests/legacy/test_user.py index d5981c22d7..0112e657ed 100644 --- a/backend/tests/legacy/test_user.py +++ b/backend/tests/legacy/test_user.py @@ -3,12 +3,16 @@ from app import exceptions from app.extensions import db from app.infrastructure import database -from app.legacy.controllers import allocations as allocations_controller +from app.modules.dto import ( + AllocationItem, + UserAllocationPayload, + UserAllocationRequestPayload, +) +from app.modules.user.allocations import controller as allocations_controller from app.legacy.controllers.user import ( get_patron_mode_status, toggle_patron_mode, ) -from app.legacy.core.allocations import add_allocations_to_db, Allocation from app.legacy.core.user.budget import get_budget from tests.conftest import ( MOCKED_PENDING_EPOCH_NO, @@ -25,14 +29,20 @@ def before(app, patch_epochs, patch_proposals, patch_is_contract): @pytest.fixture() def make_allocations(app, proposal_accounts): def make_allocations(user, epoch): - nonce = allocations_controller.get_allocation_nonce(user.address) + nonce = allocations_controller.get_user_next_nonce(user.address) - allocations = [ - Allocation(proposal_accounts[0].address, 10 * 10**18), - Allocation(proposal_accounts[1].address, 20 * 10**18), - Allocation(proposal_accounts[2].address, 30 * 10**18), + allocation_items = [ + AllocationItem(proposal_accounts[0].address, 10 * 10**18), + AllocationItem(proposal_accounts[1].address, 20 * 10**18), + AllocationItem(proposal_accounts[2].address, 30 * 10**18), ] - add_allocations_to_db(epoch, user.address, nonce, allocations, True) + + request = UserAllocationRequestPayload( + payload=UserAllocationPayload(allocations=allocation_items, nonce=nonce), + signature="0xdeadbeef", + ) + + database.allocations.store_allocation_request(user.address, epoch, request) db.session.commit() @@ -102,12 +112,15 @@ def test_patron_mode_revokes_allocations_for_the_epoch( ): toggle_true_sig = "52d249ca8ac8f40c01613635dac8a9b01eb50230ad1467451a058170726650b92223e80032a4bff4d25c3554e9d1347043c53b4c2dc9f1ba3f071bd3a1c8b9121b" make_allocations(alice, MOCKED_PENDING_EPOCH_NO) - assert len(allocations_controller.get_all_by_user_and_epoch(alice.address)) == 3 + allocations, _ = allocations_controller.get_last_user_allocation( + alice.address, MOCKED_PENDING_EPOCH_NO + ) + assert len(allocations) == 3 toggle_patron_mode(alice.address, toggle_true_sig) - user_active_allocations = allocations_controller.get_all_by_user_and_epoch( - alice.address + user_active_allocations, _ = allocations_controller.get_last_user_allocation( + alice.address, MOCKED_PENDING_EPOCH_NO ) assert len(user_active_allocations) == 0 @@ -122,12 +135,13 @@ def test_when_patron_mode_changes_revoked_allocations_are_not_restored( toggle_patron_mode(alice.address, toggle_true_sig) toggle_patron_mode(alice.address, toggle_false_sig) - user_active_allocations = allocations_controller.get_all_by_user_and_epoch( - alice.address + user_active_allocations, _ = allocations_controller.get_last_user_allocation( + alice.address, MOCKED_PENDING_EPOCH_NO ) assert len(user_active_allocations) == 0 +@pytest.mark.skip("Cannot create epoch context for epoch 0") def test_patron_mode_does_not_revoke_allocations_from_previous_epochs( alice, make_allocations, mock_pending_epoch_snapshot_db ): @@ -135,10 +149,13 @@ def test_patron_mode_does_not_revoke_allocations_from_previous_epochs( make_allocations(alice, MOCKED_PENDING_EPOCH_NO - 1) make_allocations(alice, MOCKED_PENDING_EPOCH_NO) - user_active_allocations_pre = allocations_controller.get_all_by_user_and_epoch( - alice.address + user_active_allocations_pre, _ = allocations_controller.get_last_user_allocation( + alice.address, MOCKED_PENDING_EPOCH_NO ) - user_prev_epoch_allocations_pre = allocations_controller.get_all_by_user_and_epoch( + ( + user_prev_epoch_allocations_pre, + _, + ) = allocations_controller.get_last_user_allocation( alice.address, MOCKED_PENDING_EPOCH_NO - 1 ) @@ -147,10 +164,13 @@ def test_patron_mode_does_not_revoke_allocations_from_previous_epochs( toggle_patron_mode(alice.address, toggle_true_sig) - user_active_allocations_post = allocations_controller.get_all_by_user_and_epoch( - alice.address + user_active_allocations_post, _ = allocations_controller.get_last_user_allocation( + alice.address, MOCKED_PENDING_EPOCH_NO ) - user_prev_epoch_allocations_post = allocations_controller.get_all_by_user_and_epoch( + ( + user_prev_epoch_allocations_post, + _, + ) = allocations_controller.get_last_user_allocation( alice.address, MOCKED_PENDING_EPOCH_NO - 1 ) diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index f554254dc4..5e94f8b0ce 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -103,7 +103,10 @@ def test_pending_services_factory(): events_based_patron_mode = EventsBasedUserPatronMode() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) - user_allocations = PendingUserAllocations(octant_rewards=octant_rewards) + user_allocations = PendingUserAllocations( + octant_rewards=octant_rewards, + patrons_mode=events_based_patron_mode, + ) user_rewards = CalculatedUserRewards( user_budgets=SavedUserBudgets(), patrons_mode=events_based_patron_mode, diff --git a/backend/tests/modules/user/allocations/test_core.py b/backend/tests/modules/user/allocations/test_core.py index ee22fd7e55..e6df364739 100644 --- a/backend/tests/modules/user/allocations/test_core.py +++ b/backend/tests/modules/user/allocations/test_core.py @@ -104,7 +104,7 @@ def test_allocation_fails_with_invalid_proposals(alice, bob, context, projects): ) -def test_allocation_fails_with_invalid_proposals(alice, bob, context): +def test_allocation_fails_with_duplucated_proposals(alice, bob, context): projects = context.projects_details.projects allocations = build_allocations( [(p, 17 * 10**16) for p in projects] + [(projects[1], 1)] diff --git a/backend/tests/modules/user/allocations/test_history_allocations.py b/backend/tests/modules/user/allocations/test_history_allocations.py deleted file mode 100644 index 6fc0f83f9e..0000000000 --- a/backend/tests/modules/user/allocations/test_history_allocations.py +++ /dev/null @@ -1,104 +0,0 @@ -import pytest - -from app.infrastructure import database -from app.modules.dto import UserAllocationRequestPayload, UserAllocationPayload -from app.modules.user.allocations.service.history import UserAllocationsHistory - - -@pytest.fixture() -def service(app): - return UserAllocationsHistory() - - -def _mock_request(nonce): - fake_signature = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - - return UserAllocationRequestPayload( - payload=UserAllocationPayload([], nonce), signature=fake_signature - ) - - -def test_user_nonce_for_non_existent_user_is_0(service, alice): - assert database.user.get_by_address(alice.address) is None - assert service.get_next_user_nonce(alice.address) == 0 - - -def test_user_nonce_for_new_user_is_0(service, mock_users_db): - alice, _, _ = mock_users_db - - assert service.get_next_user_nonce(alice.address) == 0 - - -def test_user_nonce_changes_increases_at_each_allocation_request( - service, mock_users_db -): - alice, _, _ = mock_users_db - - database.allocations.store_allocation_request(alice.address, 0, _mock_request(0)) - new_nonce = service.get_next_user_nonce(alice.address) - - assert new_nonce == 1 - - database.allocations.store_allocation_request( - alice.address, 0, _mock_request(new_nonce) - ) - new_nonce = service.get_next_user_nonce(alice.address) - - assert new_nonce == 2 - - -def test_user_nonce_changes_increases_at_each_allocation_request_for_each_user( - service, mock_users_db -): - alice, bob, carol = mock_users_db - - for i in range(0, 5): - database.allocations.store_allocation_request( - alice.address, 0, _mock_request(i) - ) - next_user_nonce = service.get_next_user_nonce(alice.address) - assert next_user_nonce == i + 1 - - # for other users, nonces do not change - assert service.get_next_user_nonce(bob.address) == 0 - assert service.get_next_user_nonce(carol.address) == 0 - - for i in range(0, 4): - database.allocations.store_allocation_request(bob.address, 0, _mock_request(i)) - next_user_nonce = service.get_next_user_nonce(bob.address) - assert next_user_nonce == i + 1 - - # for other users, nonces do not change - assert service.get_next_user_nonce(alice.address) == 5 - assert service.get_next_user_nonce(carol.address) == 0 - - for i in range(0, 3): - database.allocations.store_allocation_request( - carol.address, 0, _mock_request(i) - ) - next_user_nonce = service.get_next_user_nonce(carol.address) - assert next_user_nonce == i + 1 - - # for other users, nonces do not change - assert service.get_next_user_nonce(alice.address) == 5 - assert service.get_next_user_nonce(bob.address) == 4 - - -def test_user_nonce_is_continuous_despite_epoch_changes(service, mock_users_db): - alice, _, _ = mock_users_db - - database.allocations.store_allocation_request(alice.address, 1, _mock_request(0)) - new_nonce = service.get_next_user_nonce(alice.address) - assert new_nonce == 1 - - database.allocations.store_allocation_request( - alice.address, 2, _mock_request(new_nonce) - ) - new_nonce = service.get_next_user_nonce(alice.address) - assert new_nonce == 2 - - database.allocations.store_allocation_request( - alice.address, 10, _mock_request(new_nonce) - ) - new_nonce = service.get_next_user_nonce(alice.address) - assert new_nonce == 3 diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index f24a4e964f..fc988c5b85 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -5,7 +5,6 @@ from app.infrastructure import database from app.context.epoch_state import EpochState from app.modules.dto import AllocationDTO -from app.modules.user.allocations.service.history import UserAllocationsHistory from app.modules.user.allocations.service.pending import PendingUserAllocations from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context @@ -22,7 +21,6 @@ def service(mock_octant_rewards, mock_patron_mode, mock_user_budgets): octant_rewards=mock_octant_rewards, user_budgets=mock_user_budgets, patrons_mode=mock_patron_mode, - user_nonce=UserAllocationsHistory(), ) diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index bbf5368028..0b93e6a9e9 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -58,6 +58,100 @@ def _alloc_item_to_donation(item, user): return ProposalDonationDTO(user.address, item.amount, item.proposal_address) +def _mock_request(nonce): + fake_signature = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + + return UserAllocationRequestPayload( + payload=UserAllocationPayload([], nonce), signature=fake_signature + ) + + +def test_user_nonce_for_non_existent_user_is_0(service, alice): + assert database.user.get_by_address(alice.address) is None + assert service.get_user_next_nonce(alice.address) == 0 + + +def test_user_nonce_for_new_user_is_0(service, mock_users_db): + alice, _, _ = mock_users_db + + assert service.get_user_next_nonce(alice.address) == 0 + + +def test_user_nonce_changes_increases_at_each_allocation_request( + service, mock_users_db +): + alice, _, _ = mock_users_db + + database.allocations.store_allocation_request(alice.address, 0, _mock_request(0)) + new_nonce = service.get_user_next_nonce(alice.address) + + assert new_nonce == 1 + + database.allocations.store_allocation_request( + alice.address, 0, _mock_request(new_nonce) + ) + new_nonce = service.get_user_next_nonce(alice.address) + + assert new_nonce == 2 + + +def test_user_nonce_changes_increases_at_each_allocation_request_for_each_user( + service, mock_users_db +): + alice, bob, carol = mock_users_db + + for i in range(0, 5): + database.allocations.store_allocation_request( + alice.address, 0, _mock_request(i) + ) + next_user_nonce = service.get_user_next_nonce(alice.address) + assert next_user_nonce == i + 1 + + # for other users, nonces do not change + assert service.get_user_next_nonce(bob.address) == 0 + assert service.get_user_next_nonce(carol.address) == 0 + + for i in range(0, 4): + database.allocations.store_allocation_request(bob.address, 0, _mock_request(i)) + next_user_nonce = service.get_user_next_nonce(bob.address) + assert next_user_nonce == i + 1 + + # for other users, nonces do not change + assert service.get_user_next_nonce(alice.address) == 5 + assert service.get_user_next_nonce(carol.address) == 0 + + for i in range(0, 3): + database.allocations.store_allocation_request( + carol.address, 0, _mock_request(i) + ) + next_user_nonce = service.get_user_next_nonce(carol.address) + assert next_user_nonce == i + 1 + + # for other users, nonces do not change + assert service.get_user_next_nonce(alice.address) == 5 + assert service.get_user_next_nonce(bob.address) == 4 + + +def test_user_nonce_is_continuous_despite_epoch_changes(service, mock_users_db): + alice, _, _ = mock_users_db + + database.allocations.store_allocation_request(alice.address, 1, _mock_request(0)) + new_nonce = service.get_user_next_nonce(alice.address) + assert new_nonce == 1 + + database.allocations.store_allocation_request( + alice.address, 2, _mock_request(new_nonce) + ) + new_nonce = service.get_user_next_nonce(alice.address) + assert new_nonce == 2 + + database.allocations.store_allocation_request( + alice.address, 10, _mock_request(new_nonce) + ) + new_nonce = service.get_user_next_nonce(alice.address) + assert new_nonce == 3 + + def test_get_all_donors_addresses(service, mock_users_db, make_user_allocation): user1, user2, user3 = mock_users_db context_epoch_1 = get_context(1) From 4adcd648461b5015dd3e3eef5ff41f16fdc92886 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 26 Mar 2024 13:01:37 +0100 Subject: [PATCH 045/107] chore: add minor fixes after CR --- .../app/infrastructure/database/allocations.py | 2 +- .../app/infrastructure/routes/allocations.py | 2 +- .../app/modules/user/allocations/controller.py | 2 -- backend/app/modules/user/allocations/core.py | 2 +- .../user/allocations/service/pending.py | 13 +++++++------ ...f89a0fae4ae_drop_allocation_nonce_column.py | 7 ------- .../allocations/test_pending_allocations.py | 18 ------------------ 7 files changed, 10 insertions(+), 36 deletions(-) diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 57ef297844..d95273e35e 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -153,7 +153,7 @@ def get_user_alloc_sum_by_epoch(epoch: int, user_address: str) -> int: def store_allocation_request( - user_address: int, epoch_num: int, request: UserAllocationRequestPayload, **kwargs + user_address: str, epoch_num: int, request: UserAllocationRequestPayload, **kwargs ): now = datetime.utcnow() diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 1a805bd29a..66be78ef96 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -300,7 +300,7 @@ def get(self, proposal_address: str, epoch: int): f"Getting donors for proposal {proposal_address} in epoch {epoch}" ) donors = [ - {"address": w.donor, "amount": str(w.amount)} + {"address": w.donor, "amount": w.amount} for w in controller.get_all_donations_by_project(proposal_address, epoch) ] app.logger.debug(f"Proposal donors {donors}") diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index 9ea005d571..a59144383e 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -91,8 +91,6 @@ def simulate_allocation( def revoke_previous_allocation(user_address: str): - context = None - try: context = state_context(EpochState.PENDING) except InvalidEpoch: diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index dedcd7fdfd..bb82c3ffa6 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -79,7 +79,7 @@ def _verify_epoch_state(epoch_state: EpochState): raise exceptions.NotInDecisionWindow -def _verify_nonce(nonce, expected_nonce): +def _verify_nonce(nonce: int, expected_nonce: int): # if expected_nonce is not None and request.payload.nonce != expected_nonce: if nonce != expected_nonce: raise exceptions.WrongAllocationsNonce(nonce, expected_nonce) diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 6cff6ad03b..93b25e6f9a 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -4,11 +4,9 @@ from app import exceptions from app.extensions import db from app.context.manager import Context -from app.context.epoch_state import EpochState from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database from app.modules.dto import AllocationDTO, UserAllocationRequestPayload -from app.modules.modules_factory.protocols import UserPatronMode from app.modules.user.allocations import core from app.modules.user.allocations.service.saved import SavedUserAllocations @@ -25,10 +23,16 @@ def get_budget(self, context: Context, user_address: str) -> int: ... +@runtime_checkable +class GetPatronsAddressesProtocol(Protocol): + def get_all_patrons_addresses(self, context: Context) -> List[str]: + ... + + class PendingUserAllocations(SavedUserAllocations, Model): octant_rewards: OctantRewards user_budgets: UserBudgetProtocol - patrons_mode: UserPatronMode + patrons_mode: GetPatronsAddressesProtocol def allocate( self, context: Context, payload: UserAllocationRequestPayload, **kwargs @@ -75,9 +79,6 @@ def simulate_allocation( ) def revoke_previous_allocation(self, context: Context, user_address: str): - if context.epoch_state is not EpochState.PENDING: - raise exceptions.NotInDecisionWindow - user = database.user.get_by_address(user_address) if user is None: raise exceptions.UserNotFound diff --git a/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py b/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py index 9b69c33329..51baddaae1 100644 --- a/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py +++ b/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py @@ -9,7 +9,6 @@ import sqlalchemy as sa -# revision identifiers, used by Alembic. revision = "1f89a0fae4ae" down_revision = "7bb6835486a5" branch_labels = None @@ -17,20 +16,14 @@ def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("users", schema=None) as batch_op: batch_op.drop_column("allocation_nonce") - # ### end Alembic commands ### - def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### with op.batch_alter_table("users", schema=None) as batch_op: batch_op.add_column( sa.Column( "allocation_nonce", sa.INTEGER(), autoincrement=False, nullable=True ) ) - - # ### end Alembic commands ### diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index fc988c5b85..a9b6562802 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -1,6 +1,5 @@ import pytest -from app import exceptions from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database from app.context.epoch_state import EpochState @@ -71,20 +70,3 @@ def test_revoke_previous_allocation(service, mock_users_db): assert service.get_user_allocation_sum(context, user1.address) == 100_000000000 service.revoke_previous_allocation(context, user1.address) assert service.get_user_allocation_sum(context, user1.address) == 0 - - -def test_revoke_previous_allocation_fails_outside_decision_window( - service, mock_users_db -): - user1, _, _ = mock_users_db - - for state in [ - EpochState.FUTURE, - EpochState.CURRENT, - EpochState.PRE_PENDING, - EpochState.FINALIZING, - EpochState.FINALIZED, - ]: - context = get_context(epoch_state=state) - with pytest.raises(exceptions.NotInDecisionWindow): - service.revoke_previous_allocation(context, user1.address) From ece03ee2d460d2c0a88c6a57a797b109021718d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 26 Mar 2024 13:02:26 +0100 Subject: [PATCH 046/107] feature: use default value for created_at field in db entities --- backend/app/infrastructure/database/allocations.py | 6 ------ backend/app/infrastructure/database/models.py | 4 ++-- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index d95273e35e..d3e3afc089 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -155,8 +155,6 @@ def get_user_alloc_sum_by_epoch(epoch: int, user_address: str) -> int: def store_allocation_request( user_address: str, epoch_num: int, request: UserAllocationRequestPayload, **kwargs ): - now = datetime.utcnow() - user: User = get_by_address(user_address) options = {"is_manually_edited": None, **kwargs} @@ -168,7 +166,6 @@ def store_allocation_request( nonce=request.payload.nonce, proposal_address=to_checksum_address(a.proposal_address), amount=str(a.amount), - created_at=now, ) for a in request.payload.allocations ] @@ -187,8 +184,6 @@ def store_allocation_request( @deprecated("Alloc rework") def add_all(epoch: int, user_id: int, nonce: int, allocations): - now = datetime.utcnow() - new_allocations = [ Allocation( epoch=epoch, @@ -196,7 +191,6 @@ def add_all(epoch: int, user_id: int, nonce: int, allocations): nonce=nonce, proposal_address=to_checksum_address(a.proposal_address), amount=str(a.amount), - created_at=now, ) for a in allocations ] diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index 85f770987d..742e8e9669 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -1,7 +1,7 @@ -from datetime import datetime as dt from typing import Optional from app.extensions import db +from app.modules.common import time # Alias common SQLAlchemy names Column = db.Column @@ -12,7 +12,7 @@ class BaseModel(Model): __abstract__ = True - created_at = Column(db.TIMESTAMP, default=dt.utcnow) + created_at = Column(db.TIMESTAMP, default=lambda: time.now().datetime()) class User(BaseModel): From 58cfe7103b21fc36f0ccc76e7d946c5159eeb316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 26 Mar 2024 16:05:03 +0100 Subject: [PATCH 047/107] chore: revert drop of allocation_nonce from users table --- backend/app/infrastructure/database/models.py | 5 ++++ .../user/allocations/service/pending.py | 3 ++ ...89a0fae4ae_drop_allocation_nonce_column.py | 29 ------------------- 3 files changed, 8 insertions(+), 29 deletions(-) delete mode 100644 backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index 742e8e9669..5abc9b6406 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -20,6 +20,11 @@ class User(BaseModel): id = Column(db.Integer, primary_key=True) address = Column(db.String(42), unique=True, nullable=False) + allocation_nonce = Column( + db.Integer, + nullable=True, + comment="Allocations signing nonce, last used value. Range [0..inf)", + ) def get_effective_deposit(self, epoch: int) -> Optional[int]: effective_deposit = None diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 93b25e6f9a..1d1ebbd4b3 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -48,6 +48,9 @@ def allocate( ) self.revoke_previous_allocation(context, user_address) + + user = database.user.get_by_address(user_address) + user.allocation_nonce = expected_nonce database.allocations.store_allocation_request( user_address, context.epoch_details.epoch_num, payload, **kwargs ) diff --git a/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py b/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py deleted file mode 100644 index 51baddaae1..0000000000 --- a/backend/migrations/versions/1f89a0fae4ae_drop_allocation_nonce_column.py +++ /dev/null @@ -1,29 +0,0 @@ -"""drop allocation nonce column from user table - -Revision ID: 1f89a0fae4ae -Revises: 7bb6835486a5 -Create Date: 2024-03-12 18:00:32.503807 - -""" -from alembic import op -import sqlalchemy as sa - - -revision = "1f89a0fae4ae" -down_revision = "7bb6835486a5" -branch_labels = None -depends_on = None - - -def upgrade(): - with op.batch_alter_table("users", schema=None) as batch_op: - batch_op.drop_column("allocation_nonce") - - -def downgrade(): - with op.batch_alter_table("users", schema=None) as batch_op: - batch_op.add_column( - sa.Column( - "allocation_nonce", sa.INTEGER(), autoincrement=False, nullable=True - ) - ) From c8152982850ca23c3c7705531f9876874e8576e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 27 Mar 2024 11:35:37 +0100 Subject: [PATCH 048/107] feature: use new alloc in websockets --- backend/app/infrastructure/events.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index a25fc5b3c0..a5c5dc94c8 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -11,11 +11,9 @@ from app.modules.dto import ProposalDonationDTO from app.modules.user.allocations import controller -from app.legacy.controllers.allocations import allocate from app.legacy.controllers.rewards import ( get_allocation_threshold, ) -from app.legacy.core.allocations import AllocationRequest from app.modules.project_rewards.controller import get_estimated_project_rewards @@ -39,12 +37,11 @@ def handle_disconnect(): @socketio.on("allocate") def handle_allocate(msg): msg = json.loads(msg) - payload, signature = msg["payload"], msg["signature"] is_manually_edited = msg["isManuallyEdited"] if "isManuallyEdited" in msg else None - app.logger.info(f"User allocation payload: {payload}, signature: {signature}") - user_address = allocate( - AllocationRequest(payload, signature, override_existing_allocations=True), - is_manually_edited, + app.logger.info(f"User allocation payload: {msg}") + user_address = controller.allocate( + msg, + is_manually_edited=is_manually_edited, ) app.logger.info(f"User: {user_address} allocated successfully") From 66c2d6fdcee125e624f983bb911047dd100f6ba3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 27 Mar 2024 10:36:16 +0100 Subject: [PATCH 049/107] chore: clean up tests --- .../infrastructure/database/allocations.py | 38 +-- .../app/infrastructure/routes/allocations.py | 10 +- backend/app/legacy/controllers/allocations.py | 14 - backend/app/legacy/core/allocations.py | 117 ------- .../modules/user/allocations/controller.py | 7 +- backend/tests/conftest.py | 99 ++---- backend/tests/helpers/__init__.py | 1 + backend/tests/helpers/allocations.py | 59 ++++ backend/tests/legacy/test_allocations.py | 311 ------------------ backend/tests/legacy/test_rewards.py | 21 +- backend/tests/legacy/test_user.py | 2 +- .../modules_factory/test_modules_factory.py | 6 +- .../test_finalized_octant_rewards.py | 17 +- .../test_pending_octant_rewards.py | 15 +- .../project_rewards/test_estimated_rewards.py | 13 +- .../finalized/test_finalizing_snapshots.py | 7 +- .../test_simulated_finalized_snapshots.py | 8 +- .../allocations/test_pending_allocations.py | 284 +++++++++++++++- .../allocations/test_saved_allocations.py | 70 +--- 19 files changed, 425 insertions(+), 674 deletions(-) delete mode 100644 backend/app/legacy/controllers/allocations.py delete mode 100644 backend/app/legacy/core/allocations.py create mode 100644 backend/tests/helpers/allocations.py delete mode 100644 backend/tests/legacy/test_allocations.py diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index d3e3afc089..19c61a24bd 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -1,6 +1,6 @@ from collections import defaultdict from datetime import datetime -from typing import List, Optional +from typing import List from eth_utils import to_checksum_address from sqlalchemy.orm import Query @@ -182,42 +182,6 @@ def store_allocation_request( db.session.add_all(new_allocations) -@deprecated("Alloc rework") -def add_all(epoch: int, user_id: int, nonce: int, allocations): - new_allocations = [ - Allocation( - epoch=epoch, - user_id=user_id, - nonce=nonce, - proposal_address=to_checksum_address(a.proposal_address), - amount=str(a.amount), - ) - for a in allocations - ] - db.session.add_all(new_allocations) - - -@deprecated("Alloc rework") -def add_allocation_request( - user_address: str, - epoch: int, - nonce: int, - signature: str, - is_manually_edited: Optional[bool] = None, -): - user: User = get_by_address(user_address) - - allocation_request = AllocationRequest( - user=user, - epoch=epoch, - nonce=nonce, - signature=signature, - is_manually_edited=is_manually_edited, - ) - - db.session.add(allocation_request) - - def get_allocation_request_by_user_nonce( user_address: str, nonce: int ) -> AllocationRequest | None: diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 66be78ef96..73ed8eed59 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -3,8 +3,6 @@ from flask import current_app as app from flask_restx import Namespace, fields -from app.legacy.controllers import allocations -from app.legacy.core.allocations import AllocationRequest from app.extensions import api from app.infrastructure import OctantResource from app.modules.user.allocations import controller @@ -165,14 +163,12 @@ class Allocation(OctantResource): @ns.expect(user_allocation_request) @ns.response(201, "User allocated successfully") def post(self): - payload, signature = ns.payload["payload"], ns.payload["signature"] + app.logger.info(f"User allocation: {ns.payload}") is_manually_edited = ( ns.payload["isManuallyEdited"] if "isManuallyEdited" in ns.payload else None ) - app.logger.info(f"User allocation payload: {payload}, signature: {signature}") - user_address = allocations.allocate( - AllocationRequest(payload, signature, override_existing_allocations=True), - is_manually_edited, + user_address = controller.allocate( + ns.payload, is_manually_edited=is_manually_edited ) app.logger.info(f"User: {user_address} allocated successfully") diff --git a/backend/app/legacy/controllers/allocations.py b/backend/app/legacy/controllers/allocations.py deleted file mode 100644 index 693535f21a..0000000000 --- a/backend/app/legacy/controllers/allocations.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing_extensions import deprecated -from typing import Optional - -from app.modules.user.allocations import controller as new_controller -from app.legacy.core.allocations import ( - AllocationRequest, -) - - -@deprecated("ALLOCATIONS REWORK") -def allocate( - request: AllocationRequest, is_manually_edited: Optional[bool] = None -) -> str: - return new_controller.allocate(request, is_manually_edited=is_manually_edited) diff --git a/backend/app/legacy/core/allocations.py b/backend/app/legacy/core/allocations.py deleted file mode 100644 index 2e6c665b5e..0000000000 --- a/backend/app/legacy/core/allocations.py +++ /dev/null @@ -1,117 +0,0 @@ -from typing_extensions import deprecated -from dataclasses import dataclass -from typing import List, Dict, Tuple, Optional - -from dataclass_wizard import JSONWizard -from eth_utils import to_checksum_address - -from app import exceptions -from app.extensions import proposals -from app.infrastructure import database -from app.legacy.core.epochs.epoch_snapshots import has_pending_epoch_snapshot -from app.legacy.core.user.budget import get_budget -from app.legacy.core.user.patron_mode import get_patron_mode_status - - -@dataclass(frozen=True) -class Allocation(JSONWizard): - proposal_address: str - amount: int - - -@dataclass(frozen=True) -class AllocationRequest(JSONWizard): - payload: Dict - signature: str - override_existing_allocations: bool - - -def add_allocations_to_db( - epoch: int, - user_address: str, - nonce: int, - allocations: List[Allocation], - delete_existing_user_epoch_allocations: bool, -): - user = database.user.get_by_address(user_address) - if not user: - user = database.user.add_user(user_address) - - if delete_existing_user_epoch_allocations: - revoke_previous_allocation(user.address, epoch) - - database.allocations.add_all(epoch, user.id, nonce, allocations) - - -def deserialize_payload(payload) -> Tuple[int, List[Allocation]]: - allocations = [ - Allocation.from_dict(allocation_data) - for allocation_data in payload["allocations"] - ] - return payload["nonce"], allocations - - -@deprecated("ALLOCATIONS REWORK") -def verify_allocations( - epoch: Optional[int], user_address: str, allocations: List[Allocation] -): - if epoch is None: - raise exceptions.NotInDecisionWindow - - if not has_pending_epoch_snapshot(epoch): - raise exceptions.MissingSnapshot - - patron_mode_enabled = get_patron_mode_status( - user_address=to_checksum_address(user_address) - ) - if patron_mode_enabled: - raise exceptions.NotAllowedInPatronMode(user_address) - - # Check if allocations list is empty - if len(allocations) == 0: - raise exceptions.EmptyAllocations() - - # Check if the list of proposal addresses is a subset of - # proposal addresses in the Proposals contract - proposal_addresses = [a.proposal_address for a in allocations] - valid_proposals = proposals.get_proposal_addresses(epoch) - invalid_proposals = list(set(proposal_addresses) - set(valid_proposals)) - - if invalid_proposals: - raise exceptions.InvalidProjects(invalid_proposals) - - # Check if any allocation address has been duplicated in the payload - [proposal_addresses.remove(p) for p in set(proposal_addresses)] - - if proposal_addresses: - raise exceptions.DuplicatedProposals(proposal_addresses) - - # Check if user address is not in one of the allocations - for allocation in allocations: - if allocation.proposal_address == user_address: - raise exceptions.ProposalAllocateToItself - - # Check if user didn't exceed his budget - user_budget = get_budget(user_address, epoch) - proposals_sum = sum([a.amount for a in allocations]) - - if proposals_sum > user_budget: - raise exceptions.RewardsBudgetExceeded - - -@deprecated("ALLOCATIONS REWORK") -def revoke_previous_allocation(user_address: str, epoch: int): - user = database.user.get_by_address(user_address) - if user is None: - raise exceptions.UserNotFound - - database.allocations.soft_delete_all_by_epoch_and_user_id(epoch, user.id) - - -def has_user_allocated_rewards(user_address: str, epoch: int) -> List[str]: - allocation_signature = ( - database.allocations.get_allocation_request_by_user_and_epoch( - user_address, epoch - ) - ) - return allocation_signature is not None diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index a59144383e..a164bbc56d 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -103,9 +103,10 @@ def revoke_previous_allocation(user_address: str): def _deserialize_payload(payload: Dict) -> UserAllocationRequestPayload: - allocation_items = _deserialize_items(payload.payload) - nonce = int(payload.payload["nonce"]) - signature = payload.signature + allocation_payload = payload["payload"] + allocation_items = _deserialize_items(allocation_payload) + nonce = int(allocation_payload["nonce"]) + signature = payload["signature"] return UserAllocationRequestPayload( payload=UserAllocationPayload(allocation_items, nonce), signature=signature diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 310d196f85..5fd0353e0f 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -4,13 +4,10 @@ import os import time import urllib.request -from typing import List -from random import randint from unittest.mock import MagicMock, Mock import gql import pytest -from eth_account import Account from flask import g as request_context from flask.testing import FlaskClient from web3 import Web3 @@ -23,8 +20,6 @@ from app.infrastructure.contracts.erc20 import ERC20 from app.infrastructure.contracts.proposals import Proposals from app.infrastructure.contracts.vault import Vault -from app.legacy.controllers.allocations import allocate -from app.legacy.core.allocations import Allocation, AllocationRequest from app.legacy.crypto.account import Account as CryptoAccount from app.legacy.crypto.eip712 import build_allocations_eip712_data, sign from app.modules.dto import AccountFundsDTO, AllocationItem @@ -55,6 +50,7 @@ MATCHED_REWARDS_AFTER_OVERHAUL, NO_PATRONS_REWARDS, ) +from tests.helpers import make_user_allocation from tests.helpers.context import get_context from tests.helpers.gql_client import MockGQLClient from tests.helpers.mocked_epoch_details import EPOCH_EVENTS @@ -437,7 +433,6 @@ def patch_epochs(monkeypatch): @pytest.fixture(scope="function") def patch_proposals(monkeypatch, proposal_accounts): - monkeypatch.setattr("app.legacy.core.allocations.proposals", MOCK_PROPOSALS) monkeypatch.setattr("app.legacy.core.proposals.proposals", MOCK_PROPOSALS) monkeypatch.setattr("app.context.projects.proposals", MOCK_PROPOSALS) @@ -492,12 +487,6 @@ def patch_eth_get_balance(monkeypatch): @pytest.fixture(scope="function") def patch_has_pending_epoch_snapshot(monkeypatch): - ( - monkeypatch.setattr( - "app.legacy.core.allocations.has_pending_epoch_snapshot", - MOCK_HAS_PENDING_SNAPSHOT, - ) - ) ( monkeypatch.setattr( "app.context.epoch_state._has_pending_epoch_snapshot", @@ -520,7 +509,6 @@ def patch_last_finalized_snapshot(monkeypatch): @pytest.fixture(scope="function") def patch_user_budget(monkeypatch): - monkeypatch.setattr("app.legacy.core.allocations.get_budget", MOCK_GET_USER_BUDGET) monkeypatch.setattr( "app.modules.user.budgets.service.saved.SavedUserBudgets.get_budget", MOCK_GET_USER_BUDGET, @@ -608,45 +596,51 @@ def mock_finalized_epoch_snapshot_db(app, user_accounts): @pytest.fixture(scope="function") -def mock_allocations_db(app, user_accounts, proposal_accounts): - user1 = database.user.get_or_add_user(user_accounts[0].address) - user2 = database.user.get_or_add_user(user_accounts[1].address) - db.session.commit() +def mock_allocations_db(app, mock_users_db, proposal_accounts): + prev_epoch_context = get_context(MOCKED_PENDING_EPOCH_NO - 1) + pending_epoch_context = get_context(MOCKED_PENDING_EPOCH_NO) + user1, user2, _ = mock_users_db user1_allocations = [ - Allocation(proposal_accounts[0].address, 10 * 10**18), - Allocation(proposal_accounts[1].address, 5 * 10**18), - Allocation(proposal_accounts[2].address, 300 * 10**18), + AllocationItem(proposal_accounts[0].address, 10 * 10**18), + AllocationItem(proposal_accounts[1].address, 5 * 10**18), + AllocationItem(proposal_accounts[2].address, 300 * 10**18), ] user1_allocations_prev_epoch = [ - Allocation(proposal_accounts[0].address, 101 * 10**18), - Allocation(proposal_accounts[1].address, 51 * 10**18), - Allocation(proposal_accounts[2].address, 3001 * 10**18), + AllocationItem(proposal_accounts[0].address, 101 * 10**18), + AllocationItem(proposal_accounts[1].address, 51 * 10**18), + AllocationItem(proposal_accounts[2].address, 3001 * 10**18), ] user2_allocations = [ - Allocation(proposal_accounts[1].address, 1050 * 10**18), - Allocation(proposal_accounts[3].address, 500 * 10**18), + AllocationItem(proposal_accounts[1].address, 1050 * 10**18), + AllocationItem(proposal_accounts[3].address, 500 * 10**18), ] user2_allocations_prev_epoch = [ - Allocation(proposal_accounts[1].address, 10501 * 10**18), - Allocation(proposal_accounts[3].address, 5001 * 10**18), + AllocationItem(proposal_accounts[1].address, 10501 * 10**18), + AllocationItem(proposal_accounts[3].address, 5001 * 10**18), ] - database.allocations.add_all( - MOCKED_PENDING_EPOCH_NO - 1, user1.id, 0, user1_allocations_prev_epoch + make_user_allocation( + prev_epoch_context, + user1, + nonce=0, + allocation_items=user1_allocations_prev_epoch, ) - database.allocations.add_all( - MOCKED_PENDING_EPOCH_NO - 1, user2.id, 0, user2_allocations_prev_epoch + make_user_allocation( + prev_epoch_context, + user2, + nonce=0, + allocation_items=user2_allocations_prev_epoch, ) - database.allocations.add_all( - MOCKED_PENDING_EPOCH_NO, user1.id, 1, user1_allocations + make_user_allocation( + pending_epoch_context, user1, nonce=1, allocation_items=user1_allocations ) - database.allocations.add_all( - MOCKED_PENDING_EPOCH_NO, user2.id, 1, user2_allocations + make_user_allocation( + pending_epoch_context, user2, nonce=1, allocation_items=user2_allocations ) db.session.commit() @@ -765,41 +759,6 @@ def mock_user_rewards(alice, bob): return user_rewards_service_mock -def allocate_user_rewards( - user_account: Account, proposal_account, allocation_amount, nonce: int = 0 -): - payload = create_payload([proposal_account], [allocation_amount], nonce) - signature = sign(user_account, build_allocations_eip712_data(payload)) - request = AllocationRequest(payload, signature, override_existing_allocations=False) - - allocate(request) - - -def create_payload(proposals, amounts: list[int] | None, nonce: int = 0): - if amounts is None: - amounts = [randint(1 * 10**18, 1000 * 10**18) for _ in proposals] - - allocations = [ - { - "proposalAddress": proposal.address, - "amount": str(amount), - } - for proposal, amount in zip(proposals, amounts) - ] - - return {"allocations": allocations, "nonce": nonce} - - -def deserialize_allocations(payload) -> List[Allocation]: - return [ - AllocationItem( - proposal_address=allocation_data["proposalAddress"], - amount=int(allocation_data["amount"]), - ) - for allocation_data in payload["allocations"] - ] - - def _split_deposit_events(deposit_events): deposit_events = deposit_events if deposit_events is not None else [] diff --git a/backend/tests/helpers/__init__.py b/backend/tests/helpers/__init__.py index d9b0cbbc2f..c93de11a06 100644 --- a/backend/tests/helpers/__init__.py +++ b/backend/tests/helpers/__init__.py @@ -1,3 +1,4 @@ +from .allocations import make_user_allocation # noqa from .subgraph.events import ( # noqa create_epoch_event, generate_epoch_events, diff --git a/backend/tests/helpers/allocations.py b/backend/tests/helpers/allocations.py new file mode 100644 index 0000000000..e8417402a0 --- /dev/null +++ b/backend/tests/helpers/allocations.py @@ -0,0 +1,59 @@ +from typing import List +from random import randint + +from app.modules.dto import ( + AllocationItem, + UserAllocationPayload, + UserAllocationRequestPayload, +) +from app.infrastructure import database + + +def create_payload(proposals, amounts: list[int] | None, nonce: int = 0): + if amounts is None: + amounts = [randint(1 * 10**18, 1000 * 10**18) for _ in proposals] + + allocations = [ + { + "proposalAddress": proposal.address, + "amount": str(amount), + } + for proposal, amount in zip(proposals, amounts) + ] + + return {"allocations": allocations, "nonce": nonce} + + +def deserialize_allocations(payload) -> List[AllocationItem]: + return [ + AllocationItem( + proposal_address=allocation_data["proposalAddress"], + amount=int(allocation_data["amount"]), + ) + for allocation_data in payload["allocations"] + ] + + +def make_user_allocation(context, user, allocations=1, nonce=0, **kwargs): + projects = context.projects_details.projects + database.allocations.soft_delete_all_by_epoch_and_user_id( + context.epoch_details.epoch_num, user.id + ) + + allocation_items = [ + AllocationItem(projects[i], (i + 1) * 100) for i in range(allocations) + ] + + if kwargs.get("allocation_items"): + allocation_items = kwargs.get("allocation_items") + + request = UserAllocationRequestPayload( + payload=UserAllocationPayload(allocations=allocation_items, nonce=nonce), + signature="0xdeadbeef", + ) + + database.allocations.store_allocation_request( + user.address, context.epoch_details.epoch_num, request, **kwargs + ) + + return allocation_items diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py deleted file mode 100644 index 978f7ec484..0000000000 --- a/backend/tests/legacy/test_allocations.py +++ /dev/null @@ -1,311 +0,0 @@ -import pytest - -from app import exceptions -from app.infrastructure import database -from app.legacy.controllers.allocations import ( - allocate, -) -from app.legacy.core.allocations import ( - AllocationRequest, -) -from app.legacy.crypto.eip712 import sign, build_allocations_eip712_data -from tests.conftest import ( - create_payload, - deserialize_allocations, - mock_graphql, - MOCKED_PENDING_EPOCH_NO, - MOCK_PROPOSALS, - MOCK_GET_USER_BUDGET, -) -from tests.helpers import create_epoch_event - - -from app.modules.user.allocations import controller as new_controller - - -def get_allocation_nonce(user_address): - return new_controller.get_user_next_nonce(user_address) - - -def get_all_by_epoch(epoch, include_zeroes=False): - return new_controller.get_all_allocations(epoch) - - -@pytest.fixture(autouse=True) -def before( - app, - mocker, - graphql_client, - proposal_accounts, - patch_epochs, - patch_proposals, - patch_has_pending_epoch_snapshot, - patch_user_budget, -): - MOCK_PROPOSALS.get_proposal_addresses.return_value = [ - p.address for p in proposal_accounts[0:5] - ] - - mock_graphql( - mocker, epochs_events=[create_epoch_event(epoch=MOCKED_PENDING_EPOCH_NO)] - ) - - -def test_user_allocates_for_the_first_time(tos_users, proposal_accounts): - # Test data - payload = create_payload(proposal_accounts[0:2], None) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - - # Call allocate method - allocate(AllocationRequest(payload, signature, override_existing_allocations=True)) - - # Check if allocations were created - check_allocations(tos_users[0].address, payload, 2) - - # Check if threshold is properly calculated - check_allocation_threshold(payload) - - -def test_multiple_users_allocate_for_the_first_time(tos_users, proposal_accounts): - # Test data - payload1 = create_payload(proposal_accounts[0:2], None) - signature1 = sign(tos_users[0], build_allocations_eip712_data(payload1)) - - payload2 = create_payload(proposal_accounts[0:3], None) - signature2 = sign(tos_users[1], build_allocations_eip712_data(payload2)) - - # Call allocate method for both users - allocate( - AllocationRequest(payload1, signature1, override_existing_allocations=True) - ) - allocate( - AllocationRequest(payload2, signature2, override_existing_allocations=True) - ) - - # Check if allocations were created for both users - check_allocations(tos_users[0].address, payload1, 2) - check_allocations(tos_users[1].address, payload2, 3) - - # Check if threshold is properly calculated - check_allocation_threshold(payload1, payload2) - - -def test_allocate_updates_with_more_proposals(tos_users, proposal_accounts): - # Test data - initial_payload = create_payload(proposal_accounts[0:2], None, 0) - initial_signature = sign( - tos_users[0], build_allocations_eip712_data(initial_payload) - ) - - # Call allocate method - allocate( - AllocationRequest( - initial_payload, initial_signature, override_existing_allocations=True - ) - ) - - # Create a new payload with more proposals - updated_payload = create_payload(proposal_accounts[0:3], None, 1) - updated_signature = sign( - tos_users[0], build_allocations_eip712_data(updated_payload) - ) - - # Call allocate method with updated_payload - allocate( - AllocationRequest( - updated_payload, updated_signature, override_existing_allocations=True - ) - ) - - # Check if allocations were updated - check_allocations(tos_users[0].address, updated_payload, 3) - - # Check if threshold is properly calculated - check_allocation_threshold(updated_payload) - - -def test_allocate_updates_with_less_proposals(tos_users, proposal_accounts): - # Test data - initial_payload = create_payload(proposal_accounts[0:3], None, 0) - initial_signature = sign( - tos_users[0], build_allocations_eip712_data(initial_payload) - ) - - # Call allocate method - allocate( - AllocationRequest( - initial_payload, initial_signature, override_existing_allocations=True - ) - ) - - # Create a new payload with fewer proposals - updated_payload = create_payload(proposal_accounts[0:2], None, 1) - updated_signature = sign( - tos_users[0], build_allocations_eip712_data(updated_payload) - ) - - # Call allocate method with updated_payload - allocate( - AllocationRequest( - updated_payload, updated_signature, override_existing_allocations=True - ) - ) - - # Check if allocations were updated - check_allocations(tos_users[0].address, updated_payload, 2) - - # Check if threshold is properly calculated - check_allocation_threshold(updated_payload) - - -def test_multiple_users_change_their_allocations(tos_users, proposal_accounts): - # Create initial payloads and signatures for both users - initial_payload1 = create_payload(proposal_accounts[0:2], None, 0) - initial_signature1 = sign( - tos_users[0], build_allocations_eip712_data(initial_payload1) - ) - initial_payload2 = create_payload(proposal_accounts[0:3], None, 0) - initial_signature2 = sign( - tos_users[1], build_allocations_eip712_data(initial_payload2) - ) - - # Call allocate method with initial payloads for both users - allocate( - AllocationRequest( - initial_payload1, initial_signature1, override_existing_allocations=True - ) - ) - allocate( - AllocationRequest( - initial_payload2, initial_signature2, override_existing_allocations=True - ) - ) - - # Create updated payloads for both users - updated_payload1 = create_payload(proposal_accounts[0:4], None, 1) - updated_signature1 = sign( - tos_users[0], build_allocations_eip712_data(updated_payload1) - ) - updated_payload2 = create_payload(proposal_accounts[2:5], None, 1) - updated_signature2 = sign( - tos_users[1], build_allocations_eip712_data(updated_payload2) - ) - - # Call allocate method with updated payloads for both users - allocate( - AllocationRequest( - updated_payload1, updated_signature1, override_existing_allocations=True - ) - ) - allocate( - AllocationRequest( - updated_payload2, updated_signature2, override_existing_allocations=True - ) - ) - - # Check if allocations were updated for both users - check_allocations(tos_users[0].address, updated_payload1, 4) - check_allocations(tos_users[1].address, updated_payload2, 3) - - # Check if threshold is properly calculated - check_allocation_threshold(updated_payload1, updated_payload2) - - -def test_user_exceeded_rewards_budget_in_allocations(app, proposal_accounts, tos_users): - # Set some reasonable user rewards budget - MOCK_GET_USER_BUDGET.return_value = 100 * 10**18 - - # First payload sums up to 110 eth (budget is set to 100) - payload = create_payload( - proposal_accounts[0:3], [10 * 10**18, 50 * 10**18, 50 * 10**18] - ) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - - with pytest.raises(exceptions.RewardsBudgetExceeded): - allocate( - AllocationRequest(payload, signature, override_existing_allocations=True) - ) - - # Lower it to 100 total (should pass) - payload = create_payload( - proposal_accounts[0:3], [10 * 10**18, 40 * 10**18, 50 * 10**18] - ) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - allocate(AllocationRequest(payload, signature, override_existing_allocations=True)) - - -def test_nonces(tos_users, proposal_accounts): - nonce0 = get_allocation_nonce(tos_users[0].address) - payload = create_payload( - proposal_accounts[0:2], [10 * 10**18, 20 * 10**18], nonce0 - ) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - allocate(AllocationRequest(payload, signature, override_existing_allocations=True)) - nonce1 = get_allocation_nonce(tos_users[0].address) - assert nonce0 != nonce1 - payload = create_payload( - proposal_accounts[0:2], [10 * 10**18, 30 * 10**18], nonce1 - ) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - allocate(AllocationRequest(payload, signature, override_existing_allocations=True)) - nonce2 = get_allocation_nonce(tos_users[0].address) - assert nonce1 != nonce2 - - payload = create_payload( - proposal_accounts[0:2], [10 * 10**18, 10 * 10**18], nonce1 - ) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - with pytest.raises(exceptions.WrongAllocationsNonce): - allocate( - AllocationRequest(payload, signature, override_existing_allocations=True) - ) - - -def test_stores_allocation_request_signature(tos_users, proposal_accounts): - nonce0 = get_allocation_nonce(tos_users[0].address) - payload = create_payload( - proposal_accounts[0:2], [10 * 10**18, 20 * 10**18], nonce0 - ) - signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - - allocate(AllocationRequest(payload, signature, override_existing_allocations=True)) - - alloc_signature = database.allocations.get_allocation_request_by_user_nonce( - tos_users[0].address, nonce0 - ) - - assert alloc_signature is not None - - assert alloc_signature.epoch == MOCKED_PENDING_EPOCH_NO - assert alloc_signature.signature == signature - - -def check_allocations(user_address, expected_payload, expected_count): - epoch = MOCKED_PENDING_EPOCH_NO - expected_allocations = deserialize_allocations(expected_payload) - user = database.user.get_by_address(user_address) - assert user is not None - - db_allocations = database.allocations.get_all_by_epoch_and_user_id(epoch, user.id) - assert len(db_allocations) == expected_count - - for db_allocation, expected_allocation in zip(db_allocations, expected_allocations): - assert db_allocation.epoch == epoch - assert db_allocation.user_id == user.id - assert db_allocation.user is not None - assert db_allocation.proposal_address == expected_allocation.proposal_address - assert int(db_allocation.amount) == expected_allocation.amount - - -def check_allocation_threshold(*payloads): - epoch = MOCKED_PENDING_EPOCH_NO - expected = [deserialize_allocations(payload) for payload in payloads] - - db_allocations = database.allocations.get_all_by_epoch(epoch) - - total_allocations = sum([int(allocation.amount) for allocation in db_allocations]) - total_payload_allocations = sum( - [allocation.amount for allocations in expected for allocation in allocations] - ) - - assert total_allocations == total_payload_allocations diff --git a/backend/tests/legacy/test_rewards.py b/backend/tests/legacy/test_rewards.py index 8c23b9b4cf..b1800ac328 100644 --- a/backend/tests/legacy/test_rewards.py +++ b/backend/tests/legacy/test_rewards.py @@ -1,21 +1,17 @@ import pytest from app import exceptions -from app.legacy.controllers.allocations import allocate +from app.modules.user.allocations.controller import allocate +from app.legacy.crypto.eip712 import build_allocations_eip712_data, sign + from app.legacy.controllers.rewards import ( get_allocation_threshold, ) -from app.legacy.core.allocations import AllocationRequest from tests.conftest import ( MOCK_EPOCHS, - deserialize_allocations, MOCK_PROPOSALS, ) -from tests.legacy.test_allocations import ( - build_allocations_eip712_data, - create_payload, - sign, -) +from tests.helpers.allocations import create_payload, deserialize_allocations from app.modules.user.allocations import controller as new_controller @@ -28,6 +24,7 @@ def get_allocation_nonce(user_address): @pytest.fixture(autouse=True) def before( proposal_accounts, + mock_epoch_details, patch_epochs, patch_proposals, patch_has_pending_epoch_snapshot, @@ -64,12 +61,8 @@ def _allocate_random_individual_rewards(user_accounts, proposal_accounts) -> int signature2 = sign(user_accounts[1], build_allocations_eip712_data(payload2)) # Call allocate method for both users - allocate( - AllocationRequest(payload1, signature1, override_existing_allocations=True) - ) - allocate( - AllocationRequest(payload2, signature2, override_existing_allocations=True) - ) + allocate({"payload": payload1, "signature": signature1}) + allocate({"payload": payload2, "signature": signature2}) allocations1 = sum([int(a.amount) for a in deserialize_allocations(payload1)]) allocations2 = sum([int(a.amount) for a in deserialize_allocations(payload2)]) diff --git a/backend/tests/legacy/test_user.py b/backend/tests/legacy/test_user.py index 0112e657ed..d820351816 100644 --- a/backend/tests/legacy/test_user.py +++ b/backend/tests/legacy/test_user.py @@ -22,7 +22,7 @@ @pytest.fixture(autouse=True) -def before(app, patch_epochs, patch_proposals, patch_is_contract): +def before(app, patch_epochs, patch_proposals, patch_is_contract, mock_epoch_details): pass diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 5e94f8b0ce..8f9d79529f 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -102,13 +102,15 @@ def test_pending_services_factory(): result = PendingServices.create() events_based_patron_mode = EventsBasedUserPatronMode() + saved_user_budgets = SavedUserBudgets() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) user_allocations = PendingUserAllocations( - octant_rewards=octant_rewards, + user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, + octant_rewards=octant_rewards, ) user_rewards = CalculatedUserRewards( - user_budgets=SavedUserBudgets(), + user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, allocations=user_allocations, ) diff --git a/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py b/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py index 722fb987c9..8bd849f704 100644 --- a/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py +++ b/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py @@ -1,6 +1,7 @@ -from app.infrastructure import database -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationItem from app.modules.octant_rewards.service.finalized import FinalizedOctantRewards + +from tests.helpers import make_user_allocation from tests.helpers.constants import ( USER1_BUDGET, COMMUNITY_FUND, @@ -59,13 +60,13 @@ def test_finalized_get_leverage( proposal_accounts, mock_users_db, mock_finalized_epoch_snapshot_db ): user, _, _ = mock_users_db - database.allocations.add_all( - 1, - user.id, - 0, - [AllocationDTO(proposal_accounts[0].address, USER1_BUDGET)], - ) context = get_context() + make_user_allocation( + context, + user, + allocation_items=[AllocationItem(proposal_accounts[0].address, USER1_BUDGET)], + ) + service = FinalizedOctantRewards() result = service.get_leverage(context) diff --git a/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py b/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py index 8102a2a807..d56a0dc58f 100644 --- a/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py +++ b/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py @@ -1,7 +1,6 @@ from unittest.mock import Mock -from app.infrastructure import database -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationItem from app.modules.octant_rewards.service.pending import PendingOctantRewards from tests.helpers.constants import ( USER1_BUDGET, @@ -9,6 +8,7 @@ COMMUNITY_FUND, PPF, ) +from tests.helpers import make_user_allocation from tests.helpers.context import get_context from tests.helpers.pending_snapshot import create_pending_snapshot from tests.modules.octant_rewards.helpers.checker import check_octant_rewards @@ -75,14 +75,13 @@ def test_pending_get_leverage( proposal_accounts, mock_users_db, mock_pending_epoch_snapshot_db, mock_patron_mode ): user, _, _ = mock_users_db - database.allocations.add_all( - 1, - user.id, - 0, - [AllocationDTO(proposal_accounts[0].address, USER1_BUDGET)], - ) context = get_context() service = PendingOctantRewards(patrons_mode=mock_patron_mode) + make_user_allocation( + context, + user, + allocation_items=[AllocationItem(proposal_accounts[0].address, USER1_BUDGET)], + ) result = service.get_leverage(context) diff --git a/backend/tests/modules/project_rewards/test_estimated_rewards.py b/backend/tests/modules/project_rewards/test_estimated_rewards.py index 7a600b5787..1117d09d9b 100644 --- a/backend/tests/modules/project_rewards/test_estimated_rewards.py +++ b/backend/tests/modules/project_rewards/test_estimated_rewards.py @@ -1,8 +1,8 @@ import pytest -from app.infrastructure import database -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationItem from app.modules.project_rewards.service.estimated import EstimatedProjectRewards +from tests.helpers import make_user_allocation from tests.helpers.constants import USER1_BUDGET from tests.helpers.context import get_context @@ -35,11 +35,10 @@ def test_estimated_project_rewards_with_allocations( context = get_context(3) user, _, _ = mock_users_db - database.allocations.add_all( - 3, - user.id, - 0, - [AllocationDTO(proposal_accounts[0].address, USER1_BUDGET)], + make_user_allocation( + context, + user, + allocation_items=[AllocationItem(proposal_accounts[0].address, USER1_BUDGET)], ) service = EstimatedProjectRewards(octant_rewards=mock_octant_rewards) diff --git a/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py b/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py index ab0b7e3015..708dda4331 100644 --- a/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py +++ b/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py @@ -3,8 +3,9 @@ import pytest from app.infrastructure import database -from app.modules.dto import AllocationDTO +from app.modules.dto import AllocationItem from app.modules.snapshots.finalized.service.finalizing import FinalizingSnapshots +from tests.helpers import make_user_allocation from tests.helpers.constants import MATCHED_REWARDS, USER2_BUDGET from tests.helpers.context import get_context @@ -19,8 +20,8 @@ def test_create_finalized_snapshots_with_rewards( ): context = get_context(1) projects = context.projects_details.projects - database.allocations.add_all( - 1, mock_users_db[2].id, 0, [AllocationDTO(projects[0], 100)] + make_user_allocation( + context, mock_users_db[2], allocation_items=[AllocationItem(projects[0], 100)] ) service = FinalizingSnapshots( diff --git a/backend/tests/modules/snapshots/finalized/test_simulated_finalized_snapshots.py b/backend/tests/modules/snapshots/finalized/test_simulated_finalized_snapshots.py index bc636b8f17..2a6156a5eb 100644 --- a/backend/tests/modules/snapshots/finalized/test_simulated_finalized_snapshots.py +++ b/backend/tests/modules/snapshots/finalized/test_simulated_finalized_snapshots.py @@ -1,12 +1,12 @@ import pytest -from app.infrastructure import database -from app.modules.dto import AccountFundsDTO, AllocationDTO, ProjectAccountFundsDTO +from app.modules.dto import AccountFundsDTO, ProjectAccountFundsDTO from app.modules.snapshots.finalized.service.simulated import ( SimulatedFinalizedSnapshots, ) from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context +from tests.helpers import make_user_allocation @pytest.fixture(autouse=True) @@ -19,9 +19,7 @@ def test_simulate_finalized_snapshots( ): context = get_context(1) projects = context.projects_details.projects - database.allocations.add_all( - 1, mock_users_db[2].id, 0, [AllocationDTO(projects[0], 100)] - ) + make_user_allocation(context, mock_users_db[2]) service = SimulatedFinalizedSnapshots( patrons_mode=mock_patron_mode, diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index a9b6562802..db90320010 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -1,17 +1,57 @@ import pytest +from app import exceptions from app.engine.projects.rewards import ProjectRewardDTO -from app.infrastructure import database from app.context.epoch_state import EpochState +from app.infrastructure import database from app.modules.dto import AllocationDTO +from app.modules.user.allocations import controller from app.modules.user.allocations.service.pending import PendingUserAllocations + +from app.legacy.crypto.eip712 import sign, build_allocations_eip712_data + +from tests.conftest import ( + mock_graphql, + MOCKED_PENDING_EPOCH_NO, + MOCK_PROPOSALS, + MOCK_GET_USER_BUDGET, +) +from tests.helpers import create_epoch_event +from tests.helpers.allocations import ( + create_payload, + deserialize_allocations, + make_user_allocation, +) from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context +def get_allocation_nonce(user_address): + return controller.get_user_next_nonce(user_address) + + +def get_all_by_epoch(epoch, include_zeroes=False): + return controller.get_all_allocations(epoch) + + @pytest.fixture(autouse=True) -def before(app): - pass +def before( + app, + mocker, + graphql_client, + proposal_accounts, + patch_epochs, + patch_proposals, + patch_has_pending_epoch_snapshot, + patch_user_budget, +): + MOCK_PROPOSALS.get_proposal_addresses.return_value = [ + p.address for p in proposal_accounts[0:5] + ] + + mock_graphql( + mocker, epochs_events=[create_epoch_event(epoch=MOCKED_PENDING_EPOCH_NO)] + ) @pytest.fixture() @@ -27,10 +67,7 @@ def test_simulate_allocation(service, mock_users_db): user1, _, _ = mock_users_db context = get_context() projects = context.projects_details.projects - prev_allocation = [ - AllocationDTO(projects[0], 100_000000000), - ] - database.allocations.add_all(1, user1.id, 0, prev_allocation) + make_user_allocation(context, user1) next_allocations = [ AllocationDTO(projects[1], 200_000000000), @@ -56,17 +93,236 @@ def test_simulate_allocation(service, mock_users_db): ProjectRewardDTO(sorted_projects[9], 0, 0), ] + # but the allocation didn't change + assert service.get_user_allocation_sum(context, user1.address) == 100 + def test_revoke_previous_allocation(service, mock_users_db): user1, _, _ = mock_users_db context = get_context(epoch_state=EpochState.PENDING) + make_user_allocation(context, user1) - projects = context.projects_details.projects - prev_allocation = [ - AllocationDTO(projects[0], 100_000000000), - ] - database.allocations.add_all(1, user1.id, 0, prev_allocation) - - assert service.get_user_allocation_sum(context, user1.address) == 100_000000000 + assert service.get_user_allocation_sum(context, user1.address) == 100 service.revoke_previous_allocation(context, user1.address) assert service.get_user_allocation_sum(context, user1.address) == 0 + + +def test_user_allocates_for_the_first_time(tos_users, proposal_accounts): + # Test data + payload = create_payload(proposal_accounts[0:2], None) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + + # Call allocate method + controller.allocate({"payload": payload, "signature": signature}) + + # Check if allocations were created + check_allocations(tos_users[0].address, payload, 2) + + # Check if threshold is properly calculated + check_allocation_threshold(payload) + + +def test_multiple_users_allocate_for_the_first_time(tos_users, proposal_accounts): + # Test data + payload1 = create_payload(proposal_accounts[0:2], None) + signature1 = sign(tos_users[0], build_allocations_eip712_data(payload1)) + + payload2 = create_payload(proposal_accounts[0:3], None) + signature2 = sign(tos_users[1], build_allocations_eip712_data(payload2)) + + # Call allocate method for both users + controller.allocate({"payload": payload1, "signature": signature1}) + controller.allocate({"payload": payload2, "signature": signature2}) + + # Check if allocations were created for both users + check_allocations(tos_users[0].address, payload1, 2) + check_allocations(tos_users[1].address, payload2, 3) + + # Check if threshold is properly calculated + check_allocation_threshold(payload1, payload2) + + +def test_allocate_updates_with_more_proposals(tos_users, proposal_accounts): + # Test data + initial_payload = create_payload(proposal_accounts[0:2], None, 0) + initial_signature = sign( + tos_users[0], build_allocations_eip712_data(initial_payload) + ) + + # Call allocate method + controller.allocate({"payload": initial_payload, "signature": initial_signature}) + + # Create a new payload with more proposals + updated_payload = create_payload(proposal_accounts[0:3], None, 1) + updated_signature = sign( + tos_users[0], build_allocations_eip712_data(updated_payload) + ) + + # Call allocate method with updated_payload + controller.allocate({"payload": updated_payload, "signature": updated_signature}) + + # Check if allocations were updated + check_allocations(tos_users[0].address, updated_payload, 3) + + # Check if threshold is properly calculated + check_allocation_threshold(updated_payload) + + +def test_allocate_updates_with_less_proposals(tos_users, proposal_accounts): + # Test data + initial_payload = create_payload(proposal_accounts[0:3], None, 0) + initial_signature = sign( + tos_users[0], build_allocations_eip712_data(initial_payload) + ) + + # Call allocate method + controller.allocate({"payload": initial_payload, "signature": initial_signature}) + + # Create a new payload with fewer proposals + updated_payload = create_payload(proposal_accounts[0:2], None, 1) + updated_signature = sign( + tos_users[0], build_allocations_eip712_data(updated_payload) + ) + + # Call allocate method with updated_payload + controller.allocate({"payload": updated_payload, "signature": updated_signature}) + + # Check if allocations were updated + check_allocations(tos_users[0].address, updated_payload, 2) + + # Check if threshold is properly calculated + check_allocation_threshold(updated_payload) + + +def test_multiple_users_change_their_allocations(tos_users, proposal_accounts): + # Create initial payloads and signatures for both users + initial_payload1 = create_payload(proposal_accounts[0:2], None, 0) + initial_signature1 = sign( + tos_users[0], build_allocations_eip712_data(initial_payload1) + ) + initial_payload2 = create_payload(proposal_accounts[0:3], None, 0) + initial_signature2 = sign( + tos_users[1], build_allocations_eip712_data(initial_payload2) + ) + + # Call allocate method with initial payloads for both users + controller.allocate({"payload": initial_payload1, "signature": initial_signature1}) + controller.allocate({"payload": initial_payload2, "signature": initial_signature2}) + + # Create updated payloads for both users + updated_payload1 = create_payload(proposal_accounts[0:4], None, 1) + updated_signature1 = sign( + tos_users[0], build_allocations_eip712_data(updated_payload1) + ) + updated_payload2 = create_payload(proposal_accounts[2:5], None, 1) + updated_signature2 = sign( + tos_users[1], build_allocations_eip712_data(updated_payload2) + ) + + # Call allocate method with updated payloads for both users + controller.allocate({"payload": updated_payload1, "signature": updated_signature1}) + controller.allocate({"payload": updated_payload2, "signature": updated_signature2}) + + # Check if allocations were updated for both users + check_allocations(tos_users[0].address, updated_payload1, 4) + check_allocations(tos_users[1].address, updated_payload2, 3) + + # Check if threshold is properly calculated + check_allocation_threshold(updated_payload1, updated_payload2) + + +def test_user_exceeded_rewards_budget_in_allocations(app, proposal_accounts, tos_users): + # Set some reasonable user rewards budget + MOCK_GET_USER_BUDGET.return_value = 100 * 10**18 + + # First payload sums up to 110 eth (budget is set to 100) + payload = create_payload( + proposal_accounts[0:3], [10 * 10**18, 50 * 10**18, 50 * 10**18] + ) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + + with pytest.raises(exceptions.RewardsBudgetExceeded): + controller.allocate({"payload": payload, "signature": signature}) + + # Lower it to 100 total (should pass) + payload = create_payload( + proposal_accounts[0:3], [10 * 10**18, 40 * 10**18, 50 * 10**18] + ) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + controller.allocate({"payload": payload, "signature": signature}) + + +def test_nonces(tos_users, proposal_accounts): + nonce0 = get_allocation_nonce(tos_users[0].address) + payload = create_payload( + proposal_accounts[0:2], [10 * 10**18, 20 * 10**18], nonce0 + ) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + controller.allocate({"payload": payload, "signature": signature}) + nonce1 = get_allocation_nonce(tos_users[0].address) + assert nonce0 != nonce1 + payload = create_payload( + proposal_accounts[0:2], [10 * 10**18, 30 * 10**18], nonce1 + ) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + controller.allocate({"payload": payload, "signature": signature}) + + nonce2 = get_allocation_nonce(tos_users[0].address) + assert nonce1 != nonce2 + + payload = create_payload( + proposal_accounts[0:2], [10 * 10**18, 10 * 10**18], nonce1 + ) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + with pytest.raises(exceptions.WrongAllocationsNonce): + controller.allocate({"payload": payload, "signature": signature}) + + +def test_stores_allocation_request_signature(tos_users, proposal_accounts): + nonce0 = get_allocation_nonce(tos_users[0].address) + payload = create_payload( + proposal_accounts[0:2], [10 * 10**18, 20 * 10**18], nonce0 + ) + signature = sign(tos_users[0], build_allocations_eip712_data(payload)) + + controller.allocate({"payload": payload, "signature": signature}) + + alloc_signature = database.allocations.get_allocation_request_by_user_nonce( + tos_users[0].address, nonce0 + ) + + assert alloc_signature is not None + + assert alloc_signature.epoch == MOCKED_PENDING_EPOCH_NO + assert alloc_signature.signature == signature + + +def check_allocations(user_address, expected_payload, expected_count): + epoch = MOCKED_PENDING_EPOCH_NO + expected_allocations = deserialize_allocations(expected_payload) + user = database.user.get_by_address(user_address) + assert user is not None + + db_allocations = database.allocations.get_all_by_epoch_and_user_id(epoch, user.id) + assert len(db_allocations) == expected_count + + for db_allocation, expected_allocation in zip(db_allocations, expected_allocations): + assert db_allocation.epoch == epoch + assert db_allocation.user_id == user.id + assert db_allocation.user is not None + assert db_allocation.proposal_address == expected_allocation.proposal_address + assert int(db_allocation.amount) == expected_allocation.amount + + +def check_allocation_threshold(*payloads): + epoch = MOCKED_PENDING_EPOCH_NO + expected = [deserialize_allocations(payload) for payload in payloads] + + db_allocations = database.allocations.get_all(epoch) + + total_allocations = sum([int(allocation.amount) for allocation in db_allocations]) + total_payload_allocations = sum( + [allocation.amount for allocations in expected for allocation in allocations] + ) + + assert total_allocations == total_payload_allocations diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index 0b93e6a9e9..d0acbf4f64 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -13,6 +13,7 @@ from app.modules.history.dto import AllocationItem as HistoryAllocationItem from tests.helpers.context import get_context +from tests.helpers import make_user_allocation @pytest.fixture(autouse=True) @@ -25,35 +26,6 @@ def service(): return SavedUserAllocations() -@pytest.fixture() -def make_user_allocation(proposal_accounts): - def _make_user_allocation(context, user, allocations=1, nonce=0, **kwargs): - database.allocations.soft_delete_all_by_epoch_and_user_id( - context.epoch_details.epoch_num, user.id - ) - - allocation_items = [ - AllocationItem(proposal_accounts[i].address, (i + 1) * 100) - for i in range(allocations) - ] - - if kwargs.get("allocation_items"): - allocation_items = kwargs.get("allocation_items") - - request = UserAllocationRequestPayload( - payload=UserAllocationPayload(allocations=allocation_items, nonce=nonce), - signature="0xdeadbeef", - ) - - database.allocations.store_allocation_request( - user.address, context.epoch_details.epoch_num, request, **kwargs - ) - - return allocation_items - - return _make_user_allocation - - def _alloc_item_to_donation(item, user): return ProposalDonationDTO(user.address, item.amount, item.proposal_address) @@ -152,7 +124,7 @@ def test_user_nonce_is_continuous_despite_epoch_changes(service, mock_users_db): assert new_nonce == 3 -def test_get_all_donors_addresses(service, mock_users_db, make_user_allocation): +def test_get_all_donors_addresses(service, mock_users_db): user1, user2, user3 = mock_users_db context_epoch_1 = get_context(1) context_epoch_2 = get_context(2) @@ -168,9 +140,7 @@ def test_get_all_donors_addresses(service, mock_users_db, make_user_allocation): assert result_epoch_2 == [user3.address] -def test_return_only_not_removed_allocations( - service, mock_users_db, make_user_allocation -): +def test_return_only_not_removed_allocations(service, mock_users_db): user1, user2, _ = mock_users_db context = get_context(1) @@ -183,7 +153,7 @@ def test_return_only_not_removed_allocations( assert result == [user1.address] -def test_get_user_allocation_sum(service, context, mock_users_db, make_user_allocation): +def test_get_user_allocation_sum(service, context, mock_users_db): user1, user2, _ = mock_users_db make_user_allocation(context, user1, allocations=2) make_user_allocation(context, user2, allocations=2) @@ -193,9 +163,7 @@ def test_get_user_allocation_sum(service, context, mock_users_db, make_user_allo assert result == 300 -def test_has_user_allocated_rewards( - service, context, mock_users_db, make_user_allocation -): +def test_has_user_allocated_rewards(service, context, mock_users_db): user1, _, _ = mock_users_db make_user_allocation(context, user1) @@ -204,9 +172,7 @@ def test_has_user_allocated_rewards( assert result is True -def test_has_user_allocated_rewards_returns_false( - service, context, mock_users_db, make_user_allocation -): +def test_has_user_allocated_rewards_returns_false(service, context, mock_users_db): user1, user2, _ = mock_users_db make_user_allocation(context, user1) # other user makes an allocation @@ -218,7 +184,7 @@ def test_has_user_allocated_rewards_returns_false( @freeze_time("2024-03-18 00:00:00") def test_user_allocations_by_timestamp( - service, context, mock_users_db, proposal_accounts, make_user_allocation + service, context, mock_users_db, proposal_accounts ): user1, _, _ = mock_users_db timestamp_before = from_timestamp_s(1710719999) @@ -274,7 +240,7 @@ def test_get_all_allocations_returns_empty_list_when_no_allocations( def test_get_all_allocations_returns_list_of_allocations( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, user2, _ = mock_users_db @@ -292,7 +258,7 @@ def test_get_all_allocations_returns_list_of_allocations( def test_get_all_allocations_does_not_include_revoked_allocations_in_returned_list( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, user2, _ = mock_users_db @@ -312,7 +278,7 @@ def test_get_all_allocations_does_not_include_revoked_allocations_in_returned_li def test_get_all_allocations_does_not_return_allocations_from_previous_and_future_epochs( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, _, _ = mock_users_db context_epoch_1 = get_context(1) @@ -326,7 +292,7 @@ def test_get_all_allocations_does_not_return_allocations_from_previous_and_futur def test_get_all_with_allocation_amount_equal_0( - service, context, mock_users_db, proposal_accounts, make_user_allocation + service, context, mock_users_db, proposal_accounts ): user1, _, _ = mock_users_db allocation_items = [AllocationItem(proposal_accounts[0].address, 0)] @@ -336,14 +302,12 @@ def test_get_all_with_allocation_amount_equal_0( assert service.get_all_allocations(context) == expected_result -def test_get_last_user_allocation_when_no_allocation( - service, context, alice, make_user_allocation -): +def test_get_last_user_allocation_when_no_allocation(service, context, alice): assert service.get_last_user_allocation(context, alice.address) == ([], None) def test_get_last_user_allocation_returns_the_only_allocation( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, _, _ = mock_users_db expected_result = make_user_allocation(context, user1) @@ -355,7 +319,7 @@ def test_get_last_user_allocation_returns_the_only_allocation( def test_get_last_user_allocation_returns_the_only_the_last_allocation( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, _, _ = mock_users_db _ = make_user_allocation(context, user1) @@ -368,7 +332,7 @@ def test_get_last_user_allocation_returns_the_only_the_last_allocation( def test_get_last_user_allocation_returns_stored_metadata( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, _, _ = mock_users_db @@ -395,7 +359,7 @@ def test_get_allocations_by_project_returns_empty_list_when_no_allocations( def test_get_allocations_by_project_returns_list_of_donations_per_project( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, user2, _ = mock_users_db project1, project2 = ( @@ -427,7 +391,7 @@ def test_get_allocations_by_project_returns_list_of_donations_per_project( def test_get_allocations_by_project_with_allocation_amount_equal_0( - service, context, mock_users_db, make_user_allocation + service, context, mock_users_db ): user1, _, _ = mock_users_db project1 = context.projects_details.projects[0] From 8d09e48fc7068777d75f18dfe81dba222e4bd402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Miko=C5=82ajczyk?= Date: Wed, 27 Mar 2024 13:19:26 +0100 Subject: [PATCH 050/107] oct-1482: epoch container fix --- .../SettingsMainInfoBox/SettingsMainInfoBox.tsx | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx b/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx index f170fc8b02..e1e7918a71 100644 --- a/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx +++ b/client/src/components/Settings/SettingsMainInfoBox/SettingsMainInfoBox.tsx @@ -25,14 +25,12 @@ const SettingsMainInfoBox = (): ReactNode => { isVertical textAlign="left" > -
- -
{t('epoch', { epoch: currentEpoch })}
-
+ +
{t('epoch', { epoch: currentEpoch })}
{t('golemFoundationProject')} From 9953344429907154bd600f5f7a8ad11cc1b823b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 27 Mar 2024 10:36:16 +0100 Subject: [PATCH 051/107] chore: clean up tests --- .../tests/modules/user/allocations/test_pending_allocations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index db90320010..a82bbc9422 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -24,6 +24,7 @@ ) from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context +from tests.helpers import make_user_allocation def get_allocation_nonce(user_address): From 500d9b5a02dd4d5de551c76849116523318a21e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Tue, 19 Mar 2024 12:46:34 +0100 Subject: [PATCH 052/107] feat: automatically canonize user address arg in routers --- backend/app/infrastructure/__init__.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/backend/app/infrastructure/__init__.py b/backend/app/infrastructure/__init__.py index 11f65eb8de..5e61c8c8e9 100644 --- a/backend/app/infrastructure/__init__.py +++ b/backend/app/infrastructure/__init__.py @@ -1,5 +1,7 @@ from flask_restx import Resource +from eth_utils import to_checksum_address + from gql import Client from gql.transport.requests import RequestsHTTPTransport @@ -19,12 +21,29 @@ class OctantResource(Resource): def __init__(self, *args, **kwargs): Resource.__init__(self, *args, *kwargs) + @classmethod + def canonize_address(cls, field_name: str = "user_address", force=True): + def _add_address_canonization(handler): + def _decorated(*args, **kwargs): + field_value = kwargs.get(field_name) + if force or field_value is not None: + updated_field = to_checksum_address(field_value) + kwargs.update([(field_name, updated_field)]) + + return handler(*args, **kwargs) + + return _decorated + + return _add_address_canonization + def __getattribute__(self, name): + user_address_canonizer = OctantResource.canonize_address(force=False) + attr = object.__getattribute__(self, name) decorator = default_decorators.get(name) if decorator is not None: - attr = decorator(attr) + attr = user_address_canonizer(decorator(attr)) return attr From 3c385fea64f902fdd31d63395ea46dc735cd6578 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20=C5=BBelazko?= Date: Wed, 27 Mar 2024 10:36:16 +0100 Subject: [PATCH 053/107] chore: clean up tests --- backend/tests/legacy/test_allocations.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 backend/tests/legacy/test_allocations.py diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py new file mode 100644 index 0000000000..e69de29bb2 From e0651cf8fa1a01262bb51100ca26a9f1bf15dde2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Wed, 20 Mar 2024 16:15:34 +0100 Subject: [PATCH 054/107] feat: add msg signatures model, route and controller --- backend/app/infrastructure/database/models.py | 11 ++++ .../routes/multisig_signatures.py | 52 +++++++++++++++++++ .../modules/multisig_signatures/controller.py | 18 +++++++ 3 files changed, 81 insertions(+) create mode 100644 backend/app/infrastructure/routes/multisig_signatures.py create mode 100644 backend/app/modules/multisig_signatures/controller.py diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index 5abc9b6406..c333ae877d 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -159,3 +159,14 @@ class EpochZeroClaim(BaseModel): address = Column(db.String(42), primary_key=True, nullable=False) claimed = Column(db.Boolean, default=False) claim_nonce = db.Column(db.Integer(), unique=True, nullable=True) + + +class MultisigSignatures(BaseModel): + __tablename__ = "multisig_signatures" + + id = Column(db.Integer, primary_key=True) + address = Column(db.String(42), nullable=False) + type = Column(db.String, nullable=False) + message = Column(db.String, nullable=False) + hash = Column(db.String, nullable=False) + status = Column(db.String, nullable=False) diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py new file mode 100644 index 0000000000..6ebf49c43a --- /dev/null +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -0,0 +1,52 @@ +from flask import current_app as app +from flask_restx import Namespace, fields + +from app.extensions import api +from app.infrastructure import OctantResource +from app.modules.multisig_signatures.controller import ( + get_last_pending_signature, + save_pending_signature, +) + +ns = Namespace( + "multisig-signatures", + description="Information about multisig signatures stored in Octant.", +) +api.add_namespace(ns) + +pending_signature = api.model( + "PendingSignature", + { + "message": fields.String(description="The message to be signed."), + "hash": fields.String(description="The hash of the message."), + }, +) + + +@ns.route("/pending//type/") +class MultisigPendingSignature(OctantResource): + @ns.marshal_with(pending_signature) + @ns.response(200, "Success") + @ns.doc( + description="Retrieve last pending multisig signature for a specific user and type." + ) + def get(self, user_address: str, op_type: str): + app.logger.debug( + f"Retrieving last pending multisig signature for user {user_address} and type {op_type}." + ) + response = get_last_pending_signature(user_address, op_type) + app.logger.debug(f"Retrieved last pending multisig signature {response}.") + + return response + + @ns.expect(pending_signature) + @ns.response(201, "Success") + def post(self, user_address: str, op_type: str): + app.logger.debug( + f"Adding new multisig signature for user {user_address} and type {op_type}." + ) + signature_data = api.payload + save_pending_signature(user_address, op_type, signature_data) + app.logger.debug(f"Added new multisig signature.") + + return {}, 201 diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py new file mode 100644 index 0000000000..f2c8d4241c --- /dev/null +++ b/backend/app/modules/multisig_signatures/controller.py @@ -0,0 +1,18 @@ +from dataclasses import dataclass + +from dataclass_wizard import JSONWizard + + +@dataclass(frozen=True) +class Signature(JSONWizard): + message: str + hash: str + + +def get_last_pending_signature(user_address: str, op_type) -> Signature: + return Signature(message="message", hash="hash") + + +def save_pending_signature(user_address: str, op_type, signature_data: dict): + signature = Signature.from_dict(signature_data) + ... From c13a021313184599fca9fc2a67ea008b1df5c049 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Thu, 21 Mar 2024 11:53:36 +0100 Subject: [PATCH 055/107] feat: controller impl and service template --- .../modules/multisig_signatures/controller.py | 30 +++++++++++-------- .../app/modules/multisig_signatures/dto.py | 15 ++++++++++ .../multisig_signatures/service/offchain.py | 20 +++++++++++++ .../user/allocations/service/history.py | 0 4 files changed, 53 insertions(+), 12 deletions(-) create mode 100644 backend/app/modules/multisig_signatures/dto.py create mode 100644 backend/app/modules/multisig_signatures/service/offchain.py delete mode 100644 backend/app/modules/user/allocations/service/history.py diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py index f2c8d4241c..6cb60263bb 100644 --- a/backend/app/modules/multisig_signatures/controller.py +++ b/backend/app/modules/multisig_signatures/controller.py @@ -1,18 +1,24 @@ -from dataclasses import dataclass +from app.context.epoch_state import EpochState +from app.context.manager import state_context +from app.modules.multisig_signatures.dto import Signature, SignatureOpType +from app.modules.registry import get_services -from dataclass_wizard import JSONWizard +def get_last_pending_signature( + user_address: str, op_type: SignatureOpType +) -> Signature: + context = state_context(EpochState.CURRENT) + service = get_services(context.epoch_state).multisig_signatures_service -@dataclass(frozen=True) -class Signature(JSONWizard): - message: str - hash: str + return service.get_last_pending_signature(context, user_address, op_type) -def get_last_pending_signature(user_address: str, op_type) -> Signature: - return Signature(message="message", hash="hash") +def save_pending_signature( + user_address: str, op_type: SignatureOpType, signature_data: dict +): + context = state_context(EpochState.CURRENT) + service = get_services(context.epoch_state).multisig_signatures_service - -def save_pending_signature(user_address: str, op_type, signature_data: dict): - signature = Signature.from_dict(signature_data) - ... + return service.save_pending_signature( + context, user_address, op_type, signature_data + ) diff --git a/backend/app/modules/multisig_signatures/dto.py b/backend/app/modules/multisig_signatures/dto.py new file mode 100644 index 0000000000..4720e7b642 --- /dev/null +++ b/backend/app/modules/multisig_signatures/dto.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from enum import StrEnum + +from dataclass_wizard import JSONWizard + + +class SignatureOpType(StrEnum): + TOS = "tos" + ALLOCATION = "allocation" + + +@dataclass(frozen=True) +class Signature(JSONWizard): + message: str + hash: str diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py new file mode 100644 index 0000000000..d2c6e21e2f --- /dev/null +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -0,0 +1,20 @@ +from app.context.manager import Context +from app.modules.multisig_signatures.dto import Signature, SignatureOpType +from app.pydantic import Model + + +class CalculatedOctantRewards(Model): + def get_last_pending_signature( + self, context: Context, user_address: str, op_type: SignatureOpType + ) -> Signature: + return Signature(message="message", hash="hash") + + def save_pending_signature( + self, + context: Context, + user_address: str, + op_type: SignatureOpType, + signature_data: dict, + ): + signature = Signature.from_dict(signature_data) + ... diff --git a/backend/app/modules/user/allocations/service/history.py b/backend/app/modules/user/allocations/service/history.py deleted file mode 100644 index e69de29bb2..0000000000 From 25a1adb0a9296e75bf409bcbbd6e46ae0634760e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Thu, 21 Mar 2024 14:11:44 +0100 Subject: [PATCH 056/107] feat: implement get_last_pending_signature --- .../app/infrastructure/database/__init__.py | 1 + .../database/multisig_signature.py | 42 ++++++++++++ .../routes/multisig_signatures.py | 2 +- .../app/modules/modules_factory/current.py | 5 ++ .../app/modules/modules_factory/protocols.py | 18 +++++ .../multisig_signatures/service/offchain.py | 20 ++++-- .../modules_factory/test_modules_factory.py | 4 ++ .../test_offchain_multisig_signatures.py | 65 +++++++++++++++++++ 8 files changed, 151 insertions(+), 6 deletions(-) create mode 100644 backend/app/infrastructure/database/multisig_signature.py create mode 100644 backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py diff --git a/backend/app/infrastructure/database/__init__.py b/backend/app/infrastructure/database/__init__.py index 097ccbb950..e7f80cd7f5 100644 --- a/backend/app/infrastructure/database/__init__.py +++ b/backend/app/infrastructure/database/__init__.py @@ -10,4 +10,5 @@ patrons, claims, user_consents, + multisig_signature, ) diff --git a/backend/app/infrastructure/database/multisig_signature.py b/backend/app/infrastructure/database/multisig_signature.py new file mode 100644 index 0000000000..ebfc6dc167 --- /dev/null +++ b/backend/app/infrastructure/database/multisig_signature.py @@ -0,0 +1,42 @@ +from datetime import datetime +from enum import StrEnum +from typing import Optional + +from app.extensions import db +from app.infrastructure.database.models import MultisigSignatures +from app.modules.multisig_signatures.dto import SignatureOpType + + +class SigStatus(StrEnum): + PENDING = "pending" + APPROVED = "approved" + + +def get_last_pending_signature( + user_address: str, op_type: SignatureOpType, dt: Optional[datetime] = None +) -> MultisigSignatures | None: + last_signature = MultisigSignatures.query.filter_by( + address=user_address, type=op_type, status=SigStatus.PENDING + ).order_by(MultisigSignatures.created_at.desc()) + + if dt is not None: + last_signature.filter(MultisigSignatures.created_at <= dt) + + return last_signature.first() + + +def save_signature( + user_address: str, + op_type: SignatureOpType, + message: str, + msg_hash: str, + status: SigStatus = SigStatus.PENDING, +): + signature = MultisigSignatures( + address=user_address, + type=op_type, + message=message, + hash=msg_hash, + status=status, + ) + db.session.add(signature) diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py index 6ebf49c43a..0209f6e327 100644 --- a/backend/app/infrastructure/routes/multisig_signatures.py +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -47,6 +47,6 @@ def post(self, user_address: str, op_type: str): ) signature_data = api.payload save_pending_signature(user_address, op_type, signature_data) - app.logger.debug(f"Added new multisig signature.") + app.logger.debug("Added new multisig signature.") return {}, 201 diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index 35a52938ed..347030452d 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -8,8 +8,10 @@ UserEffectiveDeposits, TotalEffectiveDeposits, HistoryService, + MultisigSignatures, ) from app.modules.modules_factory.protocols import SimulatePendingSnapshots +from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures from app.modules.octant_rewards.service.calculated import CalculatedOctantRewards from app.modules.snapshots.pending.service.simulated import SimulatedPendingSnapshots from app.modules.staking.proceeds.service.estimated import EstimatedStakingProceeds @@ -34,6 +36,7 @@ class CurrentServices(Model): octant_rewards_service: OctantRewards history_service: HistoryService simulated_pending_snapshot_service: SimulatePendingSnapshots + multisig_signatures_service: MultisigSignatures @staticmethod def _prepare_simulation_data( @@ -70,6 +73,7 @@ def create(chain_id: int) -> "CurrentServices": user_withdrawals=user_withdrawals, patron_donations=patron_donations, ) + multisig_signatures = OffchainMultisigSignatures() return CurrentServices( user_allocations_service=user_allocations, user_deposits_service=user_deposits, @@ -79,4 +83,5 @@ def create(chain_id: int) -> "CurrentServices": ), history_service=history, simulated_pending_snapshot_service=simulated_pending_snapshot_service, + multisig_signatures_service=multisig_signatures, ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index b0c525b72d..c6b5d72eb9 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -14,6 +14,7 @@ UserAllocationRequestPayload, ) from app.modules.history.dto import UserHistoryDTO +from app.modules.multisig_signatures.dto import SignatureOpType, Signature @runtime_checkable @@ -159,3 +160,20 @@ def get_user_history( self, context: Context, user_address: str, cursor: str = None, limit: int = 20 ) -> UserHistoryDTO: ... + + +@runtime_checkable +class MultisigSignatures(Protocol): + def get_last_pending_signature( + self, context: Context, user_address: str, op_type: SignatureOpType + ) -> Signature: + ... + + def save_pending_signature( + self, + context: Context, + user_address: str, + op_type: SignatureOpType, + signature_data: dict, + ): + ... diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index d2c6e21e2f..d8e45e177c 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -1,13 +1,24 @@ from app.context.manager import Context +from app.infrastructure import database from app.modules.multisig_signatures.dto import Signature, SignatureOpType from app.pydantic import Model -class CalculatedOctantRewards(Model): +class OffchainMultisigSignatures(Model): def get_last_pending_signature( - self, context: Context, user_address: str, op_type: SignatureOpType - ) -> Signature: - return Signature(message="message", hash="hash") + self, _: Context, user_address: str, op_type: SignatureOpType + ) -> Signature | None: + signature_db = database.multisig_signature.get_last_pending_signature( + user_address, op_type + ) + + if signature_db is None: + return None + + return Signature( + message=signature_db.message, + hash=signature_db.hash, + ) def save_pending_signature( self, @@ -16,5 +27,4 @@ def save_pending_signature( op_type: SignatureOpType, signature_data: dict, ): - signature = Signature.from_dict(signature_data) ... diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 8f9d79529f..9b0c78dba0 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -5,6 +5,7 @@ from app.modules.modules_factory.future import FutureServices from app.modules.modules_factory.pending import PendingServices from app.modules.modules_factory.pre_pending import PrePendingServices +from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures from app.modules.octant_rewards.service.calculated import CalculatedOctantRewards from app.modules.octant_rewards.service.finalized import FinalizedOctantRewards from app.modules.octant_rewards.service.pending import PendingOctantRewards @@ -63,9 +64,12 @@ def test_current_services_factory(): user_withdrawals=user_withdrawals, patron_donations=patron_donations, ) + multisig_signatures = OffchainMultisigSignatures() + assert result.user_deposits_service == user_deposits assert result.octant_rewards_service == octant_rewards assert result.history_service == history + assert result.multisig_signatures_service == multisig_signatures def test_pre_pending_services_factory_when_mainnet(): diff --git a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py new file mode 100644 index 0000000000..b54dc07bc0 --- /dev/null +++ b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py @@ -0,0 +1,65 @@ +import pytest + +from app.extensions import db +from app.infrastructure import database +from app.infrastructure.database.multisig_signature import SigStatus +from app.modules.multisig_signatures.dto import Signature, SignatureOpType +from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures + + +@pytest.fixture(autouse=True) +def before(app): + pass + + +def test_get_last_pending_signature_returns_expected_signature_when_signature_exists( + context, alice, bob +): + # Given + database.multisig_signature.save_signature( + alice.address, + SignatureOpType.ALLOCATION, + "last pending msg", + "last pending hash", + ) + database.multisig_signature.save_signature( + alice.address, + SignatureOpType.ALLOCATION, + "test_message", + "test_hash", + status=SigStatus.APPROVED, + ) + database.multisig_signature.save_signature( + alice.address, SignatureOpType.TOS, "test_message", "test_hash" + ) + database.multisig_signature.save_signature( + bob.address, SignatureOpType.ALLOCATION, "test_message", "test_hash" + ) + db.session.commit() + + service = OffchainMultisigSignatures() + + # When + result = service.get_last_pending_signature( + context, alice.address, SignatureOpType.ALLOCATION + ) + + # Then + assert isinstance(result, Signature) + assert result.message == "last pending msg" + assert result.hash == "last pending hash" + + +def test_get_last_pending_signature_returns_none_when_no_signature_exists( + context, alice +): + # Given + service = OffchainMultisigSignatures() + + # When + result = service.get_last_pending_signature( + context, alice.address, SignatureOpType.ALLOCATION + ) + + # Then + assert result is None From a8adbaca0a7366984aaeebe1d5b8ba20fda434ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Fri, 22 Mar 2024 08:51:28 +0100 Subject: [PATCH 057/107] feat: move user tos to new arch --- .../routes/multisig_signatures.py | 7 +- backend/app/infrastructure/routes/user.py | 16 +- backend/app/legacy/controllers/user.py | 14 -- backend/app/legacy/core/user/tos.py | 20 --- backend/app/modules/common/signature.py | 31 ++++ .../app/modules/modules_factory/current.py | 5 + .../app/modules/modules_factory/protocols.py | 17 ++ .../modules/multisig_signatures/controller.py | 13 +- backend/app/modules/user/tos/controller.py | 16 ++ .../user/tos/core.py} | 4 +- backend/app/modules/user/tos/service/basic.py | 30 ++++ backend/tests/conftest.py | 6 +- backend/tests/helpers/signature.py | 14 ++ backend/tests/legacy/test_user_consents.py | 145 ------------------ .../modules_factory/test_modules_factory.py | 3 + .../modules/user/tos/test_basic_user_tos.py | 101 ++++++++++++ .../tests/modules/user/tos/test_tos_core.py | 55 +++++++ 17 files changed, 297 insertions(+), 200 deletions(-) delete mode 100644 backend/app/legacy/core/user/tos.py create mode 100644 backend/app/modules/common/signature.py create mode 100644 backend/app/modules/user/tos/controller.py rename backend/app/{legacy/crypto/eth_sign/terms_and_conditions_consent.py => modules/user/tos/core.py} (79%) create mode 100644 backend/app/modules/user/tos/service/basic.py create mode 100644 backend/tests/helpers/signature.py delete mode 100644 backend/tests/legacy/test_user_consents.py create mode 100644 backend/tests/modules/user/tos/test_basic_user_tos.py create mode 100644 backend/tests/modules/user/tos/test_tos_core.py diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py index 0209f6e327..f4f1ddf11d 100644 --- a/backend/app/infrastructure/routes/multisig_signatures.py +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -7,6 +7,7 @@ get_last_pending_signature, save_pending_signature, ) +from app.modules.multisig_signatures.dto import SignatureOpType ns = Namespace( "multisig-signatures", @@ -34,7 +35,7 @@ def get(self, user_address: str, op_type: str): app.logger.debug( f"Retrieving last pending multisig signature for user {user_address} and type {op_type}." ) - response = get_last_pending_signature(user_address, op_type) + response = get_last_pending_signature(user_address, SignatureOpType(op_type)) app.logger.debug(f"Retrieved last pending multisig signature {response}.") return response @@ -45,8 +46,8 @@ def post(self, user_address: str, op_type: str): app.logger.debug( f"Adding new multisig signature for user {user_address} and type {op_type}." ) - signature_data = api.payload - save_pending_signature(user_address, op_type, signature_data) + message = api.payload + save_pending_signature(user_address, SignatureOpType(op_type), message) app.logger.debug("Added new multisig signature.") return {}, 201 diff --git a/backend/app/infrastructure/routes/user.py b/backend/app/infrastructure/routes/user.py index 326f04c3e9..c6a59d0141 100644 --- a/backend/app/infrastructure/routes/user.py +++ b/backend/app/infrastructure/routes/user.py @@ -1,16 +1,18 @@ +from eth_utils import to_checksum_address from flask import current_app as app, request from flask_restx import Namespace, fields from flask_restx import reqparse -from eth_utils import to_checksum_address - import app.legacy.controllers.user as user_controller from app.extensions import api from app.infrastructure import OctantResource from app.modules.user.patron_mode.controller import get_patrons_addresses +from app.modules.user.tos.controller import ( + post_user_terms_of_service_consent, + get_user_terms_of_service_consent_status, +) from app.settings import config - ns = Namespace("user", description="Octant user settings") api.add_namespace(ns) @@ -81,9 +83,7 @@ class TermsOfService(OctantResource): @ns.response(200, "User's consent to Terms of Service status retrieved") def get(self, user_address): app.logger.debug(f"Getting user {user_address} ToS consent status") - consent_status = user_controller.get_user_terms_of_service_consent_status( - user_address - ) + consent_status = get_user_terms_of_service_consent_status(user_address) app.logger.debug(f"User {user_address} ToS consent status: {consent_status}") return {"accepted": consent_status} @@ -107,9 +107,7 @@ def post(self, user_address): else: user_ip = request.remote_addr - user_controller.post_user_terms_of_service_consent( - user_address, signature, user_ip - ) + post_user_terms_of_service_consent(user_address, signature, user_ip) app.logger.info(f"User {user_address} ToS consent status updated") return {"accepted": True}, 201 diff --git a/backend/app/legacy/controllers/user.py b/backend/app/legacy/controllers/user.py index afc08ee2d7..691d6587cc 100644 --- a/backend/app/legacy/controllers/user.py +++ b/backend/app/legacy/controllers/user.py @@ -4,23 +4,9 @@ from app.extensions import db from app.modules.user.allocations import controller as allocations_controller from app.legacy.core.user import patron_mode as patron_mode_core -from app.legacy.core.user.tos import ( - has_user_agreed_to_terms_of_service, - add_user_terms_of_service_consent, -) from app.legacy.crypto.eth_sign import patron_mode as patron_mode_crypto -def get_user_terms_of_service_consent_status(user_address: str) -> bool: - return has_user_agreed_to_terms_of_service(user_address) - - -def post_user_terms_of_service_consent(user_address: str, signature: str, user_ip: str): - add_user_terms_of_service_consent(user_address, signature, user_ip) - - db.session.commit() - - def get_patron_mode_status(user_address: str) -> bool: try: return patron_mode_core.get_patron_mode_status(user_address) diff --git a/backend/app/legacy/core/user/tos.py b/backend/app/legacy/core/user/tos.py deleted file mode 100644 index 868d7e7d59..0000000000 --- a/backend/app/legacy/core/user/tos.py +++ /dev/null @@ -1,20 +0,0 @@ -from app.exceptions import DuplicateConsent, InvalidSignature -from app.infrastructure import database -from app.legacy.crypto.eth_sign import terms_and_conditions_consent - - -def has_user_agreed_to_terms_of_service(user_address: str) -> bool: - consent = database.user_consents.get_last_by_address(user_address) - return consent is not None - - -def add_user_terms_of_service_consent( - user_address: str, consent_signature: str, ip_address: str -): - if has_user_agreed_to_terms_of_service(user_address): - raise DuplicateConsent(user_address) - - if not terms_and_conditions_consent.verify(user_address, consent_signature): - raise InvalidSignature(user_address, consent_signature) - - database.user_consents.add_consent(user_address, ip_address) diff --git a/backend/app/modules/common/signature.py b/backend/app/modules/common/signature.py new file mode 100644 index 0000000000..a74e3e939a --- /dev/null +++ b/backend/app/modules/common/signature.py @@ -0,0 +1,31 @@ +from eth_account import Account +from eth_account.messages import encode_defunct +from eth_keys.exceptions import BadSignature +from web3.exceptions import ContractLogicError + +from app.legacy.crypto.account import is_contract +from app.legacy.crypto.eip1271 import is_valid_signature + + +def verify_signed_message(user_address: str, msg_text: str, signature: str) -> bool: + if is_contract(user_address): + return _verify_multisig(user_address, msg_text, signature) + else: + return _verify_eoa(user_address, msg_text, signature) + + +def _verify_multisig(user_address: str, msg_text: str, signature: str) -> bool: + try: + return is_valid_signature(user_address, msg_text, signature) + except ContractLogicError: + return False + + +def _verify_eoa(user_address: str, msg_text: str, signature: str) -> bool: + encoded_msg = encode_defunct(text=msg_text) + try: + recovered_address = Account.recover_message(encoded_msg, signature=signature) + except BadSignature: + return False + + return recovered_address == user_address diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index 347030452d..3c1252ecc9 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -9,6 +9,7 @@ TotalEffectiveDeposits, HistoryService, MultisigSignatures, + UserTos, ) from app.modules.modules_factory.protocols import SimulatePendingSnapshots from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures @@ -21,6 +22,7 @@ DbAndGraphEventsGenerator, ) from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode +from app.modules.user.tos.service.basic import BasicUserTos from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.pydantic import Model from app.shared.blockchain_types import compare_blockchain_types, ChainTypes @@ -33,6 +35,7 @@ class CurrentUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protoco class CurrentServices(Model): user_allocations_service: SavedUserAllocations user_deposits_service: CurrentUserDeposits + user_tos_service: UserTos octant_rewards_service: OctantRewards history_service: HistoryService simulated_pending_snapshot_service: SimulatePendingSnapshots @@ -66,6 +69,7 @@ def create(chain_id: int) -> "CurrentServices": ) user_allocations = SavedUserAllocations() user_withdrawals = FinalizedWithdrawals() + user_tos = BasicUserTos() patron_donations = EventsBasedUserPatronMode() history = FullHistory( user_deposits=user_deposits, @@ -84,4 +88,5 @@ def create(chain_id: int) -> "CurrentServices": history_service=history, simulated_pending_snapshot_service=simulated_pending_snapshot_service, multisig_signatures_service=multisig_signatures, + user_tos_service=user_tos, ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index c6b5d72eb9..2e71d169d2 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -177,3 +177,20 @@ def save_pending_signature( signature_data: dict, ): ... + + +@runtime_checkable +class UserTos(Protocol): + def has_user_agreed_to_terms_of_service( + self, context: Context, user_address: str + ) -> bool: + ... + + def add_user_terms_of_service_consent( + self, + context: Context, + user_address: str, + consent_signature: str, + ip_address: str, + ): + ... diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py index 6cb60263bb..4f25022a8c 100644 --- a/backend/app/modules/multisig_signatures/controller.py +++ b/backend/app/modules/multisig_signatures/controller.py @@ -1,5 +1,5 @@ from app.context.epoch_state import EpochState -from app.context.manager import state_context +from app.context.manager import state_context, Context from app.modules.multisig_signatures.dto import Signature, SignatureOpType from app.modules.registry import get_services @@ -14,11 +14,18 @@ def get_last_pending_signature( def save_pending_signature( - user_address: str, op_type: SignatureOpType, signature_data: dict + user_address: str, op_type: SignatureOpType, signature_data: str ): - context = state_context(EpochState.CURRENT) + context = _get_context(op_type) service = get_services(context.epoch_state).multisig_signatures_service return service.save_pending_signature( context, user_address, op_type, signature_data ) + + +def _get_context(op_type: SignatureOpType) -> Context: + if op_type == SignatureOpType.ALLOCATION: + return state_context(EpochState.PENDING) + elif op_type == SignatureOpType.TOS: + return state_context(EpochState.CURRENT) diff --git a/backend/app/modules/user/tos/controller.py b/backend/app/modules/user/tos/controller.py new file mode 100644 index 0000000000..ebf88c92df --- /dev/null +++ b/backend/app/modules/user/tos/controller.py @@ -0,0 +1,16 @@ +from app.context.epoch_state import EpochState +from app.context.manager import state_context +from app.modules.registry import get_services + + +def get_user_terms_of_service_consent_status(user_address: str) -> bool: + context = state_context(EpochState.CURRENT) + service = get_services(context.epoch_state).user_tos_service + + return service.has_user_agreed_to_terms_of_service(context, user_address) + + +def post_user_terms_of_service_consent(user_address: str, signature: str, user_ip: str): + context = state_context(EpochState.CURRENT) + service = get_services(context.epoch_state).user_tos_service + service.add_user_terms_of_service_consent(context, user_address, signature, user_ip) diff --git a/backend/app/legacy/crypto/eth_sign/terms_and_conditions_consent.py b/backend/app/modules/user/tos/core.py similarity index 79% rename from backend/app/legacy/crypto/eth_sign/terms_and_conditions_consent.py rename to backend/app/modules/user/tos/core.py index 243a506b04..45c525f0f1 100644 --- a/backend/app/legacy/crypto/eth_sign/terms_and_conditions_consent.py +++ b/backend/app/modules/user/tos/core.py @@ -1,4 +1,4 @@ -from app.legacy.crypto.eth_sign.signature import verify_signed_message +from app.modules.common.signature import verify_signed_message def build_consent_message(user_address: str) -> str: @@ -15,7 +15,7 @@ def build_consent_message(user_address: str) -> str: ) -def verify(user_address: str, signature: str) -> bool: +def verify_signature(user_address: str, signature: str) -> bool: msg_text = build_consent_message(user_address) return verify_signed_message(user_address, msg_text, signature) diff --git a/backend/app/modules/user/tos/service/basic.py b/backend/app/modules/user/tos/service/basic.py new file mode 100644 index 0000000000..7d721ce2a8 --- /dev/null +++ b/backend/app/modules/user/tos/service/basic.py @@ -0,0 +1,30 @@ +from app.extensions import db +from app.context.manager import Context +from app.exceptions import DuplicateConsent, InvalidSignature +from app.infrastructure import database +from app.modules.user.tos.core import verify_signature +from app.pydantic import Model + + +class BasicUserTos(Model): + def has_user_agreed_to_terms_of_service( + self, _: Context, user_address: str + ) -> bool: + consent = database.user_consents.get_last_by_address(user_address) + return consent is not None + + def add_user_terms_of_service_consent( + self, + context: Context, + user_address: str, + consent_signature: str, + ip_address: str, + ): + if self.has_user_agreed_to_terms_of_service(context, user_address): + raise DuplicateConsent(user_address) + + if not verify_signature(user_address, consent_signature): + raise InvalidSignature(user_address, consent_signature) + + database.user_consents.add_consent(user_address, ip_address) + db.session.commit() diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 5fd0353e0f..91f1a3229e 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -459,16 +459,14 @@ def patch_vault(monkeypatch): @pytest.fixture(scope="function") def patch_is_contract(monkeypatch): - monkeypatch.setattr( - "app.legacy.crypto.eth_sign.signature.is_contract", MOCK_IS_CONTRACT - ) + monkeypatch.setattr("app.modules.common.signature.is_contract", MOCK_IS_CONTRACT) MOCK_IS_CONTRACT.return_value = False @pytest.fixture(scope="function") def patch_eip1271_is_valid_signature(monkeypatch): monkeypatch.setattr( - "app.legacy.crypto.eth_sign.signature.is_valid_signature", + "app.modules.common.signature.is_valid_signature", MOCK_EIP1271_IS_VALID_SIGNATURE, ) MOCK_EIP1271_IS_VALID_SIGNATURE.return_value = True diff --git a/backend/tests/helpers/signature.py b/backend/tests/helpers/signature.py new file mode 100644 index 0000000000..5241e25640 --- /dev/null +++ b/backend/tests/helpers/signature.py @@ -0,0 +1,14 @@ +from eth_account.messages import encode_defunct + +from app.modules.user.tos.core import build_consent_message + + +def build_user_signature(user, user_address=None): + if user_address is None: + user_address = user.address + + msg = build_consent_message(user_address) + message = encode_defunct(text=msg) + signature_bytes = user.sign_message(message).signature + + return signature_bytes diff --git a/backend/tests/legacy/test_user_consents.py b/backend/tests/legacy/test_user_consents.py deleted file mode 100644 index 2bd78b6b7f..0000000000 --- a/backend/tests/legacy/test_user_consents.py +++ /dev/null @@ -1,145 +0,0 @@ -import pytest -from eth_account.messages import encode_defunct - -from app import exceptions -from app.infrastructure import database -from app.legacy.core.user.tos import ( - has_user_agreed_to_terms_of_service, - add_user_terms_of_service_consent, -) -from app.legacy.crypto.eth_sign import terms_and_conditions_consent -from app.legacy.crypto.eth_sign.terms_and_conditions_consent import ( - build_consent_message, -) -from tests.conftest import MOCK_IS_CONTRACT - - -@pytest.fixture(autouse=True) -def before(patch_is_contract): - pass - - -@pytest.fixture -def consenting_alice(app, alice): - database.user_consents.add_consent(alice.address, "127.0.0.1") - return alice - - -@pytest.fixture -def unwilling_bob(bob): - return bob - - -@pytest.fixture -def metamask_valid_signature(): - address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" - signature = "0x304cd12d8b9a0e39dbab476d4a5ca04733786a2f25ce0fd5b244e86b61499080392c6bd98b8076b45c1bfcb91f2a7c4ede559bf398941b41821f1c9eb5ba06071b" - - return address, signature - - -def _build_user_signature(user, user_address=None): - if user_address is None: - user_address = user.address - - msg = build_consent_message(user_address) - message = encode_defunct(text=msg) - signature_bytes = user.sign_message(message).signature - - return signature_bytes - - -def test_verifies_valid_signature(alice): - signature = _build_user_signature(alice) - - assert terms_and_conditions_consent.verify(alice.address, signature) - - -def test_verifies_valid_multisig_signature(alice, patch_eip1271_is_valid_signature): - MOCK_IS_CONTRACT.return_value = True - - signature = _build_user_signature(alice) - - assert terms_and_conditions_consent.verify(alice.address, signature) - - -def test_verifies_metamask_signature(metamask_valid_signature): - (address, signature) = metamask_valid_signature - - assert terms_and_conditions_consent.verify(address, signature) - - -def test_rejects_someone_elses_signature(alice, bob): - # when - # bob signs alices address - signature = _build_user_signature(bob, alice.address) - - # then - # bob cannot verify neither as alice nor as himself - assert terms_and_conditions_consent.verify(alice.address, signature) is False - assert terms_and_conditions_consent.verify(bob.address, signature) is False - - -def test_rejects_invalid_signature(alice): - invalid_signature = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" - assert ( - terms_and_conditions_consent.verify(alice.address, invalid_signature) is False - ) - - -def test_get_user_terms_of_service_consent(app, consenting_alice, unwilling_bob): - assert has_user_agreed_to_terms_of_service(consenting_alice.address) is True - assert has_user_agreed_to_terms_of_service(unwilling_bob.address) is False - - -def test_getting_consent_status_does_not_create_new_user_if_one_does_not_yet_exist( - app, consenting_alice, unwilling_bob -): - assert has_user_agreed_to_terms_of_service(consenting_alice.address) is True - assert database.user.get_by_address(consenting_alice.address) is not None - - assert has_user_agreed_to_terms_of_service(unwilling_bob.address) is False - assert database.user.get_by_address(unwilling_bob.address) is None - - -def test_can_add_users_consent(app, alice): - # given - signature = _build_user_signature(alice) - assert has_user_agreed_to_terms_of_service(alice.address) is False - - # when - add_user_terms_of_service_consent(alice.address, signature, "127.0.0.1") - - # then - assert has_user_agreed_to_terms_of_service(alice.address) is True - - -def test_cannot_add_users_consent_twice(app, consenting_alice): - # given - signature = _build_user_signature(consenting_alice) - assert has_user_agreed_to_terms_of_service(consenting_alice.address) is True - - # when - with pytest.raises(exceptions.DuplicateConsent): - add_user_terms_of_service_consent( - consenting_alice.address, signature, "127.0.0.1" - ) - - # then - assert has_user_agreed_to_terms_of_service(consenting_alice.address) is True - - -def test_rejects_to_add_consent_with_invalid_signature(app, alice, bob): - # given - signature = _build_user_signature(bob, alice.address) - - # when - with pytest.raises(exceptions.InvalidSignature): - add_user_terms_of_service_consent(alice.address, signature, "127.0.0.1") - - with pytest.raises(exceptions.InvalidSignature): - add_user_terms_of_service_consent(bob.address, signature, "127.0.0.1") - - # then - assert has_user_agreed_to_terms_of_service(alice.address) is False - assert has_user_agreed_to_terms_of_service(bob.address) is False diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 9b0c78dba0..d00dba45f0 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -33,6 +33,7 @@ from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.calculated import CalculatedUserRewards from app.modules.user.rewards.service.saved import SavedUserRewards +from app.modules.user.tos.service.basic import BasicUserTos from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.modules.withdrawals.service.pending import PendingWithdrawals from app.shared.blockchain_types import ChainTypes @@ -53,6 +54,7 @@ def test_current_services_factory(): user_deposits = CalculatedUserDeposits(events_generator=DbAndGraphEventsGenerator()) user_allocations = SavedUserAllocations() user_withdrawals = FinalizedWithdrawals() + user_tos = BasicUserTos() patron_donations = EventsBasedUserPatronMode() octant_rewards = CalculatedOctantRewards( staking_proceeds=EstimatedStakingProceeds(), @@ -69,6 +71,7 @@ def test_current_services_factory(): assert result.user_deposits_service == user_deposits assert result.octant_rewards_service == octant_rewards assert result.history_service == history + assert result.user_tos_service == user_tos assert result.multisig_signatures_service == multisig_signatures diff --git a/backend/tests/modules/user/tos/test_basic_user_tos.py b/backend/tests/modules/user/tos/test_basic_user_tos.py new file mode 100644 index 0000000000..b4f30c8226 --- /dev/null +++ b/backend/tests/modules/user/tos/test_basic_user_tos.py @@ -0,0 +1,101 @@ +import pytest + +from app.exceptions import DuplicateConsent, InvalidSignature +from app.infrastructure import database +from app.modules.user.tos.service.basic import BasicUserTos +from tests.helpers.signature import build_user_signature + + +@pytest.fixture(autouse=True) +def before(app): + pass + + +@pytest.fixture +def consenting_alice(app, alice): + database.user_consents.add_consent(alice.address, "127.0.0.1") + return alice + + +@pytest.fixture +def unwilling_bob(bob): + return bob + + +def test_get_user_terms_of_service_consent(context, consenting_alice, unwilling_bob): + service = BasicUserTos() + + assert ( + service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) + is True + ) + assert ( + service.has_user_agreed_to_terms_of_service(context, unwilling_bob.address) + is False + ) + + +def test_getting_consent_status_does_not_create_new_user_if_one_does_not_yet_exist( + context, consenting_alice, unwilling_bob +): + service = BasicUserTos() + + assert ( + service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) + is True + ) + assert database.user.get_by_address(consenting_alice.address) is not None + + assert ( + service.has_user_agreed_to_terms_of_service(context, unwilling_bob.address) + is False + ) + assert database.user.get_by_address(unwilling_bob.address) is None + + +def test_can_add_users_consent(context, alice): + signature = build_user_signature(alice) + service = BasicUserTos() + assert service.has_user_agreed_to_terms_of_service(context, alice.address) is False + + service.add_user_terms_of_service_consent( + context, alice.address, signature, "127.0.0.1" + ) + assert service.has_user_agreed_to_terms_of_service(context, alice.address) is True + + +def test_cannot_add_users_consent_twice(context, consenting_alice): + service = BasicUserTos() + signature = build_user_signature(consenting_alice) + assert ( + service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) + is True + ) + + with pytest.raises(DuplicateConsent): + service.add_user_terms_of_service_consent( + context, consenting_alice.address, signature, "127.0.0.1" + ) + assert ( + service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) + is True + ) + + +def test_rejects_to_add_consent_with_invalid_signature(context, alice, bob): + service = BasicUserTos() + signature = build_user_signature(bob, alice.address) + + # when + with pytest.raises(InvalidSignature): + service.add_user_terms_of_service_consent( + context, alice.address, signature, "127.0.0.1" + ) + + with pytest.raises(InvalidSignature): + service.add_user_terms_of_service_consent( + context, bob.address, signature, "127.0.0.1" + ) + + assert service.has_user_agreed_to_terms_of_service(context, alice.address) is False + assert service.has_user_agreed_to_terms_of_service(context, bob.address) is False diff --git a/backend/tests/modules/user/tos/test_tos_core.py b/backend/tests/modules/user/tos/test_tos_core.py new file mode 100644 index 0000000000..fe617ea8a7 --- /dev/null +++ b/backend/tests/modules/user/tos/test_tos_core.py @@ -0,0 +1,55 @@ +import pytest +from eth_account.messages import encode_defunct + +from app.modules.user.tos.core import build_consent_message, verify_signature +from tests.conftest import MOCK_IS_CONTRACT +from tests.helpers.signature import build_user_signature + + +@pytest.fixture(autouse=True) +def before(patch_is_contract): + pass + + +@pytest.fixture +def metamask_valid_signature(): + address = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" + signature = "0x304cd12d8b9a0e39dbab476d4a5ca04733786a2f25ce0fd5b244e86b61499080392c6bd98b8076b45c1bfcb91f2a7c4ede559bf398941b41821f1c9eb5ba06071b" + + return address, signature + + +def test_verifies_valid_signature(alice): + signature = build_user_signature(alice) + + assert verify_signature(alice.address, signature) + + +def test_verifies_valid_multisig_signature(alice, patch_eip1271_is_valid_signature): + MOCK_IS_CONTRACT.return_value = True + + signature = build_user_signature(alice) + + assert verify_signature(alice.address, signature) + + +def test_verifies_metamask_signature(metamask_valid_signature): + (address, signature) = metamask_valid_signature + + assert verify_signature(address, signature) + + +def test_rejects_someone_elses_signature(alice, bob): + # when + # bob signs alices address + signature = build_user_signature(bob, alice.address) + + # then + # bob cannot verify neither as alice nor as himself + assert verify_signature(alice.address, signature) is False + assert verify_signature(bob.address, signature) is False + + +def test_rejects_invalid_signature(alice): + invalid_signature = "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000" + assert verify_signature(alice.address, invalid_signature) is False From e16aabbc762b8cbd49e246c6e42704b96fcdcb26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Fri, 22 Mar 2024 12:47:55 +0100 Subject: [PATCH 058/107] feat: abstract verifier logic --- backend/app/exceptions.py | 8 +++++ .../database/multisig_signature.py | 2 +- .../routes/multisig_signatures.py | 2 +- backend/app/modules/common/verifier.py | 28 +++++++++++++++++ backend/app/modules/dto.py | 5 ++++ .../app/modules/modules_factory/current.py | 11 +++++-- .../app/modules/modules_factory/pending.py | 20 ++++++++++--- .../app/modules/modules_factory/protocols.py | 3 +- .../modules/multisig_signatures/controller.py | 7 +++-- .../app/modules/multisig_signatures/dto.py | 5 ---- .../multisig_signatures/service/offchain.py | 21 +++++++++++-- backend/app/modules/registry.py | 6 ++++ backend/app/modules/user/allocations/core.py | 4 +-- .../user/allocations/service/pending.py | 30 +++++++++++++++---- backend/app/modules/user/tos/service/basic.py | 26 ++++++++++++---- .../test_offchain_multisig_signatures.py | 3 +- 16 files changed, 145 insertions(+), 36 deletions(-) create mode 100644 backend/app/modules/common/verifier.py diff --git a/backend/app/exceptions.py b/backend/app/exceptions.py index c90c94e519..9ba384c0f3 100644 --- a/backend/app/exceptions.py +++ b/backend/app/exceptions.py @@ -246,3 +246,11 @@ class InvalidBlocksRange(OctantException): def __init__(self): super().__init__(self.description, self.code) + + +class InvalidMultisigSignatureRequest(OctantException): + code = 400 + description = "Given multisig signature request failed validation" + + def __init__(self): + super().__init__(self.description, self.code) diff --git a/backend/app/infrastructure/database/multisig_signature.py b/backend/app/infrastructure/database/multisig_signature.py index ebfc6dc167..98f35e4b8e 100644 --- a/backend/app/infrastructure/database/multisig_signature.py +++ b/backend/app/infrastructure/database/multisig_signature.py @@ -4,7 +4,7 @@ from app.extensions import db from app.infrastructure.database.models import MultisigSignatures -from app.modules.multisig_signatures.dto import SignatureOpType +from app.modules.dto import SignatureOpType class SigStatus(StrEnum): diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py index f4f1ddf11d..14301db0a9 100644 --- a/backend/app/infrastructure/routes/multisig_signatures.py +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -3,11 +3,11 @@ from app.extensions import api from app.infrastructure import OctantResource +from app.modules.dto import SignatureOpType from app.modules.multisig_signatures.controller import ( get_last_pending_signature, save_pending_signature, ) -from app.modules.multisig_signatures.dto import SignatureOpType ns = Namespace( "multisig-signatures", diff --git a/backend/app/modules/common/verifier.py b/backend/app/modules/common/verifier.py new file mode 100644 index 0000000000..ecfe3c9eb1 --- /dev/null +++ b/backend/app/modules/common/verifier.py @@ -0,0 +1,28 @@ +from abc import ABC, abstractmethod + +from app.context.manager import Context + + +def verifier_wrapper(verify_func): + def wrapper(self, context: Context, **kwargs) -> bool: + result = verify_func(self, context, **kwargs) + return result if result is not None else True + + return wrapper + + +class Verifier(ABC): + @abstractmethod + @verifier_wrapper + def verify_logic(self, context: Context, **kwargs) -> bool: + ... + + @abstractmethod + @verifier_wrapper + def verify_signature(self, context: Context, **kwargs) -> bool: + ... + + def verify(self, context: Context, **kwargs) -> bool: + return self.verify_logic(context, **kwargs) and self.verify_signature( + context, **kwargs + ) diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 595ed14b7d..4aff22ffd4 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -98,3 +98,8 @@ class WithdrawableEth: amount: int proof: list[str] status: WithdrawalStatus + + +class SignatureOpType(StrEnum): + TOS = "tos" + ALLOCATION = "allocation" diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index 3c1252ecc9..023d4e96ae 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -2,6 +2,7 @@ import app.modules.staking.proceeds.service.aggregated as aggregated import app.modules.staking.proceeds.service.contract_balance as contract_balance +from app.modules.dto import SignatureOpType from app.modules.history.service.full import FullHistory from app.modules.modules_factory.protocols import ( OctantRewards, @@ -22,7 +23,7 @@ DbAndGraphEventsGenerator, ) from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode -from app.modules.user.tos.service.basic import BasicUserTos +from app.modules.user.tos.service.basic import BasicUserTos, BasicUserTosVerifier from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.pydantic import Model from app.shared.blockchain_types import compare_blockchain_types, ChainTypes @@ -69,7 +70,8 @@ def create(chain_id: int) -> "CurrentServices": ) user_allocations = SavedUserAllocations() user_withdrawals = FinalizedWithdrawals() - user_tos = BasicUserTos() + tos_verifier = BasicUserTosVerifier() + user_tos = BasicUserTos(verifier=tos_verifier) patron_donations = EventsBasedUserPatronMode() history = FullHistory( user_deposits=user_deposits, @@ -77,7 +79,10 @@ def create(chain_id: int) -> "CurrentServices": user_withdrawals=user_withdrawals, patron_donations=patron_donations, ) - multisig_signatures = OffchainMultisigSignatures() + + multisig_signatures = OffchainMultisigSignatures( + verifiers={SignatureOpType.TOS: tos_verifier} + ) return CurrentServices( user_allocations_service=user_allocations, user_deposits_service=user_deposits, diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 76c19268f9..6d6ab49cbb 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -1,5 +1,6 @@ from typing import Protocol +from app.modules.dto import SignatureOpType from app.modules.modules_factory.protocols import ( UserPatronMode, UserRewards, @@ -15,12 +16,17 @@ DonorsAddresses, AllocationManipulationProtocol, GetUserAllocationsProtocol, + MultisigSignatures, ) +from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures from app.modules.octant_rewards.service.pending import PendingOctantRewards from app.modules.snapshots.finalized.service.simulated import ( SimulatedFinalizedSnapshots, ) -from app.modules.user.allocations.service.pending import PendingUserAllocations +from app.modules.user.allocations.service.pending import ( + PendingUserAllocations, + PendingUserAllocationsVerifier, +) from app.modules.user.budgets.service.saved import SavedUserBudgets from app.modules.user.deposits.service.saved import SavedUserDeposits from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode @@ -58,21 +64,23 @@ class PendingServices(Model): finalized_snapshots_service: SimulateFinalizedSnapshots withdrawals_service: WithdrawalsService project_rewards_service: EstimatedProjectRewardsService + multisig_signatures_service: MultisigSignatures @staticmethod def create() -> "PendingServices": events_based_patron_mode = EventsBasedUserPatronMode() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) saved_user_budgets = SavedUserBudgets() - saved_user_allocations = PendingUserAllocations( + allocations_verifier = PendingUserAllocationsVerifier( user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, octant_rewards=octant_rewards, ) + pending_user_allocations = PendingUserAllocations(verifier=allocations_verifier) user_rewards = CalculatedUserRewards( user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, - allocations=saved_user_allocations, + allocations=pending_user_allocations, ) finalized_snapshots_service = SimulatedFinalizedSnapshots( octant_rewards=octant_rewards, @@ -81,15 +89,19 @@ def create() -> "PendingServices": ) withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) project_rewards = EstimatedProjectRewards(octant_rewards=octant_rewards) + multisig_signatures = OffchainMultisigSignatures( + verifiers={SignatureOpType.ALLOCATION: allocations_verifier} + ) return PendingServices( user_deposits_service=SavedUserDeposits(), octant_rewards_service=octant_rewards, - user_allocations_service=saved_user_allocations, + user_allocations_service=pending_user_allocations, user_patron_mode_service=events_based_patron_mode, finalized_snapshots_service=finalized_snapshots_service, user_budgets_service=saved_user_budgets, user_rewards_service=user_rewards, withdrawals_service=withdrawals_service, project_rewards_service=project_rewards, + multisig_signatures_service=multisig_signatures, ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index 2e71d169d2..a7b609cbf7 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -12,9 +12,10 @@ PendingSnapshotDTO, WithdrawableEth, UserAllocationRequestPayload, + SignatureOpType, ) from app.modules.history.dto import UserHistoryDTO -from app.modules.multisig_signatures.dto import SignatureOpType, Signature +from app.modules.multisig_signatures.dto import Signature @runtime_checkable diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py index 4f25022a8c..e6eea755dd 100644 --- a/backend/app/modules/multisig_signatures/controller.py +++ b/backend/app/modules/multisig_signatures/controller.py @@ -1,20 +1,21 @@ from app.context.epoch_state import EpochState from app.context.manager import state_context, Context -from app.modules.multisig_signatures.dto import Signature, SignatureOpType +from app.modules.dto import SignatureOpType +from app.modules.multisig_signatures.dto import Signature from app.modules.registry import get_services def get_last_pending_signature( user_address: str, op_type: SignatureOpType ) -> Signature: - context = state_context(EpochState.CURRENT) + context = _get_context(op_type) service = get_services(context.epoch_state).multisig_signatures_service return service.get_last_pending_signature(context, user_address, op_type) def save_pending_signature( - user_address: str, op_type: SignatureOpType, signature_data: str + user_address: str, op_type: SignatureOpType, signature_data: dict ): context = _get_context(op_type) service = get_services(context.epoch_state).multisig_signatures_service diff --git a/backend/app/modules/multisig_signatures/dto.py b/backend/app/modules/multisig_signatures/dto.py index 4720e7b642..e59e7e81d4 100644 --- a/backend/app/modules/multisig_signatures/dto.py +++ b/backend/app/modules/multisig_signatures/dto.py @@ -4,11 +4,6 @@ from dataclass_wizard import JSONWizard -class SignatureOpType(StrEnum): - TOS = "tos" - ALLOCATION = "allocation" - - @dataclass(frozen=True) class Signature(JSONWizard): message: str diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index d8e45e177c..262aafa17f 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -1,10 +1,18 @@ +import json + from app.context.manager import Context +from app.exceptions import InvalidMultisigSignatureRequest +from app.extensions import db from app.infrastructure import database -from app.modules.multisig_signatures.dto import Signature, SignatureOpType +from app.modules.common.verifier import Verifier +from app.modules.dto import SignatureOpType +from app.modules.multisig_signatures.dto import Signature from app.pydantic import Model class OffchainMultisigSignatures(Model): + verifiers: dict[SignatureOpType, Verifier] + def get_last_pending_signature( self, _: Context, user_address: str, op_type: SignatureOpType ) -> Signature | None: @@ -27,4 +35,13 @@ def save_pending_signature( op_type: SignatureOpType, signature_data: dict, ): - ... + verifier = self.verifiers[op_type] + if not verifier.verify_logic( + context, user_address=user_address, **signature_data + ): + raise InvalidMultisigSignatureRequest() + + database.multisig_signature.save_signature( + user_address, op_type, json.dumps(signature_data), "" + ) + db.session.commit() diff --git a/backend/app/modules/registry.py b/backend/app/modules/registry.py index ef81a10f47..bbbc06088c 100644 --- a/backend/app/modules/registry.py +++ b/backend/app/modules/registry.py @@ -9,6 +9,7 @@ from app.modules.modules_factory.pre_pending import PrePendingServices SERVICE_REGISTRY: Dict[EpochState, Any] = {} +SERVICE_REGISTRY: Dict[EpochState, Any] = {} def get_services(epoch_state: EpochState): @@ -24,3 +25,8 @@ def register_services(app): SERVICE_REGISTRY[EpochState.PENDING] = PendingServices.create() SERVICE_REGISTRY[EpochState.FINALIZING] = FinalizingServices.create() SERVICE_REGISTRY[EpochState.FINALIZED] = FinalizedServices.create() + + +def register_verifiers(app): + verifier_factory = VerifierFactory(app) + verifier_factory.register_verifiers() diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index bb82c3ffa6..367bb581b7 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -56,7 +56,7 @@ def verify_user_allocation_request( expected_nonce: int, user_budget: int, patrons: List[str], -) -> bool: +): _verify_epoch_state(context.epoch_state) _verify_nonce(request.payload.nonce, expected_nonce) _verify_user_not_a_patron(user_address, patrons) @@ -68,8 +68,6 @@ def verify_user_allocation_request( _verify_no_self_allocation(request.payload.allocations, user_address) _verify_allocations_within_budget(request.payload.allocations, user_budget) - return True - def _verify_epoch_state(epoch_state: EpochState): if epoch_state is EpochState.PRE_PENDING: diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 1d1ebbd4b3..e4851682e3 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -1,4 +1,6 @@ from typing import List, Tuple, Protocol, runtime_checkable + +from app.modules.common.verifier import Verifier from app.pydantic import Model from app import exceptions @@ -29,16 +31,13 @@ def get_all_patrons_addresses(self, context: Context) -> List[str]: ... -class PendingUserAllocations(SavedUserAllocations, Model): +class PendingUserAllocationsVerifier(Verifier, Model): octant_rewards: OctantRewards user_budgets: UserBudgetProtocol patrons_mode: GetPatronsAddressesProtocol - def allocate( - self, context: Context, payload: UserAllocationRequestPayload, **kwargs - ) -> str: - user_address = core.recover_user_address(payload) - + def verify_logic(self, context: Context, **kwargs): + user_address, payload = kwargs["user_address"], kwargs["payload"] expected_nonce = self.get_user_next_nonce(user_address) user_budget = self.user_budgets.get_budget(context, user_address) patrons = self.patrons_mode.get_all_patrons_addresses(context) @@ -47,6 +46,25 @@ def allocate( context, payload, user_address, expected_nonce, user_budget, patrons ) + def verify_signature(self, _: Context, **kwargs): + user_address, signature = kwargs["user_address"], kwargs["consent_signature"] + # TODO: implement verify_signature + + +class PendingUserAllocations(SavedUserAllocations, Model): + octant_rewards: OctantRewards + verifier: Verifier + + def allocate( + self, context: Context, payload: UserAllocationRequestPayload, **kwargs + ) -> str: + user_address = core.recover_user_address(payload) + + expected_nonce = self.get_user_next_nonce(user_address) + self.verifier.verify( + context, user_address=user_address, payload=payload, **kwargs + ) + self.revoke_previous_allocation(context, user_address) user = database.user.get_by_address(user_address) diff --git a/backend/app/modules/user/tos/service/basic.py b/backend/app/modules/user/tos/service/basic.py index 7d721ce2a8..90d2150cc0 100644 --- a/backend/app/modules/user/tos/service/basic.py +++ b/backend/app/modules/user/tos/service/basic.py @@ -2,11 +2,28 @@ from app.context.manager import Context from app.exceptions import DuplicateConsent, InvalidSignature from app.infrastructure import database +from app.modules.common.verifier import Verifier from app.modules.user.tos.core import verify_signature from app.pydantic import Model +class BasicUserTosVerifier(Verifier): + def verify_logic(self, _: Context, **kwargs): + user_address = kwargs["user_address"] + consent = database.user_consents.get_last_by_address(user_address) + if consent is not None: + raise DuplicateConsent(user_address) + + def verify_signature(self, _: Context, **kwargs): + user_address, signature = kwargs["user_address"], kwargs["consent_signature"] + + if not verify_signature(user_address, signature): + raise InvalidSignature(user_address, signature) + + class BasicUserTos(Model): + verifier: Verifier + def has_user_agreed_to_terms_of_service( self, _: Context, user_address: str ) -> bool: @@ -20,11 +37,8 @@ def add_user_terms_of_service_consent( consent_signature: str, ip_address: str, ): - if self.has_user_agreed_to_terms_of_service(context, user_address): - raise DuplicateConsent(user_address) - - if not verify_signature(user_address, consent_signature): - raise InvalidSignature(user_address, consent_signature) - + self.verifier.verify( + context, user_address=user_address, consent_signature=consent_signature + ) database.user_consents.add_consent(user_address, ip_address) db.session.commit() diff --git a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py index b54dc07bc0..d648f93dd7 100644 --- a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py +++ b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py @@ -3,7 +3,8 @@ from app.extensions import db from app.infrastructure import database from app.infrastructure.database.multisig_signature import SigStatus -from app.modules.multisig_signatures.dto import Signature, SignatureOpType +from app.modules.dto import SignatureOpType +from app.modules.multisig_signatures.dto import Signature from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures From 32b79a2b0b9ebfcedca1eecdb10c0385a6911919 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Mon, 25 Mar 2024 14:34:08 +0100 Subject: [PATCH 059/107] test: verifier tests --- backend/app/modules/common/signature.py | 6 +- backend/app/modules/common/verifier.py | 25 ++++--- .../app/modules/modules_factory/pending.py | 5 +- .../app/modules/multisig_signatures/dto.py | 1 - .../multisig_signatures/service/offchain.py | 7 +- backend/app/modules/registry.py | 6 -- .../user/allocations/service/pending.py | 26 ++++--- backend/app/modules/user/tos/service/basic.py | 4 +- backend/tests/conftest.py | 10 +++ backend/tests/modules/common/test_verifier.py | 62 +++++++++++++++++ .../modules_factory/test_modules_factory.py | 27 ++++++-- .../test_offchain_multisig_signatures.py | 67 ++++++++++++++++++- .../modules/user/allocations/test_core.py | 14 ++-- .../allocations/test_pending_allocations.py | 11 ++- .../modules/user/tos/test_basic_user_tos.py | 20 +++--- .../tests/modules/user/tos/test_tos_core.py | 3 +- 16 files changed, 230 insertions(+), 64 deletions(-) create mode 100644 backend/tests/modules/common/test_verifier.py diff --git a/backend/app/modules/common/signature.py b/backend/app/modules/common/signature.py index a74e3e939a..1bb46319f3 100644 --- a/backend/app/modules/common/signature.py +++ b/backend/app/modules/common/signature.py @@ -1,5 +1,5 @@ from eth_account import Account -from eth_account.messages import encode_defunct +from eth_account.messages import encode_defunct, defunct_hash_message from eth_keys.exceptions import BadSignature from web3.exceptions import ContractLogicError @@ -14,6 +14,10 @@ def verify_signed_message(user_address: str, msg_text: str, signature: str) -> b return _verify_eoa(user_address, msg_text, signature) +def hash_message(msg_text: str) -> str: + return defunct_hash_message(text=msg_text).hex() + + def _verify_multisig(user_address: str, msg_text: str, signature: str) -> bool: try: return is_valid_signature(user_address, msg_text, signature) diff --git a/backend/app/modules/common/verifier.py b/backend/app/modules/common/verifier.py index ecfe3c9eb1..3d813733a1 100644 --- a/backend/app/modules/common/verifier.py +++ b/backend/app/modules/common/verifier.py @@ -1,27 +1,26 @@ from abc import ABC, abstractmethod from app.context.manager import Context +from app.pydantic import Model -def verifier_wrapper(verify_func): - def wrapper(self, context: Context, **kwargs) -> bool: - result = verify_func(self, context, **kwargs) - return result if result is not None else True - - return wrapper - - -class Verifier(ABC): +class Verifier(ABC, Model): @abstractmethod - @verifier_wrapper - def verify_logic(self, context: Context, **kwargs) -> bool: + def _verify_logic(self, context: Context, **kwargs) -> bool: ... @abstractmethod - @verifier_wrapper - def verify_signature(self, context: Context, **kwargs) -> bool: + def _verify_signature(self, context: Context, **kwargs) -> bool: ... + def verify_logic(self, context: Context, **kwargs) -> bool: + result = self._verify_logic(context, **kwargs) + return result if result is not None else True + + def verify_signature(self, context: Context, **kwargs) -> bool: + result = self._verify_signature(context, **kwargs) + return result if result is not None else True + def verify(self, context: Context, **kwargs) -> bool: return self.verify_logic(context, **kwargs) and self.verify_signature( context, **kwargs diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 6d6ab49cbb..4671869996 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -74,9 +74,10 @@ def create() -> "PendingServices": allocations_verifier = PendingUserAllocationsVerifier( user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, - octant_rewards=octant_rewards, ) - pending_user_allocations = PendingUserAllocations(verifier=allocations_verifier) + pending_user_allocations = PendingUserAllocations( + octant_rewards=octant_rewards, verifier=allocations_verifier + ) user_rewards = CalculatedUserRewards( user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, diff --git a/backend/app/modules/multisig_signatures/dto.py b/backend/app/modules/multisig_signatures/dto.py index e59e7e81d4..2887afcc45 100644 --- a/backend/app/modules/multisig_signatures/dto.py +++ b/backend/app/modules/multisig_signatures/dto.py @@ -1,5 +1,4 @@ from dataclasses import dataclass -from enum import StrEnum from dataclass_wizard import JSONWizard diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index 262aafa17f..66c62662da 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -4,6 +4,7 @@ from app.exceptions import InvalidMultisigSignatureRequest from app.extensions import db from app.infrastructure import database +from app.modules.common.signature import hash_message from app.modules.common.verifier import Verifier from app.modules.dto import SignatureOpType from app.modules.multisig_signatures.dto import Signature @@ -41,7 +42,7 @@ def save_pending_signature( ): raise InvalidMultisigSignatureRequest() - database.multisig_signature.save_signature( - user_address, op_type, json.dumps(signature_data), "" - ) + msg = json.dumps(signature_data) + msg_hash = hash_message(msg) + database.multisig_signature.save_signature(user_address, op_type, msg, msg_hash) db.session.commit() diff --git a/backend/app/modules/registry.py b/backend/app/modules/registry.py index bbbc06088c..ef81a10f47 100644 --- a/backend/app/modules/registry.py +++ b/backend/app/modules/registry.py @@ -9,7 +9,6 @@ from app.modules.modules_factory.pre_pending import PrePendingServices SERVICE_REGISTRY: Dict[EpochState, Any] = {} -SERVICE_REGISTRY: Dict[EpochState, Any] = {} def get_services(epoch_state: EpochState): @@ -25,8 +24,3 @@ def register_services(app): SERVICE_REGISTRY[EpochState.PENDING] = PendingServices.create() SERVICE_REGISTRY[EpochState.FINALIZING] = FinalizingServices.create() SERVICE_REGISTRY[EpochState.FINALIZED] = FinalizedServices.create() - - -def register_verifiers(app): - verifier_factory = VerifierFactory(app) - verifier_factory.register_verifiers() diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index e4851682e3..79c5b5d5e5 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -1,16 +1,15 @@ from typing import List, Tuple, Protocol, runtime_checkable -from app.modules.common.verifier import Verifier -from app.pydantic import Model - from app import exceptions -from app.extensions import db from app.context.manager import Context from app.engine.projects.rewards import ProjectRewardDTO +from app.extensions import db from app.infrastructure import database +from app.modules.common.verifier import Verifier from app.modules.dto import AllocationDTO, UserAllocationRequestPayload from app.modules.user.allocations import core from app.modules.user.allocations.service.saved import SavedUserAllocations +from app.pydantic import Model @runtime_checkable @@ -36,9 +35,12 @@ class PendingUserAllocationsVerifier(Verifier, Model): user_budgets: UserBudgetProtocol patrons_mode: GetPatronsAddressesProtocol - def verify_logic(self, context: Context, **kwargs): - user_address, payload = kwargs["user_address"], kwargs["payload"] - expected_nonce = self.get_user_next_nonce(user_address) + def _verify_logic(self, context: Context, **kwargs): + user_address, payload, expected_nonce = ( + kwargs["user_address"], + kwargs["payload"], + kwargs["expected_nonce"], + ) user_budget = self.user_budgets.get_budget(context, user_address) patrons = self.patrons_mode.get_all_patrons_addresses(context) @@ -46,9 +48,9 @@ def verify_logic(self, context: Context, **kwargs): context, payload, user_address, expected_nonce, user_budget, patrons ) - def verify_signature(self, _: Context, **kwargs): - user_address, signature = kwargs["user_address"], kwargs["consent_signature"] + def _verify_signature(self, _: Context, **kwargs): # TODO: implement verify_signature + ... class PendingUserAllocations(SavedUserAllocations, Model): @@ -62,7 +64,11 @@ def allocate( expected_nonce = self.get_user_next_nonce(user_address) self.verifier.verify( - context, user_address=user_address, payload=payload, **kwargs + context, + user_address=user_address, + payload=payload, + expected_nonce=expected_nonce, + **kwargs ) self.revoke_previous_allocation(context, user_address) diff --git a/backend/app/modules/user/tos/service/basic.py b/backend/app/modules/user/tos/service/basic.py index 90d2150cc0..90dbc89010 100644 --- a/backend/app/modules/user/tos/service/basic.py +++ b/backend/app/modules/user/tos/service/basic.py @@ -8,13 +8,13 @@ class BasicUserTosVerifier(Verifier): - def verify_logic(self, _: Context, **kwargs): + def _verify_logic(self, _: Context, **kwargs): user_address = kwargs["user_address"] consent = database.user_consents.get_last_by_address(user_address) if consent is not None: raise DuplicateConsent(user_address) - def verify_signature(self, _: Context, **kwargs): + def _verify_signature(self, _: Context, **kwargs): user_address, signature = kwargs["user_address"], kwargs["consent_signature"] if not verify_signature(user_address, signature): diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 91f1a3229e..a2736a8cb4 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -22,6 +22,7 @@ from app.infrastructure.contracts.vault import Vault from app.legacy.crypto.account import Account as CryptoAccount from app.legacy.crypto.eip712 import build_allocations_eip712_data, sign +from app.modules.common.verifier import Verifier from app.modules.dto import AccountFundsDTO, AllocationItem from app.settings import DevConfig, TestConfig from tests.helpers.constants import ( @@ -661,6 +662,15 @@ def mock_staking_proceeds(): return staking_proceeds_service_mock +@pytest.fixture(scope="function") +def mock_verifier(): + verifier_mock = Mock(Verifier) + verifier_mock.verify_logic.return_value = True + verifier_mock.verify_signature.return_value = True + + return verifier_mock + + @pytest.fixture(scope="function") def mock_user_nonce(): user_nonce_service_mock = Mock() diff --git a/backend/tests/modules/common/test_verifier.py b/backend/tests/modules/common/test_verifier.py new file mode 100644 index 0000000000..ecd894dc0a --- /dev/null +++ b/backend/tests/modules/common/test_verifier.py @@ -0,0 +1,62 @@ +import pytest + +from app.modules.common.verifier import Verifier + + +@pytest.mark.parametrize( + "verify_logic, verify_signature, expected_result", + [ + (True, True, True), + (True, False, False), + (False, True, False), + (False, False, False), + (None, None, True), + (None, True, True), + (True, None, True), + (None, False, False), + (False, None, False), + ], +) +def test_verifier(verify_logic, verify_signature, expected_result): + class MockVerifier(Verifier): + def _verify_logic(self, context, **kwargs): + return verify_logic + + def _verify_signature(self, context, **kwargs): + return verify_signature + + verifier = MockVerifier() + + result = verifier.verify(None) + + assert result == expected_result + + +def test_verifier_raises_exception_in_logic(): + class MockVerifier(Verifier): + def _verify_logic(self, context, **kwargs): + raise Exception("logic") + + def _verify_signature(self, context, **kwargs): + return True + + verifier = MockVerifier() + + with pytest.raises(Exception) as exc_info: + verifier.verify(None) + assert str(exc_info.value) == "logic" + + +def test_verifier_raises_exception_in_signature(): + class MockVerifier(Verifier): + def _verify_logic(self, context, **kwargs): + return True + + def _verify_signature(self, context, **kwargs): + raise Exception("signature") + + verifier = MockVerifier() + + with pytest.raises(Exception) as exc_info: + verifier.verify(None) + assert str(exc_info.value) == "signature" diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index d00dba45f0..40d389d2ec 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -1,3 +1,4 @@ +from app.modules.dto import SignatureOpType from app.modules.history.service.full import FullHistory from app.modules.modules_factory.current import CurrentServices from app.modules.modules_factory.finalized import FinalizedServices @@ -19,7 +20,10 @@ ContractBalanceStakingProceeds, ) from app.modules.staking.proceeds.service.estimated import EstimatedStakingProceeds -from app.modules.user.allocations.service.pending import PendingUserAllocations +from app.modules.user.allocations.service.pending import ( + PendingUserAllocations, + PendingUserAllocationsVerifier, +) from app.modules.user.allocations.service.saved import SavedUserAllocations from app.modules.user.budgets.service.saved import SavedUserBudgets from app.modules.user.deposits.service.calculated import CalculatedUserDeposits @@ -33,7 +37,7 @@ from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.calculated import CalculatedUserRewards from app.modules.user.rewards.service.saved import SavedUserRewards -from app.modules.user.tos.service.basic import BasicUserTos +from app.modules.user.tos.service.basic import BasicUserTos, BasicUserTosVerifier from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.modules.withdrawals.service.pending import PendingWithdrawals from app.shared.blockchain_types import ChainTypes @@ -54,7 +58,8 @@ def test_current_services_factory(): user_deposits = CalculatedUserDeposits(events_generator=DbAndGraphEventsGenerator()) user_allocations = SavedUserAllocations() user_withdrawals = FinalizedWithdrawals() - user_tos = BasicUserTos() + tos_verifier = BasicUserTosVerifier() + user_tos = BasicUserTos(verifier=tos_verifier) patron_donations = EventsBasedUserPatronMode() octant_rewards = CalculatedOctantRewards( staking_proceeds=EstimatedStakingProceeds(), @@ -66,7 +71,9 @@ def test_current_services_factory(): user_withdrawals=user_withdrawals, patron_donations=patron_donations, ) - multisig_signatures = OffchainMultisigSignatures() + multisig_signatures = OffchainMultisigSignatures( + verifiers={SignatureOpType.TOS: tos_verifier} + ) assert result.user_deposits_service == user_deposits assert result.octant_rewards_service == octant_rewards @@ -109,12 +116,14 @@ def test_pending_services_factory(): result = PendingServices.create() events_based_patron_mode = EventsBasedUserPatronMode() - saved_user_budgets = SavedUserBudgets() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) - user_allocations = PendingUserAllocations( + saved_user_budgets = SavedUserBudgets() + allocations_verifier = PendingUserAllocationsVerifier( user_budgets=saved_user_budgets, patrons_mode=events_based_patron_mode, - octant_rewards=octant_rewards, + ) + user_allocations = PendingUserAllocations( + octant_rewards=octant_rewards, verifier=allocations_verifier ) user_rewards = CalculatedUserRewards( user_budgets=saved_user_budgets, @@ -127,6 +136,9 @@ def test_pending_services_factory(): patrons_mode=events_based_patron_mode, ) withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) + multisig_signatures = OffchainMultisigSignatures( + verifiers={SignatureOpType.ALLOCATION: allocations_verifier} + ) assert result.user_deposits_service == SavedUserDeposits() assert result.octant_rewards_service == octant_rewards @@ -135,6 +147,7 @@ def test_pending_services_factory(): assert result.user_rewards_service == user_rewards assert result.finalized_snapshots_service == finalized_snapshots_service assert result.withdrawals_service == withdrawals_service + assert result.multisig_signatures_service == multisig_signatures def test_finalizing_services_factory(): diff --git a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py index d648f93dd7..d13d3f4a5a 100644 --- a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py +++ b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py @@ -1,8 +1,10 @@ import pytest +from app.exceptions import InvalidMultisigSignatureRequest from app.extensions import db from app.infrastructure import database from app.infrastructure.database.multisig_signature import SigStatus +from app.modules.common.verifier import Verifier from app.modules.dto import SignatureOpType from app.modules.multisig_signatures.dto import Signature from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures @@ -38,7 +40,7 @@ def test_get_last_pending_signature_returns_expected_signature_when_signature_ex ) db.session.commit() - service = OffchainMultisigSignatures() + service = OffchainMultisigSignatures(verifiers={}) # When result = service.get_last_pending_signature( @@ -55,7 +57,7 @@ def test_get_last_pending_signature_returns_none_when_no_signature_exists( context, alice ): # Given - service = OffchainMultisigSignatures() + service = OffchainMultisigSignatures(verifiers={}) # When result = service.get_last_pending_signature( @@ -64,3 +66,64 @@ def test_get_last_pending_signature_returns_none_when_no_signature_exists( # Then assert result is None + + +def test_save_signature_when_verified_successfully(context, alice, mock_verifier): + # Given + service = OffchainMultisigSignatures(verifiers={SignatureOpType.TOS: mock_verifier}) + + # When + service.save_pending_signature( + context, alice.address, SignatureOpType.TOS, {"message": "test_message"} + ) + + result = database.multisig_signature.get_last_pending_signature( + alice.address, SignatureOpType.TOS + ) + + assert result.address == alice.address + assert result.type == SignatureOpType.TOS + assert result.status == SigStatus.PENDING + assert result.message == '{"message": "test_message"}' + assert ( + result.hash + == "0x15259cb98ed495a577ec69a3a75f3b16168507d0099b70483fdab3d0d1b3e71a" + ) + + +def test_does_not_save_signature_when_verification_returns_false( + context, alice, mock_verifier +): + mock_verifier.verify_logic.return_value = False + service = OffchainMultisigSignatures(verifiers={SignatureOpType.TOS: mock_verifier}) + + with pytest.raises(InvalidMultisigSignatureRequest): + service.save_pending_signature( + context, alice.address, SignatureOpType.TOS, {"message": "test_message"} + ) + result = database.multisig_signature.get_last_pending_signature( + alice.address, SignatureOpType.TOS + ) + assert result is None + + +def test_does_not_save_signature_when_verification_throws_exception(context, alice): + class MockVerifier(Verifier): + def _verify_logic(self, context, **kwargs): + raise ValueError("logic") + + def _verify_signature(self, context, **kwargs): + return True + + service = OffchainMultisigSignatures( + verifiers={SignatureOpType.TOS: MockVerifier()} + ) + + with pytest.raises(ValueError): + service.save_pending_signature( + context, alice.address, SignatureOpType.TOS, {"message": "test_message"} + ) + result = database.multisig_signature.get_last_pending_signature( + alice.address, SignatureOpType.TOS + ) + assert result is None diff --git a/backend/tests/modules/user/allocations/test_core.py b/backend/tests/modules/user/allocations/test_core.py index e6df364739..a68b23a14c 100644 --- a/backend/tests/modules/user/allocations/test_core.py +++ b/backend/tests/modules/user/allocations/test_core.py @@ -161,8 +161,11 @@ def test_allocation_does_not_fail_with_allocation_equal_to_budget(alice, bob, co ) request = build_request(alice, allocations) - assert core.verify_user_allocation_request( - context, request, alice.address, 0, 10**18, [bob.address] + assert ( + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + is None ) @@ -177,6 +180,9 @@ def test_allocation_does_not_fail_with_allocation_below_budget(alice, bob, conte ) request = build_request(alice, allocations) - assert core.verify_user_allocation_request( - context, request, alice.address, 0, 10**18, [bob.address] + assert ( + core.verify_user_allocation_request( + context, request, alice.address, 0, 10**18, [bob.address] + ) + is None ) diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index a82bbc9422..39fe97e663 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -22,6 +22,10 @@ deserialize_allocations, make_user_allocation, ) +from app.modules.user.allocations.service.pending import ( + PendingUserAllocations, + PendingUserAllocationsVerifier, +) from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context from tests.helpers import make_user_allocation @@ -57,11 +61,14 @@ def before( @pytest.fixture() def service(mock_octant_rewards, mock_patron_mode, mock_user_budgets): - return PendingUserAllocations( - octant_rewards=mock_octant_rewards, + verifier = PendingUserAllocationsVerifier( user_budgets=mock_user_budgets, patrons_mode=mock_patron_mode, ) + return PendingUserAllocations( + octant_rewards=mock_octant_rewards, + verifier=verifier, + ) def test_simulate_allocation(service, mock_users_db): diff --git a/backend/tests/modules/user/tos/test_basic_user_tos.py b/backend/tests/modules/user/tos/test_basic_user_tos.py index b4f30c8226..feb4df4ca8 100644 --- a/backend/tests/modules/user/tos/test_basic_user_tos.py +++ b/backend/tests/modules/user/tos/test_basic_user_tos.py @@ -2,7 +2,7 @@ from app.exceptions import DuplicateConsent, InvalidSignature from app.infrastructure import database -from app.modules.user.tos.service.basic import BasicUserTos +from app.modules.user.tos.service.basic import BasicUserTos, BasicUserTosVerifier from tests.helpers.signature import build_user_signature @@ -22,8 +22,10 @@ def unwilling_bob(bob): return bob -def test_get_user_terms_of_service_consent(context, consenting_alice, unwilling_bob): - service = BasicUserTos() +def test_get_user_terms_of_service_consent( + context, consenting_alice, unwilling_bob, mock_verifier +): + service = BasicUserTos(verifier=mock_verifier) assert ( service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) @@ -36,9 +38,9 @@ def test_get_user_terms_of_service_consent(context, consenting_alice, unwilling_ def test_getting_consent_status_does_not_create_new_user_if_one_does_not_yet_exist( - context, consenting_alice, unwilling_bob + context, consenting_alice, unwilling_bob, mock_verifier ): - service = BasicUserTos() + service = BasicUserTos(verifier=mock_verifier) assert ( service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) @@ -53,9 +55,9 @@ def test_getting_consent_status_does_not_create_new_user_if_one_does_not_yet_exi assert database.user.get_by_address(unwilling_bob.address) is None -def test_can_add_users_consent(context, alice): +def test_can_add_users_consent(context, alice, mock_verifier): signature = build_user_signature(alice) - service = BasicUserTos() + service = BasicUserTos(verifier=mock_verifier) assert service.has_user_agreed_to_terms_of_service(context, alice.address) is False service.add_user_terms_of_service_consent( @@ -65,7 +67,7 @@ def test_can_add_users_consent(context, alice): def test_cannot_add_users_consent_twice(context, consenting_alice): - service = BasicUserTos() + service = BasicUserTos(verifier=BasicUserTosVerifier()) signature = build_user_signature(consenting_alice) assert ( service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) @@ -83,7 +85,7 @@ def test_cannot_add_users_consent_twice(context, consenting_alice): def test_rejects_to_add_consent_with_invalid_signature(context, alice, bob): - service = BasicUserTos() + service = BasicUserTos(verifier=BasicUserTosVerifier()) signature = build_user_signature(bob, alice.address) # when diff --git a/backend/tests/modules/user/tos/test_tos_core.py b/backend/tests/modules/user/tos/test_tos_core.py index fe617ea8a7..6808cce17a 100644 --- a/backend/tests/modules/user/tos/test_tos_core.py +++ b/backend/tests/modules/user/tos/test_tos_core.py @@ -1,7 +1,6 @@ import pytest -from eth_account.messages import encode_defunct -from app.modules.user.tos.core import build_consent_message, verify_signature +from app.modules.user.tos.core import verify_signature from tests.conftest import MOCK_IS_CONTRACT from tests.helpers.signature import build_user_signature From cb4bb8c482a49bca04bc873a55e420f7bb602e29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Mon, 25 Mar 2024 15:20:35 +0100 Subject: [PATCH 060/107] fix: fix former migration + fix route + add missing migration --- backend/app/infrastructure/routes/__init__.py | 1 + .../routes/multisig_signatures.py | 16 ++++++-- ...0fabc0926_add_multisig_signatures_table.py | 38 +++++++++++++++++++ 3 files changed, 51 insertions(+), 4 deletions(-) create mode 100644 backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py diff --git a/backend/app/infrastructure/routes/__init__.py b/backend/app/infrastructure/routes/__init__.py index 6ee99a87a7..3df9d23b3f 100644 --- a/backend/app/infrastructure/routes/__init__.py +++ b/backend/app/infrastructure/routes/__init__.py @@ -15,6 +15,7 @@ epochs, user, validators_stats, + multisig_signatures, ) diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py index 14301db0a9..9da9bfd776 100644 --- a/backend/app/infrastructure/routes/multisig_signatures.py +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -15,7 +15,7 @@ ) api.add_namespace(ns) -pending_signature = api.model( +pending_signature_response = api.model( "PendingSignature", { "message": fields.String(description="The message to be signed."), @@ -24,9 +24,17 @@ ) -@ns.route("/pending//type/") +pending_signature_request = api.model( + "PendingSignature", + { + "message": fields.String(description="The message to be signed."), + }, +) + + +@ns.route("/pending//type/") class MultisigPendingSignature(OctantResource): - @ns.marshal_with(pending_signature) + @ns.marshal_with(pending_signature_response) @ns.response(200, "Success") @ns.doc( description="Retrieve last pending multisig signature for a specific user and type." @@ -40,7 +48,7 @@ def get(self, user_address: str, op_type: str): return response - @ns.expect(pending_signature) + @ns.expect(pending_signature_request) @ns.response(201, "Success") def post(self, user_address: str, op_type: str): app.logger.debug( diff --git a/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py b/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py new file mode 100644 index 0000000000..8d0a8fd590 --- /dev/null +++ b/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py @@ -0,0 +1,38 @@ +"""add multisig signatures table + +Revision ID: 8d20fabc0926 +Revises: 1f89a0fae4ae +Create Date: 2024-03-25 14:46:00.087064 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "8d20fabc0926" +down_revision = "1f89a0fae4ae" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "multisig_signatures", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("address", sa.String(length=42), nullable=False), + sa.Column("type", sa.String(), nullable=False), + sa.Column("message", sa.String(), nullable=False), + sa.Column("hash", sa.String(), nullable=False), + sa.Column("status", sa.String(), nullable=False), + sa.Column("created_at", sa.TIMESTAMP(), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("multisig_signatures") + # ### end Alembic commands ### From 779d3d5c8d33fdef2ae166c0e4a14d5a28db513c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Wed, 27 Mar 2024 12:17:01 +0100 Subject: [PATCH 061/107] fix: fix after rebasing --- backend/app/modules/user/allocations/service/pending.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 79c5b5d5e5..500db05aae 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -31,7 +31,6 @@ def get_all_patrons_addresses(self, context: Context) -> List[str]: class PendingUserAllocationsVerifier(Verifier, Model): - octant_rewards: OctantRewards user_budgets: UserBudgetProtocol patrons_mode: GetPatronsAddressesProtocol From 79a03c5ff97bb5bd4eff618453f69e7841ceac40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Wed, 27 Mar 2024 12:53:45 +0100 Subject: [PATCH 062/107] fix: fixes after review --- .../database/multisig_signature.py | 8 ++------ backend/app/modules/common/verifier.py | 5 +++-- .../app/modules/modules_factory/current.py | 6 +++--- .../user/tos/service/{basic.py => initial.py} | 8 ++++---- ...0fabc0926_add_multisig_signatures_table.py | 5 ----- .../modules_factory/test_modules_factory.py | 6 +++--- .../test_offchain_multisig_signatures.py | 1 + .../allocations/test_pending_allocations.py | 20 ++++++++----------- .../modules/user/tos/test_basic_user_tos.py | 12 +++++------ 9 files changed, 30 insertions(+), 41 deletions(-) rename backend/app/modules/user/tos/service/{basic.py => initial.py} (88%) diff --git a/backend/app/infrastructure/database/multisig_signature.py b/backend/app/infrastructure/database/multisig_signature.py index 98f35e4b8e..d928ab2424 100644 --- a/backend/app/infrastructure/database/multisig_signature.py +++ b/backend/app/infrastructure/database/multisig_signature.py @@ -1,4 +1,3 @@ -from datetime import datetime from enum import StrEnum from typing import Optional @@ -13,15 +12,12 @@ class SigStatus(StrEnum): def get_last_pending_signature( - user_address: str, op_type: SignatureOpType, dt: Optional[datetime] = None -) -> MultisigSignatures | None: + user_address: str, op_type: SignatureOpType +) -> Optional[MultisigSignatures]: last_signature = MultisigSignatures.query.filter_by( address=user_address, type=op_type, status=SigStatus.PENDING ).order_by(MultisigSignatures.created_at.desc()) - if dt is not None: - last_signature.filter(MultisigSignatures.created_at <= dt) - return last_signature.first() diff --git a/backend/app/modules/common/verifier.py b/backend/app/modules/common/verifier.py index 3d813733a1..38c1035d70 100644 --- a/backend/app/modules/common/verifier.py +++ b/backend/app/modules/common/verifier.py @@ -1,4 +1,5 @@ from abc import ABC, abstractmethod +from typing import Optional from app.context.manager import Context from app.pydantic import Model @@ -6,11 +7,11 @@ class Verifier(ABC, Model): @abstractmethod - def _verify_logic(self, context: Context, **kwargs) -> bool: + def _verify_logic(self, context: Context, **kwargs) -> Optional[bool]: ... @abstractmethod - def _verify_signature(self, context: Context, **kwargs) -> bool: + def _verify_signature(self, context: Context, **kwargs) -> Optional[bool]: ... def verify_logic(self, context: Context, **kwargs) -> bool: diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index 023d4e96ae..b50000176b 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -23,7 +23,7 @@ DbAndGraphEventsGenerator, ) from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode -from app.modules.user.tos.service.basic import BasicUserTos, BasicUserTosVerifier +from app.modules.user.tos.service.initial import InitialUserTos, InitialUserTosVerifier from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.pydantic import Model from app.shared.blockchain_types import compare_blockchain_types, ChainTypes @@ -70,8 +70,8 @@ def create(chain_id: int) -> "CurrentServices": ) user_allocations = SavedUserAllocations() user_withdrawals = FinalizedWithdrawals() - tos_verifier = BasicUserTosVerifier() - user_tos = BasicUserTos(verifier=tos_verifier) + tos_verifier = InitialUserTosVerifier() + user_tos = InitialUserTos(verifier=tos_verifier) patron_donations = EventsBasedUserPatronMode() history = FullHistory( user_deposits=user_deposits, diff --git a/backend/app/modules/user/tos/service/basic.py b/backend/app/modules/user/tos/service/initial.py similarity index 88% rename from backend/app/modules/user/tos/service/basic.py rename to backend/app/modules/user/tos/service/initial.py index 90dbc89010..82925d0732 100644 --- a/backend/app/modules/user/tos/service/basic.py +++ b/backend/app/modules/user/tos/service/initial.py @@ -3,11 +3,11 @@ from app.exceptions import DuplicateConsent, InvalidSignature from app.infrastructure import database from app.modules.common.verifier import Verifier -from app.modules.user.tos.core import verify_signature +from app.modules.user.tos import core from app.pydantic import Model -class BasicUserTosVerifier(Verifier): +class InitialUserTosVerifier(Verifier): def _verify_logic(self, _: Context, **kwargs): user_address = kwargs["user_address"] consent = database.user_consents.get_last_by_address(user_address) @@ -17,11 +17,11 @@ def _verify_logic(self, _: Context, **kwargs): def _verify_signature(self, _: Context, **kwargs): user_address, signature = kwargs["user_address"], kwargs["consent_signature"] - if not verify_signature(user_address, signature): + if not core.verify_signature(user_address, signature): raise InvalidSignature(user_address, signature) -class BasicUserTos(Model): +class InitialUserTos(Model): verifier: Verifier def has_user_agreed_to_terms_of_service( diff --git a/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py b/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py index 8d0a8fd590..6a34f5a3bc 100644 --- a/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py +++ b/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py @@ -9,7 +9,6 @@ import sqlalchemy as sa -# revision identifiers, used by Alembic. revision = "8d20fabc0926" down_revision = "1f89a0fae4ae" branch_labels = None @@ -17,7 +16,6 @@ def upgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.create_table( "multisig_signatures", sa.Column("id", sa.Integer(), nullable=False), @@ -29,10 +27,7 @@ def upgrade(): sa.Column("created_at", sa.TIMESTAMP(), nullable=True), sa.PrimaryKeyConstraint("id"), ) - # ### end Alembic commands ### def downgrade(): - # ### commands auto generated by Alembic - please adjust! ### op.drop_table("multisig_signatures") - # ### end Alembic commands ### diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 40d389d2ec..3f6debf8d9 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -37,7 +37,7 @@ from app.modules.user.patron_mode.service.events_based import EventsBasedUserPatronMode from app.modules.user.rewards.service.calculated import CalculatedUserRewards from app.modules.user.rewards.service.saved import SavedUserRewards -from app.modules.user.tos.service.basic import BasicUserTos, BasicUserTosVerifier +from app.modules.user.tos.service.initial import InitialUserTos, InitialUserTosVerifier from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.modules.withdrawals.service.pending import PendingWithdrawals from app.shared.blockchain_types import ChainTypes @@ -58,8 +58,8 @@ def test_current_services_factory(): user_deposits = CalculatedUserDeposits(events_generator=DbAndGraphEventsGenerator()) user_allocations = SavedUserAllocations() user_withdrawals = FinalizedWithdrawals() - tos_verifier = BasicUserTosVerifier() - user_tos = BasicUserTos(verifier=tos_verifier) + tos_verifier = InitialUserTosVerifier() + user_tos = InitialUserTos(verifier=tos_verifier) patron_donations = EventsBasedUserPatronMode() octant_rewards = CalculatedOctantRewards( staking_proceeds=EstimatedStakingProceeds(), diff --git a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py index d13d3f4a5a..24be1f0bb7 100644 --- a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py +++ b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py @@ -77,6 +77,7 @@ def test_save_signature_when_verified_successfully(context, alice, mock_verifier context, alice.address, SignatureOpType.TOS, {"message": "test_message"} ) + # Then result = database.multisig_signature.get_last_pending_signature( alice.address, SignatureOpType.TOS ) diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index 39fe97e663..4982c5d331 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -1,15 +1,16 @@ import pytest from app import exceptions -from app.engine.projects.rewards import ProjectRewardDTO from app.context.epoch_state import EpochState +from app.engine.projects.rewards import ProjectRewardDTO from app.infrastructure import database +from app.legacy.crypto.eip712 import sign, build_allocations_eip712_data from app.modules.dto import AllocationDTO from app.modules.user.allocations import controller -from app.modules.user.allocations.service.pending import PendingUserAllocations - -from app.legacy.crypto.eip712 import sign, build_allocations_eip712_data - +from app.modules.user.allocations.service.pending import ( + PendingUserAllocations, + PendingUserAllocationsVerifier, +) from tests.conftest import ( mock_graphql, MOCKED_PENDING_EPOCH_NO, @@ -17,25 +18,20 @@ MOCK_GET_USER_BUDGET, ) from tests.helpers import create_epoch_event +from tests.helpers import make_user_allocation from tests.helpers.allocations import ( create_payload, deserialize_allocations, - make_user_allocation, -) -from app.modules.user.allocations.service.pending import ( - PendingUserAllocations, - PendingUserAllocationsVerifier, ) from tests.helpers.constants import MATCHED_REWARDS from tests.helpers.context import get_context -from tests.helpers import make_user_allocation def get_allocation_nonce(user_address): return controller.get_user_next_nonce(user_address) -def get_all_by_epoch(epoch, include_zeroes=False): +def get_all_by_epoch(epoch): return controller.get_all_allocations(epoch) diff --git a/backend/tests/modules/user/tos/test_basic_user_tos.py b/backend/tests/modules/user/tos/test_basic_user_tos.py index feb4df4ca8..27026998cc 100644 --- a/backend/tests/modules/user/tos/test_basic_user_tos.py +++ b/backend/tests/modules/user/tos/test_basic_user_tos.py @@ -2,7 +2,7 @@ from app.exceptions import DuplicateConsent, InvalidSignature from app.infrastructure import database -from app.modules.user.tos.service.basic import BasicUserTos, BasicUserTosVerifier +from app.modules.user.tos.service.initial import InitialUserTos, InitialUserTosVerifier from tests.helpers.signature import build_user_signature @@ -25,7 +25,7 @@ def unwilling_bob(bob): def test_get_user_terms_of_service_consent( context, consenting_alice, unwilling_bob, mock_verifier ): - service = BasicUserTos(verifier=mock_verifier) + service = InitialUserTos(verifier=mock_verifier) assert ( service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) @@ -40,7 +40,7 @@ def test_get_user_terms_of_service_consent( def test_getting_consent_status_does_not_create_new_user_if_one_does_not_yet_exist( context, consenting_alice, unwilling_bob, mock_verifier ): - service = BasicUserTos(verifier=mock_verifier) + service = InitialUserTos(verifier=mock_verifier) assert ( service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) @@ -57,7 +57,7 @@ def test_getting_consent_status_does_not_create_new_user_if_one_does_not_yet_exi def test_can_add_users_consent(context, alice, mock_verifier): signature = build_user_signature(alice) - service = BasicUserTos(verifier=mock_verifier) + service = InitialUserTos(verifier=mock_verifier) assert service.has_user_agreed_to_terms_of_service(context, alice.address) is False service.add_user_terms_of_service_consent( @@ -67,7 +67,7 @@ def test_can_add_users_consent(context, alice, mock_verifier): def test_cannot_add_users_consent_twice(context, consenting_alice): - service = BasicUserTos(verifier=BasicUserTosVerifier()) + service = InitialUserTos(verifier=InitialUserTosVerifier()) signature = build_user_signature(consenting_alice) assert ( service.has_user_agreed_to_terms_of_service(context, consenting_alice.address) @@ -85,7 +85,7 @@ def test_cannot_add_users_consent_twice(context, consenting_alice): def test_rejects_to_add_consent_with_invalid_signature(context, alice, bob): - service = BasicUserTos(verifier=BasicUserTosVerifier()) + service = InitialUserTos(verifier=InitialUserTosVerifier()) signature = build_user_signature(bob, alice.address) # when From 23329924b91c23b5263a4867a94ba5f08e25bb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Wed, 27 Mar 2024 13:18:01 +0100 Subject: [PATCH 063/107] test: fix unit tests --- backend/tests/conftest.py | 5 ++++- backend/tests/modules/user/tos/test_basic_user_tos.py | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index a2736a8cb4..45e3ea07ee 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -25,6 +25,7 @@ from app.modules.common.verifier import Verifier from app.modules.dto import AccountFundsDTO, AllocationItem from app.settings import DevConfig, TestConfig +from tests.helpers import make_user_allocation from tests.helpers.constants import ( ALICE, BOB, @@ -51,7 +52,6 @@ MATCHED_REWARDS_AFTER_OVERHAUL, NO_PATRONS_REWARDS, ) -from tests.helpers import make_user_allocation from tests.helpers.context import get_context from tests.helpers.gql_client import MockGQLClient from tests.helpers.mocked_epoch_details import EPOCH_EVENTS @@ -460,6 +460,9 @@ def patch_vault(monkeypatch): @pytest.fixture(scope="function") def patch_is_contract(monkeypatch): + monkeypatch.setattr( + "app.legacy.crypto.eth_sign.signature.is_contract", MOCK_IS_CONTRACT + ) monkeypatch.setattr("app.modules.common.signature.is_contract", MOCK_IS_CONTRACT) MOCK_IS_CONTRACT.return_value = False diff --git a/backend/tests/modules/user/tos/test_basic_user_tos.py b/backend/tests/modules/user/tos/test_basic_user_tos.py index 27026998cc..570202ed7f 100644 --- a/backend/tests/modules/user/tos/test_basic_user_tos.py +++ b/backend/tests/modules/user/tos/test_basic_user_tos.py @@ -7,7 +7,7 @@ @pytest.fixture(autouse=True) -def before(app): +def before(app, patch_is_contract): pass From 4842e03868ab2b5c4f9a77f4ca3f0175ee30235a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Wed, 27 Mar 2024 14:32:11 +0100 Subject: [PATCH 064/107] fix: fix db revision --- ...y => f923075e5877_add_multisig_signatures_table.py} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename backend/migrations/versions/{8d20fabc0926_add_multisig_signatures_table.py => f923075e5877_add_multisig_signatures_table.py} (83%) diff --git a/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py b/backend/migrations/versions/f923075e5877_add_multisig_signatures_table.py similarity index 83% rename from backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py rename to backend/migrations/versions/f923075e5877_add_multisig_signatures_table.py index 6a34f5a3bc..f3986525ad 100644 --- a/backend/migrations/versions/8d20fabc0926_add_multisig_signatures_table.py +++ b/backend/migrations/versions/f923075e5877_add_multisig_signatures_table.py @@ -1,16 +1,16 @@ """add multisig signatures table -Revision ID: 8d20fabc0926 -Revises: 1f89a0fae4ae -Create Date: 2024-03-25 14:46:00.087064 +Revision ID: f923075e5877 +Revises: 7bb6835486a5 +Create Date: 2024-03-27 14:30:56.869637 """ from alembic import op import sqlalchemy as sa -revision = "8d20fabc0926" -down_revision = "1f89a0fae4ae" +revision = "f923075e5877" +down_revision = "7bb6835486a5" branch_labels = None depends_on = None From aec955a78e7c0587d13fb6d441658344540c3245 Mon Sep 17 00:00:00 2001 From: Pawel Peregud Date: Wed, 27 Mar 2024 13:12:24 +0100 Subject: [PATCH 065/107] implement verify_signature for multisig messages Handles both EIP-191 and EIP-712 message formats. --- backend/app/legacy/crypto/eip1271.py | 9 +-- backend/app/legacy/crypto/eip712.py | 8 ++- .../app/legacy/crypto/eth_sign/signature.py | 4 +- backend/app/modules/common/signature.py | 59 +++++++++++++++---- .../multisig_signatures/service/offchain.py | 10 +++- .../user/allocations/service/pending.py | 14 ++++- backend/app/modules/user/tos/core.py | 9 ++- backend/tests/legacy/test_rewards.py | 1 + .../allocations/test_pending_allocations.py | 1 + .../tests/modules/user/tos/test_tos_core.py | 2 +- 10 files changed, 89 insertions(+), 28 deletions(-) diff --git a/backend/app/legacy/crypto/eip1271.py b/backend/app/legacy/crypto/eip1271.py index 4318da2a31..d534c35fee 100644 --- a/backend/app/legacy/crypto/eip1271.py +++ b/backend/app/legacy/crypto/eip1271.py @@ -1,13 +1,10 @@ -from eth_account.messages import defunct_hash_message - from app.infrastructure.contracts import abi from app.infrastructure.contracts.gnosis_safe import GnosisSafe from app.extensions import w3 -def is_valid_signature(address: str, message: str, signature: str) -> bool: +def is_valid_signature(address: str, msg_hash: str, signature: str) -> bool: contract = GnosisSafe( - w3=w3, contact=w3.eth.contract(address=address, abi=abi.GNOSIS_SAFE) + w3=w3, contract=w3.eth.contract(address=address, abi=abi.GNOSIS_SAFE) ) - msg_hash = defunct_hash_message(text=message) - return contract.is_valid_signature(msg_hash.hex(), signature) + return contract.is_valid_signature(msg_hash, signature) diff --git a/backend/app/legacy/crypto/eip712.py b/backend/app/legacy/crypto/eip712.py index ff1ccc685a..09d1dd5af8 100644 --- a/backend/app/legacy/crypto/eip712.py +++ b/backend/app/legacy/crypto/eip712.py @@ -1,12 +1,12 @@ from typing import Union from eth_account import Account -from eth_account.messages import encode_structured_data from eth_account.signers.local import LocalAccount from flask import current_app as app from app.extensions import w3 from app.modules.dto import UserAllocationPayload +from app.modules.common.signature import encode_for_signing, EncodingStandardFor def build_domain(): @@ -89,7 +89,9 @@ def sign(account: Union[Account, LocalAccount], data: dict) -> str: Signs the provided message with w3.eth.account following EIP-712 structure :returns signature as a hexadecimal string. """ - return account.sign_message(encode_structured_data(data)).signature.hex() + return account.sign_message( + encode_for_signing(EncodingStandardFor.DATA, data) + ).signature.hex() def recover_address(data: dict, signature: str) -> str: @@ -98,5 +100,5 @@ def recover_address(data: dict, signature: str) -> str: :returns address as a hexadecimal string. """ return w3.eth.account.recover_message( - encode_structured_data(data), signature=signature + encode_for_signing(EncodingStandardFor.DATA, data), signature=signature ) diff --git a/backend/app/legacy/crypto/eth_sign/signature.py b/backend/app/legacy/crypto/eth_sign/signature.py index a74e3e939a..610826f399 100644 --- a/backend/app/legacy/crypto/eth_sign/signature.py +++ b/backend/app/legacy/crypto/eth_sign/signature.py @@ -5,6 +5,7 @@ from app.legacy.crypto.account import is_contract from app.legacy.crypto.eip1271 import is_valid_signature +from app.modules.common.signature import hash_signable_message, EncodingStandardFor def verify_signed_message(user_address: str, msg_text: str, signature: str) -> bool: @@ -15,8 +16,9 @@ def verify_signed_message(user_address: str, msg_text: str, signature: str) -> b def _verify_multisig(user_address: str, msg_text: str, signature: str) -> bool: + msg_hash = hash_signable_message(EncodingStandardFor.TEXT, msg_text) try: - return is_valid_signature(user_address, msg_text, signature) + return is_valid_signature(user_address, msg_hash, signature) except ContractLogicError: return False diff --git a/backend/app/modules/common/signature.py b/backend/app/modules/common/signature.py index 1bb46319f3..c10b3adbb8 100644 --- a/backend/app/modules/common/signature.py +++ b/backend/app/modules/common/signature.py @@ -1,5 +1,20 @@ +""" +Handles signatures from two types of signers (EOA and smart contracts), +for two different formats of signed message. + +First is known as `personal_sign` (known as EIP-191, version E) and signs strings is a safe way. +Second one (EIP-1271) is used for signing structured data and builds on top of the first one. +""" +from enum import StrEnum +from typing import Union, Dict + from eth_account import Account -from eth_account.messages import encode_defunct, defunct_hash_message +from eth_account.messages import ( + SignableMessage, + _hash_eip191_message, + encode_defunct, + encode_structured_data, +) from eth_keys.exceptions import BadSignature from web3.exceptions import ContractLogicError @@ -7,26 +22,48 @@ from app.legacy.crypto.eip1271 import is_valid_signature -def verify_signed_message(user_address: str, msg_text: str, signature: str) -> bool: - if is_contract(user_address): - return _verify_multisig(user_address, msg_text, signature) +class EncodingStandardFor(StrEnum): + TEXT = "eip191" + DATA = "eip1271" + + +def verify_signed_message( + user_address: str, encoded_msg: SignableMessage, signature: str +) -> bool: + contract = is_contract(user_address) + if contract: + return _verify_multisig(user_address, encoded_msg, signature) else: - return _verify_eoa(user_address, msg_text, signature) + return _verify_eoa(user_address, encoded_msg, signature) + + +def encode_for_signing( + standard: EncodingStandardFor, message: Union[str | Dict] +) -> SignableMessage: + if standard == EncodingStandardFor.DATA: + return encode_structured_data(message) + if standard == EncodingStandardFor.TEXT: + return encode_defunct(text=message) + raise ValueError(standard) -def hash_message(msg_text: str) -> str: - return defunct_hash_message(text=msg_text).hex() +def hash_signable_message(encoded_msg: SignableMessage) -> str: + return "0x" + _hash_eip191_message(encoded_msg).hex() -def _verify_multisig(user_address: str, msg_text: str, signature: str) -> bool: +def _verify_multisig( + user_address: str, encoded_msg: SignableMessage, signature: str +) -> bool: + msg_hash = hash_signable_message(encoded_msg) try: - return is_valid_signature(user_address, msg_text, signature) + return is_valid_signature(user_address, msg_hash, signature) except ContractLogicError: return False -def _verify_eoa(user_address: str, msg_text: str, signature: str) -> bool: - encoded_msg = encode_defunct(text=msg_text) +def _verify_eoa( + user_address: str, encoded_msg: SignableMessage, signature: str +) -> bool: try: recovered_address = Account.recover_message(encoded_msg, signature=signature) except BadSignature: diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index 66c62662da..07bc34a856 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -4,7 +4,11 @@ from app.exceptions import InvalidMultisigSignatureRequest from app.extensions import db from app.infrastructure import database -from app.modules.common.signature import hash_message +from app.modules.common.signature import ( + encode_for_signing, + EncodingStandardFor, + hash_signable_message, +) from app.modules.common.verifier import Verifier from app.modules.dto import SignatureOpType from app.modules.multisig_signatures.dto import Signature @@ -43,6 +47,8 @@ def save_pending_signature( raise InvalidMultisigSignatureRequest() msg = json.dumps(signature_data) - msg_hash = hash_message(msg) + msg_hash = hash_signable_message( + encode_for_signing(EncodingStandardFor.TEXT, msg) + ) database.multisig_signature.save_signature(user_address, op_type, msg, msg_hash) db.session.commit() diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 500db05aae..ba3ea99e3a 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -3,8 +3,15 @@ from app import exceptions from app.context.manager import Context from app.engine.projects.rewards import ProjectRewardDTO +from app.exceptions import InvalidSignature from app.extensions import db from app.infrastructure import database +from app.modules.common.signature import ( + verify_signed_message, + encode_for_signing, + EncodingStandardFor, +) +from app.legacy.crypto.eip712 import build_allocations_eip712_structure from app.modules.common.verifier import Verifier from app.modules.dto import AllocationDTO, UserAllocationRequestPayload from app.modules.user.allocations import core @@ -48,8 +55,11 @@ def _verify_logic(self, context: Context, **kwargs): ) def _verify_signature(self, _: Context, **kwargs): - # TODO: implement verify_signature - ... + user_address, signature = kwargs["user_address"], kwargs["payload"].signature + eip712_encoded = build_allocations_eip712_structure(kwargs["payload"].payload) + encoded_msg = encode_for_signing(EncodingStandardFor.DATA, eip712_encoded) + if not verify_signed_message(user_address, encoded_msg, signature): + raise InvalidSignature() class PendingUserAllocations(SavedUserAllocations, Model): diff --git a/backend/app/modules/user/tos/core.py b/backend/app/modules/user/tos/core.py index 45c525f0f1..cf0b2d1348 100644 --- a/backend/app/modules/user/tos/core.py +++ b/backend/app/modules/user/tos/core.py @@ -1,4 +1,8 @@ -from app.modules.common.signature import verify_signed_message +from app.modules.common.signature import ( + verify_signed_message, + encode_for_signing, + EncodingStandardFor, +) def build_consent_message(user_address: str) -> str: @@ -17,5 +21,6 @@ def build_consent_message(user_address: str) -> str: def verify_signature(user_address: str, signature: str) -> bool: msg_text = build_consent_message(user_address) + encoded_msg = encode_for_signing(EncodingStandardFor.TEXT, msg_text) - return verify_signed_message(user_address, msg_text, signature) + return verify_signed_message(user_address, encoded_msg, signature) diff --git a/backend/tests/legacy/test_rewards.py b/backend/tests/legacy/test_rewards.py index b1800ac328..54f59eadc2 100644 --- a/backend/tests/legacy/test_rewards.py +++ b/backend/tests/legacy/test_rewards.py @@ -29,6 +29,7 @@ def before( patch_proposals, patch_has_pending_epoch_snapshot, patch_user_budget, + patch_is_contract, ): MOCK_PROPOSALS.get_proposal_addresses.return_value = [ p.address for p in proposal_accounts[0:5] diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index 4982c5d331..4bf42ce34a 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -45,6 +45,7 @@ def before( patch_proposals, patch_has_pending_epoch_snapshot, patch_user_budget, + patch_is_contract, ): MOCK_PROPOSALS.get_proposal_addresses.return_value = [ p.address for p in proposal_accounts[0:5] diff --git a/backend/tests/modules/user/tos/test_tos_core.py b/backend/tests/modules/user/tos/test_tos_core.py index 6808cce17a..96be970236 100644 --- a/backend/tests/modules/user/tos/test_tos_core.py +++ b/backend/tests/modules/user/tos/test_tos_core.py @@ -6,7 +6,7 @@ @pytest.fixture(autouse=True) -def before(patch_is_contract): +def before(app, patch_is_contract): pass From ec8ccc7f5ff9f557a664fff2188fd698c22786e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Wed, 27 Mar 2024 17:42:56 +0100 Subject: [PATCH 066/107] fix: client -- do not call /allocate/leverage when not connected, show 0 threshold when not available --- .../AllocationItemRewards/AllocationItemRewards.tsx | 8 +++++--- client/src/views/AllocationView/AllocationView.tsx | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx b/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx index fd183ab5a0..81f38f9db3 100644 --- a/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx +++ b/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx @@ -2,6 +2,7 @@ import cx from 'classnames'; import { motion } from 'framer-motion'; import React, { FC, useEffect, useState } from 'react'; import { useTranslation, Trans } from 'react-i18next'; +import { useAccount } from 'wagmi'; import useIsDonationAboveThreshold from 'hooks/helpers/useIsDonationAboveThreshold'; import useMediaQuery from 'hooks/helpers/useMediaQuery'; @@ -34,6 +35,7 @@ const AllocationItemRewards: FC = ({ keyPrefix: 'views.allocation.allocationItem', }); const { isDesktop } = useMediaQuery(); + const { isConnected } = useAccount(); const { data: currentEpoch } = useCurrentEpoch(); const { data: individualReward } = useIndividualReward(); const { data: userAllocations } = useUserAllocations(); @@ -112,8 +114,7 @@ const AllocationItemRewards: FC = ({ const rewardsSumWithValueAndSimulationFormatted = getFormattedEthValue( rewardsSumWithValueAndSimulation, ); - const thresholdToUseFormatted = - thresholdToUse !== undefined ? getFormattedEthValue(thresholdToUse) : undefined; + const thresholdToUseFormatted = getFormattedEthValue(thresholdToUse || BigInt(0)); const areValueAndSimulatedSuffixesTheSame = valueFormatted.suffix === simulatedMatchedFormatted?.suffix; @@ -152,7 +153,8 @@ const AllocationItemRewards: FC = ({ !isLoadingAllocateSimulate && !isRewardsDataDefined && !simulatedMatched && - !isDecisionWindowOpen)) && + !isDecisionWindowOpen && + !isConnected)) && i18n.t( isDesktop ? 'common.thresholdDataUnavailable.desktop' diff --git a/client/src/views/AllocationView/AllocationView.tsx b/client/src/views/AllocationView/AllocationView.tsx index fe10daf513..ab1aa209d1 100644 --- a/client/src/views/AllocationView/AllocationView.tsx +++ b/client/src/views/AllocationView/AllocationView.tsx @@ -315,7 +315,8 @@ const AllocationView = (): ReactElement => { allocationValues.length === 0 || areAllValuesZero || addressesWithError.length > 0 || - !isDecisionWindowOpen + !isDecisionWindowOpen || + !isConnected ) { return; } From 9f37856009a88fa09c6386c6ef38d872b9ce2fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 28 Mar 2024 09:57:16 +0100 Subject: [PATCH 067/107] test: disable earn.cy.ts lock 1000 GLM scenario --- client/cypress/e2e/earn.cy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index ea1c8192ce..fa5cabafe7 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -210,7 +210,9 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); }); - it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { + // TODO OCT-1506 enable this scenario. + // eslint-disable-next-line jest/no-disabled-tests + it.skip('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { connectWallet(); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') From 24d7615d066fb955c5a734f46a0fff4833c52000 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 28 Mar 2024 09:57:16 +0100 Subject: [PATCH 068/107] test: disable earn.cy.ts lock 1000 GLM scenario --- client/cypress/e2e/earn.cy.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index ea1c8192ce..fa5cabafe7 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -210,7 +210,9 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); }); - it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { + // TODO OCT-1506 enable this scenario. + // eslint-disable-next-line jest/no-disabled-tests + it.skip('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { connectWallet(); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') From 69a840c4a58d867e4af567d679648f5b524b03ed Mon Sep 17 00:00:00 2001 From: Housekeeper Bot Date: Thu, 28 Mar 2024 10:15:05 +0000 Subject: [PATCH 069/107] [CI/CD] Update uat.env contracts --- ci/argocd/contracts/uat.env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ci/argocd/contracts/uat.env b/ci/argocd/contracts/uat.env index a7882df5ca..4bdc1a845f 100644 --- a/ci/argocd/contracts/uat.env +++ b/ci/argocd/contracts/uat.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=5489785 +BLOCK_NUMBER=5577583 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0xCb01c9275bb53FD55e23Ad3a1cF91679E5efa1cF -DEPOSITS_CONTRACT_ADDRESS=0x9e1859c27D0Be7a15c07B4f023505f9D5e929ee7 -EPOCHS_CONTRACT_ADDRESS=0x0EB07F9C2D86d46A08F8B583d10c5834872EBc66 -PROPOSALS_CONTRACT_ADDRESS=0x8f43149D7E1eC70e7420c09ABE2a6C974F54D789 -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x9E47DCbcE02f3D83a0ea56EA0d90A6A6D9896369 -VAULT_CONTRACT_ADDRESS=0x9021A0d8EDDfe058F23d9BA82A4C7412ce85f1e1 +AUTH_CONTRACT_ADDRESS=0xF2C1C0EB8bD6eE15629Eca8AF44D869c0369B5cB +DEPOSITS_CONTRACT_ADDRESS=0xd8965459D357CCdeb9548DE4083f32eEf657BEa4 +EPOCHS_CONTRACT_ADDRESS=0xa1f4090B85a40e9Fb447Ad8FE2081Bca584b3C46 +PROPOSALS_CONTRACT_ADDRESS=0xbf121932993EbE7432c7613fF9CE1161516d097C +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x19123Ee7a29adb3e1574a53Ce965194DD2eB0baD +VAULT_CONTRACT_ADDRESS=0xf493bfAe0Ba5832BFaAA38Ac19837e2bE834b17D From a2df7e84f431c1e0f89a41c153ad7d7af3ece373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Thu, 28 Mar 2024 13:00:14 +0100 Subject: [PATCH 070/107] CAQD-348: Adjust permissions for CI runs in PR from forks --- .github/workflows/ci-run.yml | 57 +++++++++++++++++++++++++++ .github/workflows/deploy-pr.yml | 18 +++++++++ .github/workflows/tpl-destroy-env.yml | 18 +++++++++ 3 files changed, 93 insertions(+) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index 070c9f48fb..ac074378e2 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -219,6 +219,8 @@ jobs: start-e2e-env: name: Start E2E Env + needs: + - permissions-check uses: ./.github/workflows/tpl-start-env.yml secrets: inherit with: @@ -229,6 +231,8 @@ jobs: start-apitest-env: name: Start APITest Env + needs: + - permissions-check uses: ./.github/workflows/tpl-start-env.yml secrets: inherit with: @@ -239,10 +243,13 @@ jobs: docker: name: Docker + needs: + - permissions-check uses: ./.github/workflows/tpl-images.yml secrets: inherit with: image-tag: ${{ github.sha }} + # +------------------------- # | Tests: NodeJS # +------------------------- @@ -390,6 +397,7 @@ jobs: yarn eslint yarn type-check shell: bash + # +------------------------- # | Build # | client @@ -428,6 +436,7 @@ jobs: ${{ matrix.SERVICE }}/.yarn ${{ matrix.SERVICE }}/node-modules key: "${{ github.sha }}-yarn-${{ matrix.SERVICE }}" + # +------------------------- # | Build backend # +------------------------- @@ -435,6 +444,8 @@ jobs: name: Build Services runs-on: - metal + needs: + - permissions-check container: image: registry.gitlab.com/golemfoundation/devops/container-builder/octant/python-poetry-ext:ad1d9179 credentials: @@ -447,6 +458,7 @@ jobs: with: path: backend/.venv key: "${{ github.sha }}-poetry-backend" + # +------------------------- # | Build contracts # +------------------------- @@ -454,6 +466,8 @@ jobs: name: Build Contracts runs-on: - metal + needs: + - permissions-check container: image: registry.gitlab.com/golemfoundation/devops/container-builder/octant/node-extended:bdda411c credentials: @@ -480,3 +494,46 @@ jobs: contracts-v1/artifacts contracts-v1/typechain key: "${{ github.sha }}-yarn-contracts-v1-extras" + + + permissions-check: + name: Permissions check + runs-on: + - metal + steps: + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member-pr + with: + result-encoding: string + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.pull_request.user.login }}').toString() + + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member-review + with: + result-encoding: string + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.pull_request_review.sender.login }}').toString() + + - name: Validate CI run checks + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ steps.is-organization-member-pr.outputs.result }}" == "false" ]]; then + echo 'Not an org member' + exit 1 + fi + + if [[ "${{ github.event_name }}" == "pull_request_review" && "${{ steps.is-organization-member-review.outputs.result }}" == "false" && "${{ github.event.review.state }}" == "approved" ]]; then + echo 'Not an org member' + exit 2 + fi + shell: bash diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index f69b23f4d6..eabb79527c 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -59,6 +59,24 @@ jobs: uses: xt0rted/pull-request-comment-branch@v2 id: comment-branch + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member + with: + result-encoding: string + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() + + - name: Cancel workflow + if: ${{ steps.is-organization-member.outputs.result == 'false' }} + run: | + echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' + exit 1 + - uses: actions/github-script@v7 id: get-pr-number with: diff --git a/.github/workflows/tpl-destroy-env.yml b/.github/workflows/tpl-destroy-env.yml index c039088602..c7e1b50cf7 100644 --- a/.github/workflows/tpl-destroy-env.yml +++ b/.github/workflows/tpl-destroy-env.yml @@ -48,6 +48,24 @@ jobs: if: github.event_name == 'issue_comment' id: comment-branch + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member + with: + result-encoding: string + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() + + - name: Cancel workflow + if: ${{ steps.is-organization-member.outputs.result == 'false' }} + run: | + echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' + exit 1 + - uses: actions/github-script@v7 id: get-pr-number if: github.event_name == 'issue_comment' From 2fcbabec1531ac59d5b449c914cf06dbe73a278c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Thu, 28 Mar 2024 13:50:21 +0100 Subject: [PATCH 071/107] CAQD-348: bugfix --- .github/workflows/ci-run.yml | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index ac074378e2..7dc0aa76a5 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -506,6 +506,7 @@ jobs: id: is-organization-member-pr with: result-encoding: string + github-token: ${{ secrets.GH_BOT_TOKEN }} script: | return ( await github.rest.orgs.listMembers({ @@ -513,17 +514,17 @@ jobs: }) ).data.map(({login}) => login).includes('${{ github.event.pull_request.user.login }}').toString() - - name: Check if user is an org member - uses: actions/github-script@v7 - id: is-organization-member-review - with: - result-encoding: string - script: | - return ( - await github.rest.orgs.listMembers({ - org: 'golemfoundation' - }) - ).data.map(({login}) => login).includes('${{ github.event.pull_request_review.sender.login }}').toString() + # - name: Check if user is an org member + # uses: actions/github-script@v7 + # id: is-organization-member-review + # with: + # result-encoding: string + # script: | + # return ( + # await github.rest.orgs.listMembers({ + # org: 'golemfoundation' + # }) + # ).data.map(({login}) => login).includes('${{ github.event.pull_request_review.sender.login }}').toString() - name: Validate CI run checks run: | @@ -532,8 +533,8 @@ jobs: exit 1 fi - if [[ "${{ github.event_name }}" == "pull_request_review" && "${{ steps.is-organization-member-review.outputs.result }}" == "false" && "${{ github.event.review.state }}" == "approved" ]]; then - echo 'Not an org member' - exit 2 - fi + # if [[ "${{ github.event_name }}" == "pull_request_review" && "${{ steps.is-organization-member-review.outputs.result }}" == "false" && "${{ github.event.review.state }}" == "approved" ]]; then + # echo 'Not an org member' + # exit 2 + # fi shell: bash From a91ec0797a79192550c978216060de28120f2372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Thu, 28 Mar 2024 14:07:49 +0100 Subject: [PATCH 072/107] Revert "CAQD-348: Adjust permissions for CI runs in PR from forks" This reverts commit a2df7e84f431c1e0f89a41c153ad7d7af3ece373. --- .github/workflows/ci-run.yml | 58 --------------------------- .github/workflows/deploy-pr.yml | 18 --------- .github/workflows/tpl-destroy-env.yml | 18 --------- 3 files changed, 94 deletions(-) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index 7dc0aa76a5..070c9f48fb 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -219,8 +219,6 @@ jobs: start-e2e-env: name: Start E2E Env - needs: - - permissions-check uses: ./.github/workflows/tpl-start-env.yml secrets: inherit with: @@ -231,8 +229,6 @@ jobs: start-apitest-env: name: Start APITest Env - needs: - - permissions-check uses: ./.github/workflows/tpl-start-env.yml secrets: inherit with: @@ -243,13 +239,10 @@ jobs: docker: name: Docker - needs: - - permissions-check uses: ./.github/workflows/tpl-images.yml secrets: inherit with: image-tag: ${{ github.sha }} - # +------------------------- # | Tests: NodeJS # +------------------------- @@ -397,7 +390,6 @@ jobs: yarn eslint yarn type-check shell: bash - # +------------------------- # | Build # | client @@ -436,7 +428,6 @@ jobs: ${{ matrix.SERVICE }}/.yarn ${{ matrix.SERVICE }}/node-modules key: "${{ github.sha }}-yarn-${{ matrix.SERVICE }}" - # +------------------------- # | Build backend # +------------------------- @@ -444,8 +435,6 @@ jobs: name: Build Services runs-on: - metal - needs: - - permissions-check container: image: registry.gitlab.com/golemfoundation/devops/container-builder/octant/python-poetry-ext:ad1d9179 credentials: @@ -458,7 +447,6 @@ jobs: with: path: backend/.venv key: "${{ github.sha }}-poetry-backend" - # +------------------------- # | Build contracts # +------------------------- @@ -466,8 +454,6 @@ jobs: name: Build Contracts runs-on: - metal - needs: - - permissions-check container: image: registry.gitlab.com/golemfoundation/devops/container-builder/octant/node-extended:bdda411c credentials: @@ -494,47 +480,3 @@ jobs: contracts-v1/artifacts contracts-v1/typechain key: "${{ github.sha }}-yarn-contracts-v1-extras" - - - permissions-check: - name: Permissions check - runs-on: - - metal - steps: - - name: Check if user is an org member - uses: actions/github-script@v7 - id: is-organization-member-pr - with: - result-encoding: string - github-token: ${{ secrets.GH_BOT_TOKEN }} - script: | - return ( - await github.rest.orgs.listMembers({ - org: 'golemfoundation' - }) - ).data.map(({login}) => login).includes('${{ github.event.pull_request.user.login }}').toString() - - # - name: Check if user is an org member - # uses: actions/github-script@v7 - # id: is-organization-member-review - # with: - # result-encoding: string - # script: | - # return ( - # await github.rest.orgs.listMembers({ - # org: 'golemfoundation' - # }) - # ).data.map(({login}) => login).includes('${{ github.event.pull_request_review.sender.login }}').toString() - - - name: Validate CI run checks - run: | - if [[ "${{ github.event_name }}" == "pull_request" && "${{ steps.is-organization-member-pr.outputs.result }}" == "false" ]]; then - echo 'Not an org member' - exit 1 - fi - - # if [[ "${{ github.event_name }}" == "pull_request_review" && "${{ steps.is-organization-member-review.outputs.result }}" == "false" && "${{ github.event.review.state }}" == "approved" ]]; then - # echo 'Not an org member' - # exit 2 - # fi - shell: bash diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index eabb79527c..f69b23f4d6 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -59,24 +59,6 @@ jobs: uses: xt0rted/pull-request-comment-branch@v2 id: comment-branch - - name: Check if user is an org member - uses: actions/github-script@v7 - id: is-organization-member - with: - result-encoding: string - script: | - return ( - await github.rest.orgs.listMembers({ - org: 'golemfoundation' - }) - ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() - - - name: Cancel workflow - if: ${{ steps.is-organization-member.outputs.result == 'false' }} - run: | - echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' - exit 1 - - uses: actions/github-script@v7 id: get-pr-number with: diff --git a/.github/workflows/tpl-destroy-env.yml b/.github/workflows/tpl-destroy-env.yml index c7e1b50cf7..c039088602 100644 --- a/.github/workflows/tpl-destroy-env.yml +++ b/.github/workflows/tpl-destroy-env.yml @@ -48,24 +48,6 @@ jobs: if: github.event_name == 'issue_comment' id: comment-branch - - name: Check if user is an org member - uses: actions/github-script@v7 - id: is-organization-member - with: - result-encoding: string - script: | - return ( - await github.rest.orgs.listMembers({ - org: 'golemfoundation' - }) - ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() - - - name: Cancel workflow - if: ${{ steps.is-organization-member.outputs.result == 'false' }} - run: | - echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' - exit 1 - - uses: actions/github-script@v7 id: get-pr-number if: github.event_name == 'issue_comment' From ff3b4edb4eaa8308bd6816727446d194a8314582 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Thu, 28 Mar 2024 14:52:38 +0100 Subject: [PATCH 073/107] Reapply "CAQD-348: Adjust permissions for CI runs in PR from forks" This reverts commit a91ec0797a79192550c978216060de28120f2372. --- .github/workflows/ci-run.yml | 58 +++++++++++++++++++++++++++ .github/workflows/deploy-pr.yml | 18 +++++++++ .github/workflows/tpl-destroy-env.yml | 18 +++++++++ 3 files changed, 94 insertions(+) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index 070c9f48fb..7dc0aa76a5 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -219,6 +219,8 @@ jobs: start-e2e-env: name: Start E2E Env + needs: + - permissions-check uses: ./.github/workflows/tpl-start-env.yml secrets: inherit with: @@ -229,6 +231,8 @@ jobs: start-apitest-env: name: Start APITest Env + needs: + - permissions-check uses: ./.github/workflows/tpl-start-env.yml secrets: inherit with: @@ -239,10 +243,13 @@ jobs: docker: name: Docker + needs: + - permissions-check uses: ./.github/workflows/tpl-images.yml secrets: inherit with: image-tag: ${{ github.sha }} + # +------------------------- # | Tests: NodeJS # +------------------------- @@ -390,6 +397,7 @@ jobs: yarn eslint yarn type-check shell: bash + # +------------------------- # | Build # | client @@ -428,6 +436,7 @@ jobs: ${{ matrix.SERVICE }}/.yarn ${{ matrix.SERVICE }}/node-modules key: "${{ github.sha }}-yarn-${{ matrix.SERVICE }}" + # +------------------------- # | Build backend # +------------------------- @@ -435,6 +444,8 @@ jobs: name: Build Services runs-on: - metal + needs: + - permissions-check container: image: registry.gitlab.com/golemfoundation/devops/container-builder/octant/python-poetry-ext:ad1d9179 credentials: @@ -447,6 +458,7 @@ jobs: with: path: backend/.venv key: "${{ github.sha }}-poetry-backend" + # +------------------------- # | Build contracts # +------------------------- @@ -454,6 +466,8 @@ jobs: name: Build Contracts runs-on: - metal + needs: + - permissions-check container: image: registry.gitlab.com/golemfoundation/devops/container-builder/octant/node-extended:bdda411c credentials: @@ -480,3 +494,47 @@ jobs: contracts-v1/artifacts contracts-v1/typechain key: "${{ github.sha }}-yarn-contracts-v1-extras" + + + permissions-check: + name: Permissions check + runs-on: + - metal + steps: + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member-pr + with: + result-encoding: string + github-token: ${{ secrets.GH_BOT_TOKEN }} + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.pull_request.user.login }}').toString() + + # - name: Check if user is an org member + # uses: actions/github-script@v7 + # id: is-organization-member-review + # with: + # result-encoding: string + # script: | + # return ( + # await github.rest.orgs.listMembers({ + # org: 'golemfoundation' + # }) + # ).data.map(({login}) => login).includes('${{ github.event.pull_request_review.sender.login }}').toString() + + - name: Validate CI run checks + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ steps.is-organization-member-pr.outputs.result }}" == "false" ]]; then + echo 'Not an org member' + exit 1 + fi + + # if [[ "${{ github.event_name }}" == "pull_request_review" && "${{ steps.is-organization-member-review.outputs.result }}" == "false" && "${{ github.event.review.state }}" == "approved" ]]; then + # echo 'Not an org member' + # exit 2 + # fi + shell: bash diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index f69b23f4d6..eabb79527c 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -59,6 +59,24 @@ jobs: uses: xt0rted/pull-request-comment-branch@v2 id: comment-branch + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member + with: + result-encoding: string + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() + + - name: Cancel workflow + if: ${{ steps.is-organization-member.outputs.result == 'false' }} + run: | + echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' + exit 1 + - uses: actions/github-script@v7 id: get-pr-number with: diff --git a/.github/workflows/tpl-destroy-env.yml b/.github/workflows/tpl-destroy-env.yml index c039088602..c7e1b50cf7 100644 --- a/.github/workflows/tpl-destroy-env.yml +++ b/.github/workflows/tpl-destroy-env.yml @@ -48,6 +48,24 @@ jobs: if: github.event_name == 'issue_comment' id: comment-branch + - name: Check if user is an org member + uses: actions/github-script@v7 + id: is-organization-member + with: + result-encoding: string + script: | + return ( + await github.rest.orgs.listMembers({ + org: 'golemfoundation' + }) + ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() + + - name: Cancel workflow + if: ${{ steps.is-organization-member.outputs.result == 'false' }} + run: | + echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' + exit 1 + - uses: actions/github-script@v7 id: get-pr-number if: github.event_name == 'issue_comment' From e48d17e68db4f96308f2a3b32d1596cde8e4f23d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Thu, 28 Mar 2024 15:09:51 +0100 Subject: [PATCH 074/107] Destroy env bugfix --- .github/workflows/tpl-destroy-env.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tpl-destroy-env.yml b/.github/workflows/tpl-destroy-env.yml index c7e1b50cf7..de4835a08c 100644 --- a/.github/workflows/tpl-destroy-env.yml +++ b/.github/workflows/tpl-destroy-env.yml @@ -61,7 +61,7 @@ jobs: ).data.map(({login}) => login).includes('${{ github.event.comment.user.login }}').toString() - name: Cancel workflow - if: ${{ steps.is-organization-member.outputs.result == 'false' }} + if: ${{ github.event_name == 'issue_comment' && steps.is-organization-member.outputs.result == 'false' }} run: | echo '${{ github.event.comment.user.login }} is not a member of the golemfoundation org' exit 1 From 5073dfe0407845a5b5a38977d6b9e85a87aaa873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 28 Mar 2024 15:47:38 +0100 Subject: [PATCH 075/107] Revert "oct-1043: pending transaction loader - filter" This reverts commit d1c6714c33b3b340030bf35317437346dcf3efbe. --- .../Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx | 19 +++++-------------- .../EarnBoxPersonalAllocation.tsx | 18 ++++-------------- 2 files changed, 9 insertions(+), 28 deletions(-) diff --git a/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx b/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx index 4a590d18e2..a1730d7953 100644 --- a/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx +++ b/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx @@ -24,18 +24,9 @@ const EarnBoxGlmLock: FC = ({ classNameBox }) => { keyPrefix: 'components.dedicated.boxGlmLock', }); const { isConnected } = useAccount(); - const { isAppWaitingForTransactionToBeIndexed, transactionsPending } = useTransactionLocalStore( - state => ({ - isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, - transactionsPending: state.data.transactionsPending, - }), - ); - - const isPendingLockingOrUnlockingTransaction = - isAppWaitingForTransactionToBeIndexed && - !!transactionsPending?.filter( - ({ type, isFinalized }) => (type === 'lock' || 'unlock') && !isFinalized, - ).length; + const { isAppWaitingForTransactionToBeIndexed } = useTransactionLocalStore(state => ({ + isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, + })); const [isModalGlmLockOpen, setIsModalGlmLockOpen] = useState(false); const { data: estimatedEffectiveDeposit, isFetching: isFetchingEstimatedEffectiveDeposit } = @@ -52,7 +43,7 @@ const EarnBoxGlmLock: FC = ({ classNameBox }) => { doubleValueProps: { cryptoCurrency: 'golem', dataTest: 'BoxGlmLock__Section--current__DoubleValue', - isFetching: isFetchingDepositValue || isPendingLockingOrUnlockingTransaction, + isFetching: isFetchingDepositValue || isAppWaitingForTransactionToBeIndexed, valueCrypto: depositsValue, }, isDisabled: isPreLaunch && !isConnected, @@ -64,7 +55,7 @@ const EarnBoxGlmLock: FC = ({ classNameBox }) => { coinPricesServerDowntimeText: '...', cryptoCurrency: 'golem', dataTest: 'BoxGlmLock__Section--effective__DoubleValue', - isFetching: isFetchingEstimatedEffectiveDeposit || isPendingLockingOrUnlockingTransaction, + isFetching: isFetchingEstimatedEffectiveDeposit || isAppWaitingForTransactionToBeIndexed, valueCrypto: estimatedEffectiveDeposit, }, isDisabled: isPreLaunch && !isConnected, diff --git a/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx b/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx index 3a215ba3d8..90bdbf84f3 100644 --- a/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx +++ b/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx @@ -37,17 +37,9 @@ const EarnBoxPersonalAllocation: FC = ({ classNa const { data: isPatronMode } = useIsPatronMode(); const { data: totalPatronDonations, isFetching: isFetchingTotalPatronDonations } = useTotalPatronDonations({ isEnabledAdditional: !!isPatronMode }); - const { isAppWaitingForTransactionToBeIndexed, transactionsPending } = useTransactionLocalStore( - state => ({ - isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, - transactionsPending: state.data.transactionsPending, - }), - ); - - const isPendingWithdrawalTransaction = - isAppWaitingForTransactionToBeIndexed && - !!transactionsPending?.filter(({ type, isFinalized }) => type === 'withdrawal' && !isFinalized) - .length; + const { isAppWaitingForTransactionToBeIndexed } = useTransactionLocalStore(state => ({ + isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, + })); const isPreLaunch = getIsPreLaunch(currentEpoch); const isProjectAdminMode = useIsProjectAdminMode(); @@ -99,9 +91,7 @@ const EarnBoxPersonalAllocation: FC = ({ classNa coinPricesServerDowntimeText: !isProjectAdminMode ? '...' : undefined, cryptoCurrency: 'ethereum', dataTest: 'BoxPersonalAllocation__Section--availableNow__DoubleValue', - isFetching: isPatronMode - ? isFetchingTotalPatronDonations - : isFetchingWithdrawals || isPendingWithdrawalTransaction, + isFetching: isPatronMode ? isFetchingTotalPatronDonations : isFetchingWithdrawals, valueCrypto: isPatronMode ? totalPatronDonations?.value : withdrawals?.sums.available, }, label: isPatronMode && !isProjectAdminMode ? t('allTime') : i18n.t('common.availableNow'), From ea7e488f834480c2786081563b3cf93dbf7d5b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Thu, 28 Mar 2024 15:57:26 +0100 Subject: [PATCH 076/107] Deploy env bugfix --- .github/workflows/deploy-pr.yml | 1 + .github/workflows/tpl-destroy-env.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index eabb79527c..fd0d45862c 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -64,6 +64,7 @@ jobs: id: is-organization-member with: result-encoding: string + github-token: ${{ secrets.GH_BOT_TOKEN }} script: | return ( await github.rest.orgs.listMembers({ diff --git a/.github/workflows/tpl-destroy-env.yml b/.github/workflows/tpl-destroy-env.yml index de4835a08c..175ce3a0fe 100644 --- a/.github/workflows/tpl-destroy-env.yml +++ b/.github/workflows/tpl-destroy-env.yml @@ -53,6 +53,7 @@ jobs: id: is-organization-member with: result-encoding: string + github-token: ${{ secrets.GH_BOT_TOKEN }} script: | return ( await github.rest.orgs.listMembers({ From 118a2bea5c3ce51cc1803adc370cc34df9894ea9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Garbaci=C5=84ski?= <57113816+kgarbacinski@users.noreply.github.com> Date: Thu, 28 Mar 2024 17:04:05 +0100 Subject: [PATCH 077/107] FIX-OCT-1348: Add PPF & CF to the response (#110) --- backend/app/infrastructure/routes/epochs.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/backend/app/infrastructure/routes/epochs.py b/backend/app/infrastructure/routes/epochs.py index 3ee45a22b6..28632f939f 100644 --- a/backend/app/infrastructure/routes/epochs.py +++ b/backend/app/infrastructure/routes/epochs.py @@ -98,6 +98,14 @@ def get(self): required=False, description="The amount that will be used to increase staking and for other Octant related operations. Includes donations to projects that didn't reach the threshold.", ), + "ppf": fields.String( + required=False, + description="PPF for the given epoch. It's calculated from staking proceeds directly.", + ), + "communityFund": fields.String( + required=False, + description="Community fund for the given epoch. It's calculated from staking proceeds directly.", + ), }, ) From 62c9909e313c33e6a8cfbcfdd3d99d03f7365f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 28 Mar 2024 18:31:42 +0100 Subject: [PATCH 078/107] OCT-1498 Can't unlock GLM on M-C without going to lock view & back (#107) --- client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx | 2 +- client/src/components/Earn/EarnGlmLock/utils.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx b/client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx index bc8af06b6b..6ca11de82f 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx +++ b/client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx @@ -153,7 +153,7 @@ const EarnGlmLock: FC = ({ currentMode, onCurrentModeChange, o return ( ({ + currentMode, valueToDeposeOrWithdraw: '', -}; +}); export const validationSchema = ( dataAvailableFunds: bigint | undefined, From 2b6b9016d5c2cd2c837304e32d43294ee51f370d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 10:30:15 +0100 Subject: [PATCH 079/107] [FIX] Do not fetch project donors outside AW (#108) --- .../hooks/queries/donors/useProjectDonors.ts | 13 +++--------- .../hooks/queries/donors/useProjectsDonors.ts | 21 ++++++++++--------- .../queries/useProjectsIpfsWithRewards.ts | 17 ++++++++++----- 3 files changed, 26 insertions(+), 25 deletions(-) diff --git a/client/src/hooks/queries/donors/useProjectDonors.ts b/client/src/hooks/queries/donors/useProjectDonors.ts index e12432d481..7aa43bbf66 100644 --- a/client/src/hooks/queries/donors/useProjectDonors.ts +++ b/client/src/hooks/queries/donors/useProjectDonors.ts @@ -39,16 +39,9 @@ export default function useProjectDonors( }); return useQuery({ - enabled: !!projectAddress && (epoch !== undefined || !!(currentEpoch && currentEpoch > 1)), - queryFn: () => - apiGetProjectDonors( - projectAddress, - epoch || (isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch!), - ), - queryKey: QUERY_KEYS.projectDonors( - projectAddress, - epoch || (isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch!), - ), + enabled: !!projectAddress && (!!currentEpoch && currentEpoch > 1) && (isDecisionWindowOpen === true || epoch !== undefined), + queryFn: () => apiGetProjectDonors(projectAddress, epoch || currentEpoch! - 1), + queryKey: QUERY_KEYS.projectDonors(projectAddress, epoch || currentEpoch! - 1), select: response => mapDataToProjectDonors(response), staleTime: Infinity, ...options, diff --git a/client/src/hooks/queries/donors/useProjectsDonors.ts b/client/src/hooks/queries/donors/useProjectsDonors.ts index 6a547cbb31..b9cb37edab 100644 --- a/client/src/hooks/queries/donors/useProjectsDonors.ts +++ b/client/src/hooks/queries/donors/useProjectsDonors.ts @@ -12,6 +12,7 @@ import { mapDataToProjectDonors } from './utils'; export default function useProjectsDonors(epoch?: number): { data: { [key: string]: ProjectDonor[] }; isFetching: boolean; + isSuccess: boolean; } { const { data: currentEpoch } = useCurrentEpoch(); const { data: projectsAddresses } = useProjectsContract(epoch); @@ -21,16 +22,13 @@ export default function useProjectsDonors(epoch?: number): { const projectsDonorsResults: UseQueryResult[] = useQueries({ queries: (projectsAddresses || []).map(projectAddress => ({ - enabled: !!projectsAddresses && isDecisionWindowOpen !== undefined, - queryFn: () => - apiGetProjectDonors( - projectAddress, - epoch || (isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch!), - ), - queryKey: QUERY_KEYS.projectDonors( - projectAddress, - epoch || (isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch!), - ), + enabled: + !!projectsAddresses && + !!currentEpoch && + currentEpoch > 1 && + (isDecisionWindowOpen === true || epoch !== undefined), + queryFn: () => apiGetProjectDonors(projectAddress, epoch || currentEpoch! - 1), + queryKey: QUERY_KEYS.projectDonors(projectAddress, epoch || currentEpoch! - 1), select: response => mapDataToProjectDonors(response), })), }); @@ -46,6 +44,7 @@ export default function useProjectsDonors(epoch?: number): { return { data: {}, isFetching, + isSuccess: false, }; } @@ -57,5 +56,7 @@ export default function useProjectsDonors(epoch?: number): { }; }, {}), isFetching: false, + // Ensures projectsDonorsResults is actually fetched with data, and not just an object with undefined values. + isSuccess: !projectsDonorsResults.some(element => !element.isSuccess), }; } diff --git a/client/src/hooks/queries/useProjectsIpfsWithRewards.ts b/client/src/hooks/queries/useProjectsIpfsWithRewards.ts index 75d4246cb9..ab9133c045 100644 --- a/client/src/hooks/queries/useProjectsIpfsWithRewards.ts +++ b/client/src/hooks/queries/useProjectsIpfsWithRewards.ts @@ -1,3 +1,4 @@ +import env from 'env'; import { ExtendedProject } from 'types/extended-project'; import getSortedElementsByTotalValueOfAllocationsAndAlphabetical from 'utils/getSortedElementsByTotalValueOfAllocationsAndAlphabetical'; @@ -18,7 +19,7 @@ export default function useProjectsIpfsWithRewards(epoch?: number): { isFetching: boolean; } { // TODO OCT-1270 TODO OCT-1312 Remove this override. - const epochOverrideForDataFetch = epoch === 2 ? 3 : epoch; + const epochOverrideForDataFetch = env.network === 'Mainnet' && epoch === 2 ? 3 : epoch; const { data: projectsAddresses, isFetching: isFetchingProjectsContract } = useProjectsContract(epochOverrideForDataFetch); @@ -31,7 +32,12 @@ export default function useProjectsIpfsWithRewards(epoch?: number): { isFetching: isFetchingMatchedProjectRewards, isRefetching: isRefetchingMatchedProjectRewards, } = useMatchedProjectRewards(epoch); - const { data: projectsDonors, isFetching: isFetchingProjectsDonors } = useProjectsDonors(epoch); + + const { + data: projectsDonors, + isFetching: isFetchingProjectsDonors, + isSuccess: isSuccessProjectsDonors, + } = useProjectsDonors(epoch); const isFetching = isFetchingProjectsContract || @@ -54,10 +60,11 @@ export default function useProjectsIpfsWithRewards(epoch?: number): { * passed threshold. For those that did not, we reduce on their donors and get the value. */ const totalValueOfAllocations = - projectMatchedProjectRewards?.sum || - projectsDonors[project.address].reduce((acc, curr) => acc + curr.amount, BigInt(0)); + projectMatchedProjectRewards?.sum || isSuccessProjectsDonors + ? projectsDonors[project.address].reduce((acc, curr) => acc + curr.amount, BigInt(0)) + : BigInt(0); return { - numberOfDonors: projectsDonors[project.address].length, + numberOfDonors: isSuccessProjectsDonors ? projectsDonors[project.address].length : 0, percentage: projectMatchedProjectRewards?.percentage, totalValueOfAllocations, ...project, From c6853c3c31cacf8506c6f73996f6fe6ce36a516f Mon Sep 17 00:00:00 2001 From: Housekeeper Bot Date: Fri, 29 Mar 2024 09:47:27 +0000 Subject: [PATCH 080/107] [CI/CD] Update uat.env contracts --- ci/argocd/contracts/uat.env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ci/argocd/contracts/uat.env b/ci/argocd/contracts/uat.env index 4bdc1a845f..a6aa474439 100644 --- a/ci/argocd/contracts/uat.env +++ b/ci/argocd/contracts/uat.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=5577583 +BLOCK_NUMBER=5584166 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0xF2C1C0EB8bD6eE15629Eca8AF44D869c0369B5cB -DEPOSITS_CONTRACT_ADDRESS=0xd8965459D357CCdeb9548DE4083f32eEf657BEa4 -EPOCHS_CONTRACT_ADDRESS=0xa1f4090B85a40e9Fb447Ad8FE2081Bca584b3C46 -PROPOSALS_CONTRACT_ADDRESS=0xbf121932993EbE7432c7613fF9CE1161516d097C -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x19123Ee7a29adb3e1574a53Ce965194DD2eB0baD -VAULT_CONTRACT_ADDRESS=0xf493bfAe0Ba5832BFaAA38Ac19837e2bE834b17D +AUTH_CONTRACT_ADDRESS=0x9770a3De2278c1fF34eae37A2eDd3E815EF063A7 +DEPOSITS_CONTRACT_ADDRESS=0x54e4cDAE50302dDC4Ad19a62Fa06cbe7717b95D7 +EPOCHS_CONTRACT_ADDRESS=0x4E39c931b858628E8e85f8D3EA206aF969b837D5 +PROPOSALS_CONTRACT_ADDRESS=0x07708E4ffc2eB8E8947035eB5f64d9777489bDa6 +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0xEA8dd6635F9a5226fBCED739605850d66FC98925 +VAULT_CONTRACT_ADDRESS=0x5523DaC9FD779a9d6D4c77B242726d91F87279dB From a809f6d61bd5b585a2d77c6dea02a9168c16e191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 11:39:41 +0100 Subject: [PATCH 081/107] test: disable snapshotter for E2E env --- .github/workflows/ci-run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index add163cf12..b13202ed9a 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -184,7 +184,7 @@ jobs: chain-id: 1337 network-name: local chain-name: localhost - snapshotter-enabled: true + snapshotter-enabled: false scheduler-enabled: true glm-claim-enabled: true vault-confirm-withdrawals-enabled: true From 0d36ac6375e05568126db38fac43686fa322de6a Mon Sep 17 00:00:00 2001 From: Pawel Peregud Date: Fri, 29 Mar 2024 11:43:06 +0100 Subject: [PATCH 082/107] fix: use user_address as passed as a parameter in /allocate API Previously we relied on erecover. This doesn't work for EIP-1271. --- .../app/infrastructure/routes/allocations.py | 10 ++- .../modules/user/allocations/controller.py | 4 +- .../user/allocations/service/pending.py | 8 +- backend/docs/websockets-api.yaml | 5 ++ backend/tests/conftest.py | 11 ++- backend/tests/legacy/test_rewards.py | 4 +- .../allocations/test_pending_allocations.py | 76 ++++++++++++++----- 7 files changed, 88 insertions(+), 30 deletions(-) diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 73ed8eed59..ee55802388 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -62,6 +62,10 @@ "UserAllocationRequest", { "payload": fields.Nested(allocation_payload), + "user_address": fields.String( + required=True, + description="Wallet address of the user. EOA or EIP-1271", + ), "signature": fields.String( required=True, description="EIP-712 signature of the allocation payload as a hexadecimal string", @@ -167,10 +171,10 @@ def post(self): is_manually_edited = ( ns.payload["isManuallyEdited"] if "isManuallyEdited" in ns.payload else None ) - user_address = controller.allocate( - ns.payload, is_manually_edited=is_manually_edited + controller.allocate( + ns.user_address, ns.payload, is_manually_edited=is_manually_edited ) - app.logger.info(f"User: {user_address} allocated successfully") + app.logger.info(f"User: {ns.user_address} allocated successfully") return {}, 201 diff --git a/backend/app/modules/user/allocations/controller.py b/backend/app/modules/user/allocations/controller.py index a164bbc56d..5fa26076a8 100644 --- a/backend/app/modules/user/allocations/controller.py +++ b/backend/app/modules/user/allocations/controller.py @@ -63,14 +63,14 @@ def get_donors(epoch_num: int) -> List[str]: return service.get_all_donors_addresses(context) -def allocate(payload: Dict, **kwargs): +def allocate(user_address: str, payload: Dict, **kwargs): context = state_context(EpochState.PENDING) service: PendingUserAllocations = get_services( context.epoch_state ).user_allocations_service allocation_request = _deserialize_payload(payload) - service.allocate(context, allocation_request, **kwargs) + service.allocate(context, user_address, allocation_request, **kwargs) def simulate_allocation( diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index ba3ea99e3a..1b3df41015 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -67,10 +67,12 @@ class PendingUserAllocations(SavedUserAllocations, Model): verifier: Verifier def allocate( - self, context: Context, payload: UserAllocationRequestPayload, **kwargs + self, + context: Context, + user_address: str, + payload: UserAllocationRequestPayload, + **kwargs ) -> str: - user_address = core.recover_user_address(payload) - expected_nonce = self.get_user_next_nonce(user_address) self.verifier.verify( context, diff --git a/backend/docs/websockets-api.yaml b/backend/docs/websockets-api.yaml index 7b98ea2221..c04c829521 100644 --- a/backend/docs/websockets-api.yaml +++ b/backend/docs/websockets-api.yaml @@ -54,6 +54,10 @@ channels: description: Amount of rewards donated for given proposal, BigNumber (WEI) type: string signature: + description: EIP-712 signature of the allocation payload as a hexadecimal string + type: string + user_address: + description: Wallet address of the user. EOA or EIP-1271 type: string isManuallyEdited: descritpion: Whether allocation has been manually edited by user @@ -68,6 +72,7 @@ channels: - proposalAddress: "0xBcd4042DE499D14e55001CcbB24a551F3b954096" amount: "5000" signature: "8d704f19cde0f1f9d310e57621229b919a8e17187be332c4bd08bf797d0fb50232b4aa30639b741723e647667d87da1af38fd4601600f4d4e2c6f724abea03d61b" + user_address: "0x17F6AD8Ef982297579C203069C1DbfFE4348c372" exception: subscribe: diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 45e3ea07ee..c733ce6af9 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -247,7 +247,7 @@ def allocate(self, amount: int, addresses: list[str]): } signature = sign(self._account, build_allocations_eip712_data(payload)) - self._client.allocate(payload, signature) + self._client.allocate(payload, self._account.address, signature) @property def address(self): @@ -311,9 +311,14 @@ def get_allocation_nonce(self, address: str) -> int: ).text return json.loads(rv)["allocationNonce"] - def allocate(self, payload: dict, signature: str) -> int: + def allocate(self, payload: dict, user_address: str, signature: str) -> int: rv = self._flask_client.post( - "/allocations/allocate", json={"payload": payload, "signature": signature} + "/allocations/allocate", + json={ + "payload": payload, + "user_address": user_address, + "signature": signature, + }, ) assert rv.status_code == 201, rv.text diff --git a/backend/tests/legacy/test_rewards.py b/backend/tests/legacy/test_rewards.py index 54f59eadc2..7a4613ceba 100644 --- a/backend/tests/legacy/test_rewards.py +++ b/backend/tests/legacy/test_rewards.py @@ -62,8 +62,8 @@ def _allocate_random_individual_rewards(user_accounts, proposal_accounts) -> int signature2 = sign(user_accounts[1], build_allocations_eip712_data(payload2)) # Call allocate method for both users - allocate({"payload": payload1, "signature": signature1}) - allocate({"payload": payload2, "signature": signature2}) + allocate(user_accounts[0].address, {"payload": payload1, "signature": signature1}) + allocate(user_accounts[1].address, {"payload": payload2, "signature": signature2}) allocations1 = sum([int(a.amount) for a in deserialize_allocations(payload1)]) allocations2 = sum([int(a.amount) for a in deserialize_allocations(payload2)]) diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index 4bf42ce34a..df06152f93 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -118,7 +118,9 @@ def test_user_allocates_for_the_first_time(tos_users, proposal_accounts): signature = sign(tos_users[0], build_allocations_eip712_data(payload)) # Call allocate method - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) # Check if allocations were created check_allocations(tos_users[0].address, payload, 2) @@ -136,8 +138,12 @@ def test_multiple_users_allocate_for_the_first_time(tos_users, proposal_accounts signature2 = sign(tos_users[1], build_allocations_eip712_data(payload2)) # Call allocate method for both users - controller.allocate({"payload": payload1, "signature": signature1}) - controller.allocate({"payload": payload2, "signature": signature2}) + controller.allocate( + tos_users[0].address, {"payload": payload1, "signature": signature1} + ) + controller.allocate( + tos_users[1].address, {"payload": payload2, "signature": signature2} + ) # Check if allocations were created for both users check_allocations(tos_users[0].address, payload1, 2) @@ -155,7 +161,10 @@ def test_allocate_updates_with_more_proposals(tos_users, proposal_accounts): ) # Call allocate method - controller.allocate({"payload": initial_payload, "signature": initial_signature}) + controller.allocate( + tos_users[0].address, + {"payload": initial_payload, "signature": initial_signature}, + ) # Create a new payload with more proposals updated_payload = create_payload(proposal_accounts[0:3], None, 1) @@ -164,7 +173,10 @@ def test_allocate_updates_with_more_proposals(tos_users, proposal_accounts): ) # Call allocate method with updated_payload - controller.allocate({"payload": updated_payload, "signature": updated_signature}) + controller.allocate( + tos_users[0].address, + {"payload": updated_payload, "signature": updated_signature}, + ) # Check if allocations were updated check_allocations(tos_users[0].address, updated_payload, 3) @@ -181,7 +193,10 @@ def test_allocate_updates_with_less_proposals(tos_users, proposal_accounts): ) # Call allocate method - controller.allocate({"payload": initial_payload, "signature": initial_signature}) + controller.allocate( + tos_users[0].address, + {"payload": initial_payload, "signature": initial_signature}, + ) # Create a new payload with fewer proposals updated_payload = create_payload(proposal_accounts[0:2], None, 1) @@ -190,7 +205,10 @@ def test_allocate_updates_with_less_proposals(tos_users, proposal_accounts): ) # Call allocate method with updated_payload - controller.allocate({"payload": updated_payload, "signature": updated_signature}) + controller.allocate( + tos_users[0].address, + {"payload": updated_payload, "signature": updated_signature}, + ) # Check if allocations were updated check_allocations(tos_users[0].address, updated_payload, 2) @@ -211,8 +229,14 @@ def test_multiple_users_change_their_allocations(tos_users, proposal_accounts): ) # Call allocate method with initial payloads for both users - controller.allocate({"payload": initial_payload1, "signature": initial_signature1}) - controller.allocate({"payload": initial_payload2, "signature": initial_signature2}) + controller.allocate( + tos_users[0].address, + {"payload": initial_payload1, "signature": initial_signature1}, + ) + controller.allocate( + tos_users[1].address, + {"payload": initial_payload2, "signature": initial_signature2}, + ) # Create updated payloads for both users updated_payload1 = create_payload(proposal_accounts[0:4], None, 1) @@ -225,8 +249,14 @@ def test_multiple_users_change_their_allocations(tos_users, proposal_accounts): ) # Call allocate method with updated payloads for both users - controller.allocate({"payload": updated_payload1, "signature": updated_signature1}) - controller.allocate({"payload": updated_payload2, "signature": updated_signature2}) + controller.allocate( + tos_users[0].address, + {"payload": updated_payload1, "signature": updated_signature1}, + ) + controller.allocate( + tos_users[1].address, + {"payload": updated_payload2, "signature": updated_signature2}, + ) # Check if allocations were updated for both users check_allocations(tos_users[0].address, updated_payload1, 4) @@ -247,14 +277,18 @@ def test_user_exceeded_rewards_budget_in_allocations(app, proposal_accounts, tos signature = sign(tos_users[0], build_allocations_eip712_data(payload)) with pytest.raises(exceptions.RewardsBudgetExceeded): - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) # Lower it to 100 total (should pass) payload = create_payload( proposal_accounts[0:3], [10 * 10**18, 40 * 10**18, 50 * 10**18] ) signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) def test_nonces(tos_users, proposal_accounts): @@ -263,14 +297,18 @@ def test_nonces(tos_users, proposal_accounts): proposal_accounts[0:2], [10 * 10**18, 20 * 10**18], nonce0 ) signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) nonce1 = get_allocation_nonce(tos_users[0].address) assert nonce0 != nonce1 payload = create_payload( proposal_accounts[0:2], [10 * 10**18, 30 * 10**18], nonce1 ) signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) nonce2 = get_allocation_nonce(tos_users[0].address) assert nonce1 != nonce2 @@ -280,7 +318,9 @@ def test_nonces(tos_users, proposal_accounts): ) signature = sign(tos_users[0], build_allocations_eip712_data(payload)) with pytest.raises(exceptions.WrongAllocationsNonce): - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) def test_stores_allocation_request_signature(tos_users, proposal_accounts): @@ -290,7 +330,9 @@ def test_stores_allocation_request_signature(tos_users, proposal_accounts): ) signature = sign(tos_users[0], build_allocations_eip712_data(payload)) - controller.allocate({"payload": payload, "signature": signature}) + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) alloc_signature = database.allocations.get_allocation_request_by_user_nonce( tos_users[0].address, nonce0 From cb639db6379f6b01d36126f3a688844528bff464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 11:57:56 +0100 Subject: [PATCH 083/107] feat: send user address when allocating --- client/src/hooks/events/useAllocate.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/client/src/hooks/events/useAllocate.ts b/client/src/hooks/events/useAllocate.ts index 7772a495cb..b39d2c6392 100644 --- a/client/src/hooks/events/useAllocate.ts +++ b/client/src/hooks/events/useAllocate.ts @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { useSignTypedData } from 'wagmi'; +import { useAccount, useSignTypedData } from 'wagmi'; import { handleError } from 'api/errorMessages'; import networkConfig from 'constants/networkConfig'; @@ -37,6 +37,8 @@ const types = { }; export default function useAllocate({ onSuccess, nonce }: UseAllocateProps): UseAllocate { + const { address } = useAccount(); + const [isLoading, setIsLoading] = useState(false); const { signTypedData } = useSignTypedData({ domain, @@ -60,6 +62,7 @@ export default function useAllocate({ onSuccess, nonce }: UseAllocateProps): Use isManuallyEdited, payload: restVariables, signature: data, + userAddress: address, }), () => { if (onSuccess) { From 9561cb0b65835d4353f9c42da0a04e155b950459 Mon Sep 17 00:00:00 2001 From: Paul Peregud Date: Fri, 29 Mar 2024 11:59:30 +0100 Subject: [PATCH 084/107] Update backend/app/infrastructure/routes/allocations.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Andrzej ZióƂek --- backend/app/infrastructure/routes/allocations.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index ee55802388..3893fc2700 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -62,7 +62,7 @@ "UserAllocationRequest", { "payload": fields.Nested(allocation_payload), - "user_address": fields.String( + "userAddress": fields.String( required=True, description="Wallet address of the user. EOA or EIP-1271", ), @@ -172,9 +172,9 @@ def post(self): ns.payload["isManuallyEdited"] if "isManuallyEdited" in ns.payload else None ) controller.allocate( - ns.user_address, ns.payload, is_manually_edited=is_manually_edited + ns.userAddress, ns.payload, is_manually_edited=is_manually_edited ) - app.logger.info(f"User: {ns.user_address} allocated successfully") + app.logger.info(f"User: {ns.userAddress} allocated successfully") return {}, 201 From b9bfac012fff98dd927727953bad29cc0757bc00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 12:08:09 +0100 Subject: [PATCH 085/107] fix: projects rewards when donors not available --- client/src/hooks/queries/useProjectsIpfsWithRewards.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/hooks/queries/useProjectsIpfsWithRewards.ts b/client/src/hooks/queries/useProjectsIpfsWithRewards.ts index ab9133c045..56e02d5b05 100644 --- a/client/src/hooks/queries/useProjectsIpfsWithRewards.ts +++ b/client/src/hooks/queries/useProjectsIpfsWithRewards.ts @@ -60,9 +60,10 @@ export default function useProjectsIpfsWithRewards(epoch?: number): { * passed threshold. For those that did not, we reduce on their donors and get the value. */ const totalValueOfAllocations = - projectMatchedProjectRewards?.sum || isSuccessProjectsDonors + projectMatchedProjectRewards?.sum || + (isSuccessProjectsDonors ? projectsDonors[project.address].reduce((acc, curr) => acc + curr.amount, BigInt(0)) - : BigInt(0); + : BigInt(0)); return { numberOfDonors: isSuccessProjectsDonors ? projectsDonors[project.address].length : 0, percentage: projectMatchedProjectRewards?.percentage, From af7d142e19e4a059f991b39421736e8506a3fe8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 12:20:43 +0100 Subject: [PATCH 086/107] fix: adjust useUserAllocationsAllEpochs for an error for E0 --- .../helpers/useUserAllocationsAllEpochs.ts | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts b/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts index 492ef3aa9f..eff5554713 100644 --- a/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts +++ b/client/src/hooks/helpers/useUserAllocationsAllEpochs.ts @@ -22,7 +22,19 @@ export default function useUserAllocationsAllEpochs(): { data: Response; isFetch const userAllocationsAllEpochs: UseQueryResult[] = useQueries({ queries: [...Array(currentEpoch).keys()].map(epoch => ({ enabled: !!address && currentEpoch !== undefined && currentEpoch > 1, - queryFn: () => apiGetUserAllocations(address as string, epoch), + queryFn: async () => { + // For Epoch 0 error 400 is returned. + try { + return await apiGetUserAllocations(address as string, epoch); + } catch (error) { + return new Promise(resolve => { + resolve({ + allocations: [], + isManuallyEdited: false, + }); + }); + } + }, queryKey: QUERY_KEYS.userAllocations(epoch), retry: false, })), @@ -43,16 +55,16 @@ export default function useUserAllocationsAllEpochs(): { data: Response; isFetch return { data: userAllocationsAllEpochs.map(({ data }, index) => { - const userAllocationsFromBackend = data!.allocations.map(element => ({ + const userAllocationsFromBackend = data?.allocations.map(element => ({ address: element.address, epoch: index, value: parseUnitsBigInt(element.amount, 'wei'), })); return { - elements: userAllocationsFromBackend.filter(({ value }) => value !== 0n), + elements: userAllocationsFromBackend?.filter(({ value }) => value !== 0n) || [], hasUserAlreadyDoneAllocation: !!userAllocationsFromBackend?.length, - isManuallyEdited: !!data!.isManuallyEdited, + isManuallyEdited: !!data?.isManuallyEdited, }; }), isFetching: false, From 05e2ae425011e12bbd61e9b7a04b2149db0d8d37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 12:58:57 +0100 Subject: [PATCH 087/107] CI: enable snapshotter for E2E --- .github/workflows/ci-run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index b13202ed9a..add163cf12 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -184,7 +184,7 @@ jobs: chain-id: 1337 network-name: local chain-name: localhost - snapshotter-enabled: false + snapshotter-enabled: true scheduler-enabled: true glm-claim-enabled: true vault-confirm-withdrawals-enabled: true From 7741c78cbfb893ae5a9ad5b7e20b674c04c600dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 13:33:19 +0100 Subject: [PATCH 088/107] CI: disable snapshotter for E2E --- .github/workflows/ci-run.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index add163cf12..b13202ed9a 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -184,7 +184,7 @@ jobs: chain-id: 1337 network-name: local chain-name: localhost - snapshotter-enabled: true + snapshotter-enabled: false scheduler-enabled: true glm-claim-enabled: true vault-confirm-withdrawals-enabled: true From af78f16cf22098eb94ecce900eced36e9f076854 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 13:34:37 +0100 Subject: [PATCH 089/107] [TEST] Make snapshot at the start of E2E (#115) --- ...round.cy.ts => _1metamaskWorkaround.cy.ts} | 0 .../cypress/e2e/_2makePandingSnapshot.cy.ts | 33 +++++++++++++++++++ client/src/views/SyncView/SyncView.tsx | 2 +- 3 files changed, 34 insertions(+), 1 deletion(-) rename client/cypress/e2e/{_metamaskWorkaround.cy.ts => _1metamaskWorkaround.cy.ts} (100%) create mode 100644 client/cypress/e2e/_2makePandingSnapshot.cy.ts diff --git a/client/cypress/e2e/_metamaskWorkaround.cy.ts b/client/cypress/e2e/_1metamaskWorkaround.cy.ts similarity index 100% rename from client/cypress/e2e/_metamaskWorkaround.cy.ts rename to client/cypress/e2e/_1metamaskWorkaround.cy.ts diff --git a/client/cypress/e2e/_2makePandingSnapshot.cy.ts b/client/cypress/e2e/_2makePandingSnapshot.cy.ts new file mode 100644 index 0000000000..1a78151a13 --- /dev/null +++ b/client/cypress/e2e/_2makePandingSnapshot.cy.ts @@ -0,0 +1,33 @@ +import axios from 'axios'; + +import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; +import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; +import env from 'src/env'; +import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; + +// In E2E snapshotter is disabled. Before the first test can be run, pending snapshot needs to be done. +describe('Make pending snapshot', () => { + before(() => { + /** + * Global Metamask setup done by Synpress is not always done. + * Since Synpress needs to have valid provider to fetch the data from contracts, + * setupMetamask is required in each test suite. + */ + cy.setupMetamask(); + }); + + beforeEach(() => { + cy.disconnectMetamaskWalletFromAllDapps(); + mockCoinPricesServer(); + localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); + localStorage.setItem(IS_ONBOARDING_DONE, 'true'); + visitWithLoader(ROOT_ROUTES.earn.absolute); + }); + + it('make pending snapshot', () => { + cy.window().then(async () => { + await axios.post(`${env.serverEndpoint}snapshots/pending`); + cy.get('[data-test=SyncView]', { timeout: 60000 }).should('not.exist'); + }); + }); +}); diff --git a/client/src/views/SyncView/SyncView.tsx b/client/src/views/SyncView/SyncView.tsx index 57186281c3..3972f5b889 100644 --- a/client/src/views/SyncView/SyncView.tsx +++ b/client/src/views/SyncView/SyncView.tsx @@ -8,7 +8,7 @@ import { octantSemiTransparent } from 'svg/logo'; import styles from './SyncView.module.scss'; const SyncView = (): ReactElement => ( -
+
From 5b01b08bc91506e99e7cf1a0bfae4329d2439b9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 14:56:56 +0100 Subject: [PATCH 090/107] feat: adjust onboarding text to Epoch 3 --- client/src/locales/en/translation.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 5d3fb397e2..b5d41718c4 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -414,7 +414,7 @@ }, "donateToProjects": { "header": "Donate to projects", - "text": "Check out the lineup for Epoch 1 in the <0>Projects view. Tap a project tile to read its details. Help your favourite projects out by donating to them.

Your donation will be fund-matched by Golem Foundation. Just tap the heart to add a project to the <0>Allocate view where you can donate." + "text": "Check out the lineup for Epoch 3 in the <0>Projects view. Tap a project tile to read its details. Help your favourite projects out by donating to them.

Your donation will be fund-matched by Golem Foundation. Just tap the heart to add a project to the <0>Allocate view where you can donate." }, "slideIt": { "header": "Just slide it", @@ -423,16 +423,16 @@ }, "stepsDecisionWindowClosed": { "welcomeToOctant": { - "header": "Welcome to Octant Epoch 2", - "text": "To get started, lock some GLM in the <0>Earn view, and see how the Epoch 1 projects performed in the Epoch 1 archive.

If you made personal allocations in the previous epoch, your ETH will be available to withdraw in the <0>Earn view." + "header": "Welcome to Octant Epoch 3", + "text": "To get started, lock some GLM in the <0>Earn view, and see how the previous Epochs projects performed in the archive.

If you made personal allocations in the previous epoch, your ETH will be available to withdraw in the <0>Earn view." }, "earnRewards": { "header": "Earn ETH rewards", - "text": "Use the <0>Earn view calculator to work out your return per epoch for any amount of GLM. You can unlock your tokens at any time.

If you already have GLM locked, you will have some rewards to use during Epoch 2 allocation which will begin in late January 2024." + "text": "Use the <0>Earn view calculator to work out your return per epoch for any amount of GLM. You can unlock your tokens at any time.

If you already have GLM locked, you will have some rewards to use during Epoch 3 allocation which will begin in mid April 2024." }, "getInvolved": { "header": "Get involved", - "text": "Epoch 2 projects will start appearing in the app after a 2-3 week cooling off period. Get involved and suggest a project in our Discord.

Once projects are shortlisted, a community vote on Snapshot decides which projects enter this epoch. All participating voters get a POAP." + "text": "Epoch 3 projects will start appearing in the app after a 2-3 week cooling off period. Get involved and suggest a project in our Discord.

Once projects are shortlisted, a community vote on Snapshot decides which projects enter this epoch. All participating voters get a POAP." } } }, From 25e856189e90697733cdaa786a8f8fd88d6b02d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 21:37:25 +0100 Subject: [PATCH 091/107] test: moveEpoch for projectsArchive suite --- client/cypress/e2e/projectsArchive.cy.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/cypress/e2e/projectsArchive.cy.ts b/client/cypress/e2e/projectsArchive.cy.ts index d2abe9b5d0..43e05b930d 100644 --- a/client/cypress/e2e/projectsArchive.cy.ts +++ b/client/cypress/e2e/projectsArchive.cy.ts @@ -1,4 +1,4 @@ -import { checkLocationWithLoader, visitWithLoader } from 'cypress/utils/e2e'; +import { checkLocationWithLoader, moveEpoch, visitWithLoader } from 'cypress/utils/e2e'; import viewports from 'cypress/utils/viewports'; import { QUERY_KEYS } from 'src/api/queryKeys'; import { IS_ONBOARDING_ALWAYS_VISIBLE, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; @@ -21,7 +21,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => const currentEpochBefore = Number( win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), ); - await win.mutateAsyncMoveEpoch(); + await moveEpoch(win); const currentEpochAfter = Number( win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), ); From 6ac180032fbdb9aead76ae9b1737547eb98d0e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Fri, 29 Mar 2024 21:40:47 +0100 Subject: [PATCH 092/107] [FEAT] Adjust metrics placeholder text for Epoch 1 (#111) --- client/src/views/MetricsView/MetricsView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/views/MetricsView/MetricsView.tsx b/client/src/views/MetricsView/MetricsView.tsx index 8366f00c98..4ce445cf06 100644 --- a/client/src/views/MetricsView/MetricsView.tsx +++ b/client/src/views/MetricsView/MetricsView.tsx @@ -29,7 +29,7 @@ const MetricsView = (): ReactElement => { {/* Workaround for epoch 0 allocation window (no epoch 0 metrics) */} {/* useMetricsEpoch.tsx:19 -> const lastEpoch = currentEpoch! - 1; */} {currentEpoch === 1 ? ( - "It's epoch 0." + "It's Epoch 1, so there are no metrics for the past. It's just a placeholder, please come back in Epoch 2." ) : ( <> From abdb23746a05859bdfeaf9ffbbb4f3f07a69ccda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Sat, 30 Mar 2024 18:34:55 +0100 Subject: [PATCH 093/107] OCT-1506 Enable earn.cy.ts lock 1000 GLM scenario (#105) --- client/cypress/e2e/earn.cy.ts | 52 +++++++++++++++--------- client/cypress/e2e/projectsArchive.cy.ts | 16 +++++--- client/cypress/utils/e2e.ts | 37 ++++++++++++----- 3 files changed, 70 insertions(+), 35 deletions(-) diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts index fa5cabafe7..31234e86ca 100644 --- a/client/cypress/e2e/earn.cy.ts +++ b/client/cypress/e2e/earn.cy.ts @@ -149,6 +149,8 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes cy.get('[data-test=GlmLockTabs__Button]').click(); cy.get( '[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', + // Small timeout ensures skeleton shows up quickly after the transaction. + { timeout: 1000 }, ).should('be.visible'); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { timeout: 60000, @@ -194,6 +196,8 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes cy.get('[data-test=GlmLockTabs__Button]').click(); cy.get( '[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', + // Small timeout ensures skeleton shows up quickly after the transaction. + { timeout: 1000 }, ).should('be.visible'); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { timeout: 60000, @@ -210,9 +214,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes }); }); - // TODO OCT-1506 enable this scenario. - // eslint-disable-next-line jest/no-disabled-tests - it.skip('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { + it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { connectWallet(); cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') @@ -235,24 +237,36 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDes ); cy.get('[data-test=GlmLockNotification--success]').should('be.visible'); cy.get('[data-test=GlmLockTabs__Button]').click(); + cy.get( + '[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', + // Small timeout ensures skeleton shows up quickly after the transaction. + { timeout: 1000 }, + ).should('be.visible'); + // Waiting for skeletons to disappear ensures Graph indexed lock/unlock. + cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', { + timeout: 60000, + }).should('not.exist'); cy.window().then(async win => { - await moveEpoch(win); - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { - timeout: 60000, - }) - .invoke('text') - .then(nextText => { - const lockedGlmsAfterLock = parseInt(nextText.replace(/\u200a/g, ''), 10); - expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); - }); - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__primary]', { - timeout: 60000, - }) - .invoke('text') - .then(nextText => { - const lockedGlmsAfterLock = parseInt(nextText.replace(/\u200a/g, ''), 10); - expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); + cy.wrap(null).then(() => { + return moveEpoch(win).then(() => { + cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { + timeout: 60000, + }) + .invoke('text') + .then(nextText => { + const lockedGlmsAfterLock = parseInt(nextText.replace(/\u200a/g, ''), 10); + expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); + }); + cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__primary]', { + timeout: 60000, + }) + .invoke('text') + .then(nextText => { + const lockedGlmsAfterLock = parseInt(nextText.replace(/\u200a/g, ''), 10); + expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); + }); }); + }); }); }); }); diff --git a/client/cypress/e2e/projectsArchive.cy.ts b/client/cypress/e2e/projectsArchive.cy.ts index 43e05b930d..d80fe1f16b 100644 --- a/client/cypress/e2e/projectsArchive.cy.ts +++ b/client/cypress/e2e/projectsArchive.cy.ts @@ -21,12 +21,16 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => const currentEpochBefore = Number( win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), ); - await moveEpoch(win); - const currentEpochAfter = Number( - win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), - ); - wasEpochMoved = true; - expect(currentEpochBefore + 1).to.eq(currentEpochAfter); + + cy.wrap(null).then(() => { + return moveEpoch(win).then(() => { + const currentEpochAfter = Number( + win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), + ); + wasEpochMoved = true; + expect(currentEpochBefore + 1).to.eq(currentEpochAfter); + }); + }); }); } else { expect(true).to.be.true; diff --git a/client/cypress/utils/e2e.ts b/client/cypress/utils/e2e.ts index a673ac807a..d5e8bd7580 100644 --- a/client/cypress/utils/e2e.ts +++ b/client/cypress/utils/e2e.ts @@ -50,14 +50,31 @@ export const connectWallet = ( return cy.acceptMetamaskAccess(); }; -export const moveEpoch = async (cypressWindow: Cypress.AUTWindow): Promise => { - await cypressWindow.mutateAsyncMoveEpoch(); - // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). - cy.wait(2000); - // Manually taking a pending snapshot after the epoch shift ensures that the snapshot is taken. Passing epoch multiple times without manually triggering pending snapshot in a short period of time may cause the e2e environment to fail. - await axios.post(`${env.serverEndpoint}snapshots/pending`); - // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). - cy.wait(2000); - // reload is needed to get updated data in the app - cy.reload(); +export const moveEpoch = (cypressWindow: Cypress.AUTWindow): Promise => { + return new Promise(resolve => { + cypressWindow.mutateAsyncMoveEpoch().then(() => { + // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). + cy.wait(2000); + // Manually taking a pending snapshot after the epoch shift ensures that the snapshot is taken. Passing epoch multiple times without manually triggering pending snapshot in a short period of time may cause the e2e environment to fail. + axios.post(`${env.serverEndpoint}snapshots/pending`).then(() => { + // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). + cy.wait(2000); + // reload is needed to get updated data in the app + cy.reload(); + cy.get('[data-test=SyncView]', { timeout: 60000 }).should('not.exist'); + // reload is needed to get updated data in the app + cy.reload(); + axios.post(`${env.serverEndpoint}snapshots/finalized`).then(() => { + // Waiting 2s is a way to prevent the effects of slowing down the e2e environment (data update). + cy.wait(2000); + // reload is needed to get updated data in the app + cy.reload(); + cy.get('[data-test=SyncView]', { timeout: 60000 }).should('not.exist'); + // reload is needed to get updated data in the app + cy.reload(); + resolve(true); + }); + }); + }); + }); }; From ff6df1a3c8f4bf47608852a668c2eced4f709dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Tue, 2 Apr 2024 08:57:23 +0200 Subject: [PATCH 094/107] OCT-1509: Include user ip in multisig signature request (#113) --- backend/app/infrastructure/database/models.py | 1 + .../database/multisig_signature.py | 2 ++ .../routes/multisig_signatures.py | 35 +++++++++++++------ .../modules/multisig_signatures/controller.py | 4 +-- .../multisig_signatures/service/offchain.py | 5 ++- ...dd_user_ip_in_multisig_signatures_table.py | 25 +++++++++++++ .../test_offchain_multisig_signatures.py | 25 ++++++++++--- 7 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 backend/migrations/versions/0dbe7ab3ce9d_add_user_ip_in_multisig_signatures_table.py diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index c333ae877d..d57c097e7c 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -170,3 +170,4 @@ class MultisigSignatures(BaseModel): message = Column(db.String, nullable=False) hash = Column(db.String, nullable=False) status = Column(db.String, nullable=False) + user_ip = Column(db.String, nullable=False) diff --git a/backend/app/infrastructure/database/multisig_signature.py b/backend/app/infrastructure/database/multisig_signature.py index d928ab2424..ae01b64caf 100644 --- a/backend/app/infrastructure/database/multisig_signature.py +++ b/backend/app/infrastructure/database/multisig_signature.py @@ -26,6 +26,7 @@ def save_signature( op_type: SignatureOpType, message: str, msg_hash: str, + user_ip: str, status: SigStatus = SigStatus.PENDING, ): signature = MultisigSignatures( @@ -33,6 +34,7 @@ def save_signature( type=op_type, message=message, hash=msg_hash, + user_ip=user_ip, status=status, ) db.session.add(signature) diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py index 9da9bfd776..20b776a8b6 100644 --- a/backend/app/infrastructure/routes/multisig_signatures.py +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -1,5 +1,5 @@ -from flask import current_app as app -from flask_restx import Namespace, fields +from flask import current_app as app, request +from flask_restx import Namespace, fields, reqparse from app.extensions import api from app.infrastructure import OctantResource @@ -8,6 +8,7 @@ get_last_pending_signature, save_pending_signature, ) +from app.settings import config ns = Namespace( "multisig-signatures", @@ -23,12 +24,15 @@ }, ) - -pending_signature_request = api.model( - "PendingSignature", - { - "message": fields.String(description="The message to be signed."), - }, +pending_signature_request_parser = reqparse.RequestParser() +pending_signature_request_parser.add_argument( + "message", required=True, type=str, location="json" +) +pending_signature_request_parser.add_argument( + "x-real-ip", + required=config.X_REAL_IP_REQUIRED, + location="headers", + case_sensitive=False, ) @@ -48,14 +52,23 @@ def get(self, user_address: str, op_type: str): return response - @ns.expect(pending_signature_request) + @ns.expect(pending_signature_request_parser) @ns.response(201, "Success") def post(self, user_address: str, op_type: str): app.logger.debug( f"Adding new multisig signature for user {user_address} and type {op_type}." ) - message = api.payload - save_pending_signature(user_address, SignatureOpType(op_type), message) + args = pending_signature_request_parser.parse_args() + signature_data = ns.payload + + if app.config["X_REAL_IP_REQUIRED"]: + user_ip = args.get("x-real-ip") + else: + user_ip = request.remote_addr + + save_pending_signature( + user_address, SignatureOpType(op_type), signature_data, user_ip + ) app.logger.debug("Added new multisig signature.") return {}, 201 diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py index e6eea755dd..bda12db1ac 100644 --- a/backend/app/modules/multisig_signatures/controller.py +++ b/backend/app/modules/multisig_signatures/controller.py @@ -15,13 +15,13 @@ def get_last_pending_signature( def save_pending_signature( - user_address: str, op_type: SignatureOpType, signature_data: dict + user_address: str, op_type: SignatureOpType, signature_data: dict, user_ip: str ): context = _get_context(op_type) service = get_services(context.epoch_state).multisig_signatures_service return service.save_pending_signature( - context, user_address, op_type, signature_data + context, user_address, op_type, signature_data, user_ip ) diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index 07bc34a856..9f45691bad 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -39,6 +39,7 @@ def save_pending_signature( user_address: str, op_type: SignatureOpType, signature_data: dict, + user_ip: str, ): verifier = self.verifiers[op_type] if not verifier.verify_logic( @@ -50,5 +51,7 @@ def save_pending_signature( msg_hash = hash_signable_message( encode_for_signing(EncodingStandardFor.TEXT, msg) ) - database.multisig_signature.save_signature(user_address, op_type, msg, msg_hash) + database.multisig_signature.save_signature( + user_address, op_type, msg, msg_hash, user_ip + ) db.session.commit() diff --git a/backend/migrations/versions/0dbe7ab3ce9d_add_user_ip_in_multisig_signatures_table.py b/backend/migrations/versions/0dbe7ab3ce9d_add_user_ip_in_multisig_signatures_table.py new file mode 100644 index 0000000000..c7d8cee4c3 --- /dev/null +++ b/backend/migrations/versions/0dbe7ab3ce9d_add_user_ip_in_multisig_signatures_table.py @@ -0,0 +1,25 @@ +"""add user ip in multisig signatures table + +Revision ID: 0dbe7ab3ce9d +Revises: f923075e5877 +Create Date: 2024-03-29 12:15:18.712651 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = "0dbe7ab3ce9d" +down_revision = "f923075e5877" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("multisig_signatures", schema=None) as batch_op: + batch_op.add_column(sa.Column("user_ip", sa.String(), nullable=False)) + + +def downgrade(): + with op.batch_alter_table("multisig_signatures", schema=None) as batch_op: + batch_op.drop_column("user_ip") diff --git a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py index 24be1f0bb7..fa3c7cbfbe 100644 --- a/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py +++ b/backend/tests/modules/multisig_signatures/test_offchain_multisig_signatures.py @@ -24,19 +24,21 @@ def test_get_last_pending_signature_returns_expected_signature_when_signature_ex SignatureOpType.ALLOCATION, "last pending msg", "last pending hash", + "0.0.0.0", ) database.multisig_signature.save_signature( alice.address, SignatureOpType.ALLOCATION, "test_message", "test_hash", + "0.0.0.0", status=SigStatus.APPROVED, ) database.multisig_signature.save_signature( - alice.address, SignatureOpType.TOS, "test_message", "test_hash" + alice.address, SignatureOpType.TOS, "test_message", "test_hash", "0.0.0.0" ) database.multisig_signature.save_signature( - bob.address, SignatureOpType.ALLOCATION, "test_message", "test_hash" + bob.address, SignatureOpType.ALLOCATION, "test_message", "test_hash", "0.0.0.0" ) db.session.commit() @@ -74,7 +76,11 @@ def test_save_signature_when_verified_successfully(context, alice, mock_verifier # When service.save_pending_signature( - context, alice.address, SignatureOpType.TOS, {"message": "test_message"} + context, + alice.address, + SignatureOpType.TOS, + {"message": "test_message"}, + "0.0.0.0", ) # Then @@ -86,6 +92,7 @@ def test_save_signature_when_verified_successfully(context, alice, mock_verifier assert result.type == SignatureOpType.TOS assert result.status == SigStatus.PENDING assert result.message == '{"message": "test_message"}' + assert result.user_ip == "0.0.0.0" assert ( result.hash == "0x15259cb98ed495a577ec69a3a75f3b16168507d0099b70483fdab3d0d1b3e71a" @@ -100,7 +107,11 @@ def test_does_not_save_signature_when_verification_returns_false( with pytest.raises(InvalidMultisigSignatureRequest): service.save_pending_signature( - context, alice.address, SignatureOpType.TOS, {"message": "test_message"} + context, + alice.address, + SignatureOpType.TOS, + {"message": "test_message"}, + "0.0.0.0", ) result = database.multisig_signature.get_last_pending_signature( alice.address, SignatureOpType.TOS @@ -122,7 +133,11 @@ def _verify_signature(self, context, **kwargs): with pytest.raises(ValueError): service.save_pending_signature( - context, alice.address, SignatureOpType.TOS, {"message": "test_message"} + context, + alice.address, + SignatureOpType.TOS, + {"message": "test_message"}, + "0.0.0.0", ) result = database.multisig_signature.get_last_pending_signature( alice.address, SignatureOpType.TOS From 0203ab64f3d7dc657f4e1dde1cc5e6d07a717145 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Tue, 2 Apr 2024 08:57:51 +0200 Subject: [PATCH 095/107] OCT-1514 UAT: once allocation is done, GET /allocations/user returns null addresses (#116) --- .../infrastructure/database/allocations.py | 7 ++--- .../modules/user/allocations/service/saved.py | 2 +- .../allocations/test_saved_allocations.py | 29 +++++++++++++++---- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 19c61a24bd..5c03b5cf2f 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -10,7 +10,6 @@ from app.infrastructure.database.models import Allocation, User, AllocationRequest from app.infrastructure.database.user import get_by_address from app.modules.dto import ( - AllocationItem, AllocationDTO, AccountFundsDTO, UserAllocationRequestPayload, @@ -64,7 +63,7 @@ def get_user_allocations_history( def get_all_by_user_addr_and_epoch( user_address: str, epoch: int -) -> List[AllocationItem]: +) -> List[AccountFundsDTO]: allocations: List[Allocation] = ( Allocation.query.join(User, User.id == Allocation.user_id) .filter(User.address == user_address) @@ -74,8 +73,8 @@ def get_all_by_user_addr_and_epoch( ) return [ - AllocationItem( - proposal_address=alloc.proposal_address, + AccountFundsDTO( + address=alloc.proposal_address, amount=int(alloc.amount), ) for alloc in allocations diff --git a/backend/app/modules/user/allocations/service/saved.py b/backend/app/modules/user/allocations/service/saved.py index 738445e841..7bc1809b25 100644 --- a/backend/app/modules/user/allocations/service/saved.py +++ b/backend/app/modules/user/allocations/service/saved.py @@ -84,7 +84,7 @@ def get_allocations_by_project( def get_last_user_allocation( self, context: Context, user_address: str - ) -> Tuple[List[AllocationItem], Optional[bool]]: + ) -> Tuple[List[AccountFundsDTO], Optional[bool]]: epoch_num = context.epoch_details.epoch_num last_request = database.allocations.get_allocation_request_by_user_and_epoch( user_address, epoch_num diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index d0acbf4f64..fc6402b5fd 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -8,6 +8,7 @@ ProposalDonationDTO, UserAllocationRequestPayload, UserAllocationPayload, + AccountFundsDTO, ) from app.modules.user.allocations.service.saved import SavedUserAllocations from app.modules.history.dto import AllocationItem as HistoryAllocationItem @@ -310,7 +311,11 @@ def test_get_last_user_allocation_returns_the_only_allocation( service, context, mock_users_db ): user1, _, _ = mock_users_db - expected_result = make_user_allocation(context, user1) + allocations = make_user_allocation(context, user1) + expected_result = [ + AccountFundsDTO(address=a.proposal_address, amount=a.amount) + for a in allocations + ] assert service.get_last_user_allocation(context, user1.address) == ( expected_result, @@ -323,7 +328,11 @@ def test_get_last_user_allocation_returns_the_only_the_last_allocation( ): user1, _, _ = mock_users_db _ = make_user_allocation(context, user1) - expected_result = make_user_allocation(context, user1, allocations=10, nonce=1) + allocations = make_user_allocation(context, user1, allocations=10, nonce=1) + expected_result = [ + AccountFundsDTO(address=a.proposal_address, amount=a.amount) + for a in allocations + ] assert service.get_last_user_allocation(context, user1.address) == ( expected_result, @@ -336,15 +345,23 @@ def test_get_last_user_allocation_returns_stored_metadata( ): user1, _, _ = mock_users_db - expected_result = make_user_allocation(context, user1, is_manually_edited=False) + allocations = make_user_allocation(context, user1, is_manually_edited=False) + expected_result = [ + AccountFundsDTO(address=a.proposal_address, amount=a.amount) + for a in allocations + ] + assert service.get_last_user_allocation(context, user1.address) == ( expected_result, False, ) - expected_result = make_user_allocation( - context, user1, nonce=1, is_manually_edited=True - ) + allocations = make_user_allocation(context, user1, nonce=1, is_manually_edited=True) + expected_result = [ + AccountFundsDTO(address=a.proposal_address, amount=a.amount) + for a in allocations + ] + assert service.get_last_user_allocation(context, user1.address) == ( expected_result, True, From 781f094e76dbf66d6002226573bfda832cb99173 Mon Sep 17 00:00:00 2001 From: Housekeeper Bot Date: Tue, 2 Apr 2024 06:58:43 +0000 Subject: [PATCH 096/107] [CI/CD] Update uat.env contracts --- ci/argocd/contracts/uat.env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ci/argocd/contracts/uat.env b/ci/argocd/contracts/uat.env index a6aa474439..8fef04dbba 100644 --- a/ci/argocd/contracts/uat.env +++ b/ci/argocd/contracts/uat.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=5584166 +BLOCK_NUMBER=5611649 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0x9770a3De2278c1fF34eae37A2eDd3E815EF063A7 -DEPOSITS_CONTRACT_ADDRESS=0x54e4cDAE50302dDC4Ad19a62Fa06cbe7717b95D7 -EPOCHS_CONTRACT_ADDRESS=0x4E39c931b858628E8e85f8D3EA206aF969b837D5 -PROPOSALS_CONTRACT_ADDRESS=0x07708E4ffc2eB8E8947035eB5f64d9777489bDa6 -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0xEA8dd6635F9a5226fBCED739605850d66FC98925 -VAULT_CONTRACT_ADDRESS=0x5523DaC9FD779a9d6D4c77B242726d91F87279dB +AUTH_CONTRACT_ADDRESS=0x08238BF42F1f3fAD797Fcd513abdeD28A74E7FEA +DEPOSITS_CONTRACT_ADDRESS=0xE9721850B0dEb1B9D4D6747f6F4106cdA274E3C1 +EPOCHS_CONTRACT_ADDRESS=0x2d87d8607Cc2bc0d7AC40E862825675FC5A95208 +PROPOSALS_CONTRACT_ADDRESS=0xB95da9a422A5731cDCAb5978d54A724B23F94E62 +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x5d619197A1d14Da4dC8eCBbF52dDeAE74243b489 +VAULT_CONTRACT_ADDRESS=0x8Ee3d6aC8b3eD29eCF1B03F00C84B509954cb82e From fe155e4acdd8d1c3a44a4e2aacf45f291d71534a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Tue, 2 Apr 2024 10:55:05 +0200 Subject: [PATCH 097/107] OCT-1511 User cannot manually allocate different values to more than 1 projects (#114) --- .../src/components/Allocation/AllocationItem/AllocationItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx index 4f4fc9344c..bb14cc4bc1 100644 --- a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx +++ b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx @@ -81,7 +81,7 @@ const AllocationItem: FC = ({ 250, { trailing: true }, ), - [], + [onChange], ); const _onChange = (newValue: string) => { From 9ff5213367e6bfe340d83598deda78c89cf41680 Mon Sep 17 00:00:00 2001 From: Housekeeper Bot Date: Tue, 2 Apr 2024 09:12:29 +0000 Subject: [PATCH 098/107] [CI/CD] Update uat.env contracts --- ci/argocd/contracts/uat.env | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ci/argocd/contracts/uat.env b/ci/argocd/contracts/uat.env index 8fef04dbba..625b4ad429 100644 --- a/ci/argocd/contracts/uat.env +++ b/ci/argocd/contracts/uat.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=5611649 +BLOCK_NUMBER=5612319 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0x08238BF42F1f3fAD797Fcd513abdeD28A74E7FEA -DEPOSITS_CONTRACT_ADDRESS=0xE9721850B0dEb1B9D4D6747f6F4106cdA274E3C1 -EPOCHS_CONTRACT_ADDRESS=0x2d87d8607Cc2bc0d7AC40E862825675FC5A95208 -PROPOSALS_CONTRACT_ADDRESS=0xB95da9a422A5731cDCAb5978d54A724B23F94E62 -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x5d619197A1d14Da4dC8eCBbF52dDeAE74243b489 -VAULT_CONTRACT_ADDRESS=0x8Ee3d6aC8b3eD29eCF1B03F00C84B509954cb82e +AUTH_CONTRACT_ADDRESS=0x1684F294fe4fB1C6ABcA9f0ABa30c258b81A404f +DEPOSITS_CONTRACT_ADDRESS=0xB122BF4AA350ff5BA6DECE3E3091Ed6a07b9b724 +EPOCHS_CONTRACT_ADDRESS=0x4c8a3e04FbdA2869Ea20f01f07C0DB5aa6be897f +PROPOSALS_CONTRACT_ADDRESS=0x2BAaE7b5379D65F344bf509B3aaF0E40A1F5d620 +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0xA0888aD17C9074c6F363847d8BEDaBFcd5D16B6E +VAULT_CONTRACT_ADDRESS=0x2bdAa71531e4683b0510f59D0c1948ADFeDB5Bbe From 442fc7b2ae840988402394812a811e1aeeb1b2bf Mon Sep 17 00:00:00 2001 From: Pawel Peregud Date: Tue, 2 Apr 2024 11:33:59 +0200 Subject: [PATCH 099/107] fix allocate in websockets API --- backend/app/infrastructure/events.py | 4 +++- backend/docs/websockets-api.yaml | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index a5c5dc94c8..6da7b986ff 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -38,8 +38,10 @@ def handle_disconnect(): def handle_allocate(msg): msg = json.loads(msg) is_manually_edited = msg["isManuallyEdited"] if "isManuallyEdited" in msg else None + user_address = msg["userAddress"] app.logger.info(f"User allocation payload: {msg}") - user_address = controller.allocate( + controller.allocate( + user_address, msg, is_manually_edited=is_manually_edited, ) diff --git a/backend/docs/websockets-api.yaml b/backend/docs/websockets-api.yaml index c04c829521..dc90021cf8 100644 --- a/backend/docs/websockets-api.yaml +++ b/backend/docs/websockets-api.yaml @@ -56,7 +56,7 @@ channels: signature: description: EIP-712 signature of the allocation payload as a hexadecimal string type: string - user_address: + userAddress: description: Wallet address of the user. EOA or EIP-1271 type: string isManuallyEdited: @@ -72,7 +72,7 @@ channels: - proposalAddress: "0xBcd4042DE499D14e55001CcbB24a551F3b954096" amount: "5000" signature: "8d704f19cde0f1f9d310e57621229b919a8e17187be332c4bd08bf797d0fb50232b4aa30639b741723e647667d87da1af38fd4601600f4d4e2c6f724abea03d61b" - user_address: "0x17F6AD8Ef982297579C203069C1DbfFE4348c372" + userAddress: "0x17F6AD8Ef982297579C203069C1DbfFE4348c372" exception: subscribe: From 4ff541e1be9b2f9c99da65e657a2637a2f72dfa4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Tue, 2 Apr 2024 11:49:57 +0200 Subject: [PATCH 100/107] OCT-1496: Threshold 1n (#106) --- backend/app/engine/epochs_settings.py | 13 +++- .../app/engine/projects/rewards/__init__.py | 4 ++ .../app/engine/projects/rewards/default.py | 15 ++-- .../projects/rewards/threshold/default.py | 10 ++- backend/app/infrastructure/events.py | 5 +- backend/app/infrastructure/routes/rewards.py | 8 ++- backend/app/legacy/controllers/rewards.py | 10 --- backend/app/legacy/core/proposals.py | 8 --- .../app/modules/modules_factory/finalized.py | 4 ++ .../app/modules/modules_factory/finalizing.py | 4 ++ .../app/modules/modules_factory/pending.py | 11 ++- .../modules/modules_factory/pre_pending.py | 4 ++ .../app/modules/modules_factory/protocols.py | 6 ++ .../app/modules/project_rewards/controller.py | 21 +++++- .../project_rewards/service/estimated.py | 3 +- .../modules/project_rewards/service/saved.py | 13 ++++ backend/tests/conftest.py | 1 - .../projects/rewards/test_default_rewards.py | 8 +-- .../tests/engine/projects/test_threshold.py | 23 +++--- backend/tests/engine/test_epoch_settings.py | 16 ++++- backend/tests/legacy/test_allocations.py | 0 backend/tests/legacy/test_rewards.py | 71 ------------------- .../modules_factory/test_modules_factory.py | 13 ++++ .../project_rewards/test_estimated_rewards.py | 2 +- .../test_projects_saved_rewards.py | 38 ++++++++++ ..._rewards.py => test_user_saved_rewards.py} | 0 26 files changed, 189 insertions(+), 122 deletions(-) create mode 100644 backend/app/modules/project_rewards/service/saved.py delete mode 100644 backend/tests/legacy/test_allocations.py delete mode 100644 backend/tests/legacy/test_rewards.py create mode 100644 backend/tests/modules/project_rewards/test_projects_saved_rewards.py rename backend/tests/modules/user/rewards/{test_saved_rewards.py => test_user_saved_rewards.py} (100%) diff --git a/backend/app/engine/epochs_settings.py b/backend/app/engine/epochs_settings.py index 6969326881..23d8781b3f 100644 --- a/backend/app/engine/epochs_settings.py +++ b/backend/app/engine/epochs_settings.py @@ -17,7 +17,8 @@ from app.engine.octant_rewards.total_and_individual.preliminary import ( PreliminaryTotalAndAllIndividualRewards, ) -from app.engine.projects import ProjectSettings +from app.engine.projects import ProjectSettings, DefaultProjectRewards +from app.engine.projects.rewards.threshold.default import DefaultProjectThreshold from app.engine.user.budget.preliminary import PreliminaryUserBudget from app.engine.user import UserSettings, DefaultWeightedAverageEffectiveDeposit from app.engine.user.effective_deposit.weighted_average.weights.timebased.default import ( @@ -54,6 +55,11 @@ def register_epoch_settings(): timebased_weights=DefaultTimebasedWeights(), ), ), + project=ProjectSettings( + rewards=DefaultProjectRewards( + projects_threshold=DefaultProjectThreshold(2), + ), + ), ) SETTINGS[2] = EpochSettings( @@ -64,6 +70,11 @@ def register_epoch_settings(): community_fund=NotSupportedCFCalculator(), ), user=UserSettings(budget=PreliminaryUserBudget()), + project=ProjectSettings( + rewards=DefaultProjectRewards( + projects_threshold=DefaultProjectThreshold(2), + ), + ), ) SETTINGS[3] = EpochSettings() diff --git a/backend/app/engine/projects/rewards/__init__.py b/backend/app/engine/projects/rewards/__init__.py index 062ef3e327..dd7595b6c7 100644 --- a/backend/app/engine/projects/rewards/__init__.py +++ b/backend/app/engine/projects/rewards/__init__.py @@ -45,3 +45,7 @@ def calculate_project_rewards( self, payload: ProjectRewardsPayload ) -> ProjectRewardsResult: pass + + @abstractmethod + def calculate_threshold(self, total_allocated: int, projects: list[str]) -> int: + pass diff --git a/backend/app/engine/projects/rewards/default.py b/backend/app/engine/projects/rewards/default.py index 59fd687cf8..8c8d36b62f 100644 --- a/backend/app/engine/projects/rewards/default.py +++ b/backend/app/engine/projects/rewards/default.py @@ -25,9 +25,16 @@ class DefaultProjectRewards(ProjectRewards): default_factory=DefaultProjectAllocations ) projects_threshold: ProjectThreshold = field( - default_factory=DefaultProjectThreshold + default_factory=lambda: DefaultProjectThreshold(1) ) + def calculate_threshold(self, total_allocated: int, projects: list[str]) -> int: + return self.projects_threshold.calculate_threshold( + ProjectThresholdPayload( + total_allocated=total_allocated, projects_count=len(projects) + ) + ) + def calculate_project_rewards( self, payload: ProjectRewardsPayload ) -> ProjectRewardsResult: @@ -37,11 +44,7 @@ def calculate_project_rewards( ) = self.projects_allocations.group_allocations_by_projects( ProjectAllocationsPayload(allocations=payload.allocations) ) - threshold = self.projects_threshold.calculate_threshold( - ProjectThresholdPayload( - total_allocated=total_allocated, projects_count=len(payload.projects) - ) - ) + threshold = self.calculate_threshold(total_allocated, payload.projects) total_allocated_above_threshold = sum( [allocated for _, allocated in allocated_by_addr if allocated > threshold] diff --git a/backend/app/engine/projects/rewards/threshold/default.py b/backend/app/engine/projects/rewards/threshold/default.py index 638d7812ca..b0ac1d9225 100644 --- a/backend/app/engine/projects/rewards/threshold/default.py +++ b/backend/app/engine/projects/rewards/threshold/default.py @@ -1,13 +1,21 @@ +from dataclasses import dataclass + from app.engine.projects.rewards.threshold import ( ProjectThreshold, ProjectThresholdPayload, ) +@dataclass class DefaultProjectThreshold(ProjectThreshold): + PROJECTS_COUNT_MULTIPLIER: int + def calculate_threshold(self, payload: ProjectThresholdPayload) -> int: return ( - int(payload.total_allocated / (payload.projects_count * 2)) + int( + payload.total_allocated + / (payload.projects_count * self.PROJECTS_COUNT_MULTIPLIER) + ) if payload.projects_count else 0 ) diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index a5c5dc94c8..0aa497aa54 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -9,12 +9,11 @@ from app.extensions import socketio, epochs from app.infrastructure.exception_handler import UNEXPECTED_EXCEPTION, ExceptionHandler from app.modules.dto import ProposalDonationDTO -from app.modules.user.allocations import controller - -from app.legacy.controllers.rewards import ( +from app.modules.project_rewards.controller import ( get_allocation_threshold, ) from app.modules.project_rewards.controller import get_estimated_project_rewards +from app.modules.user.allocations import controller @socketio.on("connect") diff --git a/backend/app/infrastructure/routes/rewards.py b/backend/app/infrastructure/routes/rewards.py index 501167647f..06922a7cf3 100644 --- a/backend/app/infrastructure/routes/rewards.py +++ b/backend/app/infrastructure/routes/rewards.py @@ -10,7 +10,10 @@ from app.legacy.controllers import rewards from app.modules.common.time import days_to_sec from app.modules.octant_rewards.controller import get_leverage -from app.modules.project_rewards.controller import get_estimated_project_rewards +from app.modules.project_rewards.controller import ( + get_estimated_project_rewards, + get_allocation_threshold, +) from app.modules.user.budgets.controller import estimate_budget, get_budgets, get_budget from app.modules.user.rewards.controller import get_unused_rewards @@ -243,12 +246,13 @@ def post(self): }, ) @ns.response(200, "Returns allocation threshold value as uint256") +@ns.response(400, "Returns when called for an epoch that is not finalized or pending") class Threshold(OctantResource): @ns.marshal_with(threshold_model) @ns.response(200, "Threshold successfully retrieved") def get(self, epoch): app.logger.debug(f"Getting threshold for epoch {epoch}") - threshold = rewards.get_allocation_threshold(epoch) + threshold = get_allocation_threshold(epoch) app.logger.debug(f"Threshold in epoch: {epoch}: {threshold}") return {"threshold": threshold} diff --git a/backend/app/legacy/controllers/rewards.py b/backend/app/legacy/controllers/rewards.py index ba3f4b97f9..f070f9715c 100644 --- a/backend/app/legacy/controllers/rewards.py +++ b/backend/app/legacy/controllers/rewards.py @@ -5,7 +5,6 @@ from dataclass_wizard import JSONWizard from app import exceptions -from app.extensions import epochs from app.infrastructure import database from app.legacy.core import proposals from app.legacy.core.epochs import epoch_snapshots as core_epoch_snapshots @@ -34,15 +33,6 @@ class RewardsMerkleTree(JSONWizard): leaf_encoding: List[str] -def get_allocation_threshold(epoch: int = None) -> int: - epoch = epochs.get_pending_epoch() if epoch is None else epoch - - if epoch is None: - raise exceptions.NotInDecisionWindow - - return proposals.get_proposal_allocation_threshold(epoch) - - def get_finalized_epoch_proposals_rewards(epoch: int = None) -> List[ProposalReward]: last_finalized_epoch = core_epoch_snapshots.get_last_finalized_snapshot() if epoch > last_finalized_epoch: diff --git a/backend/app/legacy/core/proposals.py b/backend/app/legacy/core/proposals.py index e7f389f9b6..2e7cf79c9f 100644 --- a/backend/app/legacy/core/proposals.py +++ b/backend/app/legacy/core/proposals.py @@ -3,7 +3,6 @@ from app.extensions import epochs, proposals from app.infrastructure import database -from app.infrastructure.database import allocations as allocation_db def get_proposals_addresses(epoch: Optional[int]) -> List[str]: @@ -11,13 +10,6 @@ def get_proposals_addresses(epoch: Optional[int]) -> List[str]: return proposals.get_proposal_addresses(epoch) -def get_proposal_allocation_threshold(epoch: int) -> int: - proposals_addresses = get_proposals_addresses(epoch) - total_allocated = allocation_db.get_alloc_sum_by_epoch(epoch) - - return int(total_allocated / (len(proposals_addresses) * 2)) - - def get_proposals_with_allocations(epoch: int) -> (str, int): # Get *all* project allocations in the given epoch allocations = database.allocations.get_all_by_epoch(epoch) diff --git a/backend/app/modules/modules_factory/finalized.py b/backend/app/modules/modules_factory/finalized.py index 1a38827c9f..001c989c43 100644 --- a/backend/app/modules/modules_factory/finalized.py +++ b/backend/app/modules/modules_factory/finalized.py @@ -11,8 +11,10 @@ Leverage, UserBudgets, WithdrawalsService, + SavedProjectRewardsService, ) from app.modules.octant_rewards.service.finalized import FinalizedOctantRewards +from app.modules.project_rewards.service.saved import SavedProjectRewards from app.modules.user.allocations.service.saved import SavedUserAllocations from app.modules.user.budgets.service.saved import SavedUserBudgets from app.modules.user.deposits.service.saved import SavedUserDeposits @@ -44,6 +46,7 @@ class FinalizedServices(Model): user_budgets_service: UserBudgets user_rewards_service: UserRewards withdrawals_service: WithdrawalsService + project_rewards_service: SavedProjectRewardsService @staticmethod def create() -> "FinalizedServices": @@ -65,4 +68,5 @@ def create() -> "FinalizedServices": user_budgets_service=saved_user_budgets, user_rewards_service=user_rewards, withdrawals_service=withdrawals_service, + project_rewards_service=SavedProjectRewards(), ) diff --git a/backend/app/modules/modules_factory/finalizing.py b/backend/app/modules/modules_factory/finalizing.py index d591c0e1e4..ffe2c5d288 100644 --- a/backend/app/modules/modules_factory/finalizing.py +++ b/backend/app/modules/modules_factory/finalizing.py @@ -11,8 +11,10 @@ UserBudgets, CreateFinalizedSnapshots, WithdrawalsService, + SavedProjectRewardsService, ) from app.modules.octant_rewards.service.pending import PendingOctantRewards +from app.modules.project_rewards.service.saved import SavedProjectRewards from app.modules.snapshots.finalized.service.finalizing import FinalizingSnapshots from app.modules.user.allocations.service.saved import SavedUserAllocations from app.modules.user.budgets.service.saved import SavedUserBudgets @@ -40,6 +42,7 @@ class FinalizingServices(Model): user_rewards_service: UserRewards finalized_snapshots_service: CreateFinalizedSnapshots withdrawals_service: WithdrawalsService + project_rewards_service: SavedProjectRewardsService @staticmethod def create() -> "FinalizingServices": @@ -68,4 +71,5 @@ def create() -> "FinalizingServices": user_rewards_service=user_rewards, finalized_snapshots_service=finalized_snapshots_service, withdrawals_service=withdrawals_service, + project_rewards_service=SavedProjectRewards(), ) diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 4671869996..a535f97389 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -16,10 +16,12 @@ DonorsAddresses, AllocationManipulationProtocol, GetUserAllocationsProtocol, + SavedProjectRewardsService, MultisigSignatures, ) from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures from app.modules.octant_rewards.service.pending import PendingOctantRewards +from app.modules.project_rewards.service.estimated import EstimatedProjectRewards from app.modules.snapshots.finalized.service.simulated import ( SimulatedFinalizedSnapshots, ) @@ -33,7 +35,6 @@ from app.modules.user.rewards.service.calculated import CalculatedUserRewards from app.modules.withdrawals.service.pending import PendingWithdrawals from app.pydantic import Model -from app.modules.project_rewards.service.estimated import EstimatedProjectRewards class PendingOctantRewardsService(OctantRewards, Leverage, Protocol): @@ -54,6 +55,12 @@ class PendingUserAllocationsProtocol( pass +class PendingProjectRewardsProtocol( + EstimatedProjectRewardsService, SavedProjectRewardsService, Protocol +): + pass + + class PendingServices(Model): user_deposits_service: PendingUserDeposits octant_rewards_service: PendingOctantRewardsService @@ -63,7 +70,7 @@ class PendingServices(Model): user_rewards_service: UserRewards finalized_snapshots_service: SimulateFinalizedSnapshots withdrawals_service: WithdrawalsService - project_rewards_service: EstimatedProjectRewardsService + project_rewards_service: PendingProjectRewardsProtocol multisig_signatures_service: MultisigSignatures @staticmethod diff --git a/backend/app/modules/modules_factory/pre_pending.py b/backend/app/modules/modules_factory/pre_pending.py index 558133dae4..6d92692c06 100644 --- a/backend/app/modules/modules_factory/pre_pending.py +++ b/backend/app/modules/modules_factory/pre_pending.py @@ -7,8 +7,10 @@ OctantRewards, PendingSnapshots, UserEffectiveDeposits, + SavedProjectRewardsService, ) from app.modules.octant_rewards.service.calculated import CalculatedOctantRewards +from app.modules.project_rewards.service.saved import SavedProjectRewards from app.modules.snapshots.pending.service.pre_pending import PrePendingSnapshots from app.modules.user.deposits.service.calculated import CalculatedUserDeposits from app.modules.user.events_generator.service.db_and_graph import ( @@ -26,6 +28,7 @@ class PrePendingServices(Model): user_deposits_service: PrePendingUserDeposits octant_rewards_service: OctantRewards pending_snapshots_service: PendingSnapshots + project_rewards_service: SavedProjectRewardsService @staticmethod def create(chain_id: int) -> "PrePendingServices": @@ -49,4 +52,5 @@ def create(chain_id: int) -> "PrePendingServices": user_deposits_service=user_deposits, octant_rewards_service=octant_rewards, pending_snapshots_service=pending_snapshots_service, + project_rewards_service=SavedProjectRewards(), ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index a7b609cbf7..f56135e68a 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -155,6 +155,12 @@ def get_project_rewards(self, context: Context) -> ProjectRewardsResult: ... +@runtime_checkable +class SavedProjectRewardsService(Protocol): + def get_allocation_threshold(self, context: Context) -> int: + ... + + @runtime_checkable class HistoryService(Protocol): def get_user_history( diff --git a/backend/app/modules/project_rewards/controller.py b/backend/app/modules/project_rewards/controller.py index 314058aa21..9abe601a56 100644 --- a/backend/app/modules/project_rewards/controller.py +++ b/backend/app/modules/project_rewards/controller.py @@ -1,4 +1,5 @@ -from app.context.manager import state_context +from app.context.manager import state_context, epoch_context, Context +from app.exceptions import NotImplementedForGivenEpochState from app.modules.registry import get_services from app.context.epoch_state import EpochState from app.engine.projects.rewards import ProjectRewardsResult @@ -8,3 +9,21 @@ def get_estimated_project_rewards() -> ProjectRewardsResult: context = state_context(EpochState.PENDING) service = get_services(context.epoch_state).project_rewards_service return service.get_project_rewards(context) + + +def get_allocation_threshold(epoch: int = None) -> int: + context = _get_context(epoch) + service = get_services(context.epoch_state).project_rewards_service + return service.get_allocation_threshold(context) + + +def _get_context(epoch: int = None) -> Context: + if epoch is not None: + context = epoch_context(epoch) + else: + context = state_context(EpochState.PENDING) + + if context.epoch_state > EpochState.PENDING: + raise NotImplementedForGivenEpochState() + + return context diff --git a/backend/app/modules/project_rewards/service/estimated.py b/backend/app/modules/project_rewards/service/estimated.py index facfe61c4b..8986a8a48a 100644 --- a/backend/app/modules/project_rewards/service/estimated.py +++ b/backend/app/modules/project_rewards/service/estimated.py @@ -4,6 +4,7 @@ from app.engine.projects.rewards import ProjectRewardsResult from app.infrastructure import database from app.modules.common.project_rewards import get_projects_rewards +from app.modules.project_rewards.service.saved import SavedProjectRewards from app.pydantic import Model @@ -13,7 +14,7 @@ def get_matched_rewards(self, context: Context) -> int: ... -class EstimatedProjectRewards(Model): +class EstimatedProjectRewards(SavedProjectRewards, Model): octant_rewards: OctantRewards def get_project_rewards(self, context: Context) -> ProjectRewardsResult: diff --git a/backend/app/modules/project_rewards/service/saved.py b/backend/app/modules/project_rewards/service/saved.py new file mode 100644 index 0000000000..45f5b8b58b --- /dev/null +++ b/backend/app/modules/project_rewards/service/saved.py @@ -0,0 +1,13 @@ +from app.context.manager import Context +from app.infrastructure import database +from app.pydantic import Model + + +class SavedProjectRewards(Model): + def get_allocation_threshold(self, context: Context) -> int: + epoch_num = context.epoch_details.epoch_num + total_allocated = database.allocations.get_alloc_sum_by_epoch(epoch_num) + + return context.epoch_settings.project.rewards.calculate_threshold( + total_allocated, context.projects_details.projects + ) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 45e3ea07ee..30b0a3de0d 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -413,7 +413,6 @@ def mock_epoch_details(mocker, graphql_client): @pytest.fixture(scope="function") def patch_epochs(monkeypatch): monkeypatch.setattr("app.legacy.controllers.snapshots.epochs", MOCK_EPOCHS) - monkeypatch.setattr("app.legacy.controllers.rewards.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.legacy.core.proposals.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.context.epoch_state.epochs", MOCK_EPOCHS) monkeypatch.setattr("app.context.epoch_details.epochs", MOCK_EPOCHS) diff --git a/backend/tests/engine/projects/rewards/test_default_rewards.py b/backend/tests/engine/projects/rewards/test_default_rewards.py index 39172c75dc..e5bc191463 100644 --- a/backend/tests/engine/projects/rewards/test_default_rewards.py +++ b/backend/tests/engine/projects/rewards/test_default_rewards.py @@ -122,7 +122,7 @@ def test_compute_rewards_when_one_project_is_below_threshold(): MATCHED_REWARDS + 500_000000000 + 200_000000000, 0.00000000000000000001 ) - assert result.threshold == 76_900000000 + assert result.threshold == 153_800000000 def test_compute_rewards_when_one_project_is_at_threshold(): @@ -151,7 +151,7 @@ def test_compute_rewards_when_one_project_is_at_threshold(): assert result.rewards_sum == pytest.approx( MATCHED_REWARDS + 500_000000000 + 400_000000000, 0.00000000000000000001 ) - assert result.threshold == 100_000000000 + assert result.threshold == 200_000000000 def test_compute_rewards_when_multiple_projects_are_below_threshold(): @@ -178,7 +178,7 @@ def test_compute_rewards_when_multiple_projects_are_below_threshold(): MATCHED_REWARDS + 500_000000000, 0.00000000000000000001 ) - assert result.threshold == 56_000000000 + assert result.threshold == 112_000000000 def test_total_allocated_is_computed(): @@ -196,4 +196,4 @@ def test_total_allocated_is_computed(): result = uut.calculate_project_rewards(payload) assert result.total_allocated == 1300_000000000 - assert result.threshold == 130_000000000 + assert result.threshold == 260_000000000 diff --git a/backend/tests/engine/projects/test_threshold.py b/backend/tests/engine/projects/test_threshold.py index 2d364d9795..f3dbc942ea 100644 --- a/backend/tests/engine/projects/test_threshold.py +++ b/backend/tests/engine/projects/test_threshold.py @@ -5,18 +5,25 @@ @pytest.mark.parametrize( - "allocated,projects_count,expected", + "allocated,projects_count,projects_count_multiplier,expected", [ - (0, 20, 0), - (1_000000000_000000000, 5, 100000000_000000000), - (1_000000000_000000000, 25, 20000000_000000000), - (9987_443300000, 25, 199_748866000), - (9987_443300000, 0, 0), + (0, 20, 1, 0), + (1_000000000_000000000, 5, 1, 200000000_000000000), + (1_000000000_000000000, 25, 1, 40000000_000000000), + (9987_443300000, 25, 1, 399_497732000), + (9987_443300000, 0, 1, 0), + (0, 20, 2, 0), + (1_000000000_000000000, 5, 2, 100000000_000000000), + (1_000000000_000000000, 25, 2, 20000000_000000000), + (9987_443300000, 25, 2, 199_748866000), + (9987_443300000, 0, 2, 0), ], ) -def test_default_threshold(allocated, projects_count, expected): +def test_default_threshold( + allocated, projects_count, projects_count_multiplier, expected +): payload = ProjectThresholdPayload(allocated, projects_count) - uut = DefaultProjectThreshold() + uut = DefaultProjectThreshold(projects_count_multiplier) result = uut.calculate_threshold(payload) diff --git a/backend/tests/engine/test_epoch_settings.py b/backend/tests/engine/test_epoch_settings.py index 69e4198e5e..913c3f6d77 100644 --- a/backend/tests/engine/test_epoch_settings.py +++ b/backend/tests/engine/test_epoch_settings.py @@ -53,6 +53,9 @@ def test_default_epoch_settings(): community_fund=CommunityFundPercent(OctantRewardsDefaultValues.COMMUNITY_FUND), user_budget=UserBudgetWithPPF(), matched_rewards=MatchedRewardsWithPPF(), + projects_rewards=DefaultProjectRewards( + projects_threshold=DefaultProjectThreshold(1), + ), ) @@ -68,6 +71,9 @@ def test_epoch_1_settings(): ppf=NotSupportedPPFCalculator(), community_fund=NotSupportedCFCalculator(), user_budget=PreliminaryUserBudget(), + projects_rewards=DefaultProjectRewards( + projects_threshold=DefaultProjectThreshold(2), + ), ) @@ -83,6 +89,9 @@ def test_epoch_2_settings(): ppf=NotSupportedPPFCalculator(), community_fund=NotSupportedCFCalculator(), user_budget=PreliminaryUserBudget(), + projects_rewards=DefaultProjectRewards( + projects_threshold=DefaultProjectThreshold(2), + ), ) @@ -101,6 +110,9 @@ def test_epoch_3_settings(): community_fund=CommunityFundPercent(OctantRewardsDefaultValues.COMMUNITY_FUND), ppf=PPFCalculatorPercent(OctantRewardsDefaultValues.PPF), user_budget=UserBudgetWithPPF(), + projects_rewards=DefaultProjectRewards( + projects_threshold=DefaultProjectThreshold(1), + ), ) @@ -114,6 +126,7 @@ def check_settings( ppf, community_fund, user_budget, + projects_rewards, ): assert settings.octant_rewards.locked_ratio == DefaultLockedRatio() assert ( @@ -131,6 +144,5 @@ def check_settings( timebased_weights=timebased_weights ) - assert settings.project.rewards == DefaultProjectRewards() - assert settings.project.rewards.projects_threshold == DefaultProjectThreshold() + assert settings.project.rewards == projects_rewards assert settings.project.rewards.projects_allocations == DefaultProjectAllocations() diff --git a/backend/tests/legacy/test_allocations.py b/backend/tests/legacy/test_allocations.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/tests/legacy/test_rewards.py b/backend/tests/legacy/test_rewards.py deleted file mode 100644 index 54f59eadc2..0000000000 --- a/backend/tests/legacy/test_rewards.py +++ /dev/null @@ -1,71 +0,0 @@ -import pytest - -from app import exceptions -from app.modules.user.allocations.controller import allocate -from app.legacy.crypto.eip712 import build_allocations_eip712_data, sign - -from app.legacy.controllers.rewards import ( - get_allocation_threshold, -) -from tests.conftest import ( - MOCK_EPOCHS, - MOCK_PROPOSALS, -) -from tests.helpers.allocations import create_payload, deserialize_allocations - - -from app.modules.user.allocations import controller as new_controller - - -def get_allocation_nonce(user_address): - return new_controller.get_user_next_nonce(user_address) - - -@pytest.fixture(autouse=True) -def before( - proposal_accounts, - mock_epoch_details, - patch_epochs, - patch_proposals, - patch_has_pending_epoch_snapshot, - patch_user_budget, - patch_is_contract, -): - MOCK_PROPOSALS.get_proposal_addresses.return_value = [ - p.address for p in proposal_accounts[0:5] - ] - - -def test_get_allocation_threshold(app, tos_users, proposal_accounts): - total_allocated = _allocate_random_individual_rewards(tos_users, proposal_accounts) - - assert get_allocation_threshold(None) == int(total_allocated / 10) - - -def test_get_allocation_threshold_raises_when_not_in_allocation_period(app): - MOCK_EPOCHS.get_pending_epoch.return_value = None - - with pytest.raises(exceptions.NotInDecisionWindow): - get_allocation_threshold(None) - - -def _allocate_random_individual_rewards(user_accounts, proposal_accounts) -> int: - """ - Allocates individual rewards from 2 users for 5 projects total - - Returns the sum of these allocations - """ - payload1 = create_payload(proposal_accounts[0:2], None, 0) - signature1 = sign(user_accounts[0], build_allocations_eip712_data(payload1)) - - payload2 = create_payload(proposal_accounts[0:3], None, 0) - signature2 = sign(user_accounts[1], build_allocations_eip712_data(payload2)) - - # Call allocate method for both users - allocate({"payload": payload1, "signature": signature1}) - allocate({"payload": payload2, "signature": signature2}) - - allocations1 = sum([int(a.amount) for a in deserialize_allocations(payload1)]) - allocations2 = sum([int(a.amount) for a in deserialize_allocations(payload2)]) - - return allocations1 + allocations2 diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 3f6debf8d9..3dee43657c 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -10,6 +10,8 @@ from app.modules.octant_rewards.service.calculated import CalculatedOctantRewards from app.modules.octant_rewards.service.finalized import FinalizedOctantRewards from app.modules.octant_rewards.service.pending import PendingOctantRewards +from app.modules.project_rewards.service.estimated import EstimatedProjectRewards +from app.modules.project_rewards.service.saved import SavedProjectRewards from app.modules.snapshots.finalized.service.finalizing import FinalizingSnapshots from app.modules.snapshots.finalized.service.simulated import ( SimulatedFinalizedSnapshots, @@ -90,11 +92,13 @@ def test_pre_pending_services_factory_when_mainnet(): staking_proceeds=AggregatedStakingProceeds(), effective_deposits=user_deposits, ) + project_rewards_service = SavedProjectRewards() assert result.user_deposits_service == user_deposits assert result.octant_rewards_service == octant_rewards assert result.pending_snapshots_service == PrePendingSnapshots( effective_deposits=user_deposits, octant_rewards=octant_rewards ) + assert result.project_rewards_service == project_rewards_service def test_pre_pending_services_factory_when_not_mainnet(): @@ -105,11 +109,14 @@ def test_pre_pending_services_factory_when_not_mainnet(): staking_proceeds=ContractBalanceStakingProceeds(), effective_deposits=user_deposits, ) + project_rewards_service = SavedProjectRewards() + assert result.user_deposits_service == user_deposits assert result.octant_rewards_service == octant_rewards assert result.pending_snapshots_service == PrePendingSnapshots( effective_deposits=user_deposits, octant_rewards=octant_rewards ) + assert result.project_rewards_service == project_rewards_service def test_pending_services_factory(): @@ -136,6 +143,7 @@ def test_pending_services_factory(): patrons_mode=events_based_patron_mode, ) withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) + project_rewards = EstimatedProjectRewards(octant_rewards=octant_rewards) multisig_signatures = OffchainMultisigSignatures( verifiers={SignatureOpType.ALLOCATION: allocations_verifier} ) @@ -147,6 +155,7 @@ def test_pending_services_factory(): assert result.user_rewards_service == user_rewards assert result.finalized_snapshots_service == finalized_snapshots_service assert result.withdrawals_service == withdrawals_service + assert result.project_rewards_service == project_rewards assert result.multisig_signatures_service == multisig_signatures @@ -167,6 +176,7 @@ def test_finalizing_services_factory(): patrons_mode=events_based_patron_mode, ) withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) + project_rewards_service = SavedProjectRewards() assert result.user_deposits_service == SavedUserDeposits() assert result.octant_rewards_service == octant_rewards @@ -175,6 +185,7 @@ def test_finalizing_services_factory(): assert result.user_rewards_service == user_rewards assert result.finalized_snapshots_service == finalized_snapshots_service assert result.withdrawals_service == withdrawals_service + assert result.project_rewards_service == project_rewards_service def test_finalized_services_factory(): @@ -188,6 +199,7 @@ def test_finalized_services_factory(): allocations=saved_user_allocations, ) withdrawals_service = FinalizedWithdrawals(user_rewards=user_rewards) + project_rewards_service = SavedProjectRewards() assert result.user_deposits_service == SavedUserDeposits() assert result.octant_rewards_service == FinalizedOctantRewards() @@ -195,3 +207,4 @@ def test_finalized_services_factory(): assert result.user_patron_mode_service == events_based_patron_mode assert result.user_rewards_service == user_rewards assert result.withdrawals_service == withdrawals_service + assert result.project_rewards_service == project_rewards_service diff --git a/backend/tests/modules/project_rewards/test_estimated_rewards.py b/backend/tests/modules/project_rewards/test_estimated_rewards.py index 1117d09d9b..46f5c1d771 100644 --- a/backend/tests/modules/project_rewards/test_estimated_rewards.py +++ b/backend/tests/modules/project_rewards/test_estimated_rewards.py @@ -53,4 +53,4 @@ def test_estimated_project_rewards_with_allocations( assert result.total_allocated == 1526868989237987 assert result.rewards_sum == 220115925184490486394 - assert result.threshold == 76343449461899 + assert result.threshold == 152686898923798 diff --git a/backend/tests/modules/project_rewards/test_projects_saved_rewards.py b/backend/tests/modules/project_rewards/test_projects_saved_rewards.py new file mode 100644 index 0000000000..c077535430 --- /dev/null +++ b/backend/tests/modules/project_rewards/test_projects_saved_rewards.py @@ -0,0 +1,38 @@ +import pytest + +from app.extensions import db +from app.infrastructure import database +from app.modules.dto import ( + AllocationDTO, + UserAllocationRequestPayload, + UserAllocationPayload, +) +from app.modules.project_rewards.service.saved import SavedProjectRewards +from tests.helpers.context import get_context + + +@pytest.fixture(autouse=True) +def before(app): + pass + + +def test_get_allocation_threshold(mock_users_db): + context = get_context(3) + user1, user2, _ = mock_users_db + projects = context.projects_details.projects + allocation = [ + AllocationDTO(projects[0], 100), + AllocationDTO(projects[1], 200), + AllocationDTO(projects[2], 300), + ] + payload = UserAllocationRequestPayload( + payload=UserAllocationPayload(allocation, 0), signature="0xdeadbeef" + ) + database.allocations.store_allocation_request(user1.address, 3, payload) + database.allocations.store_allocation_request(user2.address, 3, payload) + db.session.commit() + + service = SavedProjectRewards() + result = service.get_allocation_threshold(context) + + assert result == 120 diff --git a/backend/tests/modules/user/rewards/test_saved_rewards.py b/backend/tests/modules/user/rewards/test_user_saved_rewards.py similarity index 100% rename from backend/tests/modules/user/rewards/test_saved_rewards.py rename to backend/tests/modules/user/rewards/test_user_saved_rewards.py From 87920449ce65d1fce5e4d8717e3850f898218e9b Mon Sep 17 00:00:00 2001 From: Paul Peregud Date: Tue, 2 Apr 2024 12:15:21 +0200 Subject: [PATCH 101/107] Update backend/tests/conftest.py MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ɓukasz Kujawski --- backend/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 05c2be980a..2c118d1a71 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -316,7 +316,7 @@ def allocate(self, payload: dict, user_address: str, signature: str) -> int: "/allocations/allocate", json={ "payload": payload, - "user_address": user_address, + "userAddress": user_address, "signature": signature, }, ) From 858291e5cc6a2e0d626466c8d0c2b84e5c0ccbe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Garbaci=C5=84ski?= <57113816+kgarbacinski@users.noreply.github.com> Date: Tue, 2 Apr 2024 17:34:19 +0200 Subject: [PATCH 102/107] OCT-1469: Approve and apply multisig messages --- backend/app/constants.py | 2 + .../database/multisig_signature.py | 4 + backend/app/infrastructure/events.py | 2 +- .../external_api/safe/__init__.py | 0 .../external_api/safe/message_details.py | 27 ++++ .../external_api/safe/user_details.py | 24 +++ .../routes/multisig_signatures.py | 12 ++ .../app/modules/facades/confirm_multisig.py | 32 ++++ .../app/modules/modules_factory/current.py | 11 +- .../app/modules/modules_factory/pending.py | 10 +- .../app/modules/modules_factory/protocols.py | 6 + .../modules/multisig_signatures/controller.py | 33 +++- .../app/modules/multisig_signatures/dto.py | 9 ++ .../multisig_signatures/service/offchain.py | 61 ++++++++ backend/app/modules/registry.py | 2 +- backend/app/modules/user/allocations/core.py | 5 +- .../user/allocations/service/pending.py | 2 +- backend/tests/conftest.py | 148 +++++++++++++++++- backend/tests/helpers/constants.py | 6 + backend/tests/helpers/signature.py | 23 +++ .../modules_factory/test_modules_factory.py | 6 +- .../test_approving_signatures.py | 128 +++++++++++++++ .../allocations/test_saved_allocations.py | 5 +- 23 files changed, 537 insertions(+), 21 deletions(-) create mode 100644 backend/app/infrastructure/external_api/safe/__init__.py create mode 100644 backend/app/infrastructure/external_api/safe/message_details.py create mode 100644 backend/app/infrastructure/external_api/safe/user_details.py create mode 100644 backend/app/modules/facades/confirm_multisig.py create mode 100644 backend/tests/modules/multisig_signatures/test_approving_signatures.py diff --git a/backend/app/constants.py b/backend/app/constants.py index daaec3f8aa..453952aa83 100644 --- a/backend/app/constants.py +++ b/backend/app/constants.py @@ -13,3 +13,5 @@ BEACONCHAIN_API = "https://beaconcha.in/api" ETHERSCAN_API = "https://api.etherscan.io/api" BITQUERY_API = "https://graphql.bitquery.io" +SAFE_API_MAINNET = "https://safe-transaction-mainnet.safe.global/api/v1" +SAFE_API_SEPOLIA = "https://safe-transaction-sepolia.safe.global/api/v1" diff --git a/backend/app/infrastructure/database/multisig_signature.py b/backend/app/infrastructure/database/multisig_signature.py index ae01b64caf..da03418a9c 100644 --- a/backend/app/infrastructure/database/multisig_signature.py +++ b/backend/app/infrastructure/database/multisig_signature.py @@ -21,6 +21,10 @@ def get_last_pending_signature( return last_signature.first() +def get_all_pending_signatures() -> list[MultisigSignatures]: + return MultisigSignatures.query.filter_by(status=SigStatus.PENDING).all() + + def save_signature( user_address: str, op_type: SignatureOpType, diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index bcc8c62004..256293ccca 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -8,10 +8,10 @@ from app.exceptions import OctantException from app.extensions import socketio, epochs from app.infrastructure.exception_handler import UNEXPECTED_EXCEPTION, ExceptionHandler -from app.modules.dto import ProposalDonationDTO from app.modules.project_rewards.controller import ( get_allocation_threshold, ) +from app.modules.dto import ProposalDonationDTO from app.modules.project_rewards.controller import get_estimated_project_rewards from app.modules.user.allocations import controller diff --git a/backend/app/infrastructure/external_api/safe/__init__.py b/backend/app/infrastructure/external_api/safe/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/app/infrastructure/external_api/safe/message_details.py b/backend/app/infrastructure/external_api/safe/message_details.py new file mode 100644 index 0000000000..b8d67028bb --- /dev/null +++ b/backend/app/infrastructure/external_api/safe/message_details.py @@ -0,0 +1,27 @@ +import requests + +import app as app_module +from app.constants import SAFE_API_MAINNET, SAFE_API_SEPOLIA +from app.exceptions import ExternalApiException + + +def get_message_details(message_hash: str, is_mainnet: bool) -> dict: + api_url = _get_api_url(message_hash, is_mainnet) + + try: + response = requests.request("GET", api_url) + response.raise_for_status() + json_response = response.json() + except requests.exceptions.RequestException as e: + app_module.ExceptionHandler.print_stacktrace(e) + raise ExternalApiException(api_url, e, 500) + + return json_response + + +def _get_api_url( + message_hash: str, + is_mainnet: bool, +) -> str: + base_url = SAFE_API_MAINNET if is_mainnet else SAFE_API_SEPOLIA + return f"{base_url}/messages/{message_hash}" diff --git a/backend/app/infrastructure/external_api/safe/user_details.py b/backend/app/infrastructure/external_api/safe/user_details.py new file mode 100644 index 0000000000..b1eed18f64 --- /dev/null +++ b/backend/app/infrastructure/external_api/safe/user_details.py @@ -0,0 +1,24 @@ +import requests + +import app as app_module +from app.constants import SAFE_API_MAINNET, SAFE_API_SEPOLIA +from app.exceptions import ExternalApiException + + +def get_user_details(user_address: str, is_mainnet: bool) -> dict: + api_url = _get_api_url(user_address, is_mainnet) + + try: + response = requests.request("GET", api_url) + response.raise_for_status() + json_response = response.json() + except requests.exceptions.RequestException as e: + app_module.ExceptionHandler.print_stacktrace(e) + raise ExternalApiException(api_url, e, 500) + + return json_response + + +def _get_api_url(user_address: str, is_mainnet: bool) -> str: + base_url = SAFE_API_MAINNET if is_mainnet else SAFE_API_SEPOLIA + return f"{base_url}/safes/{user_address}" diff --git a/backend/app/infrastructure/routes/multisig_signatures.py b/backend/app/infrastructure/routes/multisig_signatures.py index 20b776a8b6..b9e388e0a4 100644 --- a/backend/app/infrastructure/routes/multisig_signatures.py +++ b/backend/app/infrastructure/routes/multisig_signatures.py @@ -4,6 +4,7 @@ from app.extensions import api from app.infrastructure import OctantResource from app.modules.dto import SignatureOpType +from app.modules.facades.confirm_multisig import confirm_multisig from app.modules.multisig_signatures.controller import ( get_last_pending_signature, save_pending_signature, @@ -72,3 +73,14 @@ def post(self, user_address: str, op_type: str): app.logger.debug("Added new multisig signature.") return {}, 201 + + +@ns.route("/pending/approve") +class MultisigApprovePending(OctantResource): + @ns.response(204, "Success") + @ns.doc(description="Approve pending multisig messages.") + def patch(self): + app.logger.debug("Approving and applying TOS & allocation signatures.") + confirm_multisig() + + return {}, 204 diff --git a/backend/app/modules/facades/confirm_multisig.py b/backend/app/modules/facades/confirm_multisig.py new file mode 100644 index 0000000000..c7d5abab49 --- /dev/null +++ b/backend/app/modules/facades/confirm_multisig.py @@ -0,0 +1,32 @@ +import json + +from app.modules.multisig_signatures.controller import ( + apply_pending_tos_signature, + apply_pending_allocation_signature, + approve_pending_signatures, +) +from app.modules.user.allocations.controller import allocate +from app.modules.user.tos.controller import post_user_terms_of_service_consent + + +def confirm_multisig(): + """ + This is a facade function that is used to confirm (i.e approve and apply) multisig approvals. + Uses multisig_signatures & user modules to confirm multisig approvals. + """ + approvals = approve_pending_signatures() + + for tos_signature in approvals.tos_signatures: + post_user_terms_of_service_consent( + tos_signature.user_address, tos_signature.hash, tos_signature.ip_address + ) + apply_pending_tos_signature(tos_signature.id) + + for allocation_signature in approvals.allocation_signatures: + message = json.loads(allocation_signature.message) + allocate( + allocation_signature.user_address, + message["payload"], + is_manually_edited=message["is_manually_edited"], + ) + apply_pending_allocation_signature(allocation_signature.id) diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index b50000176b..fa8e2559ba 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -44,10 +44,8 @@ class CurrentServices(Model): @staticmethod def _prepare_simulation_data( - chain_id: int, user_deposits: CalculatedUserDeposits + is_mainnet: bool, user_deposits: CalculatedUserDeposits ) -> CalculatedOctantRewards: - is_mainnet = compare_blockchain_types(chain_id, ChainTypes.MAINNET) - octant_rewards = CalculatedOctantRewards( staking_proceeds=aggregated.AggregatedStakingProceeds() if is_mainnet @@ -62,8 +60,11 @@ def create(chain_id: int) -> "CurrentServices": user_deposits = CalculatedUserDeposits( events_generator=DbAndGraphEventsGenerator() ) + + is_mainnet = compare_blockchain_types(chain_id, ChainTypes.MAINNET) + octant_rewards = CurrentServices._prepare_simulation_data( - chain_id, user_deposits + is_mainnet, user_deposits ) simulated_pending_snapshot_service = SimulatedPendingSnapshots( effective_deposits=user_deposits, octant_rewards=octant_rewards @@ -81,7 +82,7 @@ def create(chain_id: int) -> "CurrentServices": ) multisig_signatures = OffchainMultisigSignatures( - verifiers={SignatureOpType.TOS: tos_verifier} + verifiers={SignatureOpType.TOS: tos_verifier}, is_mainnet=is_mainnet ) return CurrentServices( user_allocations_service=user_allocations, diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index a535f97389..4a5b7ec669 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -35,6 +35,7 @@ from app.modules.user.rewards.service.calculated import CalculatedUserRewards from app.modules.withdrawals.service.pending import PendingWithdrawals from app.pydantic import Model +from app.shared.blockchain_types import compare_blockchain_types, ChainTypes class PendingOctantRewardsService(OctantRewards, Leverage, Protocol): @@ -74,7 +75,7 @@ class PendingServices(Model): multisig_signatures_service: MultisigSignatures @staticmethod - def create() -> "PendingServices": + def create(chain_id: int) -> "PendingServices": events_based_patron_mode = EventsBasedUserPatronMode() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) saved_user_budgets = SavedUserBudgets() @@ -97,8 +98,13 @@ def create() -> "PendingServices": ) withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) project_rewards = EstimatedProjectRewards(octant_rewards=octant_rewards) + + is_mainnet = compare_blockchain_types( + chain_id=chain_id, expected_chain=ChainTypes.MAINNET + ) multisig_signatures = OffchainMultisigSignatures( - verifiers={SignatureOpType.ALLOCATION: allocations_verifier} + verifiers={SignatureOpType.ALLOCATION: allocations_verifier}, + is_mainnet=is_mainnet, ) return PendingServices( diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index f56135e68a..1a25d5fcaa 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -176,6 +176,12 @@ def get_last_pending_signature( ) -> Signature: ... + def approve_pending_signatures(self, context: Context) -> list[Signature]: + ... + + def apply_staged_signatures(self, context: Context, signature_id: int): + ... + def save_pending_signature( self, context: Context, diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py index bda12db1ac..5459ea034a 100644 --- a/backend/app/modules/multisig_signatures/controller.py +++ b/backend/app/modules/multisig_signatures/controller.py @@ -1,7 +1,7 @@ from app.context.epoch_state import EpochState from app.context.manager import state_context, Context from app.modules.dto import SignatureOpType -from app.modules.multisig_signatures.dto import Signature +from app.modules.multisig_signatures.dto import Signature, ApprovedSignatureTypes from app.modules.registry import get_services @@ -25,6 +25,37 @@ def save_pending_signature( ) +def approve_pending_signatures() -> ApprovedSignatureTypes: + allocation_approvals = _approve(SignatureOpType.ALLOCATION) + tos_approvals = _approve(SignatureOpType.TOS) + + return ApprovedSignatureTypes( + allocation_signatures=allocation_approvals, tos_signatures=tos_approvals + ) + + +def apply_pending_tos_signature(signature_id: int): + _apply(SignatureOpType.TOS, signature_id) + + +def apply_pending_allocation_signature(signature_id: int): + _apply(SignatureOpType.ALLOCATION, signature_id) + + +def _apply(op_type: SignatureOpType, signature_id): + context = _get_context(op_type) + service = get_services(context.epoch_state).multisig_signatures_service + + service.apply_staged_signatures(context, signature_id) + + +def _approve(op_type: SignatureOpType) -> list[Signature]: + context = _get_context(op_type) + service = get_services(context.epoch_state).multisig_signatures_service + + return service.approve_pending_signatures(context) + + def _get_context(op_type: SignatureOpType) -> Context: if op_type == SignatureOpType.ALLOCATION: return state_context(EpochState.PENDING) diff --git a/backend/app/modules/multisig_signatures/dto.py b/backend/app/modules/multisig_signatures/dto.py index 2887afcc45..2878569dd7 100644 --- a/backend/app/modules/multisig_signatures/dto.py +++ b/backend/app/modules/multisig_signatures/dto.py @@ -5,5 +5,14 @@ @dataclass(frozen=True) class Signature(JSONWizard): + id: int message: str hash: str + user_address: str + ip_address: str + + +@dataclass(frozen=True) +class ApprovedSignatureTypes: + tos_signatures: list[Signature] + allocation_signatures: list[Signature] diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index 9f45691bad..ce41f130fb 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -4,6 +4,10 @@ from app.exceptions import InvalidMultisigSignatureRequest from app.extensions import db from app.infrastructure import database +from app.infrastructure.database.models import MultisigSignatures +from app.infrastructure.database.multisig_signature import SigStatus +from app.infrastructure.external_api.safe.message_details import get_message_details +from app.infrastructure.external_api.safe.user_details import get_user_details from app.modules.common.signature import ( encode_for_signing, EncodingStandardFor, @@ -16,8 +20,13 @@ class OffchainMultisigSignatures(Model): + is_mainnet: bool = False verifiers: dict[SignatureOpType, Verifier] + staged_signatures: list[ + MultisigSignatures + ] = [] # TODO make it invulnerable for data race & race conditions + def get_last_pending_signature( self, _: Context, user_address: str, op_type: SignatureOpType ) -> Signature | None: @@ -29,10 +38,62 @@ def get_last_pending_signature( return None return Signature( + id=signature_db.id, message=signature_db.message, hash=signature_db.hash, + user_address=signature_db.address, + ip_address=signature_db.user_ip, ) + def approve_pending_signatures(self, _: Context) -> list[Signature]: + pending_signatures = database.multisig_signature.get_all_pending_signatures() + approved_signatures = [] + + staged_signatures_ids = tuple(map(lambda x: x.id, self.staged_signatures)) + for pending_signature in pending_signatures: + if pending_signature.id in staged_signatures_ids: + approved_signatures.append( + Signature( + pending_signature.id, + pending_signature.message, + pending_signature.hash, + pending_signature.address, + pending_signature.user_ip, + ) + ) + continue + + confirmations = get_message_details( + pending_signature.hash, is_mainnet=self.is_mainnet + )["confirmations"] + threshold = int( + get_user_details(pending_signature.address, is_mainnet=self.is_mainnet)[ + "threshold" + ] + ) + + if len(confirmations) >= threshold: + self.staged_signatures.append(pending_signature) + approved_signatures.append( + Signature( + pending_signature.id, + pending_signature.message, + pending_signature.hash, + pending_signature.address, + pending_signature.user_ip, + ) + ) + + return approved_signatures + + def apply_staged_signatures(self, _: Context, signature_id: int): + for idx, pending_signature in enumerate(self.staged_signatures): + if pending_signature.id == signature_id: + pending_signature.status = SigStatus.APPROVED + db.session.commit() + self.staged_signatures.pop(idx) + return + def save_pending_signature( self, context: Context, diff --git a/backend/app/modules/registry.py b/backend/app/modules/registry.py index ef81a10f47..7deb3cf0f8 100644 --- a/backend/app/modules/registry.py +++ b/backend/app/modules/registry.py @@ -21,6 +21,6 @@ def register_services(app): SERVICE_REGISTRY[EpochState.FUTURE] = FutureServices.create() SERVICE_REGISTRY[EpochState.CURRENT] = CurrentServices.create(chain_id) SERVICE_REGISTRY[EpochState.PRE_PENDING] = PrePendingServices.create(chain_id) - SERVICE_REGISTRY[EpochState.PENDING] = PendingServices.create() + SERVICE_REGISTRY[EpochState.PENDING] = PendingServices.create(chain_id) SERVICE_REGISTRY[EpochState.FINALIZING] = FinalizingServices.create() SERVICE_REGISTRY[EpochState.FINALIZED] = FinalizedServices.create() diff --git a/backend/app/modules/user/allocations/core.py b/backend/app/modules/user/allocations/core.py index 367bb581b7..2aa4b2e419 100644 --- a/backend/app/modules/user/allocations/core.py +++ b/backend/app/modules/user/allocations/core.py @@ -1,16 +1,15 @@ from typing import List, Optional from app import exceptions -from app.context.manager import Context from app.context.epoch_state import EpochState +from app.context.manager import Context from app.engine.projects import ProjectSettings from app.infrastructure.database.models import AllocationRequest +from app.legacy.crypto.eip712 import build_allocations_eip712_structure, recover_address from app.modules.common.leverage import calculate_leverage from app.modules.common.project_rewards import get_projects_rewards from app.modules.dto import AllocationDTO, UserAllocationRequestPayload, AllocationItem -from app.legacy.crypto.eip712 import build_allocations_eip712_structure, recover_address - def next_allocation_nonce(prev_allocation_request: Optional[AllocationRequest]) -> int: return 0 if prev_allocation_request is None else prev_allocation_request.nonce + 1 diff --git a/backend/app/modules/user/allocations/service/pending.py b/backend/app/modules/user/allocations/service/pending.py index 1b3df41015..acd469a330 100644 --- a/backend/app/modules/user/allocations/service/pending.py +++ b/backend/app/modules/user/allocations/service/pending.py @@ -59,7 +59,7 @@ def _verify_signature(self, _: Context, **kwargs): eip712_encoded = build_allocations_eip712_structure(kwargs["payload"].payload) encoded_msg = encode_for_signing(EncodingStandardFor.DATA, eip712_encoded) if not verify_signed_message(user_address, encoded_msg, signature): - raise InvalidSignature() + raise InvalidSignature(user_address, signature) class PendingUserAllocations(SavedUserAllocations, Model): diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 2c118d1a71..f7089ed1b8 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -20,10 +20,11 @@ from app.infrastructure.contracts.erc20 import ERC20 from app.infrastructure.contracts.proposals import Proposals from app.infrastructure.contracts.vault import Vault +from app.infrastructure.database.multisig_signature import SigStatus from app.legacy.crypto.account import Account as CryptoAccount from app.legacy.crypto.eip712 import build_allocations_eip712_data, sign from app.modules.common.verifier import Verifier -from app.modules.dto import AccountFundsDTO, AllocationItem +from app.modules.dto import AccountFundsDTO, AllocationItem, SignatureOpType from app.settings import DevConfig, TestConfig from tests.helpers import make_user_allocation from tests.helpers.constants import ( @@ -51,12 +52,16 @@ MOCKED_EPOCH_NO_AFTER_OVERHAUL, MATCHED_REWARDS_AFTER_OVERHAUL, NO_PATRONS_REWARDS, + MULTISIG_APPROVALS_THRESHOLD, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, ) from tests.helpers.context import get_context from tests.helpers.gql_client import MockGQLClient from tests.helpers.mocked_epoch_details import EPOCH_EVENTS from tests.helpers.octant_rewards import octant_rewards from tests.helpers.pending_snapshot import create_pending_snapshot +from tests.helpers.signature import create_multisig_signature from tests.helpers.subgraph.events import create_deposit_event # Contracts mocks @@ -121,6 +126,61 @@ def mock_bitquery_api_get_blocks_rewards(*args, **kwargs): return example_resp_json["data"]["ethereum"]["blocks"][0]["reward"] +def mock_safe_api_message_details(*args, **kwargs): + example_resp_json = { + "created": "2023-10-27T07:34:09.184140Z", + "modified": "2023-10-28T20:54:46.207427Z", + "safe": "0xa40FcB633d0A6c0d27aA9367047635Ff656229B0", + "messageHash": "0x7f6dfab0a617fcb1c8f351b321a8844d98d9ee160e7532efc39ee06c02308ec6", + "message": "Welcome to Octant.\nPlease click to sign in and accept the Octant Terms of Service.\n\nSigning this message will not trigger a transaction.\n\nYour address\n0xa40FcB633d0A6c0d27aA9367047635Ff656229B0", + "proposedBy": "0x5754aC842D6eaF6a4E29101D46ac25D7C567311E", + "safeAppId": 111, + "confirmations": [ + { + "created": "2023-10-27T07:34:09.223358Z", + "modified": "2023-10-27T07:34:09.223358Z", + "owner": "0x5754aC842D6eaF6a4E29101D46ac25D7C567311E", + "signature": "0xa35a1d5689b5daf1003a06479952701bc3574d66fa89c4433c634a864910ddf337b63354a66b11bedb5b6e4f7c0bf1fe2d2797d05dd288fb56e8e5d636a5064c1c", + "signatureType": "EOA", + }, + { + "created": "2023-10-28T08:37:40.741190Z", + "modified": "2023-10-28T08:37:40.741190Z", + "owner": "0xa35E7b6524d312B7FABefd00F9A8e4524581Dc85", + "signature": "0xa966dd0a074f4891a286b115adc191469bc19fe07105468ca582bd82c952165529a452e93ddbb062859bd2fd4c6efd68808a5e3444053ddd5e46244bb300c6fd1f", + "signatureType": "ETH_SIGN", + }, + { + "created": "2023-10-28T20:54:46.207427Z", + "modified": "2023-10-28T20:54:46.207427Z", + "owner": "0x4280Ce44aFAb1e5E940574F135802E12ad2A5eF0", + "signature": "0x1d2ca05dbfda9d996aacf47b78f5ee6f477171c3895fe0bd496f68b33f68059463539264dffb513c4bf7857aaa646c170f3a47189a61ae9734d3724503c560f220", + "signatureType": "ETH_SIGN", + }, + ], + "preparedSignature": "0x1c2ca05dbfda9d996aacf47b78f5ee6f477171c3895fe0bd496f68b33f68059463539264dffb513c4bf7857aaa646c170f3a47189a61ae9734d3724503c560f220a37a1d5689b5daf1003a06479952701bc3574d66fa89c4433c634a864910ddf337b63354a66b11bedb5b6e4f7c0bf1fe2d2797d05dd288fb56e8e5d636a5064c1ca966dd0a074f4891a286b115adc191469bc19fe07105468ca582bd82c952165529a452e93ddbb062859bd2fd4c6efd68808a5e3444053ddd5e46244bb300c6fd1f", + } + return example_resp_json + + +def mock_safe_api_user_details(*args, **kwargs): + example_resp_json = { + "address": "0x89d2EcE5ca5cee0672d8BaD68cC7638D30Dc005e", + "nonce": 0, + "threshold": MULTISIG_APPROVALS_THRESHOLD, + "owners": [ + "0x94F9B0F7B5d00e33f5DEaBBf780e2E6D870E9714", + "0x6c1865c85C1ebd545FD891Aa38dE993c485aE90a", + ], + "masterCopy": "0xfb1bffC9d739B8D520DaF37dF666da4C687191EA", + "modules": [], + "fallbackHandler": "0x017062a1dE2FE6b99BE3d9d37841FeD19F573804", + "guard": "0x0000000000000000000000000000000000000000", + "version": "1.3.0+L2", + } + return example_resp_json + + def pytest_addoption(parser): parser.addoption( "--runapi", @@ -493,6 +553,12 @@ def patch_eth_get_balance(monkeypatch): @pytest.fixture(scope="function") def patch_has_pending_epoch_snapshot(monkeypatch): + ( + monkeypatch.setattr( + "app.context.epoch_state._has_pending_epoch_snapshot", + MOCK_HAS_PENDING_SNAPSHOT, + ) + ) ( monkeypatch.setattr( "app.context.epoch_state._has_pending_epoch_snapshot", @@ -546,6 +612,22 @@ def patch_bitquery_get_blocks_rewards(monkeypatch): ) +@pytest.fixture(scope="function") +def patch_safe_api_message_details(monkeypatch): + monkeypatch.setattr( + "app.modules.multisig_signatures.service.offchain.get_message_details", + mock_safe_api_message_details, + ) + + +@pytest.fixture(scope="function") +def patch_safe_api_user_details(monkeypatch): + monkeypatch.setattr( + "app.modules.multisig_signatures.service.offchain.get_user_details", + mock_safe_api_user_details, + ) + + @pytest.fixture(scope="function") def mock_users_db(app, user_accounts): alice = database.user.add_user(user_accounts[0].address) @@ -652,6 +734,70 @@ def mock_allocations_db(app, mock_users_db, proposal_accounts): db.session.commit() +@pytest.fixture(scope="function") +def mock_pending_multisig_signatures(alice): + create_multisig_signature( + alice.address, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, + SignatureOpType.TOS, + "0.0.0.0", + SigStatus.PENDING, + ) + create_multisig_signature( + alice.address, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, + SignatureOpType.ALLOCATION, + "0.0.0.0", + SigStatus.PENDING, + ) + + +@pytest.fixture(scope="function") +def mock_approved_multisig_signatures(alice): + create_multisig_signature( + alice.address, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, + SignatureOpType.ALLOCATION, + "0.0.0.0", + SigStatus.APPROVED, + ) + create_multisig_signature( + alice.address, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, + SignatureOpType.TOS, + "0.0.0.0", + SigStatus.APPROVED, + ) + + +@pytest.fixture(scope="function") +def mock_pending_allocation_signature(alice): + create_multisig_signature( + alice.address, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, + SignatureOpType.ALLOCATION, + "0.0.0.0", + SigStatus.PENDING, + ) + + +@pytest.fixture(scope="function") +def mock_pending_tos_signature(alice): + create_multisig_signature( + alice.address, + MULTISIG_MOCKED_MESSAGE, + MULTISIG_MOCKED_HASH, + SignatureOpType.TOS, + "0.0.0.0", + SigStatus.PENDING, + ) + + @pytest.fixture(scope="function") def mock_octant_rewards(): octant_rewards_service_mock = Mock() diff --git a/backend/tests/helpers/constants.py b/backend/tests/helpers/constants.py index 5cdf5b433e..6ecb2b80bf 100644 --- a/backend/tests/helpers/constants.py +++ b/backend/tests/helpers/constants.py @@ -38,3 +38,9 @@ BOB_ADDRESS = USER2_ADDRESS CAROL = "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6" CAROL_ADDRESS = USER3_ADDRESS + +MULTISIG_APPROVALS_THRESHOLD = 2 +MULTISIG_MOCKED_MESSAGE = "Hello World!" +MULTISIG_MOCKED_HASH = ( + "0x8227aee7a90b3b36779bc7d9f1f0b4c2e6c5262838dc68ffcf1f66ee4744e059" +) diff --git a/backend/tests/helpers/signature.py b/backend/tests/helpers/signature.py index 5241e25640..3bb2d1d4e6 100644 --- a/backend/tests/helpers/signature.py +++ b/backend/tests/helpers/signature.py @@ -1,5 +1,10 @@ from eth_account.messages import encode_defunct +from app.extensions import db +from app.infrastructure import database +from app.infrastructure.database.models import MultisigSignatures +from app.infrastructure.database.multisig_signature import SigStatus +from app.modules.dto import SignatureOpType from app.modules.user.tos.core import build_consent_message @@ -12,3 +17,21 @@ def build_user_signature(user, user_address=None): signature_bytes = user.sign_message(message).signature return signature_bytes + + +def create_multisig_signature( + address: str, + msg: str, + msg_hash: str, + op_type: SignatureOpType, + user_ip: str, + status: SigStatus = SigStatus.PENDING, +): + database.multisig_signature.save_signature( + address, op_type, msg, msg_hash, user_ip, status + ) + db.session.commit() + + +def get_signature_by_id(id: int) -> MultisigSignatures: + return MultisigSignatures.query.get(id) diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index 3dee43657c..645e4e5ecd 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -74,7 +74,7 @@ def test_current_services_factory(): patron_donations=patron_donations, ) multisig_signatures = OffchainMultisigSignatures( - verifiers={SignatureOpType.TOS: tos_verifier} + verifiers={SignatureOpType.TOS: tos_verifier}, is_mainnet=True ) assert result.user_deposits_service == user_deposits @@ -120,7 +120,7 @@ def test_pre_pending_services_factory_when_not_mainnet(): def test_pending_services_factory(): - result = PendingServices.create() + result = PendingServices.create(ChainTypes.MAINNET) events_based_patron_mode = EventsBasedUserPatronMode() octant_rewards = PendingOctantRewards(patrons_mode=events_based_patron_mode) @@ -145,7 +145,7 @@ def test_pending_services_factory(): withdrawals_service = PendingWithdrawals(user_rewards=user_rewards) project_rewards = EstimatedProjectRewards(octant_rewards=octant_rewards) multisig_signatures = OffchainMultisigSignatures( - verifiers={SignatureOpType.ALLOCATION: allocations_verifier} + verifiers={SignatureOpType.ALLOCATION: allocations_verifier}, is_mainnet=True ) assert result.user_deposits_service == SavedUserDeposits() diff --git a/backend/tests/modules/multisig_signatures/test_approving_signatures.py b/backend/tests/modules/multisig_signatures/test_approving_signatures.py new file mode 100644 index 0000000000..1df6798b5f --- /dev/null +++ b/backend/tests/modules/multisig_signatures/test_approving_signatures.py @@ -0,0 +1,128 @@ +import pytest + +from app.extensions import db +from app.infrastructure import database +from app.infrastructure.database.multisig_signature import SigStatus +from app.modules.dto import SignatureOpType +from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures +from tests.helpers.constants import MULTISIG_MOCKED_HASH, MULTISIG_MOCKED_MESSAGE +from tests.helpers.signature import get_signature_by_id + + +@pytest.fixture(autouse=True) +def before(app): + pass + + +def test_approve_all_pending_signatures_when_exist( + context, + alice, + mock_pending_multisig_signatures, + patch_safe_api_message_details, + patch_safe_api_user_details, +): + service = OffchainMultisigSignatures(verifiers={}) + + approved_messages = service.approve_pending_signatures(context) + + assert len(approved_messages) == 2 + for approved_message in approved_messages: + assert approved_message.message == MULTISIG_MOCKED_MESSAGE + assert approved_message.hash == MULTISIG_MOCKED_HASH + + db.session.commit() + + +def test_approve_all_pending_signatures_when_only_approved_exist( + context, mock_approved_multisig_signatures +): + service = OffchainMultisigSignatures(verifiers={}) + + approved_messages = service.approve_pending_signatures(context) + + assert len(approved_messages) == 0 + + +def test_approve_all_pending_signatures_when_approved_and_pending_exist( + context, + mock_approved_multisig_signatures, + mock_pending_multisig_signatures, + patch_safe_api_message_details, + patch_safe_api_user_details, +): + service = OffchainMultisigSignatures(verifiers={}) + + approved_signatures = service.approve_pending_signatures(context) + + assert len(approved_signatures) == 2 + for approved_signature in approved_signatures: + assert approved_signature.message == MULTISIG_MOCKED_MESSAGE + assert approved_signature.hash == MULTISIG_MOCKED_HASH + + +def test_approve_pending_allocation_signature_when_already_staged( + context, + alice, + mock_pending_allocation_signature, + mock_pending_tos_signature, + patch_safe_api_user_details, + patch_safe_api_message_details, +): + service = OffchainMultisigSignatures(verifiers={}) + pending_signature = database.multisig_signature.get_last_pending_signature( + alice.address, SignatureOpType.ALLOCATION + ) + + service.staged_signatures.append(pending_signature) + approved_signatures = service.approve_pending_signatures(context) + + assert len(approved_signatures) == 2 + assert len(service.staged_signatures) == 2 + + for approved_signature in approved_signatures: + assert approved_signature.message == MULTISIG_MOCKED_MESSAGE + assert approved_signature.hash == MULTISIG_MOCKED_HASH + + +def test_apply_pending_tos_signature( + context, + alice, + mock_pending_tos_signature, + patch_safe_api_user_details, + patch_safe_api_message_details, +): + service = OffchainMultisigSignatures(verifiers={}) + + pending_signature = database.multisig_signature.get_last_pending_signature( + alice.address, SignatureOpType.TOS + ) + + service.approve_pending_signatures(context) + service.apply_staged_signatures(context, pending_signature.id) + + updated_signature = get_signature_by_id(pending_signature.id) + + assert updated_signature.status == SigStatus.APPROVED + assert len(service.staged_signatures) == 0 + + +def test_apply_pending_allocation_signature( + context, + alice, + mock_pending_allocation_signature, + patch_safe_api_message_details, + patch_safe_api_user_details, +): + service = OffchainMultisigSignatures(verifiers={}) + + pending_signature = database.multisig_signature.get_last_pending_signature( + alice.address, SignatureOpType.ALLOCATION + ) + + service.approve_pending_signatures(context) + service.apply_staged_signatures(context, pending_signature.id) + + updated_signature = get_signature_by_id(pending_signature.id) + + assert updated_signature.status == SigStatus.APPROVED + assert len(service.staged_signatures) == 0 diff --git a/backend/tests/modules/user/allocations/test_saved_allocations.py b/backend/tests/modules/user/allocations/test_saved_allocations.py index fc6402b5fd..b8e3f77dd9 100644 --- a/backend/tests/modules/user/allocations/test_saved_allocations.py +++ b/backend/tests/modules/user/allocations/test_saved_allocations.py @@ -10,11 +10,10 @@ UserAllocationPayload, AccountFundsDTO, ) -from app.modules.user.allocations.service.saved import SavedUserAllocations from app.modules.history.dto import AllocationItem as HistoryAllocationItem - -from tests.helpers.context import get_context +from app.modules.user.allocations.service.saved import SavedUserAllocations from tests.helpers import make_user_allocation +from tests.helpers.context import get_context @pytest.fixture(autouse=True) From 6d60b3d41727e9be08555c12fd4354ad5ee0906a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Kujawski?= Date: Wed, 3 Apr 2024 11:44:30 +0200 Subject: [PATCH 103/107] OCT-1518: Change signature length in a db (#120) --- backend/app/infrastructure/database/models.py | 2 +- ...4c_change_signature_length_to_unlimited.py | 30 +++++++++++++++++++ .../allocations/test_pending_allocations.py | 21 +++++++++++++ 3 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/versions/71fa24bd9d4c_change_signature_length_to_unlimited.py diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index d57c097e7c..7b136f6bf2 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -79,7 +79,7 @@ class AllocationRequest(BaseModel): user = relationship("User", backref=db.backref("allocations_requests", lazy=True)) nonce = Column(db.Integer, nullable=False) epoch = Column(db.Integer, nullable=False) - signature = Column(db.String(132), nullable=False) + signature = Column(db.String, nullable=False) is_manually_edited = Column(db.Boolean, nullable=True) __table_args__ = ( diff --git a/backend/migrations/versions/71fa24bd9d4c_change_signature_length_to_unlimited.py b/backend/migrations/versions/71fa24bd9d4c_change_signature_length_to_unlimited.py new file mode 100644 index 0000000000..5d01cdf435 --- /dev/null +++ b/backend/migrations/versions/71fa24bd9d4c_change_signature_length_to_unlimited.py @@ -0,0 +1,30 @@ +"""Change signature length to unlimited + +Revision ID: 71fa24bd9d4c +Revises: 0dbe7ab3ce9d +Create Date: 2024-04-03 09:47:22.262425 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "71fa24bd9d4c" +down_revision = "0dbe7ab3ce9d" +branch_labels = None +depends_on = None + + +def upgrade(): + with op.batch_alter_table("allocations_requests", schema=None) as batch_op: + batch_op.alter_column( + "signature", existing_type=sa.String(132), type_=sa.String(), nullable=False + ) + + +def downgrade(): + with op.batch_alter_table("allocations_requests", schema=None) as batch_op: + batch_op.alter_column( + "signature", existing_type=sa.String(), type_=sa.String(132), nullable=False + ) diff --git a/backend/tests/modules/user/allocations/test_pending_allocations.py b/backend/tests/modules/user/allocations/test_pending_allocations.py index df06152f93..1666b9b2cf 100644 --- a/backend/tests/modules/user/allocations/test_pending_allocations.py +++ b/backend/tests/modules/user/allocations/test_pending_allocations.py @@ -16,6 +16,7 @@ MOCKED_PENDING_EPOCH_NO, MOCK_PROPOSALS, MOCK_GET_USER_BUDGET, + MOCK_IS_CONTRACT, ) from tests.helpers import create_epoch_event from tests.helpers import make_user_allocation @@ -129,6 +130,26 @@ def test_user_allocates_for_the_first_time(tos_users, proposal_accounts): check_allocation_threshold(payload) +def test_multisig_allocates_for_the_first_time( + tos_users, proposal_accounts, patch_eip1271_is_valid_signature +): + # Test data + MOCK_IS_CONTRACT.return_value = True + payload = create_payload(proposal_accounts[0:2], None) + signature = "0x89b0da9bcf620cd6005e88f58c69edff5251b80f116e25e88c65188bf116d35f5cdf6d3782885c8df66878a8b5ec8739fe1174c72b06fb277534e7d7088f9a6e1b2810662a03962f315a8ad0f448a468ab5ce0c73c31b71e499b4b735f5c04b76542cb89802f68c19918f306e29563b9f736b02559fbaccc55ad8bd2134a0e69811c" + + # Call allocate method + controller.allocate( + tos_users[0].address, {"payload": payload, "signature": signature} + ) + + # Check if allocations were created + check_allocations(tos_users[0].address, payload, 2) + + # Check if threshold is properly calculated + check_allocation_threshold(payload) + + def test_multiple_users_allocate_for_the_first_time(tos_users, proposal_accounts): # Test data payload1 = create_payload(proposal_accounts[0:2], None) From 0650058afad29177f72d4905a6a48419d53558e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Wed, 3 Apr 2024 12:00:32 +0200 Subject: [PATCH 104/107] [TEST] CY - projectsArchive - additional checks for skeletons (#119) --- client/cypress/e2e/projectsArchive.cy.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/client/cypress/e2e/projectsArchive.cy.ts b/client/cypress/e2e/projectsArchive.cy.ts index d80fe1f16b..02121a9cdf 100644 --- a/client/cypress/e2e/projectsArchive.cy.ts +++ b/client/cypress/e2e/projectsArchive.cy.ts @@ -41,6 +41,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => cy.get('[data-test=MainLayout__body]').then(el => { const mainLayoutPaddingTop = parseInt(el.css('paddingTop'), 10); + cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); cy.get('[data-test=ProjectsView__ProjectsList]') .should('be.visible') .children() @@ -53,6 +54,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => // list test cy.get('[data-test=ProjectsView__ProjectsList--archive]').first().should('be.visible'); + cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); cy.get('[data-test=ProjectsView__ProjectsList--archive]') .first() .children() @@ -80,6 +82,7 @@ Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => .should('have.length', 1); } + cy.get('[data-test^=ProjectItemSkeleton').should('not.exist'); cy.get(`[data-test=ProjectsView__ProjectsListItem--archive--${i}]`) .first() .invoke('data', 'address') From b3275e6d94ccafabb15f7e6420ca98c689a8b2b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Kluczek?= Date: Wed, 3 Apr 2024 13:56:22 +0200 Subject: [PATCH 105/107] CAQD-349: Fix incorrect branch checkout @ PR deployments --- .github/workflows/ci-run.yml | 1 + .github/workflows/deploy-master.yml | 1 + .github/workflows/deploy-pr.yml | 2 +- .github/workflows/deploy-prod.yml | 1 + .github/workflows/deploy-rc.yml | 1 + .github/workflows/deploy-uat.yml | 1 + .github/workflows/tpl-images.yml | 5 +++++ 7 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-run.yml b/.github/workflows/ci-run.yml index c428a6c065..ce41dc0ead 100644 --- a/.github/workflows/ci-run.yml +++ b/.github/workflows/ci-run.yml @@ -249,6 +249,7 @@ jobs: secrets: inherit with: image-tag: ${{ github.sha }} + git-ref: ${{ github.ref }} # +------------------------- # | Tests: NodeJS diff --git a/.github/workflows/deploy-master.yml b/.github/workflows/deploy-master.yml index 3dee356789..b86928c320 100644 --- a/.github/workflows/deploy-master.yml +++ b/.github/workflows/deploy-master.yml @@ -15,6 +15,7 @@ jobs: secrets: inherit with: image-tag: ${{ github.sha }} + git-ref: ${{ github.ref }} run: name: Run uses: ./.github/workflows/tpl-start-env.yml diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index fd0d45862c..ad5d5cabeb 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -13,7 +13,7 @@ jobs: secrets: inherit with: image-tag: ${{ needs.run.outputs.sha }} - + git-ref: ${{ needs.run.outputs.ref }} deploy: name: Deploy needs: diff --git a/.github/workflows/deploy-prod.yml b/.github/workflows/deploy-prod.yml index 558bf7786d..83db5ff185 100644 --- a/.github/workflows/deploy-prod.yml +++ b/.github/workflows/deploy-prod.yml @@ -16,6 +16,7 @@ jobs: secrets: inherit with: image-tag: ${{ github.sha }} + git-ref: ${{ github.ref }} output: name: Output Variables needs: diff --git a/.github/workflows/deploy-rc.yml b/.github/workflows/deploy-rc.yml index 78cb6ef7bb..ad9bfd258a 100644 --- a/.github/workflows/deploy-rc.yml +++ b/.github/workflows/deploy-rc.yml @@ -19,6 +19,7 @@ jobs: secrets: inherit with: image-tag: ${{ github.sha }} + git-ref: ${{ github.ref }} deploy: name: Deploy needs: diff --git a/.github/workflows/deploy-uat.yml b/.github/workflows/deploy-uat.yml index a125af0bb5..a7033ec632 100644 --- a/.github/workflows/deploy-uat.yml +++ b/.github/workflows/deploy-uat.yml @@ -15,6 +15,7 @@ jobs: secrets: inherit with: image-tag: ${{ github.sha }} + git-ref: ${{ github.ref }} run: name: Run uses: ./.github/workflows/tpl-start-env.yml diff --git a/.github/workflows/tpl-images.yml b/.github/workflows/tpl-images.yml index a395cbb510..c9e29953fd 100644 --- a/.github/workflows/tpl-images.yml +++ b/.github/workflows/tpl-images.yml @@ -6,6 +6,9 @@ on: image-tag: required: true type: string + git-ref: + required: true + type: string concurrency: group: "${{ github.ref }}-images" cancel-in-progress: true @@ -31,6 +34,8 @@ jobs: # account # see: https://github.com/actions/checkout/issues/211 path: __local + ref: ${{ inputs.git-ref }} + - name: Login to Docker registry uses: docker/login-action@v3 with: From 3cfe34a82fc077b48942795985a8de9bc32e5a22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Wed, 3 Apr 2024 16:04:16 +0200 Subject: [PATCH 106/107] OCT-1519 Prevent client from calling for threshold outside AW as it throws 400 (#122) --- client/src/hooks/queries/useProjectRewardsThreshold.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/hooks/queries/useProjectRewardsThreshold.ts b/client/src/hooks/queries/useProjectRewardsThreshold.ts index 078ecf13dc..c950c12d17 100644 --- a/client/src/hooks/queries/useProjectRewardsThreshold.ts +++ b/client/src/hooks/queries/useProjectRewardsThreshold.ts @@ -36,8 +36,9 @@ export default function useProjectRewardsThreshold( return useQuery({ enabled: - isDecisionWindowOpen !== undefined && - ((epoch !== undefined && epoch > 0) || (!!currentEpoch && currentEpoch > 1)), + !!currentEpoch && + currentEpoch > 1 && + (isDecisionWindowOpen === true || (epoch !== undefined && epoch > 0)), queryFn: () => apiGetProjectThreshold(epoch ?? (isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch!)), queryKey: QUERY_KEYS.projectRewardsThreshold( From c5727249f50da85ba3a5ac835c27fe1501bc6bcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Garbaci=C5=84ski?= <57113816+kgarbacinski@users.noreply.github.com> Date: Wed, 3 Apr 2024 17:11:50 +0200 Subject: [PATCH 107/107] FIX-OCT-1521: Dont apply allocation multisig messages outside AW --- .../app/modules/multisig_signatures/controller.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/backend/app/modules/multisig_signatures/controller.py b/backend/app/modules/multisig_signatures/controller.py index 5459ea034a..9f5b9abc9e 100644 --- a/backend/app/modules/multisig_signatures/controller.py +++ b/backend/app/modules/multisig_signatures/controller.py @@ -3,6 +3,7 @@ from app.modules.dto import SignatureOpType from app.modules.multisig_signatures.dto import Signature, ApprovedSignatureTypes from app.modules.registry import get_services +from app.exceptions import InvalidEpoch def get_last_pending_signature( @@ -43,14 +44,22 @@ def apply_pending_allocation_signature(signature_id: int): def _apply(op_type: SignatureOpType, signature_id): - context = _get_context(op_type) + try: + context = _get_context(op_type) + except InvalidEpoch: + return None + service = get_services(context.epoch_state).multisig_signatures_service service.apply_staged_signatures(context, signature_id) def _approve(op_type: SignatureOpType) -> list[Signature]: - context = _get_context(op_type) + try: + context = _get_context(op_type) + except InvalidEpoch: + return [] + service = get_services(context.epoch_state).multisig_signatures_service return service.approve_pending_signatures(context)